「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」。
前言
WebGPU 作为下一代Web平台的图形API,虽然目前还处于开发状态,但是我们已经可以在最新的Chrome canary版本上面进行尝鲜了。今天就由我带大家领略一番最新的WebGPU的风采。
准备工作
- 下载最新版本的Chrome Canary浏览器(这属于常规操作,这里我就不多说了)
- 打开浏览器,在地址栏输入
chrome://flags/
搜索WebGPU
,将WebGPU的功能打开
至此,我们的准备工作已经全部准备完毕了。
Coding
接下来,进入我们的重头戏环节,开始Coding了,在Coding的时候,我会为大家介绍WebGPU中的一些概念,并且尽可能的与WebGL中的一些API进行对比(其中可能会出现错误,欢迎各位指正)
第一步:获取WebGPU的device及上下文
首先,我们需要获取到"WebGPU的实例"。我们此处将会类比WebGL。
// WebGL
const ctx: WebGLRenderingContext = canvas.getContext('webgl')
// WebGPU
const webGPUCtx = canvas.getContext('webgpu')
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
此处我们很容易的看到,WebGPU除了需要获取webgpu的上下文之外,还需要通过 navigator.gpu.requestAdapter
和 adapter.requestDevice
来分别获取adapter和device
adapter
其中adapter是表示了系统底层对WebGPU实现的一个实例。比如说:在Windows平台上,底层的渲染是由D3D12来完成的,WebGPU是构建在D3D12之上的一套API,那么必然会存在一个“适配器”也就是adapter来进行适配,同样的,在Mac平台上,底层的渲染器是由Metal完成,所以同样也需要一个适配器来进行适配。
device
而device则是adapter的逻辑实例对象,我们大部分的操作都需要通过adapter来完成。
WebGPUContext
此处的 WebGPUContext
与 WebGLRenderingContext
不同的是:
WebGPUContext
本身并不提供任何的渲染和计算能力,它仅仅只是提供了渲染的区域以及跟渲染相关的一些方法。WebGLRenderingContext
则是具有完整地渲染API的,例如设置各类状态,创建buffer、发起drawcall等等,都可以由这个上下文对象来进行。
第二步:配置WebGPU上下文相关参数
interface GPUCanvasConfiguration {
device: GPUDevice;
format: GPUTextureFormat;
usage?: GPUTextureUsageFlags;
colorSpace?: GPUPredefinedColorSpace;
compositingAlphaMode?: GPUCanvasCompositingAlphaMode;
size?: GPUExtent3D;
}
presentationFormat = context.getPreferredFormat(adapter)
context.configure({
device,
format: presentationFormat, // 不同的设备支持的纹理格式不同
size: presentationSize, // 这里一般就是Canvas的大小
});
这里就是一个基本的配置项,没有特别需要说明的地方。大家了解即可。
第三步:创建渲染管线对象
当当当,重点来了,现在我们要创建渲染管线对象(PipelineState Object,下文中简称PSO对象)。关于渲染管线的相关知识,可以参考我之前写的WebGL概述——原理篇。
const pipeline = device.createRenderPipeline({
vertex: {
module: device.createShaderModule({
code: triangleVertWGSL,
}),
entryPoint: 'main',
},
fragment: {
module: device.createShaderModule({
code: redFragWGSL,
}),
entryPoint: 'main',
targets: [
{
format: presentationFormat,
},
],
},
primitive: {
topology: 'triangle-list',
},
});
我们可以大概的看出大致的内容:
- vertex:指定了对应的顶点着色器
- entryPoint:可以指定哪个函数为入口函数
- fragment:指定了片元着色器
- entryPoint:指定哪个函数为入口函数
- targets:为一系列的target设置一些参数
- primitive:指定需要绘制的图元类型
通过这样的配置,我们就完成了一条渲染管线中的配置工作,在这里,我们指定了 顶点着色器、片元着色器、基本的图元类型。
类比于WebGL,这相当于下面的代码:
const vertexShader = gl.createShader(); // 相当于WebGPU PSO中的vertex
const fragmentShader = gl.createShader(); // 相当于WebGPU PSO中的fragment
const program = gl.createProgram();
// 此处还有一系列的初始化Shader的过程,此处略
gl.useProgram(program)
gl.drawArrays(gl.TRIANGLES, 0, 3); // 相当于WebGPU PSO中的primitive
这里还涉及到一些着色器语言的部分,这一部分我们暂时先跳过,我们先完成整套流程,再回过头来讲解着色器语言的部分。
第四步:“录制” 并提交命令
WebGPU 不同于 WebGL,在WebGL中,drawArrays一旦被调用,GPU就会立即进行绘制。而在WebGPU中,分为两个步骤:
- “录制”命令,我们可以将我们需要进行的操作进行“录制”,然后将这些命令放入一个队列中。
- 提交命令,一旦这些命令被提交到GPU,GPU就会按照我们录制好的命令按顺序进行绘制。
这也是WebGPU能够对多线程有良好支持的关键性设计!
现在让我们来进行这些操作
const commandEncoder = device.createCommandEncoder();
const textureView = context.getCurrentTexture().createView(); // 获取画布上的纹理视图,将WebGPU渲染出的图像显示到画布上
const renderPassDescriptor: GPURenderPassDescriptor = {
colorAttachments: [
{
view: textureView, // 将颜色缓冲区与上述的画布绑定
loadValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, // 对应于WebGL中的gl.clearColor命令
storeOp: 'store',
},
],
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
passEncoder.draw(3, 1, 0, 0);
passEncoder.endPass();
device.queue.submit([commandEncoder.finish()]);
上面代码中的 commandEncoder
就是一个用于 “录制”命令的对象。后面的 passEncoder
是用于录制跟渲染相关的命令,除此以外,还有一个 commandEncoder.beginComputePass
的方法可以创建一个计算Shader相关的流程。
后续就是将我们之前创建好的PSO对象,设置到这个 passEncoder
中,然后调用draw方法,但是这里GPU并不会真正的绘制。这里只是在“录制命令” 而已!这一点一定要牢牢记住!
最后一定要记得调用 endPass
方法,它表示我们的渲染命令录制完了。
最后的最后,我们将我们所有录制好的命令提交到GPU,通过device.queue.submit([commandEncoder.finish()]);
来完成。
这样就是一个绘制三角形的整个流程了。最终的结果如下:
着色器语言
现在我们来回顾着色器语言的部分,在WebGL中,是采用的 GLSL
作为着色器语言,而在WebGPU中则采用了最新的 WGSL
作为着色器语言(着色器语言程序以下统称为Shader)。我们来看看本程序中的shader是如何编写的。
顶点着色器
[[stage(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);
}
首先,我们对这段程序作出一些解释:
Stage
在WebGPU中,有两大类的渲染管线:
- GPURenderPipeline,也就是我们最熟悉的用于渲染图形的渲染管线
- GPUComputePipeline,用于GPGPU通用计算的渲染管线
对于RenderPipeline,存在两类Stage: vertex shader stage
和 fragment shader stage
而stage则是用于表示这段程序用于哪个阶段。
[[stage(vertex)]]
表示这段程序用于 vertex shader stage
阶段,也就是渲染管线中的顶点处理的阶段。
[[stage(fragment)]]
表示这段程序用于 fragment shader stage
阶段,用于渲染每一个片元。
紧接着,fn main
声明了一个函数,这个函数的名称要与我们在创建PSO的时候指定的 entryPoint
保持一致。
[[builtin(vertex_index)]]
:
被 [[xxx]]
包围的内容表示该变量在GPU内存中的位置,而 builtin
的语义就很明确啦,就是表示这是一个系统内置的变量信息,后面的 VertexIndex: u32
分别表示的是变量名和变量类型, u32
表示 无符号的32位整型变量。
->
箭头后面表示的返回值所在的位置和返回值的类型。 这里的 [[builtin(position)]]
与之前的内置变量如出一辙,这里就不过多展开了。
接着我们来看一下函数体中的内容,在函数体中,主要是定义了几个顶点的位置,这里与 WebGL 的区别就比较大了,WebGL中是不支持在shader程序中直接定义顶点位置的。而WebGPU通过内建了 vertex_index
的变量,为我们提供了在shader中指定顶点位置这样的能力。免去了我们往GPU中传递顶点数据的步骤,也相当于是提高了性能。
最后,我们将顶点信息返回出来即可。
片元着色器
[[stage(fragment)]]
fn main() -> [[location(0)]] vec4<f32> {
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}
片元着色器中的程序就相对来说比较简单了, [[stage(frameng)]]
正如上面所说,它表示在片元着色阶段运行的程序。这里仅仅简单的直接返回了一个红色值。
总结
好了,到此为止,一个简单的WebGPU程序已经编写完毕了。我们来回顾一下其中的主要内容吧!
- WebGPU 的渲染流程大致分为两步:
- 录制命令(通过创建renderPassEncoder/ computePassEncoder、PipelineStateObject等来录制命令)
- 提交命令, 一旦提交命令后,GPU就会真正的开始进行渲染工作,在这之前GPU并不会工作,这也方便我们进行多线程的处理。
- WebGPU采用了全新的着色器语言(WGSL:WebGPU Shading Language),今天我们只是简单的了解了其中的一些基本语法,WGSL提供了一系列的内置变量,能够实现一些GLSL中不能实现的功能,比如在Shader中指定顶点位置。
OK,今天的内容就这么多,如果你觉得有用的,欢迎点赞并关注我,您的关注就是我更新的动力~