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.
18 KiB
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 |
|
stable |
|
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 (exemplemanual).- 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 dansApp::run(exemplesimple).Sceneavec identifiants String (décision prise — voir tableau Notes de Décision) : 🚧 enregistrement seul.Camera/Transformetglam: 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.wgslPhong (matriceprojection * view * world+ lumière directionnelle), uniform buffers branchés (frame : view/proj/cam_pos + lumière ; par mesh :worlddérivé duTransform),Rendererécrivant chaque frame la caméra active et la matrice monde de chaque entité — est branchée sur l'exemplecube(Étape 5, 2026-09-17) : un cube unitaire éclairé qui tourne à l'écran viaApp::render_scene. Le shaderbasicest supprimé : le 2D plat devient la variante unlit destandard(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é parmath/transform.rsetresources/camera.rsslotmapretiré — 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() -> Mat4pour 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()etprojection_matrix(fov, aspect, near, far)(Étape 4.3 :projection_matrix(aspect)utilise fov/near/far stockés)
- Struct
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) — plusuvs: 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 dansGeometry. ConversionGeometry -> Vec<Vertex>viaGeometry::to_vertices()(D6) pour l'upload.
- Refactorer
Meshpour contenir — fait (Étape 8, 8.3, 2026-09-18) :geometry: Arc<Geometry>(rétention CPU, D5) + accesseurgeometry()vertex_buffer: wgpu::Bufferindex_buffer: Option<wgpu::Buffer>- Construction via
Mesh::from_geometry(device, Arc<Geometry>, material)(D4) ; les anciennes voiesMesh::new/with_material(&[Vertex]) sont supprimées. Scene::create_mesh(id, Geometry, Option<&str>)(8.4) ; exemples cube/simple/manual réécrits surGeometry(8.5).
- Ajouter un mesh de test (cube unitaire) en exemple — fait (helper
cube_geometrydans l'exemplecube, Étape 5, 2026-09-17)
État (2026-09-18) :
Meshporte son matériau (mesh.material: Option<Arc<Material>>, Étape 7) et une source de vérité CPU partagée (geometry: Arc<Geometry>, Étape 8).Meshne porte pas detransform: un même mesh est partagé par plusieurs entités ; letransformvit surEntity.
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'exemplecube(Étape 5, 2026-09-17)
1.4 Scene avec identifiants (MVP : String IDs)
Sceneimplé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
slotmapgé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 depuistransform.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
standardsur l'exemplecube+ mesh cube + rotation viaApp::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
Cameracomme 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 :
CameraControllerorbitale pilotée par l'input unifié, branchée sur l'exempledemo)
2.2 Meshes primitifs (bibliothèque procédurale, WSGL)
- Module
math::primitivesgénérant desGeometryprêts à l'emploi (positions + normales + UVs + indices) :cube,plane,uv_sphere,icosphere,cylinder,cone(ettorusen bonus) — (Étape 15.A, 2026-09-20 : implémenté, commit4da89c7) - Factoriser le
cube_geometrydes exemples (cube.rs) versprimitives::cube— (Étape 15.A :cube.rsetspot_test.rsutilisent désormaismath::cube(1.0);shadow_test.rsgarde sonbox_geometrygé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 desWindowEvent/DeviceEventwinit, souris (position, delta, boutons, molette), clavier (touches), gamepad (v1 minimale optionnelle) — (Étape 15.B, 2026-09-20, commitb41f7e2: clavier/souris/molette faits ; gamepad réservé/reporté) - Boucle dans
App::run(begin_frame/end_frame) + expositionapp.input()/app.input_mut()— (Étape 15.B : champs publicsapp.input+ rotationbegin_frame/end_frameautour deupdate) - Contrôleur caméra orbitale (
CameraController) construit sur l'input — (Étape 15.C, 2026-09-20, prérequis de l'exemple finaldemo) - [~] Gamepad (v1 minimale optionnelle) — (reporté, DRAFT D7 ; l'API d'input s'étendra sans rupture : champ
gamepadré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
BBoxdansGeometry(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 deselectWGSL inversé ; cf. D14 dansdocs/tech/ARCHI_CPU_GPU.md) - Buffer
IndirectDrawBufferrempli 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
Textureavec chargement d'image (Étape 10 : resources::Texture, from_rgba8/bytes/file, Rgba8UnormSrgb, sampler linear/repeat) - Ajouter
uvs: Option<Vec<[f32; 2]>>dansGeometry(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)
Materialsupporte 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, tableauFrameUniforms.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+ champdir_angle(axe du cône + cos du demi-angle) + compteurnum_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-onlyshadow_shader.wgsl- pipeline ombre dans le
Renderer(shadow map 1024² Depth32Float, bias slope-scaled) ; passrender_shadow_mapen tête derender_scene; PCF 3×3 + comparateur dansstandard_shader.wgsl(groupe @3 partagé, lié mais non échantillonné quand désactivé → non-régression). Ombres éteintes par défaut. Exempleshadow_test: cube projetant une ombre douce sur un sol.)*
- pipeline ombre dans le
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, 1set_pipelinepar 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.rspure, unit-testée), exécution côté GPU (le passcullmappe 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::ResizeddansAppRunner::window_event(app.rs) (Étape 11, 2026-09-18) → recalculersize, 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 helpercreate_depth_textureisolé (Étape 9, D3) rend ce recreate trivial. - Collecte du nouveau format si la configuration change (srgb etc.) → re-valider la compat pipeline.
(D4 :
App::resizecompare 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
Resizedet la depth texture recréée en même temps (helpercreate_depth_textureisolé, É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 ; leARCHI_SCENE.mdséparé prévu est remplacé par les pagesdocs/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 --workspacevert) - 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 moduleslib/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 |