12. WebGPU 矩阵数学

news2024/11/26 9:51:43

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

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

例如,这里的缩放为 2, 1,旋转 30 度,平移为 (100, 0)。
在这里插入图片描述

这里是 先平移(100,0) ,再 旋转30 度和 最后缩放 2, 1
在这里插入图片描述

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

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

在这里插入图片描述

为了进行数学运算,我们将矩阵各列与对应 位置分量 相乘并将结果相加。
在这里插入图片描述

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

在这种情况下,结果将是

在这里插入图片描述

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

在这里插入图片描述

现在检查一下

在这里插入图片描述

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

在这里插入图片描述

或更简洁

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

而 newZ 我们并不关心。

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

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

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

可以构建一个这样的矩阵

在这里插入图片描述

应用得到这个矩阵
在这里插入图片描述
涂黑所有乘以 0 和 1 的方格,可以得到

在这里插入图片描述

简化后得到

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

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

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

构建一个这样的矩阵

在这里插入图片描述

应用矩阵后得到这个

在这里插入图片描述

这实际是

在这里插入图片描述

最后的简化为

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,
    ]);

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

在这里插入图片描述

列转置

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.

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

在这里插入图片描述

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

  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);

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

在这里插入图片描述

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

在这里插入图片描述

左边的那个可以描述为缩放和旋转的 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的显示结果。

在这里插入图片描述

拖动滑块并查看每个后续“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 围绕其中心旋转和缩放。

在这里插入图片描述

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

添加投影

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

// 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 步的更灵活的着色器,这一切都归功于矩阵数学的魔力。

在这里插入图片描述

逐步矩阵乘法

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

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 在内存中的样子

在这里插入图片描述

这就是为什么需要这段代码将其复制到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 。相反,可以直接对其进行操作。

在这里插入图片描述

坐标变换和空间变换

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

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 步:无矩阵(或单位矩阵)
在这里插入图片描述

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);

在这里插入图片描述

在这里插入图片描述

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);
在这里插入图片描述

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

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

在这里插入图片描述

空间已经围绕 tx,ty 旋转

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

在这里插入图片描述

先前以 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 移动的地方出现的东西。

在这里插入图片描述

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 在正确的位置。

在这里插入图片描述

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 计算投影矩阵的纵横比在技术上更正确

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/665840.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第十章 STM32+ESP8266接入机智云 实现小型IOT智能家居项目

前言 最近有不少小伙伴私信留言&#xff0c;想要我推出一章能够通过APP进行远程控制并获取传感器信息的实验教程。说实话在嵌入式毕设里边&#xff0c;这算是中等偏上水平的了。刚好我也有兴趣写写。全篇4700多字&#xff0c;我写的很详细&#xff0c;按着文章一步一步操作即可…

arm64架构的linux中断分析(二)

文章目录 3. GICv3中断控制器3.1 GICv3中断控制器设备树3.2 GICv3中断控制器驱动 3. GICv3中断控制器 gic在soc中的位置如下&#xff1a; GICv3&#xff08;Generic Interrupt Controller Version 3&#xff09;是一种基于ARM Cortex-A架构的中断控制器&#xff0c;它提供了…

联邦元学习综述

联邦元学习综述 张传尧1,2, 司世景1, 王健宗1&#xff0c;肖京1 1 平安科技&#xff08;深圳&#xff09;有限公司&#xff0c;广东 深圳 518063 2 中国科学技术大学&#xff0c;安徽 合肥 230026 摘要&#xff1a;随着移动设备的普及&#xff0c;海量的数据在不断产生。数据隐…

express的使用(六) 中间件的理解

原文链接 express的使用(六) 中间件的理解") 不要脸的求关注&#xff0c;希望能让大家批评我的不足点&#xff0c;方便学习&#xff0c;一键三连最好不过了~另外&#xff0c;乌鸦玩心之钢是真的爽&#xff01; 看前提示 本篇主要讲的是关于express中间件的一些基础概念…

商品支付金额篡改测试-业务安全测试实操(16)

商品支付金额篡改测试,商品订购数量篡改测试 商品支付金额篡改测试 测试原理和方法 电商类网站在业务流程整个环节,需要对业务数据的完整性和一致性进行保护,特别是确保在用户客户端与服务、业务系统接口之间的数据传输的一致性,通常在订购类交易流程中,容易出现服务器端未…

如何在纺织服装行业运用IPD?

纺织服装行业是我国的传统支柱产业&#xff0c;对促进国民经济发展、解决就业、增加国民收入、促进社会和谐发展等方面具有十分重要的意义。纺织服装行业属于劳动密集型产业&#xff0c;产业链上下游关联度较大。产业链上游原材料主要包括棉花、麻、蚕茧丝等天然纤维以及人造纤…

艾默生CE4001S2T2B4控制模块

​ 艾默生CE4001S2T2B4控制模块 艾默生CE4001S2T2B4控制模块 集散控制系统简称dcs&#xff0c;也可直译为“分散控制系统”或“分布式计算机控制系统”。它采用控制分散、操作和管理集中的基本设计思想&#xff0c;采用多层分级、合作自治的结构形式。其主要特征是它的集中管理…

spring boot 校运会赛事管理系统-计算机毕设 附源码87890

spring boot校运会赛事管理系统 摘 要 科技进步的飞速发展引起人们日常生活的巨大变化&#xff0c;电子信息技术的飞速发展使得电子信息技术的各个领域的应用水平得到普及和应用。信息时代的到来已成为不可阻挡的时尚潮流&#xff0c;人类发展的历史正进入一个新时代。在现实运…

TC8:ICMPv4_TYPE_18-22

ICMPv4_TYPE_18: Send ICMP Destination Unreachable for unknown protocol 目的 主机收到IP Header中协议字段不支持的IP数据包时,回复ICMP目的不可达报文(未知协议) 协议不可达报文的Type为3,Code为2 测试步骤 Tester:发送一条IP报文,其中协议字段值为无效值DUT:发送…

MySQL数据库基础 14

第十四章 视图 1. 常见的数据库对象2. 视图概述2.1 为什么使用视图&#xff1f;2.2 视图的理解 3. 创建视图3.1 创建单表视图3.2 创建多表联合视图3.3 基于视图创建视图 4. 查看视图5. 更新视图的数据5.1 一般情况5.2 不可更新的视图 6. 修改、删除视图6.1 修改视图6.2 删除视图…

软考高级系统架构设计师(三) 基础知识之操作系统2(分页/分段/段页存储)

目录 存储管理 页式存储 段式存储 段页式存储 存储管理 存储管理的主要目的&#xff1a;解决多个用户共同使用主存的问题&#xff08;怎么分配内存&#xff1f;&#xff1f;&#xff09; 主要包括分区存储管理、分页存储管理、分段存储器管理、段页式存储管理以及虚拟存储…

eNSP中对NAT的配置(网络地址转换)

地址转换是把局域网的私有地址转换为公有地址&#xff0c;如果想要上外网&#xff0c;而公有地址是有限的&#xff0c;则需要将私有地址转换成公有地址&#xff0c;用端口号进行区分。 一.基本原理 NAT是改变IP报文中的源或目的地址的一种处理方式&#xff1b;让局域网用户…

清微智能TX5368A与飞桨完成Ⅱ级兼容性测试,助力全行业智能化升级

近日&#xff0c;清微智能的高性能视觉芯片TX5368A与飞桨完成Ⅱ级兼容性测试&#xff08;基于Paddle2ONNX工具&#xff09;。测试结果显示&#xff0c;双方兼容性表现良好&#xff0c;整体运行稳定。这是清微智能加入“硬件生态共创计划”后取得的又一阶段性成果。 产品兼容性证…

差分信号隔离放大变送模块光电转换器0-10mV/0-20mV/0-±10mV/0-±20mV转0-5V/0-10V/4-20mA

概述&#xff1a; DIN11 IPO 压力应变桥信号处理系列隔离放大器是一种将差分输入信号隔离放大、转换成按比例输出的直流信号导轨安装变送模块。产品广泛应用在电力、远程监控、仪器仪表、医疗设备、工业自控等行业。此系列模块内部嵌入了一个高效微功率的电源&#xff0c;向输…

软考高级系统架构设计师(三) 基础知识之操作系统1进程

目录 概要 操作系统 概述 ​编辑 进程管理 进程的状态 进程管理-PV操作 利用PV操作实现进程的同步 进程管理-前趋图 进程管理-死锁 概要 操作系统 概述 操作系统的功能&#xff1a; 进程管理、存储管理、文件管理、作业管理、设备管理 操作系统的特征 并发性共享性虚拟性不…

竞逐对话式AI,百度、字节各有千秋

随着OpenAI陆续发布的ChatGPT引发了AI界热议&#xff0c;新一代的AI热度便开始持续走高。与此同时&#xff0c;以ChatGPT模型为代表的大型预训练模型的出现&#xff0c;也使得对话式AI的生成能力和智能水平得到了飞跃式的提升&#xff0c;得益于此&#xff0c;对话式AI的发展又…

FPGA_学习_10_IP核_PLL

1 PLL IP核配置步骤 (Vivado 赛灵思) 我看的教程里面&#xff0c;那个兄弟是选的下面这个。 看来还是比较注重开发效率。 下面按照截图路径打开这个veo文件&#xff0c;学习如何在FPGA程序中例化IP核&#xff08;有点像C你创建了一个类&#xff0c;然后你实例化一个&#xff…

MES生产管理系统简介与实践经验分享,一篇就够了

MES系统是制造业企业数字化转型的重要组成部分&#xff0c;也是实现智能制造的基础。本文将从MES系统的定义、功能和应用实践等方面进行介绍和分享。 一、MES系统简介 MES系统&#xff08;Manufacturing Execution System&#xff09;又称生产执行系统&#xff0c;是在企业信…

JS自定义打印网页内容(详细全面)

浏览器自身打印 (不在赘述,重点是讲述打印自定义的几种方法与常见的问题) 使用 window.print() 调起浏览器自带的打印预览弹框打印默认会打印 body 里面所有内容const handlePrintPdf = () => {window.print(); }方式一:通过 iframe 打印部分区域 动态创建一个不可见的 i…

【Android -- JNI 和 NDK】认识 NDK

简介 定义&#xff1a;Native Development Kit &#xff0c;是 Android 的一个工具开发包。 NDK 是属于 Android 的&#xff0c;与 Java 并无直接关系 作用&#xff1a;快速开发 C、 C 的动态库&#xff0c;并自动将 so 和应用一起打包成 APK 即可通过 NDK 在 Android 中 使用…