12. WebGPU 矩阵数学

在最近的 3 篇文章中,介绍了如何平移、旋转和缩放顶点位置。平移、旋转和缩放都被认为是一种变换。这些变换中的每一个都需要对着色器进行修改,并且 3 个转换中的每一个都依赖于顺序。

在之前的示例中,先缩放,然后旋转,最后平移。如果以不同的顺序应用它们,会得到不同的结果。

例如,这里的缩放为 2, 1,旋转 30 度,平移为 (100, 0)。
12. WebGPU 矩阵数学_第1张图片

这里是 先平移(100,0) ,再 旋转30 度和 最后缩放 2, 1
12. WebGPU 矩阵数学_第2张图片

结果完全不同。更糟糕的是,如果需要第二个示例,必须编写一个不同的着色器,以这个新的顺序应用平移、旋转和缩放。

好吧,一些聪明人发现你可以用矩阵数学做同样的事情。对于 2D,可以使用 3x3 矩阵。一个 3x3 矩阵就像一个有 9 个方框的网格:

12. WebGPU 矩阵数学_第3张图片

为了进行数学运算,我们将矩阵各列与对应 位置分量 相乘并将结果相加。
12. WebGPU 矩阵数学_第4张图片

位置分量 只有 2 个值,x 和 y,但要进行矩阵数学运算,需要 3 个值,因此将使用 1 作为第三个值。

在这种情况下,结果将是

12. WebGPU 矩阵数学_第5张图片

您可能正在看着它并思考“这有什么意义?” 好吧,假设现在要平移。可以将tx 和 ty 设置为想要平移的数量。让我们构造一个这样的矩阵

12. WebGPU 矩阵数学_第6张图片

现在检查一下

12. WebGPU 矩阵数学_第7张图片

如果你记得基础代数,可以删除任何乘以零的地方。乘以 1 实际上什么都不做
所以简化一下看看发生了什么

12. WebGPU 矩阵数学_第8张图片

或更简洁

newX = x + tx;
newY = y + ty;

而 newZ 我们并不关心。

这看起来很像之前的 平移示例中的平移代码。

按同样的方法设置旋转。就像之前在旋转例子中指出的那样,只需要旋转角度的正弦和余弦,所以

s = Math.sin(angleToRotateInRadians);
c = Math.cos(angleToRotateInRadians);

可以构建一个这样的矩阵

12. WebGPU 矩阵数学_第9张图片

应用得到这个矩阵
12. WebGPU 矩阵数学_第10张图片
涂黑所有乘以 0 和 1 的方格,可以得到

12. WebGPU 矩阵数学_第11张图片

简化后得到

newX = x * c - y * s;
newY = x * s + y * c;

这正是之前在旋转示例中所拥有的。

最后是缩放。设置 2 个缩放因子为 sx 和 sy

构建一个这样的矩阵

12. WebGPU 矩阵数学_第12张图片

应用矩阵后得到这个

12. WebGPU 矩阵数学_第13张图片

这实际是

12. WebGPU 矩阵数学_第14张图片

最后的简化为

newX = x * sx;
newY = y * sy;

这与之前的缩放示例相同。

现在我敢肯定你可能还在想“那又怎样?重点是什么?” 这似乎只是做之前已经做过的同样的事情。

这就是神奇的地方。事实证明可以将矩阵相乘并一次应用所有变换。假设有一个函数, m3.multiply ,它接受两个矩阵,将它们相乘并返回结果。

const mat3 = {
  multiply: function(a, b) {
    const a00 = a[0 * 3 + 0];
    const a01 = a[0 * 3 + 1];
    const a02 = a[0 * 3 + 2];
    const a10 = a[1 * 3 + 0];
    const a11 = a[1 * 3 + 1];
    const a12 = a[1 * 3 + 2];
    const a20 = a[2 * 3 + 0];
    const a21 = a[2 * 3 + 1];
    const a22 = a[2 * 3 + 2];
    const b00 = b[0 * 3 + 0];
    const b01 = b[0 * 3 + 1];
    const b02 = b[0 * 3 + 2];
    const b10 = b[1 * 3 + 0];
    const b11 = b[1 * 3 + 1];
    const b12 = b[1 * 3 + 2];
    const b20 = b[2 * 3 + 0];
    const b21 = b[2 * 3 + 1];
    const b22 = b[2 * 3 + 2];
 
    return [
      b00 * a00 + b01 * a10 + b02 * a20,
      b00 * a01 + b01 * a11 + b02 * a21,
      b00 * a02 + b01 * a12 + b02 * a22,
      b10 * a00 + b11 * a10 + b12 * a20,
      b10 * a01 + b11 * a11 + b12 * a21,
      b10 * a02 + b11 * a12 + b12 * a22,
      b20 * a00 + b21 * a10 + b22 * a20,
      b20 * a01 + b21 * a11 + b22 * a21,
      b20 * a02 + b21 * a12 + b22 * a22,
    ];
  }
}

为了让事情更清楚,让我们创建函数来构建用于平移、旋转和缩放的矩阵。

const mat3 = {
  multiply(a, b) {
    ...
  },
  translation([tx, ty]) {
    return [
      1, 0, 0,
      0, 1, 0,
      tx, ty, 1,
    ];
  },
 
  rotation(angleInRadians) {
    const c = Math.cos(angleInRadians);
    const s = Math.sin(angleInRadians);
    return [
      c, s, 0,
      -s, c, 0,
      0, 0, 1,
    ];
  },
 
  scaling([sx, sy]) {
    return [
      sx, 0, 0,
      0, sy, 0,
      0, 0, 1,
    ];
  },
};

现在更改着色器以使用矩阵

struct Uniforms {
  color: vec4f,
  resolution: vec2f,
  matrix: mat3x3f,
};
 
...
 
@vertex fn vs(vert: Vertex) -> VSOutput {
  var vsOut: VSOutput;
 
  // Scale the position
  //let scaledPosition = vert.position * uni.scale;
 
  // Rotate the position
  //let rotatedPosition = vec2f(
  //  scaledPosition.x * uni.rotation.x - scaledPosition.y * uni.rotation.y,
  //  scaledPosition.x * uni.rotation.y + scaledPosition.y * uni.rotation.x
  //);
 
  // Add in the translation
  // let position = rotatedPosition + uni.translation;
  // Multiply by a matrix
  let position = (uni.matrix * vec3f(vert.position, 1)).xy;
 
  ...

正如在上面看到的,z 传入了 1。将位置乘以矩阵,然后只保留结果中的 x 和 y。

还要再次更新uniform 缓冲区大小和偏移量

  // color, resolution, translation, rotation, scale
  //const uniformBufferSize = (4 + 2 + 2 + 2 + 2) * 4;
  // color, resolution, padding, matrix
  const uniformBufferSize = (4 + 2 + 2 + 12) * 4;
  const uniformBuffer = device.createBuffer({
    label: 'uniforms',
    size: uniformBufferSize,
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
  });
 
  const uniformValues = new Float32Array(uniformBufferSize / 4);
 
  // offsets to the various uniform values in float32 indices
  const kColorOffset = 0;
  const kResolutionOffset = 4;
  const kMatrixOffset = 8;
 
  const colorValue = uniformValues.subarray(kColorOffset, kColorOffset + 4);
  const resolutionValue = uniformValues.subarray(kResolutionOffset, kResolutionOffset + 2);
 // const translationValue = uniformValues.subarray(kTranslationOffset, kTranslationOffset + 2);
 // const rotationValue = uniformValues.subarray(kRotationOffset, kRotationOffset + 2);
 // const scaleValue = uniformValues.subarray(kScaleOffset, kScaleOffset + 2);
  const matrixValue = uniformValues.subarray(kMatrixOffset, kMatrixOffset + 12);

最后需要在渲染时做一些矩阵运算

  function render() {
    ...
    const translationMatrix = mat3.translation(settings.translation);
    const rotationMatrix = mat3.rotation(settings.rotation);
    const scaleMatrix = mat3.scaling(settings.scale);
 
    let matrix = mat3.multiply(translationMatrix, rotationMatrix);
    matrix = mat3.multiply(matrix, scaleMatrix);
 
    // Set the uniform values in our JavaScript side Float32Array
    resolutionValue.set([canvas.width, canvas.height]);
    //translationValue.set(settings.translation);
    //rotationValue.set([
    //    Math.cos(settings.rotation),
    //    Math.sin(settings.rotation),
    //]);
    //scaleValue.set(settings.scale);
    matrixValue.set([
      ...matrix.slice(0, 3), 0,
      ...matrix.slice(3, 6), 0,
      ...matrix.slice(6, 9), 0,
    ]);

这是使用新的代码。滑块可以调整 平移、旋转和缩放。但是它们在着色器中的使用方式要简单得多。

12. WebGPU 矩阵数学_第15张图片

列转置

In the description of how a matrix works we talked about multiplying by columns. As one example we showed this matrix as an example of a translation matrix.

在矩阵工作原理的描述中,我们乘以列。作为一个示例,将平移矩阵作为示例。

12. WebGPU 矩阵数学_第16张图片

但是当实际在代码中构建矩阵时,我们这样做了

  translation([tx, ty]) {
    return [
      1, 0, 0,
      0, 1, 0,
      tx, ty, 1,
    ];
  },

tx, ty, 1 部分位于底行,而不是最后一列

  translation([tx, ty]) {
    return [
      1, 0, 0,   // <-- 1st column
      0, 1, 0,   // <-- 2nd column
      tx, ty, 1, // <-- 3rd column
    ];
  },

一些图形专家称这样的排列方式为列。可悲的是,这只是必须习惯的事情。网络上的数学书籍和数学文章将显示如上图所示的矩阵,其中 tx, ty, 1 位于最后一列,但是当将它们放入代码中时,至少在 WebGPU 中,需要按上述方式指定它们。

矩阵数学很灵活

不过,您可能会问,那又怎样?这似乎没什么好处。好处是,现在,如果想改变操作顺序,不必编写新的着色器。可以在 JavaScript 中改变数学

    //平移->旋转->缩放
    //let matrix = mat3.multiply(translationMatrix, rotationMatrix);
    //matrix = mat3.multiply(matrix, scaleMatrix);
    //缩放->旋转->平移
    let matrix = mat3.multiply(scaleMatrix, rotationMatrix);
    matrix = mat3.multiply(matrix, translationMatrix);

上面从应用平移→旋转→缩放 切换到 缩放→旋转→平移

12. WebGPU 矩阵数学_第17张图片

拖动滑块,会看到以不同的顺序组成矩阵时的不同反应。例如,平移发生在旋转之后

12. WebGPU 矩阵数学_第18张图片

左边的那个可以描述为缩放和旋转的 F,向左和向右平移。右边的那个可以更好地描述为平移本身已经旋转​​和缩放。运动不是左 ↔ 右的,它是对角线的。更进一步,右边的 F 没有移动那么远,因为平移本身已经缩放。

这种灵活性就是为什么矩阵数学是所有计算机图形的核心组成部分。

能够像这样应用矩阵对于分层动画尤其重要,例如身体上的胳膊和腿、围绕太阳的行星周围的卫星或树上的树枝。对于分层矩阵应用的简单示例,让我们绘制“F”五次,但每次都从前一个“F”的矩阵开始。

为此,需要 5 个uniform buffer、5 个uniform value和 5 个绑定组

  const numObjects = 5;   //here
  const objectInfos = []; //here
  for (let i = 0; i < numObjects; ++i) {
    // color, resolution, padding, matrix
    const uniformBufferSize = (4 + 2 + 2 + 12) * 4;
    const uniformBuffer = device.createBuffer({
      label: 'uniforms',
      size: uniformBufferSize,
      usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
    });
 
    const uniformValues = new Float32Array(uniformBufferSize / 4);
 
    // offsets to the various uniform values in float32 indices
    const kColorOffset = 0;
    const kResolutionOffset = 4;
    const kMatrixOffset = 8;
 
    const colorValue = uniformValues.subarray(kColorOffset, kColorOffset + 4);
    const resolutionValue = uniformValues.subarray(kResolutionOffset, kResolutionOffset + 2);
    const matrixValue = uniformValues.subarray(kMatrixOffset, kMatrixOffset + 12);
 
    // The color will not change so let's set it once at init time
    colorValue.set([Math.random(), Math.random(), Math.random(), 1]);
 
    const bindGroup = device.createBindGroup({
      label: 'bind group for object',
      layout: pipeline.getBindGroupLayout(0),
      entries: [
        { binding: 0, resource: { buffer: uniformBuffer }},
      ],
    });
 
    objectInfos.push({
      uniformBuffer,
      uniformValues,
      resolutionValue,
      matrixValue,
      bindGroup,
    });
  }

在渲染时,循环遍历它们并将之前的矩阵乘以平移、旋转和缩放矩阵。

function render() {
  ...
 
  const translationMatrix = mat3.translation(settings.translation);
  const rotationMatrix = mat3.rotation(settings.rotation);
  const scaleMatrix = mat3.scaling(settings.scale);
 
  //let matrix = mat3.multiply(translationMatrix, rotationMatrix);
  //matrix = mat3.multiply(matrix, scaleMatrix);
 
  // Starting Matrix.
  let matrix = mat3.identity();
 
  for (const {
    uniformBuffer,
    uniformValues,
    resolutionValue,
    matrixValue,
    bindGroup,
  } of objectInfos) {
    matrix = mat3.multiply(matrix, translationMatrix)
    matrix = mat3.multiply(matrix, rotationMatrix);
    matrix = mat3.multiply(matrix, scaleMatrix);
 
    // Set the uniform values in our JavaScript side Float32Array
    resolutionValue.set([canvas.width, canvas.height]);
    matrixValue.set([
      ...matrix.slice(0, 3), 0,
      ...matrix.slice(3, 6), 0,
      ...matrix.slice(6, 9), 0,
    ]);
 
    // upload the uniform values to the uniform buffer
    device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
 
    pass.setBindGroup(0, bindGroup);
    pass.drawIndexed(numVertices);
  }
 
  pass.end();

为了完成这项工作,引入了函数 mat3.identity ,它可以生成单位矩阵。单位矩阵是一个表示 1.0 的矩阵,因此如果乘以单位矩阵,什么也不会发生。就像

X * 1 = X

这样和上边的相似

matrixX * identity = matrixX

这是设置单位矩阵的代码。

const mat3 = {
  ...
  identity() {
    return [
      1, 0, 0,
      0, 1, 0,
      0, 0, 1,
    ];
  },
 
  ...

这是五个 F的显示结果。

12. WebGPU 矩阵数学_第19张图片

拖动滑块并查看每个后续“F”是如何相对于前一个“F”的大小和方向绘制的。这就是 CG 人物手臂的工作方式,其中手臂的旋转影响前臂,前臂的旋转影响手,手的旋转影响手指等

更改旋转中心或缩放比例

再看一个例子。到目前为止,在每个示例中,“F”都围绕其左上角旋转(好吧,除了颠倒了上面的顺序的示例)。这是因为使用的数学总是围绕原点旋转,而“F”的左上角位于原点 (0, 0)。

但是现在,因为可以进行矩阵运算,并且可以选择应用变换的顺序,所以可以移动原点。

    const translationMatrix = mat3.translation(settings.translation);
    const rotationMatrix = mat3.rotation(settings.rotation);
    const scaleMatrix = mat3.scaling(settings.scale);
    // make a matrix that will move the origin of the 'F' to its center.
    const moveOriginMatrix = mat3.translation([-50, -75]); //here
 
    let matrix = mat3.multiply(translationMatrix, rotationMatrix);
    matrix = mat3.multiply(matrix, scaleMatrix);
    matrix = mat3.multiply(matrix, moveOriginMatrix); //here

上面先平移 F -50,-75。这将移动它的所有点,因此 0,0 位于 F 的中心。拖动滑块并注意 F 围绕其中心旋转和缩放。

12. WebGPU 矩阵数学_第20张图片

使用该技术,可以从任意点旋转或缩放。现在您知道图像编辑程序是如何移动旋转点了。

添加投影

让我们更近一步。您可能记得在着色器中有代码将 像素 转换为 裁剪空间,如下所示。

// convert the position from pixels to a 0.0 to 1.0 value
let zeroToOne = position / uni.resolution;
 
// convert from 0 <-> 1 to 0 <-> 2
let zeroToTwo = zeroToOne * 2.0;
 
// covert from 0 <-> 2 to -1 <-> +1 (clip space)
let flippedClipSpace = zeroToTwo - 1.0;
 
// flip Y
let clipSpace = flippedClipSpace * vec2f(1, -1);
 
vsOut.position = vec4f(clipSpace, 0.0, 1.0);

如果依次查看每个步骤:

第一步,“将位置从像素转换为 0.0 到 1.0 的值”,实际上是一个缩放操作。zeroToOne = position / uni.resolution与正在缩放的​​ zeroToOne = position * (1 / uni.resolution) 相同。

第二步, let zeroToTwo = zeroToOne * 2.0; 也是缩放操作。它缩放 2 倍。

第三步,flippedClipSpace = zeroToTwo - 1.0;是平移。

第四步, clipSpace = flippedClipSpace * vec2f(1, -1); 是一个缩放。

所以,可以把这个加到数学中

  const scaleBy1OverResolutionMatrix = mat3.scaling([1 / canvas.width, 1 / canvas.height]); //here
  const scaleBy2Matrix = mat3.scaling([2, 2]); //here
  const translateByMinus1 = mat3.translation([-1, -1]);//here
  const scaleBy1Minus1 = mat3.scaling([1, -1]);//here
 
  const translationMatrix = mat3.translation(settings.translation);
  const rotationMatrix = mat3.rotation(settings.rotation);
  const scaleMatrix = mat3.scaling(settings.scale);
 
  //let matrix = mat3.multiply(translationMatrix, rotationMatrix);
  let matrix = mat3.multiply(scaleBy1Minus1, translateByMinus1); //here
  matrix = mat3.multiply(matrix, scaleBy2Matrix); //here
  matrix = mat3.multiply(matrix, scaleBy1OverResolutionMatrix); //here
  matrix = mat3.multiply(matrix, translationMatrix); //here
  matrix = mat3.multiply(matrix, rotationMatrix); //here
  matrix = mat3.multiply(matrix, scaleMatrix);

然后着色器会变成这样

struct Uniforms {
  color: vec4f,
  // resolution: vec2f,
  matrix: mat3x3f,
};
 
struct Vertex {
  @location(0) position: vec2f,
};
 
struct VSOutput {
  @builtin(position) position: vec4f,
};
 
@group(0) @binding(0) var<uniform> uni: Uniforms;
 
@vertex fn vs(vert: Vertex) -> VSOutput {
  var vsOut: VSOutput;
 
  //let position = (uni.matrix * vec3f(vert.position, 1)).xy;
 
  // convert the position from pixels to a 0.0 to 1.0 value
  //let zeroToOne = position / uni.resolution;
 
  // convert from 0 <-> 1 to 0 <-> 2
  //let zeroToTwo = zeroToOne * 2.0;
 
  // covert from 0 <-> 2 to -1 <-> +1 (clip space)
  //let flippedClipSpace = zeroToTwo - 1.0;
 
  // flip Y
  //let clipSpace = flippedClipSpace * vec2f(1, -1);
 
  //vsOut.position = vec4f(clipSpace, 0.0, 1.0);
  let clipSpace = (uni.matrix * vec3f(vert.position, 1)).xy;
 
  vsOut.position = vec4f(clipSpace, 0.0, 1.0);
  return vsOut;
}
 
@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f {
  return uni.color;
}

着色器现在非常简单,而且在功能上没有任何损失。事实上,它变得更加灵活!不再硬编码来表示像素。可以从着色器外部选择不同的单位。都是因为使用的是矩阵数学。

与其制作这 4 个额外的矩阵,不如制作一个生成相同结果的函数

const mat3 = {
  projection(width, height) {
    // Note: This matrix flips the Y axis so that 0 is at the top.
    return [
      2 / width, 0, 0,
      0, -2 / height, 0,
      -1, 1, 1,
    ];
  },
 
  ...

JavaScript 会变成这样

 // const scaleBy1OverResolutionMatrix = mat3.scaling([1 / canvas.width, 1 / canvas.height]);
 // const scaleBy2Matrix = mat3.scaling([2, 2]);
 // const translateByMinus1 = mat3.translation([-1, -1]);
 // const scaleBy1Minus1 = mat3.scaling([1, -1]);
  const projectionMatrix = mat3.projection(canvas.clientWidth, canvas.clientHeight);
  const translationMatrix = mat3.translation(settings.translation);
  const rotationMatrix = mat3.rotation(settings.rotation);
  const scaleMatrix = mat3.scaling(settings.scale);
 
 // let matrix = mat3.multiply(scaleBy1Minus1, translateByMinus1);
 // matrix = mat3.multiply(matrix, scaleBy2Matrix);
 // matrix = mat3.multiply(matrix, scaleBy1OverResolutionMatrix);
 // matrix = mat3.multiply(matrix, translationMatrix);
  let matrix = mat3.multiply(projectionMatrix, translationMatrix);
  matrix = mat3.multiply(matrix, rotationMatrix);
  matrix = mat3.multiply(matrix, scaleMatrix);
  matrix = mat3.multiply(matrix, moveOriginMatrix);

还删除了在resolution 缓冲区中为 分辨率 腾出空间的代码和设置它的代码。

通过这最后一步,从一个相当复杂的 有6-7 步的着色器变成了一个非常简单的只有 1 步的更灵活的着色器,这一切都归功于矩阵数学的魔力。

12. WebGPU 矩阵数学_第21张图片

逐步矩阵乘法

在继续之前,稍微简化一下。虽然生成各种矩阵并将它们单独相乘是很常见的,但在进行时将它们相乘也很常见。实际上可以这样写函数

const mat3 = {
 
  ...
 
  translate: function(m, translation) {
    return m3.multiply(m, m3.translation(translation));
  },
 
  rotate: function(m, angleInRadians) {
    return m3.multiply(m, m3.rotation(angleInRadians));
  },
 
  scale: function(m, scale) {
    return m3.multiply(m, m3.scaling(scale));
  },
 
  ...
 
};

这将上面的 7 行矩阵代码更改为 4 行,如下所示

const projectionMatrix = mat3.projection(canvas.clientWidth, canvas.clientHeight);
//const translationMatrix = mat3.translation(settings.translation);
//const rotationMatrix = mat3.rotation(settings.rotation);
//const scaleMatrix = mat3.scaling(settings.scale);
 
//let matrix = mat3.multiply(projectionMatrix, translationMatrix);
//matrix = mat3.multiply(matrix, rotationMatrix);
//matrix = mat3.multiply(matrix, scaleMatrix);
let matrix = mat3.translate(projectionMatrix, settings.translation);
matrix = mat3.rotate(matrix, settings.rotation);
matrix = mat3.scale(matrix, settings.scale);

mat3x3 是 3 个填充的 vec3f

正如内存布局文章中指出的那样, vec3f 通常占用 4 个浮点数的空间,而不是 3 个。

这就是 mat3x3f 在内存中的样子

12. WebGPU 矩阵数学_第22张图片

这就是为什么需要这段代码将其复制到uniform 中

    matrixValue.set([
      ...matrix.slice(0, 3), 0,
      ...matrix.slice(3, 6), 0,
      ...matrix.slice(6, 9), 0,
    ]);

可以通过更改矩阵函数来期望/处理填充来解决这个问题。

const mat3 = {
  projection(width, height) {
    // Note: This matrix flips the Y axis so that 0 is at the top.
    return [
     // 2 / width, 0, 0,
     // 0, -2 / height, 0,
     // -1, 1, 1,
      2 / width, 0, 0, 0,
      0, -2 / height, 0, 0,
      -1, 1, 1, 0,
    ];
  },
  identity() {
    return [
      //1, 0, 0,
      //0, 1, 0,
      //0, 0, 1,
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
    ];
  },
  multiply(a, b) {
    //const a00 = a[0 * 3 + 0];
    //const a01 = a[0 * 3 + 1];
    //const a02 = a[0 * 3 + 2];
    //const a10 = a[1 * 3 + 0];
    //const a11 = a[1 * 3 + 1];
    //const a12 = a[1 * 3 + 2];
    //const a20 = a[2 * 3 + 0];
    //const a21 = a[2 * 3 + 1];
    //const a22 = a[2 * 3 + 2];
    //const b00 = b[0 * 3 + 0];
    //const b01 = b[0 * 3 + 1];
    //const b02 = b[0 * 3 + 2];
    //const b10 = b[1 * 3 + 0];
    //const b11 = b[1 * 3 + 1];
    //const b12 = b[1 * 3 + 2];
    //const b20 = b[2 * 3 + 0];
    //const b21 = b[2 * 3 + 1];
    //const b22 = b[2 * 3 + 2];
    const a00 = a[0 * 4 + 0];
    const a01 = a[0 * 4 + 1];
    const a02 = a[0 * 4 + 2];
    const a10 = a[1 * 4 + 0];
    const a11 = a[1 * 4 + 1];
    const a12 = a[1 * 4 + 2];
    const a20 = a[2 * 4 + 0];
    const a21 = a[2 * 4 + 1];
    const a22 = a[2 * 4 + 2];
    const b00 = b[0 * 4 + 0];
    const b01 = b[0 * 4 + 1];
    const b02 = b[0 * 4 + 2];
    const b10 = b[1 * 4 + 0];
    const b11 = b[1 * 4 + 1];
    const b12 = b[1 * 4 + 2];
    const b20 = b[2 * 4 + 0];
    const b21 = b[2 * 4 + 1];
    const b22 = b[2 * 4 + 2];
 
    return [
      b00 * a00 + b01 * a10 + b02 * a20,
      b00 * a01 + b01 * a11 + b02 * a21,
      b00 * a02 + b01 * a12 + b02 * a22,
      0,
      b10 * a00 + b11 * a10 + b12 * a20,
      b10 * a01 + b11 * a11 + b12 * a21,
      b10 * a02 + b11 * a12 + b12 * a22,
      0,
      b20 * a00 + b21 * a10 + b22 * a20,
      b20 * a01 + b21 * a11 + b22 * a21,
      b20 * a02 + b21 * a12 + b22 * a22,
      0,
    ];
  },
  translation([tx, ty]) {
    return [
      //1, 0, 0,
      //0, 1, 0,
      //tx, ty, 1,
      1, 0, 0, 0,
      0, 1, 0, 0, 
      tx, ty, 1, 0,
    ];
  },
 
  rotation(angleInRadians) {
    const c = Math.cos(angleInRadians);
    const s = Math.sin(angleInRadians);
    return [
      //c, s, 0,
      //-s, c, 0,
      //0, 0, 1,
      c, s, 0, 0,
      -s, c, 0, 0,
      0, 0, 1, 0,
    ];
  },
 
  scaling([sx, sy]) {
    return [
      //sx, 0, 0,
      //0, sy, 0,
      //0, 0, 1,
      sx, 0, 0, 0, 
      0, sy, 0, 0,
      0, 0, 1, 0,
    ];
  },
};

现在可以更改设置矩阵的部分

    //matrixValue.set([
    //  ...matrix.slice(0, 3), 0,
    //  ...matrix.slice(3, 6), 0,
    //  ...matrix.slice(6, 9), 0,
    //]);
    matrixValue.set(matrix);

原地更新矩阵

可以做的另一件事是允许将矩阵传递给矩阵函数。这将允许 原地更新矩阵,而不是复制它。拥有这两个选项很有用,所以我们将这样做,以便如果未传入目标矩阵,将创建一个新矩阵。否则将使用传入的那个。

举3个例子

const mat3 = {
  multiply(a, b) {
  multiply(a, b, dst) {
    dst = dst || new Float32Array(12);// here
    const a00 = a[0 * 4 + 0];
    const a01 = a[0 * 4 + 1];
    const a02 = a[0 * 4 + 2];
    const a10 = a[1 * 4 + 0];
    const a11 = a[1 * 4 + 1];
    const a12 = a[1 * 4 + 2];
    const a20 = a[2 * 4 + 0];
    const a21 = a[2 * 4 + 1];
    const a22 = a[2 * 4 + 2];
    const b00 = b[0 * 4 + 0];
    const b01 = b[0 * 4 + 1];
    const b02 = b[0 * 4 + 2];
    const b10 = b[1 * 4 + 0];
    const b11 = b[1 * 4 + 1];
    const b12 = b[1 * 4 + 2];
    const b20 = b[2 * 4 + 0];
    const b21 = b[2 * 4 + 1];
    const b22 = b[2 * 4 + 2];
 
    //return [
    //  b00 * a00 + b01 * a10 + b02 * a20,
    //  b00 * a01 + b01 * a11 + b02 * a21,
    //  b00 * a02 + b01 * a12 + b02 * a22,
    //  0,
    //  b10 * a00 + b11 * a10 + b12 * a20,
    //  b10 * a01 + b11 * a11 + b12 * a21,
    //  b10 * a02 + b11 * a12 + b12 * a22,
    //  0,
    //  b20 * a00 + b21 * a10 + b22 * a20,
    //  b20 * a01 + b21 * a11 + b22 * a21,
    //  b20 * a02 + b21 * a12 + b22 * a22,
    //  0,
    // ];
    dst[ 0] = b00 * a00 + b01 * a10 + b02 * a20;
    dst[ 1] = b00 * a01 + b01 * a11 + b02 * a21;
    dst[ 2] = b00 * a02 + b01 * a12 + b02 * a22;
 
    dst[ 4] = b10 * a00 + b11 * a10 + b12 * a20;
    dst[ 5] = b10 * a01 + b11 * a11 + b12 * a21;
    dst[ 6] = b10 * a02 + b11 * a12 + b12 * a22;
 
    dst[ 7] = b20 * a00 + b21 * a10 + b22 * a20;
    dst[ 8] = b20 * a01 + b21 * a11 + b22 * a21;
    dst[ 9] = b20 * a02 + b21 * a12 + b22 * a22;
    return dst;
  },
  //translation([tx, ty]) {
  translation([tx, ty], dst) {
    dst = dst || new Float32Array(12);
    //return [
    //  1, 0, 0, 0,
    //  0, 1, 0, 0,
    //  tx, ty, 1, 0,
    //];
    dst[0] = 1;   dst[1] = 0;   dst[ 2] = 0;
    dst[4] = 0;   dst[5] = 1;   dst[ 6] = 0;
    dst[8] = tx;  dst[9] = ty;  dst[10] = 1;
    return dst;
  },
  //translate(m, translation) {
  //  return mat3.multiply(m, mat3.translation(m));
  translate(m, translation, dst) {
    return mat3.multiply(m, mat3.translation(m), dst);
  }
 
  ...

对其他的函数做同样的事情,现在代码可以更改为

    //const projectionMatrix = mat3.projection(canvas.clientWidth, canvas.clientHeight);
    //let matrix = mat3.translate(projectionMatrix, settings.translation);
    //matrix = mat3.rotate(matrix, settings.rotation);
    //matrix = mat3.scale(matrix, settings.scale);
    //matrixValue.set(matrix);
    mat3.projection(canvas.clientWidth, canvas.clientHeight, matrixValue);
    mat3.translate(matrixValue, settings.translation, matrixValue);
    mat3.rotate(matrixValue, settings.rotation, matrixValue);
    mat3.scale(matrixValue, settings.scale, matrixValue);

再需要将矩阵复制到 matrixValue 。相反,可以直接对其进行操作。

12. WebGPU 矩阵数学_第23张图片

坐标变换和空间变换

最后一件事,我们看到了上述操作顺序。在第一个例子中是

translation * rotation * scale

在第二个是

scale * rotation * translation

我们看到了它们的不同之处。

两种查看矩阵的方法。给定表达式

projectionMat * translationMat * rotationMat * scaleMat * position

许多人认为自然的第一种方式从右开始,然后向左工作

首先将位置乘以缩放矩阵得到一个缩放后的位置

scaledPosition = scaleMat * position

然后将 scaledPosition 乘以旋转矩阵得到一个 rotatedScaledPosition

rotatedScaledPosition = rotationMat * scaledPosition

然后将 rotatedScaledPosition 乘以平移矩阵得到 translatedRotatedScaledPosition

translatedRotatedScaledPosition = translationMat * rotatedScaledPosition

最后将其乘以投影矩阵以获得裁剪空间位置

clipSpacePosition = projectionMatrix * translatedRotatedScaledPosition

The 2nd way to look at matrices is reading from left to right. In that case each matrix changes the space represented by the texture we’re drawing to. The texture starts with representing clip space (-1 to +1) in each direction. Each matrix applied from left to right changes the space represented by the canvas.

查看矩阵的第二种方法从左到右阅读。在这种情况下,每个矩阵都会改变正在绘制的纹理所代表的空间。纹理从在每个方向上表示剪辑空间(-1 到 +1)开始。从左到右应用的每个矩阵都会改变画布所代表的空间。

第 1 步:无矩阵(或单位矩阵)
12. WebGPU 矩阵数学_第24张图片

The white area is the texture. Blue is outside the texture. We’re in clip space. Positions passed in need to be in clip space. The green area in the top right is the top left corner of the F. It’s upside down because in clip space +Y is up but the F was designed in pixel space which is +Y down. Further, clip space shows only 2x2 units but the F is 100x150 units big so we just see one unit’s worth.

白色区域是纹理。蓝色在纹理之外。我们在剪辑空间中。传入的位置需要在剪辑空间中。右上角的绿色区域是 F 的左上角。它是倒置的,因为在剪辑空间中 +Y 向上,但 F 是在 +Y 向下的像素空间中设计的。此外,剪辑空间仅显示 2x2 个单位,但 F 的大小为 100x150 个单位,因此我们只看到一个单位的值。

第 2 步: mat3.projection(canvas.clientWidth, canvas.clientHeight, matrixValue);

12. WebGPU 矩阵数学_第25张图片

12. WebGPU 矩阵数学_第26张图片

We’re now in pixel space. X = 0 to textureWidth, Y = 0 to textureHeight with 0,0 at the top left. Positions passed using this matrix in need to be in pixel space. The flash you see is when the space flips from positive Y = up to positive Y = down.

我们现在处于像素空间。 X = 0 到 textureWidth,Y = 0 到 textureHeight,0,0 位于左上角。使用此矩阵传递的位置需要在像素空间中。你看到的闪烁是当空间从 +Y = up 翻转到+Y = down 时(像素空间+Y 向下,裁剪空间+Y 向上)。

第 3 步: mat3.translate(matrixValue, settings.translation, matrixValue);
12. WebGPU 矩阵数学_第27张图片

空间原点现已移至 tx, ty (150, 100)。

第 4 步: mat3.rotate(matrixValue, settings.rotation, matrixValue);

12. WebGPU 矩阵数学_第28张图片

空间已经围绕 tx,ty 旋转

第 5 步: mat3.scale(matrixValue, settings.scale, matrixValue);

12. WebGPU 矩阵数学_第29张图片

先前以 tx, ty 为中心旋转的空间在 x 方向缩放为 2,在 y 方向缩放为 1.5

然后在着色器中执行 clipSpace = uni.matrix * vert.position; vert.position 值有效地应用于这个最终空间。

使用您认为更容易理解的方式。

我希望这些文章有助于揭开矩阵数学的神秘面纱。接下来我们将转向 3D。在 3D 中,矩阵数学遵循相同的原则和用法。我们从 2D 开始,希望让它易于理解。

Also, if you really want to become an expert in matrix math check out this amazing videos.
另外,如果你真的想成为矩阵数学专家,请观看这​​个精彩的视频。


什么是 clientWidth 和 clientHeight ?

到目前为止,每当我们提到画布的尺寸时,我们都会使用 canvas.width 和 canvas.height ,但在上面调用 mat3.projection 时,我们会使用 canvas.clientWidth 和 canvas.clientHeight 。为什么?

投影矩阵关注如何获取剪辑空间(每个维度中的 -1 到 +1)并将其转换回像素。但是,在浏览器中,我们正在处理两种类型的像素。一个是画布本身的像素数。因此,例如像这样定义的画布。

  <canvas width="400" height="300"></canvas>

或者像这样定义的

  const canvas = document.createElement("canvas");
  canvas.width = 400;
  canvas.height = 300;

两者都包含一张 400 像素宽 x 300 像素高的图像。但是,该尺寸与浏览器实际显示 400x300 像素画布的尺寸不同。 CSS 定义画布显示的大小。例如,如果我们制作这样的画布。

  <style>
    canvas {
      width: 100%;
      height: 100%;
    }
  </style>
  ...
  <canvas width="400" height="300"></canvas>

无论其容器大小如何,画布都会显示。这可能不是 400x300。

Here are two examples that set the canvas’s CSS display size to 100% so the canvas is stretched out to fill the page. The first one uses canvas.width and canvas.height when calling mat3.projection. Open it in a new window and resize the window. Notice how the ‘F’ doesn’t have the correct aspect. It gets distorted. It’s also not in the correct place. The code says the top left corner should be at 150, 25 but as the canvas is stretched and shrunk the position where something we want to appear at 150, 25 moves.

下面是两个将画布的 CSS 显示大小设置为 100% 的示例,因此画布被拉伸以填充页面。第一个调用 mat3.projection 时使用 canvas.width 和 canvas.height 。在新窗口中打开它并调整窗口大小。请注意“F”如何没有正确的方面。它会变形。它也不在正确的位置。代码说左上角应该在 150, 25,但随着画布被拉伸和收缩,我们想要在 150, 25 移动的地方出现的东西。

12. WebGPU 矩阵数学_第30张图片

This second example uses canvas.clientWidth and canvas.clientHeight when calling mat3.projection. canvas.clientWidth and canvas.clientHeight report the size the canvas is actually being displayed by the browser so in this case, even though the canvas still only has 400x300 pixels since we’re defining our aspect ratio based on the size the canvas is being displayed the F always looks correct and the F is in the correct place.

第二个示例在调用 mat3.projection 时使用 canvas.clientWidth 和 canvas.clientHeight 。 canvas.clientWidth 和 canvas.clientHeight 报告浏览器实际显示的画布大小,所以在这种情况下,即使画布仍然只有 400x300 像素,因为我们根据画布显示的大小定义纵横比 F 看起来总是正确的,F 在正确的位置。

12. WebGPU 矩阵数学_第31张图片

Most apps that allow their canvases to be resized try to make the canvas.width and canvas.height match the canvas.clientWidth and canvas.clientHeight because they want there to be one pixel in the canvas for each pixel displayed by the browser.[^device-pixel-ratio] But, as we’ve seen above, that’s not the only option. That means, in almost all cases, it’s more technically correct to compute a projection matrix’s aspect ratio using canvas.clientHeight and canvas.clientWidth.

大多数允许调整画布大小的应用程序都会尝试使 canvas.width 和 canvas.height 与 canvas.clientWidth 和 canvas.clientHeight 匹配,因为它们希望浏览器显示的每个像素对应画布中的一个像素。[^ device-pixel-ratio] 但是,正如我们在上面看到的,这不是唯一的选择。这意味着,在几乎所有情况下,使用 canvas.clientHeight 和 canvas.clientWidth 计算投影矩阵的纵横比在技术上更正确

你可能感兴趣的:(webgpu,矩阵,算法,线性代数)