--- type: Roadmap title: WSG Engine Development Roadmap description: Development roadmap for the WSG engine from prototype to full-featured 3D rendering engine tags: [roadmap, development, planning, wsg-lib, 3d-rendering] status: stable generated: { by: human:jerome, at: 2026-07-31T00:00:00Z } --- # Roadmap WSG — Prototype → Moteur Complet > Basé sur l'architecture existante (ARCHI_APP, ARCHI_ARENES, ARCHI_CPU_GPU, ARCHI_RENDU). > Objectif : prototype fonctionnel d'abord, enrichissement progressif ensuite. > > **Point de départ (état réel au 2026-09-16 — la source de vérité est README.md).** > Les fondations suivantes existent et fonctionnent déjà ; cette roadmap décrit la **trajectoire à > venir** à partir de cet état (elle reprend les étapes 1-4 du README avant la montée GPU-driven) : > - Workflow manuel (`Context` + `Renderer` + `PipelineCache`) : ✅ fonctionnel (exemple `manual`). > - Façade `App` / `AppBuilder` / `AppHandler` : ✅ **Scene auto-render** (2026-09-16) — la vue de frame > est exposée (`Frame::view()`), `render()` dessine la scène en une passe groupée > (`App::render_scene`) et la présentation est automatique dans `App::run` (exemple `simple`). > - `Scene` avec identifiants **String** (décision prise — voir tableau Notes de Décision) : 🚧 enregistrement seul. > - `Camera` / `Transform` et `glam` : types et mathématiques présents (`math/`, `resources/camera.rs`), > initialement non branchés au pipeline — **désormais branchés** (caméra active + matrices monde écrites > chaque frame, Étape 4.3, 2026-09-16 ; voir §1.1/1.5 ci-dessous). > **Étape suivante (résolue 2026-09-17).** « 3D + éclairage Phong » (ROADMAP 1.3 + 1.5) est **atteinte** : > le rendu automatique n'est plus plat. L'infrastructure (Étapes 3+4, 2026-09-16) — `standard_shader.wgsl` > Phong (matrice `projection * view * world` + lumière directionnelle), uniform buffers branchés > (frame : view/proj/cam_pos + lumière ; par mesh : `world` dérivé du `Transform`), `Renderer` écrivant > chaque frame la caméra active et la matrice monde de chaque entité — est **branchée** sur l'exemple > `cube` (Étape 5, 2026-09-17) : un cube unitaire éclairé qui tourne à l'écran via `App::render_scene`. > Le shader `basic` est supprimé : le 2D plat devient la variante **unlit** de `standard` > (`Renderer::set_unlit(true)`). Objectif MVP **atteint**. --- ## Phase 1️⃣ — Prototype MVP : Un Mesh 3D éclairé à l'écran **Objectif** : Afficher un cube (ou autre mesh) 3D avec un éclairage Phong basique. ### 1.1 Dépendances & Mathématiques - [x] `glam = "0.33"` ajouté (`lib/Cargo.toml`) — déjà présent, utilisé par `math/transform.rs` et `resources/camera.rs` - [x] `slotmap` **retiré** — décision prise : **String IDs pour le MVP** ; slotmap reporté à l'étape "handles typés" (voir Notes de Décision) - [x] Module `math/` / `transform.rs`: - [x] Struct `Transform { translation: Vec3, rotation: Quat, scale: Vec3 }` - [x] Méthode `to_matrix() -> Mat4` pour calculer la matrice locale - [x] Struct `Camera { position: Vec3, target: Vec3, up: Vec3 }` : resources/camera.rs — enrichi en Étape 4.3 (fov/near/far + `with_perspective`) - [x] Fonctions `view_matrix()` et `projection_matrix(fov, aspect, near, far)` (Étape 4.3 : `projection_matrix(aspect)` utilise fov/near/far stockés) ### 1.2 Geometry & Mesh - [x] Créer struct `Geometry` (math/geometry.rs) — **fait** : - [x] `positions: Vec<[f32; 3]>` (obligatoire) - [x] `indices: Option>` (optionnel) - [x] `normals: Option>` (pour Phong) — plus `uvs: Option>` - [x] `colors: Option>` — **fait (Étape 8, 8.1, 2026-09-18)** : décidé en DRAFT Étape 8 (D1) ; le shader lit la couleur unlit, elle est donc portée dans `Geometry`. Conversion `Geometry -> Vec` via `Geometry::to_vertices()` (D6) pour l'upload. - [x] Refactorer `Mesh` pour contenir — **fait (Étape 8, 8.3, 2026-09-18)** : - [x] `geometry: Arc` (rétention CPU, D5) + accesseur `geometry()` - [x] `vertex_buffer: wgpu::Buffer` - [x] `index_buffer: Option` - Construction via `Mesh::from_geometry(device, Arc, material)` (D4) ; les anciennes voies `Mesh::new`/`with_material` (`&[Vertex]`) sont supprimées. - `Scene::create_mesh(id, Geometry, Option<&str>)` (8.4) ; exemples cube/simple/manual réécrits sur `Geometry` (8.5). - [x] Ajouter un mesh de test (cube unitaire) en exemple — **fait** (helper `cube_geometry` dans l'exemple `cube`, Étape 5, 2026-09-17) > **État (2026-09-18)** : `Mesh` porte son matériau (`mesh.material: Option>`, Étape 7) et > une source de vérité CPU partagée (`geometry: Arc`, Étape 8). `Mesh` ne porte **pas** de > `transform` : un même mesh est partagé par plusieurs entités ; le `transform` vit sur `Entity`. ### 1.3 Shader Phong Minimal - [x] Créer `standard_shader.wgsl` (Étape 2, 2026-09-16) : - [x] Vertex shader : projection * view * world * position - [x] Fragment shader : éclairage directionnel (+ hémisphérique) - [x] Uniforms : `view`, `proj`, `cam_pos`, `light_dir`, `light_color`, `options` - [x] Mettre à jour `Material` / pipeline pour supporter les uniforms du shader Phong (bind group layouts frame+object, Étape 3) — **désormais branché** sur l'exemple `cube` (Étape 5, 2026-09-17) ### 1.4 Scene avec identifiants (MVP : String IDs) - [x] `Scene` implémentée avec **String IDs** (`HashMap>`, `...Material`, entités) — état actuel validé ; décision : rester en String IDs pour le MVP - [x] Méthodes : `add_mesh()`, `get_mesh()`, `add_material()`, `add_entity()`, `iter_entities()`, `remove_entity()` - [x] Caméra active dans la `Scene` : `set_camera()` / `camera()` (Étape 4.3) - [ ] **Reporté (étape "Handles typés")** : migrer vers `slotmap` générationnel (`MeshId`/`MaterialId`) quand l'éviction/les performances le justifieront ### 1.5 Rendu du Prototype - [x] Uniform buffer pour la frame : `view`, `proj`, `cam_pos`, `light_dir` (Étapes 3+4) — écrit chaque frame depuis la caméra active - [x] Uniform buffer par mesh : `world` (calculée sur CPU depuis `transform.to_matrix()`, Étape 4.2) - [x] `Renderer::render_scene()` itère sur les entités de la Scene et dessine chacune (liaison bind groups frame+object) - [x] Exemple fonctionnel : un cube éclairé tourne à l'écran — **fait** (Étape 5, 2026-09-17 : brancher `standard` sur l'exemple `cube` + mesh cube + rotation via `App::render_scene`) --- ## Phase 2️⃣ — Scène enrichie **Objectif** : Étoffer la `Scene` au-delà du MVP. > Les ressources sont, pour le MVP, identifiées par **String IDs** (décision actée — voir Notes de > Décision). Une migration vers des **handles typés** (slotmap générationnel) reste planifiée quand > l'éviction/les performances le justifieront (voir 1.4, « reporté »). ### 2.1 Camera dans la Scene - [x] Intégrer `Camera` comme ressource de la Scene (Étape 4.3 : `Scene::set_camera` / `camera()`, caméra active unique) - [ ] Permettre plusieurs caméras (actuelle/inactive) et une sélection par identifiant (`scene.set_active_camera(camera_id)`) - [x] Exposer une caméra orbitale contrôlable (exemple final, Phase 5) — *(Étape 15.C, 2026-09-20 : `CameraController` orbitale pilotée par l'input unifié, branchée sur l'exemple `demo`)* ### 2.2 Meshes primitifs (bibliothèque procédurale, WSGL) - [x] 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.A, 2026-09-20 : implémenté, commit `4da89c7`)* - [x] Factoriser le `cube_geometry` des exemples (`cube.rs`) vers `primitives::cube` — *(Étape 15.A : `cube.rs` et `spot_test.rs` utilisent désormais `math::cube(1.0)` ; `shadow_test.rs` garde son `box_geometry` générique)* - [x] Tests unitaires : comptes de sommets/indices cohérents, normales unitaires orientées — *(6 tests dans `primitives.rs`)* ### 2.3 Input unifié (clavier / souris / gamepad, WSGL) - [x] 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) — *(Étape 15.B, 2026-09-20, commit `b41f7e2` : clavier/souris/molette faits ; gamepad réservé/reporté)* - [x] Boucle dans `App::run` (`begin_frame`/`end_frame`) + exposition `app.input()` / `app.input_mut()` — *(Étape 15.B : champs publics `app.input` + rotation `begin_frame`/`end_frame` autour de `update`)* - [x] Contrôleur caméra orbitale (`CameraController`) construit sur l'input — *(Étape 15.C, 2026-09-20, prérequis de l'exemple final `demo`)* - [~] Gamepad (v1 minimale optionnelle) — *(reporté, DRAFT D7 ; l'API d'input s'étendra sans rupture : champ `gamepad` réservé)* --- ## Phase 3️⃣ — GPU-Driven Rendering ✅ (2026-09-22) **Objectif** : Déléguer les calculs de transformation et culling au GPU (suivre ARCHI_CPU_GPU.md). **Statut** : implémenté (Étape 17, validé 2026-09-22, décisions D1–D14 — référence durable : `docs/tech/ARCHI_CPU_GPU.md`, texte intégral du draft : git `3a424af`). Culling **désactivé par défaut** (non-régression), opt-in `AppBuilder::with_culling(true)`. **Correction 2026-09-22** : bug « fenêtre noire » avec culling ON (arguments de `select` WGSL écrits à la convention HLSL — toutes les entités visibles étaient remises à 0) ; corrigé et vérifié par readback GPU (D14). ### 3.1 Compute Shader - [x] Buffer `TransformBuffer` (CPU → GPU) : positions/rotations/échelles brutes (`TransformSlot`, 64 B) - [x] Buffer `MatrixBuffer` (GPU calculé) : World Matrices finales (`MatSlot`, `STORAGE|UNIFORM`) - [x] Compute shader : calcul des World Matrices pour tous les meshes (`compute_matrices`) ### 3.2 Frustum Culling GPU - [x] Ajouter `BBox` dans `Geometry` (coins min/max locaux) + `math::Frustum` (Gribb–Hartmann `[0,1]`) - [x] Buffer `BoundingBoxBuffer` (CPU → GPU, ré-upload quand l'ensemble des meshes change, peu coûteux) - [x] Compute shader : culling sphère vs frustum (`cull`), **désactivé par défaut** *(bug « fenêtre noire » corrigé le 2026-09-22 — ordre des arguments de `select` WGSL inversé ; cf. D14 dans `docs/tech/ARCHI_CPU_GPU.md`)* - [x] Buffer `IndirectDrawBuffer` rempli par le GPU (`DrawSlot`, 80 B, zéro = no-op) ### 3.3 Rendu Indirect - [x] `draw_indexed_indirect()`/`draw_indirect()` au lieu de draw calls individuels - [x] Un draw indirect **par slot actif** (décision D1 — pas un draw fusionné unique) ; le shadow pass est aussi indirect --- ## Phase 4️⃣ — Fonctionnalités Avancées **Objectif** : Qualité visuelle et performances. ### 4.1 Textures - [x] Struct `Texture` avec chargement d'image *(Étape 10 : resources::Texture, from_rgba8/bytes/file, Rgba8UnormSrgb, sampler linear/repeat)* - [x] Ajouter `uvs: Option>` dans `Geometry` *(prérequis Étape 10, déjà présent dans le code — seul l'échantillonnage manquait)* - [x] BindGroup pour les textures dans le shader *(Étape 10 : groupe @2 sampler+texture sur toutes les pipelines, placeholder blanc)* - [x] `Material` supporte une texture diffuse *(Étape 10 : Material.texture + texture_bind_group, placeholder si None)* ### 4.2 Éclairage avancé - [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] 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] 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 projetant une ombre douce sur un sol.)* ### 4.3 Optimisations - [x] Batching par Material (réduction des state changes GPU) — 2026-09-22 (Étape 18 : draws groupés par `Arc` dans la passe principale, 1 `set_pipeline` par matériau distinct — le démo passe de 7 à 3 ; pass d'ombre inchangé) - [x] Level of Detail (LOD) — 2026-09-23 (Étape 19 : ≤ 4 niveaux/mesh — L0 exacte, L1–L3 par décimation gloutonne au setup (`Geometry::decimated`/`generate_lod_levels`, Règle A : retrait des plus petits triangles, weld par position exacte, rebase u16), packés dans les buffers vertex/index du mesh (offsets en unités d'élément, plafond 65 535 sommets) ; décision par frame **côté CPU** (sphère bounding projetée en pixels + hystérésis asymétrique ×0.8 — `math/lod.rs` pure, unit-testée), exécution **côté GPU** (le pass `cull` mappe niveau → ligne de la table LOD → args indirects) ; **activé par défaut**, `set_lod_enabled(false)` → rendu bit-à-bit identique au pré-LOD. Vérifié par readback GPU : zoom 4,6× → tous les meshes multi-niveaux passent au niveau 1 avec exactement leurs lignes L1 (ex. sphère 3840 → 1824 indices), stable frame à frame) - [ ] HDR + Tone Mapping (optionnel) ### 4.4 Gestion du Resize (cycle de vie Surface + Depth) - [x] Handler `WindowEvent::Resized` dans `AppRunner::window_event` (`app.rs`) *(Étape 11, 2026-09-18)* → recalculer `size`, prévenir de ne pas rendre tant que la taille est invalide (0). - [x] Reconfigurer la surface (`Context::configure`) à la nouvelle taille. - [x] Recréer la depth texture à la nouvelle taille (`Renderer::resize_depth(width, height)`) — le helper `create_depth_texture` isolé (Étape 9, D3) rend ce recreate trivial. - [x] Collecte du nouveau format si la configuration change (srgb etc.) → re-valider la compat pipeline. *(D4 : `App::resize` compare l'ancien/nouveau format et re-synchronise Renderer (`set_format`) + Scene (`init_gpu`) ; cas pathologique, structuré non exercé couramment)* > Géré en Étape 11 (2026-09-18) : la surface est désormais reconfigurée à chaque `Resized` et la > depth texture recréée en même temps (helper `create_depth_texture` isolé, Étape 9, D3). Le present > mode FIFO reste figé (voir Notes de Décision). --- ## Phase 5️⃣ — Documentation & Polish - [x] Exemple complet : mesh texturé, éclairé, avec caméra orbitale — *(Étape 15, 2026-09-20 : exemple `demo` — les 7 primitives, textures procédurales, 3 lumières, ombre portée, caméra orbitale live ; vérifié headless)* - [x] Documentation API — *(Étape 16, 2026-07-19 : guide utilisateur `docs/user/` en français (8 pages interconnectées) + rustdoc complet sur toute l'API publique ; le `ARCHI_SCENE.md` séparé prévu est remplacé par les pages `docs/user/` + rustdoc — DRAFT Étape 16, décision D3)* - [x] Tests unitaires : `Geometry`, `Scene`, `Transform` — *(Étape 16 : modules de tests ajoutés à `math/geometry.rs`, `math/transform.rs`, `scene/scene.rs` ; 28 tests unitaires + doctests au total, `cargo test --workspace` vert)* - [x] README mis à jour avec les nouvelles fonctionnalités — *(Étape 16 : README racine re-ancré — workflow déclaratif = recommandé, manuel = avancé, `demo` = showcase, pollster 1.x ; README de modules `lib/src/**` à jour ; liens tech docs interconnectés)* --- ## Notes de Décision | Décision | Raison | |----------|--------| | **Normals dès Phase 1** | Nécessaires pour le shader Phong ; sans elles, pas d'éclairage | | **BBox en Phase 3** | Utile uniquement pour le frustum culling GPU | | **World Matrix CPU → MVP, GPU → Phase 3** | Le MVP est plus simple avec un uniform par mesh ; la migration GPU-driven est progressive | | **String IDs pour le MVP, slotmap reporté** | Le code et le README utilisent des String IDs (simples, sûrs, figés avant la boucle de rendu) ; `ARCHI_ARENES.md` reste la cible "handles typés" pour plus tard. La dépendance `slotmap` a été retirée tant qu'elle est inutilisée | | **Present mode FIFO figé pour l'instant** | Le swapchain utilise `PresentMode::Fifo` avec `desired_maximum_frame_latency: 2` (double buffering vsync) — défaut sûr : pas de tearing, énergie minimale, zéro artefact. On **gèle ce choix** ; `Mailbox` (triple buffering) pourra être exposé en option et `Immediate` restera réservé à l'offscreen, **on s'occupera du present mode le moment venu** (quand le pipeline GPU-driven arrivera, Phase 3) — ce n'est pas bloquant pour les étapes 1-2 | | **Resize géré (avec recréation de la depth texture), acté en D3 (2026-09-18), réalisé en Étape 11 (2026-09-18)** | L'app reconfigure désormais la surface et recrée la depth texture **en même temps** à chaque `Resized` (`App::resize` → `Context::configure` + `Renderer::resize_depth`), via le helper `create_depth_texture` isolé. Vérifié au runtime (exemple `cube`) : pas de crash, pas d'artefact, aspect correct | | **WGSL `select(reject, accept, cond)`** | L'ordre des arguments est l'inverse de la convention HLSL : le **second** argument est retenu quand la condition est vraie. L'avoir écrit à la convention HLSL a produit le bug « fenêtre noire » du culling (comptes remis à 0 pour les entités visibles), corrigé le 2026-09-22 (D14). Piège documenté en tête de `gpu_driven.wgsl`, dans `AGENTS.md` et `docs/tech/ARCHI_CPU_GPU.md` |