WebGPU迁移指南
迁移到 WebGPU 不仅仅意味着切换图形 API。 这也是迈向网络图形未来的一步。 但是,通过准备和理解,这种迁移会变得更好。在本文中,我们将讨论 WebGL 和即将推出的 WebGPU 之间的差异,并将介绍如何准备项目进行迁移。
1、WebGL和WebGPU的时间线
WebGL 与许多其他 Web 技术一样,其根源可以追溯到很久以前。 要了解转向 WebGPU 背后的动力和动机,首先快速浏览一下 WebGL 开发的历史会很有帮助:
- OpenGL 桌面版 (1993) OpenGL 桌面版首次亮相。
- WebGL 1.0 (2011):这是 WebGL 的第一个稳定版本,基于 OpenGL ES 2.0,该版本本身于 2007 年推出。它为 Web 开发人员提供了直接在浏览器中使用 3D 图形的能力,而无需额外的插件。
- WebGL 2.0 (2017):在第一个版本六年后推出,WebGL 2.0 基于 OpenGL ES 3.0 (2012)。 该版本带来了许多改进和新功能,使网络上的 3D 图形更加强大。
近年来,人们对新的图形 API 的兴趣激增,这些 API 为开发人员提供了更多的控制力和灵活性:
- Vulkan (2016):由 Khronos 小组创建,这个跨平台 API 是 OpenGL 的“继承者”。 Vulkan 提供对图形硬件资源的较低级别访问,允许高性能应用程序更好地控制图形硬件。
- D3D12 (2015):该 API 由 Microsoft 创建,专用于 Windows 和 Xbox。 D3D12 是 D3D10/11 的后继版本,为开发人员提供了对图形资源更深入的控制。
- Metal (2014):Metal 由 Apple 创建,是 Apple 设备的专有 API。 它的设计考虑到了 Apple 硬件上的最佳性能。
2、WebGPU 的当前状态以及未来发展
如今,从版本 113 开始,WebGPU 可通过 Google Chrome 和 Microsoft Edge 浏览器在 Windows、Mac 和 ChromeOS 等多个平台上使用。预计在不久的将来将支持 Linux 和 Android。
以下是一些已经支持(或提供实验性支持)WebGPU 的引擎:
- Babylon JS:完全支持 WebGPU。
- ThreeJS:目前处于实验支持状态。
- PlayCanvas:正在开发中,但前景非常光明。
- Unity:2023.2 alpha 版本中宣布了非常早期的实验性 WebGPU 支持。
- Cocos Creator 3.6.2:正式支持WebGPU,成为该领域的先驱之一。
- Construct:目前仅在 Windows、macOS 和 ChromeOS 的 v113+ 中受支持。
考虑到这一点,过渡到 WebGPU 或至少为这种过渡准备项目似乎是在不久的将来采取的及时步骤。
3、高层概念差异
让我们从初始化开始,看看 WebGL 和 WebGPU 之间的一些高级概念差异。
3.1 初始化
当开始使用图形 API 时,第一步是初始化用于交互的主对象。 WebGL 和 WebGPU 之间的这个过程有所不同,两个系统都有一些特殊之处。
- WebGL:上下文模型
在 WebGL 中,这个对象被称为“上下文”,它本质上代表了在 HTML5 画布元素上绘图的接口。 获取这个上下文非常简单:
const gl = canvas.getContext('webgl');
WebGL 的上下文实际上与特定的画布相关联。 这意味着如果您需要在多个画布上渲染,则将需要多个上下文。
- WebGPU:设备模型
WebGPU引入了一个称为“设备”的新概念。 该设备代表你将与之交互的 GPU 抽象。 初始化过程比 WebGL 中的稍微复杂一些,但它提供了更多的灵活性:
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const context = canvas.getContext('webgpu');
context.configure({
device,
format: 'bgra8unorm',
});
该模型的优点之一是一台设备可以在多个画布上渲染,甚至可以在没有画布上渲染。 这提供了额外的灵活性; 例如,一个设备可以控制多个窗口或上下文中的渲染。
3.2 程序和管道
WebGL 和 WebGPU 代表了管理和组织图形管道的不同方法。
- WebGL:程序
在WebGL中,主要关注的是着色器程序。 该程序结合了顶点和片段着色器,定义了顶点应如何变换以及每个像素应如何着色。
const program = gl.createProgram();
gl.attachShader(program, vertShader);
gl.attachShader(program, fragShader);
gl.bindAttribLocation(program, 'position', 0);
gl.linkProgram(program);
在 WebGL 中创建程序的步骤:
- 创建着色器:编写并编译着色器的源代码。
- 创建程序:将编译好的着色器附加到程序中,然后进行链接。
- 使用程序:渲染前激活程序。
- 数据传输:数据传输到激活的程序。
此过程允许灵活的图形控制,但也可能很复杂并且容易出错,特别是对于大型复杂的项目。
- WebGPU:管道
WebGPU 引入了“管道”的概念,而不是单独的程序。 该管道不仅结合了着色器,还结合了其他信息,这些信息在 WebGL 中被建立为状态。 因此,在 WebGPU 中创建管道看起来更复杂:
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: shaderModule, entryPoint: 'vertexMain',
buffers: [{
arrayStride: 12,
attributes: [{
shaderLocation: 0, offset: 0, format: 'float32x3'
}]
}],
},
fragment: {
module: shaderModule, entryPoint: 'fragmentMain',
targets: [{ format, }],
},
});
在 WebGPU 中创建管道的步骤:
- 着色器定义:着色器源代码的编写和编译,类似于在 WebGL 中的完成方式。
- 管道创建:将着色器和其他渲染参数组合到管道中。
- 管道使用:管道在渲染之前激活。
WebGL 将渲染的各个方面分开,而 WebGPU 则尝试将更多方面封装到单个对象中,使系统更加模块化和灵活。 WebGPU 不像 WebGL 那样单独管理着色器和渲染状态,而是将所有内容组合到一个管道对象中。 这使得该过程更加可预测并且不易出错。
3.3 Uniform
统一变量提供可供所有着色器实例使用的常量数据。
- WebGL 1 中的Uniforms
在基本的WebGL中,我们能够直接通过API调用来设置统一变量。
GLSL:
uniform vec3 u_LightPos;
uniform vec3 u_LightDir;
uniform vec3 u_LightColor;
JavaScript:
const location = gl.getUniformLocation(p, "u_LightPos");
gl.uniform3fv(location, [100, 300, 500]);
此方法很简单,但需要对每个统一变量进行多次 API 调用。
- WebGL 2 中的Uniform
随着 WebGL 2 的到来,我们现在能够将统一变量分组到缓冲区中。 尽管您仍然可以使用单独的统一着色器,但更好的选择是使用统一缓冲区将不同的统一分组为更大的结构。 然后,您可以立即将所有这些统一数据发送到 GPU,类似于在 WebGL 1 中加载顶点缓冲区的方式。这具有多个性能优势,例如减少 API 调用并更接近现代 GPU 的工作方式。
GLSL:
layout(std140) uniform ub_Params {
vec4 u_LightPos;
vec4 u_LightDir;
vec4 u_LightColor;
};
JavaScript:
gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, gl.createBuffer());
要在 WebGL 2 中绑定大型统一缓冲区的子集,您可以使用称为 bindBufferRange 的特殊 API 调用。 在 WebGPU 中,有类似的称为动态统一缓冲区偏移量,您可以在调用 setBindGroup API 时传递偏移量列表。
4、着色器
从 WebGL 迁移到 WebGPU 不仅需要更改 API,还需要更改着色器。 WGSL 规范旨在使这种过渡平滑且直观,同时保持现代 GPU 的效率和性能。
4.1 着色器语言:GLSL 与 WGSL
WGSL 旨在成为 WebGPU 和本机图形 API 之间的桥梁。 与 GLSL 相比,WGSL 看起来更冗长一些,但结构仍然很熟悉。
这是纹理着色器的示例:
GLSL:
sampler2D myTexture;
varying vec2 vTexCoord;
void main() {
return texture(myTexture, vTexCoord);
}
WGSL:
[[group(0), binding(0)]] var mySampler: sampler;
[[group(0), binding(1)]] var myTexture: texture_2d<f32>;
[[stage(fragment)]]
fn main([[location(0)]] vTexCoord: vec2<f32>) -> [[location(0)]] vec4<f32> {
return textureSample(myTexture, mySampler, vTexCoord);
}
4.2 数据类型比较
下表显示了 GLSL 和 WGSL 中基本数据类型和矩阵数据类型的比较:
从 GLSL 过渡到 WGSL 表明了对更严格的类型和数据大小的明确定义的渴望,这可以提高代码可读性并减少错误的可能性。
4.3 结构
声明结构的语法也发生了变化:
GLSL:
struct Light {
vec3 position;
vec4 color;
float attenuation;
vec3 direction;
float innerAngle;
float angle;
float range;
};
WGSL:
struct Light {
position: vec3<f32>,
color: vec4<f32>,
attenuation: f32,
direction: vec3<f32>,
innerAngle: f32,
angle: f32,
range: f32,
};
引入显式语法来声明 WGSL 结构中的字段强调了对更高清晰度的渴望,并简化了对着色器中数据结构的理解。
4.4 函数声明
GLSL:
float saturate(float x) {
return clamp(x, 0.0, 1.0);
}
WGSL:
fn saturate(x: f32) -> f32 {
return clamp(x, 0.0, 1.0);
}
更改 WGSL 中函数的语法反映了声明和返回值方法的统一,使代码更加一致和可预测。
4.5 内置函数
在 WGSL 中,许多内置 GLSL 函数已被重命名或替换。 例如:
重命名WGSL中的内置函数不仅简化了它们的名称,而且使它们更加直观,这可以方便熟悉其他图形API的开发人员的过渡过程。
5、着色器转换
对于那些计划将项目从 WebGL 转换为 WebGPU 的人来说,重要的是要知道有一些工具可以自动将 GLSL 转换为 WGSL,例如 Naga,这是一个用于将 GLSL 转换为 WGSL 的 Rust 库。 在 WebAssembly 的帮助下,它甚至可以在浏览器中正常工作。
以下是 Naga 支持的端点:
6、转换差异
纹理
迁移后,您可能会遇到翻转图像形式的惊喜。 那些曾经将应用程序从 OpenGL 移植到 Direct3D(或反之亦然)的人已经面临过这个经典问题。
在 OpenGL 和 WebGL 的上下文中,纹理通常以起始像素对应于左下角的方式加载。 但在实际应用中,很多开发者会从左上角开始加载图片,从而导致图片翻转错误。 然而,这个错误可以通过其他因素来补偿,最终解决问题。
与 OpenGL 不同,Direct3D 和 Metal 等系统传统上使用左上角作为纹理的起点。 考虑到这种方法对于许多开发人员来说似乎是最直观的,WebGPU 的创建者决定遵循这种做法。
视口空间
如果你的 WebGL 代码从帧缓冲区选择像素,请做好准备,因为 WebGPU 使用不同的坐标系。 您可能需要应用简单的“y = 1.0 - y”操作来纠正坐标。
剪辑空间
当开发人员面临对象比预期更早被剪切或消失的问题时,这通常与深度域的差异有关。 WebGL 和 WebGPU 之间的区别在于它们如何定义剪辑空间的深度范围。 WebGL 使用从 -1 到 1 的范围,而 WebGPU 使用从 0 到 1 的范围,类似于 Direct3D、Metal 和 Vulkan 等其他图形 API。 做出此决定是由于使用 0 到 1 范围的多个优点,这些优点是在使用其他图形 API 时发现的。
将模型位置转换为剪辑空间的主要责任在于投影矩阵。 调整代码的最简单方法是确保投影矩阵输出结果在 0 到 1 的范围内。对于使用 gl-matrix 等库的用户,有一个简单的解决方案:您可以使用 透视ZO; 类似的函数可用于其他矩阵运算。
if (webGPU) {
// Creates a matrix for a symetric perspective-view frustum
// using left-handed coordinates
mat4.perspectiveZO(out, Math.PI / 4, ...);
} else {
// Creates a matrix for a symetric perspective-view frustum
// based on the default handedness and default near
// and far clip planes definition.
mat4.perspective(out, Math.PI / 4, …);
}
但是,有时你可能有一个现有的投影矩阵,并且无法更改其源。 在这种情况下,要将其转换为 0 到 1 的范围,您可以将投影矩阵与另一个校正深度范围的矩阵预乘。
7、WebGPU 提示与技巧
现在,我们来讨论一些使用 WebGPU 的提示和技巧。
- 最大限度地减少你使用的管道数量。
使用的管道越多,状态切换就越多,性能就越差; 这可能不是一件小事,具体取决于您的资产来自哪里。
- 提前创建管道
创建管道并立即使用它可能会起作用,但不建议这样做。 相反,创建立即返回并开始在不同线程上工作的函数。 当您使用管道时,执行队列需要等待待处理的管道创建完成。 这可能会导致严重的性能问题。 为了避免这种情况,请确保在创建管道和首次使用管道之间留出一些时间。
或者,更好的是,使用 create*PipelineAsync 变体! 当管道准备好使用时,承诺就会解决,不会出现任何停顿。
device.createComputePipelineAsync({
compute: {
module: shaderModule,
entryPoint: 'computeMain'
}
}).then((pipeline) => {
const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(pipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.dispatchWorkgroups(128);
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);
});
- 使用渲染包
渲染包是预先记录的、部分的、可重复使用的渲染通道。 它们可以包含大多数渲染命令(除了设置视口之类的命令),并且可以在稍后作为实际渲染通道的一部分“重播”。
const renderPass = encoder.beginRenderPass(descriptor);
renderPass.setPipeline(renderPipeline);
renderPass.draw(3);
renderPass.executeBundles([renderBundle]);
renderPass.setPipeline(renderPipeline);
renderPass.draw(3);
renderPass.end();
渲染包可以与常规渲染通道命令一起执行。 每次捆绑包执行之前和之后,渲染通道状态都会重置为默认值。 这样做主要是为了减少绘图的 JavaScript 开销。 无论采用何种方法,GPU 性能都保持不变。
8、结束语
过渡到 WebGPU 不仅仅意味着切换图形 API。 这也是迈向 Web 图形未来的一步,结合了各种图形 API 的成功功能和实践。 这种迁移需要对技术和理念变化有透彻的理解,但好处是显着的。
原文链接:Migrating from WebGL to WebGPU
BimAnt翻译整理,转载请标明出处