80 lines
3.4 KiB
Markdown
80 lines
3.4 KiB
Markdown
# 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/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/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](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)
|