五、WebGPU Vertex Buffers 顶点缓冲区
在上一篇文章中,我们将顶点数据放入存储缓冲区中,并使用内置的vertex_index对其进行索引。虽然这种技术越来越受欢迎,但向顶点着色器提供顶点数据的传统方式是通过顶点缓冲和属性。
顶点缓冲区就像任何其他WebGPU缓冲区一样。它们保存着数据。不同之处在于我们不直接从顶点着色器访问它们。相反,我们告诉WebGPU缓冲区中有什么类型的数据,以及它在哪里以及它是如何组织的。然后它将数据从缓冲区中取出并提供给我们。
让我们以上一篇文章中的最后一个示例为例,将其从使用存储缓冲区更改为使用顶点缓冲区。
首先要做的是改变着色器,从顶点缓冲区中获取顶点数据。
struct OurStruct {color: vec4f,offset: vec2f,
};struct OtherStruct {scale: vec2f,
};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>;@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;return vsOut;
}...
正如你所看到的,这是一个很小的变化。我们声明了一个结构体Vertex来定义顶点的数据。重要的部分是用@location(0)声明位置字段,然后,当我们创建渲染管道时,我们必须告诉WebGPU如何获取@location(0)的数据。
然后,当我们创建渲染管道时,我们必须告诉WebGPU如何获取@location(0)的数据。
const pipeline = device.createRenderPipeline({label: 'vertex buffer pipeline',layout: 'auto',vertex: {module,entryPoint: 'vs',buffers: [{arrayStride: 2 * 4, // 2 floats, 4 bytes eachattributes: [{shaderLocation: 0, offset: 0, format: 'float32x2'}, // position],},],},fragment: {module,entryPoint: 'fs',targets: [{ format: presentationFormat }],},});
对于pipeline descriptor 的 vertex entry,我们添加了一个缓冲区数组,用于描述如何从一个或多个顶点缓冲区中提取数据。对于第一个也是唯一一个缓冲区,我们以字节数为单位设置arrayStride。在这种情况下,步长是指从缓冲区中一个顶点的数据到缓冲区中的下一个顶点的字节数。
因为我们的数据是vec2f,这是两个float32数字,所以我们将arrayStride设置为8。
接下来我们定义一个属性数组。我们只有一个。shaderLocation: 0对应于我们顶点结构中的location(0)。offset: 0表示此属性的数据从顶点缓冲区中的第0字节开始。最后format:'float32x2’表示我们希望WebGPU将数据从缓冲区中取出为两个32位浮点数。
我们需要将保存顶点数据的缓冲区的用法从STORAGE更改为vertex,并将其从绑定组中删除。
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 }},],});
然后在绘制时,我们需要告诉webgpu使用哪个顶点缓冲区。
pass.setPipeline(pipeline);pass.setVertexBuffer(0, vertexBuffer);
这里的0对应于我们上面指定的渲染管道缓冲区数组的第一个元素。
这样我们就从顶点存储缓冲区切换到了顶点缓冲区。
以下为完整代码及运行结果:
HTML:
<!--* @Description: * @Author: tianyw* @Date: 2022-11-11 12:50:23* @LastEditTime: 2023-09-19 22:06:18* @LastEditors: tianyw
-->
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>001hello-triangle</title><style>html,body {margin: 0;width: 100%;height: 100%;background: #000;color: #fff;display: flex;text-align: center;flex-direction: column;justify-content: center;}div,canvas {height: 100%;width: 100%;}</style>
</head><body><div id="007vertex-srandom-circle"><canvas id="gpucanvas"></canvas></div><script type="module" src="./007vertex-srandom-circle.ts"></script></body></html>
TS:
/** @Description:* @Author: tianyw* @Date: 2023-04-08 20:03:35* @LastEditTime: 2023-09-19 22:12:42* @LastEditors: tianyw*/
export type SampleInit = (params: {canvas: HTMLCanvasElement;
}) => void | Promise<void>;import shaderWGSL from "./shaders/shader.wgsl?raw";const rand = (min: undefined | number = undefined,max: undefined | number = undefined
) => {if (min === undefined) {min = 0;max = 1;} else if (max === undefined) {max = min;min = 0;}return min + Math.random() * (max - min);
};function createCircleVertices({radius = 1,numSubdivisions = 24,innerRadius = 0,startAngle = 0,endAngle = Math.PI * 2
} = {}) {// 2 triangles per subdivision, 3 verts per tri, 2 values(xy) eachconst numVertices = numSubdivisions * 3 * 2;const vertexData = new Float32Array(numSubdivisions * 2 * 3 * 2);let offset = 0;const addVertex = (x: number, y: number) => {vertexData[offset++] = x;vertexData[offset++] = y;};// 2 vertices per subdivisionfor (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 angleaddVertex(c1 * radius, s1 * radius);addVertex(c2 * radius, s2 * radius);addVertex(c1 * innerRadius, s1 * innerRadius);// second triangleaddVertex(c1 * innerRadius, s1 * innerRadius);addVertex(c2 * radius, s2 * radius);addVertex(c2 * innerRadius, s2 * innerRadius);}return {vertexData,numVertices};
}const init: SampleInit = async ({ canvas }) => {const adapter = await navigator.gpu?.requestAdapter();if (!adapter) return;const device = await adapter?.requestDevice();if (!device) {console.error("need a browser that supports WebGPU");return;}const context = canvas.getContext("webgpu");if (!context) return;const devicePixelRatio = window.devicePixelRatio || 1;canvas.width = canvas.clientWidth * devicePixelRatio;canvas.height = canvas.clientHeight * devicePixelRatio;const presentationFormat = navigator.gpu.getPreferredCanvasFormat();context.configure({device,format: presentationFormat,alphaMode: "premultiplied"});const shaderModule = device.createShaderModule({label: "our hardcoded rgb triangle shaders",code: shaderWGSL});const renderPipeline = device.createRenderPipeline({label: "hardcoded rgb triangle pipeline",layout: "auto",vertex: {module: shaderModule,entryPoint: "vs",buffers: [{arrayStride: 2 * 4, // 2 floats, 4 bytes eachattributes: [{shaderLocation: 0, offset: 0,format: "float32x2"} // position]}]},fragment: {module: shaderModule,entryPoint: "fs",targets: [{format: presentationFormat}]},primitive: {// topology: "line-list"// topology: "line-strip"// topology: "point-list"topology: "triangle-list"// topology: "triangle-strip"}});const kNumObjects = 100;const staticStorageUnitSize =4 * 4 + // color is 4 32bit floats (4bytes each)2 * 4 + // scale is 2 32bit floats (4bytes each)2 * 4; // paddingconst storageUnitSzie = 2 * 4; // scale is 2 32 bit floatsconst staticStorageBufferSize = staticStorageUnitSize * kNumObjects;const storageBufferSize = storageUnitSzie * kNumObjects;const staticStorageBuffer = device.createBuffer({label: "static storage for objects",size: staticStorageBufferSize,usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST});const storageBuffer = device.createBuffer({label: "changing storage for objects",size: storageBufferSize,usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST});const staticStorageValues = new Float32Array(staticStorageBufferSize / 4);const storageValues = new Float32Array(storageBufferSize / 4);const kColorOffset = 0;const kOffsetOffset = 4;const kScaleOffset = 0;const objectInfos: {scale: number;}[] = [];for (let i = 0; i < kNumObjects; ++i) {const staticOffset = i * (staticStorageUnitSize / 4);staticStorageValues.set([rand(), rand(), rand(), 1],staticOffset + kColorOffset);staticStorageValues.set([rand(-0.9, 0.9), rand(-0.9, 0.9)],staticOffset + kOffsetOffset);objectInfos.push({scale: rand(0.2, 0.5)});}device.queue.writeBuffer(staticStorageBuffer, 0, staticStorageValues);const { vertexData, 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 bindGroup = device.createBindGroup({label: "bind group for objects",layout: renderPipeline.getBindGroupLayout(0),entries: [{ binding: 0, resource: { buffer: staticStorageBuffer } },{ binding: 1, resource: { buffer: storageBuffer } }]});function frame() {const aspect = canvas.width / canvas.height;const renderCommandEncoder = device.createCommandEncoder({label: "render vert frag"});if (!context) return;const textureView = context.getCurrentTexture().createView();const renderPassDescriptor: GPURenderPassDescriptor = {label: "our basic canvas renderPass",colorAttachments: [{view: textureView,clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },loadOp: "clear",storeOp: "store"}]};const renderPass =renderCommandEncoder.beginRenderPass(renderPassDescriptor);renderPass.setPipeline(renderPipeline);renderPass.setVertexBuffer(0,vertexBuffer);objectInfos.forEach(({ scale }, ndx) => {const offset = ndx * (storageUnitSzie / 4);storageValues.set([scale / aspect, scale], offset + kScaleOffset); // set the scale});device.queue.writeBuffer(storageBuffer, 0, storageValues);renderPass.setBindGroup(0, bindGroup);renderPass.draw(numVertices, kNumObjects);renderPass.end();const renderBuffer = renderCommandEncoder.finish();device.queue.submit([renderBuffer]);requestAnimationFrame(frame);}requestAnimationFrame(frame);
};const canvas = document.getElementById("gpucanvas") as HTMLCanvasElement;
init({ canvas: canvas });
Shaders:
shader:
struct OurStruct {color: vec4f,offset: vec2f
};struct OtherStruct {scale: vec2f
};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>;
@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;return vsOut;
}@fragment
fn fs(vsOut: VSOutput) -> @location(0) vec4f {return vsOut.color;
}
执行draw命令时的状态如下所示:
属性格式字段可以是这些类型之一:
Vertex format | Data type | Components | Byte size | Example WGSL type |
---|---|---|---|---|
"uint8x2" | unsigned int | 2 | 2 | vec2<u32> , vec2u |
"uint8x4" | unsigned int | 4 | 4 | vec4<u32> , vec4u |
"sint8x2" | signed int | 2 | 2 | vec2<i32> , vec2i |
"sint8x4" | signed int | 4 | 4 | vec4<i32> , vec4i |
"unorm8x2" | unsigned normalized | 2 | 2 | vec2<f32> , vec2f |
"unorm8x4" | unsigned normalized | 4 | 4 | vec4<f32> , vec4f |
"snorm8x2" | signed normalized | 2 | 2 | vec2<f32> , vec2f |
"snorm8x4" | signed normalized | 4 | 4 | vec4<f32> , vec4f |
"uint16x2" | unsigned int | 2 | 4 | vec2<u32> , vec2u |
"uint16x4" | unsigned int | 4 | 8 | vec4<u32> , vec4u |
"sint16x2" | signed int | 2 | 4 | vec2<i32> , vec2i |
"sint16x4" | signed int | 4 | 8 | vec4<i32> , vec4i |
"unorm16x2" | unsigned normalized | 2 | 4 | vec2<f32> , vec2f |
"unorm16x4" | unsigned normalized | 4 | 8 | vec4<f32> , vec4f |
"snorm16x2" | signed normalized | 2 | 4 | vec2<f32> , vec2f |
"snorm16x4" | signed normalized | 4 | 8 | vec4<f32> , vec4f |
"float16x2" | float | 2 | 4 | vec2<f16> , vec2h |
"float16x4" | float | 4 | 8 | vec4<f16> , vec4h |
"float32" | float | 1 | 4 | f32 |
"float32x2" | float | 2 | 8 | vec2<f32> , vec2f |
"float32x3" | float | 3 | 12 | vec3<f32> , vec3f |
"float32x4" | float | 4 | 16 | vec4<f32> , vec4f |
"uint32" | unsigned int | 1 | 4 | u32 |
"uint32x2" | unsigned int | 2 | 8 | vec2<u32> , vec2u |
"uint32x3" | unsigned int | 3 | 12 | vec3<u32> , vec3u |
"uint32x4" | unsigned int | 4 | 16 | vec4<u32> , vec4u |
"sint32" | signed int | 1 | 4 | i32 |
"sint32x2" | signed int | 2 | 8 | vec2<i32> , vec2i |
"sint32x3" | signed int | 3 | 12 | vec3<i32> , vec3i |
"sint32x4" | signed int | 4 | 16 | vec4<i32> , vec4i |
实例化顶点缓冲区
属性可以按顶点或实例推进。在每个实例中推进它们实际上是我们在索引otherStructs[instanceIndex]和ourStructs[instanceIndex]时所做的相同的事情,其中instanceIndex从@builtin(instance_index)获得其值。
让我们去掉存储缓冲区,使用顶点缓冲区来完成同样的事情。首先让我们改变着色器使用顶点属性而不是存储缓冲区。
struct Vertex {@location(0) position: vec2f,@location(1) color: vec4f,@location(2) offset: vec2f,@location(3) scale: 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>;@vertex fn vs(vert: Vertex,
) -> VSOutput {var vsOut: VSOutput;vsOut.position = vec4f(vert.position * vert.scale + vert.offset, 0.0, 1.0);vsOut.color = vert.color;return vsOut;
}@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f {return vsOut.color;
}
现在我们需要更新渲染管道,告诉它我们希望如何向这些属性提供数据。为了保持最小的变化,我们将使用我们为存储缓冲区创建的数据。我们将使用两个缓冲区,一个缓冲区将保存每个实例的颜色和偏移量,另一个将保存比例。
const pipeline = device.createRenderPipeline({label: 'flat colors',layout: 'auto',vertex: {module,entryPoint: 'vs',buffers: [{arrayStride: 2 * 4, // 2 floats, 4 bytes eachattributes: [{shaderLocation: 0, offset: 0, format: 'float32x2'}, // position],},{arrayStride: 6 * 4, // 6 floats, 4 bytes eachstepMode: 'instance',attributes: [{shaderLocation: 1, offset: 0, format: 'float32x4'}, // color{shaderLocation: 2, offset: 16, format: 'float32x2'}, // offset],},{arrayStride: 2 * 4, // 2 floats, 4 bytes eachstepMode: 'instance',attributes: [{shaderLocation: 3, offset: 0, format: 'float32x2'}, // scale],},],},fragment: {module,entryPoint: 'fs',targets: [{ format: presentationFormat }],},});
上面我们在流水线描述中向buffers数组中添加了2个条目,所以现在有3个缓冲区条目,这意味着我们告诉WebGPU我们将在3个缓冲区中提供数据。
对于我们的两个新实体,我们将stepMode设置为instance。这意味着该属性在每个实例中只前进到下一个值一次。默认是stepMode: ‘vertex’,每个顶点前进一次(每个实例重新开始)。
我们有2个缓冲区。保持比例的那个很简单。就像我们第一个保存位置的缓冲区一样,每个顶点有2*32个浮点数。
另一个缓冲区保存颜色和偏移量它们将像这样在数据中交错:
所以上面我们说从一组数据到下一组数据的arrayStride是6 * 4,6个32位浮点数,每4字节(总共24字节)。颜色从偏移量0开始,但偏移量从16字节开始。
接下来,我们可以更改设置缓冲区的代码。
// create 2 storage buffersconst staticUnitSize =4 * 4 + // color is 4 32bit floats (4bytes each)2 * 4; // offset is 2 32bit floats (4bytes each)const changingUnitSize =2 * 4; // scale is 2 32bit floats (4bytes each)const staticVertexBufferSize = staticUnitSize * kNumObjects;const changingVertexBufferSize = changingUnitSize * kNumObjects;const staticVertexBuffer = device.createBuffer({label: 'static vertex for objects',size: staticVertexBufferSize,usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,});const changingVertexBuffer = device.createBuffer({label: 'changing vertex for objects',size: changingVertexBufferSize,usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,});
顶点属性与存储缓冲区中的结构没有相同的填充限制,所以我们不再需要填充。否则,我们所做的就是将用法从STORAGE更改为VERTEX(我们将所有变量从“STORAGE”重命名为“VERTEX”)。
由于我们不再使用存储缓冲区,因此不再需要bindGroup。
最后,我们不需要设置bindGroup但我们需要设置顶点缓冲区:
const encoder = device.createCommandEncoder();const pass = encoder.beginRenderPass(renderPassDescriptor);pass.setPipeline(pipeline);pass.setVertexBuffer(0, vertexBuffer);pass.setVertexBuffer(1, staticVertexBuffer);pass.setVertexBuffer(2, changingVertexBuffer);...pass.draw(numVertices, kNumObjects);pass.end();
在这里,setVertexBuffer的第一个参数对应于我们上面创建的管道中的buffers数组的元素。
这和我们之前做的是一样的但是我们用的都是顶点缓冲区,没有存储缓冲区。
以下为完整代码及运行结果:
HTML:
<!--* @Description: * @Author: tianyw* @Date: 2022-11-11 12:50:23* @LastEditTime: 2023-09-19 22:06:18* @LastEditors: tianyw
-->
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>001hello-triangle</title><style>html,body {margin: 0;width: 100%;height: 100%;background: #000;color: #fff;display: flex;text-align: center;flex-direction: column;justify-content: center;}div,canvas {height: 100%;width: 100%;}</style>
</head><body><div id="007vertex-srandom-circle"><canvas id="gpucanvas"></canvas></div><script type="module" src="./007vertex-srandom-circle2.ts"></script></body></html>
TS:
/** @Description:* @Author: tianyw* @Date: 2023-04-08 20:03:35* @LastEditTime: 2023-10-08 22:47:31* @LastEditors: tianyw*/
export type SampleInit = (params: {canvas: HTMLCanvasElement;
}) => void | Promise<void>;import shaderWGSL from "./shaders/shader.wgsl?raw";const rand = (min: undefined | number = undefined,max: undefined | number = undefined
) => {if (min === undefined) {min = 0;max = 1;} else if (max === undefined) {max = min;min = 0;}return min + Math.random() * (max - min);
};function createCircleVertices({radius = 1,numSubdivisions = 24,innerRadius = 0,startAngle = 0,endAngle = Math.PI * 2
} = {}) {// 2 triangles per subdivision, 3 verts per tri, 2 values(xy) eachconst numVertices = numSubdivisions * 3 * 2;const vertexData = new Float32Array(numSubdivisions * 2 * 3 * 2);let offset = 0;const addVertex = (x: number, y: number) => {vertexData[offset++] = x;vertexData[offset++] = y;};// 2 vertices per subdivisionfor (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 angleaddVertex(c1 * radius, s1 * radius);addVertex(c2 * radius, s2 * radius);addVertex(c1 * innerRadius, s1 * innerRadius);// second triangleaddVertex(c1 * innerRadius, s1 * innerRadius);addVertex(c2 * radius, s2 * radius);addVertex(c2 * innerRadius, s2 * innerRadius);}return {vertexData,numVertices};
}const init: SampleInit = async ({ canvas }) => {const adapter = await navigator.gpu?.requestAdapter();if (!adapter) return;const device = await adapter?.requestDevice();if (!device) {console.error("need a browser that supports WebGPU");return;}const context = canvas.getContext("webgpu");if (!context) return;const devicePixelRatio = window.devicePixelRatio || 1;canvas.width = canvas.clientWidth * devicePixelRatio;canvas.height = canvas.clientHeight * devicePixelRatio;const presentationFormat = navigator.gpu.getPreferredCanvasFormat();context.configure({device,format: presentationFormat,alphaMode: "premultiplied"});const shaderModule = device.createShaderModule({label: "our hardcoded rgb triangle shaders",code: shaderWGSL});const renderPipeline = device.createRenderPipeline({label: "flat colors",layout: "auto",vertex: {module: shaderModule,entryPoint: "vs",buffers: [{arrayStride: 2 * 4, // 2 floats, 4 bytes eachattributes: [{shaderLocation: 0,offset: 0,format: "float32x2"} // position]},{arrayStride: 6 * 4, // 6 floats, 4 bytes eachstepMode: "instance",attributes: [{shaderLocation: 1,offset: 0,format: "float32x4" // color},{shaderLocation: 2,offset: 16,format: "float32x2" // offset}]},{arrayStride: 2 * 4, // 2 floats, 4 bytes eachstepMode: "instance",attributes: [{shaderLocation: 3,offset: 0,format: "float32x2"} // scale]}]},fragment: {module: shaderModule,entryPoint: "fs",targets: [{format: presentationFormat}]},primitive: {// topology: "line-list"// topology: "line-strip"// topology: "point-list"topology: "triangle-list"// topology: "triangle-strip"}});const kNumObjects = 100;const staticUnitSize =4 * 4 + // color is 4 32bit floats (4bytes each)2 * 4; // offset is 2 32bit floats (4bytes each)const changingUnitSize = 2 * 4; // scale is 2 32 bit floatsconst staticVertexBufferSize = staticUnitSize * kNumObjects;const changingVertexBufferSize = changingUnitSize * kNumObjects;const staticVertexBuffer = device.createBuffer({label: "static storage for objects",size: staticVertexBufferSize,usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST});const changingVertexBuffer = device.createBuffer({label: "changing storage for objects",size: changingVertexBufferSize,usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST});const staticVertexValues = new Float32Array(staticVertexBufferSize / 4);const changingVertexValues = new Float32Array(changingVertexBufferSize / 4);const kColorOffset = 0;const kOffsetOffset = 4;const kScaleOffset = 0;const objectInfos: {scale: number;}[] = [];for (let i = 0; i < kNumObjects; ++i) {const staticOffset = i * (staticUnitSize / 4);staticVertexValues.set([rand(), rand(), rand(), 1],staticOffset + kColorOffset);staticVertexValues.set([rand(-0.9, 0.9), rand(-0.9, 0.9)],staticOffset + kOffsetOffset);objectInfos.push({scale: rand(0.2, 0.5)});}device.queue.writeBuffer(staticVertexBuffer, 0, staticVertexValues);const { vertexData, 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);function frame() {const aspect = canvas.width / canvas.height;const renderCommandEncoder = device.createCommandEncoder({label: "render vert frag"});if (!context) return;const textureView = context.getCurrentTexture().createView();const renderPassDescriptor: GPURenderPassDescriptor = {label: "our basic canvas renderPass",colorAttachments: [{view: textureView,clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },loadOp: "clear",storeOp: "store"}]};const renderPass =renderCommandEncoder.beginRenderPass(renderPassDescriptor);renderPass.setPipeline(renderPipeline);renderPass.setVertexBuffer(0, vertexBuffer);objectInfos.forEach(({ scale }, ndx) => {const offset = ndx * (changingUnitSize / 4);changingVertexValues.set([scale / aspect, scale], offset + kScaleOffset); // set the scale});device.queue.writeBuffer(changingVertexBuffer, 0, changingVertexValues);renderPass.setVertexBuffer(1, staticVertexBuffer);renderPass.setVertexBuffer(2, changingVertexBuffer);renderPass.draw(numVertices, kNumObjects);renderPass.end();const renderBuffer = renderCommandEncoder.finish();device.queue.submit([renderBuffer]);requestAnimationFrame(frame);}requestAnimationFrame(frame);
};const canvas = document.getElementById("gpucanvas") as HTMLCanvasElement;
init({ canvas: canvas });
Shaders:
shader.wgsl:
struct Vertex {@location(0) position: vec2f,@location(1) color: vec4f,@location(2) offset: vec2f,@location(3) scale: vec2f,
};struct VSOutput {@builtin(position) position: vec4f,@location(0) color: vec4f
};@vertex
fn vs(vert: Vertex) -> VSOutput {var vsOut: VSOutput;vsOut.position = vec4f(vert.position * vert.scale + vert.offset, 0.0, 1.0);vsOut.color = vert.color;return vsOut;
}@fragment
fn fs(vsOut: VSOutput) -> @location(0) vec4f {return vsOut.color;
}
为了好玩,让我们添加第二个让我们为每个顶点的颜色添加另一个属性。首先改变着色器。
struct Vertex {@location(0) position: vec2f,@location(1) color: vec4f,@location(2) offset: vec2f,@location(3) scale: vec2f,@location(4) perVertexColor: vec3f,
};struct VSOutput {@builtin(position) position: vec4f,@location(0) color: vec4f,
};@vertex fn vs(vert: Vertex,
) -> VSOutput {var vsOut: VSOutput;vsOut.position = vec4f(vert.position * vert.scale + vert.offset, 0.0, 1.0);vsOut.color = vert.color * vec4f(vert.perVertexColor, 1);return vsOut;
}@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f {return vsOut.color;
}
然后我们需要更新管道以描述我们将如何提供数据。我们将像这样将perVertexColor数据与位置交织起来:
因此,需要更改arrayStride以覆盖我们的新数据,我们需要添加新属性。它从两个32位浮点数开始,所以它在缓冲区中的偏移量是8字节。
const pipeline = device.createRenderPipeline({label: 'per vertex color',layout: 'auto',vertex: {module,entryPoint: 'vs',buffers: [{arrayStride: 5 * 4, // 5 floats, 4 bytes eachattributes: [{shaderLocation: 0, offset: 0, format: 'float32x2'}, // position{shaderLocation: 4, offset: 8, format: 'float32x3'}, // perVertexColor],},{arrayStride: 6 * 4, // 6 floats, 4 bytes eachstepMode: 'instance',attributes: [{shaderLocation: 1, offset: 0, format: 'float32x4'}, // color{shaderLocation: 2, offset: 16, format: 'float32x2'}, // offset],},{arrayStride: 2 * 4, // 2 floats, 4 bytes eachstepMode: 'instance',attributes: [{shaderLocation: 3, offset: 0, format: 'float32x2'}, // scale],},],},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 + 3) * 3 * 2);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--1 4// | / /|// |/ / |// 2 3--5for (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 triangleaddVertex(c1 * radius, s1 * radius, ...outerColor);addVertex(c2 * radius, s2 * radius, ...outerColor);addVertex(c1 * innerRadius, s1 * innerRadius, ...innerColor);addVertex(c1 * innerRadius, s1 * innerRadius, ...innerColor);addVertex(c2 * radius, s2 * radius, ...outerColor);addVertex(c2 * innerRadius, s2 * innerRadius, ...innerColor);}return {vertexData,numVertices,};
}
这样我们就得到了阴影圈。
以下为完整代码及运行效果:
HTML:
<!--* @Description: * @Author: tianyw* @Date: 2022-11-11 12:50:23* @LastEditTime: 2023-10-08 23:52:55* @LastEditors: tianyw
-->
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>001hello-triangle</title><style>html,body {margin: 0;width: 100%;height: 100%;background: #000;color: #fff;display: flex;text-align: center;flex-direction: column;justify-content: center;}div,canvas {height: 100%;width: 100%;}</style>
</head><body><div id="007vertex-srandom-circle3"><canvas id="gpucanvas"></canvas></div><script type="module" src="./007vertex-srandom-circle3.ts"></script></body></html>
TS:
/** @Description:* @Author: tianyw* @Date: 2023-04-08 20:03:35* @LastEditTime: 2023-10-09 00:00:17* @LastEditors: tianyw*/
export type SampleInit = (params: {canvas: HTMLCanvasElement;
}) => void | Promise<void>;import shaderWGSL from "./shaders/shader.wgsl?raw";const rand = (min: undefined | number = undefined,max: undefined | number = undefined
) => {if (min === undefined) {min = 0;max = 1;} else if (max === undefined) {max = min;min = 0;}return min + Math.random() * (max - min);
};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) eachconst numVertices = numSubdivisions * 3 * 2;const vertexData = new Float32Array(numSubdivisions * (2 + 3) * 3 * 2);let offset = 0;const addVertex = (x: number, y: number, r: number, g: number, b: number) => {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--5for (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 angleaddVertex(c1 * radius,s1 * radius,outerColor[0],outerColor[1],outerColor[2]);addVertex(c2 * radius,s2 * radius,outerColor[0],outerColor[1],outerColor[2]);addVertex(c1 * innerRadius,s1 * innerRadius,innerColor[0],innerColor[1],innerColor[2]);// second triangleaddVertex(c1 * innerRadius,s1 * innerRadius,innerColor[0],innerColor[1],innerColor[2]);addVertex(c2 * radius,s2 * radius,outerColor[0],outerColor[1],outerColor[2]);addVertex(c2 * innerRadius,s2 * innerRadius,innerColor[0],innerColor[1],innerColor[2]);}return {vertexData,numVertices};
}const init: SampleInit = async ({ canvas }) => {const adapter = await navigator.gpu?.requestAdapter();if (!adapter) return;const device = await adapter?.requestDevice();if (!device) {console.error("need a browser that supports WebGPU");return;}const context = canvas.getContext("webgpu");if (!context) return;const devicePixelRatio = window.devicePixelRatio || 1;canvas.width = canvas.clientWidth * devicePixelRatio;canvas.height = canvas.clientHeight * devicePixelRatio;const presentationFormat = navigator.gpu.getPreferredCanvasFormat();context.configure({device,format: presentationFormat,alphaMode: "premultiplied"});const shaderModule = device.createShaderModule({label: "our hardcoded rgb triangle shaders",code: shaderWGSL});const renderPipeline = device.createRenderPipeline({label: "per vertex color",layout: "auto",vertex: {module: shaderModule,entryPoint: "vs",buffers: [{arrayStride: 5 * 4, // 5 floats, 4 bytes eachattributes: [{shaderLocation: 0,offset: 0,format: "float32x2"}, // position{shaderLocation: 4,offset: 8,format: "float32x3"} // position]},{arrayStride: 6 * 4, // 6 floats, 4 bytes eachstepMode: "instance",attributes: [{shaderLocation: 1,offset: 0,format: "float32x4" // color},{shaderLocation: 2,offset: 16,format: "float32x2" // offset}]},{arrayStride: 2 * 4, // 2 floats, 4 bytes eachstepMode: "instance",attributes: [{shaderLocation: 3,offset: 0,format: "float32x2"} // scale]}]},fragment: {module: shaderModule,entryPoint: "fs",targets: [{format: presentationFormat}]},primitive: {// topology: "line-list"// topology: "line-strip"// topology: "point-list"topology: "triangle-list"// topology: "triangle-strip"}});const kNumObjects = 100;const staticUnitSize =4 * 4 + // color is 4 32bit floats (4bytes each)2 * 4; // offset is 2 32bit floats (4bytes each)const changingUnitSize = 2 * 4; // scale is 2 32 bit floatsconst staticVertexBufferSize = staticUnitSize * kNumObjects;const changingVertexBufferSize = changingUnitSize * kNumObjects;const staticVertexBuffer = device.createBuffer({label: "static vertex for objects",size: staticVertexBufferSize,usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST});const changingVertexBuffer = device.createBuffer({label: "changing vertex for objects",size: changingVertexBufferSize,usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST});const staticVertexValues = new Float32Array(staticVertexBufferSize / 4);const changingVertexValues = new Float32Array(changingVertexBufferSize / 4);const kColorOffset = 0;const kOffsetOffset = 4;const kScaleOffset = 0;const objectInfos: {scale: number;}[] = [];for (let i = 0; i < kNumObjects; ++i) {const staticOffset = i * (staticUnitSize / 4);staticVertexValues.set([rand(), rand(), rand(), 1],staticOffset + kColorOffset);staticVertexValues.set([rand(-0.9, 0.9), rand(-0.9, 0.9)],staticOffset + kOffsetOffset);objectInfos.push({scale: rand(0.2, 0.5)});}device.queue.writeBuffer(staticVertexBuffer, 0, staticVertexValues);const { vertexData, 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);function frame() {const aspect = canvas.width / canvas.height;const renderCommandEncoder = device.createCommandEncoder({label: "render vert frag"});if (!context) return;const textureView = context.getCurrentTexture().createView();const renderPassDescriptor: GPURenderPassDescriptor = {label: "our basic canvas renderPass",colorAttachments: [{view: textureView,clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },loadOp: "clear",storeOp: "store"}]};const renderPass =renderCommandEncoder.beginRenderPass(renderPassDescriptor);renderPass.setPipeline(renderPipeline);renderPass.setVertexBuffer(0, vertexBuffer);objectInfos.forEach(({ scale }, ndx) => {const offset = ndx * (changingUnitSize / 4);changingVertexValues.set([scale / aspect, scale], offset + kScaleOffset); // set the scale});device.queue.writeBuffer(changingVertexBuffer, 0, changingVertexValues);renderPass.setVertexBuffer(1, staticVertexBuffer);renderPass.setVertexBuffer(2, changingVertexBuffer);renderPass.draw(numVertices, kNumObjects);renderPass.end();const renderBuffer = renderCommandEncoder.finish();device.queue.submit([renderBuffer]);requestAnimationFrame(frame);}requestAnimationFrame(frame);
};const canvas = document.getElementById("gpucanvas") as HTMLCanvasElement;
init({ canvas: canvas });
Shaders:
shader.wgsl:
struct Vertex {@location(0) position: vec2f,@location(1) color: vec4f,@location(2) offset: vec2f,@location(3) scale: vec2f,@location(4) perVertexColor: vec3f,
};struct VSOutput {@builtin(position) position: vec4f,@location(0) color: vec4f
};@vertex
fn vs(vert: Vertex) -> VSOutput {var vsOut: VSOutput;vsOut.position = vec4f(vert.position * vert.scale + vert.offset, 0.0, 1.0);vsOut.color = vert.color * vec4f(vert.perVertexColor, 1);return vsOut;
}@fragment
fn fs(vsOut: VSOutput) -> @location(0) vec4f {return vsOut.color;
}
WGSL中的属性不必与JavaScript中的属性匹配
在上面的WGSL中,我们将perVertexColor属性声明为vec3f,如下所示
struct Vertex {@location(0) position: vec2f,@location(1) color: vec4f,@location(2) offset: vec2f,@location(3) scale: vec2f,@location(4) perVertexColor: vec3f,
};
像这样使用它
@vertex fn vs(vert: Vertex,
) -> VSOutput {var vsOut: VSOutput;vsOut.position = vec4f(vert.position * vert.scale + vert.offset, 0.0, 1.0);vsOut.color = vert.color * vec4f(vert.perVertexColor, 1);return vsOut;
}
我们也可以将它声明为vec4f,然后像这样使用它
struct Vertex {@location(0) position: vec2f,@location(1) color: vec4f,@location(2) offset: vec2f,@location(3) scale: vec2f,@location(4) perVertexColor: vec4f,
};...@vertex fn vs(vert: Vertex,
) -> VSOutput {var vsOut: VSOutput;vsOut.position = vec4f(vert.position * vert.scale + vert.offset, 0.0, 1.0);vsOut.color = vert.color * vert.perVertexColor;return vsOut;
}
不会改变任何其他东西。在JavaScript中,我们仍然只提供每个顶点3个浮点数的数据。
{arrayStride: 5 * 4, // 5 floats, 4 bytes eachattributes: [{shaderLocation: 0, offset: 0, format: 'float32x2'}, // position{shaderLocation: 4, offset: 8, format: 'float32x3'}, // perVertexColor],},
这是可行的,因为在着色器中属性总是有4个值。它们默认为0,0,0,1,所以我们不提供的任何值都会得到默认值。
使用规范化的值来节省空间
我们使用32位浮点数表示颜色。每个perVertexColor有3个值,每个顶点每种颜色总共有12字节。每种颜色有4个值,每种颜色每个实例总共16字节。
我们可以优化一下,使用8位的值,并告诉WebGPU它们应该从0↔255到0.0↔1.0进行规范化
查看有效的属性格式列表,没有3值8bit格式,但有unorm8x4
,所以让我们使用它。
首先,让我们更改生成顶点的代码,将颜色存储为8bit值并进行归一化
function createCircleVertices({radius = 1,numSubdivisions = 24,innerRadius = 0,startAngle = 0,endAngle = Math.PI * 2,
} = {}) {// 2 triangles per subdivision, 3 verts per triconst numVertices = numSubdivisions * 3 * 2;// 2 32-bit values for position (xy) and 1 32-bit value for color (rgb_)// The 32-bit color value will be written/read as 4 8-bit valuesconst vertexData = new Float32Array(numVertices * (2 + 1));const colorData = new Uint8Array(vertexData.buffer);let offset = 0;let colorOffset = 8;const addVertex = (x, y, r, g, b) => {vertexData[offset++] = x;vertexData[offset++] = y;offset += 1; // skip the colorcolorData[colorOffset++] = r * 255;colorData[colorOffset++] = g * 255;colorData[colorOffset++] = b * 255;colorOffset += 9; // skip extra byte and the position};
上面我们创建了colorData,它是一个Uint8Array视图,与vertexData相同的数据
然后使用colorData来插入颜色,从0↔1扩展到0↔255
这些数据的内存布局如下所示
然后我们需要修改管道,将数据提取为8位无符号值,并将它们归一化回0↔1,更新偏移量,并将步长更新为新的大小。
const pipeline = device.createRenderPipeline({label: 'per vertex color',layout: 'auto',vertex: {module,entryPoint: 'vs',buffers: [{arrayStride: 2 * 4 + 4, // 2 floats, 4 bytes each + 4 bytesattributes: [{shaderLocation: 0, offset: 0, format: 'float32x2'}, // position{shaderLocation: 4, offset: 8, format: 'unorm8x4'}, // perVertexColor],},{arrayStride: 4 + 2 * 4, // 4 bytes + 2 floats, 4 bytes eachstepMode: 'instance',attributes: [{shaderLocation: 1, offset: 0, format: 'unorm8x4'}, // color{shaderLocation: 2, offset: 4, format: 'float32x2'}, // offset],},{arrayStride: 2 * 4, // 2 floats, 4 bytes eachstepMode: 'instance',attributes: [{shaderLocation: 3, offset: 0, format: 'float32x2'}, // scale],},],},fragment: {module,entryPoint: 'fs',targets: [{ format: presentationFormat }],},});
这样就节省了一点空间。我们以前每个顶点使用20字节,现在我们使用12字节,节省了40%。我们在每个实例中使用24字节,现在我们使用12字节,节省了50%。
以下为完整代码及其运行效果:
HTML:
<!--* @Description: * @Author: tianyw* @Date: 2022-11-11 12:50:23* @LastEditTime: 2023-10-09 21:00:14* @LastEditors: tianyw
-->
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>001hello-triangle</title><style>html,body {margin: 0;width: 100%;height: 100%;background: #000;color: #fff;display: flex;text-align: center;flex-direction: column;justify-content: center;}div,canvas {height: 100%;width: 100%;}</style>
</head><body><div id="007vertex-srandom-circle4"><canvas id="gpucanvas"></canvas></div><script type="module" src="./007vertex-srandom-circle4.ts"></script></body></html>
TS:
/** @Description:* @Author: tianyw* @Date: 2023-04-08 20:03:35* @LastEditTime: 2023-10-09 21:13:36* @LastEditors: tianyw*/
export type SampleInit = (params: {canvas: HTMLCanvasElement;
}) => void | Promise<void>;import shaderWGSL from "./shaders/shader.wgsl?raw";const rand = (min: undefined | number = undefined,max: undefined | number = undefined
) => {if (min === undefined) {min = 0;max = 1;} else if (max === undefined) {max = min;min = 0;}return min + Math.random() * (max - min);
};function createCircleVertices({radius = 1,numSubdivisions = 24,innerRadius = 0,startAngle = 0,endAngle = Math.PI * 2
} = {}) {// 2 triangles per subdivision, 3 verts per triconst numVertices = numSubdivisions * 3 * 2;// 2 32-bit values for position(xy) and 1 32-bit value for color(rgb_)// the 32-bit color value will be written/read as 4 8-bit valuesconst vertexData = new Float32Array(numVertices * (2 + 1));const colorData = new Uint8Array(vertexData.buffer);let offset = 0;let colorOffset = 8;const addVertex = (x: number, y: number, r: number, g: number, b: number) => {vertexData[offset++] = x;vertexData[offset++] = y;offset += 1; // skip the colorcolorData[colorOffset++] = r * 255;colorData[colorOffset++] = g * 255;colorData[colorOffset++] = b * 255;colorOffset += 9; // skip extra byte and the position};const innerColor = [1, 1, 1];const outerColor = [0.1, 0.1, 0.1];// 2 vertices per subdivision//// 0--1 4// | / /|// |/ / |// 2 3--5for (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 angleaddVertex(c1 * radius,s1 * radius,outerColor[0],outerColor[1],outerColor[2]);addVertex(c2 * radius,s2 * radius,outerColor[0],outerColor[1],outerColor[2]);addVertex(c1 * innerRadius,s1 * innerRadius,innerColor[0],innerColor[1],innerColor[2]);// second triangleaddVertex(c1 * innerRadius,s1 * innerRadius,innerColor[0],innerColor[1],innerColor[2]);addVertex(c2 * radius,s2 * radius,outerColor[0],outerColor[1],outerColor[2]);addVertex(c2 * innerRadius,s2 * innerRadius,innerColor[0],innerColor[1],innerColor[2]);}return {vertexData,numVertices};
}const init: SampleInit = async ({ canvas }) => {const adapter = await navigator.gpu?.requestAdapter();if (!adapter) return;const device = await adapter?.requestDevice();if (!device) {console.error("need a browser that supports WebGPU");return;}const context = canvas.getContext("webgpu");if (!context) return;const devicePixelRatio = window.devicePixelRatio || 1;canvas.width = canvas.clientWidth * devicePixelRatio;canvas.height = canvas.clientHeight * devicePixelRatio;const presentationFormat = navigator.gpu.getPreferredCanvasFormat();context.configure({device,format: presentationFormat,alphaMode: "premultiplied"});const shaderModule = device.createShaderModule({label: "our hardcoded rgb triangle shaders",code: shaderWGSL});const renderPipeline = device.createRenderPipeline({label: "per vertex color",layout: "auto",vertex: {module: shaderModule,entryPoint: "vs",buffers: [{arrayStride: 2 * 4 + 4, // 2 floats, 4 bytes each + 4 bytesattributes: [{shaderLocation: 0,offset: 0,format: "float32x2"}, // position{shaderLocation: 4,offset: 8,format: "unorm8x4"} // position]},{arrayStride: 4 + 2 * 4, // 4 bytes + 2 floats,4 bytes eachstepMode: "instance",attributes: [{shaderLocation: 1,offset: 0,format: "unorm8x4" // color},{shaderLocation: 2,offset: 4,format: "float32x2" // offset}]},{arrayStride: 2 * 4, // 2 floats, 4 bytes eachstepMode: "instance",attributes: [{shaderLocation: 3,offset: 0,format: "float32x2"} // scale]}]},fragment: {module: shaderModule,entryPoint: "fs",targets: [{format: presentationFormat}]},primitive: {// topology: "line-list"// topology: "line-strip"// topology: "point-list"topology: "triangle-list"// topology: "triangle-strip"}});const kNumObjects = 100;const staticUnitSize =4 + // color is 4 bytes2 * 4; // offset is 2 32bit floats (4bytes each)const changingUnitSize = 2 * 4; // scale is 2 32 bit floatsconst staticVertexBufferSize = staticUnitSize * kNumObjects;const changingVertexBufferSize = changingUnitSize * kNumObjects;const staticVertexBuffer = device.createBuffer({label: "static vertex for objects",size: staticVertexBufferSize,usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST});const changingVertexBuffer = device.createBuffer({label: "changing vertex for objects",size: changingVertexBufferSize,usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST});const staticVertexValuesU8 = new Uint8Array(staticVertexBufferSize);const staticVertexValuesF32 = new Float32Array(staticVertexValuesU8.buffer);const changingVertexValues = new Float32Array(changingVertexBufferSize / 4);const kColorOffset = 0;const kOffsetOffset = 1;const kScaleOffset = 0;const objectInfos: {scale: number;}[] = [];for (let i = 0; i < kNumObjects; ++i) {const staticOffsetU8 = i * staticUnitSize;const staticOffsetF32 = staticOffsetU8 / 4;staticVertexValuesU8.set([rand() * 255, rand() * 255, rand() * 255, 255],staticOffsetU8 + kColorOffset);staticVertexValuesF32.set([rand(-0.9, 0.9), rand(-0.9, 0.9)],staticOffsetF32 + kOffsetOffset);objectInfos.push({scale: rand(0.2, 0.5)});}device.queue.writeBuffer(staticVertexBuffer, 0, staticVertexValuesF32);const { vertexData, 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);function frame() {const aspect = canvas.width / canvas.height;const renderCommandEncoder = device.createCommandEncoder({label: "render vert frag"});if (!context) return;const textureView = context.getCurrentTexture().createView();const renderPassDescriptor: GPURenderPassDescriptor = {label: "our basic canvas renderPass",colorAttachments: [{view: textureView,clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },loadOp: "clear",storeOp: "store"}]};const renderPass =renderCommandEncoder.beginRenderPass(renderPassDescriptor);renderPass.setPipeline(renderPipeline);renderPass.setVertexBuffer(0, vertexBuffer);objectInfos.forEach(({ scale }, ndx) => {const offset = ndx * (changingUnitSize / 4);changingVertexValues.set([scale / aspect, scale], offset + kScaleOffset); // set the scale});device.queue.writeBuffer(changingVertexBuffer, 0, changingVertexValues);renderPass.setVertexBuffer(1, staticVertexBuffer);renderPass.setVertexBuffer(2, changingVertexBuffer);renderPass.draw(numVertices, kNumObjects);renderPass.end();const renderBuffer = renderCommandEncoder.finish();device.queue.submit([renderBuffer]);requestAnimationFrame(frame);}requestAnimationFrame(frame);
};const canvas = document.getElementById("gpucanvas") as HTMLCanvasElement;
init({ canvas: canvas });
Shaders:
shader.wgsl:
struct Vertex {@location(0) position: vec2f,@location(1) color: vec4f,@location(2) offset: vec2f,@location(3) scale: vec2f,@location(4) perVertexColor: vec3f,
};struct VSOutput {@builtin(position) position: vec4f,@location(0) color: vec4f
};@vertex
fn vs(vert: Vertex) -> VSOutput {var vsOut: VSOutput;vsOut.position = vec4f(vert.position * vert.scale + vert.offset, 0.0, 1.0);vsOut.color = vert.color * vec4f(vert.perVertexColor, 1);return vsOut;
}@fragment
fn fs(vsOut: VSOutput) -> @location(0) vec4f {return vsOut.color;
}
索引缓冲区
这里要介绍的最后一件事是索引缓冲区。索引缓冲区描述了处理和使用顶点的顺序。
你可以把绘制看成是按顺序遍历顶点
0, 1, 2, 3, 4, 5, .....
通过索引缓冲区,我们可以改变这个顺序。
我们为每个圆的细分创建了6个顶点,尽管其中2个是相同的。
现在,我们只创建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 vertices at each subdivision, + 1 to wrap around the circle.const numVertices = (numSubdivisions + 1) * 2;// 2 32-bit values for position (xy) and 1 32-bit value for color (rgb)// The 32-bit color value will be written/read as 4 8-bit valuesconst vertexData = new Float32Array(numVertices * (2 + 1));const colorData = new Uint8Array(vertexData.buffer);let offset = 0;let colorOffset = 8;const addVertex = (x, y, r, g, b) => {vertexData[offset++] = x;vertexData[offset++] = y;offset += 1; // skip the colorcolorData[colorOffset++] = r * 255;colorData[colorOffset++] = g * 255;colorData[colorOffset++] = b * 255;colorOffset += 9; // skip extra byte and the position};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 triangleindexData[ndx++] = ndxOffset;indexData[ndx++] = ndxOffset + 1;indexData[ndx++] = ndxOffset + 2;// second triangleindexData[ndx++] = ndxOffset + 2;indexData[ndx++] = ndxOffset + 1;indexData[ndx++] = ndxOffset + 3;}return {positionData,colorData,indexData,numVertices: indexData.length,};
}
然后我们需要创建一个索引缓冲区
const { vertexData, indexData, numVertices } = createCircleVertices({radius: 0.5,innerRadius: 0.25,});const vertexBuffer = device.createBuffer({label: 'vertex buffer',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);
注意,我们将用法设置为INDEX。
最后,在绘制时,我们需要指定索引缓冲区
pass.setPipeline(pipeline);pass.setVertexBuffer(0, vertexBuffer);pass.setVertexBuffer(1, staticVertexBuffer);pass.setVertexBuffer(2, changingVertexBuffer);pass.setIndexBuffer(indexBuffer, 'uint32');
因为我们的缓冲区包含32位无符号整数索引,我们需要在这里传递uint32
。我们也可以使用16位无符号索引,在这种情况下,我们需要传入uint16
。
我们需要调用drawindex而不是draw
pass.drawIndexed(numVertices, kNumObjects);
这样,我们节省了一些空间(33%),并且在顶点着色器中计算顶点时的潜在处理量类似,因为GPU可以重用它已经计算过的顶点。
以下为完整代码及运行结果:
HTML:
<!--* @Description: * @Author: tianyw* @Date: 2022-11-11 12:50:23* @LastEditTime: 2023-10-09 21:42:18* @LastEditors: tianyw
-->
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>001hello-triangle</title><style>html,body {margin: 0;width: 100%;height: 100%;background: #000;color: #fff;display: flex;text-align: center;flex-direction: column;justify-content: center;}div,canvas {height: 100%;width: 100%;}</style>
</head><body><div id="007vertex-srandom-circle5"><canvas id="gpucanvas"></canvas></div><script type="module" src="./007vertex-srandom-circle5.ts"></script></body></html>
TS:
/** @Description:* @Author: tianyw* @Date: 2023-04-08 20:03:35* @LastEditTime: 2023-10-09 21:53:07* @LastEditors: tianyw*/
export type SampleInit = (params: {canvas: HTMLCanvasElement;
}) => void | Promise<void>;import shaderWGSL from "./shaders/shader.wgsl?raw";const rand = (min: undefined | number = undefined,max: undefined | number = undefined
) => {if (min === undefined) {min = 0;max = 1;} else if (max === undefined) {max = min;min = 0;}return min + Math.random() * (max - min);
};function createCircleVertices({radius = 1,numSubdivisions = 24,innerRadius = 0,startAngle = 0,endAngle = Math.PI * 2
} = {}) {// 2 triangles per subdivision, 3 verts per triconst numVertices = (numSubdivisions + 1) * 2;// 2 32-bit values for position(xy) and 1 32-bit value for color(rgb_)// the 32-bit color value will be written/read as 4 8-bit valuesconst vertexData = new Float32Array(numVertices * (2 + 1));const colorData = new Uint8Array(vertexData.buffer);let offset = 0;let colorOffset = 8;const addVertex = (x: number, y: number, r: number, g: number, b: number) => {vertexData[offset++] = x;vertexData[offset++] = y;offset += 1; // skip the colorcolorData[colorOffset++] = r * 255;colorData[colorOffset++] = g * 255;colorData[colorOffset++] = b * 255;colorOffset += 9; // skip extra byte and the position};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[0],outerColor[1],outerColor[2]);addVertex(c1 * innerRadius,s1 * innerRadius,innerColor[0],innerColor[1],innerColor[2]);}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 triangleindexData[ndx++] = ndxOffset;indexData[ndx++] = ndxOffset + 1;indexData[ndx++] = ndxOffset + 2;// second triangleindexData[ndx++] = ndxOffset + 2;indexData[ndx++] = ndxOffset + 1;indexData[ndx++] = ndxOffset + 3;}return {vertexData,indexData,numVertices: indexData.length};
}const init: SampleInit = async ({ canvas }) => {const adapter = await navigator.gpu?.requestAdapter();if (!adapter) return;const device = await adapter?.requestDevice();if (!device) {console.error("need a browser that supports WebGPU");return;}const context = canvas.getContext("webgpu");if (!context) return;const devicePixelRatio = window.devicePixelRatio || 1;canvas.width = canvas.clientWidth * devicePixelRatio;canvas.height = canvas.clientHeight * devicePixelRatio;const presentationFormat = navigator.gpu.getPreferredCanvasFormat();context.configure({device,format: presentationFormat,alphaMode: "premultiplied"});const shaderModule = device.createShaderModule({label: "our hardcoded rgb triangle shaders",code: shaderWGSL});const renderPipeline = device.createRenderPipeline({label: "per vertex color",layout: "auto",vertex: {module: shaderModule,entryPoint: "vs",buffers: [{arrayStride: 2 * 4 + 4, // 2 floats, 4 bytes each + 4 bytesattributes: [{shaderLocation: 0,offset: 0,format: "float32x2"}, // position{shaderLocation: 4,offset: 8,format: "unorm8x4"} // position]},{arrayStride: 4 + 2 * 4, // 4 bytes + 2 floats,4 bytes eachstepMode: "instance",attributes: [{shaderLocation: 1,offset: 0,format: "unorm8x4" // color},{shaderLocation: 2,offset: 4,format: "float32x2" // offset}]},{arrayStride: 2 * 4, // 2 floats, 4 bytes eachstepMode: "instance",attributes: [{shaderLocation: 3,offset: 0,format: "float32x2"} // scale]}]},fragment: {module: shaderModule,entryPoint: "fs",targets: [{format: presentationFormat}]},primitive: {// topology: "line-list"// topology: "line-strip"// topology: "point-list"topology: "triangle-list"// topology: "triangle-strip"}});const kNumObjects = 100;const staticUnitSize =4 + // color is 4 bytes2 * 4; // offset is 2 32bit floats (4bytes each)const changingUnitSize = 2 * 4; // scale is 2 32 bit floatsconst staticVertexBufferSize = staticUnitSize * kNumObjects;const changingVertexBufferSize = changingUnitSize * kNumObjects;const staticVertexBuffer = device.createBuffer({label: "static vertex for objects",size: staticVertexBufferSize,usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST});const changingVertexBuffer = device.createBuffer({label: "changing vertex for objects",size: changingVertexBufferSize,usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST});const staticVertexValuesU8 = new Uint8Array(staticVertexBufferSize);const staticVertexValuesF32 = new Float32Array(staticVertexValuesU8.buffer);const changingVertexValues = new Float32Array(changingVertexBufferSize / 4);const kColorOffset = 0;const kOffsetOffset = 1;const kScaleOffset = 0;const objectInfos: {scale: number;}[] = [];for (let i = 0; i < kNumObjects; ++i) {const staticOffsetU8 = i * staticUnitSize;const staticOffsetF32 = staticOffsetU8 / 4;staticVertexValuesU8.set([rand() * 255, rand() * 255, rand() * 255, 255],staticOffsetU8 + kColorOffset);staticVertexValuesF32.set([rand(-0.9, 0.9), rand(-0.9, 0.9)],staticOffsetF32 + kOffsetOffset);objectInfos.push({scale: rand(0.2, 0.5)});}device.queue.writeBuffer(staticVertexBuffer, 0, staticVertexValuesF32);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);function frame() {const aspect = canvas.width / canvas.height;const renderCommandEncoder = device.createCommandEncoder({label: "render vert frag"});if (!context) return;const textureView = context.getCurrentTexture().createView();const renderPassDescriptor: GPURenderPassDescriptor = {label: "our basic canvas renderPass",colorAttachments: [{view: textureView,clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },loadOp: "clear",storeOp: "store"}]};const renderPass =renderCommandEncoder.beginRenderPass(renderPassDescriptor);renderPass.setPipeline(renderPipeline);renderPass.setVertexBuffer(0, vertexBuffer);objectInfos.forEach(({ scale }, ndx) => {const offset = ndx * (changingUnitSize / 4);changingVertexValues.set([scale / aspect, scale], offset + kScaleOffset); // set the scale});device.queue.writeBuffer(changingVertexBuffer, 0, changingVertexValues);renderPass.setVertexBuffer(1, staticVertexBuffer);renderPass.setVertexBuffer(2, changingVertexBuffer);renderPass.setIndexBuffer(indexBuffer, "uint32");renderPass.drawIndexed(numVertices, kNumObjects);renderPass.end();const renderBuffer = renderCommandEncoder.finish();device.queue.submit([renderBuffer]);requestAnimationFrame(frame);}requestAnimationFrame(frame);
};const canvas = document.getElementById("gpucanvas") as HTMLCanvasElement;
init({ canvas: canvas });
Shaders:
shader.wgsl:
struct Vertex {@location(0) position: vec2f,@location(1) color: vec4f,@location(2) offset: vec2f,@location(3) scale: vec2f,@location(4) perVertexColor: vec3f,
};struct VSOutput {@builtin(position) position: vec4f,@location(0) color: vec4f
};@vertex
fn vs(vert: Vertex) -> VSOutput {var vsOut: VSOutput;vsOut.position = vec4f(vert.position * vert.scale + vert.offset, 0.0, 1.0);vsOut.color = vert.color * vec4f(vert.perVertexColor, 1);return vsOut;
}@fragment
fn fs(vsOut: VSOutput) -> @location(0) vec4f {return vsOut.color;
}
请注意,我们还可以将索引缓冲区与上一篇文章中的存储缓冲区示例一起使用。在这种情况下,传入的@builtin(vertex_index)的值与索引缓冲区中的索引匹配。
接下来我们将介绍纹理。