从 WebGL 到 WebGPU

François Beaufort
François Beaufort

作为 WebGL 开发者,您可能会对开始使用 WebGPU 感到既紧张又兴奋。WebGPU 是 WebGL 的后继者,可将现代图形 API 的进步带到 Web 上。

值得庆幸的是,WebGL 和 WebGPU 共享许多核心概念。这两个 API 都允许您在 GPU 上运行名为着色器的小程序。WebGL 支持顶点着色器和片段着色器,而 WebGPU 还支持计算着色器。WebGL 使用 OpenGL 着色语言 (GLSL),而 WebGPU 使用 WebGPU 着色语言 (WGSL)。虽然这两种语言不同,但底层概念基本相同。

有鉴于此,本文重点介绍了 WebGL 和 WebGPU 之间的一些区别,以帮助您上手使用。

全局状态

WebGL 具有许多全局状态。某些设置适用于所有渲染操作,例如绑定哪些纹理和缓冲区。您可以通过调用各种 API 函数来设置此全局状态,该状态在您更改之前会一直有效。WebGL 中的全局状态是主要的错误来源,因为很容易忘记更改全局设置。此外,全局状态会导致代码共享变得困难,因为开发者需要小心,避免以影响代码其他部分的方式意外更改全局状态。

WebGPU 是一种无状态 API,不保持全局状态。相反,它使用“管道”的概念来封装在 WebGL 中属于全局性的所有渲染状态。管道包含要使用的混合、拓扑和属性等信息。流水线不可变。如果要更改某些设置,您需要再创建一个流水线。WebGPU 还使用命令编码器来批量处理命令,并按命令的记录顺序执行这些命令。例如,这在阴影映射中非常有用,在对对象进行单次传递时,应用可以记录多个命令流,每个光源的阴影映射对应一个命令流。

总而言之,由于 WebGL 的全局状态模型使得创建强大的可组合库和应用变得困难而脆弱,因此 WebGPU 显著减少了开发者在向 GPU 发送命令时需要跟踪的状态数量。

不再同步

在 GPU 上,同步发送命令并等待它们通常效率不高,因为这可能会刷新流水线并导致气泡。这在 WebGPU 和 WebGL 中尤为如此,它们使用多进程架构,GPU 驱动程序在与 JavaScript 分开的进程中运行。

例如,在 WebGL 中,调用 gl.getError() 需要从 JavaScript 进程到 GPU 进程再到执行同步的 IPC。这可能会导致两个进程通信时 CPU 端出现气泡。

为避免出现这些气泡,WebGPU 采用了完全异步的设计。错误模型和所有其他操作都是异步发生的。例如,当您创建纹理时,该操作似乎会立即成功,即使纹理实际上存在错误也是如此。您只能异步发现该错误。这种设计可确保跨进程通信不会出现气泡,并为应用提供可靠的性能。

计算着色器

计算着色器是在 GPU 上运行的程序,用于执行通用计算。它们仅适用于 WebGPU,而不适用于 WebGL。

与顶点和片段着色器不同,它们不限于图形处理,并且可用于各种任务,如机器学习、物理模拟和科学计算。计算着色器由数百甚至数千个线程并行执行,因此非常高效地处理大型数据集。如需详细了解 GPU 计算,请参阅这篇有关 WebGPU 的详细文章

视频帧处理

使用 JavaScript 和 WebAssembly 处理视频帧有一些缺点:从 GPU 内存复制到 CPU 内存的开销,以及工作器和 CPU 线程可实现的并行性有限。WebGPU 没有这些限制,并且与 WebCodecs API 紧密集成,因此非常适合处理视频帧。

以下代码段展示了如何将 VideoFrame 作为外部纹理导入 WebGPU 并对其进行处理。您可以试用此演示版

// Init WebGPU device and pipeline...
// Configure canvas context...
// Feed camera stream to video...

(function render() {
  const videoFrame = new VideoFrame(video);
  applyFilter(videoFrame);
  requestAnimationFrame(render);
})();

function applyFilter(videoFrame) {
  const texture = device.importExternalTexture({ source: videoFrame });
  const bindgroup = device.createBindGroup({
    layout: pipeline.getBindGroupLayout(0),
    entries: [{ binding: 0, resource: texture }],
  });
  // Finally, submit commands to GPU
}

默认的应用可移植性

WebGPU 会强制您请求 limits。默认情况下,requestDevice() 会返回一个可能与实体设备的硬件功能不匹配的 GPUDevice,而是一个合理且适用于所有 GPU 的最小公分母。通过要求开发者请求设备限制,WebGPU 可确保应用能够在尽可能多的设备上运行。

画布处理

在您创建 WebGL 上下文并提供 alpha、抗锯齿、 colorSpace、深度、preserveDrawingBuffer 或 stencil 等上下文属性后,WebGL 会自动管理画布。

另一方面,WebGPU 要求您自行管理画布。例如,如需在 WebGPU 中实现抗锯齿,您需要创建多采样纹理并渲染到该纹理。然后,您将多采样纹理解析为常规纹理,并将该纹理绘制到画布。通过这种手动管理,您可以从单个 GPUDevice 对象输出到任意数量的画布。相比之下,WebGL 只能为每个画布创建一个上下文。

查看 WebGPU 多个画布演示

另外请注意,浏览器目前对每个网页的 WebGL 画布数量有限制。在撰写本文时,Chrome 和 Safari 最多只能同时使用 16 个 WebGL 画布;Firefox 最多可以创建 200 个 WebGL 画布。另一方面,每个网页的 WebGPU 画布数量没有限制。

显示 Safari、Chrome 和 Firefox 浏览器中 WebGL 画布数量上限的屏幕截图
Safari、Chrome 和 Firefox 中的 WebGL 画布数量上限(从左到右) - 演示

实用的错误消息

WebGPU 会为从 API 返回的每条消息提供调用堆栈。这意味着,您可以快速查看代码中发生错误的位置,这对调试和修复错误非常有帮助

除了提供调用堆栈之外,WebGPU 错误消息也易于理解且具有实用价值。错误消息通常包含错误说明和错误修正建议。

WebGPU 还允许您为每个 WebGPU 对象提供自定义 label。然后,浏览器会在 GPUError 消息、控制台警告和浏览器开发者工具中使用此标签。

从名称到索引

在 WebGL 中,许多内容都是通过名称关联的。例如,您可以在 GLSL 中声明一个名为 myUniform 的 uniform 变量,并使用 gl.getUniformLocation(program, 'myUniform') 获取其位置。这样,如果您输错了统一变量的名称,就会收到错误消息。

另一方面,在 WebGPU 中,所有内容都是通过字节偏移量或索引(通常称为位置)完全关联的。您有责任确保 WGSL 和 JavaScript 中的代码位置保持同步。

mipmap 生成

在 WebGL 中,您可以创建纹理的第 0 级 MIP,然后调用 gl.generateMipmap()。WebGL 随后会为您生成所有其他 mip 级别。

在 WebGPU 中,您必须自行生成 MIP 贴图。我们没有内置函数可执行此操作。如需详细了解此决定,请参阅规范讨论。您可以使用 webgpu-utils 等方便的库生成 mipmap 或了解如何自行生成。

存储缓冲区和存储纹理

WebGL 和 WebGPU 都支持 uniform 缓冲区,可让您将大小受限的常量参数传递给着色器。存储缓冲区与统一缓冲区非常相似,仅由 WebGPU 支持,而且比统一缓冲区更强大、更灵活。

  • 传递给着色器的存储缓冲区数据可能比统一缓冲区大得多。虽然规范规定均匀缓冲区绑定的大小不得超过 64KB(请参阅 maxUniformBufferBindingSize),但在 WebGPU 中,存储缓冲区绑定的大小上限至少为 128MB(请参阅 maxStorageBufferBindingSize)。

  • 存储缓冲区是可写的,支持一些原子操作,而统一缓冲区只是只读的。这样就可以实现新的算法类。

  • 存储缓冲区绑定支持运行时大小的数组,可实现更灵活的算法,而 uniform 缓冲区数组大小必须在着色器中提供。

只有 WebGPU 支持存储纹理,它们与纹理的关系就像存储缓冲区与统一缓冲区的关系。它们比常规纹理更灵活,支持随机访问写入(以及未来读取)。

缓冲区和纹理更改

例如,在 WebGL 中,您可以创建缓冲区或纹理,然后随时使用 gl.bufferData()gl.texImage2D() 分别更改其大小。

在 WebGPU 中,缓冲区和纹理是不可变的。也就是说,您无法在创建后更改其大小、用途或格式。您只能更改其内容。

聊天室命名惯例差异

在 WebGL 中,Z 剪裁空间范围为 -1 到 1。在 WebGPU 中,Z 裁剪空间的范围为 0 到 1。也就是说,z 值为 0 的对象距离相机最近,而 z 值为 1 的对象距离相机最远。

WebGL 和 WebGPU 中的 Z 裁剪空间范围的图示。
WebGL 和 WebGPU 中的 Z 剪辑空间范围。

WebGL 使用 OpenGL 惯例,其中 Y 轴朝上,Z 轴朝向观看者。WebGPU 使用 Metal 惯例,其中 Y 轴在屏幕之外,Z 轴在屏幕范围外。请注意,在帧缓冲区坐标、视口坐标和片段/像素坐标中,Y 轴方向为向下。在剪辑空间中,Y 轴方向仍然与 WebGL 中的方向相同。

致谢

感谢 Corentin Wallez、Gregg Tavares、Stephen White、Ken Russell 和 Rachel Andrew 审核本文。

我还建议您访问 WebGPUFundamentals.org,深入了解 WebGPU 和 WebGL 之间的差异。