WebGPU 速度与优化

原作者:webgpufundamentals Contributors
原始链接:https://webgpufundamentals.org/webgpu/lessons/webgpu-optimization.html

编译说明:本文不是逐字全盘翻译,而是基于原文进行深度解读,保留核心代码与技术要点,并提炼出最关键的优化方向。原文示例使用 WebGPU 与 WGSL(WebGPU Shading Language),非 GLSL,但渲染概念与 WebGL/GLSL 高度对应。


三个核心优化要点

  1. 合并顶点数据,使用单个交错缓冲区
    将位置、法线、纹理坐标等属性合并到一个缓冲区并交错排列,减少 setVertexBuffer 调用次数,同时提升 GPU 内存访问局部性。

  2. 拆分 Uniform 并合并到大缓冲区 + 偏移
    将全局共享 uniform(如视图投影矩阵、光源位置)与每对象 uniform、材质 uniform 分离;所有对象共享一个大的 uniform buffer,通过 offset 和 size 在 bind group 中引用不同区域,将数千次 writeBuffer 降为一次。

  3. 使用 Mapped Buffer 替代 queue.writeBuffer
    映射一个暂存缓冲区(transfer buffer),直接在其映射内存中写入数据,再用 copyBufferToBuffer 命令复制到实际 uniform buffer。这跳过了一次数据拷贝,实测可提升接近 2 倍帧率。


正文解读

大多数示例为了可读性,并未采用最高效的 WebGPU 用法。本文以绘制大量立方体为例,逐步展示基础优化。作者强调:通常不需要过度优化,但如果场景中有成百上千个对象,了解这些技巧就很有价值。

基本原理:做得越少,让 WebGPU 做得越少,就越快。

未优化基线

初始代码为每个对象创建独立的 uniform buffer 和 bind group,渲染时对每个对象:

  • 更新对应的 typed array
  • 调用 device.queue.writeBuffer 写入该对象的 uniform buffer
  • 设置 bind group
  • 调用 draw

当对象数量达到约 8000 时,帧率开始下降(作者环境)。


优化一:创建时映射(Mapped on Creation)

创建缓冲区时传 mappedAtCreation: true,可以直接获取映射内存写入数据,省去 writeBuffer,并且不需要 GPUBufferUsage.COPY_DST。

function createBufferWithData(device, data, usage) {
  const buffer = device.createBuffer({
    size: data.byteLength,
    usage: usage,
    mappedAtCreation: true,
  });
  const dst = new Uint8Array(buffer.getMappedRange());
  dst.set(new Uint8Array(data.buffer));
  buffer.unmap();
  return buffer;
}

此优化仅影响初始化阶段,不影响渲染时性能。


优化二:打包并交错顶点数据

原本将顶点位置、法线、纹理坐标分别放在三个缓冲区,需要三次 setVertexBuffer。合并为一个缓冲区后只需一次调用,并且交错排列数据可提升 GPU 缓存命中率。

合并后的顶点数据(部分展示):

const vertexData = new Float32Array([
// position       normal        texcoord
   1,  1, -1,     1,  0,  0,    1, 0,
   1,  1,  1,     1,  0,  0,    0, 0,
   // ... 其余顶点
]);

管线配置修改:

buffers: [
  {
    arrayStride: (3 + 3 + 2) * 4, // 8 floats
    attributes: [
      {shaderLocation: 0, offset: 0 * 4, format: 'float32x3'}, // position
      {shaderLocation: 1, offset: 3 * 4, format: 'float32x3'}, // normal
      {shaderLocation: 2, offset: 6 * 4, format: 'float32x2'}, // texcoord
    ],
  },
],

渲染时只绑定一个顶点缓冲区:

pass.setVertexBuffer(0, vertexBuffer);

优化三:拆分 Uniform(全局 / 每对象 / 材质)

原始 uniform 结构包含全部字段,其中 viewProjection、光源位置、视图位置等对于所有对象相同,不必每对象复制。拆分后:

WGSL 结构定义:

struct GlobalUniforms {
  viewProjection: mat4x4f,
  lightWorldPosition: vec3f,
  viewWorldPosition: vec3f,
};
struct PerObjectUniforms {
  normalMatrix: mat3x3f,
  world: mat4x4f,
};
struct MaterialUniforms {
  color: vec4f,
  shininess: f32,
};

对应 bind group 布局增加绑定项:

@group(0) @binding(2) `var<uniform>` obj: PerObjectUniforms;
@group(0) @binding(3) `var<uniform>` glb: GlobalUniforms;
@group(0) @binding(4) `var<uniform>` material: MaterialUniforms;

全局 uniform buffer 每帧只更新一次,材质 uniform 在初始化时写入,渲染循环中只更新每对象的世界矩阵和法线矩阵。


优化四:使用一个大 Uniform Buffer + 偏移

为所有对象分配一个大的均匀缓冲区,每个对象在 bind group 中通过 offset 引用自己的一段区域。渲染循环中,将所有对象的数据写入同一个 typed array,最后只调用一次 writeBuffer。

分配大 buffer:

const uniformBufferSize = (12 + 16) * 4;
const uniformBufferSpace = roundUp(uniformBufferSize, device.limits.minUniformBufferOffsetAlignment);
const uniformBuffer = device.createBuffer({
  label: 'uniforms',
  size: uniformBufferSpace * maxObjects,
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const uniformValues = new Float32Array(uniformBuffer.size / 4);

每个对象的 bind group:

const uniformBufferOffset = i * uniformBufferSpace;
resource: {
  buffer: uniformBuffer,
  offset: uniformBufferOffset,
  size: uniformBufferSize,
},

渲染时只写一次:

if (settings.numObjects) {
  const size = (settings.numObjects - 1) * uniformBufferSpace + uniformBufferSize;
  device.queue.writeBuffer(uniformBuffer, 0, uniformValues, 0, size / uniformValues.BYTES_PER_ELEMENT);
}

作者实测:这使 JavaScript 时间减少约 40%。


优化五:使用 Mapped Buffers

writeBuffer 会复制 typed array 数据到 GPU 进程,再复制到 GPU buffer。使用映射缓冲区可以避免第一次复制。

获取预映射暂存缓冲区:

const mappedTransferBuffers = [];
const getMappedTransferBuffer = () => {
  return mappedTransferBuffers.pop() || device.createBuffer({
    label: 'transfer buffer',
    size: uniformBufferSpace * maxObjects,
    usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
    mappedAtCreation: true,
  });
};

渲染时编码 copy 命令(在 draw 之前):

const transferBuffer = getMappedTransferBuffer();
const uniformValues = new Float32Array(transferBuffer.getMappedRange());
if (settings.numObjects) {
  const size = (settings.numObjects - 1) * uniformBufferSpace + uniformBufferSize;
  encoder.copyBufferToBuffer(transferBuffer, 0, uniformBuffer, 0, size);
}

循环中直接创建映射内存的局部视图并写入数据,不再调用 writeBuffer:

const uniformBufferOffset = i * uniformBufferSpace;
const f32Offset = uniformBufferOffset / 4;
const normalMatrixValue = uniformValues.subarray(
    f32Offset + kNormalMatrixOffset, f32Offset + kNormalMatrixOffset + 12);
const worldValue = uniformValues.subarray(
    f32Offset + kWorldOffset, f32Offset + kWorldOffset + 16);

mat4.identity(worldValue);
mat4.axisRotate(worldValue, axis, i + time * speed, worldValue);
// ...
mat3.fromMat4(mat4.transpose(mat4.inverse(worldValue)), normalMatrixValue);

提交后重新异步映射:

transferBuffer.unmap();
device.queue.submit([commandBuffer]);
transferBuffer.mapAsync(GPUMapMode.WRITE).then(() => {
  mappedTransferBuffers.push(transferBuffer);
});

作者实测:该版本在 75 fps 下可绘制约 15000 个对象,关闭渲染后性能提升近 2 倍。


其他潜在优化

  • 双缓冲大 uniform buffer:避免 GPU 使用中更新同一缓冲区。
  • 带偏移的矩阵运算:避免为每个对象创建临时 Float32Array 视图,可提升约 7%。
  • 直接映射 uniform buffer:WebGPU v1 尚不支持,未来可能作为可选特性。
  • 间接绘制(Indirect Drawing):绘制参数来自 GPU buffer,可实现 GPU 视锥剔除。
  • 渲染捆绑包(Render Bundles):预录制命令序列,适合相对静态场景。
  • Immediates(2026 年新增):快速向 shader 发送少量数据。

术语检查(WebGPU/WGSL ↔ 中文)

英文术语推荐译名说明
uniform buffer统一缓冲区存储 shader 常量数据
bind group绑定组绑定资源到 shader 的集合
render pass渲染通道定义渲染目标与操作
pipeline管线包含顶点、片元等阶段状态
vertex buffer顶点缓冲区存放顶点属性数据
index buffer索引缓冲区存放顶点索引
storage buffer存储缓冲区可读写的大型缓冲区
instancing实例化一次绘制多个相同模型
frustum culling视锥剔除剔除视锥外对象
indirect drawing间接绘制绘制参数从 GPU buffer 读取
render bundle渲染捆绑包预录制命令序列
mapped buffer映射缓冲区通过映射内存直接访问
writeBuffer写入缓冲区队列方法,拷贝数据到 GPU buffer
copyBufferToBuffer缓冲区拷贝编码 GPU 内部拷贝命令
mipmap多级渐远纹理纹理 LOD
sampler采样器定义纹理采样方式
cull mode剔除模式如背面剔除
depth stencil深度模板深度测试与模板测试
normal matrix法线矩阵用于变换法线
view projection matrix视图投影矩阵视图与投影的合成矩阵
world matrix世界矩阵模型到世界空间变换
inter-stage variable阶段间变量顶点着色器传递到片元着色器的插值变量
specular highlight镜面高光高光反射效果

以上术语均在本文代码中出现,翻译符合 WebGPU 社区惯例,并与 WebGL/GLSL 概念保持一致。