WebGPU教程(说人话)批量绘制几何图形(二)
作者:秋了秋 发表时间:2024年03月13日
上一篇文章《WebGPU教程(说人话)绘制任意形状的2D图形(一)》介绍了绘制任意形状的2D图形,我们总结下,想要绘制任意形状的图形,我们只需要把这个图形的所有顶点数据放到一个数组里面再发给GPU,然后使用WGSL代码对顶点数据处理,配置渲染管线怎么处理数据,最后通过调用draw方法,把这些顶点全部绘制出来。
那么是不是绘制多个图形就要多次走这一套流程呢?很显然不是。GPU绘制是多线程的,如果这样做就失去了使用GPU的意义了,会带来很大的性能消耗。正确的做法应该是把数据都写在顶点数组里面,通过一次draw出来。
比如绘制一个五边形和一个三角形顶点数据是:
const vertexData = new Float32Array([ -0.2,0.5, // 五边形第一个顶点 -0.6,0.4, // 五边形第二个顶点 -0.3,0.2, // 五边形第三个顶点 -0.5, -0.3, // 五边形第四个顶点 -0.1,0, // 五边形第五个顶点 -0.2,0.5, // 闭合,回到起点 // 三角形 0.3, 0.2, // 三角形第一个顶点 0.6,0.1, // 三角形第二个顶点 0.1,-0.3, // 三角形第三个顶点 0.3, 0.2, // 闭合,回到起点 ]);
一共十个顶点,顶点处理函数为:
const shaderCode = ` struct VertexOut { @builtin(position) pos: vec4f, @location(0) color: vec4f } @vertex fn v_main(@location(0) pos:vec2f) -> VertexOut { var out: VertexOut; out.pos = vec4f(pos, 0, 1); out.color = vec4f(1, 0, 0, 1); return out; } @fragment fn f_main(@location(0) c:vec4f) -> @location(0) vec4f { return c; } `;
渲染管线的配置:
const pipeLineConfig ={ ... vertex: { ... buffers: [{ ... arrayStride: 8, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x2' }] }] }, primitive: { topology: 'line-strip' // 连续线条绘制 } }
因为给的顶点数据只有二维坐标,没有颜色信息,所以我们也不需要做颜色解析了,直接使用固定颜色vec4f(1, 0, 0, 1)红色,attributes数组也就只有一项了,arrayStride步进改为8了。
最后通过draw 10次把图形绘制出来。
pass.draw(10);
看看现在出来的图形会很诡异,中间多了一条线,是五边形的结束点和三角形的起始点连了一条线,这是我们所不期望的,问题出在哪里?这是我们的topology设置成了line-strip。
topology有以下几个值: triangle-list, triangle-strip, line-list, line-strip, point-list。这几个值分别对应的模式简单明了的阐述为:
triangle-list:三角形列表,三个点一组三个点一组去渲染,是有填充颜色的,填充颜色是顶点颜色。 例: 给定顶点ABCDEF,GPU会绘制两个三角形ABC、DEF。
triangle-strip:连续的三角形,也是三个点一组三个点一组去渲染,跟triangle-list不同的是接下来绘制三角形的起点是上一次绘制的终点,如果是首次绘制就是数组里面的第一个坐标点 例: 给定顶点ABCDEF,GPU会绘制两个三角形ABC、CDE,F没有找到成立三角形的其它点被舍弃了。
line-list:线段列表,两个点一组两个点一组去渲染,没有填充颜色。例: 给定顶点ABCDEF,GPU会绘制三条线段AB、CD、EF。
line-strip:连续线段列表,也是两个点一组两个点一组去渲染线段,跟line-list不同的是接下来绘制线段的起点是上一次绘制的终点,如果是首次绘制就是数组里面的第一个坐标点。例: 给定顶点ABCDEF,GPU会绘制五条线段AB、BC、CD、DE、EF
point-list: 就单纯把顶点绘制出来,不进行连线等任何操作。
所以针对以上bug我们只需要设置topology: 'line-list' 就行,当你设置了之后发现还是不行,现在发现缺失了一些线条,仔细看我们的顶点数据,结合line-list的说明才恍然大悟,要想通过此模式绘制连续的线条,顶点里面必须增加一些重复的顶点来实现:
const vertexData = new Float32Array([ -0.2,0.5, -0.6,0.4, -0.6,0.4, // 新增 -0.3,0.2, -0.3,0.2, // 新增 -0.5, -0.3, -0.5,- 0.3, // 新增 -0.1,0, -0.1,0, // 新增 -0.2,0.5, // 三角形 0.3, 0.2, 0.6,0.1, 0.6,0.1, // 新增 0.1,-0.3, 0.1,-0.3, // 新增 0.3, 0.2, ]);
绘制16个顶点
pass.draw(16);
现在两个图形终于分开了,两个三个四个更多独立图形,按照此逻辑即可批量进行绘制。
方法二:
如果是绘制多个形状相同的图形呢,只是位置或者颜色有差异,WebGPU给我们提供了绘制实例的方法,解放了定义过多顶点数据问题和定义重复顶点的问题。只需要定义一个图形的所有顶点,draw的第二个参数就是绘制实例值的个数。比如:
const vertexData = new Float32Array([ -0.2,0.5, -0.6,0.4, -0.3,0.2, -0.5, -0.3, -0.1,0, -0.2,0.5 ]);
我们还是可以使用topology: 'line-strip'这样可以少定义一些重复顶点提升性能。然后通过
pass.draw(6, 2)绘制两个实例,第二个参数就是实例的个数。
现在我们看到画布还是只有一个图形,其实是有两个的,它们两个重叠在一起了所以看不出来,如果让实例之间按照规则错开排列,需要使用WGSL语言借助内部变量instance_index来实现:
const shaderCode = ` struct VertexOut { @builtin(position) pos:vec4f, @location(0) color:vec4f } @vertex fn v_main(@location(0) pos:vec2f, @builtin(instance_index) index: u32) -> VertexOut { var out: VertexOut; let i = f32(index); let vecI = vec2f(i / 4, 0); let myPos = pos + vecI; out.pos = vec4f(myPos, 0, 1); out.color = vec4f(1, i / 1.4, i / 1.4, 1); return out; } @fragment fn f_main(@location(0) c:vec4f) -> @location(0) vec4f { return c; } `;
WGSL代码里面的运算跟JS一样,但是要注意的是类型一定要相同才能运算,比如实例类型是无符号32位整型,需要使用f32()函数转成32位有符号浮点数。同时运算都是向量的运算,需要使用vec2f()函数转成二维向量,再通过向量与向量的加法分别对xy做偏移,let vecI = vec2f(i / 4, 0);
let myPos = pos + vecI;表示的就是x移动i/4,y移动0。而i是instance_index演变而来,这是个变量,当绘制第一个实例的时候它是0,绘制第二个实例的时候它是1。除了位置变了颜色也改变了out.color = vec4f(1, i / 1.4, i / 1.4, 1);最终渲染的结果就是
多绘制几个,只需要改变实例个数即可,比如绘制4个:
pass.draw(6, 4);
你学废了吗?