|
| 1 | +Depth buffer <span class="bullet">🟠</span> |
| 2 | +============ |
| 3 | + |
| 4 | +```{lit-setup} |
| 5 | +:tangle-root: 052 - Depth buffer - Next - vanilla |
| 6 | +:parent: 050 - A simple example - Next - vanilla |
| 7 | +:alias: Vanilla |
| 8 | +:debug: |
| 9 | +``` |
| 10 | + |
| 11 | +```{lit-setup} |
| 12 | +:tangle-root: 052 - Depth buffer - Next |
| 13 | +:parent: 050 - A simple example - Next |
| 14 | +``` |
| 15 | + |
| 16 | +````{tab} With webgpu.hpp |
| 17 | +*Resulting code:* [`step052-next`](https://github.com/eliemichel/LearnWebGPU-Code/tree/step052-next) |
| 18 | +```` |
| 19 | + |
| 20 | +````{tab} Vanilla webgpu.h |
| 21 | +*Resulting code:* [`step052-vanilla-next`](https://github.com/eliemichel/LearnWebGPU-Code/tree/step052-vanilla-next) |
| 22 | +```` |
| 23 | + |
| 24 | +```{figure} /images/pyramid-zissue.png |
| 25 | +:align: center |
| 26 | +:class: with-shadow |
| 27 | +There is something wrong with the depth. |
| 28 | +``` |
| 29 | + |
| 30 | +The Z-Buffer algorithm |
| 31 | +---------------------- |
| 32 | + |
| 33 | +The issue we are facing with this basic example comes from the problem of **visibility**. As easy to conceive as it is, the question "does this point see that one" (i.e., does the line between them intersect any geometry) is hard to answer efficiently. |
| 34 | + |
| 35 | +In particular, when producing a **fragment**, we must figure out whether the 3D point it corresponds to is seen by the viewpoint in order decide whether it must be blended into the output texture. |
| 36 | + |
| 37 | +The **Z-Buffer algorithm** is what the GPU's render pipeline uses to solve the visibility problem: |
| 38 | + |
| 39 | + 1. **For each pixel**, it stores the depth of the last fragment that has been blended into this pixel, or a default value (that represents the furthest depth possible). |
| 40 | + 2. Each time a new fragment is produced, its **depth is compared** to this value. If the fragment depth is larger than the currently stored depth, it is **discarded** without being blended. Otherwise, it is blended normally and the stored value is updated to the depth of this new closest fragment. |
| 41 | + |
| 42 | +As a result, **only the fragment with the lowest depth is visible** in the resulting image. The depth value for each pixel is stored in a special **texture** called the **Z-buffer**. This is the only memory overhead required by the Z-buffer algorithm, making it a good fit for real time rendering. |
| 43 | + |
| 44 | +```{topic} About transparency |
| 45 | +The fact that only the fragment with the lowest depth is visible is **not guaranteed** when fragments have **opacity values that are neither 0 or 1** (and alpha-blending is used). Even worse; the order in which fragments are emitted has an impact on the result (because blending a fragment **A** and then a fragment **B** is different than blending **B** then **A**). |
| 46 | +
|
| 47 | +Long story short: **transparent objects are always a bit tricky** to handle in a Z-buffer pipeline. A simple solution is to limit the number of transparent objects, and dynamically sort them wrt. their distance to the viewpoint (this is what [Gaussian Splatting](https://en.wikipedia.org/wiki/Gaussian_splatting) does for instance. More advanced schemes exist such as [Order-independent transparency](https://en.wikipedia.org/wiki/Order-independent_transparency) techniques. |
| 48 | +``` |
| 49 | + |
| 50 | +Pipeline State |
| 51 | +-------------- |
| 52 | + |
| 53 | +Since this Z-Buffer algorithm is a **critical step** of the 3D rasterization pipeline, it is implemented as a **fixed-function** stage. We configure it through the `pipelineDesc.depthStencil` field, which we had left null so far. |
| 54 | + |
| 55 | +````{tab} With webgpu.hpp |
| 56 | +```{lit} C++, Describe render pipeline (append) |
| 57 | +DepthStencilState depthStencilState = Default; |
| 58 | +{{Describe depth/stencil state}} |
| 59 | +pipelineDesc.depthStencil = &depthStencilState; |
| 60 | +``` |
| 61 | +```` |
| 62 | +
|
| 63 | +````{tab} Vanilla webgpu.h |
| 64 | +```{lit} C++, Describe render pipeline (append, for tangle root "Vanilla") |
| 65 | +WGPUDepthStencilState depthStencilState = WGPU_DEPTH_STENCIL_STATE_INIT; |
| 66 | +{{Describe depth/stencil state}} |
| 67 | +pipelineDesc.depthStencil = &depthStencilState; |
| 68 | +``` |
| 69 | +```` |
| 70 | +
|
| 71 | +### Comparison function |
| 72 | +
|
| 73 | +The first aspect of the Z-Buffer algorithm that we can configure is the **comparison function** that is used to decide whether we should keep a new fragment or not. |
| 74 | +
|
| 75 | +**The most common choice** is to set it to `Less` to mean that a fragment is blended only if its depth is **less** than the current value of the Z-Buffer (i.e., it is closer to the viewpoint). |
| 76 | +
|
| 77 | +````{tab} With webgpu.hpp |
| 78 | +```{lit} C++, Describe depth/stencil state |
| 79 | +depthStencilState.depthCompare = CompareFunction::Less; |
| 80 | +``` |
| 81 | +```` |
| 82 | + |
| 83 | +````{tab} Vanilla webgpu.h |
| 84 | +```{lit} C++, Describe depth/stencil state (for tangle root "Vanilla") |
| 85 | +depthStencilState.depthCompare = WGPUCompareFunction_Less; |
| 86 | +``` |
| 87 | +```` |
| 88 | +
|
| 89 | +### Depth write |
| 90 | +
|
| 91 | +The second option we have is whether or not we want to **update the value** of the Z-Buffer once a fragment passes the test. It can be useful to deactivate this, when rendering user interface elements for instance, or when dealing with transparent objects, but **for a regular use case**, we indeed want to write the new depth each time a fragment is blended. |
| 92 | +
|
| 93 | +````{tab} With webgpu.hpp |
| 94 | +```{lit} C++, Describe depth/stencil state (append) |
| 95 | +depthStencilState.depthWriteEnabled = OptionalBool::True; |
| 96 | +``` |
| 97 | +```` |
| 98 | +
|
| 99 | +````{tab} Vanilla webgpu.h |
| 100 | +```{lit} C++, Describe depth/stencil state (append, for tangle root "Vanilla") |
| 101 | +depthStencilState.depthWriteEnabled = WGPUOptionalBool_True; |
| 102 | +``` |
| 103 | +```` |
| 104 | + |
| 105 | +```{note} |
| 106 | +This field uses the special type `WGPUOptionalBool`, which may be either `True`, `False` or `Undefined` because it is supposed to remain undefined when the depth/stencil state only contains a stencil and no depth buffer. |
| 107 | +``` |
| 108 | + |
| 109 | +### Texture format |
| 110 | + |
| 111 | +Lastly, we must tell the pipeline how the depth values of the Z-Buffer are **encoded** in memory. We define for this a `m_depthTextureFormat` class attribute because we need it in both `InitializePipeline()` and when configuring the surface in `Initialize()`: |
| 112 | + |
| 113 | +````{tab} With webgpu.hpp |
| 114 | +```{lit} C++, Application attributes (append) |
| 115 | +private: // In Application.h |
| 116 | + wgpu::TextureFormat m_depthTextureFormat = TextureFormat::Depth24Plus; |
| 117 | +``` |
| 118 | +```` |
| 119 | +
|
| 120 | +````{tab} Vanilla webgpu.h |
| 121 | +```{lit} C++, Application attributes (append, for tangle root "Vanilla") |
| 122 | +private: // In Application.h |
| 123 | + WGPUTextureFormat m_depthTextureFormat = WGPUTextureFormat_Depth24Plus; |
| 124 | +``` |
| 125 | +```` |
| 126 | +
|
| 127 | +We then use it when describing the depth state: |
| 128 | +
|
| 129 | +```{lit} C++, Describe depth/stencil state (append, also for tangle root "Vanilla") |
| 130 | +depthStencilState.format = m_depthTextureFormat; |
| 131 | +``` |
| 132 | +
|
| 133 | +```{important} |
| 134 | +Depth textures **do not use the same formats** as color textures, they have their own set of possible values (all starting with `Depth`). The same texture is used to represent both the depth and the *stencil* value, when enabled. It is common to use a depth encoded on **24 bits** and leave the last 8 bits to a potential stencil buffer, so that it sums it to 32, which is a nice size for byte alignment. |
| 135 | +``` |
| 136 | +
|
| 137 | +Depth texture |
| 138 | +------------- |
| 139 | +
|
| 140 | +We must allocate the texture where the GPU stores the Z-buffer ourselves. I'm going to be quick on this part, as **we will come back on textures later on**. |
| 141 | +
|
| 142 | +We first create a texture that has the size of our swap chain texture, with a usage of `RenderAttachment` and a format that matches the one declared in `depthStencilState.format`. |
| 143 | +
|
| 144 | +````{note} |
| 145 | +We create this texture next to the surface configuration (in `Initialize()`), because when we will start resizing the window, we will also want to resize the depth buffer: |
| 146 | +
|
| 147 | +```{lit} C++, Create the depth texture and view (insert in {{Initialize}} after "{{Surface Configuration}}", also for tangle root "Vanilla") |
| 148 | +// Create the depth texture after surface configuration |
| 149 | +``` |
| 150 | +```` |
| 151 | + |
| 152 | +````{tab} With webgpu.hpp |
| 153 | +```{lit} C++, Create the depth texture and view (append) |
| 154 | +// Create the depth texture |
| 155 | +TextureDescriptor depthTextureDesc = Default; |
| 156 | +depthTextureDesc.label = StringView("Z Buffer"); |
| 157 | +depthTextureDesc.usage = TextureUsage::RenderAttachment; |
| 158 | +depthTextureDesc.size = { 640, 480, 1 }; |
| 159 | +depthTextureDesc.format = m_depthTextureFormat; |
| 160 | +Texture depthTexture = m_device.createTexture(depthTextureDesc); |
| 161 | +``` |
| 162 | +```` |
| 163 | +
|
| 164 | +````{tab} Vanilla webgpu.h |
| 165 | +```{lit} C++, Create the depth texture and view (append, for tangle root "Vanilla") |
| 166 | +// Create the depth texture |
| 167 | +WGPUTextureDescriptor depthTextureDesc = WGPU_TEXTURE_DESCRIPTOR_INIT; |
| 168 | +depthTextureDesc.label = toWgpuStringView("Z Buffer"); |
| 169 | +depthTextureDesc.usage = WGPUTextureUsage_RenderAttachment; |
| 170 | +depthTextureDesc.size = { 640, 480, 1 }; |
| 171 | +depthTextureDesc.format = m_depthTextureFormat; |
| 172 | +WGPUTexture depthTexture = wgpuDeviceCreateTexture(m_device, &depthTextureDesc); |
| 173 | +``` |
| 174 | +```` |
| 175 | +
|
| 176 | +We also create a **texture view**, which is what the render pipeline expects (like we did in chapter [*First color*](../../getting-started/first-color.md)). The default view descriptor is all we need here, so we are even allowed not to specify a descriptor: |
| 177 | +
|
| 178 | +````{tab} With webgpu.hpp |
| 179 | +```{lit} C++, Create the depth texture and view (append) |
| 180 | +// Create the view of the depth texture manipulated by the rasterizer |
| 181 | +m_depthTextureView = depthTexture.createView(); |
| 182 | +
|
| 183 | +// We can now release the texture and only hold to the view |
| 184 | +depthTexture.release(); |
| 185 | +``` |
| 186 | +```` |
| 187 | + |
| 188 | +````{tab} Vanilla webgpu.h |
| 189 | +```{lit} C++, Create the depth texture and view (append, for tangle root "Vanilla") |
| 190 | +// Create the view of the depth texture manipulated by the rasterizer |
| 191 | +m_depthTextureView = wgpuTextureCreateView(depthTexture, nullptr); |
| 192 | + |
| 193 | +// We can now release the texture and only hold to the view |
| 194 | +wgpuTextureRelease(depthTexture); |
| 195 | +``` |
| 196 | +```` |
| 197 | +
|
| 198 | +The view will only be released at the end of the program, which is why we define it at the class level: |
| 199 | +
|
| 200 | +````{tab} With webgpu.hpp |
| 201 | +```{lit} C++, Application attributes (append) |
| 202 | +private: // In Application.h |
| 203 | + wgpu::TextureView m_depthTextureView = nullptr; |
| 204 | +``` |
| 205 | +```` |
| 206 | +
|
| 207 | +````{tab} Vanilla webgpu.h |
| 208 | +```{lit} C++, Application attributes (append, for tangle root "Vanilla") |
| 209 | +private: // In Application.h |
| 210 | + WGPUTextureView m_depthTextureView = nullptr; |
| 211 | +``` |
| 212 | +```` |
| 213 | + |
| 214 | +And in `Application::Terimnate()`: |
| 215 | + |
| 216 | +```{lit} C++, Terminate (hidden, prepend, also for tangle root "Vanilla") |
| 217 | +{{Release the depth texture view}} |
| 218 | +``` |
| 219 | + |
| 220 | +````{tab} With webgpu.hpp |
| 221 | +```{lit} C++, Release the depth texture view |
| 222 | +// Release the depth texture view |
| 223 | +m_depthTextureView.release(); |
| 224 | +``` |
| 225 | +```` |
| 226 | +
|
| 227 | +````{tab} Vanilla webgpu.h |
| 228 | +```{lit} C++, Release the depth texture view (for tangle root "Vanilla") |
| 229 | +// Release the depth texture view |
| 230 | +wgpuTextureViewRelease(m_depthTextureView); |
| 231 | +``` |
| 232 | +```` |
| 233 | +
|
| 234 | +```{admonition} TODO |
| 235 | +Check whether the render pass holds a reference to the texture view, in which case we can even release it right after. Then maybe define the depth texture in `InitializePipeline()` actually. |
| 236 | +``` |
| 237 | +
|
| 238 | +Depth attachment |
| 239 | +---------------- |
| 240 | +
|
| 241 | +Like when attaching a color target or binding a uniform buffer, we define an object to "connect" our depth texture to the render pipeline. This is the `RenderPassDepthStencilAttachment`: |
| 242 | +
|
| 243 | +````{tab} With webgpu.hpp |
| 244 | +```{lit} C++, Describe Render Pass (append) |
| 245 | +// We already had a color attachment |
| 246 | +// e.g., renderPassDesc.colorAttachments = &colorAttachment; |
| 247 | +
|
| 248 | +// We now add a depth/stencil attachment: |
| 249 | +RenderPassDepthStencilAttachment depthStencilAttachment = Default; |
| 250 | +{{Describe depth/stencil attachment}} |
| 251 | +renderPassDesc.depthStencilAttachment = &depthStencilAttachment; |
| 252 | +``` |
| 253 | +```` |
| 254 | + |
| 255 | +````{tab} Vanilla webgpu.h |
| 256 | +```{lit} C++, Describe Render Pass (append, for tangle root "Vanilla") |
| 257 | +// We already had a color attachment |
| 258 | +// e.g., renderPassDesc.colorAttachments = &colorAttachment; |
| 259 | + |
| 260 | +// We now add a depth/stencil attachment: |
| 261 | +WGPURenderPassDepthStencilAttachment depthStencilAttachment = WGPU_RENDER_PASS_DEPTH_STENCIL_ATTACHMENT_INIT; |
| 262 | +{{Describe depth/stencil attachment}} |
| 263 | +renderPassDesc.depthStencilAttachment = &depthStencilAttachment; |
| 264 | +``` |
| 265 | +```` |
| 266 | +
|
| 267 | +We must set up clear/store operations for the stencil part as well even if we do not use it: |
| 268 | +
|
| 269 | +````{tab} With webgpu.hpp |
| 270 | +```{lit} C++, Describe depth/stencil attachment |
| 271 | +// Describe depth/stencil attachment |
| 272 | +
|
| 273 | +// The view of the depth texture |
| 274 | +depthStencilAttachment.view = m_depthTextureView; |
| 275 | +
|
| 276 | +// The initial value of the depth buffer, meaning "far" |
| 277 | +depthStencilAttachment.depthClearValue = 1.0f; |
| 278 | +
|
| 279 | +// Operation settings comparable to the color attachment |
| 280 | +depthStencilAttachment.depthLoadOp = LoadOp::Clear; |
| 281 | +depthStencilAttachment.depthStoreOp = StoreOp::Store; |
| 282 | +
|
| 283 | +// we could turn off writing to the depth buffer globally here |
| 284 | +depthStencilAttachment.depthReadOnly = false; // NB: this is the default |
| 285 | +``` |
| 286 | +```` |
| 287 | +
|
| 288 | +````{tab} Vanilla webgpu.h |
| 289 | +```{lit} C++, Describe depth/stencil attachment (for tangle root "Vanilla") |
| 290 | +// Describe depth/stencil attachment |
| 291 | +
|
| 292 | +// The view of the depth texture |
| 293 | +depthStencilAttachment.view = m_depthTextureView; |
| 294 | +
|
| 295 | +// The initial value of the depth buffer, meaning "far" |
| 296 | +depthStencilAttachment.depthClearValue = 1.0f; |
| 297 | +
|
| 298 | +// Operation settings comparable to the color attachment |
| 299 | +depthStencilAttachment.depthLoadOp = WGPULoadOp_Clear; |
| 300 | +depthStencilAttachment.depthStoreOp = WGPUStoreOp_Store; |
| 301 | +
|
| 302 | +// we could turn off writing to the depth buffer globally here |
| 303 | +depthStencilAttachment.depthReadOnly = false; // NB: this is the default |
| 304 | +``` |
| 305 | +```` |
| 306 | + |
| 307 | +Shader |
| 308 | +------ |
| 309 | + |
| 310 | +The last thing we need to do is to set up a depth for each fragment, which we can do through the **vertex shader** (and the rasterizer will interpolate it for each fragment): |
| 311 | + |
| 312 | +```rust |
| 313 | +out.position = vec4f(position.x, position.y * ratio, /* set the depth here */ 1.0); |
| 314 | +``` |
| 315 | + |
| 316 | +The depth value must be in the range $(0,1)$. We will build a proper way to define it in the next chapter but for now let use simply remap our `position.z` from its range $(-1,1)$ to $(0,1)$: |
| 317 | + |
| 318 | +```rust |
| 319 | +out.position = vec4f(position.x, position.y * ratio, position.z * 0.5 + 0.5, 1.0); |
| 320 | +``` |
| 321 | + |
| 322 | +```{lit} rust, Set vertex out position (hidden, replace, also for tangle root "Vanilla") |
| 323 | +let angle = uMyUniforms.time; // you can multiply it go rotate faster |
| 324 | +let alpha = cos(angle); |
| 325 | +let beta = sin(angle); |
| 326 | +var position = vec3f( |
| 327 | + in.position.x, |
| 328 | + alpha * in.position.y + beta * in.position.z, |
| 329 | + alpha * in.position.z - beta * in.position.y, |
| 330 | +); |
| 331 | +out.position = vec4f(position.x, position.y * ratio, position.z * 0.5 + 0.5, 1.0); |
| 332 | +// This changes: ^^^^^^^^^^^^^^^^^^^^^^ |
| 333 | +``` |
| 334 | + |
| 335 | +Conclusion |
| 336 | +---------- |
| 337 | + |
| 338 | +We now **fixed the depth issue**! And with this depth attachment we have set up an important part of the 3D rendering pipeline that we won't have to edit so much. We are now ready for **more 3d transforms**. |
| 339 | + |
| 340 | +```{figure} /images/pyramid-zissue-fixed.png |
| 341 | +:align: center |
| 342 | +:class: with-shadow |
| 343 | +The depth ordering issue is gone! |
| 344 | +``` |
| 345 | + |
| 346 | +````{tab} With webgpu.hpp |
| 347 | +*Resulting code:* [`step052-next`](https://github.com/eliemichel/LearnWebGPU-Code/tree/step052-next) |
| 348 | +```` |
| 349 | + |
| 350 | +````{tab} Vanilla webgpu.h |
| 351 | +*Resulting code:* [`step052-vanilla-next`](https://github.com/eliemichel/LearnWebGPU-Code/tree/step052-vanilla-next) |
| 352 | +```` |
0 commit comments