diff --git a/docs/DRAFT.md b/docs/DRAFT.md index cb4629c..23a7428 100644 --- a/docs/DRAFT.md +++ b/docs/DRAFT.md @@ -1,23 +1,159 @@ # DRAFT — Étape suivante -> 📅 **Document vidé le 2026-09-19** (fin de l'Étape 14, Shadows — Phase 4.2, bilan archivé -> dans l'historique git). Ce fichier accueillera le plan de l'étape suivante. -> -> **Étape 14 (2026-09-19) : Shadow mapping mono-lumière — FAIT & vérifié (build + tests).** -> Ombres portées classiques en **deux passes** sur une lumière unique (directionnelle **ou** spot), -> choisie par `Scene::set_shadow_caster(index)` (D1). Le `Renderer` possède désormais la shadow map -> (1024², `Depth32Float`, `RENDER_ATTACHMENT | TEXTURE_BINDING`), le sampler **comparateur** -> (`LessEqual`, WebGPU NDC [0,1]), le bind group **@3** (sampler + texture), un buffer uniform -> `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 projetant une ombre douce sur -> un sol (lumière avant-droite basse, `toward_light (1.0, 0.5, 0.0)` — ombre visible). Commit-log : -> `c2cbd7f` (impl), `39167ee` (comparateur), `67bd7af` (direction directionnelle), -> `9a51ff7` (test : clip depth WebGPU + NdotL), `ab13fa7` (fix visibilité : réapplique `LessEqual` -> + lumière réglée après probe offscreen — ombre invisible car trop raide/occultée par le cube). +> 📅 **Plan de l'Étape 15 (2026-09-19).** L'Étape 14 (shadow mapping) est **FAIT & vérifié** (voir +> historique git + ROADMAP 4.2). Cette étape est une **« grosse étape »** : elle réunit les acquis en +> un **exemple final interactif** et pose deux briques réutilisables — une **bibliothèque de meshes +> primitifs** et un **module d'input unifié** (clavier / souris / gamepad). Elle relance aussi deux +> items de la ROADMAP : `2.2` (primitifs) et `2.3` (input), prérequis de l'exemple final (Phase 5). > > Source de vérité = code + README.md. Ce document est vidé à la complétion de chaque étape. + +--- + +## Objectif de l'Étape 15 + +Produire un **démonstrateur `demo`** interactif qui cumule tout le travail des étapes 8-14 : +un sol + plusieurs **primitives** (cube, icosphère, cylindre, cône) **texturées**, **éclairées** +(multi + spot, Étapes 12-13), projetant des **ombres** (Étape 14), et survolées par une **caméra +orbitale** pilotée au **clavier/souris** via un nouveau module d'input unifié — le tout dans le +workflow déclaratif `AppBuilder` + `AppHandler`, sans importer wgpu. + +Trois volets, à faire en sous-étapes de façon **incrémentale** (chaque volet compilable/testable seul) : + +- **15.A — `math::primitives`** : générateurs de `Geometry` prêts à l'emploi. +- **15.B — `core::input`** : `InputState` unifié (clavier/souris/gamepad) branché dans `App`. +- **15.C — `demo`** : exemple final = primitives + textures + lumières + ombres + caméra orbitale. + +--- + +## Décisions (D1…Dn) — à confirmer au fil de l'étape + +- **D1 — Emplacement des primitives : `math::primitives`** (à côté de `math::geometry`). Les + primitives **produisent une `Geometry`** (purement CPU, sans GPU) ; les ranger dans `math` préserve + la cohérence avec `Geometry`/`GeometryError` (`math/geometry.rs`). Exposées via `math::primitives::*`. + *Alternative* : `resources::primitives` (discoverabilité près de `Mesh`) — **rejetée** car on reste + en mathématiques pures avant tout upload GPU. +- **D2 — API des primitives** : chaque générateur renvoie une **`Geometry` complète** (positions + + normales + UVs + indices, pas de couleurs → défaut blanc opaque). Signatures proposées : + - `cube(size: f32) -> Geometry` (arête `size`, normales/UVs par face, 24 sommets / 36 indices) + - `plane(width: f32, depth: f32, seg_x: u32, seg_z: u32) -> Geometry` (normales +Y, UVs étirés) + - `uv_sphere(radius: f32, sectors: u32, stacks: u32) -> Geometry` (normales sphériques + UV lat/long) + - `icosphere(radius: f32, subdivisions: u32) -> Geometry` (subdivision d'icosaèdre ; normales = position normalisée) + - `cylinder(radius: f32, height: f32, sectors: u32) -> Geometry` (couvercle/haut/bas + flanc) + - `cone(radius: f32, height: f32, sectors: u32) -> Geometry` (sommet + base ouverte/fermée) + - `torus(major: f32, minor: f32, major_segments: u32, minor_segments: u32) -> Geometry` *(bonus)* + Chaque fonction documente les conventions de repère (axe Y vers le haut) et l'orientation des + normales (pertinent pour l'éclairage car le culling reste désactivé par défaut). +- **D3 — Factorisation du `cube_geometry` des exemples** : remplacer le helper local de `cube.rs` + (et le `shadow_test`/`spot_test` s'ils en ont un) par `math::primitives::cube(1.0)`. Pas de + changement visuel (mêmes normales/UVs) → non-régression vérifiable. +- **D4 — Emplacement de l'input : `core::input`** (à côté de `context.rs`/`frame.rs`/`renderer.rs`). + `InputState` est **CPU** (consomme les événements winit) ; il n'est **pas** dans WGSL (langage de + shader côté GPU, sans I/O) — D4a. Un éventuel uniform `InputUniforms` consultable en WGSL (ex. + debug/gizmo) est **hors périmètre** pour l'instant, gardé en note pour une étape ultérieure. +- **D5 — Modèle d'état `InputState`** : sémantique **pressed / held / released** par frame, plus + souris (position, delta, boutons, molette) et gamepad (v1 minimale). Rotation par + `begin_frame()` (à chaque `about_to_wait`, avant `AppHandler::update`) et `end_frame()` (après + render), permettant de dériver `pressed`/`released` depuis les événements et de remettre les deltas + à zéro proprement. +- **D6 — Exposition dans `App`** : `App` possède un `InputState` ; `app.input()` (lecture) / + `app.input_mut()` (écriture) disponibles dans `setup`/`update`/`render`. Le `event_handler` + (winit 0.30) **forwarde** les `WindowEvent` concernés (KeyboardInput, MouseInput, CursorMoved, + MouseWheel, CursorLeft/Entered) et les `DeviceEvent` (MouseMotion, boutons de gamepad si + activés) vers `InputState::handle(...)`. `KeyboardInput` demande `listen_device_events` si on veut + le delta souris hors capture — à vérifier selon la config winit 0.30. +- **D7 — Gamepad (v1 minimale / optionnelle)** : winit fournit les `DeviceEvent::GamepadButtonChanged` / + `GamepadAxisChanged` (virtuels) ; on les capture **si présent** dans l'API (`Rc`/`Vec` par slot ou + index), sans abstraction complète de mapping. Si le support winit 0.30 s'avère trop fragile, on + **reporte** le gamepad (item ROADMAP `2.3` reste alors partiellement `[ ]`) et on livre d'abord + clavier/souris — l'exemple final n'a besoin **que** du clavier + souris. +- **D8 — Caméra orbitale** : **contrôleur** `CameraController { yaw, pitch, distance, target }` qui + met à jour la `Camera` active (position recomputée sphériquement autour de `target`, `with_perspective` + inchangée) depuis `InputState`. Le **cline multi-caméras (`set_active_camera`, ROADMAP 2.1) n'est PAS + requis** pour l'étape : on pilote la caméra active unique. 2.1 reste un item à part plus tard (ou est + optionnel ici si le temps le permet). +- **D9 — Exemple `demo`** : scène statique de primitives (sol = `plane` texturé damier ; cube, + icosphère, cylindre, cône posés dessus, chacun texturé procéduralement) + lumière directionnelle + ombre-porteuse (Étape 14) + point + spot (Étapes 12-13). `update` fait lentement tourner la scène + (comme `cube`) **et** met à jour la caméra orbitale via l'input (glisser = orbite, molette = zoom, + touches = reset / présettes / bascule light). Un petit helper de **texture procédurale** + (damier/couleurs) évite tout asset disque (réutilise le pattern `checkerboard_rgba` de `cube.rs`). + +--- + +## Détail d'implémentation + +### 15.A — `math::primitives` (bibliothèque de meshes prédéfinis) + +Fichier : `lib/src/math/primitives.rs` (+ `pub mod primitives;` dans `math/mod.rs`). + +- Algorithmes procéduraux classiques ; chaque sortie est une `Geometry` validée + (`Geometry::validate` ne doit pas renvoyer d'erreur — invariant testé). +- Conventions : axe **Y vers le haut**, origine centrée (sauf `plane` éventuellement ancré), + normales **orientées vers l'extérieur**, UVs [0,1]² aussi continus que possible. +- `cube` : quotient le code de `cube_geometry` de `cube.rs` (mêmes faces/normales/UVs/indices). + +Tests (dans `math/primitives.rs` ou un `#[cfg(test)]`) : +- comptes attendus : cube → 24 sommets / 36 indices ; uv_sphere → `(sectors+1)*(stacks+1)` sommets ; +- `icosphere` : subdivision 0 → icosaèdre (12 sommets / 20 faces / 60 indices), refinement augmente ; +- normales de longueur == positions, normes ≈ 1 (tolérance), indices dans les bornes ; +- `plane(width, depth, 1, 1)` → 4 sommets / 6 indices, normales +Y. + +### 15.B — `core::input` (module d'input unifié) + +Fichier : `lib/src/core/input.rs` (+ `pub mod input;` et `pub use input::InputState;` dans +`core/mod.rs` si pertinent). + +Structures : +- `struct InputState { keys: KeyStates, mouse: MouseState, gamepad: GamepadState }` +- `KeyStates` : ensembles `pressed/held/released` sur une clé + (`winit::keyboard::Key`/`VirtualKeyCode` selon la version winit 0.30 — à trancher en D-impl) ; +- `MouseState` : `position: (f32, f32)`, `delta: (f32, f32)`, `scroll: (f32, f32)`, + boutons `MouseButton` avec sémantique pressed/held/released ; +- `GamepadState` : (v1 minimale) tableaux boutons/axes par index, optionnels. +- `GamepadState` peut être factorable/ignoré si winit ne livre pas proprement (D7). + +Méthodes publiques : +- `begin_frame()` / `end_frame()` — rotation des états, remise à zéro des deltas ; +- `handle(event: &winit::event::WindowEvent)` / `handle_device(event: &impl)` — + dispatch interne (kbd/mouse/scroll/gamepad) ; +- requêtes : `key_pressed/held/released(k)`, `mouse_button_*`, `mouse_position()/delta()/scroll()`. + +Branchement (`app.rs`) : +- le `AppRunner` (window_event, ~l.293) forwarde les événements concernés vers `app.input_mut()`, + et à chaque `about_to_wait` (l.283) appelle `input_mut().begin_frame()` avant + `handler.update()`, puis `input_mut().end_frame()` après la frame. +- `App` expose `input()` / `input_mut()`. + +### 15.C — Caméra orbitale + exemple final `demo` + +- `COMBO` : ajouter `OrbitalCamera` (dans `resources/camera.rs` ou un controleur proche) — + `pub struct CameraController { pub yaw: f32, pub pitch: f32, pub distance: f32, pub target: Vec3 }` + + `fn apply_to(&self, cam: &mut Camera)` (repositionne `cam.position` en sphériques). +- Exemple `lib/examples/demo.rs` (workflow déclaratif, comme `cube`/`shadow_test`) : + - `setup` : textures procédurales par mesh, `primitives::*`, matériaux `standard`, + lumières directionnelle (ombre-porteuse via `set_shadow_caster`) + point + spot, caméra par défaut ; + - `update` : lire `app.input()`, mettre à jour `CameraController` (glisser/déplacer la souris = + yaw/pitch ; molette = distance ; `R` = reset ; touches `1/2/3` = présettes de vue), appliquer à + la caméra active, rotation lente des primitives ; + - doc du fichier en anglais (convention README/exemples), commentaires internes au besoin. + +### Livrables & checklist finale + +- [x] 15.A `math::primitives` + tests (build + `cargo test`) +- [ ] 15.B `core::input` + boucle `App` + tests légers +- [ ] 15.C caméra orbitale + exemple `demo` vérifié au runtime +- [ ] Factorisations exemples (primitives::cube) sans régression visuelle +- [ ] `cargo fmt --all -- --check` propre, build workspace + exemples + tests +- [ ] ROADMAP : cocher `2.2` et `2.3` (ou marquer le gamepad `[~]` partiel si reporté) ; + README : ajouter les items roadmap N°13-15 (primitives, input, démo) +- [ ] Fin d'étape : vider ce `DRAFT.md` (bandeau bilan archivé) + +--- + +## Périmètre hors-étape (reporté / refusé) + +- `set_active_camera` multi-caméras (ROADMAP 2.1) — sauf si trivial, sinon item séparé. +- Uniform `InputUniforms` consultable en WGSL (debug/gizmo shader) — note pour plus tard. +- GPU-driven (Phase 3), batching matériau (4.3), LOD, HDR/tone mapping — étapes ultérieures. +- Gamepad complet (mapping abstrait) — v1 minimale seulement (D7). diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 3f1cb9a..2d0cba1 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -104,6 +104,16 @@ generated: { by: human:jerome, at: 2026-07-31T00:00:00Z } - [ ] Permettre plusieurs caméras (actuelle/inactive) et une sélection par identifiant (`scene.set_active_camera(camera_id)`) - [ ] Exposer une caméra orbitale contrôlable (exemple final, Phase 5) +### 2.2 Meshes primitifs (bibliothèque procédurale, WSGL) +- [ ] Module `math::primitives` générant des `Geometry` prêts à l'emploi (positions + normales + UVs + indices) : `cube`, `plane`, `uv_sphere`, `icosphere`, `cylinder`, `cone` (et `torus` en bonus) — *(Étape 15, avec l'exemple final)* +- [ ] Factoriser le `cube_geometry` des exemples (`cube.rs`) vers `primitives::cube` +- [ ] Tests unitaires : comptes de sommets/indices cohérents, normales unitaires orientées + +### 2.3 Input unifié (clavier / souris / gamepad, WSGL) +- [ ] Module `core::input` : `InputState` à sémantique cross-frame (pressed/held/released), consommation des `WindowEvent`/`DeviceEvent` winit, souris (position, delta, boutons, molette), clavier (touches), gamepad (v1 minimale optionnelle) +- [ ] Boucle dans `App::run` (`begin_frame`/`end_frame`) + exposition `app.input()` / `app.input_mut()` +- [ ] Contrôleur caméra orbitale (`CameraController`) construit sur l'input — *(Étape 15, prérequis de l'exemple final)* + --- ## Phase 3️⃣ — GPU-Driven Rendering