|
| 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 | + |
0 commit comments