Hello,WebGPU —— 绘制第一个三角形

3,378 阅读8分钟

「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」。

前言

WebGPU 作为下一代Web平台的图形API,虽然目前还处于开发状态,但是我们已经可以在最新的Chrome canary版本上面进行尝鲜了。今天就由我带大家领略一番最新的WebGPU的风采。

准备工作

  1. 下载最新版本的Chrome Canary浏览器(这属于常规操作,这里我就不多说了)
  2. 打开浏览器,在地址栏输入 chrome://flags/ 搜索 WebGPU,将WebGPU的功能打开

image.png

至此,我们的准备工作已经全部准备完毕了。

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.requestAdapteradapter.requestDevice 来分别获取adapter和device

adapter

其中adapter是表示了系统底层对WebGPU实现的一个实例。比如说:在Windows平台上,底层的渲染是由D3D12来完成的,WebGPU是构建在D3D12之上的一套API,那么必然会存在一个“适配器”也就是adapter来进行适配,同样的,在Mac平台上,底层的渲染器是由Metal完成,所以同样也需要一个适配器来进行适配。

device

而device则是adapter的逻辑实例对象,我们大部分的操作都需要通过adapter来完成。

WebGPUContext 此处的 WebGPUContextWebGLRenderingContext 不同的是:

  • 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中,分为两个步骤:

  1. “录制”命令,我们可以将我们需要进行的操作进行“录制”,然后将这些命令放入一个队列中。
  2. 提交命令,一旦这些命令被提交到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()]);来完成。

这样就是一个绘制三角形的整个流程了。最终的结果如下:

image.png

着色器语言

现在我们来回顾着色器语言的部分,在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中,有两大类的渲染管线:

  1. GPURenderPipeline,也就是我们最熟悉的用于渲染图形的渲染管线
  2. GPUComputePipeline,用于GPGPU通用计算的渲染管线

对于RenderPipeline,存在两类Stage: vertex shader stagefragment 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程序已经编写完毕了。我们来回顾一下其中的主要内容吧!

  1. WebGPU 的渲染流程大致分为两步:
    • 录制命令(通过创建renderPassEncoder/ computePassEncoder、PipelineStateObject等来录制命令)
    • 提交命令, 一旦提交命令后,GPU就会真正的开始进行渲染工作,在这之前GPU并不会工作,这也方便我们进行多线程的处理。
  2. WebGPU采用了全新的着色器语言(WGSL:WebGPU Shading Language),今天我们只是简单的了解了其中的一些基本语法,WGSL提供了一系列的内置变量,能够实现一些GLSL中不能实现的功能,比如在Shader中指定顶点位置。

OK,今天的内容就这么多,如果你觉得有用的,欢迎点赞并关注我,您的关注就是我更新的动力~