在 WebGPU 中使用 MSAA
前言 & 上一篇文章入口:juejin.cn/post/722479…
WebGPU 的第二个示例乍一看似乎和第一个示例没有区别,但如果将屏幕通过 ctrl + 滚轮放大的话,对比看的话可以发现,第一个示例的三角形边缘有非常明显的锯齿,而第二个示例则相对好了许多。这是因为第二个示例采用了 MSAA 技术。
MSAA (MultiSample Anti-Aliasing):多重采样,假设采用的是 4X 多重采样(即每个像素中都有四个子像素),那么就会使用 4 倍于屏幕分辨率的后台缓冲区,最后计算像素颜色的时候对该像素的四个子像素的颜色进行加权平均。比如在绿色的三角形边缘有一个像素,它的两个子像素是黑色(clearValue),另外两个是绿色,那么最终得到的颜色值就是 rgb(0, 0, 0.5)。
对比第一个示例和第二个示例可以发现在 WebGPU 中使用 MSAA 非常简单,只需要修改几行代码即可实现
multisample: { // 在流水线配置参数中添加
count: 4
},
const texture = device.createTexture({
size: [canvas.width, canvas.height], // 表示渲染目标纹理的大小
sampleCount: 4, // 指定使用 4x 多重采样,一般都是采用 4
format: presentationFormat,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
const view = texture.createView();
const renderPassDescriptor: GPURenderPassDescriptor = {
colorAttachments: [
{
view, // view 不再指定为当前的后台缓存区,而是提前创建好的纹理视图
resolveTarget: context.getCurrentTexture().createView(), // 新加 resolveTarget 指向后台缓存区纹理视图
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
loadOp: 'clear',
storeOp: 'discard', // storeOp 指定为丢弃
},
],
};
js 向 shader 传值
在第一个示例中,我们的绿色三角形的三个顶点的位置和颜色都是写死在着色器语言中的,这非常的不方便。假如这个三角形一会儿是绿色,一会儿又是红色,一会儿又变成五彩斑斓,总不能每种颜色的三角形都去单独去写一份片元着色器吧。所以,我们需要想个办法将 js 中的变量传递到 shader 中,这样每当在 js 代码中修改这个变量时,shader 代码就能根据这个 js 代码渲染出不同的效果。
这部分的内容在官方的第四个示例中,虽然效果很好很炫酷,但其中一下子出现了比较多的新概念,对于我这种初学者来说不太友好。所以,让我们循序渐进,通过 js 向 shader 传值写一个三角形吧。
第一步:创建 GPUBuffer 对象
// eslint-disable-next-line @typescript-eslint/no-var-requires, prettier/prettier
const triangleVertices = new Float32Array([
0.0, 0.5,
-0.5, -0.5,
0.5, -0.5
]);
const verticesBuffer = device.createBuffer({
size: triangleVertices.byteLength,
usage: GPUBufferUsage.VERTEX,
/**
* If `true` creates the buffer in an already mapped state, allowing
* {@link GPUBuffer#getMappedRange} to be called immediately. It is valid to set
* {@link GPUBufferDescriptor#mappedAtCreation} to `true` even if {@link GPUBufferDescriptor#usage}
* does not contain {@link GPUBufferUsage#MAP_READ} or {@link GPUBufferUsage#MAP_WRITE}. This can be
* used to set the buffer's initial data.
* Guarantees that even if the buffer creation eventually fails, it will still appear as if the
* mapped range can be written/read to until it is unmapped.
*/
mappedAtCreation: true,
});
new Float32Array(verticesBuffer.getMappedRange()).set(triangleVertices);
/**
* Unmaps the mapped range of the {@link GPUBuffer} and makes it's contents available for use by the
* GPU again.
*/
verticesBuffer.unmap();
看到数组里的值是不是非常熟悉?没错,这三个顶点坐标就是第一个示例中写死在顶点着色器中的坐标,此外还不需用 Float32Array 构造函数来创建一个强类型的顶点数组,因为 shader 不认识 js 中 number 类型(可以这样理解吧)。
随后就是调用 device.createBuffer 来创建 GPUBuffer 对象啦,size 就是顶点数组的字节长度,至于 usage 则表明该 GPUBuffer 是作为顶点数据使用的,另外 usage 是不是感觉也似曾相识,在 MSAA 示例中使用 device.createTexture 方法创建纹理对象的时候也指定了一个 usage: RENDER_ATTACHMENT,这个纹理对象在示例中是作为渲染目标使用的。那我们就可以推断总结出一个规律:纹理对象也好,GPUBuffer也好,都是会被 GPU 使用的资源,在创建这些资源的时候必须指定它的用途。最后 mappedAtCreation 参考注释。
创建完 GPUBuffer 对象后再把顶点数组设置上去,最后 unmap 一下使 GPU 能够使用。
第二步:告诉着色器我要给你传 GPUBuffer 啦
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: device.createShaderModule({
code: triangleVertWGSL,
}),
buffers: [
{
arrayStride: 4 * 2,
attributes: [
{
// position
shaderLocation: 0,
offset: 0,
format: 'float32x2',
},
],
},
],
entryPoint: 'main',
},
fragment: {
module: device.createShaderModule({
code: redFragWGSL,
}),
entryPoint: 'main',
targets: [
{
format: presentationFormat,
},
],
},
primitive: {
topology: 'triangle-list',
},
});
上一篇文章中说过,着色器是作为模块绑定到渲染流水线上的,而刚刚创建的 GPUBuffer 很明显是要把数据传到顶点着色器中去。所以,我们需要在渲染流水线的顶点着色器描述告知着色器这一信息。
非常简单,只是多了一个 buffers 数组用于描述我们的 GPUBuffer 对象,arrayStride 表明我们的一个顶点的字节长度(float32 的字节长度是 4,一个顶点由两个数组成),attributes 中的 shaderLocation 需要和顶点着色器中的地址一一对应,我们先指定为 0,offset 也先指定为 0(等会不为 0 的时候再讲解),format 就表示该属性的格式。
第三步:修改顶点着色器用于接收 GPUBuffer
@vertex
fn main(
@location(0) position : vec2<f32>,
) -> @builtin(position) vec4<f32> {
return vec4<f32>(position, 0.0, 1.0);
}
将 main 函数的参数修改为 @location(0) postion: vec2,非常容易理解,0 就是创建渲染流水线中的shaderLocation,而 vec2 就是顶点属性类型,这两者都需要一一对应,否则 GPUBuffer 无法传入到顶点着色器中,返回值直接将 position 构造成 vec 返回即可。
第四步:在渲染函数中将 GPUBuffer 传给着色器
passEncoder.setVertexBuffer(0, verticesBuffer);
注意在 draw 函数调用之前设置即可。
运行,观看效果发现熟悉的绿色三角形又回来了。
第五步:将顶点颜色值也传给 顶点着色器
const triangleVertices = new Float32Array([ 0.0, 0.5, 1.0, 0.0, 0.0, -0.5, -0.5, 0.0, 1.0, 0.0, 0.5, -0.5, 0.0, 0.0, 1.0,]);
// createRenderPipeline 中 vertex
buffers: [
{
arrayStride: 4 * 5,
attributes: [
{
// position
shaderLocation: 0,
offset: 0,
format: 'float32x2',
},
{
// color
shaderLocation: 1,
offset: 4 * 2,
format: 'float32x3',
},
],
},
],
顶点位置成功传递,顶点颜色在顶点位置的基础上扩展即可。首先,在每个顶点坐标后跟上一个颜色值,例如上面的代码就是分别把红绿蓝三种颜色给到一个顶点,然后就是修改buffers[0] 的 arrayStride,现在一个顶点已经有 5个 f32 了,其次这个 5 个 f32 表示了位置和颜色两个属性,所以我们需要在 attributes 中清楚地描述出来,每个属性都占用一个不同的 shaderLocation,另外还需要注意 color 属性是从第三个数开始的,所以它需要偏移 8 个字节。好了,顶点颜色成功添加进入 GPUBuffer 啦,现在再修改一下着色器就可以了。
struct VertexOutput {
@builtin(position) Position : vec4<f32>,
@location(0) color: vec4<f32> // 和片元着色器的入参一一对应
}
@vertex
fn main(
@location(0) position : vec2<f32>, // 和main.ts 中的shaderLocation 一一对应
@location(1) color : vec3<f32>
) -> VertexOutput {
var output : VertexOutput;
output.Position = vec4<f32>(position, 0.0, 1.0);
output.color = vec4<f32>(color, 1.0);
return output;
}
@fragment
fn main(
@location(0) color: vec4<f32>,
) -> @location(0) vec4<f32> {
return color;
}
由于我们的颜色数据会随着 GPUBuffer 到达顶点着色器,所以我们需要通过顶点着色器再把颜色值交给片元着色器,又由于顶点着色器必须输出每一个顶点的位置坐标,所以需要定义一个带有位置和颜色信息的结构体。其中 @buildin(position) 就是内建位置地址,color 就是我们自定义的输出地址,需要和片元着色器中的输入地址一一对应。大概注意一下这些细节,着色器代码还是挺好理解的。
好啦,接下来就是见证奇迹的时刻,ctrl + s 保存,回到浏览器,睁开眼,大功告成!
第六步:通过 js 实时修改 GPUBuffer 中的值并传给着色器
const verticesBuffer = device.createBuffer({
size: triangleVertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
mappedAtCreation: true,
});
为了能给向顶点 GPUBuffer 中实时写入数据,我们需要修改它的 usage,现在这个 verticesBuffer 就有两个作用了,一个是作为顶点buffer 传值给顶点着色器,一个是作为可写入的普通缓存。
const rand = () => Math.random() * 2 - 1;
const randArr = new Float32Array(new Array(15).fill(0).map(() => rand()));
device.queue.writeBuffer(
verticesBuffer,
0,
randArr.buffer,
randArr.byteOffset,
randArr.byteLength
);
随后我们就可以在 js 帧循环中不断向这个 GPUBuffer 中写入不同的数据了,要记得在 draw 函数调用之前。看到上面的代码大火应该能猜出是什么效果了。
学会了使用 js 向 shader 传值,其实就可以实现许多有意思的想法了,发挥自己的创造力,然后去实现它!