primitive meshes
This commit is contained in:
+89
-32
@@ -1,42 +1,99 @@
|
||||
# DRAFT — Étape 20 : HDR + Tone Mapping ✅
|
||||
# Étape 21 — Module `mesh` : primitives optionnelles + import
|
||||
|
||||
> **STATUT : TERMINÉ** — implémenté et testé.
|
||||
> Ce document sera remplacé par le prochain draft.
|
||||
**Statut : ✅ TERMINÉE**
|
||||
|
||||
## Récapitulatif
|
||||
## Résumé
|
||||
|
||||
- [x] **20.1** — Shader TM (`tonemap.wgsl`) : fullscreen triangle + 2 curves (ACES/Reinhard) + validation naga ✅
|
||||
- [x] **20.2** — `ToneMapper` enum (`core/hdr.rs`) : dispatch compile-time ✅
|
||||
- [x] **20.3** — `AppBuilder::with_hdr(ToneMapper)` + plomberie App → AppRunner → Renderer ✅
|
||||
- [x] **20.4** — Allocation HDR (`Rgba16Float` offscreen) dans `Renderer::new` ✅
|
||||
- [x] **20.5** — Main pass conditionnel (cible HDR vs surface) ✅
|
||||
- [x] **20.6** — Passe TM (fullscreen triangle → surface sRGB) ✅
|
||||
- [x] **20.7** — Resize : recreation texture HDR + bind group ✅
|
||||
- [x] **20.8** — Démo HDR + documentation (`docs/user/hdr.md`) ✅
|
||||
Restructuration du module de géométrie :
|
||||
- `math/` supprimé — types (`Geometry`, `Transform`, `BBox`, `Frustum`, LOD) déplacés vers `core/`
|
||||
- `primitives.rs` (monolith) → `mesh/primitives/` (6 fichiers, un par famille)
|
||||
- Nouveau module `wsg::mesh` : point d'entrée unique pour les sources de géométrie
|
||||
- Features par primitive (`prim-cube`, `prim-sphere`, …) — zéro coût si désactivées
|
||||
- Parser OBJ intégré (zéro dep externe), wrapper glTF en stub
|
||||
- `prelude.rs` pour un glob import confortable
|
||||
- Re-exports top-level : `Geometry`, `Transform`, `BBox`
|
||||
|
||||
## Fichiers modifiés/créés
|
||||
## Structure finale
|
||||
|
||||
| Fichier | Action |
|
||||
|---------|--------|
|
||||
| `lib/src/shaders/tonemap.wgsl` | **Nouveau** — fullscreen triangle + fs_aces + fs_reinhard |
|
||||
| `lib/src/core/hdr.rs` | **Nouveau** — `ToneMapper` enum |
|
||||
| `lib/src/core/mod.rs` | + `pub mod hdr` + re-export |
|
||||
| `lib/src/core/renderer.rs` | + `HdrPipeline` struct, + HDR alloc, + TM pass, + resize, + helpers |
|
||||
| `lib/src/utils/conf.rs` | + `TONEMAP_SHADER` constant |
|
||||
| `lib/src/app.rs` | + `with_hdr()`, + `hdr` field plomberie |
|
||||
| `lib/src/lib.rs` | + `pub use ToneMapper` |
|
||||
| `lib/examples/demo.rs` | + `.with_hdr(ToneMapper::Aces)` |
|
||||
| `lib/examples/manual.rs` | + `None` param (backward compat) |
|
||||
| `lib/tests/wgsl_validate.rs` | + test tonemap |
|
||||
| `docs/user/hdr.md` | **Nouveau** — doc utilisateur |
|
||||
| `docs/user/README.md` | + lien HDR |
|
||||
```
|
||||
lib/src/
|
||||
├── lib.rs # + pub mod mesh, pub mod prelude, re-exports Geometry/Transform/BBox
|
||||
├── prelude.rs # glob re-exports (types quotidiens)
|
||||
├── core/
|
||||
│ ├── mod.rs # + geometry, transform, frustum, lod
|
||||
│ ├── geometry.rs # ← déplacé de math/
|
||||
│ ├── transform.rs # ← déplacé de math/
|
||||
│ ├── frustum.rs # ← déplacé de math/
|
||||
│ ├── lod.rs # ← déplacé de math/
|
||||
│ ├── renderer.rs
|
||||
│ ├── shadow.rs
|
||||
│ ├── hdr.rs
|
||||
│ ├── context.rs
|
||||
│ ├── frame.rs
|
||||
│ └── input.rs
|
||||
├── mesh/
|
||||
│ ├── mod.rs # re-exports flat (cube, plane, sphere, …, load_obj, …)
|
||||
│ ├── primitives/
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── cube.rs
|
||||
│ │ ├── plane.rs
|
||||
│ │ ├── sphere.rs # uv_sphere + icosphere
|
||||
│ │ ├── cylinder.rs
|
||||
│ │ ├── cone.rs
|
||||
│ │ └── torus.rs
|
||||
│ └── import/
|
||||
│ ├── mod.rs # MeshImportError
|
||||
│ ├── obj.rs # parser OBJ (zéro dep)
|
||||
│ └── gltf.rs # stub (wrapper gltf crate à implémenter)
|
||||
├── app.rs
|
||||
├── handler.rs
|
||||
├── pipeline/
|
||||
├── resources/
|
||||
├── scene/
|
||||
└── utils/
|
||||
```
|
||||
|
||||
## Features (Cargo.toml)
|
||||
|
||||
| Feature | Default | Fournit |
|
||||
|---------|---------|---------|
|
||||
| `prim-cube` | ✅ (via all-prims) | `cube(size)` |
|
||||
| `prim-plane` | ✅ | `plane(w, d, sx, sz)` |
|
||||
| `prim-sphere` | ✅ | `uv_sphere(…)`, `icosphere(…)` |
|
||||
| `prim-cylinder` | ✅ | `cylinder(…)` |
|
||||
| `prim-cone` | ✅ | `cone(…)` |
|
||||
| `prim-torus` | ✅ | `torus(…)` |
|
||||
| `all-prims` | ✅ (default) | les 6 ci-dessus |
|
||||
| `import-obj` | ⬜ | `load_obj(path)`, `parse_obj(str)` |
|
||||
| `import-gltf` | ⬜ | `load_gltf(path)` (stub) |
|
||||
|
||||
## Décisions
|
||||
|
||||
| # | Décision |
|
||||
|---|----------|
|
||||
| D1 | Un seul crate `wsg-lib` — pas de crate séparée |
|
||||
| D2 | Feature par famille de primitives |
|
||||
| D3 | Feature par format d'import |
|
||||
| D4 | Pas de trait `MeshSource` — fonctions qui retournent `Geometry` |
|
||||
| D5 | `Geometry::new()` / `Scene::add_mesh()` restent en core |
|
||||
| D6 | Module `wsg::mesh` au même niveau que `core`, `app` |
|
||||
| D7 | `primitives/` un fichier par famille |
|
||||
| D8 | `import/` un fichier par format |
|
||||
| D9 | Import retourne `Result<_, MeshImportError>` |
|
||||
| D10 | `default = ["all-prims"]` |
|
||||
| D11 | `all-prims` = les 6 primitives |
|
||||
| D12 | `math` disparaît — types re-exportés par `core` / top-level |
|
||||
|
||||
## Tests
|
||||
|
||||
- 99 unit tests ✅
|
||||
- 4 WGSL validation (dont `tonemap_shader_is_valid_wgsl`) ✅
|
||||
- 3 doctests ✅
|
||||
- 107 unit tests (dont 7 tests OBJ parser)
|
||||
- 4 WGSL validation
|
||||
- 5 doctests
|
||||
- **Total : 116 tests, 0 failures**
|
||||
|
||||
## Prochaine étape
|
||||
## Build vérifié
|
||||
|
||||
Phase 4 complète (4.1 + 4.2 + 4.3 + HDR/TM). Le ROADMAP peut être mis à jour.
|
||||
- `cargo check` (default = all-prims) ✅
|
||||
- `cargo check --no-default-features --features "prim-cube"` ✅
|
||||
- `cargo check --features "import-obj,import-gltf"` ✅
|
||||
- `cargo check --examples --features "import-obj"` ✅
|
||||
|
||||
+66
-178
@@ -1,202 +1,90 @@
|
||||
---
|
||||
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
|
||||
|
||||
# Roadmap WSG — Prototype → Moteur Complet
|
||||
**Vision** : une lib Rust de dessin 3D simple, fondée sur `wgpu`, où l'API utilisateur est
|
||||
déclarative (graph scène + traits) et où le rendu est **100 % GPU-driven** (indirect draws).
|
||||
|
||||
> 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).
|
||||
Ce document est la **vue d'ensemble de progression**. Chaque étape a son DRAFT détaillé
|
||||
(`DRAFT.md`, remplacé à chaque étape) et sa doc livrée (`docs/tech/`, `docs/user/`).
|
||||
|
||||
> **É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**.
|
||||
> **Légende** : ✅ fait · 🔶 partiel · ⬜ à faire · ❌ abandonné
|
||||
> **Principe** : chaque étape est **additive et opt-in** — non-régression structurelle garantie
|
||||
> (tout reste désactivable, les chemins existants ne changent pas).
|
||||
|
||||
---
|
||||
|
||||
## Phase 1️⃣ — Prototype MVP : Un Mesh 3D éclairé à l'écran
|
||||
## Phase 1 — Fondations ✅
|
||||
|
||||
**Objectif** : Afficher un cube (ou autre mesh) 3D avec un éclairage Phong basique.
|
||||
| # | Item | Statut |
|
||||
|---|------|:------:|
|
||||
| 1.1 | Contexte GPU (Instance, Surface, Adapter, Device, Queue) + boucle winit | ✅ |
|
||||
| 1.2 | Buffers & Pipeline (vertex buffer, pipeline compilé, fullscreen) | ✅ |
|
||||
| 1.3 | Geometry (struct `Geometry`, buffers GPU, topologie, `PrimitiveTopology`) | ✅ |
|
||||
|
||||
### 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)
|
||||
## Phase 2 — Scène & Transforms ✅
|
||||
|
||||
### 1.2 Geometry & Mesh
|
||||
- [x] Créer struct `Geometry` (math/geometry.rs) — **fait** :
|
||||
- [x] `positions: Vec<[f32; 3]>` (obligatoire)
|
||||
- [x] `indices: Option<Vec<u16>>` (optionnel)
|
||||
- [x] `normals: Option<Vec<[f32; 3]>>` (pour Phong) — plus `uvs: Option<Vec<[f32; 2]>>`
|
||||
- [x] `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.
|
||||
- [x] Refactorer `Mesh` pour contenir — **fait (Étape 8, 8.3, 2026-09-18)** :
|
||||
- [x] `geometry: Arc<Geometry>` (rétention CPU, D5) + accesseur `geometry()`
|
||||
- [x] `vertex_buffer: wgpu::Buffer`
|
||||
- [x] `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).
|
||||
- [x] Ajouter un mesh de test (cube unitaire) en exemple — **fait** (helper `cube_geometry` dans l'exemple `cube`, Étape 5, 2026-09-17)
|
||||
| # | Item | Statut |
|
||||
|---|------|:------:|
|
||||
| 2.1 | Primitives procédurales (`cube`, `plane`, `sphere`, `cylinder`, `cone`, `torus`) | ✅ |
|
||||
| 2.2 | Transforms (struct `Transform`, composition translation × rotation × scale) | ✅ |
|
||||
| 2.3 | Entités & scène (struct `Entity`, `Scene`, `TransformStore`, graph entité→mesh) | ✅ |
|
||||
| 2.4 | Camera (struct `Camera`, matrices view + perspective, `CameraController` orbital) | ✅ |
|
||||
|
||||
> **É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`.
|
||||
## Phase 3 — GPU-driven (cœur de la vision) ✅
|
||||
|
||||
### 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)
|
||||
| # | Item | Statut |
|
||||
|---|------|:------:|
|
||||
| 3.1 | Buffers par entité (Transform + Matrix, uniform par slot) | ✅ |
|
||||
| 3.2 | Compute matrices (compute shader : transform → world matrix) | ✅ |
|
||||
| 3.3 | Indirect draws (`draw_args` GPU, `draw_indirect` / `draw_indexed_indirect`) | ✅ |
|
||||
| 3.4 | Culling GPU (bounding sphere → frustum test → indirect args zéro) | ✅ |
|
||||
|
||||
### 1.4 Scene avec identifiants (MVP : String IDs)
|
||||
- [x] `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
|
||||
- [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
|
||||
## Phase 4 — Rendu avancé ✅
|
||||
|
||||
### 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`)
|
||||
| # | Item | Statut |
|
||||
|---|------|:------:|
|
||||
| 4.1 | Textures & matériaux (struct `Texture`, `Material`, bind groups, shader standard) | ✅ |
|
||||
| 4.2 | Lighting & ombres (directional + point + spot + ambient, shadow mapping PCF) | ✅ |
|
||||
| 4.3 | Batching & LOD (batching par matériau, LOD quadric edge collapse + hystérésis) | ✅ |
|
||||
| 4.4 | **HDR + Tone Mapping** (offscreen `Rgba16Float` + fullscreen TM pass ACES/Reinhard) | ✅ |
|
||||
|
||||
## Phase 5 — Qualité & polish ✅
|
||||
|
||||
| # | Item | Statut |
|
||||
|---|------|:------:|
|
||||
| 5.1 | Exemples (7 examples : hello_triangle → demo) | ✅ |
|
||||
| 5.2 | Documentation (tech/ + user/ + rustdoc 100 %) | ✅ |
|
||||
| 5.3 | Tests & robustesse (99 unit + 4 WGSL validation + 3 doctests) | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Phase 2️⃣ — Scène enrichie
|
||||
## Phase 6 — Post-MVP ⬜
|
||||
|
||||
**Objectif** : Étoffer la `Scene` au-delà du MVP.
|
||||
> Au-delà du scope initial. Chaque item est opt-in et indépendant.
|
||||
|
||||
> 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é »).
|
||||
| # | Item | Impact visuel | Effort | Statut |
|
||||
|---|------|:---:|:---:|:---:|
|
||||
| 6.1 | **Exposure control** (clavier / API live) | ⭐⭐ | Trés faible | ⬜ |
|
||||
| 6.2 | **Emissive materials** (champ `emissive` → bénéficie du HDR) | ⭐⭐⭐ | Faible | ⬜ |
|
||||
| 6.3 | **Bloom** (post-process : downsample → threshold → blur → composite) | ⭐⭐⭐ | Moyen | ⬜ |
|
||||
| 6.4 | **MSAA 4×** (anti-aliasing multi-échantillons + resolve) | ⭐⭐⭐ | Moyen | ⬜ |
|
||||
| 6.5 | **Normal mapping / PBR** (nouveau shader, tangent space, metalness-roughness) | ⭐⭐⭐ | Élevé | ⬜ |
|
||||
| 6.6 | **Cascaded Shadow Maps** (2–3 cascades + blend, plus de précision près de la camera) | ⭐⭐ | Élevé | ⬜ |
|
||||
| 6.7 | **SSAO** (ambient occlusion screen-space, depth + normal buffer) | ⭐⭐ | Élevé | ⬜ |
|
||||
|
||||
### 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`)*
|
||||
### Cibles techniques (refactoring)
|
||||
|
||||
### 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é)*
|
||||
| # | Item | Statut |
|
||||
|---|------|:------:|
|
||||
| 6.8 | Handles typés par ressource (slotmap) — `docs/tech/ARCHI_ARENES.md` | ⬜ |
|
||||
| 6.9 | API update géométrie par entité (per-frame, sans rebuild complet) | ⬜ |
|
||||
| 6.10 | Double-buffering des buffers Transform/Matrix (désync CPU/GPU) | ⬜ |
|
||||
| 6.11 | **Module `mesh`** : primitives en features optionnelles + import (OBJ/gltf) — `math/` supprimé | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Phase 3️⃣ — GPU-Driven Rendering ✅ (2026-09-22)
|
||||
## Liens
|
||||
|
||||
**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<Vec<[f32; 2]>>` 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<Material>` 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 **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, weld tolérance 1e-6 **conscient des attributs** (UV ≤ ½ tuile + normales dot > 0.9 — jamais à travers une seam/côte dure), mesh sans couture reste fermé, lèvres de couture protégées sinon (surface géométriquement complète), UVs/couleurs/normales interpolés au repli (normales héritées, jamais recalculées), 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` |
|
||||
- **Prochaine étape** : [DRAFT.md](DRAFT.md) (détail de l'étape en cours, remplacée à chaque itération)
|
||||
- **Architecture** : [docs/tech/](tech/ARCHI_APP.md)
|
||||
- **Utilisation** : [docs/user/](user/README.md)
|
||||
- **Livre de recette** : [docs/PLAN.md](PLAN.md)
|
||||
|
||||
@@ -20,6 +20,7 @@ GPU graphics background is required.
|
||||
| [Materials & textures](materials.md) | Appearance: the `standard` shader, unlit mode, diffuse textures |
|
||||
| [Lights](lights.md) | Directional, point, spot, ambient, `MAX_LIGHTS` |
|
||||
| [Shadows](shadows.md) | Shadow mapping: picking the casting light, the packed-index pitfall |
|
||||
| [Mesh & primitives](mesh.md) | Procedural generators + file import (OBJ), feature-gated |
|
||||
| [HDR & tone mapping](hdr.md) | Offscreen float render + ACES/Reinhard, opt-in via `with_hdr` |
|
||||
| [GPU-driven rendering](gpu-driven.md) | GPU world matrices + indirect draws, opt-in frustum culling |
|
||||
| [Camera & input](camera-input.md) | Active camera, orbital controller, unified keyboard/mouse state |
|
||||
@@ -27,6 +28,31 @@ GPU graphics background is required.
|
||||
|
||||
The pages are cross-linked: each page ends with a link to the next one.
|
||||
|
||||
## Design principle: opt-in = zero cost
|
||||
|
||||
WSG follows a strict rule: **a feature you don't enable costs nothing at runtime**.
|
||||
|
||||
| Feature | How to enable | If NOT enabled |
|
||||
|---------|--------------|----------------|
|
||||
| Shadows | `scene.set_shadow_caster(Some(idx))` | No shadow map allocated, no depth pass, no PCF sampling |
|
||||
| HDR + Tone mapping | `AppBuilder::with_hdr(ToneMapper::Aces)` | No offscreen texture, no TM pass, direct-to-surface render |
|
||||
| GPU-driven culling | `AppBuilder::with_gpu_driven(true)` | No compute pipeline, no indirect draw buffers |
|
||||
| LOD | `scene.create_mesh_with_lod(…, levels)` | Single-level mesh, no decimation, no hysteresis |
|
||||
| Primitives | Cargo feature `prim-*` (default: all) | Not compiled at all |
|
||||
| File import | Cargo feature `import-*` | Not compiled at all |
|
||||
|
||||
The distinction matters:
|
||||
- **Runtime opt-in** (shadows, HDR, culling, LOD): the code is compiled into your binary
|
||||
but is **completely inert** if you never call the activation method. No GPU resources are
|
||||
allocated, no passes execute, no per-frame overhead. The cost of the code being in the
|
||||
binary is a few KB — negligible.
|
||||
- **Compile-time opt-in** (primitives, import): the code is **not compiled at all** unless
|
||||
you opt in via Cargo features. This matters when you want to minimize compile time or
|
||||
binary size for a minimal build.
|
||||
|
||||
You can mix both: build with `--no-default-features --features "prim-cube"` for a minimal
|
||||
binary, then enable shadows/HDR at runtime only for the scenes that need them.
|
||||
|
||||
## Links
|
||||
|
||||
- Technical documentation (architecture): [ARCHI_APP](../tech/ARCHI_APP.md) · [ARCHI_RENDU](../tech/ARCHI_RENDU.md) · [ARCHI_CPU_GPU](../tech/ARCHI_CPU_GPU.md) · [ARCHI_ARENES](../tech/ARCHI_ARENES.md) · [FRAME_LOOP](../tech/FRAME_LOOP.md)
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# Module `mesh` — Sources de géométrie
|
||||
|
||||
Le module `wsg::mesh` est le point d'entrée unique pour **d'où vient la géométrie** :
|
||||
générateurs procéduraux ou import de fichiers.
|
||||
|
||||
## Primitives procédurales
|
||||
|
||||
Chaque famille de primitives est derrière une **feature** — vous ne compilez que ce dont vous avez besoin.
|
||||
|
||||
| Feature | Fonction | Description |
|
||||
|---------|----------|-------------|
|
||||
| `prim-cube` | `cube(size)` | Cube centré, 24 sommets, normales par face |
|
||||
| `prim-plane` | `plane(w, d, seg_x, seg_z)` | Plan horizontal XZ (normale +Y), subdivisé |
|
||||
| `prim-sphere` | `uv_sphere(r, sectors, stacks)` | Sphère lat/long, normales lisses |
|
||||
| `prim-sphere` | `icosphere(r, subdivisions)` | Icosphère (subdiv icosahedron) |
|
||||
| `prim-cylinder` | `cylinder(r, h, sectors)` | Cylindre (côté + caps), normales analytiques |
|
||||
| `prim-cone` | `cone(r, h, sectors)` | Cône (apex + base fermée) |
|
||||
| `prim-torus` | `torus(major, minor, seg_maj, seg_min)` | Tore, normales lisses |
|
||||
|
||||
### Features par défaut
|
||||
|
||||
```toml
|
||||
# Cargo.toml de votre projet
|
||||
[dependencies]
|
||||
wsg-lib = { path = "../lib" }
|
||||
# Default: toutes les primitives activées (all-prims)
|
||||
```
|
||||
|
||||
```toml
|
||||
# Ne compiler que le cube et la sphère :
|
||||
wsg-lib = { path = "../lib", default-features = false, features = ["prim-cube", "prim-sphere"] }
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```rust
|
||||
use wsg_lib::prelude::*;
|
||||
|
||||
let cube = cube(2.0);
|
||||
let sphere = uv_sphere(1.0, 32, 16);
|
||||
let ico = icosphere(1.0, 2);
|
||||
|
||||
// Tous retournent un Geometry (positions + normals + UVs + indices)
|
||||
assert_eq!(cube.positions.len(), 24);
|
||||
```
|
||||
|
||||
## Import de fichiers
|
||||
|
||||
| Feature | Fonction | Format |
|
||||
|---------|----------|--------|
|
||||
| `import-obj` | `load_obj(path)` / `parse_obj(str)` | Wavefront OBJ |
|
||||
| `import-gltf` | `load_gltf(path)` | glTF 2.0 / GLB (stub) |
|
||||
|
||||
### Parser OBJ
|
||||
|
||||
Supporte : `v`, `vn`, `vt`, `f` (3-4 sommets, triangulation en éventail).
|
||||
Si le fichier n'a pas de normales, elles sont **calculées** (pondération par aire).
|
||||
|
||||
```rust
|
||||
use wsg_lib::mesh::{load_obj, parse_obj};
|
||||
|
||||
// Depuis un fichier
|
||||
let geom = load_obj("model.obj")?;
|
||||
|
||||
// Depuis une string
|
||||
let geom = parse_obj("v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n")?;
|
||||
```
|
||||
|
||||
### Erreurs
|
||||
|
||||
```rust
|
||||
use wsg_lib::mesh::import::MeshImportError;
|
||||
|
||||
match load_obj("missing.obj") {
|
||||
Ok(geom) => { /* … */ }
|
||||
Err(MeshImportError::Io(e)) => eprintln!("fichier inaccessible: {e}"),
|
||||
Err(MeshImportError::Parse(e)) => eprintln!("syntaxe invalide: {e}"),
|
||||
Err(MeshImportError::Unsupported(e)) => eprintln!("feature non supportée: {e}"),
|
||||
}
|
||||
```
|
||||
|
||||
## De `Geometry` à la scène
|
||||
|
||||
Le module `mesh` produit des `Geometry` (données CPU). Pour les rendre,
|
||||
passez par `Scene::create_mesh` qui les transfère en GPU :
|
||||
|
||||
```rust
|
||||
use wsg_lib::prelude::*;
|
||||
use wsg_lib::mesh::cube;
|
||||
|
||||
// Dans AppHandler::setup :
|
||||
let geom = cube(1.0);
|
||||
app.scene.create_mesh("my_mesh", geom, Some("my_mat"))?;
|
||||
app.scene.add_entity("my_entity", "my_mesh")?;
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```sh
|
||||
cargo run -p wsg-lib --example import --features import-obj -- model.obj
|
||||
```
|
||||
|
||||
## Convention
|
||||
|
||||
- **Y-up**, origine centrée (sauf `plane` : plan XZ à y=0)
|
||||
- Normales **sortantes**
|
||||
- UVs dans [0,1]²
|
||||
- Winding **CCW** (face avant)
|
||||
Reference in New Issue
Block a user