从 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 紧密集成,因此非常适合处理视频帧。

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

// 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') 获取其位置。如果您输错了 uniform 变量的名称,这会很有用,因为您会收到错误消息。

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

mipmap 生成

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

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

存储缓冲区和存储纹理

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

  • 传递给着色器的存储缓冲区数据可以比均匀缓冲区大得多。虽然规范规定均匀缓冲区绑定的大小不得超过 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 之间的差异。