在本文中,我们将使用 WebGPU 绘制一个简单的三角形。示例地址
初始化 WebGPU
WebGPU 初始化的流程比 WebGL 要更复杂。
在 WebGL 中,我们只需从 Canvas 元素获取 WebGL 渲染上下文,如 getContext(“webgl” 或者 “webgl2”)。
const gl = canvas.getContext("webgl2");
对于 WebGPU 来说,不仅需要获取上下文,还需要获取设备。
// 获取 webgpu 上下文
const context = canvas.getContext('webgpu') as GPUCanvasContext;
// 获取device
const g_adapter = await navigator.gpu.requestAdapter();
const g_device = await g_adapter.requestDevice();
关于adapter和device的区别,adapter是指物理设备(物理GPU),device是指逻辑设备(抽象GPU)。
一般的低级API比如Vulkan可以从物理设备上获取很多供应商特定的信息比如GPU供应商信息,但是目前的WebGPU并不能获取很多特定的信息。
接下来,我们来设置上下文。
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device: g_device,
format: presentationFormat,
alphaMode: 'opaque', // or 'premultiplied'
});
在context.configure
函数中我们需要指定device
、format
和alphaMode
。
对于device
,指定之前获取的逻辑设备g_device
。
对于format
,可以通过navigator.gpu.getPreferredCanvasFormat()
获取 canvas 的原生像素格式。该函数通常返回“rgba8unorm”或“bgra8unorm”的格式。
为alphaMode
指定字符串 ‘opaque’。alphaMode
设置的是 Canvas 和 HTML 元素背景的混合方式。如果设置为’opaque’,则用 WebGPU 绘图内容完全覆盖。也可以为alphaMode
设置为 ‘premultiplied’ (相当于alpha预乘),在这种情况下,作为 WebGPU 绘图的结果,如果画布像素的 alpha 小于 1,则该像素将是画布和 HTML 元素背景混合的颜色。
渲染管线设置
接下来,我们来配置 RenderPipeline。
在 WebGL 中,GPU 的设置是使用各种 gl 函数来设置的,而当前的低级 API(例如 WebGPU)是将 GPU 的大部分设置捆绑在一个通常称为 PipelineStateObject (PSO) 的对象中。
PSO 是对 GPU 的每个处理步骤(流水线步骤)的设置的抽象。通过预先创建多个这样的设置并根据情况将它们附加到 GPU,可以很快速的更改 GPU 的流水线状态。
在 WebGPU 中,这个 PSO 被称为 RenderPipeline。
初始化WebGPU之后,接下来我们就进入这个RenderPipeline的设置。RenderPipeline 有 5 个主要类别可以设置:
- GPUVertexState
- GPUFragmentState
- GPUPrimitiveState
- GPUDepthStencilState
- GPUMultisampleState
其中,前三个设置基本是必不可少的,让我们先看一下设置这三个状态的代码。
// create a render pipeline
const pipeline = g_device.createRenderPipeline({
layout: 'auto',
vertex: {
module: g_device.createShaderModule({
code: vertWGSL,
}),
entryPoint: 'main',
},
fragment: {
module: g_device.createShaderModule({
code: fragWGSL,
}),
entryPoint: 'main',
targets: [
// 0
{ // @location(0) in fragment shader
format: presentationFormat,
},
],
},
primitive: {
topology: 'triangle-list',
},
});
在大多数情况下,pipeline的layout
设置是’auto’。
接下来我们来看 vertex 的设置。我们将着色器字符串传递给逻辑设备g_device
的createShaderModule
函数。此外,entryPoint
指定的是将成为着色器入口函数的名称。
接下来是 fragment 的设置。module
和entryPoint
的设置与 vertex 类似,但是还有一个属性叫做 targets
,它指定了要绘制到的渲染目标的格式。我们在这里指定用navigator.gpu.getPreferredCanvasFormat()
获取的格式,和设置 context 时一样。
最后是 primitive 的设置。这是一个字符串,指定要绘制的几何图形的拓扑类型。
在绘制功能中做的事情
初始化 WebGPU 并创建 RenderPipeline 后,就可以编写渲染函数了。
CommandEncoder(命令编码器)
WebGPU 等低级 3D API 通常有一个称为 CommandBuffer 的缓冲区,用于将各种指令打包到 GPU。在 WebGPU 中,它被称为 CommandEncoder。
const commandEncoder = g_device.createCommandEncoder();
CommandEncoder生成后我们来调用函数beginRenderPass
,得到 renderPassEncoder 对象。这个 renderPassEncoder 会加载各种指令,但首先需要提前准备一个GPURenderPassDescriptor
类型的参数对象,用于调用beginRenderPass
函数。
const commandEncoder = g_device.createCommandEncoder();
const textureView = context.getCurrentTexture().createView();
const renderPassDescriptor: GPURenderPassDescriptor = {
colorAttachments: [
{
view: textureView,
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
loadOp: 'clear',
storeOp: 'store',
},
],
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
WebGPU 有一个render pass(一系列绘图处理单元)的概念,为了加载命令,需要先设置这个 render pass。
在渲染过程中设置colorAttachments
,这是要绘制到的颜色缓冲区的设置。
对于view
,我们要指定要绘制到的渲染目标纹理。对于基本使用来说,我们可以指定通过调用 getCurrentTexture().createView()
方法从上下文中获得对象。这也意味着是与我们正在绘制的画布相关联的缓冲区。
对于loadOp
,是指定在执行各种指令之前要执行的处理。如果指定’clear’,代表首先要清除指定的缓冲区。在clearValue
中就要设置清除的颜色信息。
storeOp
指定执行各种命令后如何处理缓冲区。这次我们将保持原样,因此指定’store’。
加载命令
接下来,我们将添加命令。不过,由于我们这次只画三角形,所以命令的数量并没有那么多。
首先,使用setPipeline
函数设置上面创建的 RenderPipeline。然后调用draw
函数(这里其实就是一个实际的绘制命令)。最后,调用end
函数来表示记录命令序列结束。
然后,把 commandEncoder 的finish()
函数的返回值交给g_device.queue.submit()
。这里的意思就是向 GPU 发出命令,GPU 接到命令将绘制三角形。
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
// 绘制图元
// param vertexCount - 要绘制的顶点数.
// param instanceCount - 要绘制的实例数.
// param firstVertex - 顶点缓冲区中开始绘制的偏移量(以顶点为单位).
// param firstInstance - 第一次绘制的实例.
passEncoder.draw(3, 1, 0, 0);
// 完成记录渲染过程命令序列
passEncoder.end();
g_device.queue.submit([commandEncoder.finish()]);
哪些可以提前设置,哪些不能
与 WebGL 相比,WebGPU 的优势在于可以保存和重复使用 GPU 设置,该配置对象就是 RenderPipeline。WebGL 就做不到这一点,每次切换绘图对象时都需要再次调用各种 gl 函数,但是我们使用 WebGPU,我们就可以切换到预先设置的 RenderPipeline。
另一方面,有些东西是不能预先创建的,也就是必须在每次绘制时生成和设置的东西,即本例中的 CommandEncoder 和 RenderPassEncoder。某些 API(例如 Vulkan)可以创建和重用等效项,但遗憾的是 WebGPU 无法做到这一点。
尽管如此,WebGPU 重用 RenderPipelines 的能力就使其成为比 WebGL 更高效的 API。
WebGPU 着色器语言 WGSL
WebGPU 的着色器语言是 WGSL,一种具有类似于 Rust 语言的独特语法的语言。
在这个示例中,每个顶点着色器和片元着色器的着色器代码如下:
// 顶点着色器
@vertex
fn main(
@builtin(vertex_index) VertexIndex : u32
) -> @builtin(position) vec4<f32> {
var pos = array<vec2<f32>, 3>(
vec2<f32>(0.0, 0.5),
vec2<f32>(-0.5, -0.5),
vec2<f32>(0.5, -0.5)
);
return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}
// 片元着色器
@fragment
fn main() -> @location(0) vec4<f32> {
return vec4<f32>(0.5, 0.0, 0.0, 0.5);
}
顶点着色器
第一个@vertex
意味着这是一个顶点着色器。接下来fn
是 function 缩写, Rust 语言也是用 fn 来写函数声明,所以 wgsl 很像 Rust。
@builtin(vertex_index) VertexIndex : u32
@builtin
意味着使用已经内置到着色器语言 WGSL 中的变量。换句话说,它是一个内部变量,可以作为标准使用,而无需在 WebGPU 端进行任何特殊设置。这里声明vertex_index
为内部变量,然后起了一个VertexIndex
的别名。: u32
是 VertexIndex 的类型,代表无符号 32 位整型。
) -> @builtin(position) vec4<f32> {
->
代表主函数的返回类型,我们可以看到类型是@builtin(position) vec4<f32>
。@bultin(position)
意味着是position
也是一个 WGSL 内部变量,这相当于 GLSL 中的gl_Position
。vec4<f32>
是position
的类型,这代表着它是一个4向量类型的 32 位浮点数。
片段着色器
第一个@fragment
意味着这是一个片段着色器。
fn main() -> @location(0) vec4<f32> {
return vec4<f32>(0.5, 0.0, 0.0, 0.5);
}
-> @location(0) vec4<f32>
显示了主函数的返回值,这是片元着色器的输出。其中@location(0)
对应于fragment.targets
数组中的第0个格式,也对应于renderPassDescriptor.colorAttachments
数组中的第0个规范。@location(0)
也意味着片元着色器的context.getCurrentTexture().createView();
指向画布的当前缓冲区。
结论
WebGPU 比 WebGL 需要更多的初始化和规范,所以一开始可能会很困难,但是一旦掌握了它,高性能的绘图体验就在等待着你。此外,由于设计理念比 WebGL 更符合当前 GPU 的设计,因此它有助于理解现代 GPU 的结构。