JS 的线性变换和矩阵乘法失败

Linear transformation and matrix multiplication fails with JS

我正在学习线性代数,并尝试编写一个带有基本线性变换(旋转、缩放、平移)的小程序。

这是完整的示例:

https://codesandbox.io/embed/determined-diffie-t2iy5?fontsize=14&hidenavigation=1&theme=dark

我编写了为每个变换生成每个矩阵的函数和计算它们的函数(矩阵乘以点,矩阵相乘) .

export const multiplyMatrixWithPoint = (matrix, point) => {
  return point.map((dimension, index) => {
    let result = 0;

    for (let i = 0; i < 4; i++) {
      const matrixIndex = index * 4 + i;
      result += dimension * matrix[matrixIndex];
    }

    return +result.toFixed(2);
  })
};

// Just creating 2D array to make it easy to calculate the matrix
export const matrixToPoints = matrix => {
  const result = [];

  for (let i = 0; i < 4; i++) {
    const onePoint = [];

    for (let j = 0; j < 4; j++) {
      onePoint.push(matrix[4 * i + j]);
    }
    
    result.push(onePoint);
  }

  return result;
};

// Just making 2D array to 1D
export const pointsToMatrix = points => points.reduce((acc, points) => [...acc, ...points], []);

// Transpose function to make the matrix multiplication correct
export const transposeCSSMatrixToTransform = matrix => matrix[0].map((column, index) => matrix.map(row => row[index]));

export const multiplyMatrices = (matrixA, matrixB) => {
  const separatePoints = transposeCSSMatrixToTransform(matrixToPoints(matrixB));

  return pointsToMatrix(transposeCSSMatrixToTransform(separatePoints.map(point => multiplyMatrixWithPoint(matrixA, point))));
};

export const rotationMatrixGenerator = (matrix, angle) => {
  const radians = degreeToRadian(angle);

  matrix[0] = Math.cos(radians);
  matrix[1] = -Math.sin(radians);
  matrix[4] = Math.sin(radians);
  matrix[5] = Math.cos(radians);

  return matrix; 
};

export const scaleMatrixGenerator = (matrix, { x, y }) => {
  matrix[0] = x;
  matrix[5] = y;

  return matrix;
};

export const translateMatrixGenerator = (matrix, { x, y }) => {
  matrix[12] = x;
  matrix[13] = y;

  return matrix;
};

为了显示变换,我使用了 CSS 的 matrix3d

export const matrixToCSSMatrix = matrix => `matrix3d(${matrix.join(',')})`;

下面是代码的执行部分

  ...
  const [matrix, setMatrix] = useState(DEFAULT_MATRIX);
  const [rotationAngle, setRotationAngle] = useState(0);
  const [scale, setScale] = useState(DEFAULT_XY);
  const [translate, setTranslate] = useState(DEFAULT_XY);

  const classes = useStyles();
  const { app } = classes;

  // ---------------------- Rotate --------------------- //
  const rotate = useCallback(e => {
    const { value: angle } = e.target;

    const rotationMatrix = rotationMatrixGenerator(matrix, +angle);
    const updatedMatrix = multiplyMatrices(matrix, rotationMatrix);

    setMatrix(updatedMatrix);
    setRotationAngle(angle);
  }, [matrix]);

  // ---------------------- Scale --------------------- //
  const changeScale = useCallback((e, dimension) => {
    const { value } = e.target;
    const updatedScale = {
      ...scale,
      [dimension]: +value
    };

    const scaleMatrix = scaleMatrixGenerator(matrix, updatedScale);
    const updatedMatrix = multiplyMatrices(matrix, scaleMatrix);

    setMatrix(scaleMatrix);
    setScale(updatedScale);
  }, [matrix, scale]);

  // ------------------ Translate --------------------- //
  const changeTranslation = useCallback((e, position) => {
    const { value } = e.target;
    const updatedTranslation = {
      ...translate,
      [position]: +value
    };

    const translateMatrix = translateMatrixGenerator(matrix, updatedTranslation);
    const updatedMatrix = multiplyMatrices(matrix, translateMatrix);

    setMatrix(translateMatrix);
    setTranslate(updatedTranslation);
  }, [matrix, translate]);
  ...

起初我以为是因为我没有转置矩阵,我试着转置它,它有一点帮助但没有修复所有错误。此外,我尝试收集所有转换并立即应用它们,但这是一个非常糟糕的解决方案,效果不佳。

我需要单独的旋转、缩放和过渡,但旋转和过渡效果不佳,因为它们使用相同的矩阵段。另外,我无法实现对Z轴的正常旋转。

在这里,我为 2D 平移、缩放和旋转计算 3D 变换矩阵(4 x 4 矩阵)。我还在样式转换中通过显式连接或通过预乘矩阵来组合它们。

const resultEl = document.getElementById('result')
const angleEl = document.getElementById('angle')
const xEl = document.getElementById('x')
const yEl = document.getElementById('y')
const sxEl = document.getElementById('sx')
const syEl = document.getElementById('sy')
const premulEl = document.getElementById('premul')
/** Counter clock-wise rotation */
function getXRotation(angle){
  const c = Math.cos(angle)
  const s = Math.sin(angle)
  return [
  +c, -s,  0,  0,
  +s, +c,  0,  0,
   0,  0,  1,  0,
   0,  0,  0,  1];
}

function getTranslation(x, y){
return [
 1, 0, 0, 0,
 0, 1, 0, 0,
 0, 0, 1, 0,
 x, y, 0, 1]
}
function getScale(sx, sy){
return [
 sx, 0, 0, 0,
  0,sy, 0, 0,
  0, 0, 1, 0,
  0, 0, 0, 1]
}

function mat4mul(B, A){
  const C = [];
  for(let i = 0; i < 4; ++i){
    for(let j = 0; j < 4; ++j){
      let c = 0;
      for(let k = 0; k < 4; ++k){
        c += A[4*i+k] * B[4*k+j]
      }
      C.push(c);
    }
  }
  return C
}

const update = (e) => {
  const T1 = getTranslation(xEl.value, yEl.value)
  const T2 = getXRotation(angleEl.value * Math.PI / 180)
  const T3 = getScale(sxEl.value, syEl.value)
  let T = '';
  if(premul.checked){
    T = `matrix3d(${
      mat4mul(mat4mul(T1,T2),T3)
    })`
    resultEl.style.backgroundColor = 'yellow';
  }else{
    T = `
    matrix3d(${T1})
    matrix3d(${T2})
    matrix3d(${T3})
    `
    resultEl.style.backgroundColor = 'gray'
  }
  resultEl.style.transform = T;
}

angleEl.addEventListener('input', update)
xEl.addEventListener('input', update)
yEl.addEventListener('input', update)
sxEl.addEventListener('input', update)
syEl.addEventListener('input', update)
premulEl.addEventListener('input', update)
div#result {
 background-color: gray;
 border: 2px solid black;
 color: black;
 width: 3cm; 
 height: 2cm;
}
<label>Rotation angle <input type=range id=angle min=0 max=360 value=0>
</label><br>
<label>Translation X <input type=range id=x min=0 max=360 value=0></label><br>
<label>Translation Y <input type=range id=y min=0 max=360 value=0></label><br>
<label>Scale X <input type=range id=sx min=0.5 max=2 value=1 step=0.01></label><br>
<label>Scale Y <input type=range id=sy min=0.5 max=2 value=1 step=0.01></label><br>
<label><input type=checkbox id=premul> Multiply matrices

<br>
<br>
<br>
<br>
<div id=result>Result</div>