迁移到即将推出的 WebGPU 不仅仅意味着切换图形 API。这也是迈向 Web 图形未来的一步。但如果做好准备并理解,迁移将会更加顺利 — 本文将帮助您做好准备。
在本文中,我们将讨论 WebGL 和即将推出的 WebGPU 之间的区别,并阐述如何为项目迁移做好准备。
WebGL 和 WebGPU 的时间线
WebGL 与许多其他 Web 技术一样,其历史可以追溯到很久以前。要了解转向 WebGPU 背后的动态和动机,首先快速回顾一下 WebGL 开发的历史会有所帮助:
- OpenGL 桌面版(1993 年) OpenGL 桌面版首次亮相。
- WebGL 1.0(2011):这是 WebGL 的第一个稳定版本,基于 2007 年推出的 OpenGL ES 2.0。它为 Web 开发人员提供了直接在浏览器中使用 3D 图形的能力,而无需额外的插件。
- WebGL 2.0(2017 年):WebGL 2.0 在第一版发布六年后推出,基于 OpenGL ES 3.0(2012 年)。此版本带来了许多改进和新功能,使 Web 上的 3D 图形更加强大。
近年来,人们对新的图形 API 的兴趣日益浓厚,这些 API 可以为开发人员提供更多的控制力和灵活性:
- Vulkan(2016 年):由 Khronos 集团创建的跨平台 API 是 OpenGL 的“后继者”。Vulkan 提供对图形硬件资源的低级访问,让高性能应用程序能够更好地控制图形硬件。
- D3D12(2015):此 API 由 Microsoft 创建,专用于 Windows 和 Xbox。D3D12 是 D3D10/11 的后继者,为开发人员提供了对图形资源的更深层次的控制。
- Metal(2014):由 Apple 创建,是 Apple 设备的专属 API。其设计初衷是实现 Apple 硬件的最大性能。
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 或至少为这种过渡准备项目似乎是近期及时的一步。
高层概念差异
让我们缩小范围,看看 WebGL 和 WebGPU 之间的一些高级概念差异,从初始化开始。
初始化
开始使用图形 API 时,第一步是初始化交互的主要对象。此过程在 WebGL 和 WebGPU 之间有所不同,两个系统都有一些特殊之处。
WebGL:上下文模型
在 WebGL 中,此对象称为“上下文”,它本质上表示在 HTML5 Canvas 元素上绘图的界面。获取此上下文非常简单:
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',
});
此模型的优点之一是,一台设备可以在多个画布上渲染,甚至可以不渲染任何画布。这提供了额外的灵活性;例如,一台设备可以控制多个窗口或上下文中的渲染。
程序和管道
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 那样单独管理着色器和渲染状态,而是将所有内容组合到一个管道对象中。这使得该过程更加可预测,并且不容易出错:
Uniform
统一变量提供可供所有着色器实例使用的常量数据。
WebGL 1 中的 Uniforms
在基本的 WebGL 中,我们能够uniform
直接通过 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]);
这种方法很简单,但是每个uniform
变量都需要多次调用 API。
WebGL 2 中的 Uniforms
随着 WebGL 2 的到来,我们现在能够将uniform
变量分组到缓冲区中。虽然您仍然可以使用单独的统一着色器,但更好的选择是使用统一缓冲区将不同的统一分组到更大的结构中。然后,您可以一次性将所有这些统一数据发送到 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 中绑定大型统一缓冲区的子集,您可以使用称为 的特殊 API 调用bindBufferRange
。在 WebGPU 中,有一种类似的东西,称为动态统一缓冲区偏移,您可以在调用setBindGroup
API 时传递偏移列表。
WebGPU 中的统一规则
WebGPU 为我们提供了一种更好的方法。在这种情况下,uniform
不再支持单个变量,工作完全通过uniform
缓冲区完成。
工作流级别:
[[block]] struct Params {
u_LightPos : vec4<f32>;
u_LightColor : vec4<f32>;
u_LightDirection : vec4<f32>;
};
[[group(0), binding(0)]] var<uniform> ub_Params : Params;
JavaScript:
const buffer = device.createBuffer({
usage: GPUBufferUsage.UNIFORM,
size: 8
});
现代 GPU 倾向于将数据加载到一个大块中,而不是多个小块中。与其每次都重新创建和重新绑定小缓冲区,不如考虑创建一个大缓冲区,并将其不同部分用于不同的绘制调用。这种方法可以显著提高性能。
WebGL 更具命令性,每次调用时都会重置全局状态,并力求尽可能简单。另一方面,WebGPU 的目标是更加面向对象,并专注于资源重用,从而提高效率。
由于方法不同,从 WebGL 过渡到 WebGPU 可能看起来很困难。但是,从过渡到 WebGL 2 作为中间步骤可以简化您的生活。
着色器
从 WebGL 迁移到 WebGPU 不仅需要更改 API,还需要更改着色器。WGSL 规范旨在使这一过渡顺畅而直观,同时保持现代 GPU 的效率和性能。
着色器语言:GLSL 与 WGSL
WGSL 旨在成为 WebGPU 和原生图形 API 之间的桥梁。与 GLSL 相比,WGSL 看起来更冗长一些,但结构仍然很熟悉。
以下是纹理着色器的示例:
全局语言资源管理器(GLSL):
sampler2D myTexture;
varying vec2 vTexCoord;
void main() {
return texture(myTexture, vTexCoord);
}
工作组:
[[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);
}
数据类型比较
下表显示了 GLSL 和 WGSL 中基本数据类型和矩阵数据类型的比较:
从 GLSL 过渡到 WGSL 表明了对更严格的类型和数据大小的明确定义的需求,这可以提高代码的可读性并降低出现错误的可能性。
结构
声明结构的语法也发生了变化:
// 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 结构中字段的明确语法强调了对更高清晰度的渴望,并简化了对着色器中数据结构的理解。
函数声明
全局语言资源管理器(GLSL):
float saturate(float x) {
return clamp(x, 0.0, 1.0);
}
工作组:
fn saturate(x: f32) -> f32 {
return clamp(x, 0.0, 1.0);
}
WGSL 中函数语法的改变反映了声明和返回值方法的统一,使得代码更加一致和可预测。
内置函数
在 WGSL 中,许多内置的 GLSL 函数已被重命名或替换。例如:
重命名 WGSL 中的内置函数不仅简化了它们的名称,而且使它们更加直观,这可以方便熟悉其他图形 API 的开发人员的过渡过程。
着色器转换
对于那些计划将项目从 WebGL 转换为 WebGPU 的人来说,重要的是要知道有一些工具可以自动将 GLSL 转换为 WGSL,例如Naga,这是一个用于将 GLSL 转换为 WGSL 的 Rust 库。它甚至可以在 WebAssembly 的帮助下在您的浏览器中运行。
以下是 Naga 支持的端点:
惯例差异
纹理
迁移后,您可能会遇到图像翻转的意外情况。曾经将应用程序从 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。做出此决定是因为在使用其他图形 API 时发现了使用从 0 到 1 的范围的几个优点。
将模型位置转换到裁剪空间的主要责任在于投影矩阵。调整代码的最简单方法是确保投影矩阵输出结果在 0 到 1 的范围内。对于使用 gl-matrix 等库的用户,有一个简单的解决方案:perspective
您可以使用 而不是 函数perspectiveZO
;其他矩阵操作也有类似的函数可用。
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 的范围,您可以将投影矩阵预乘以另一个校正深度范围的矩阵。
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()]);
});
使用 RenderBundles
渲染包是预先录制的、部分且可重复使用的渲染过程。它们可以包含大多数渲染命令(除了设置视口之类的命令),并且可以在以后作为实际渲染过程的一部分进行“重放”。
const renderPass = encoder.beginRenderPass(descriptor);
renderPass.setPipeline(renderPipeline);
renderPass.draw(3);
renderPass.executeBundles([renderBundle]);
renderPass.setPipeline(renderPipeline);
renderPass.draw(3);
renderPass.end();
渲染包可以与常规渲染过程命令一起执行。每次执行包之前和之后,渲染过程状态都会重置为默认值。这样做主要是为了减少绘制的 JavaScript 开销。无论采用哪种方法,GPU 性能都保持不变。
概括
过渡到 WebGPU 不仅仅意味着切换图形 API。这也是迈向 Web 图形未来的一步,它将各种图形 API 的成功功能和实践相结合。这种迁移需要彻底了解技术和理念上的变化,但其好处是巨大的。
RA/SD 衍生者AI训练营。发布者:chris,转载请注明出处:https://www.shxcj.com/archives/6190