Files
wsg/docs/user/shadows.md
T
Jérôme Bousquié 5ae978da23 doc
2026-09-21 10:07:32 +02:00

3.4 KiB
Raw Blame History

Shadows (shadow mapping)

Shadows are off by default and are enabled by designating a single casting light:

app.scene.set_shadow_caster(Some(index));  // packed index — see the pitfall below
app.scene.set_shadow_caster(None);         // shadows off (default)

Only a directional or spot light can cast shadows. A point light index disables the shadow pass (cubemap shadows are out of scope).

⚠️ The packed-index pitfall

set_shadow_caster takes the light's index in the packed array (directionals first, then point, then spot — recalled in Lights).

Index 0 is the default +Z directional pre-loaded by Lights::new(), not necessarily your light. Symptom of a wrong index: the shadow camera looks in an unexpected direction and misaligned objects occlude each other (blackened objects, ghost shadows).

Two ways to avoid it:

  1. Clear the list before adding yours — your light becomes index 0:

    app.scene.clear_lights();                       // removes the default +Z
    app.scene.add_directional_light(dir, [1.0, 0.98, 0.92], 1.6).unwrap();
    app.scene.set_shadow_caster(Some(0));           // now it really is YOUR light
    

    This is the technique in shadow_test.rs.

  2. Count the indices — if you keep the default light and add yours, it lands at index 1:

    app.scene.add_directional_light(toward_light, [1.0, 0.98, 0.92], 1.5).unwrap(); // → index 1
    app.scene.set_shadow_caster(Some(1)); // this is the demo's warm light that casts
    

    This is the technique in demo.rs.

How it works (to understand the limits)

Each frame, if a caster is active, the engine runs two passes (technical details in FRAME_LOOP):

  1. Shadow pass: the scene is rendered as seen from the light (depth-only shadow_shader.wgsl shader) into a 1024² Depth32Float shadow map (size configurable via SHADOW_MAP_SIZE), with a depth bias (slope-scaled + constant) to avoid shadow acne.
  2. Color pass: the standard fragment shader re-projects each fragment into light space and compares its depth against the map via a 3×3 PCF (softened shadow edges).

Things to know:

  • Directional light: the shadow frustum is orthographic, centered on the scene center (SHADOW_SCENE_CENTER, radius SHADOW_SCENE_RADIUS = 5.0 by default). Objects far from the origin may fall outside the frustum and stop casting.
  • Spot light: the light's cone naturally bounds the shadow.
  • Only one light casts at a time (no multi-light shadows).
  • Shadows only affect meshes rendered by standard in lit mode — a renderer in unlit mode (see Materials & textures) receives none.

Tuning shadow rendering

The constants SHADOW_MAP_SIZE, SHADOW_DEPTH_BIAS, SHADOW_SCENE_RADIUS, SHADOW_SCENE_CENTER are exposed in wsg_lib::utils (defaults: 1024, 0.006, 5.0, origin).

Tuning tips:

  • Speckled shadow edges (acne): raise the bias.
  • Peter-panning (shadow detached from the object): lower the bias.
  • Shadow clipped at the scene edge: raise the frustum radius (directional).
  • Shadows too blurry, want them crisper: raise the map size.