docs: Étape 14 (shadows) finale — bilan DRAFT, ROADMAP §4.2, README roadmap
- docs/DRAFT.md: document vidé (bilan Étape 14 archivé dans l'historique git) - docs/ROADMAP.md §4.2: Shadows marqué [x] (Étape 14, mono-lumière PCF) - README.md: item 12 de la roadmap (shadow mapping) - cargo fmt --all sur les sources Étape 14
This commit is contained in:
@@ -188,3 +188,4 @@ The architecture docs live in `docs/tech/` and are written in **French**. Each d
|
|||||||
9. ✅ **Window resize (Étape 11, Phase 4.4)** — `App::resize` reconfigures the surface (`Context::configure`) and recreates the depth texture (`Renderer::resize_depth`) together on each `WindowEvent::Resized`, so color and depth attachments always match. Guards against 0×0 (minimize). The surface format is re-synced to the Renderer and Scene if it ever changes. (Done 2026-09-18; verified at runtime on the `cube` example.)
|
9. ✅ **Window resize (Étape 11, Phase 4.4)** — `App::resize` reconfigures the surface (`Context::configure`) and recreates the depth texture (`Renderer::resize_depth`) together on each `WindowEvent::Resized`, so color and depth attachments always match. Guards against 0×0 (minimize). The surface format is re-synced to the Renderer and Scene if it ever changes. (Done 2026-09-18; verified at runtime on the `cube` example.)
|
||||||
10. ✅ **Multi-lighting (Étape 12, Phase 4.2)** — the scene now carries a global light list (directional + point) with a white ambient, uploaded into the per-frame `FrameUniforms` array each frame. `Scene::add_directional_light` / `add_point_light` / `set_ambient` / `clear_lights` configure it; `FrameUniforms::default()` (one white directional along +Z + white ambient) reproduces the pre-multi-light look exactly. The `standard` fragment accumulates ambient + all lights; the `cube` example adds a warm point light on top of the default directional. (Done 2026-09-18.)
|
10. ✅ **Multi-lighting (Étape 12, Phase 4.2)** — the scene now carries a global light list (directional + point) with a white ambient, uploaded into the per-frame `FrameUniforms` array each frame. `Scene::add_directional_light` / `add_point_light` / `set_ambient` / `clear_lights` configure it; `FrameUniforms::default()` (one white directional along +Z + white ambient) reproduces the pre-multi-light look exactly. The `standard` fragment accumulates ambient + all lights; the `cube` example adds a warm point light on top of the default directional. (Done 2026-09-18.)
|
||||||
11. ✅ **Spot lights (Étape 13, Phase 4.2)** — spot lights (oriented cone + half-angle) added on top of the multi-lighting system. `Scene::add_spot_light(pos, dir, color, intensity, radius, half_angle)` registers a spot light; the `standard` fragment accumulates a spot term with a smoothed penumbra (half-angle ± 0.1 rad) and linear attenuation. `Light` grew from 48 to 64 bytes (added `dir_angle`); `FrameUniforms` from 576 to 704 bytes (added `num_spot`). Non-regression: default scene unchanged. The `cube` example adds a green spot light aimed at the cube. (Done 2026-09-18.)
|
11. ✅ **Spot lights (Étape 13, Phase 4.2)** — spot lights (oriented cone + half-angle) added on top of the multi-lighting system. `Scene::add_spot_light(pos, dir, color, intensity, radius, half_angle)` registers a spot light; the `standard` fragment accumulates a spot term with a smoothed penumbra (half-angle ± 0.1 rad) and linear attenuation. `Light` grew from 48 to 64 bytes (added `dir_angle`); `FrameUniforms` from 576 to 704 bytes (added `num_spot`). Non-regression: default scene unchanged. The `cube` example adds a green spot light aimed at the cube. (Done 2026-09-18.)
|
||||||
|
12. ✅ **Shadows — shadow mapping (Étape 14, Phase 4.2, optionnel)** — classic two-pass shadow mapping on a **single** light (directional or spot), selected by `Scene::set_shadow_caster(index)`. A depth-only pass (`shadow_shader.wgsl` + dedicated `shadow_pipeline`) renders the scene into a 1024² `Depth32Float` shadow map (`Renderer`-owned, slope-scaled depth bias); the `standard` fragment re-projects each fragment into light space and applies a **PCF 3×3** comparison-sampler test (bind group **@3**, shared). `FrameUniforms` grew from 704 to 784 bytes (`shadow_light_index`, `light_view_proj`, `shadow_params`). Shadows are **off by default** (`shadow_caster = None`) so `simple`/`cube`/`manual`/`spot_test` are unchanged. The `shadow_test` example casts a soft shadow from a rotating cube onto a ground slab. (Done 2026-09-19.)
|
||||||
|
|||||||
+18
-216
@@ -1,219 +1,21 @@
|
|||||||
# DRAFT — Étape 14 : Shadows (Phase 4.2, optionnel)
|
# DRAFT — Étape suivante
|
||||||
|
|
||||||
> **Objectif** : ajouter les **ombres portées** (shadow mapping) à `wsg-lib`, sur une **lumière
|
> 📅 **Document vidé le 2026-09-19** (fin de l'Étape 14, Shadows — Phase 4.2, bilan archivé
|
||||||
> unique** (directionnelle ou spot) choisie par la scène. C'est le dernier item optionnel restant
|
> dans l'historique git). Ce fichier accueillera le plan de l'étape suivante.
|
||||||
> de la Phase 4.2 (hémisphérique ✅, multi-lumières ✅ Étape 12, spot ✅ Étape 13, **shadows →
|
|
||||||
> ici**).
|
|
||||||
>
|
>
|
||||||
> **État de départ** : l'éclairage est entièrement CPU-packé dans `FrameUniforms` (liste
|
> **Étape 14 (2026-09-19) : Shadow mapping mono-lumière — FAIT & vérifié (build + tests).**
|
||||||
> `lights[8]`, caméra `view/proj`, `options[0]` = unlit). Le `Renderer` possède déjà une depth
|
> Ombres portées classiques en **deux passes** sur une lumière unique (directionnelle **ou** spot),
|
||||||
> texture de scène (`_depth_texture`/`depth_view`) et un cache d'object bind groups
|
> choisie par `Scene::set_shadow_caster(index)` (D1). Le `Renderer` possède désormais la shadow map
|
||||||
> (`object_cache`, par entité, modèle réécrit chaque frame). Le shader `standard_shader.wgsl` a
|
> (1024², `Depth32Float`, `RENDER_ATTACHMENT | TEXTURE_BINDING`), le sampler **comparateur**
|
||||||
> un layout unique de 3 bind groups partagé par **toutes** les pipelines (Étapes 3 + 10) : frame
|
> (`GreaterEqual`, WebGPU NDC [0,1]), le bind group **@3** (sampler + texture), un buffer uniform
|
||||||
> @0, object @1, texture diffuse @2.
|
> `light_view_proj` + sa bind group, et une **pipeline depth-only** (`shadow_shader.wgsl` + bias
|
||||||
|
> slope-scaled) — tout alloué dans `Renderer::new`, comme la depth texture de scène (D2/D4).
|
||||||
|
> `render_scene` exécute d'abord `render_shadow_map` (même `CommandEncoder`), puis la passe principale
|
||||||
|
> échantillonne la map via **PCF 3×3** dans `standard_shader.wgsl` (l'ombre ne touche pas l'ambiant).
|
||||||
|
> `FrameUniforms` est passé de 704 à 784 octets (`shadow_light_index`, `light_view_proj`, `shadow_params`).
|
||||||
|
> Ombres **éteintes par défaut** (indice sentinelle `MAX_LIGHTS`) → non-régression des exemples
|
||||||
|
> `simple`/`cube`/`manual`/`spot_test`. Exemple `shadow_test` : cube tournant projetant une ombre sur
|
||||||
|
> un sol. Commit-log : `c2cbd7f` (impl), `39167ee` (comparateur), `67bd7af` (direction directionnelle),
|
||||||
|
> `9a51ff7` (test : clip depth WebGPU + NdotL).
|
||||||
>
|
>
|
||||||
> **Définition de done** : un objet éclairé par la lumière ombreuse projette une ombre visible
|
> Source de vérité = code + README.md. Ce document est vidé à la complétion de chaque étape.
|
||||||
> sur une surface adjacente (un sol), et l'ombre bouge quand le cube tourne. Vérifié au runtime
|
|
||||||
> via un exemple dédié (`shadow_test`). Aucune régression sur les exemples existants (`simple`,
|
|
||||||
> `cube`, `manual`, `spot_test`) — les ombres sont **éteintes par défaut**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Modèle retenu : shadow mapping mono-lumière
|
|
||||||
|
|
||||||
Le shadow mapping classique se fait en **deux passes** :
|
|
||||||
|
|
||||||
1. **Pass ombre (depth-only)** : on rend la scène depuis le point de vue de la lumière dans une
|
|
||||||
texture de profondeur dédiée (la **shadow map**). Seule la profondeur compte — pas de couleur,
|
|
||||||
pas d'éclairage.
|
|
||||||
2. **Pass principale** : dans le fragment shader, on reprojette chaque fragment dans l'espace
|
|
||||||
lumière (`light_view_proj`), on compare sa profondeur à la shadow map. Si le fragment est plus
|
|
||||||
loin que l'occulteur enregistré, il est **à l'ombre** ; sinon il multiplie l'éclairage par 1.
|
|
||||||
|
|
||||||
### Portée (D1)
|
|
||||||
Une **seule** lumière ombreuse par frame, désignée par **son index dans le tableau packé**
|
|
||||||
(directionnelles d'abord, puis points, puis spots — même convention que `Lights::into_frame_array`).
|
|
||||||
Un index sentinelle `MAX_LIGHTS` (= 8) signifie « aucune ombre » → **éteint par défaut**
|
|
||||||
(non-régression). Les ombres **point** (cube map, 6 faces) sont **hors périmètre** (D6) — on couvre
|
|
||||||
les deux projections utiles :
|
|
||||||
- **directionnelle** → projection **orthographique** (soleil lointain) ;
|
|
||||||
- **spot** → projection **perspective** depuis le sommet du cône.
|
|
||||||
|
|
||||||
### Pourquoi ce choix ?
|
|
||||||
- Coût, lisibilité et validation simples : une texture, une passe, un VP lumière, un échantillon PCF.
|
|
||||||
- S'aligne sur l'architecture existante (matrices par frame, object bind group réutilisé).
|
|
||||||
- Le multi-shadow et les ombres point seront des extensions incrémentales (voir §6 « Évolutions »),
|
|
||||||
pas une réécriture.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Décisions (D1..D8)
|
|
||||||
|
|
||||||
| # | Décision |
|
|
||||||
|---|----------|
|
|
||||||
| **D1** | **Une seule lumière ombreuse** par frame, sélectionnée par index (tuple `(enable, index)`). Desactivée par défaut (index = `MAX_LIGHTS`). API : `Scene::set_shadow_caster(index)`. |
|
|
||||||
| **D2** | **Shadow map** : `DEPTH_FORMAT` (Depth32Float), résolution **1024×1024** par défaut, `usage = RENDER_ATTACHMENT | TEXTURE_BINDING`, 1 mip. Propriété du `Renderer` (comme la depth texture de scène). |
|
|
||||||
| **D3** | **Matrice `light_view_proj` calculée sur CPU** chaque frame par le `Renderer` (depuis `scene.shadow_caster()` + la lumière + un centre/rayon de scène). Ortho pour directionnelle, perspective pour spot. Uploadée dans `FrameUniforms` (tampon frame existant, `min_binding_size: None` → extension transparente). |
|
|
||||||
| **D4** | **Pipeline ombre dédiée** (vertex-only, `fragment: None`) : layout = [group 0 : `light_view_proj`, group 1 : `object_layout` **réutilisé**]. Elle réutilise donc directement `object_cache` (bind groups par entité déjà réécrits chaque frame). |
|
|
||||||
| **D5** | **PCF (Percentage Closer Filtering)** : échantillon comparateur (`sampler_comparison` + `texture_depth_2d`) + petit kernel 3×3 dans le shader, **slope-scaled depth bias** sur la pipeline ombre (`DepthBiasState { constant, slope_scale, clamp }`) + un petit bias constant dans la comparaison pour supprimer l'acné. |
|
|
||||||
| **D6** | **Ombre ponctuelle (cube map) hors périmètre** : documentée en §6, PAS implémentée à l'Étape 14. |
|
|
||||||
| **D7** | **Non-régression** : ombres éteintes par défaut → exemples `simple`/`cube`/`manual`/`spot_test` inchangés (pas de changement de leur rendu). Le groupe 3 (shadow) reste lié sur toutes les pipelines mais l'échantillonnage est porté par `options[1]`/index. |
|
|
||||||
| **D8** | **Résolution shadow** configurable plus tard ; constante `SHADOW_MAP_SIZE = 1024` maintenant (setter + recréation de texture reportés — voir §6). |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Plan d'implémentation
|
|
||||||
|
|
||||||
### 3.1 Étendre `FrameUniforms` (+ contrat WGSL) — uniform.rs / standard_shader.wgsl
|
|
||||||
Ajouter, après les compteurs, **avant** `options` (alignement 16 respecté par `repr(C)` comme par
|
|
||||||
WGSL) :
|
|
||||||
|
|
||||||
| Offset | Champ | Type | Sens |
|
|
||||||
|--------|-------|------|------|
|
|
||||||
| 672 | num_directional | u32 | (existant) |
|
|
||||||
| 676 | num_point | u32 | (existant) |
|
|
||||||
| 680 | num_spot | u32 | (existant) |
|
|
||||||
| 684 | **shadow_light_index** | u32 | index de la lumière ombreuse, `MAX_LIGHTS` = aucune |
|
|
||||||
| 688 | **light_view_proj** | mat4x4<f32> | VP de la lumière ombreuse |
|
|
||||||
| 752 | **shadow_params** | vec4<f32> | x = résolution (taille shadow map en pixels), y = bias, z/w = libres |
|
|
||||||
| 768 | options | vec4<u32> | (existant, `options[1]` = « ombres actives » 0/1) |
|
|
||||||
|
|
||||||
- Total : **784 octets** (au lieu de 704). `FRAME_UNIFORMS_SIZE = size_of` suit automatiquement ;
|
|
||||||
le `min_binding_size: None` du layout frame rend l'agrandissement transparent.
|
|
||||||
- **WGSL** `struct FrameUniforms` mis à jour à l'identique (même ordre, mêmes types) ; la table
|
|
||||||
d'offset du header du fichier est réindexée.
|
|
||||||
- **Tests** `frame_uniforms_layout_matches_wgsl` (uniform.rs) mis à jour : nouvel `offset_of`
|
|
||||||
pour `shadow_light_index` (684), `light_view_proj` (688), `shadow_params` (752), `options`
|
|
||||||
(768) et nouvelle taille 784.
|
|
||||||
|
|
||||||
### 3.2 Ressources shadow dans le `Renderer` — core/renderer.rs
|
|
||||||
Dans `Renderer::new`, allouer :
|
|
||||||
- `_shadow_texture: wgpu::Texture` + `shadow_view: wgpu::TextureView` (1024², Depth32Float,
|
|
||||||
`RENDER_ATTACHMENT | TEXTURE_BINDING`) — même pattern que `create_depth_texture`, helper isolé
|
|
||||||
`create_shadow_map(device, size)` ;
|
|
||||||
- `shadow_sampler: wgpu::Sampler` : `compare: Some(CompareFunction::GreaterEqual)` (voir D5) ,
|
|
||||||
`mag/min_filter: Linear`, `address_mode: ClampToEdge` — comparateur requis pour
|
|
||||||
`textureSampleCompare` ;
|
|
||||||
- `shadow_bind_group` : group 3 = [`shadow_sampler` (0), `shadow_view` (1)] ;
|
|
||||||
- `shadow_uniform_buffer` : 64 o (une `Mat4`), + `shadow_uniform_bind_group` (group 0 de la
|
|
||||||
pipeline ombre) ;
|
|
||||||
- `shadow_pipeline` : vertex-only (voir 3.3).
|
|
||||||
|
|
||||||
Nouveaux champs privés + les exposer par getters (au minimum `shadow_texture`,
|
|
||||||
`shadow_bind_group`, `shadow_pipeline`, `shadow_uniform_bind_group`) pour `render_scene`.
|
|
||||||
|
|
||||||
### 3.3 Pipeline ombre dédiée — pipeline/pipeline_cache.rs (helper) + renderer.rs
|
|
||||||
Nouvelle **`create_shadow_pipeline_layout(device, object_layout)`** et helper `build_shadow_pipeline` :
|
|
||||||
- Layout : [`shadow_uniform_layout` (group 0, buffer uniform VERTEX), `object_layout` (group 1)].
|
|
||||||
`object_layout` = exactement celui existant (même bind group par entité réutilisé). ✔
|
|
||||||
- `vertex` : `vs_main` d'un **nouveau shader minimal `shadow_shader.wgsl`** qui ne fait que :
|
|
||||||
`clip = light_view_proj * object.model * vec4(position,1)`. Entrées : uniquement `position`
|
|
||||||
(location 0). Pas de fragment (`fragment: None`).
|
|
||||||
- `depth_stencil` : `DEPTH_FORMAT`, `depth_write = true`, `compare = Less`, **`bias` slope-scaled**
|
|
||||||
(`constant = 2` , `slope_scale = 2.0`, clamp ~0) — anti-acné (D5).
|
|
||||||
|
|
||||||
Le shader ombre est ajouté en constante embarquée (`shadow_shader.wgsl` sous
|
|
||||||
`lib/src/shaders/`) et enregistrable via `PipelineCache::register_shader` comme les autres.
|
|
||||||
|
|
||||||
### 3.4 Pass ombre : `render_shadow_map` — core/renderer.rs
|
|
||||||
Nouvelle méthode, appelée **en tête de `render_scene`** (même `CommandEncoder`) :
|
|
||||||
1. Si `scene.shadow_caster()` est `None` → ne rien faire (ombre éteinte, `options[1] = 0`).
|
|
||||||
2. Calculer `light_view_proj` (D3, helper `light_view_proj(camera_setup, scene)` — voir 3.6),
|
|
||||||
l'écrire dans `shadow_uniform_buffer`.
|
|
||||||
3. `encoder.begin_render_pass` sur `shadow_view` (depth clear 1.0, pas de color attachment),
|
|
||||||
`set_pipeline(shadow_pipeline)`, pour chaque entité : `set_bind_group(0, shadow_uniform)` +
|
|
||||||
`set_bind_group(1, object_bind_group_for(label, transform))` + draw (indexed ou non).
|
|
||||||
4. Mettre `options[1] = 1` dans les frame uniforms.
|
|
||||||
|
|
||||||
`render` (bas niveau, sans scène) ne fait **pas** d'ombre — documenté (D7).
|
|
||||||
|
|
||||||
### 3.5 Échantillonnage PCF dans `standard_shader.wgsl`
|
|
||||||
- Déclarer le groupe 3 (ajouté à **toutes** les pipelines via `create_*_bind_group_layouts`) :
|
|
||||||
```
|
|
||||||
@group(3) @binding(0) var shadow_sampler: sampler_comparison;
|
|
||||||
@group(3) @binding(1) var shadow_texture: texture_depth_2d;
|
|
||||||
```
|
|
||||||
- Dans `fs_main`, **après** le calcul de `diffuse` et **une seule fois** (hors boucles lumière) :
|
|
||||||
1. `let in_shadow = 1.0 - sample_shadow(in.world_pos, frame);` avec `sample_shadow()` calculant
|
|
||||||
`light_uv` = NDC de `frame.light_view_proj * world_pos`, mappé en `[0,1]` (xy et z), puis
|
|
||||||
un **PCF 3×3** : moyenne de 9 `textureSampleCompare(shadow_texture, shadow_sampler, uv+off,
|
|
||||||
z-bias)` (offsets en texels / `shadow_params.x`).
|
|
||||||
2. `let lit = base * (ambient + diffuse * shadow_factor);` — l'ombre **ne touche pas l'ambiant**
|
|
||||||
(les ombres dures du soleil conservent une composante ambiante, look réaliste et pas de noir
|
|
||||||
total).
|
|
||||||
- Garde : si `frame.shadow_light_index == MAX_LIGHTS` (ou `options[1] == 0`) → `shadow_factor = 1`
|
|
||||||
(aucune ombre). Comportement identique aux exemples existants (D7).
|
|
||||||
- Le `light_view_proj` est un attribut **global** de frame (une seule lumière ombreuse, d'où la
|
|
||||||
porte _mono-lumière_ D1).
|
|
||||||
|
|
||||||
### 3.6 Helper de calcul `light_view_proj` — core/renderer.rs (ou math/)
|
|
||||||
`fn light_view_proj(light_index, lights, scene_center, area_radius) -> Mat4` :
|
|
||||||
- lire la lumière par `lights.into_frame_array()` → type par **position** (même logique que le
|
|
||||||
shader) ;
|
|
||||||
- **directionnelle** : `dir = normalize(position_dir.xyz)` (déjà « vers la lumière ») ;
|
|
||||||
position légère = `scene_center + dir * D` avec D grand (ex. `area_radius * 2 + 10`) ;
|
|
||||||
view = `look_at(light_pos, scene_center, up)` (up = Y, ou X si parallèle à Y) ;
|
|
||||||
proj = **ortho** `(-ext, ext, -ext, ext, near, far)` centrée sur la scène ;
|
|
||||||
- **spot** : view = `look_at(position_dir.xyz, position_dir.xyz + dir_angle.xyz, up)` ;
|
|
||||||
proj = **perspective** fov = `2 * acos(dir_angle.w)` (demi-angle stocké en cos), near = 0.1,
|
|
||||||
far = radius ;
|
|
||||||
- `scene_center`/`area_radius` : par défaut origine / constante (ex. `SHADOW_AREA_RADIUS = 5.0`),
|
|
||||||
surchargeables via `Scene` (optionnel à l'Étape 14 — constante en dur OK pour l'exemple).
|
|
||||||
|
|
||||||
### 3.7 API `Scene` — scene/scene.rs
|
|
||||||
- Nouveau champ `shadow_caster: Option<usize>` (+ getter `set_shadow_caster(index)`, getter
|
|
||||||
`shadow_caster()`).
|
|
||||||
- `None` par défaut → ombres éteintes (D7).
|
|
||||||
- Optionnel : `set_shadow_area(center, radius)` si on veut un réglage fin (reporté si inutile
|
|
||||||
pour l'exemple).
|
|
||||||
|
|
||||||
### 3.8 Exemple `shadow_test` — lib/examples/shadow_test.rs + README
|
|
||||||
- **Sol** (plan +Z ou XZ, quad gris) + **cube** posé dessus, éclairé par **une directionnelle**
|
|
||||||
qui caste une ombre (`scene.set_shadow_caster(0)` puisque la directionnelle par défaut est
|
|
||||||
l'index 0 après clean/setup).
|
|
||||||
- Cube qui **tourne** (réutiliser `Quat` composé de l'`spot_test`) → l'ombre sur le sol change de
|
|
||||||
forme/position à l'écran : preuve visuelle.
|
|
||||||
- Caméra fixe oblique (ou orbitale simple) pour bien voir l'ombre sur le sol.
|
|
||||||
- Ligne café : `cargo run -p wsg-lib --example shadow_test`.
|
|
||||||
- Documenter dans `lib/examples/README.md` (tableau des exemples, convention anglophone ✔).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Non-régression (obligatoire)
|
|
||||||
- `cargo build --workspace`, `cargo check --workspace` : verts.
|
|
||||||
- `cargo test --workspace` : tests `uniform.rs` (nouveaux offsets) + `lights.rs` inchangés passent.
|
|
||||||
- Exemples existants (`simple`, `cube`, `manual`, `spot_test`) : rendu **identique** (ombres
|
|
||||||
éteintes par défaut, groupe 3 lié mais non échantillonné quand `shadow_light_index == 8`).
|
|
||||||
- `cargo fmt --all` une fois le code formaté.
|
|
||||||
|
|
||||||
## 5. Vérification au runtime
|
|
||||||
1. `cargo run -p wsg-lib --example shadow_test` : un cube éclairé projette une ombre nette sur le
|
|
||||||
sol ; l'ombre **bouge** quand le cube tourne.
|
|
||||||
2. Toggle à la main (commenter `set_shadow_caster`) : ombre disparaît → le flag porte bien
|
|
||||||
l'activation.
|
|
||||||
3. Quelques frames : pas d'artefact type acné (PCF + bias D5). Zoom sur la jonction cube/sol.
|
|
||||||
4. (Optionnel) tenter une **spot** comme castor (`set_shadow_caster(..)` sur une spot) : ombre
|
|
||||||
perspective visible.
|
|
||||||
|
|
||||||
## 6. Évolutions (hors périmètre Étape 14)
|
|
||||||
- **Multi-ombres** : tableau de shadow maps + dépliage de `light_view_proj` en `[Mat4; N]` /
|
|
||||||
array de textures (array layer), PCF indexé par lumière. Change le contrat uniform → nouvelle
|
|
||||||
étape.
|
|
||||||
- **Ombre ponctuelle** : cube map 6 faces + `textureSampleCompare` sur `texture_depth_cube_array`.
|
|
||||||
- **Résolution / soft shadow dynamique** : `set_shadow_map_size` avec recréation de texture,
|
|
||||||
kernel PCF variable, blister/VSM.
|
|
||||||
- **Optimisation** : limiter la pass ombre aux seules entités dans le cône/frustum de la lumière.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Suivi (à compléter après implémentation)
|
|
||||||
> Remplir le bilan ici une fois l'étape livrée (comme pour les Étapes 12–13) : ce qui marche, ce
|
|
||||||
> qui a été vérifié, git-log du commit, et **vider** ce DRAFT pour l'étape suivante.
|
|
||||||
|
|
||||||
## 8. Ordre des commits (plan)
|
|
||||||
1. `feat(uniform): FrameUniforms étendu (shadow_light_index, light_view_proj, shadow_params)`
|
|
||||||
2. `feat(shadow): shader ombre + pipeline depth-only + ressources Renderer`
|
|
||||||
3. `feat(shadow): pass render_shadow_map + PCF dans standard_shader.wgsl`
|
|
||||||
4. `feat(scene): Scene::set_shadow_caster`
|
|
||||||
5. `test: exemple shadow_test + README des exemples`
|
|
||||||
6. (si besoin) `fix: bias/PCF — artefacts`
|
|
||||||
|
|||||||
+6
-1
@@ -141,7 +141,12 @@ generated: { by: human:jerome, at: 2026-07-31T00:00:00Z }
|
|||||||
- [x] Lumières hémisphériques *(déjà dans le `standard_shader` : mélange hémisphérique, Étape 2)*
|
- [x] Lumières hémisphériques *(déjà dans le `standard_shader` : mélange hémisphérique, Étape 2)*
|
||||||
- [x] Support multi-lumières (directionnelles, ponctuelles) *(Étape 12, 2026-09-18 : liste globale dans la `Scene`, tableau `FrameUniforms.lights[8]`, shader accumule ambiant + directionnelles + ponctuelles, `MAX_LIGHTS = 8`)*
|
- [x] Support multi-lumières (directionnelles, ponctuelles) *(Étape 12, 2026-09-18 : liste globale dans la `Scene`, tableau `FrameUniforms.lights[8]`, shader accumule ambiant + directionnelles + ponctuelles, `MAX_LIGHTS = 8`)*
|
||||||
- [x] Lumières spot (cône + angle) *(Étape 13, 2026-09-18 : même struct `Light` + champ `dir_angle` (axe du cône + cos du demi-angle) + compteur `num_spot` ; boucle d'accumulation dédiée dans le shader avec pénombre lissée et atténuation linéaire ; `Scene::add_spot_light`)*
|
- [x] Lumières spot (cône + angle) *(Étape 13, 2026-09-18 : même struct `Light` + champ `dir_angle` (axe du cône + cos du demi-angle) + compteur `num_spot` ; boucle d'accumulation dédiée dans le shader avec pénombre lissée et atténuation linéaire ; `Scene::add_spot_light`)*
|
||||||
- [ ] Shadows (optionnel)
|
- [x] Shadows (optionnel) *(Étape 14, 2026-09-19 : shadow mapping mono-lumière — light unique
|
||||||
|
(directionnelle **ou** spot) choisie par `Scene::set_shadow_caster(index)` ; depth-only `shadow_shader.wgsl`
|
||||||
|
+ pipeline ombre dans le `Renderer` (shadow map 1024² Depth32Float, bias slope-scaled) ; pass
|
||||||
|
`render_shadow_map` en tête de `render_scene` ; PCF 3×3 + comparateur dans `standard_shader.wgsl`
|
||||||
|
(groupe @3 partagé, lié mais non échantillonné quand désactivé → non-régression). Ombres **éteintes
|
||||||
|
par défaut**. Exemple `shadow_test` : cube tournant projetant une ombre sur un sol.)*
|
||||||
|
|
||||||
### 4.3 Optimisations
|
### 4.3 Optimisations
|
||||||
- [ ] Batching par Material (réduction des state changes GPU)
|
- [ ] Batching par Material (réduction des state changes GPU)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
//! proving the depth comparison is applied per-pixel.
|
//! proving the depth comparison is applied per-pixel.
|
||||||
//!
|
//!
|
||||||
//! Run with: `cargo run -p wsg-lib --example shadow_test`
|
//! Run with: `cargo run -p wsg-lib --example shadow_test`
|
||||||
use glam::{Vec3};
|
use glam::Vec3;
|
||||||
use wsg_lib::resources::{Camera, Geometry};
|
use wsg_lib::resources::{Camera, Geometry};
|
||||||
use wsg_lib::utils::WsgError;
|
use wsg_lib::utils::WsgError;
|
||||||
|
|
||||||
@@ -30,7 +30,12 @@ fn box_geometry(hx: f32, hy: f32, hz: f32) -> Geometry {
|
|||||||
), // +Z
|
), // +Z
|
||||||
(
|
(
|
||||||
[0.0, 0.0, -1.0],
|
[0.0, 0.0, -1.0],
|
||||||
[[hx, -hy, -hz], [-hx, -hy, -hz], [-hx, hy, -hz], [hx, hy, -hz]],
|
[
|
||||||
|
[hx, -hy, -hz],
|
||||||
|
[-hx, -hy, -hz],
|
||||||
|
[-hx, hy, -hz],
|
||||||
|
[hx, hy, -hz],
|
||||||
|
],
|
||||||
), // -Z
|
), // -Z
|
||||||
(
|
(
|
||||||
[1.0, 0.0, 0.0],
|
[1.0, 0.0, 0.0],
|
||||||
@@ -38,7 +43,12 @@ fn box_geometry(hx: f32, hy: f32, hz: f32) -> Geometry {
|
|||||||
), // +X
|
), // +X
|
||||||
(
|
(
|
||||||
[-1.0, 0.0, 0.0],
|
[-1.0, 0.0, 0.0],
|
||||||
[[-hx, -hy, hz], [-hx, hy, hz], [-hx, hy, -hz], [-hx, -hy, -hz]],
|
[
|
||||||
|
[-hx, -hy, hz],
|
||||||
|
[-hx, hy, hz],
|
||||||
|
[-hx, hy, -hz],
|
||||||
|
[-hx, -hy, -hz],
|
||||||
|
],
|
||||||
), // -X
|
), // -X
|
||||||
(
|
(
|
||||||
[0.0, 1.0, 0.0],
|
[0.0, 1.0, 0.0],
|
||||||
@@ -46,7 +56,12 @@ fn box_geometry(hx: f32, hy: f32, hz: f32) -> Geometry {
|
|||||||
), // +Y
|
), // +Y
|
||||||
(
|
(
|
||||||
[0.0, -1.0, 0.0],
|
[0.0, -1.0, 0.0],
|
||||||
[[-hx, -hy, hz], [hx, -hy, hz], [hx, -hy, -hz], [-hx, -hy, -hz]],
|
[
|
||||||
|
[-hx, -hy, hz],
|
||||||
|
[hx, -hy, hz],
|
||||||
|
[hx, -hy, -hz],
|
||||||
|
[-hx, -hy, -hz],
|
||||||
|
],
|
||||||
), // -Y
|
), // -Y
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
+11
-14
@@ -27,7 +27,7 @@ use crate::pipeline::{
|
|||||||
};
|
};
|
||||||
use crate::resources::uniform::{FRAME_UNIFORMS_SIZE, OBJECT_UNIFORM_SIZE, SHADOW_UNIFORM_SIZE};
|
use crate::resources::uniform::{FRAME_UNIFORMS_SIZE, OBJECT_UNIFORM_SIZE, SHADOW_UNIFORM_SIZE};
|
||||||
use crate::resources::{
|
use crate::resources::{
|
||||||
Camera, FrameUniforms, Lights, Material, Mesh, ObjectUniform, ShadowUniform, MAX_LIGHTS,
|
Camera, FrameUniforms, Lights, MAX_LIGHTS, Material, Mesh, ObjectUniform, ShadowUniform,
|
||||||
};
|
};
|
||||||
use crate::scene::Scene;
|
use crate::scene::Scene;
|
||||||
use crate::utils::conf::{
|
use crate::utils::conf::{
|
||||||
@@ -350,11 +350,9 @@ impl Renderer {
|
|||||||
-light.position_dir.y,
|
-light.position_dir.y,
|
||||||
-light.position_dir.z,
|
-light.position_dir.z,
|
||||||
),
|
),
|
||||||
crate::resources::LightType::Spot { .. } => Vec3::new(
|
crate::resources::LightType::Spot { .. } => {
|
||||||
light.dir_angle.x,
|
Vec3::new(light.dir_angle.x, light.dir_angle.y, light.dir_angle.z)
|
||||||
light.dir_angle.y,
|
}
|
||||||
light.dir_angle.z,
|
|
||||||
),
|
|
||||||
crate::resources::LightType::Point => return None,
|
crate::resources::LightType::Point => return None,
|
||||||
};
|
};
|
||||||
let r = SHADOW_SCENE_RADIUS;
|
let r = SHADOW_SCENE_RADIUS;
|
||||||
@@ -371,8 +369,7 @@ impl Renderer {
|
|||||||
// scene-radius behind the target, so the box [−r, r] around the target spans a depth range
|
// scene-radius behind the target, so the box [−r, r] around the target spans a depth range
|
||||||
// of [0, 2r] from the eye: `far = 2·r` covers the whole box (and the shadows cast behind
|
// of [0, 2r] from the eye: `far = 2·r` covers the whole box (and the shadows cast behind
|
||||||
// it), whereas `far = r` would clip the far half.
|
// it), whereas `far = r` would clip the far half.
|
||||||
let proj =
|
let proj = glam::camera::rh::proj::directx::orthographic(-r, r, -r, r, 0.0, 2.0 * r);
|
||||||
glam::camera::rh::proj::directx::orthographic(-r, r, -r, r, 0.0, 2.0 * r);
|
|
||||||
Some((index, proj * view))
|
Some((index, proj * view))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,8 +522,11 @@ impl Renderer {
|
|||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
let shadow_uniform = ShadowUniform { view_proj: vp };
|
let shadow_uniform = ShadowUniform { view_proj: vp };
|
||||||
self.queue
|
self.queue.write_buffer(
|
||||||
.write_buffer(&self.shadow_uniform_buffer, 0, bytemuck::bytes_of(&shadow_uniform));
|
&self.shadow_uniform_buffer,
|
||||||
|
0,
|
||||||
|
bytemuck::bytes_of(&shadow_uniform),
|
||||||
|
);
|
||||||
|
|
||||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
label: Some("shadow map render pass"),
|
label: Some("shadow map render pass"),
|
||||||
@@ -654,10 +654,7 @@ fn create_depth_texture(
|
|||||||
/// comparison sampler). Allocated once at the default resolution; resizing is deferred (D8).
|
/// comparison sampler). Allocated once at the default resolution; resizing is deferred (D8).
|
||||||
/// Inputs: device (GPU resource creator), size (shadow map edge length in pixels).
|
/// Inputs: device (GPU resource creator), size (shadow map edge length in pixels).
|
||||||
/// Returns the (texture, view) pair; the caller keeps both alive.
|
/// Returns the (texture, view) pair; the caller keeps both alive.
|
||||||
fn create_shadow_map(
|
fn create_shadow_map(device: &wgpu::Device, size: u32) -> (wgpu::Texture, wgpu::TextureView) {
|
||||||
device: &wgpu::Device,
|
|
||||||
size: u32,
|
|
||||||
) -> (wgpu::Texture, wgpu::TextureView) {
|
|
||||||
let shadow_texture = device.create_texture(&wgpu::TextureDescriptor {
|
let shadow_texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
label: Some("shadow map"),
|
label: Some("shadow map"),
|
||||||
size: wgpu::Extent3d {
|
size: wgpu::Extent3d {
|
||||||
|
|||||||
@@ -470,4 +470,3 @@ pub fn build_shadow_pipeline(
|
|||||||
cache: None,
|
cache: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ pub use material::Material;
|
|||||||
pub use mesh::Mesh;
|
pub use mesh::Mesh;
|
||||||
pub use texture::{Texture, TextureError};
|
pub use texture::{Texture, TextureError};
|
||||||
pub use uniform::{
|
pub use uniform::{
|
||||||
FrameUniforms, Light, LightType, MAX_LIGHTS, ObjectUniform, ShadowUniform, FRAME_UNIFORMS_SIZE,
|
FRAME_UNIFORMS_SIZE, FrameUniforms, Light, LightType, MAX_LIGHTS, OBJECT_UNIFORM_SIZE,
|
||||||
OBJECT_UNIFORM_SIZE, SHADOW_UNIFORM_SIZE,
|
ObjectUniform, SHADOW_UNIFORM_SIZE, ShadowUniform,
|
||||||
};
|
};
|
||||||
pub use vertex::Vertex;
|
pub use vertex::Vertex;
|
||||||
|
|
||||||
|
|||||||
@@ -204,8 +204,14 @@ mod tests {
|
|||||||
offset_of!(FrameUniforms, num_directional),
|
offset_of!(FrameUniforms, num_directional),
|
||||||
160 + 64 * MAX_LIGHTS
|
160 + 64 * MAX_LIGHTS
|
||||||
);
|
);
|
||||||
assert_eq!(offset_of!(FrameUniforms, num_point), 160 + 64 * MAX_LIGHTS + 4);
|
assert_eq!(
|
||||||
assert_eq!(offset_of!(FrameUniforms, num_spot), 160 + 64 * MAX_LIGHTS + 8);
|
offset_of!(FrameUniforms, num_point),
|
||||||
|
160 + 64 * MAX_LIGHTS + 4
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
offset_of!(FrameUniforms, num_spot),
|
||||||
|
160 + 64 * MAX_LIGHTS + 8
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
offset_of!(FrameUniforms, shadow_light_index),
|
offset_of!(FrameUniforms, shadow_light_index),
|
||||||
160 + 64 * MAX_LIGHTS + 12
|
160 + 64 * MAX_LIGHTS + 12
|
||||||
|
|||||||
Reference in New Issue
Block a user