Files
wsg/docs/ROADMAP.md
T
Jérôme Bousquié a3a7ff4a6b LOD: quadric edge collapse (Garland-Heckbert) replaces Rule A
Rule A (area-sorted triangle removal) left holes and open boundaries
on closed meshes (visible artifacts when zoomed far out). The decimator
is now a proper quadric edge collapse:

- Collapse: welded u32 topology, per-vertex quadrics (accumulated
  incident face planes), edge cost = quadric error at the optimal
  point (clamped to the segment) + edge length, BinaryHeap with a
  custom Ord (f32 is not Ord; inverted compare, NaN-safe).
- Standard GH semantics WITHOUT the new face: both incident pair faces
  degenerate and are removed; neighbouring faces remap and sweep over
  the region. Preserves the Euler characteristic and closedness (no
  holes, no books, no duplicate faces); interior collapse = -2 faces,
  boundary = -1. Guards: non-manifold edge (>2 faces) or a fold
  (duplicate sorted triple) rejects the collapse.
- welded(): fuzzy welding (1e-6 relative tolerance, grid + 27-
  neighbour broad phase, exact verify) - trig-generated seams differ
  by ~1e-16, exact-bit welding missed them.
- Root causes fixed along the way: dead face slots are never reused
  (stale edge/vface entries), vfaces updated on remap, degenerate
  faces dropped, best-effort target (granularity -2/-1 can land 1-2
  off; soft cap, deterministic).
- Docs: DRAFT D10, ROADMAP 4.3, ARCHI_CPU_GPU, gpu-driven, lib
  README, mesh/scene/demo comments - 'greedy decimation / Rule A'
  replaced by 'quadric edge collapse'.
- lod.rs test: deprecated glam perspective alias -> explicit
  glam::camera::rh::proj::opengl::perspective.

cargo test --workspace: 100 passed (94 lib + 3 wgsl + 3 integration),
0 failed; demo runs clean with LOD on.
2026-09-23 11:46:12 +02:00

18 KiB
Raw Blame History

type, title, description, tags, status, generated
type title description tags status generated
Roadmap WSG Engine Development Roadmap Development roadmap for the WSG engine from prototype to full-featured 3D rendering engine
roadmap
development
planning
wsg-lib
3d-rendering
stable
by at
human:jerome 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

  • glam = "0.33" ajouté (lib/Cargo.toml) — déjà présent, utilisé par math/transform.rs et resources/camera.rs
  • slotmap retiré — décision prise : String IDs pour le MVP ; slotmap reporté à l'étape "handles typés" (voir Notes de Décision)
  • Module math/ / transform.rs:
    • Struct Transform { translation: Vec3, rotation: Quat, scale: Vec3 }
    • Méthode to_matrix() -> Mat4 pour calculer la matrice locale
    • Struct Camera { position: Vec3, target: Vec3, up: Vec3 } : resources/camera.rs — enrichi en Étape 4.3 (fov/near/far + with_perspective)
    • 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

  • Créer struct Geometry (math/geometry.rs) — fait :
    • positions: Vec<[f32; 3]> (obligatoire)
    • indices: Option<Vec<u16>> (optionnel)
    • normals: Option<Vec<[f32; 3]>> (pour Phong) — plus uvs: Option<Vec<[f32; 2]>>
    • colors: Option<Vec<[f32; 4]>> — 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<Vertex> via Geometry::to_vertices() (D6) pour l'upload.
  • Refactorer Mesh pour contenir — fait (Étape 8, 8.3, 2026-09-18) :
    • geometry: Arc<Geometry> (rétention CPU, D5) + accesseur geometry()
    • vertex_buffer: wgpu::Buffer
    • index_buffer: Option<wgpu::Buffer>
    • Construction via Mesh::from_geometry(device, Arc<Geometry>, 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).
  • 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<Arc<Material>>, Étape 7) et une source de vérité CPU partagée (geometry: Arc<Geometry>, É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

  • Créer standard_shader.wgsl (Étape 2, 2026-09-16) :
    • Vertex shader : projection * view * world * position
    • Fragment shader : éclairage directionnel (+ hémisphérique)
    • Uniforms : view, proj, cam_pos, light_dir, light_color, options
  • 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)

  • Scene implémentée avec String IDs (HashMap<String, Arc<Mesh>>, ...Material, entités) — état actuel validé ; décision : rester en String IDs pour le MVP
  • Méthodes : add_mesh(), get_mesh(), add_material(), add_entity(), iter_entities(), remove_entity()
  • 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

  • Uniform buffer pour la frame : view, proj, cam_pos, light_dir (Étapes 3+4) — écrit chaque frame depuis la caméra active
  • Uniform buffer par mesh : world (calculée sur CPU depuis transform.to_matrix(), Étape 4.2)
  • Renderer::render_scene() itère sur les entités de la Scene et dessine chacune (liaison bind groups frame+object)
  • 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

  • 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))
  • 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)

  • 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)
  • 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)
  • 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)

  • 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é)
  • 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)
  • 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

  • Buffer TransformBuffer (CPU → GPU) : positions/rotations/échelles brutes (TransformSlot, 64 B)
  • Buffer MatrixBuffer (GPU calculé) : World Matrices finales (MatSlot, STORAGE|UNIFORM)
  • Compute shader : calcul des World Matrices pour tous les meshes (compute_matrices)

3.2 Frustum Culling GPU

  • Ajouter BBox dans Geometry (coins min/max locaux) + math::Frustum (Gribb–Hartmann [0,1])
  • Buffer BoundingBoxBuffer (CPU → GPU, ré-upload quand l'ensemble des meshes change, peu coûteux)
  • 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)
  • Buffer IndirectDrawBuffer rempli par le GPU (DrawSlot, 80 B, zéro = no-op)

3.3 Rendu Indirect

  • draw_indexed_indirect()/draw_indirect() au lieu de draw calls individuels
  • 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

  • Struct Texture avec chargement d'image (Étape 10 : resources::Texture, from_rgba8/bytes/file, Rgba8UnormSrgb, sampler linear/repeat)
  • Ajouter uvs: Option<Vec<[f32; 2]>> dans Geometry (prérequis Étape 10, déjà présent dans le code — seul l'échantillonnage manquait)
  • BindGroup pour les textures dans le shader (Étape 10 : groupe @2 sampler+texture sur toutes les pipelines, placeholder blanc)
  • Material supporte une texture diffuse (Étape 10 : Material.texture + texture_bind_group, placeholder si None)

4.2 Éclairage avancé

  • Lumières hémisphériques (déjà dans le standard_shader : mélange hémisphérique, Étape 2)
  • 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)
  • 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) *(É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

  • Batching par Material (réduction des state changes GPU) — 2026-09-22 (Étape 18 : draws groupés par Arc<Material> dans la passe principale, 1 set_pipeline par matériau distinct — le démo passe de 7 à 3 ; pass d'ombre inchangé)
  • Level of Detail (LOD) — 2026-09-23 (Étape 19 : ≤ 4 niveaux/mesh — L0 exacte, L1–L3 par quadric edge collapse (Garland–Heckbert) au setup (Geometry::decimated/generate_lod_levels : arêtes classées par coût quadrique, repli interne −2 faces / bordure −1, un mesh fermé reste fermé, weld tolérance 1e-6, 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)

  • 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).
  • Reconfigurer la surface (Context::configure) à la nouvelle taille.
  • 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.
  • 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

  • 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)
  • 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)
  • 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)
  • 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