第一个最简单的sample,绘制一个三角形。
hello_triangle的效果图。
我们一点一点来拆解这部分的内容。
首先是关于引入的一个第三方库 dist/utils.js
这是由Kai Ninomiya编译的将shader编译成SPIR-V的工具。具体可以参考:https://github.com/kainino0x/shaderc
看git是个狠人啊 -_-!!!
web版在https://github.com/kainino0x/-webgpu-shaderc,另一个repo。
至于SPIR-V的表述:https://blog.csdn.net/cloudqiu/article/details/60334783?utm_source=blogxgwz9
简单说就是编译出来一堆32bit的数据流,而不再是之前的明文shader了。
然后定义了一个600*600的canvas,定死了canvas的尺寸,缩放并不会改变窗口的大小。
<canvas height=600 width=600>canvas>
接下来我们看shader的部分:
const vertexShaderGLSL = `#version 450
const vec2 pos[3] = vec2[3](vec2(0.0f, 0.5f), vec2(-0.5f, -0.5f), vec2(0.5f, -0.5f));
void main() {
gl_Position = vec4(pos[gl_VertexIndex], 0.0, 1.0);
}
`;
const fragmentShaderGLSL = `#version 450
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
这个简单的demo只定义了2个shader,vertex和fragment两个,然后问题就出现了:
webgpu的shader版本是450,参考之前的WebGPU的spec:
interface GPUShaderStageBit {
const u32 NONE = 0;
const u32 VERTEX = 1;
const u32 FRAGMENT = 2;
const u32 COMPUTE = 4;
};
说明WebGPU就支持3种,vertex shader,fragment shader及compute shader,但是glsl 4.5的版本其实还支持 tessellation shader,具体包括 evaluation和control 两部分,此外还有geometry shader,也就是说,WebGPU阉割掉了3个shader模块。
再看shader本身的内容,就比较简单了,先定义了一个const的2维数组,充作三角形的3个顶点,然后通过gl_VertexIndex送入gl_Position,通过pipeline三个点被组合成了一个三角形,然后赋值红色。差不多就这样了。
再来看init函数中的实现,
const adapter = await navigator.gpu.requestAdapter(); //获取GPU适配器
const device = await adapter.requestDevice({}); //获取设备
const canvas = document.querySelector('canvas'); //拿到canvas
const context = canvas.getContext('gpupresent'); // 拿到上下文
const swapChainFormat = "bgra8unorm"; // 定义swapbuffer的格式为RGBA8位的无符号归一化格式
// 定义交换的内容
const swapChain = context.configureSwapChain({
device,
format: swapChainFormat,
});
可以看到整个过程其实和WebGL的设置似乎没有什么本质的区别,如果换作webgl,过程可能更简单些
//通过getElementById()方法获取canvas画布
var canvas=document.getElementById('webgl');
//通过方法getContext()获取WebGL上下文
var gl=canvas.getContext('webgl');
大体初始化的过程换汤不换药,还是比较好理解的。
然后通过API自带的函数,设置device和buffer类型。
设置方式和Spec中给出的接口描述也是一毛一样:
dictionary GPUSwapChainDescriptor {
required GPUDevice device;
required GPUTextureFormat format;
GPUTextureUsageFlags usage = GPUTextureUsage.OUTPUT_ATTACHMENT;
};
然后设置了渲染的流程,pipeline,
pipilne需要实现几个变量的描述符,包括layout,vertexStage,fragmentStage,primitiveTopology,vertexInput,rasterizationState,colorStates
const pipeline = device.createRenderPipeline({
// 没有layout,因为我们并没有送入什么特别的数据,点数据也是由shader自带的const数组而已,所以这里是空
layout: device.createPipelineLayout({ bindGroupLayouts: [] }),
// vertex shader
vertexStage: {
module: device.createShaderModule({
code: Utils.compile("v", vertexShaderGLSL),
}),
entryPoint: "main",
},
// fragment shader
fragmentStage: {
module: device.createShaderModule({
code: Utils.compile("f", fragmentShaderGLSL),
}),
entryPoint: "main"
},
// 绘制图元的类型
primitiveTopology: "triangle-list",
// index的类型
vertexInput: {
indexFormat: "uint32",
},
// 光栅化的设置
rasterizationState: {
frontFace: 'ccw',
cullMode: 'none',
},
// 颜色设置,没有alpha和blend
colorStates: [{
format: swapChainFormat,
alphaBlend: {},
colorBlend: {},
}],
});
到这里大致可以看到,我们要绘制一个三角形,颜色为红色,vertext shader包办了point数据,我们只需要绘制一个三角形即可。
流程和之前webgl的流程本质上没有什么特别的不同,除了需要指出颜色的格式,绘制的顺序(顺时针还是逆时针)
等等一些很细碎的设置之外。
最后我们来看一下frame函数:
function frame() {
// 新建一个commandEncoder,
const commandEncoder = device.createCommandEncoder({});
// 获取当前buffer,textureView
const textureView = swapChain.getCurrentTexture().createDefaultView();
// 创建render pass的描述符,指定操作的buffer,clear颜色,load和store时的操作。
const renderPassDescriptor = {
colorAttachments: [{
loadOp: "clear",
storeOp: "store",
clearColor: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
attachment: textureView,
}],
};
// 开始绘制
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
// 设定绘制流程
passEncoder.setPipeline(pipeline);
// 指定要画什么
passEncoder.draw(3, 1, 0, 0);
// 结束绘制
passEncoder.endPass();
// 送入命令队列
device.getQueue().submit([commandEncoder.finish()]);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
从GPU的角度来看这个问题,我们送如GPU一个命令队列(queue),也就是一堆绘制渲染的命令,这些命令包含几个步骤,
首先我们设置好要绘制的buffer,将它摆到当前(current)的位置,
然后clear一下颜色,清理成黑色,
然后设定好绘制的流程setPipeline,这个流程告诉我改怎么处理送进来的数据。
接着我们启动了draw命令,参考spec里的解释:
void draw(u32 vertexCount, u32 instanceCount, u32 firstVertex, u32 firstInstance);
我们要绘制3个vertex,1个instance,都从0开始,即vertex的index是0-1-2,正好和shader中的3个const vertex对应。
由于我们设置过了绘制的是triangle-list,每三个点被组合成一个三角形,送入fragment shager,加上之前设置过的光栅化设置,最终屏幕上出现了一个红色的三角形。
绘制任务结束。
总结:在WebGL的基础上再看这个流程,基本没有出什么大圈子,但是也能看到设置的过程比之前繁琐了很多,加上shader的版本升级,可以用的shader多了一个compute shader,应该是有非常多的新的特性可以扩展的。继续深入探索,一定可以挖掘GPU更强大的潜力。 下一章继续分析第二个sample-rotating_cube
注:整个samples使用的功能并不能涵盖WebGPU和GLSL4.5所有的内容,解释完所有的samples之后会继续自己写新的例子做些有趣的功能
统一结尾:以上均为个人理解和一家之言,有任何错漏之处欢迎留言讨论,共同进步,一经发现错漏,必立刻更新,且会在修改处指明reporter。谢谢