paint-brush
从 WebGL 迁移到 WebGPU经过@dmitrii
5,305 讀數
5,305 讀數

从 WebGL 迁移到 WebGPU

经过 Dmitrii Ivashchenko13m2023/12/20
Read on Terminal Reader

太長; 讀書

本指南阐明了从 WebGL 到 WebGPU 的过渡,涵盖关键差异、高级概念和实用技巧。随着 WebGPU 成为网络图形的未来,本文为软件工程师和项目经理等提供了宝贵的见解。
featured image - 从 WebGL 迁移到 WebGPU
Dmitrii Ivashchenko HackerNoon profile picture

迁移到即将推出的 WebGPU 不仅仅意味着切换图形 API。这也是迈向网络图形未来的一步。但是,通过准备和理解,这种迁移会变得更好——本文将帮助您做好准备。


大家好,我叫 Dmitrii Ivashchenko,是 MY.GAMES 的软件工程师。在本文中,我们将讨论 WebGL 和即将推出的 WebGPU 之间的差异,并将介绍如何准备项目进行迁移。


内容概述

  1. WebGL 和 WebGPU 的时间表

  2. WebGPU 的当前状态以及未来发展

  3. 高层概念差异

  4. 初始化

    • WebGL:上下文模型

    • WebGPU:设备模型

  5. 计划和管道

    • WebGL:程序

    • WebGPU:管道

  6. 制服

    • WebGL 1 中的制服

    • WebGL 2 中的制服

    • WebGPU 中的制服

  7. 着色器

    • 着色器语言:GLSL 与 WGSL

    • 数据类型比较

    • 结构

    • 函数声明

    • 内置功能

    • 着色器转换

  8. 约定差异

  9. 纹理

    • 视口空间

    • 剪辑空间

  10. WebGPU 提示与技巧

    • 最大限度地减少您使用的管道数量。

    • 提前创建管道

    • 使用渲染包

  11. 概括


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 硬件上的最佳性能。


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 画布元素上绘图的接口。获取这个上下文非常简单:

 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 中创建程序的步骤:


  1. 创建着色器:编写并编译着色器的源代码。
  2. 创建程序:将编译好的着色器附加到程序中,然后进行链接。
  3. 使用程序:在渲染之前激活程序。
  4. 数据传输:数据传输到激活的程序。


此过程允许灵活的图形控制,但也可能很复杂并且容易出错,特别是对于大型复杂的项目。

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 中创建管道的步骤:


  1. 着色器定义:着色器源代码的编写和编译,类似于在 WebGL 中的完成方式。
  2. 管道创建:将着色器和其他渲染参数组合到管道中。
  3. 管道使用:管道在渲染之前激活。


WebGL 将渲染的各个方面分开,而 WebGPU 则尝试将更多方面封装到单个对象中,使系统更加模块化和灵活。 WebGPU 不像 WebGL 那样单独管理着色器和渲染状态,而是将所有内容组合到一个管道对象中。这使得该过程更加可预测并且不易出错:



制服

统一变量提供可供所有着色器实例使用的常量数据。

WebGL 1 中的制服

在基本的WebGL中,我们能够直接通过API调用来设置uniform变量。


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 中的制服

随着 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 中绑定大型统一缓冲区的子集,您可以使用称为bindBufferRange的特殊 API 调用。在 WebGPU 中,有类似的东西称为动态统一缓冲区偏移量,您可以在调用setBindGroup API 时传递偏移量列表。



WebGPU 中的制服

WebGPU 为我们提供了一种更好的方法。在这种情况下,不再支持单独的uniform变量,并且工作仅通过uniform缓冲区完成。


工作组SL

 [[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); }


工作组SL

 [[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; };


工作组:

 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); }


工作组SL

 fn saturate(x: f32) -> f32 { return clamp(x, 0.0, 1.0); }


更改 WGSL 中函数的语法反映了声明和返回值方法的统一,使代码更加一致和可预测。



内置功能

在 WGSL 中,许多内置 GLSL 函数已被重命名或替换。例如:



重命名WGSL中的内置函数不仅简化了它们的名称,而且使它们更加直观,这可以方便熟悉其他图形API的开发人员的过渡过程。



着色器转换

对于那些计划将项目从 WebGL 转换为 WebGPU 的人来说,重要的是要知道有一些工具可以自动将 GLSL 转换为 WGSL,例如 **[Naga](https://github.com/gfx-rs/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。做出此决定是由于使用 0 到 1 范围的多个优点,这些优点是在使用其他图形 API 时发现的。



将模型位置转换为剪辑空间的主要责任在于投影矩阵。调整代码的最简单方法是确保投影矩阵输出结果在 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()]); });

使用渲染包

渲染包是预先记录的、部分的、可重复使用的渲染通道。它们可以包含大多数渲染命令(除了设置视口之类的命令),并且可以在稍后作为实际渲染通道的一部分“重播”。


 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 的成功功能和实践。这种迁移需要对技术和理念变化有透彻的理解,但好处是显着的。

有用的资源和链接: