WebGPU 正交投影

原文作者:webgpufundamentals Contributors
原文链接:https://webgpufundamentals.org/webgpu/lessons/webgpu-orthographic-projection.html
说明:本文为技术深度解读,非逐字全盘翻译。代码保留原文英文形式,叙述性内容编译为中文。

本文是 WebGPU 3D 数学系列的第 5 篇。前四篇依次为:平移、旋转、缩放、矩阵数学。本篇之后将进入透视投影、相机、矩阵栈和场景图等内容。


WebGPU Orthographic Projection

本文核心优化要点

  1. 启用深度纹理与深度比较测试:通过 depthStencil 状态让 GPU 自动处理遮挡,避免手动排序三角形,同时减少过度绘制(overdraw)。
  2. 合并顶点属性到单一交错缓冲区:将位置与颜色存储在同一 GPUBuffer 中,使用一次 setVertexBuffer 完成绑定,减少状态切换,提升缓存局部性。
  3. 利用背面剔除 + 顶点颜色属性 + 复用矩阵数组:设置 cullMode 剔除背向三角形,减少光栅化和片元着色器压力;用 unorm8x4 顶点颜色替代 uniform 颜色,避免每帧更新动态 uniform;矩阵运算复用 dst 数组降低分配开销。

WebGPU Orthographic Projection

从 2D 到 3D 的矩阵扩展

在之前的 2D 示例中,我们使用 3×3 矩阵处理 (x, y) 点。进入 3D 需要点 (x, y, z) 和 4×4 矩阵。顶点着色器从 vec2f 位置改为 vec4f,矩阵从 mat3x3f 改为 mat4x4f:

struct Uniforms {
  color: vec4f,
-  matrix: mat3x3f,
+  matrix: mat4x4f,
};

struct Vertex {
-  @location(0) position: vec2f,
+  @location(0) position: vec4f,
};

struct VSOutput {
  @builtin(position) position: vec4f,
};

@group(0) @binding(0) `var<uniform>` uni: Uniforms;

@vertex fn vs(vert: Vertex) -> VSOutput {
  var vsOut: VSOutput;
-
-  let clipSpace = (uni.matrix * vec3f(vert.position, 1)).xy;
-  vsOut.position = vec4f(clipSpace, 0.0, 1.0);
  vsOut.position = uni.matrix * vert.position;
  return vsOut;
}

@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f {
  return uni.color;
}

在 3D 中,w 分量默认值为 1,因此可以直接将 vec4f 位置与 4×4 矩阵相乘,简化了着色器代码。


WebGPU Orthographic Projection

构建 3D “F” 几何体

为了呈现立体效果,需要在前、后、左、右、顶、底等面上创建三角形。一个更直观的方法是复制前面顶点并将 z 坐标改为 30,然后添加索引来连接前后表面:

const positions = [
  // 前面
  0, 0, 0,
  30, 0, 0,
  0, 150, 0,
  30, 150, 0,
  // ... 其余顶点
  // 后面
  0, 0, 30,
  30, 0, 30,
  0, 150, 30,
  30, 150, 30,
  // ...
];

const indices = [
  // 前面
  0,  1,  2,    2,  1,  3,
  // ...
  // 后面
  12,  13,  14,   14, 13, 15,
  // ...
];

随后使用非索引方式并为每个面赋予不同颜色,以便在渲染时更容易辨别 3D 结构:

const quadColors = [
  200,  70, 120,  // left column front
  200,  70, 120,  // top rung front
  // ...
];

const numVertices = indices.length;
const vertexData = new Float32Array(numVertices * 4); // xyz + color
const colorData = new Uint8Array(vertexData.buffer);

for (let i = 0; i < indices.length; ++i) {
  const positionNdx = indices[i] * 3;
  const position = positions.slice(positionNdx, positionNdx + 3);
  vertexData.set(position, i * 4);

  const quadNdx = (i / 6 | 0) * 3;
  const color = quadColors.slice(quadNdx, quadNdx + 3);
  colorData.set(color, i * 16 + 12);  // set RGB
  colorData[i * 16 + 15] = 255;       // set A
}

这里将顶点数据视为 Float32Array,同时用 Uint8Array 视图写入 RGBA 颜色字节。颜色在 GPU 端通过 unorm8x4 格式自动归一化到 0~1 范围,无需手动除以 255。


WebGPU Orthographic Projection

矩阵函数与正交投影

3D 平移、旋转和缩放矩阵在 Float32Array(16) 中按列主序存储。核心函数示例:

const mat4 = {
  translation([tx, ty, tz], dst) {
    dst = dst || new Float32Array(16);
    dst[ 0] = 1;   dst[ 1] = 0;   dst[ 2] = 0;   dst[ 3] = 0;
    dst[ 4] = 0;   dst[ 5] = 1;   dst[ 6] = 0;   dst[ 7] = 0;
    dst[ 8] = 0;   dst[ 9] = 0;   dst[10] = 1;   dst[11] = 0;
    dst[12] = tx;  dst[13] = ty;  dst[14] = tz;  dst[15] = 1;
    return dst;
  },

  rotationX(angleInRadians, dst) {
    const c = Math.cos(angleInRadians);
    const s = Math.sin(angleInRadians);
    dst = dst || new Float32Array(16);
    dst[ 0] = 1;  dst[ 1] = 0;   dst[ 2] = 0;  dst[ 3] = 0;
    dst[ 4] = 0;  dst[ 5] = c;   dst[ 6] = s;  dst[ 7] = 0;
    dst[ 8] = 0;  dst[ 9] = -s;  dst[10] = c;  dst[11] = 0;
    dst[12] = 0;  dst[13] = 0;   dst[14] = 0;  dst[15] = 1;
    return dst;
  },

  // rotationY, rotationZ, scaling ...
};

正交投影函数采用更通用的参数形式,允许显式指定左、右、下、上、近、远六个裁剪面:

ortho(left, right, bottom, top, near, far, dst) {
  dst = dst || new Float32Array(16);

  dst[0] = 2 / (right - left);
  dst[1] = 0;
  dst[2] = 0;
  dst[3] = 0;

  dst[4] = 0;
  dst[5] = 2 / (top - bottom);
  dst[6] = 0;
  dst[7] = 0;

  dst[8] = 0;
  dst[9] = 0;
  dst[10] = 1 / (near - far);
  dst[11] = 0;

  dst[12] = (right + left) / (left - right);
  dst[13] = (top + bottom) / (bottom - top);
  dst[14] = near / (near - far);
  dst[15] = 1;

  return dst;
}

调用示例:

mat4.ortho(
  0,                   // left
  canvas.clientWidth,  // right
  canvas.clientHeight, // bottom
  0,                   // top
  200,                 // near
  -200,                // far
  matrixValue,         // dst
);

WebGPU Orthographic Projection

背面剔除与深度纹理

如果直接绘制所有三角形,后面的三角形可能覆盖前面的,导致视觉混乱。WebGPU 提供 cullMode 来剔除特定朝向的三角形:

primitive: {
  cullMode: 'front',  // 或 'back'
},

在像素空间中 Y 轴向下,矩阵会垂直翻转所有三角形,因此这里选择 cullMode: 'front' 并确保所有三角形顶点方向一致。

仅靠剔除仍无法解决深度遮挡顺序问题。解决方案是引入 深度纹理(深度缓冲 / Z-Buffer)。建立深度纹理并在渲染管线中配置深度测试:

depthStencil: {
  depthWriteEnabled: true,
  depthCompare: 'less',
  format: 'depth24plus',
},

depthCompare: 'less' 表示只有新片元的深度值小于深度缓冲中已有值时,才绘制该片元并更新深度缓冲。这样远处的像素不会覆盖近处的像素。

渲染通道描述符需要添加深度附件:

const renderPassDescriptor = {
  colorAttachments: [
    {
      loadOp: 'clear',
      storeOp: 'store',
    },
  ],
  depthStencilAttachment: {
    depthClearValue: 1.0,
    depthLoadOp: 'clear',
    depthStoreOp: 'store',
  },
};

在每帧渲染时,如果画布尺寸变化,需要创建/重建相同尺寸的深度纹理:

let depthTexture;

function render() {
  const canvasTexture = context.getCurrentTexture();
  renderPassDescriptor.colorAttachments[0].view = canvasTexture.createView();

  if (!depthTexture ||
      depthTexture.width !== canvasTexture.width ||
      depthTexture.height !== canvasTexture.height) {
    if (depthTexture) {
      depthTexture.destroy();
    }
    depthTexture = device.createTexture({
      size: [canvasTexture.width, canvasTexture.height],
      format: 'depth24plus',
      usage: GPUTextureUsage.RENDER_ATTACHMENT,
    });
  }
  renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView();
  // ...
}

最终即可得到正确的 3D 遮挡效果。


术语检查摘要

原文术语推荐中文译名备注
clip space裁剪空间顶点着色器输出坐标所在空间,X/Y/Z 范围为 -11 或 01
vertex shader顶点着色器处理每个顶点的着色器
fragment shader片元着色器处理每个像素的着色器
uniform统一变量一组常量数据,通过绑定组传递
inter-stage variable阶段间变量从顶点着色器传递到片元着色器的变量
cull mode剔除模式'back' / 'front' / 'none'
depth texture / depth buffer / Z-buffer深度纹理 / 深度缓冲 / Z 缓冲存储每个像素深度值的纹理
depth24plus深度格式 depth24plus24 位深度 + 可能包含模板位
unorm8x4无符号归一化 8 位 4 分量自动将 0255 归一化到 01
@builtin(position)内置位置输出顶点着色器必须输出的裁剪空间位置
@location属性位置指定着色器输入/输出位置
@group / @binding绑定组 / 绑定编号资源绑定模型
GPUBufferUsageGPU 缓冲区用途如 VERTEX、INDEX、UNIFORM、COPY_DST 等
GPUTextureUsage.RENDER_ATTACHMENT纹理渲染附件用途纹理可作为渲染通道的附件
orthographic正交源自 orthogonal(直角)

下一篇将介绍如何为场景添加透视投影,使远处的物体更小、近处的更大。