前言
WebGPU正式发布也有一段时间了,消息出来时,我一直在观望,试图想看清这门技术的发展趋势和应用长场景,但是,很遗憾,并没有有什么成果。常规的页面,在整个市场的比重都在下降,都侧向App。最近,突然明白了,学习一门技术,最好的时机是十年前,其次,是现在。先学了再说,未来的事情交给未来的自己。
准备工作
需要具备科学上网能力,很多文章都需要查看官方文档。如果没有,那只能等别人更新文章了。
环境
- 判断你的浏览器是否支持WebGPU,在浏览器控制台输入:
navigator.gpu,查看是否有该对象; - 如果没有确认使用的浏览器是否是:canary版本
- 查看浏览器是否允许WebGPU,在地址栏输入:
chrome://flags/,搜索:webgpu
- 现在,再执行:步骤1,会出现:
现在,你的浏览器支持WebGPU了,可以使用了。
如果这个时候有这个内置对象,但是,写代码的时候,却报错,看看自己的地址是不是localhost
开发工具 && 开发依赖项
- 个人采用vscode + vite,各位随意
- 安装类型检测(提供语法提示和检测):
pnpm i @webgpu/types -D - 添加
vite/client类型,便于识别:wgsl(WebGPU Shading Language),gpu识别的命令式语言,不添加则会出现错误提示:
配置:
至此,我们就可以开始写代码了
例子
使用gif,略卡
例子1:
例子2:
例子3:
官方例子(canary版本浏览器):webgpu.github.io/webgpu-samp…
通过这些例子,我们大概可以知道,WebGPU可以做什么,那么,我们也实现一个demo吧。
代码
相关概念
我们也用WebGPU实现一个demo(当然不是上面的例子),是这个例子:
在此之前,需要了解相关的api以及相关的图形学概念:WebGPU_API
实现
- 通过适配器获取
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()
- 将
device关联到 canvas(渲染的载体),获取关联后的实例:content
const content = canvas.getContext('webgpu') as GPUCanvasContext
content.configure({
device, // 上文获取到的 device
format: navigator.gpu.getPreferredCanvasFormat(), // 返回用于当前系统上显示 8 位色深、标准动态范围(SDR)内容的最佳 canvas 纹理格式
alphaMode: 'premultiplied'
})
- 通过
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);
}
- 将
pipeline和content通过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()]) // 将执行过程提交到队列
代码写完了,看效果(其实没啥看的,上图效果,但是,这里要进行对比):
这里比较难理解的是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);
}
查看效果:
可以看到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了。