1.WebGPU入门

345 阅读4分钟

前言

WebGPU正式发布也有一段时间了,消息出来时,我一直在观望,试图想看清这门技术的发展趋势和应用长场景,但是,很遗憾,并没有有什么成果。常规的页面,在整个市场的比重都在下降,都侧向App。最近,突然明白了,学习一门技术,最好的时机是十年前,其次,是现在。先学了再说,未来的事情交给未来的自己。

准备工作

需要具备科学上网能力,很多文章都需要查看官方文档。如果没有,那只能等别人更新文章了。

环境

  1. 判断你的浏览器是否支持WebGPU,在浏览器控制台输入:navigator.gpu,查看是否有该对象;
  2. 如果没有确认使用的浏览器是否是:canary版本
  3. 查看浏览器是否允许WebGPU,在地址栏输入:chrome://flags/,搜索:webgpu

Snipaste_2023-06-29_16-12-49.png

  1. 现在,再执行:步骤1,会出现:

Snipaste_2023-06-29_16-17-24.png

现在,你的浏览器支持WebGPU了,可以使用了。

如果这个时候有这个内置对象,但是,写代码的时候,却报错,看看自己的地址是不是localhost

开发工具 && 开发依赖项

  1. 个人采用vscode + vite,各位随意
  2. 安装类型检测(提供语法提示和检测):pnpm i @webgpu/types -D
  3. 添加vite/client类型,便于识别:wgsl(WebGPU Shading Language),gpu识别的命令式语言,不添加则会出现错误提示:

image.png

配置: Snipaste_2023-06-29_16-27-41.png

至此,我们就可以开始写代码了

例子

使用gif,略卡

例子1:

20230629164431_rec_.gif

例子2:

20230629164617_rec_.gif

例子3:

20230629164925_rec_.gif

官方例子(canary版本浏览器):webgpu.github.io/webgpu-samp…

通过这些例子,我们大概可以知道,WebGPU可以做什么,那么,我们也实现一个demo吧。

代码

相关概念

我们也用WebGPU实现一个demo(当然不是上面的例子),是这个例子:

image.png

在此之前,需要了解相关的api以及相关的图形学概念:WebGPU_API

实现

  1. 通过适配器获取device
  if (!navigator.gpu) {
    throw new Error('不支持webGPU')
  }

  // 获取适配器
  const adapter = await navigator.gpu.requestAdapter()

  if (!adapter) {
    throw new Error('获取不到 webGPU 适配器')
  }

  // 通过适配器获取设备
  const device = await adapter.requestDevice()
  1. device 关联到 canvas(渲染的载体),获取关联后的实例:content
const content = canvas.getContext('webgpu') as GPUCanvasContext
content.configure({
    device, // 上文获取到的 device
    format: navigator.gpu.getPreferredCanvasFormat(), // 返回用于当前系统上显示 8 位色深、标准动态范围(SDR)内容的最佳 canvas 纹理格式
    alphaMode: 'premultiplied'
})
  1. 通过 device,创建 pipeline
  const presentationFormat = navigator.gpu.getPreferredCanvasFormat()
  
  const pipeline = await device.createRenderPipelineAsync({
    layout: 'auto',
    vertex: { // 顶点着色器
      module: device.createShaderModule({ code: vertex }), // 通过createShaderModule,将着色器代码传给WebGPU
      entryPoint: 'main' // 上面着色器代码执行的入口函数
    },
    fragment: { // 片元着色器
      module: device.createShaderModule({ code: fragment }),
      entryPoint: 'main',
      targets: [
        { format: presentationFormat }
      ]
    },
    primitive: {
      topology: 'triangle-list'  // 顶点着色器渲染的规则,暂不需要管
    }

其中,vertex和fragment是与wgsl有关联关系的,vertex和fragment的wgsl代码:

@vertex // 表示是vertex代码
fn main( // vertex所有代码的入口函数,在创建pipeline是描述的:entryPoint
  @builtin(vertex_index) VertexIndex : u32
) -> @builtin(position) vec4<f32> {
  var pos = array<vec2<f32>, 3>(
    vec2(0.0, 0.5),
    vec2(-0.5, -0.5),
    vec2(0.5, -0.5)
  );

  return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}
@fragment
fn main() -> @location(0) vec4<f32> {
  return vec4(1.0, 0.0, 0.0, 1.0);
}
  1. pipelinecontent通过device提交给WebGPU:
  const commandEncoder = device.createCommandEncoder() // 创建运行渲染通道
  const textureView = content.getCurrentTexture().createView() // 创建用于WebGPU渲染的纹理视图

  const renderPassDesc: GPURenderPassDescriptor = {
    colorAttachments: [{
      view: textureView,
      clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, // 设置背景画板颜色,如果loadOp !== clear,则忽略
      loadOp: 'clear', // 在执行渲染前的加载操作,建议:clear
      storeOp: 'store' // 在执行渲染后对view的存储操作,store:保留渲染结果; discard: 丢弃
    }]
  }

  const passEncoder = commandEncoder.beginRenderPass(renderPassDesc) // 执行运行通道
  passEncoder.setPipeline(pipeline) // 给通道设置管线
  passEncoder.draw(3, 1, 0, 0) // 绘制的顶点个数、绘制实例的个数、第一个顶点的起始位置、第一个实例的位置
  passEncoder.end() // 结束通道

  device.queue.submit([commandEncoder.finish()]) // 将执行过程提交到队列

代码写完了,看效果(其实没啥看的,上图效果,但是,这里要进行对比):

image.png

这里比较难理解的是passEncoder.draw(3, 1, 0, 0),我们直接更改代码,更改为:passEncoder.draw(6, 1, 0, 0),这里渲染了6个顶点,所以,我们的vertex代码也要改:

@vertex // 表示是vertex代码
fn main( // vertex所有代码的入口函数,在创建pipeline是描述的:entryPoint
  @builtin(vertex_index) VertexIndex : u32
) -> @builtin(position) vec4<f32> {
  var pos = array<vec2<f32>, 6>(
    vec2(0.0, 0.5),
    vec2(-0.5, -0.5),
    vec2(0.5, -0.5),
    
    vec2(0.5, -0.5), // 添加了这三行
    vec2(0.75, 0.5),
    vec2(1, -0.5),
  );

  return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}

查看效果:

image.png

可以看到shader有VertexIndex,实际上是返回一个vec2(x, x),而这里的draw(x),是指执行x次顶点信息,我们将3改成了6,意思是,起了6个线程执行了6次顶点信息,每次的顶点信息,根据VertexIndex的下标返回。

那么,能不能draw(4),输入一个四边形呢?答案是:可以的。需要了解一下:draw(x)primitive: { topology: 'triangle-list' }的topology参数的关系。

实际上draw(x)是起x个线程绘制顶点,topology就是,点和点的关系(是共用点信息,是共用点和点组成的边信息,还是单独绘制的设置),这里就不展开了,感兴趣可以自行了解一下。

至于添加的vec2(x, x),是什么,不在入门篇讲解,在后续篇章分享。至此,我们已实现一个demo(实现代码),已经入门WebGPU了。