This is the seventh day of my participation in the First Challenge 2022. For details: First Challenge 2022.
preface
Following Hello WebGPU — Rotating Cube — Digging (juejin. Cn) and Hello WebGPU — Mapping manipulation — digging (juejin. Cn), this time we will continue to work on the cube, this time we will focus on increasing the number of cubes. First, let’s start by drawing two cubes.
Draw 2 cubes
Now we want to draw two cubes that are exactly the same shape, but in different positions. What do we do?
First of all, we definitely want to reuse the vertex data of the cube, we just want to change its transformation matrix and change its position. It would be easy to create two buffers, one holding the transformation matrix of the first cube and the other holding the transformation matrix of the second cube.
const matrixSize = 4 * 16; // 4x4 matrix
const uniformBuffer = device.createBuffer({
size: matrixSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const uniformBuffer2 = device.createBuffer({
size: matrixSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
Copy the code
Now that we have two buffers, we also need two sets of BindGroups.
const uniformBindGroup1 = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{binding: 0.resource: {
buffer: uniformBuffer,
offset: 0.size: matrixSize,
},
},
],
});
const uniformBindGroup2 = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{binding: 0.resource: {
buffer: uniformBuffer2,
offset: 0.size: matrixSize,
},
},
],
});
Copy the code
OK, now we just need to update the data in these two buffers during rendering before rendering.
device.queue.writeBuffer(
uniformBuffer,
0,
modelViewProjectionMatrix1.buffer,
modelViewProjectionMatrix1.byteOffset,
modelViewProjectionMatrix1.byteLength
);
device.queue.writeBuffer(
uniformBuffer2,
0,
modelViewProjectionMatrix2.buffer,
modelViewProjectionMatrix2.byteOffset,
modelViewProjectionMatrix2.byteLength
);
/ /... The code is partially omitted here
passEncoder.setVertexBuffer(0, verticesBuffer);
// Bind the bind group (with the transformation matrix) for
// each cube, and draw.
passEncoder.setBindGroup(0, uniformBindGroup1);
passEncoder.draw(cubeVertexCount, 1.0.0);
passEncoder.setBindGroup(0, uniformBindGroup2);
passEncoder.draw(cubeVertexCount, 1.0.0);
Copy the code
Thus, we have rendered two identical cubes.
Small optimization
Here is a small optimization trick, we can put the information of the two transformation matrices in a Buffer, by setting different offsets to distinguish. Like this:
const matrixSize = 4 * 16; // 4x4 matrix
const offset = 256; // uniformBindGroup offset must be 256-byte aligned
const uniformBufferSize = offset + matrixSize;
const uniformBuffer = device.createBuffer({
size: uniformBufferSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
Copy the code
Here, our Buffer size is changed to offset + matrixSize, where offset must be a multiple of 4 and the minimum must be 256 due to some limitations on memory alignment in the WebGPU. Detailed information is available WebGPU – minUniformBufferOffsetAlignment.
But we still need two Bindgroups to tell the GPU that it should start reading memory from where it starts
const uniformBindGroup2 = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{binding: 0.resource: {
buffer: uniformBuffer,
offset: offset,
size: matrixSize,
},
},
],
});
Copy the code
The subsequent rendering process is the same, but the data is written to different starting positions in the same Buffer.
Draw N cubes
Now, we have drawn two identical cubes, so now we need to draw N cubes, where N could be 3, 4, 5, 6… 200 or more. Should we draw two cubes like the one above? The answer is yes, for example we can use an array to hold our BindGroup
const bindGroups: GPUBindGroup[] = [];
for (let i = 0; i < cubeNums; i++) {
const group = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{binding: 0.resource: {
buffer: uniformBuffer,
offset: offset * i,
size: matrixSize,
},
},
],
});
bindGroups.push(group);
}
Copy the code
Draw and reuse a loop to draw
for (let i = 0; i < cubeNums; i++) {
const mat = mvpMatricesData.subarray(i * 16, (i + 1) * 16);
device.queue.writeBuffer(
uniformBuffer,
i * offset,
mat.buffer,
mat.byteOffset,
mat.byteLength
);
passEncoder.setBindGroup(0, bindGroups[i]);
passEncoder.draw(cubeVertexCount, 1.0.0);
}
Copy the code
It is certainly possible to draw multiple cubes in this way. However, WebGPU provides us with a much more convenient, friendly and efficient method of drawing — instaced Drawing.
A geometry usually needs to be rendered once. If we want to draw multiple shapes, we can only render one by one, not all at once, because each shape has different attributes such as vertices, colors, positions, etc. However, if the vertex data of the geometry is the same, the color, position and other attributes can be calculated in the shader, then we can use the multi-instance rendering method supported by WebGPU to render all the graphics at one time.
Instanced Drawing
Now let’s continue to modify our code. We don’t need multiple buffers for multi-instance drawing. We only need one Buffer, but to write data to this Buffer, we need to write all the transformation matrices for the cube.
const uniformBindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{binding: 0.resource: {
buffer: uniformBuffer,
},
},
],
});
Copy the code
The rest of the rendering steps are the same as drawing a cube, except that the last call to the draw command provides the number of instances to draw:
passEncoder.draw(cubeVertexCount, numInstances, 0.0);
Copy the code
Modify the Shader
In addition to deleting, we also need to modify our vertex shader program
fn main( [[builtin(instance_index)]] instanceIdx: u32, [[location(0)]] position: vec4<f32>, [[location(1)]] uv: vec2<f32>) -> VertexOutput { var output : VertexOutput; output.Position = uniforms.modelViewProjectionMatrix[instanceIdx] * position; output.fragUV = uv; FragPosition = 0.5 * (position + vec4<f32>(1.0, 1.0, 1.0, 1.0)); return output; }Copy the code
As we can see, the first argument our vertex shader accepts is a built-in variable, similar to the one we explained in Hello, WebGPU — Draw the first triangle — dig (juejin. Cn), [[builtin(instance_index)]]] to get the index of a multi-instance, and then get the matrix of a Uniform object based on the index value.
The results are as follows:
conclusion
Today we learned how to draw multiple cubes. Starting from drawing two cubes, we can draw multiple cubes by using multiple BindGroups and calling multiple drawCalls, or we can use the multi-instance drawing method provided by WebGPU. Multi-instance rendering provides a very simple way for us to easily draw graphs like vertex data, and this method has very good performance, I hope you can carefully experience this way of rendering.
OK, today’s content is relatively simple, if you think this article is good, don’t forget to give it a thumbs up