在上一篇文章中,我们将顶点数据放在存储缓冲区中,并使用内置的 vertex_index 对其进行索引。虽然该技术越来越受欢迎,但向顶点着色器提供顶点数据的传统方法是通过顶点缓冲区和属性。
顶点缓冲区就像任何其他 WebGPU 缓冲区一样。也用来保存数据。不同之处在于不直接从顶点着色器访问它们。相反,我们告诉 WebGPU 缓冲区中有什么类型的数据以及它在哪里以及它是如何组织的。然后它将数据从缓冲区中拉出并提供给我们。
让我们以上一篇文章中的最后一个示例为例,将其从使用存储缓冲区更改为使用顶点缓冲区。
要做的第一件事是更改着色器以从顶点缓冲区获取其顶点数据。
struct OurStruct {
color: vec4f,
offset: vec2f,
};
struct OtherStruct {
scale: vec2f,
};
//new
struct Vertex {
@location(0) position: vec2f,
};
struct VSOutput {
@builtin(position) position: vec4f,
@location(0) color: vec4f,
};
@group(0) @binding(0) var<storage, read> ourStructs: array<OurStruct>;
@group(0) @binding(1) var<storage, read> otherStructs: array<OtherStruct>;
// @group(0) @binding(2) var<storage, read> pos: array<Vertex>;
@vertex fn vs(
//@builtin(vertex_index) vertexIndex : u32,
//vert: Vertex,
@builtin(instance_index) instanceIndex: u32
) -> VSOutput {
let otherStruct = otherStructs[instanceIndex];
let ourStruct = ourStructs[instanceIndex];
var vsOut: VSOutput;
vsOut.position = vec4f(
// pos[vertexIndex].position * otherStruct.scale + ourStruct.offset, 0.0, 1.0);
vert.position * otherStruct.scale + ourStruct.offset, 0.0, 1.0);
vsOut.color = ourStruct.color;
return vsOut;
}
...
如您所见,这是一个很小的变化。我们声明了一个结构 Vertex 来定义顶点的数据。重要的部分是用 @location(0) 声明位置字段
然后,当我们创建渲染管道时,我们必须告诉 WebGPU 如何为 @location(0) 获取数据
const pipeline = device.createRenderPipeline({
label: 'vertex buffer pipeline',
layout: 'auto',
vertex: {
module,
entryPoint: 'vs',
buffers: [ //new
{
arrayStride: 2 * 4, // 2 floats, 4 bytes each
attributes: [
{
shaderLocation: 0, offset: 0, format: 'float32x2'}, // position
],
},
],
},
fragment: {
module,
entryPoint: 'fs',
targets: [{
format: presentationFormat }],
},
});
To the vertex entry of the pipeline descriptor we added a buffers array which is used to describe how to pull data out of a 1 or more vertex buffers. For the first and only buffer, we set an arrayStride in number of bytes. a stride in this case is how many bytes to get from the data for one vertex in the buffer, to the next vertex in the buffer.
在 pipeline 描述符的 vertex 条目中,我们添加了一个 buffers 数组,用于描述如何从一个或多个顶点缓冲区中提取数据。对于第一个也是唯一一个缓冲区,我们设置了一个 arrayStride 字节数。在这种情况下,步长是从缓冲区中一个顶点的数据到缓冲区中的下一个顶点的数据的字节数。
由于我们的数据是 vec2f ,它是两个 float32 数字,我们将 arrayStride 设置为 8。
Next we define an array of attributes. We only have one. shaderLocation: 0 corresponds to location(0) in our Vertex struct. offset: 0 says the data for this attribute starts at byte 0 in the vertex buffer. Finally format: ‘float32x2’ says we want WebGPU to pull the data out of the buffer as two 32bit floating point numbers.
接下来我们定义一个属性数组。我们只有一个。 shaderLocation: 0 对应于 Vertex 结构中的 location(0) 。 offset: 0 表示此属性的数据从顶点缓冲区中的字节 0 开始。最后 format: ‘float32x2’ 说我们希望 WebGPU 将数据作为两个 32 位浮点数从缓冲区中取出。
我们需要将保存顶点数据的缓冲区的用法从 STORAGE 更改为 VERTEX ,并将其从绑定组中删除。
// const vertexStorageBuffer = device.createBuffer({
// label: 'storage buffer vertices',
// size: vertexData.byteLength,
// usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
// });
const vertexBuffer = device.createBuffer({
label: 'vertex buffer vertices',
size: vertexData.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(vertexBuffer, 0, vertexData);
const bindGroup = device.createBindGroup({
label: 'bind group for objects',
layout: pipeline.getBindGroupLayout(0),
entries: [
{
binding: 0, resource: {
buffer: staticStorageBuffer }},
{
binding: 1, resource: {
buffer: changingStorageBuffer }},
// { binding: 2, resource: { buffer: vertexStorageBuffer }},
],
});
然后在绘制时我们需要告诉 webgpu 使用哪个顶点缓冲区
pass.setPipeline(pipeline);
pass.setVertexBuffer(0, vertexBuffer);
这里的 0 对应于我们上面指定的渲染管线 buffers 数组的第一个元素。
我们已经从使用顶点存储缓冲区切换到顶点缓冲区。
执行绘图命令时的状态看起来像这样
attribute format 字段可以是这些类型之一
Vertex format | Data type | Components | Byte size | Example WGSL type |
---|---|---|---|---|
"uint8x2" |
unsigned int | 2 | 2 | vec2<u32> |
"uint8x4" |
unsigned int | 4 | 4 | vec4<u32> |
"sint8x2" |
signed int | 2 | 2 | vec2<i32> |
"sint8x4" |
signed int | 4 | 4 | vec4<i32> |
"unorm8x2" |
unsigned normalized | 2 | 2 | vec2<f32> |
"unorm8x4" |
unsigned normalized | 4 | 4 | vec4<f32> |
"snorm8x2" |
signed normalized | 2 | 2 | vec2<f32> |
"snorm8x4" |
signed normalized | 4 | 4 | vec4<f32> |
"uint16x2" |
unsigned int | 2 | 4 | vec2<u32> |
"uint16x4" |
unsigned int | 4 | 8 | vec4<u32> |
"sint16x2" |
signed int | 2 | 4 | vec2<i32> |
"sint16x4" |
signed int | 4 | 8 | vec4<i32> |
"unorm16x2" |
unsigned normalized | 2 | 4 | vec2<f32> |
"unorm16x4" |
unsigned normalized | 4 | 8 | vec4<f32> |
"snorm16x2" |
signed normalized | 2 | 4 | vec2<f32> |
"snorm16x4" |
signed normalized | 4 | 8 | vec4<f32> |
"float16x2" |
float | 2 | 4 | vec2<f16> |
"float16x4" |
float | 4 | 8 | vec4<f16> |
"float32" |
float | 1 | 4 | f32 |
"float32x2" |
float | 2 | 8 | vec2<f32> |
"float32x3" |
float | 3 | 12 | vec3<f32> |
"float32x4" |
float | 4 | 16 | vec4<f32> |
"uint32" |
unsigned int | 1 | 4 | u32 |
"uint32x2" |
unsigned int | 2 | 8 | vec2<u32> |
"uint32x3" |
unsigned int | 3 | 12 | vec3<u32> |
"uint32x4" |
unsigned int | 4 | 16 | vec4<u32> |
"sint32" |
signed int | 1 | 4 | i32 |
"sint32x2" |
signed int | 2 | 8 | vec2<i32> |
"sint32x3" |
signed int | 3 | 12 | vec3<i32> |
"sint32x4" |
signed int | 4 | 16 | vec4<i32> |
让我们为颜色添加第二个属性。首先让我们改变着色器
struct OurStruct {
color: vec4f,
offset: vec2f,
};
struct OtherStruct {
scale: vec2f,
};
struct Vertex {
@location(0) position: vec2f,
@location(1) color: vec3f, //new
};
struct VSOutput {
@builtin(position) position: vec4f,
@location(0) color: vec4f,
};
@group(0) @binding(0) var<storage, read> ourStructs: array<OurStruct>;
@group(0) @binding(1) var<storage, read> otherStructs: array<OtherStruct>;
@vertex fn vs(
vert: Vertex,
@builtin(instance_index) instanceIndex: u32
) -> VSOutput {
let otherStruct = otherStructs[instanceIndex];
let ourStruct = ourStructs[instanceIndex];
var vsOut: VSOutput;
vsOut.position = vec4f(
vert.position * otherStruct.scale + ourStruct.offset, 0.0, 1.0);
// vsOut.color = ourStruct.color;
vsOut.color = ourStruct.color * vec4f(vert.color, 1);
return vsOut;
}
然后我们需要更新管道来描述将如何提供数据。将像这样交错数据
因此, arrayStride 需要更改以覆盖新数据,并且需要添加新属性。它在两个 32 位浮点数之后开始,所以它进入缓冲区的 offset 是 8 个字节。
const pipeline = device.createRenderPipeline({
label: '2 attributes',
layout: 'auto',
vertex: {
module,
entryPoint: 'vs',
buffers: [
{
arrayStride: (2 + 3) * 4, // (2 + 3) floats, 4 bytes each
attributes: [
{
shaderLocation: 0, offset: 0, format: 'float32x2'}, // position
{
shaderLocation: 1, offset: 8, format: 'float32x3'}, // color , new
],
},
],
},
fragment: {
module,
entryPoint: 'fs',
targets: [{
format: presentationFormat }],
},
});
我们将更新生成圆顶点代码,为圆外缘的顶点提供深色,为内部顶点提供浅色。
function createCircleVertices({
radius = 1,
numSubdivisions = 24,
innerRadius = 0,
startAngle = 0,
endAngle = Math.PI * 2,
} = {
}) {
// 2 triangles per subdivision, 3 verts per tri, 5 values (xyrgb) each.
const numVertices = numSubdivisions * 3 * 2;
//const vertexData = new Float32Array(numVertices * 2);
const vertexData = new Float32Array(numVertices * (2 + 3));
let offset = 0;
// const addVertex = (x, y, r, g, b) => {
const addVertex = (x, y, r, g, b) => {
vertexData[offset++] = x;
vertexData[offset++] = y;
vertexData[offset++] = r;
vertexData[offset++] = g;
vertexData[offset++] = b;
};
const innerColor = [1, 1, 1];
const outerColor = [0.1, 0.1, 0.1];
// 2 vertices per subdivision
//
// 0--1 4
// | / /|
// |/ / |
// 2 3--5
for (let i = 0; i < numSubdivisions; ++i) {
const angle1 = startAngle + (i + 0) * (endAngle - startAngle) / numSubdivisions;
const angle2 = startAngle + (i + 1) * (endAngle - startAngle) / numSubdivisions;
const c1 = Math.cos(angle1);
const s1 = Math.sin(angle1);
const c2 = Math.cos(angle2);
const s2 = Math.sin(angle2);
// first triangle
//addVertex(c1 * radius, s1 * radius);
//addVertex(c2 * radius, s2 * radius);
//addVertex(c1 * innerRadius, s1 * innerRadius);
addVertex(c1 * radius, s1 * radius, ...outerColor);
addVertex(c2 * radius, s2 * radius, ...outerColor);
addVertex(c1 * innerRadius, s1 * innerRadius, ...innerColor);
// second triangle
//addVertex(c1 * innerRadius, s1 * innerRadius);
//addVertex(c2 * radius, s2 * radius);
//addVertex(c2 * innerRadius, s2 * innerRadius);
addVertex(c1 * innerRadius, s1 * innerRadius, ...innerColor);
addVertex(c2 * radius, s2 * radius, ...outerColor);
addVertex(c2 * innerRadius, s2 * innerRadius, ...innerColor);
}
return {
vertexData,
numVertices,
};
}
这样我们就得到了阴影圆圈
Note that we don’t have to use a struct. This would work just as well
请注意,我们不必使用结构。这也可以
@vertex fn vs(
//vert: Vertex,
@location(0) position: vec2f,
@location(1) color: vec3f,
@builtin(instance_index) instanceIndex: u32
) -> VSOutput {
let otherStruct = otherStructs[instanceIndex];
let ourStruct = ourStructs[instanceIndex];
var vsOut: VSOutput;
vsOut.position = vec4f(
// vert.position * otherStruct.scale + ourStruct.offset, 0.0, 1.0);
// vsOut.color = ourStruct.color * vec4f(vert.color, 1);
position * otherStruct.scale + ourStruct.offset, 0.0, 1.0);
vsOut.color = ourStruct.color * vec4f(color, 1);
return vsOut;
}
我们还可以在单独的缓冲区中提供数据。着色器中没有任何变化。相反,我们只是更新管道
buffers: [
{
// arrayStride: (2 + 3) * 4, // (2 + 3) floats, 4 bytes each
arrayStride: 2 * 4, // 2 floats, 4 bytes each
attributes: [
{
shaderLocation: 0, offset: 0, format: 'float32x2'}, // position
// {shaderLocation: 1, offset: 8, format: 'float32x3'}, // color
],
},
{
arrayStride: 3 * 4, // 3 floats, 4 bytes each
attributes: [
{
shaderLocation: 1, offset: 0, format: 'float32x3'}, // color
],
},
],
当然我们需要分离顶点数据。
function createCircleVertices({
radius = 1,
numSubdivisions = 24,
innerRadius = 0,
startAngle = 0,
endAngle = Math.PI * 2,
} = {
}) {
// 2 triangles per subdivision, 3 verts per tri, 5 values (xyrgb) each.
const numVertices = numSubdivisions * 3 * 2;
//const vertexData = new Float32Array(numVertices * (2 + 3));
//let offset = 0;
//const addVertex = (x, y, r, g, b) => {
// vertexData[offset++] = x;
// vertexData[offset++] = y;
// vertexData[offset++] = r;
// vertexData[offset++] = g;
// vertexData[offset++] = b;
//};
// 2 triangles per subdivision, 3 verts per tri, 5 values (xy) and (rgb) each.
const numVertices = numSubdivisions * 3 * 2;
const positionData = new Float32Array(numVertices * 2);
const colorData = new Float32Array(numVertices * 3);
let posOffset = 0;
let colorOffset = 0;
const addVertex = (x, y, r, g, b) => {
positionData[posOffset++] = x;
positionData[posOffset++] = y;
colorData[colorOffset++] = r;
colorData[colorOffset++] = g;
colorData[colorOffset++] = b;
};
...
return {
// vertexData,
positionData,
colorData,
numVertices,
};
}
并创建 2 个缓冲区而不是 1 个
//const { vertexData, numVertices } = createCircleVertices({
const {
positionData, colorData, numVertices } = createCircleVertices({
radius: 0.5,
innerRadius: 0.25,
});
// const vertexBuffer = device.createBuffer({
// label: 'vertex buffer vertices',
// size: vertexData.byteLength,
// usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
// });
// device.queue.writeBuffer(vertexBuffer, 0, vertexData);
const positionBuffer = device.createBuffer({
label: 'position buffer',
size: positionData.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(positionBuffer, 0, positionData);
const colorBuffer = device.createBuffer({
label: 'color buffer',
size: colorData.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(colorBuffer, 0, colorData);
And then at render time we need to specify the both buffers
然后在渲染时我们需要指定两个缓冲区
pass.setPipeline(pipeline);
// pass.setVertexBuffer(0, vertexBuffer);
pass.setVertexBuffer(0, positionBuffer);
pass.setVertexBuffer(1, colorBuffer);
就像我们将第一个统一缓冲区分成 2 个统一缓冲区一样,您可能希望将顶点数据分成 2 个缓冲区的一个原因是,如果某些顶点数据是静态的,而其他顶点数据经常更新。
Index Buffers
这里要介绍的最后一件事是索引缓冲区。索引缓冲区描述了处理和使用顶点的顺序。
You can think of draw as going through the vertices in order
您可以将 draw 视为按顺序遍历顶点
0, 1, 2, 3, 4, 5, .....
使用索引缓冲区,我们可以更改该顺序。
我们为每个圆的细分创建了 6 个顶点,尽管其中 2 个是相同的。
Now instead, we’ll only create 4 but then use indices to use those 4 vertices 6 times by telling WebGPU to draw indices in this order
现在,我们将只创建 4 个,然后通过告诉 WebGPU 按此顺序绘制索引,使用索引来使用这 4 个顶点 6 次
0, 1, 2, 2, 1, 3, ...
function createCircleVertices({
radius = 1,
numSubdivisions = 24,
innerRadius = 0,
startAngle = 0,
endAngle = Math.PI * 2,
} = {
}) {
// 2 triangles per subdivision, 3 verts per tri, 5 values (xyrgb) each.
const numVertices = numSubdivisions * 3 * 2;
const numVertices = (numSubdivisions + 1) * 2;
const vertexData = new Float32Array(numVertices * (2 + 3));
let offset = 0;
const addVertex = (x, y, r, g, b) => {
vertexData[offset++] = x;
vertexData[offset++] = y;
vertexData[offset++] = r;
vertexData[offset++] = g;
vertexData[offset++] = b;
};
const innerColor = [1, 1, 1];
const outerColor = [0.1, 0.1, 0.1];
// 2 vertices per subdivision
//
// 0 2 4 6 8 ...
//
// 1 3 5 7 9 ...
for (let i = 0; i <= numSubdivisions; ++i) {
const angle = startAngle + (i + 0) * (endAngle - startAngle) / numSubdivisions;
const c1 = Math.cos(angle);
const s1 = Math.sin(angle);
addVertex(c1 * radius, s1 * radius, ...outerColor);
addVertex(c1 * innerRadius, s1 * innerRadius, ...innerColor);
}
const indexData = new Uint32Array(numSubdivisions * 6);
let ndx = 0;
// 0---2---4---...
// | //| //|
// |// |// |//
// 1---3-- 5---...
for (let i = 0; i < numSubdivisions; ++i) {
const ndxOffset = i * 2;
// first triangle
indexData[ndx++] = ndxOffset;
indexData[ndx++] = ndxOffset + 1;
indexData[ndx++] = ndxOffset + 2;
// second triangle
indexData[ndx++] = ndxOffset + 2;
indexData[ndx++] = ndxOffset + 1;
indexData[ndx++] = ndxOffset + 3;
}
return {
vertexData,
indexData,
numVertices: indexData.length,
};
}
然后我们需要创建一个索引缓冲区
// const { vertexData, numVertices } = createCircleVertices({
const {
vertexData, indexData, numVertices } = createCircleVertices({
radius: 0.5,
innerRadius: 0.25,
});
const vertexBuffer = device.createBuffer({
label: 'vertex buffer vertices',
size: vertexData.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(vertexBuffer, 0, vertexData);
const indexBuffer = device.createBuffer({
label: 'index buffer',
size: indexData.byteLength,
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(indexBuffer, 0, indexData);
请注意,我们将usage更改为 INDEX 。
最后在绘制时我们需要指定索引缓冲区
pass.setPipeline(pipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.setIndexBuffer(indexBuffer, 'uint32');
因为我们的缓冲区包含 32 位无符号整数索引,所以我们需要在此处传递 ‘uint32’ 。我们还可以使用 16 位无符号索引,在这种情况下我们将传入 ‘uint16’ 。
我们需要调用 drawIndexed 而不是 draw
pass.draw(numVertices, kNumObjects);
pass.drawIndexed(numVertices, kNumObjects);
这样一来,我们节省了一些空间 (33%),并且在顶点着色器中计算顶点时可能会进行类似数量的处理。
Note that we could have also used an index buffer with the storage buffer example from the previous article. In that case vertex_index that’s passed in matches the index from the index buffer.
请注意,我们还可以将索引缓冲区与上一篇文章中的存储缓冲区示例一起使用。在这种情况下,传入的 vertex_index 与索引缓冲区中的索引相匹配。
接下来我们将介绍纹理。