Graphics Programming

WebGPU Shadow Bugs Solved: The #1 Debugging Guide 2025

Struggling with WebGPU shadow bugs like Peter Panning or shadow acne? Our 2025 guide breaks down how to debug and fix them with practical code and workflows.

A

Alexei Volkov

Senior graphics engineer specializing in real-time rendering and WebGPU performance optimization.

7 min read13 views

Let's be honest: shadows are where 3D graphics go from looking like a '90s tech demo to something truly immersive. But they're also a notorious source of headaches. You've spent hours setting up your WebGPU render pipelines, your matrices are perfect, and then... you're greeted by flickering artifacts, detached shadows, or just a black void where your beautiful shadows should be. Sound familiar? You're not alone.

WebGPU, for all its power and performance, introduces its own unique set of challenges for shadow mapping. But fear not. This is the guide I wish I had when I was tearing my hair out. We're going to systematically break down, identify, and squash the most common WebGPU shadow bugs. By the end of this, you'll be debugging shadows like a pro.

Why Are Shadows So Hard in WebGPU?

The most popular technique, shadow mapping, sounds simple on paper. You render your scene from the light's point of view, storing only the depth information in a texture (the shadow map). Then, when you render your main scene, you check if a given pixel is further from the light than the depth stored in the shadow map. If it is, it's in shadow. Simple, right?

The complexity comes from the implementation details, especially in a modern, explicit API like WebGPU:

  • State Management: You need at least two distinct render pipelines: one for the shadow pass (writing to the depth texture) and one for the main pass (reading from it). A misconfigured pipeline is a common point of failure.
  • Resource Handling: Creating the depth texture with the right format (e.g., 'depth24plus'), view, and sampler (specifically a 'comparison' sampler) is critical and easy to get wrong.
  • Precision Issues: The shadow map has finite resolution and precision. This discrepancy between the depth value stored in the map and the calculated depth of a pixel is the root cause of the most infamous bugs: shadow acne and Peter Panning.

The Debugging Mindset: Isolate and Conquer

Before diving into specific bugs, adopt this one crucial practice: visualize your shadow map. Don't guess what the light sees; look at it directly. The single most effective debugging tool is to temporarily replace your main render with a simple shader that draws your shadow map onto a full-screen quad.

If you see a solid white or black texture, you know the problem is in your shadow pass. If you see a reasonable-looking depth image, the problem is likely in your main pass (sampling, comparison, or matrix math).

Common Shadow Bugs and Their Fixes

Alright, let's get to the rogues' gallery. Here are the most common culprits and how to fix them.

Shadow Acne (Self-Shadowing Artifacts)

What it looks like: Incorrect, noisy patterns of dark pixels on surfaces that should be fully lit. It looks like the surface is shadowing itself.

Advertisement

The Cause: This is a classic precision problem. A fragment on a surface is being tested against the *very same* surface's depth value in the shadow map. Due to tiny floating-point inaccuracies and the quantization of the depth map, a fragment might incorrectly decide it's *just behind* the value in the shadow map, and thus, in shadow.

The Fix: Depth Bias. The solution is to slightly "cheat" during the shadow pass. We tell the GPU to add a small, constant bias to the depth value of every fragment being written to the shadow map. This effectively pushes the shadow a tiny bit further away from the light, giving our surfaces some breathing room.

In your GPURenderPipelineDescriptor for the shadow pass, configure the depth-stencil state:


const shadowPipeline = device.createRenderPipeline({
  // ... other properties
  depthStencil: {
    format: 'depth24plus',
    depthWriteEnabled: true,
    depthCompare: 'less',
    // --- THE FIX --- 
    depthBias: 2, // An integer value, start small and increase if needed.
    depthBiasSlopeScale: 2.0, // A float, helps with surfaces at sharp angles to the light.
  },
});

You'll need to tune depthBias and depthBiasSlopeScale. Start small and increase them just enough to eliminate the acne. Too much bias will cause the next problem...

Peter Panning (Shadow Detachment)

What it looks like: The shadow appears disconnected from the base of the object casting it, making the object look like it's floating. The name comes from Peter Pan, who famously lost his shadow.

The Cause: This is the opposite of shadow acne. You've applied too much depth bias. You've pushed the shadow so far back that it no longer touches the object that's supposed to be casting it.

The Fix: Reduce Depth Bias. If you're seeing Peter Panning, your depthBias and/or depthBiasSlopeScale values are too high. Dial them back until the shadow reconnects with the object, but not so much that shadow acne reappears. It's a balancing act.

Comparison: Shadow Acne vs. Peter Panning

These two bugs are two sides of the same coin. Here’s a quick reference:

Bug Visual Symptom Primary Cause Primary Fix
Shadow Acne Noisy, incorrect self-shadowing on lit surfaces. Surface fragments failing depth test against themselves due to precision errors. Add or slightly increase depthBias and depthBiasSlopeScale.
Peter Panning Shadows appear detached from their casting objects. Excessive depthBias, pushing the shadow too far away. Reduce depthBias and depthBiasSlopeScale.

No Shadows at All (The Black Void)

What it looks like: Your scene renders perfectly, but it's fully lit. No shadows anywhere.

The Cause: This is usually a setup or configuration error. It's time to go through a checklist.

The Fix: The Debugging Checklist.

  1. Is the shadow map valid? Use your debug viewer. If it's all white or black, your shadow pass isn't rendering depth correctly. Check the shadow pass pipeline, its vertex shader, and ensure you're actually dispatching the render pass.
  2. Is the texture format correct? Your shadow map texture must be created with a depth format, like 'depth24plus' or 'depth32float'.
  3. Are you using a Comparison Sampler? This is a massive "gotcha" in WebGPU. For the hardware to perform the depth comparison for you (for techniques like Percentage-Closer Filtering), you must create a GPUSampler with compare: 'less' (or 'greater', etc.).
  4. 
    const shadowSampler = device.createSampler({
      compare: 'less', // This is the magic property!
    });
      
  5. Is your main shader correct? In your main fragment shader, you need to use the right texture and sampler types. The shadow map should be a texture_depth_2d and the sampler a sampler_comparison. The built-in textureSampleCompare function then performs the lookup and comparison in one go.

// WGSL Fragment Shader Snippet
@group(0) @binding(1) var shadowMap: texture_depth_2d;
@group(0) @binding(2) var shadowSampler: sampler_comparison;

// ... inside main function ...
let shadow_val = textureSampleCompare(
  shadowMap, 
  shadowSampler, 
  shadow_pos.xy, // The fragment's position in light space
  shadow_pos.z   // The fragment's depth from the light
);
// shadow_val will be 1.0 if not in shadow, 0.0 if in shadow

Flickering or "Swimming" Shadows

What it looks like: As the camera moves, the edges of the shadows seem to crawl, shimmer, or flicker unstably.

The Cause: The light's projection matrix is being recalculated every frame based on the camera's view frustum, causing the pixels of the shadow map to align differently with the world from one frame to the next. This temporal aliasing is jarring.

The Fix: Stabilize the Light's Projection Matrix. This is a more advanced fix. The key is to make the light's orthographic projection matrix less dependent on the exact camera position. You can do this by snapping the light's position and the projection's extents (left, right, top, bottom) to discrete increments that are a function of the shadow map's texel size. This ensures that as the camera moves slightly, the light's projection remains locked until a larger movement threshold is crossed, eliminating the shimmer.

Your WebGPU Shadow Debugging Toolkit

You don't have to debug in the dark. Use these tools:

  • Browser DevTools: Chrome (in Canary/Dev) and Firefox (in Nightly) have nascent WebGPU debugging tools. You can inspect GPUTexture objects to see if they're being created correctly, but you can't always visualize them easily.
  • Spector.js: This browser extension is a fantastic tool for capturing and dissecting a single frame of your WebGPU application. You can see every draw call, inspect pipeline state, and view textures at every stage, including your shadow map.
  • Your Own Debug Viewer: As mentioned before, this is your #1 tool. A simple pipeline that draws a texture to the screen. You can wire it up to a keypress to toggle between your main render and visualizing the shadow map, normal buffers, or any other intermediate texture.

A Practical Debugging Workflow (Step-by-Step)

Feeling overwhelmed? Just follow these steps.

  1. Identify the Symptom: Is it acne, panning, flickering, or nothing at all?
  2. Isolate the Scene: If possible, reduce your scene to one shadow-casting object, one shadow-receiving plane, and one light. Complexity is the enemy of debugging.
  3. Visualize the Shadow Map: This is your most important step. Render the depth texture to the screen. Does it look like a depth image from the light's perspective? Is it all white? All black? This tells you whether to focus on the shadow pass or the main pass.
  4. Check the Shadow Pass: If the shadow map is wrong, check this pass first. Is the pipeline using the correct `depthBias` settings? Is it using the right vertex shader? Are you clearing the depth texture correctly before the pass?
  5. Check the Main Pass: If the shadow map looks correct but the final render is wrong, inspect your main pass. Is the sampler a `comparison` sampler? Is the bind group layout correct? Is your WGSL using `textureSampleCompare` with the right coordinates? Are your light-view-projection matrices correct in the shader?
  6. Tweak and Iterate: Make one change at a time. Tweak a bias value. Check a matrix. Re-verify your bind group layout. Small, incremental changes will lead you to the solution.

Key Takeaways for Flawless Shadows

If you remember nothing else, remember these four things:

  • Always visualize your shadow map first. It's the most powerful debugging step you can take.
  • Master depth bias. Balancing depthBias and depthBiasSlopeScale is the key to defeating shadow acne and Peter Panning.
  • Use a comparison sampler. This is a non-negotiable requirement in WebGPU for efficient shadow comparisons.
  • Be systematic. When shadows disappear, don't panic. Go through the checklist: texture format, sampler type, pipeline state, and shader code. The bug is hiding in one of those places.

Shadows are a deep topic, but they aren't magic. With a solid understanding of the underlying mechanics and a systematic approach to debugging, you can conquer these common bugs and bring your WebGPU scenes to life. Happy coding!

Tags

You May Also Like