12. WebGPU 矩阵数学

这篇具有很好参考价值的文章主要介绍了12. WebGPU 矩阵数学。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

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

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

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

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

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

12. WebGPU 矩阵数学

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

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

在这种情况下,结果将是

12. WebGPU 矩阵数学

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

12. WebGPU 矩阵数学

现在检查一下

12. WebGPU 矩阵数学

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

12. WebGPU 矩阵数学

或更简洁

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

而 newZ 我们并不关心。

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

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

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

可以构建一个这样的矩阵

12. WebGPU 矩阵数学

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

12. WebGPU 矩阵数学

简化后得到

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

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

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

构建一个这样的矩阵

12. WebGPU 矩阵数学

应用矩阵后得到这个

12. WebGPU 矩阵数学

这实际是

12. WebGPU 矩阵数学

最后的简化为

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 矩阵数学

列转置

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 矩阵数学

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

  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 矩阵数学

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

12. WebGPU 矩阵数学

左边的那个可以描述为缩放和旋转的 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 矩阵数学

拖动滑块并查看每个后续“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 矩阵数学

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

添加投影

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

// 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 矩阵数学

逐步矩阵乘法

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

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 矩阵数学

这就是为什么需要这段代码将其复制到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 矩阵数学

坐标变换和空间变换

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

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 矩阵数学

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 矩阵数学

12. WebGPU 矩阵数学

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 矩阵数学

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

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

12. WebGPU 矩阵数学

空间已经围绕 tx,ty 旋转

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

12. WebGPU 矩阵数学

先前以 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 矩阵数学

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 矩阵数学

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 计算投影矩阵的纵横比在技术上更正确文章来源地址https://www.toymoban.com/news/detail-493588.html

到了这里,关于12. WebGPU 矩阵数学的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 轻量封装WebGPU渲染系统示例<12>- 基础3D对象实体(源码)

    此混合渲染与计算系统中,用户侧可直接接触和操作可渲染(计算)实体(Entity)。这些实体可以用于呈现画面效果也可以仅用于计算。实体可以加入场景,可以加入渲染核心,也可以加入计算核心。如果使用rendering or computing pass node,也可以直接将渲染实体加入对应的pass node。

    2024年02月05日
    浏览(37)
  • 结合底层源码介绍ConcurrentHashMap如何保证线程安全,佬会爱上这篇文章嘛

    前言: 本篇文章主要讲解 结合底层源码介绍ConcurrentHashMap如何保证线程安全 的知识。该专栏比较适合刚入坑Java的小白以及准备秋招的大佬阅读。 如果文章有什么需要改进的地方欢迎大佬提出,对大佬有帮助希望可以支持下哦~ 小威在此先感谢各位小伙伴儿了😁 以下正文开

    2024年02月11日
    浏览(40)
  • 数学建模 好文章和资源推荐

    数学建模入门篇(0基础必看,全是自己的经验) 【竞赛|数学建模】Part 1:什么是数学建模和各模块介绍 0基础小白,如何入门数学建模? 数学建模入门篇(0基础必看,全是自己的经验) 重申了一下题目,但是还是很有必要,简要概述:解决实际问题时构建数学模型的过程。   

    2024年02月14日
    浏览(40)
  • 2022 数学建模B题成品论文 参考文章 含全部建模 步骤 数学模型 图像

    完整见https://mianbaoduo.com/o/bread/mbd-Y5eVm5xw 无人机遂行编队飞行中的纯方位无源定位 摘要 一、问题重述 1.1 问题背景 由于无人机集群在遂行编队飞行时, 应尽可能的避免外界干扰, 因此需要尽可能的保持电磁静默减少电磁波信号的发射.为保持编队队形, 拟采用纯方位无源

    2024年02月08日
    浏览(54)
  • C# 窗体永远在最前

    C# 窗体永远在最前 1、调用系统API 2、函数调用(放在构造或Load) 文章抄录自 c#让窗体永在最前 调用windows api 将窗体设为topmost

    2024年02月11日
    浏览(57)
  • 《3D 数学基础》12 几何图元

    目录 1. 直线、线段和射线 1.1 直线 1.2 射线 2. 球 3. AABB 4. 平面 5. 三角形 6. 多边形 经典定义  书中对射线定义做了修改:有向线段。  算法定义 p0是起始点,d是方向向量,单位向量。 自变量是t,可以很大超过1. p(0)=p0. p(2)=p0+2d; 算法实现  其中||p - c|| = r是球面公式,D=2r是直

    2024年02月13日
    浏览(35)
  • pyqt怎么让窗口显示在最前面

    可以使用 PyQt 的 setWindowFlag() 函数来设置窗口的 \\\"AlwaysOnTop\\\" 标志。这样就可以让窗口始终显示在最前面了。 下面是一个例子: 注意,这个方法并不是在所有系统上都有效,有些系统可能会忽略这个标志。如果想要实现在所有系统上都能让窗口显示在最前面,可以使用 PyQt 的

    2024年02月15日
    浏览(43)
  • 解决小程序字体在最左上角问题

    问题如下 原因: 出现这种现象的原因是项目默认开启了Skyline渲染模式,因为Skyline渲染模式不支持原生导航栏,所以在json文件中设置的导航栏失效,文字就会向上移动,如果想要使用原生的导航栏,可以将app.json文件中的下图所展示的两个内容去掉,保存后重新编译即可。

    2024年01月22日
    浏览(67)
  • 读天才与算法:人脑与AI的数学思维笔记12_数学的艺术

    2.6.2.1. 这往往就是数学家最具创造性和可以发挥高深莫测技能的地方 2.6.2.2. 数学家只有一辈子都沉浸在数学的世界里,才可能获得关于数学猜想的灵敏嗅觉 2.6.2.3. 这通常是一种不需要解释的直觉和预感,是所有人梦寐以求的东西 2.6.2.4. 这就是计算机很难对猜想计算成功的

    2024年04月28日
    浏览(33)
  • 2023 数学建模高教社杯 国赛(A题)建模秘籍&文章代码思路大全

    铛铛!小秘籍来咯! 小秘籍希望大家都能轻松建模呀,国赛也是 让我们看看A题~ 完整版内容在文末领取噢~ 1. 太阳位置计算: 计算太阳高度角(as)和方位角(ys): 太阳高度角: sinas=cosθcosδcosH+sinθsinδ 太阳方位角:sinys=sinθ−sinassinδ cosys=cosascosδsinH​ 其中: θ 为地点的

    2024年02月09日
    浏览(87)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包