一部2024新的WebGPU教程,作者Shi Yan。内容很好,翻译过来与大家共享,内容上会有改动,加上自己的理解。更多精彩内容尽在 dt.sim3d.cn ,关注公众号【sky的数孪技术】,技术交流、源码下载请添加微信号:digital_twin123
在本教程中,我们将对前面的示例进行另一个细微的修改。我们不是在着色器中硬编码颜色,而是将顶点颜色作为数据传递到着色器中,从而演示更灵活、更真实的 3D 渲染方法。在线示例。
让我们先看一下着色器代码中的更改。除了 @location(0)
处的位置属性之外,我们还在@location(1)
处引入了一个新的输入参数,我们将其命名为inColor
。此 inColor
参数是一个由三个浮点数组成的向量,分别代表颜色的红色、绿色和蓝色分量。在顶点阶段,我们之前对 out.color
字段进行了硬编码,现在我们只需将输入的 inColor
分配给它即可。着色器代码的其余部分保持不变。
此示例旨在说明我们如何使用不同的位置索引传递多个属性,而不仅仅是顶点位置。这是着色器编程中的一个关键概念,允许更复杂和多样化的渲染效果。
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) color: vec3<f32>,
};
@vertex
fn vs_main(
@location(0) inPos: vec3<f32>,
@location(1) inColor: vec3<f32>
) -> VertexOutput {
var out: VertexOutput;
out.clip_position = vec4<f32>(inPos, 1.0);
out.color = inColor;
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(in.color, 1.0);
}
通过为不同的顶点分配不同的颜色,我们现在可以解决之前提出的问题:如何为位于三角形中间的那些片段生成片段颜色?此设置将使我们能够观察 GPU 的颜色插值操作,提供渲染管线的顶点阶段和片段阶段之间如何处理数据的可视化演示。
现在,让我们探索如何为新着色器设置管线。新管线与前一个管线非常相似,主要区别在于我们现在需要创建一个包含所有顶点颜色的颜色缓冲区并将其输入到管线中。
这里所涉及的步骤密切反映了我们处理位置缓冲区的方式。首先,我们创建一个颜色属性描述符。请注意,我们将 ShaderLocation
设置为 1,对应于着色器中 @location(1)
处的 inColor
属性。颜色属性的格式仍然是三个浮点数的向量。
const colorAttribDesc = {
shaderLocation: 1, // @location(1)
offset: 0,
format: 'float32x3'
};
const colorBufferLayoutDesc = {
attributes: [colorAttribDesc],
arrayStride: 4 * 3, // sizeof(float) * 3
stepMode: 'vertex'
};
……
const colors = new Float32Array([
1.0,
0.0,
0.0, // 🔴
0.0,
1.0,
0.0, // 🟢
0.0,
0.0,
1.0 // 🔵
]);
let colorBuffer = createGPUBuffer(device, colors, GPUBufferUsage.VERTEX);
接下来,我们创建缓冲区布局描述符,它通知管线如何解释每个顶点的颜色缓冲区。我们将颜色属性描述符分配给属性字段。 arrayStride
设置为 4 * 3,因为一个浮点数占用 4 个字节,每种颜色有 3 个浮点数。 stepMode
设置为顶点,因为每个顶点都有一种颜色。
使用 Float32Array 在 CPU 内存中定义 RGB 数据后(第一个顶点为红色,第二个顶点为绿色,第三个顶点为蓝色),我们继续创建 GPU 缓冲区并将数据复制到 GPU。
让我们回顾一下创建和填充 GPU 缓冲区的过程:
- 定义一个缓冲区描述符,指定缓冲区大小并将使用标志设置为
VERTEX
,因为我们将在顶点阶段使用颜色属性。 - 将
mappedAtCreation
设置为true,允许在创建缓冲区时立即复制数据。 - 创建 GPU 缓冲区,并使用映射的缓冲区范围在 CPU 内存中创建镜像缓冲区。
- 将颜色数据复制到该映射缓冲区中。
- 最后,取消映射缓冲区,表明数据传输已完成。
在我们的示例代码中,你可能会注意到我们没有明确看到这些步骤。这是因为这个过程是一个常见过程,我们需要在整个 WebGPU 程序中多次执行。为了简化我们的代码并减少重复,我创建了一个实用函数来封装这些步骤。
如前所述,WebGPU 的语法可能相当冗长。将通用代码块包装到可重用实用函数中通常是一个好习惯。这种方法不仅减少了我们的工作量,而且使我们的代码更具可读性和可维护性。
我创建的 createGPUBuffer
函数将所有这些步骤封装到一个可重用的函数中。它的定义如下:
function createGPUBuffer(device, buffer, usage) {
const bufferDesc = {
size: buffer.byteLength,
usage: usage,
mappedAtCreation: true
};
//console.log('buffer size', buffer.byteLength);
let gpuBuffer = device.createBuffer(bufferDesc);
if (buffer instanceof Float32Array) {
const writeArrayNormal = new Float32Array(gpuBuffer.getMappedRange());
writeArrayNormal.set(buffer);
}
else if (buffer instanceof Uint16Array) {
const writeArrayNormal = new Uint16Array(gpuBuffer.getMappedRange());
writeArrayNormal.set(buffer);
}
else if (buffer instanceof Uint8Array) {
const writeArrayNormal = new Uint8Array(gpuBuffer.getMappedRange());
writeArrayNormal.set(buffer);
}
else if (buffer instanceof Uint32Array) {
const writeArrayNormal = new Uint32Array(gpuBuffer.getMappedRange());
writeArrayNormal.set(buffer);
}
else {
const writeArrayNormal = new Float32Array(gpuBuffer.getMappedRange());
writeArrayNormal.set(buffer);
console.error("Unhandled buffer format ", typeof gpuBuffer);
}
gpuBuffer.unmap();
return gpuBuffer;
}
此时,我们已成功复制 GPU 上的颜色值,准备在着色器中使用。
const pipelineDesc = {
layout,
vertex: {
module: shaderModule,
entryPoint: 'vs_main',
buffers: [positionBufferLayoutDesc, colorBufferLayoutDesc]
},
fragment: {
module: shaderModule,
entryPoint: 'fs_main',
targets: [colorState]
},
primitive: {
topology: 'triangle-list',
frontFace: 'cw',
cullMode: 'back'
}
};
pipeline = device.createRenderPipeline(pipelineDesc);
…………
commandEncoder = device.createCommandEncoder();
passEncoder = commandEncoder.beginRenderPass(renderPassDesc);
passEncoder.setViewport(0, 0, canvas.width, canvas.height, 0, 1);
passEncoder.setPipeline(pipeline);
passEncoder.setVertexBuffer(0, positionBuffer);
passEncoder.setVertexBuffer(1, colorBuffer);
passEncoder.draw(3, 1);
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);
创建缓冲区后,我们来定义管线描述符。与我们之前的示例的主要区别是在顶点阶段的缓冲区列表中添加了 colorBufferLayoutDescriptor
。这可以通知管线我们现在使用两个顶点缓冲区:一个用于位置,另一个用于颜色。
在编码渲染命令时,我们现在需要设置两个顶点缓冲区。我们使用 setVertexBuffer(0,positionBuffer)
作为位置数据,使用 setVertexBuffer(1, colorBuffer)
作为颜色数据。索引 0 和 1 对应于定义管道描述符时的缓冲区布局。渲染过程的其余部分基本保持不变。
运行此代码后,我们会看到一个视觉上有趣的结果:一个彩色三角形。每个顶点都以其指定的颜色渲染 - 红色、绿色和蓝色。然而,最有趣的方面是这些顶点之间发生的情况,我们可以观察到三角形表面颜色的平滑过渡。
这种自动颜色混合是 GPU 执行的一项功能,我们将这个过程称为插值。值得注意的是,这种插值不仅限于颜色,我们在顶点阶段输出的任何值都将由 GPU 进行插值,以便为每个片段分配适当的值,特别是对于那些不直接位于顶点的片段。
片段值的插值是根据其到顶点的相对距离,遵循双线性方案来计算的。这种机制非常有用,因为考虑到场景中的片段通常远多于顶点,单独为所有片段指定值是不切实际的,所以我们可以依靠 GPU 根据仅在每个顶点定义的值有效地生成这些值。
这种插值技术是计算机图形学中的基本概念,能够以最少的输入数据实现表面上的平滑过渡和渐变。