1.13 Index Buffers
Before implementing model loading in the next tutorial, we need to explore the concept of index buffers. While we previously examined different triangle rendering topologies like triangle-list
, we encountered a drawback: redundant vertices shared by multiple triangles.
Although triangle-strip
is more memory-efficient, not all objects can be easily represented using this topology without manual partitioning of 3D meshes. For instance, a simple cube requires three triangle-strip
s: one for vertical walls and two for the top and bottom. For complex 3D models like a teapot, this partitioning becomes non-trivial, potentially resulting in hundreds or thousands of pieces - far from ideal.
Index buffers offer an alternative solution. The concept is straightforward:
Maintain a pool of unique vertices, regardless of their connectivity.
Introduce an index buffer that stores indices into the vertex pool, defining vertex connectivity.
Instead of duplicating vertex data (coordinates, colors, texture coordinates), only store redundant indices for shared vertices.
This approach saves significant storage space since the only duplications are indices, but an index is only an integer. Index buffer is also a more generic solution for memory saving.
Let's implement index buffers. Note that the shader code remains unchanged as the graphics pipeline handles topology and vertex processing.
const positions = new Float32Array([
-100.0, 100.0, 0.0,
-100.0, 100.0, 200.0,
100.0, 100.0, 0.0,
100.0, 100.0, 200.0,
100.0, -100.0, 0.0,
100.0, -100.0, 200.0,
-100.0, -100.0, 0.0,
-100.0, -100.0, 200.0,
]);
const positionBuffer = createGPUBuffer(device, positions, GPUBufferUsage.VERTEX);
We define the vertex pool with all 8 unique vertices of the cube. The index buffers will handle vertex topology.
const indices = new Uint16Array([0, 1, 2, 3, 4, 5, 6, 7, 0, 1]);
const indexBuffer = createGPUBuffer(device, indices, GPUBufferUsage.INDEX);
const indices2 = new Uint16Array([3, 1, 5, 7]);
const index2Buffer = createGPUBuffer(device, indices2, GPUBufferUsage.INDEX);
We create two index buffers: one for the vertical walls (a single triangle-strip
) and another for the cube's bottom (the cube has an open lid). We use Uint16Array
for the index buffer, as 16 bits suffice for simple geometry like a cube.
primitive: {
topology: 'triangle-strip',
stripIndexFormat: 'uint16',
frontFace: 'ccw',
cullMode: 'none'
},
In the pipeline definition, we specify the topology as triangle-strip
and set stripIndexFormat
to uint16
, indicating the use of an index buffer for drawing.
passEncoder = commandEncoder.beginRenderPass(renderPassDesc);
passEncoder.setViewport(0, 0, canvas.width, canvas.height, 0, 1);
passEncoder.setPipeline(pipeline);
passEncoder.setBindGroup(0, uniformBindGroup);
passEncoder.setVertexBuffer(0, positionBuffer);
passEncoder.setIndexBuffer(indexBuffer, 'uint16');
passEncoder.drawIndexed(10);
passEncoder.setIndexBuffer(index2Buffer, 'uint16');
passEncoder.drawIndexed(4);
passEncoder.end();
To issue draw commands, we first set the vertex buffer (positionBuffer
) to supply the vertex pool. For each draw call, we specify the index buffer using setIndexBuffer
and call drawIndexed
with the number of indices to render. The GPU pipeline uses the index buffer to retrieve vertex indices and fetch the corresponding vertices, passing them to the shader. This approach achieves the same result as previous rendering but with less data sent to the GPU.
It's important to note that while we initially discussed the limitations of triangle-strip
, in this example we still manually partition an open cube into two triangle-strip
s, despite using index buffers. The use of an index buffer doesn't conflict with your choice of triangle topology. Even if you opt for triangle-list
, using index buffer can still achieve substantial data size savings.