# Building a Reusable WebGPU & Rust Engine: Part 4 — Implementing Depth Buffering and a Post-Processing Pass

**Upgrading to True 3D**\
With basic rendering working, the next milestone is rendering a proper 3D cube without visual artifacts like self-occlusion or inverted faces. To achieve this, our engine requires a critical subsystems:

- Depth Buffering (Z-Buffer): Ensures closer surfaces correctly obscure farther ones.

On top of depth testing, I am also adding an extra architectural feature:

- Post-Processing Pass: Feeds rendered scenes into off-screen textures for screen-space effects and presentation.

---

Let’s start by breaking down how wgpu manages depth. Depth testing requires two GPU resources and two pipeline stages working in unison.

**1.. The Core GPU Resources**

`depth_texture`: The raw block of VRAM allocated on the GPU via device.create_texture(). Unlike a standard color texture storing RGBA values, each texel stores a normalized distance metric between 0.0 (near plane) and 1.0 (far plane).

`depth_view`: The access handle created via `depth_texture.create_view()`. The GPU uses this view as an active attachment to read and write depth values during rasterization.

Lifecycle Note: These resources are typically allocated once at startup, and re-allocated only when the window resizes to match the new swapchain dimensions.

**2.. The Two Configuration Stages**

(1). RenderPipeline (The Rules)\
Tells the GPU how to test and write depth values. Configured once during initialization.

- `format`: Matches your depth texture format (e.g., wgpu::TextureFormat::Depth32Float).
- `depth_write_enabled: true`: Allows closer pixels to overwrite old depth values in VRAM. -depth_compare: wgpu::CompareFunction::Less: Keeps pixels that are closer to the camera and discards anything behind them.

(2). RenderPassDescriptor (The Execution)\
Tells the GPU where and when to write depth data. Configured every frame.

- `depth_stencil_attachment:` Binds your depth_view as the active target for the pass.
- `load: wgpu::LoadOp::Clear(1.0)`: Clears the buffer to 1.0 (furthest possible distance) at the start of each frame so new geometry passes the test.
- `store: wgpu::StoreOp::Store:` Saves the written depth values during the pass so subsequent draw calls can test against them.

---

**Implementing the Post-Processing Pass**

Post-processing requires setting up an off-screen render target and the pipeline resources to sample it.

**1\. The Core Setup** The post-process pass needs a bind group layout (the interface) and a bind group (the resources). In State, `post_bind_group_layout` declares binding 0 as a filterable texture_2d and binding 1 as a filtering sampler, both visible to FRAGMENT — matching `post_process.wgsl` exactly. `post_bind_group` binds the off-screen `scene_view` and `scene_sampler` to that contract.

`post_pipeline` is built by the same `create_render_pipeline` helper as the main pass, passing depth_format: None (no depth testing in pass 2) and use_vertex_buffer: false, because the vertex shader generates a full-screen triangle from `@builtin(vertex_index)`. `post_pipeline_layout` wraps the bind group layout above.

**2\. The Hot-Reload Trade-Off & Safety Boundaries** Storing pipeline handles on State allows shader code to recompile dynamically at runtime:

- Fallback & Standalone Safety (`include_str!`): The initial pipelines are compiled using `include_str!`, embedding the default WGSL sources directly into the final binary. The engine can always run standalone without external file dependencies.
- Disk-Based Watcher: Runtime reloads use `ShaderWatcher` (powered by the notify crate) to detect changes and re-read files from disk relative to the current working directory.
- Non-Crashing Error Handling: When a file changes, `reload_shaders` builds candidate pipelines inside a `wgpu::ErrorFilter::Validation` scope via `push_error_scope`. If compilation fails due to a typo or syntax error, the engine logs the compiler output and retains the current active pipelines instead of crashing.
- Interface Immutability: Hot-reloading only swaps out the shader modules and internal pipeline logic—it does not rebuild bind group layouts. As long as `@group and @binding` slots match the original contract, you can iterate on math, colors, and post-processing algorithms completely live.

![Proper 3D Depth Testing with a Vignette Post-Processing Pass](https://cdn.hashnode.com/uploads/covers/6ab53a7058d1a9925b496a70/682bef8b-a38f-4cdd-9c9c-51a2d8d8c96b.png align="center")

Proper 3D Depth Testing with a Vignette Post-Processing Pass

---

Thanks for reading! Check out my industrial Rust projects available on the Microsoft Store: [Download on the Microsoft Store](https://apps.microsoft.com/detail/9N71XHP397ZL?hl=en-us&gl=US&ocid=pdpshare)
