This commit is contained in:
Jérôme Bousquié
2026-09-25 20:06:10 +02:00
parent 24fbafc810
commit d4c2d93fc5
31 changed files with 988 additions and 881 deletions
+79
View File
@@ -0,0 +1,79 @@
# Shadows (shadow mapping)
Shadows are **off by default** and are enabled by designating **a single** casting light:
```rust
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](lights.md)).
**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:
```rust
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`](../../../lib/examples/lights/shadow_test.rs).
2. **Count the indices** — if you keep the default light and add yours, it lands at index 1:
```rust
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`](../../../lib/examples/effects/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](../../tech/FRAME_LOOP.md)):
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](../meshes/materials.md)) 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.
## Links
- [User README](../README.md) · [Lights](lights.md) · [Examples](../examples.md)
- [Root README](../../../README.md) · [FRAME_LOOP](../../tech/FRAME_LOOP.md)