DEV Community

J
J

Posted on

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

part1 link : Windowing & Pipeline Architecture
part2 link : Loading Custom Geometry & WGSL Shaders
part3 link : 3D Camera System & Hot-Reloading Shaders


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_layoutwraps 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


Thanks for reading! Check out my industrial Rust projects available on the Microsoft Store:
Download on the Microsoft Store

Top comments (0)