五、WebGPU Vertex Buffers 顶点缓冲区

五、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 formatData typeComponentsByte sizeExample WGSL type
"uint8x2"unsigned int22vec2<u32>, vec2u
"uint8x4"unsigned int44vec4<u32>, vec4u
"sint8x2"signed int22vec2<i32>, vec2i
"sint8x4"signed int44vec4<i32>, vec4i
"unorm8x2"unsigned normalized22vec2<f32>, vec2f
"unorm8x4"unsigned normalized44vec4<f32>, vec4f
"snorm8x2"signed normalized22vec2<f32>, vec2f
"snorm8x4"signed normalized44vec4<f32>, vec4f
"uint16x2"unsigned int24vec2<u32>, vec2u
"uint16x4"unsigned int48vec4<u32>, vec4u
"sint16x2"signed int24vec2<i32>, vec2i
"sint16x4"signed int48vec4<i32>, vec4i
"unorm16x2"unsigned normalized24vec2<f32>, vec2f
"unorm16x4"unsigned normalized48vec4<f32>, vec4f
"snorm16x2"signed normalized24vec2<f32>, vec2f
"snorm16x4"signed normalized48vec4<f32>, vec4f
"float16x2"float24vec2<f16>, vec2h
"float16x4"float48vec4<f16>, vec4h
"float32"float14f32
"float32x2"float28vec2<f32>, vec2f
"float32x3"float312vec3<f32>, vec3f
"float32x4"float416vec4<f32>, vec4f
"uint32"unsigned int14u32
"uint32x2"unsigned int28vec2<u32>, vec2u
"uint32x3"unsigned int312vec3<u32>, vec3u
"uint32x4"unsigned int416vec4<u32>, vec4u
"sint32"signed int14i32
"sint32x2"signed int28vec2<i32>, vec2i
"sint32x3"signed int312vec3<i32>, vec3i
"sint32x4"signed int416vec4<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)的值与索引缓冲区中的索引匹配。

接下来我们将介绍纹理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/138862.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

手写一个PrattParser基本运算解析器3: 基于Swift的PrattParser的项目概述

点击查看 基于Swift的PrattParser项目 PrattParser项目概述 前段时间一直想着手恶补 编译原理 的相关知识, 一开始打算直接读大学的 编译原理, 虽然内容丰富, 但是着实抽象难懂. 无意间看到B站的熊爷关于普拉特解析器相关内容, 感觉是一个非常好的切入点.所以就写了基于Swift版…

【网络安全 --- win10系统安装】win10 系统详细安装过程(提供资源)

一&#xff0c;资源下载 百度网盘镜像下载地址链接&#xff1a; 百度网盘 请输入提取码百度网盘为您提供文件的网络备份、同步和分享服务。空间大、速度快、安全稳固&#xff0c;支持教育网加速&#xff0c;支持手机端。注册使用百度网盘即可享受免费存储空间https://pan.ba…

自定义Docker镜像--Jupyterlab

概述 自定义Jupyterlab镜像&#xff0c;为deployment做准备 步骤 下载基础镜像&#xff1a;centos:7.9.2009 docker search centos:7.9.2009 docker pull centos:7.9.2009 启动容器 部署应用 # 启动容器 docker run -it --name test centos:7.9.2009 bash# 在容器内部署…

4K壁纸小程序源码 全内容自动采集

全内容自动采集 4K壁纸小程序源码&#xff0c;带流量主。用的都是一个接口&#xff0c;不过这个不知是谁改的&#xff0c;成了LSP版&#xff0c;是真色啊&#xff0c;专搜小姐姐。 4K壁纸&#xff0c;静态壁纸&#xff0c;头像等都有保留&#xff0c;界面广告位很多&#xff0c…

手机通过WiFi连接调试UR机器人

1.测试物料 1.1ur机器人 https://item.taobao.com/item.htm?spma1z10.1-c.w4004-25069442759.18.2ff56d6bmuxX0Z&id740002623764 1.2 路由器&#xff08;TPLINK&#xff09; https://detail.tmall.com/item.htm?abbucket7&id548610924784&ns1&spma21n57.1.…

英国人工智能公司【TitanML】完成280万美元融资

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 猛兽财经获悉&#xff0c;总部位于英国伦敦的人工智能公司【TitanML】近期宣布已完成280万美元种子轮融资&#xff0c;该公司的产品允许机器学习团队部署大型语言模型(llm)。 本轮融资由Octopus Ventures领投&#xff0c;还…

Linux中的主要系统调用

Linux 操作系统中就是创建进程。创建进程的系统调用叫fork。在 Linux 里&#xff0c;要创建一个新的进程&#xff0c;需要一个老的进程调用 fork 来实现&#xff0c;其中老的进程叫作父进程&#xff08;Parent Process&#xff09;&#xff0c;新的进程叫作子进程&#xff08;C…

Redux详解(二)

1. 认识Redux Toolkit Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。 通过传统的redux编写逻辑方式&#xff0c;会造成文件分离过多&#xff0c;逻辑抽离过于繁琐&#xff08;具体可看上篇文章 Redux详解一&#xff09;&#xff0c;React官方为解决这一问题&#xff0c;推…

小样本学习--(1)概论

目录 一、概述 二、小样本学习的数据集 1、Omniglot 2、MiniimageNet 三、孪生网络 四、三元组损失函数 一、概述 小样本学习用于处理训练数据集中样本数量少的情况&#xff0c;一般来说&#xff0c;小样本学习流程是这样的&#xff0c;从一个多种类少量样本的巨大数据集…

微信小程序开发之会议oa(首页搭建)

前言&#xff1a; 上一篇我们掌握了关于小程序的框架&#xff0c;这篇博客带你完成小程序版的会议OA首页。效果如下&#xff1a; 一&#xff0c; 1.1先创建OA首页页面&#xff1a; 首先我们先建一个新项目&#xff0c;在app.json中编写代码 {"pages": ["pages/…

Tang Capital宣布收购纳斯达克上市公司Rain Oncology100%股权

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 猛兽财经获悉&#xff0c;纳斯达克上市公司Rain Oncology(Rain)宣布近期已收到Tang Capital Partners旗下的子公司Concentra Biosciences以每股1.25美元的现金收购要约。 这家临床阶段微型市值癌症治疗药物开发商的股价在消…

初识Java 14-1 测试

目录 测试 单元测试 JUnit 测试覆盖率 前置条件 断言 Java提供的断言语法 Guava提供的更方便的断言 契约式设计中的断言 DbC 单元测试 Guava中的前置条件 本笔记参考自&#xff1a; 《On Java 中文版》 测试 ||| 如果没有经过测试&#xff0c;代码就不可能正常工作…