Skip to content

Commit 8a85afa

Browse files
committed
Add next version of Index Buffer
1 parent a74768f commit 8a85afa

File tree

5 files changed

+352
-7
lines changed

5 files changed

+352
-7
lines changed
202 KB
Loading

images/index-buffer/quad.png

242 KB
Loading
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
Index Buffer <span class="bullet">🟢</span>
2+
============
3+
4+
```{lit-setup}
5+
:tangle-root: 034 - Index Buffer - Next - vanilla
6+
:parent: 033 - Multiple Attributes - Option A - Next - vanilla
7+
:alias: Vanilla
8+
```
9+
10+
```{lit-setup}
11+
:tangle-root: 034 - Index Buffer - Next
12+
:parent: 033 - Multiple Attributes - Option A - Next
13+
```
14+
15+
````{tab} With webgpu.hpp
16+
*Resulting code:* [`step034-next`](https://github.com/eliemichel/LearnWebGPU-Code/tree/step034-next)
17+
````
18+
19+
````{tab} Vanilla webgpu.h
20+
*Resulting code:* [`step034-next-vanilla`](https://github.com/eliemichel/LearnWebGPU-Code/tree/step034-next-vanilla)
21+
````
22+
23+
The index buffer is used to **separate** the list of **vertex attributes** from the actual order in which they are **connected**. To illustrate its interest, let us draw a square, which is made of 2 triangles.
24+
25+
```{themed-figure} /images/quad-{theme}.plain.svg
26+
:align: center
27+
```
28+
29+
Index data
30+
----------
31+
32+
A straightforward way of drawing such a square is to use the following vertex attribtues:
33+
34+
```C++
35+
std::vector<float> vertexData = {
36+
// Triangle #0
37+
-0.5, -0.5, // A
38+
+0.5, -0.5,
39+
+0.5, +0.5, // C
40+
41+
// Triangle #1
42+
-0.5, -0.5, // A
43+
+0.5, +0.5, // C
44+
-0.5, +0.5,
45+
};
46+
```
47+
48+
But as you can see some **data is duplicated** (points $A$ and $C$). And this duplication could be much worst on larger shapes with connected triangles.
49+
50+
A more compact way of expressing the square's geometry is to separate the **position** from the **connectivity**:
51+
52+
```C++
53+
// Define point data
54+
// The de-duplicated list of point positions
55+
std::vector<float> pointData = {
56+
-0.5, -0.5, // Point #0 (A)
57+
+0.5, -0.5, // Point #1
58+
+0.5, +0.5, // Point #2 (C)
59+
-0.5, +0.5, // Point #3
60+
};
61+
62+
// Define index data
63+
// This is a list of indices referencing positions in the pointData
64+
std::vector<uint16_t> indexData = {
65+
0, 1, 2, // Triangle #0 connects points #0, #1 and #2
66+
0, 2, 3 // Triangle #1 connects points #0, #2 and #3
67+
};
68+
```
69+
70+
The index data must have type `uint16_t` or `uint32_t`. The former is more compact but limited to $2^{16} = 65 536$ vertices.
71+
72+
````{note}
73+
I also keep the interleaved color attribute in this example, my point data is:
74+
75+
```{lit} C++, Define point data (also for tangle root "Vanilla")
76+
std::vector<float> pointData = {
77+
// x, y, r, g, b
78+
-0.5, -0.5, 1.0, 0.0, 0.0,
79+
+0.5, -0.5, 0.0, 1.0, 0.0,
80+
+0.5, +0.5, 0.0, 0.0, 1.0,
81+
-0.5, +0.5, 1.0, 1.0, 0.0
82+
};
83+
```
84+
85+
```{lit} C++, Define index data (hidden, also for tangle root "Vanilla")
86+
// This is a list of indices referencing positions in the pointData
87+
std::vector<uint16_t> indexData = {
88+
0, 1, 2, // Triangle #0 connects points #0, #1 and #2
89+
0, 2, 3 // Triangle #1 connects points #0, #2 and #3
90+
};
91+
```
92+
93+
Using the index buffer adds an **overhead** of `6 * sizeof(uint16_t)` = 12 bytes **but also saves** `2 * 5 * sizeof(float)` = 40 bytes, so even on this very simple example it is worth using.
94+
````
95+
96+
This split of data reorganizes our buffer initialization method:
97+
98+
```{lit} C++, InitializeBuffers method (replace, also for tangle root "Vanilla")
99+
bool Application::InitializeBuffers() {
100+
{{Define point data}}
101+
{{Define index data}}
102+
103+
// We now store the index count rather than the vertex count
104+
m_indexCount = static_cast<uint32_t>(indexData.size());
105+
106+
{{Create point buffer}}
107+
{{Create index buffer}}
108+
return true;
109+
}
110+
```
111+
112+
````{topic} Terminology
113+
I usually replace the name **vertex** data with **point** data when referring to the de-duplicated attribute buffer. In other terms, `vertex[i] = points[index[i]]`. The name *vertex* is used to mean a **corner** of triangle, i.e., a pair of a point and a triangle that uses it.
114+
115+
```{lit} C++, Create point buffer (hidden)
116+
// Create point buffer
117+
BufferDescriptor bufferDesc = Default;
118+
bufferDesc.size = pointData.size() * sizeof(float);
119+
bufferDesc.usage = BufferUsage::CopyDst | BufferUsage::Vertex; // Vertex usage here!
120+
m_pointBuffer = m_device.createBuffer(bufferDesc);
121+
122+
// Upload geometry data to the buffer
123+
m_queue.writeBuffer(m_pointBuffer, 0, pointData.data(), bufferDesc.size);
124+
```
125+
126+
```{lit} C++, Create point buffer (hidden, for tangle root "Vanilla")
127+
// Create point buffer
128+
WGPUBufferDescriptor bufferDesc = WGPU_BUFFER_DESCRIPTOR_INIT;
129+
bufferDesc.size = pointData.size() * sizeof(float);
130+
bufferDesc.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex; // Vertex usage here!
131+
m_pointBuffer = wgpuDeviceCreateBuffer(m_device, &bufferDesc);
132+
133+
// Upload geometry data to the buffer
134+
wgpuQueueWriteBuffer(m_queue, m_pointBuffer, 0, pointData.data(), bufferDesc.size);
135+
```
136+
````
137+
138+
In the list of **application attributes**, we replace `m_vertexBuffer` with `m_pointBuffer` and `m_indexBuffer`, and replace `m_vertexCount` with `m_indexCount`.
139+
140+
````{tab} With webgpu.hpp
141+
```{lit} C++, Application attributes (append)
142+
private: // Application attributes
143+
wgpu::Buffer m_pointBuffer;
144+
wgpu::Buffer m_indexBuffer;
145+
uint32_t m_indexCount;
146+
```
147+
````
148+
149+
````{tab} Vanilla webgpu.h
150+
```{lit} C++, Application attributes (append, for tangle root "Vanilla")
151+
private: // Application attributes
152+
WGPUBuffer m_pointBuffer;
153+
WGPUBuffer m_indexBuffer;
154+
uint32_t m_indexCount;
155+
```
156+
````
157+
158+
And as usual, we release buffers in `Terminate()`
159+
160+
````{tab} With webgpu.hpp
161+
```{lit} C++, Terminate (prepend)
162+
m_pointBuffer.release();
163+
m_indexBuffer.release();
164+
```
165+
````
166+
167+
````{tab} Vanilla webgpu.h
168+
```{lit} C++, Terminate (prepend, for tangle root "Vanilla")
169+
wgpuBufferRelease(m_pointBuffer);
170+
wgpuBufferRelease(m_indexBuffer);
171+
```
172+
````
173+
174+
```{lit} C++, Create point buffer (hidden, append, also for tangle root "Vanilla")
175+
// It is not easy with the auto-generation of code to remove the previously
176+
// defined `vertexBuffer` attribute, but at the same time some compilers
177+
// (rightfully) complain if we do not use it. This is a hack to mark the
178+
// variable as used and have automated build tests pass.
179+
(void)m_vertexBuffer;
180+
(void)m_vertexCount;
181+
```
182+
183+
Buffer creation
184+
---------------
185+
186+
Of course the **index data** must be stored in a **GPU-side buffer**. This buffer needs a usage of `BufferUsage::Index`.
187+
188+
````{tab} With webgpu.hpp
189+
```{lit} C++, Create index buffer
190+
// Create index buffer
191+
// (we reuse the bufferDesc initialized for the vertexBuffer)
192+
bufferDesc.size = indexData.size() * sizeof(uint16_t);
193+
{{Fix buffer size}}
194+
bufferDesc.usage = BufferUsage::CopyDst | BufferUsage::Index;
195+
m_indexBuffer = m_device.createBuffer(bufferDesc);
196+
197+
m_queue.writeBuffer(m_indexBuffer, 0, indexData.data(), bufferDesc.size);
198+
```
199+
````
200+
201+
````{tab} Vanilla webgpu.h
202+
```{lit} C++, Create index buffer (for tangle root "Vanilla")
203+
// Create index buffer
204+
// (we reuse the bufferDesc initialized for the vertexBuffer)
205+
bufferDesc.size = indexData.size() * sizeof(uint16_t);
206+
{{Fix buffer size}}
207+
bufferDesc.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Index;;
208+
m_indexBuffer = wgpuDeviceCreateBuffer(m_device, &bufferDesc);
209+
210+
wgpuQueueWriteBuffer(m_queue, m_indexBuffer, 0, indexData.data(), bufferDesc.size);
211+
```
212+
````
213+
214+
````{important}
215+
A `writeBuffer` operation must copy a number of bytes that is a **multiple of 4**. To ensure this, we must **ceil the buffer size** up to the next multiple of 4 before creating it:
216+
217+
```{lit} C++, Fix buffer size (also for tangle root "Vanilla")
218+
bufferDesc.size = (bufferDesc.size + 3) & ~3; // round up to the next multiple of 4
219+
```
220+
221+
This means that we must also make sure that `indexData.size()` is a multiple of 2 (because `sizeof(uint16_t)` is 2):
222+
223+
```{lit} C++, Fix buffer size (append, also for tangle root "Vanilla")
224+
indexData.resize((indexData.size() + 1) & ~1); // round up to the next multiple of 2
225+
```
226+
````
227+
228+
Render pass
229+
-----------
230+
231+
To draw with an index buffer, there are **two changes** in the render pass encoding:
232+
233+
1. Set the active index buffer with `renderPass.setIndexBuffer`.
234+
2. Replace `draw()` with `drawIndexed()`.
235+
236+
````{tab} With webgpu.hpp
237+
```{lit} C++, Use Render Pass (replace)
238+
renderPass.setPipeline(m_pipeline);
239+
240+
// Set both vertex and index buffers
241+
renderPass.setVertexBuffer(0, m_pointBuffer, 0, m_pointBuffer.getSize());
242+
// The second argument must correspond to the choice of uint16_t or uint32_t
243+
// we've done when creating the index buffer.
244+
renderPass.setIndexBuffer(m_indexBuffer, IndexFormat::Uint16, 0, m_indexBuffer.getSize());
245+
246+
// Replace `draw()` with `drawIndexed()` and `m_vertexCount` with `m_indexCount`
247+
// The extra argument is an offset within the index buffer.
248+
renderPass.drawIndexed(m_indexCount, 1, 0, 0, 0);
249+
```
250+
````
251+
252+
````{tab} Vanilla webgpu.h
253+
```{lit} C++, Use Render Pass (replace, for tangle root "Vanilla")
254+
wgpuRenderPassEncoderSetPipeline(renderPass, m_pipeline);
255+
256+
// Set both vertex and index buffers
257+
wgpuRenderPassEncoderSetVertexBuffer(renderPass, 0, m_pointBuffer, 0, wgpuBufferGetSize(m_pointBuffer));
258+
// The second argument must correspond to the choice of uint16_t or uint32_t
259+
// we've done when creating the index buffer.
260+
wgpuRenderPassEncoderSetIndexBuffer(renderPass, m_indexBuffer, WGPUIndexFormat_Uint16, 0, wgpuBufferGetSize(m_indexBuffer));
261+
262+
// Replace `draw()` with `drawIndexed()` and `m_vertexCount` with `m_indexCount`
263+
// The extra argument is an offset within the index buffer.
264+
wgpuRenderPassEncoderDrawIndexed(renderPass, m_indexCount, 1, 0, 0, 0);
265+
```
266+
````
267+
268+
We now see our "square", with color highlighting how the red and blue points ($A$ and $C$) are shared by both triangles:
269+
270+
```{figure} /images/index-buffer/deformed-quad.png
271+
:align: center
272+
:class: with-shadow
273+
The square is deformed because of our window's aspect ratio.
274+
```
275+
276+
Ratio correction
277+
----------------
278+
279+
The square we obtained is deformed because its coordinates are expressed **relative to the window's dimensions**. This can be fixed by multiplying one of the coordinates by the ratio of the window (which is $640/480$ in our case).
280+
281+
We could do this either in the initial vertex data vector, but this will require is to update these values whenever the window dimension changes. A more interesting option is to use the **power of the vertex shader**:
282+
283+
```{lit} rust, Vertex shader position (also for tangle root "Vanilla")
284+
// In vs_main():
285+
let ratio = 640.0 / 480.0; // The width and height of the target surface
286+
out.position = vec4f(in.position.x, in.position.y * ratio, 0.0, 1.0);
287+
```
288+
289+
```{lit} rust, Vertex shader body (replace, hidden, also for tangle root "Vanilla")
290+
var out: VertexOutput; // create the output struct
291+
{{Vertex shader position}}
292+
out.color = in.color; // forward the color attribute to the fragment shader
293+
return out;
294+
```
295+
296+
Although basic, this is a first step towards what will be the key use of the vertex shader when introducing **3D transforms**.
297+
298+
```{note}
299+
It might feel a little unsatisfying to hard-code the window resolution in the shader like this, but we will quickly see how to make this more flexible thanks to uniforms.
300+
```
301+
302+
```{figure} /images/index-buffer/quad.png
303+
:align: center
304+
:class: with-shadow
305+
The expected square
306+
```
307+
308+
Conclusion
309+
----------
310+
311+
Using an index buffer is a rather **simple concept** in the end, and can save a lot of VRAM (GPU memory).
312+
313+
Additionally, it corresponds to the way traditional formats usually encode 3D meshes in order to **keep the connectivity information**, which is important for **authoring** (so that when the user moves a points all triangles that share this point are affected).
314+
315+
Note however that this only works when the common vertices **share all their attributes**. Two vertices that have the **same position but a different color** are still considered as **different points**, because the vertex shader must run for each of them individually.
316+
317+
````{tab} With webgpu.hpp
318+
*Resulting code:* [`step034-next`](https://github.com/eliemichel/LearnWebGPU-Code/tree/step034-next)
319+
````
320+
321+
````{tab} Vanilla webgpu.h
322+
*Resulting code:* [`step034-next-vanilla`](https://github.com/eliemichel/LearnWebGPU-Code/tree/step034-next-vanilla)
323+
````
324+
325+

next/basic-3d-rendering/input-geometry/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ Contents
99
1010
a-first-vertex-attribute
1111
multiple-attributes
12+
index-buffer
1213
```

0 commit comments

Comments
 (0)