WebGPU 速度与优化
原作者:webgpufundamentals Contributors
原始链接:https://webgpufundamentals.org/webgpu/lessons/webgpu-optimization.html
编译说明:本文不是逐字全盘翻译,而是基于原文进行深度解读,保留核心代码与技术要点,并提炼出最关键的优化方向。原文示例使用 WebGPU 与 WGSL(WebGPU Shading Language),非 GLSL,但渲染概念与 WebGL/GLSL 高度对应。
三个核心优化要点
-
合并顶点数据,使用单个交错缓冲区
将位置、法线、纹理坐标等属性合并到一个缓冲区并交错排列,减少setVertexBuffer调用次数,同时提升 GPU 内存访问局部性。 -
拆分 Uniform 并合并到大缓冲区 + 偏移
将全局共享 uniform(如视图投影矩阵、光源位置)与每对象 uniform、材质 uniform 分离;所有对象共享一个大的 uniform buffer,通过offset和size在 bind group 中引用不同区域,将数千次writeBuffer降为一次。 -
使用 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 概念保持一致。
