docs: draft Étape 7 implementation plan (Scene owns pipeline cache, Mesh->Material)

This commit is contained in:
Jérôme Bousquié
2026-09-17 12:13:12 +02:00
parent 4be9737065
commit df546a4ac9
+171 -139
View File
@@ -1,162 +1,194 @@
# DRAFT — Plan d'implémentation : « 3D + éclairage Phong » # DRAFT — Plan d'implémentation : « Rattachement PipelineCache → Scene & Mesh → Material »
> **Usage.** Ce fichier (dans `docs/`) sert de brouillon pour le plan détaillé de l'étape en cours. > **Usage.** Ce fichier (dans `docs/`) sert de brouillon pour le plan détaillé de l'étape en cours.
> **Son contenu est effacé au début de chaque nouvelle étape.** La source de vérité de l'état est > **Son contenu est effacé au début de chaque nouvelle étape.** La source de vérité de l'état est
> le code + README.md ; les autres docs `docs/*` restent stables. > le code + README.md ; les autres docs `docs/*` restent stables.
> >
> **Étape.** 3D + éclairage Phong (ROADMAP 1.3 + 1.5). Objectif MVP : **un mesh 3D éclairé à l'écran**, > **Étape.** Consolidation des ressources (PLAN Phase 2 : associer le `PipelineCache` à la `Scene` ;
> rendu automatiquement par la boucle `App` (Scene auto-render déjà en place). > `Mesh` référence son `Material`) — recouvre la partie non cochée de la Phase 1 (ROADMAP 1.2, refactor
> *matériau* de `Mesh`) sans toucher au refactor `Arc<Geometry>` (reporté).
> >
> **État de départ vérifié.** > **Objectif.** La gestion des matériaux est **entièrement portée par la `Scene`** (elle possède
> - Rendu automatique fonctionnel mais **plat** : `basic_shader.wgsl` pose les positions telles quelles > device + format + `PipelineCache`, et fabrique elle-même meshes et matériaux). Chaque `Mesh` possède
> (`vec4(position, 1.0)`), aucune matrice, aucune uniform, aucun éclairage. > une référence vers son `Material` ; l'entité ne porte plus de `material_id` (le lien vit sur le mesh).
> - `Renderer::render_scene` parcourt `iter_entities()` en une passe (`&self`, `&Scene`), sans transform. > L'API déclarative reste `AppBuilder` + scène automatique, sans wgpu dans les exemples.
> - `Scene::entities` : `HashMap<String, (mesh_id, material_id)>` — pas de `Transform` par entité.
> - `Camera` (`resources/camera.rs`) : **fichier orphelin, non exporté** (absent de `resources/mod.rs`) ; > **État de départ vérifié (2026-09-17).**
> `Transform`/`Geometry` exportés via `math`. > - `App` porte `cache: Option<PipelineCache>` (champ privé, accesseur `App::cache()`), **indépendant de la
> - `PipelineCache::build_pipeline` : `bind_group_layouts: &[]`, `immediate_size: 0` — aucun binding. > `Scene`**. Les exemples font `Material::new(format, id, app.cache())` puis `scene.add_material(...)`.
> - Défaut latente : `basic_shader.wgsl` déclare `@location(1) uv`, `(2) color` alors que le > - `Scene` **ne possède ni device, ni format, ni cache** : elle ne fait que stocker des `Arc<Mesh>` /
> `VertexBufferLayout` réel expose `(1) normal`, `(2) uv`, `(3) color`. > `Arc<Material>` *préfabriqués* et résout les entités en `(mesh_id, material_id)`.
> - Le lien mesh→matériau est porté par `Entity { mesh_id, material_id, transform }` ; `Mesh` (resources/mesh.rs)
> est un **conteneur GPU pur** (`vertex_buffer`, `index_buffer`, `num_vertices`, `num_indices`), sans matériau.
> - `Renderer::render_scene` itère `scene.iter_entities()` qui re-parse `(label, &Mesh, &Material, &Transform)`
> et appelle `draw_entity` ; les buffers frame/object sont déjà portés par le `Renderer` (Étape 3-4).
> - `manual.rs` est **indépendant** de `App`/`Scene` : il possède son propre `PipelineCache` local
> (chemin bas-niveau). Il n'est pas concerné par le déplacement du cache, seulement revalidé.
> - Tests : seul `lib/tests/wgsl_validate.rs` (naga) — **aucun test ne dépend** des signatures modifiées
> (`add_entity`, `add_material`, `Mesh`, `Material`, `iter_entities`).
--- ---
## Point d'étape — 2026-09-16 (fin de session, reprise sur autre machine) ## Étape 7.1 — La `Scene` possède le contexte pipeline (device + format + cache)
**État : infra posée, MVP 3D pas encore atteint.** Dernier commit : `f10e249` **But** : donner à la `Scene` de quoi fabriquer elle-même pipelines et meshes, à la place de `App`.
(`feat(renderer): active camera wired to frame uniforms (Étape 4.3)`), dépôt propre.
**Fait et committé (bases pour la reprise)** - [ ] **7.1.1 Nouveau champ `Scene.gpu: Option<SceneGpu>`** dans `scene/scene.rs`, avec
- Étapes **1, 2.1, 2.2, 3, 4** du DRAFT → ✅ (détails cochés ci-dessous). ```
- `cargo check --workspace --examples` 0 warning, tests Pod + wgsl OK, doc 0 warning, fmt propre. pub struct SceneGpu {
- `standard_shader.wgsl` validé par naga (test permanent) mais **pas encore branché** sur un pipeline d'exemple. device: Arc<wgpu::Device>,
- `basic` ignore encore les uniforms → les exemples `simple`/`manual` tournent mais le rendu reste plat. format: wgpu::TextureFormat,
cache: PipelineCache,
}
```
`Scene::new()` → `gpu: None` (la `Scene` reste constructible sans GPU, cf. `AppBuilder::build`).
- [ ] **7.1.2 `Scene::init_gpu(&mut self, device: Arc<wgpu::Device>, format: wgpu::TextureFormat) -> &mut Self`** :
pose `gpu = Some(SceneGpu { device, format, cache: PipelineCache::new(device.clone()) })` puis `&mut *self`.
Appelé **une fois dans `AppRunner::resumed`**, juste après la création du `Context`/`Renderer` et **avant**
`handler.setup(app)` (setup enregistre shaders/matériaux/meshes/entités → doit trouver le `gpu` prêt).
- [ ] **7.1.3 Accesseurs `pub` gardés** (panique si `gpu` absent, message « scene pipeline not initialized yet ») :
- `scene.device() -> &wgpu::Device`
- `scene.format() -> wgpu::TextureFormat`
- `scene.cache() -> &PipelineCache` et `scene.cache_mut() -> &mut PipelineCache`
- `scene.register_shader(id, path) -> Result<String, String>` (délègue à `cache.register_shader`, erreur si ID déjà pris)
- [ ] **7.1.4 Retirer la duplication de cache d'`App`** (`app.rs`) :
- supprimer le champ `cache: Option<PipelineCache>` et `AppBuilder` n'initialise plus `cache: None` ;
- `resumed` ne crée plus `let cache = PipelineCache::new(device)` séparément — il appelle `scene.init_gpu(...)` ;
- accesseur `App::cache()` : **supprimé** (les exemples passent à `app.scene.cache()`), ou conservé en mince
délégation `self.scene.cache_mut()` si l'on veut préserver l'API (voir Décisions).
- [ ] **Validation** : `cargo check --workspace --examples` 0 warning ; les exemples compilent (appelés à migrer
en 7.4). `cargo doc --no-deps` 0 warning.
**Réalisé (2026-09-17) — cette étape est terminée.** MVP 3D atteint : le cube éclairé tourne à l'écran. ## Étape 7.2 — La `Scene` fabrique matériaux & meshes (liage matériau sur le mesh)
- **Étape 5 (5.1 + 5.2)** : nouvel exemple `lib/examples/cube.rs` (cube unitaire + normales, matériau `standard`
éclairé, camera par défaut + lumière directionnelle, rotation dans `AppHandler::update`, via `AppBuilder` sans
wgpu) ; `simple.rs` et `manual.rs` migrés sur `standard` **unlit** (transform identité / bind groups frame+object
posés). Le shader `basic` **disparaît comme famille séparée** (2.3) — le rendu 2D plat = variante unlit de
`standard` (`Renderer::set_unlit(true)`).
- **Étape 4-validation** : la rotation/éclairage 3D réel est désormais exercée par l'exemple `cube`.
> Les cases 2.3, Étape 4-validation et Étape 5 (5.1/5.2/validation) sont désormais **cochées** ci-dessous = état exact. **But** : le `Mesh` porte son `Material` ; la `Scene` construit matériaux (via son cache+format) et meshes
(via son device) sans que l'utilisateur touche `Material::new` / `Mesh::new`.
- [ ] **7.2.1 `Mesh` gagne un champ matériau** (`resources/mesh.rs`) :
```
pub struct Mesh {
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: Option<wgpu::Buffer>,
pub num_vertices: u32,
pub num_indices: u32,
material: Option<Arc<Material>>, // nouveau
}
```
- `Mesh::new(device, verts, indices)` **inchangé** → `material: None`.
- Nouveau `Mesh::with_material(device, verts, indices, material: Arc<Material>)` (helper).
- Accesseurs : `mesh.material() -> Option<&Arc<Material>>`, `mesh.set_material(Arc<Material>)`.
- [ ] **7.2.2 `Scene::add_material_shader(&mut self, id, shader_id) -> Result<String, String>`** : exige `gpu` ;
construit `Material::new(self.format(), shader_id, self.cache_mut())`, insère dans `materials`, retourne `id`.
- [ ] **7.2.3 `Scene::create_mesh(&mut self, id, vertices: &[Vertex], indices: Option<&[u16]>, material: Option<&str>) -> Result<String, String>`** :
exige `gpu` ; `let mut m = Mesh::new(self.device(), verts, indices)` ; si `material = Some(name)` → résout
`Arc<Material>` depuis `materials` (erreur si absent) et `m.set_material(...)` ; insère `Arc::new(m)`.
- [ ] **7.2.4 Conserver les chemins custom** pour l'utilisateur avancé :
- `add_material(id, Arc<Material>)` et `add_mesh(id, Arc<Mesh>)` **inchangés** (le `Mesh` custom peut
ensuite être lié via `mesh.set_material(...)` ou rester sans matériau → défaut en 7.3.5).
- [ ] **Validation** : `cargo check --workspace --examples` 0 warning ; `cargo doc --no-deps` OK ; un test
unitaire additionnel si souhaité (`Scene::add_material_shader`/`create_mesh` nécessitent un `gpu` — test
hors-suite GPU, validation par compilation des exemples).
## Étape 7.3 — Le lien mesh→matériau remplace le `material_id` d'entité
**But** : `Entity` ne référencie plus qu'un `mesh_id` ; le rendu résout le matériau **depuis le mesh**.
- [ ] **7.3.1 `Entity`** (`scene/entity.rs`) : retirer le champ `material_id`.
```
pub struct Entity { mesh_id: String, transform: Transform }
```
`Entity::new(mesh_id, transform)` ; supprimer `material_id()`, garder `mesh_id()`, `transform()`, `set_transform()`.
- [ ] **7.3.2 `Scene` — signatures d'entité sans `material_id`** :
- `add_entity(&mut self, label, mesh_id)` → identité.
- `add_entity_with_transform(&mut self, label, mesh_id, transform)`.
- Validation d'existence inchangée : `mesh_id` doit exister dans `meshes` (le matériau est implicite → on ne
valide plus `material_id`).
- [ ] **7.3.3 `iter_entities()`** → `impl Iterator<Item = (&str, &Arc<Mesh>, &Transform)> + '_` :
résout `meshes.get(entity.mesh_id())`, **n'appelle plus** `get_material`. Le matériau arrive via `mesh.material()`.
- [ ] **7.3.4 `Renderer::render_scene`** (`core/renderer.rs`) : boucler sur `(label, mesh, transform)` ;
matériau = `mesh.material().cloned()` **sinon** `scene.default_material()` (7.3.5). `draw_entity` inchangé
(reçoit `&Material`, pose pipeline + bind groups frame/object).
- [ ] **7.3.5 `Scene::default_material(&self) -> Arc<Material>`** : matériau `standard` fabriqué **paresseusement**
(une fois, mis en cache) depuis `SceneGpu`. Note : **le flat « unlit » reste orthogonal** — c'est le flag
`Renderer::set_unlit` qui rend le flat (options.x du shader) ; le matériau par défaut est simplement le
pipeline `standard`. La « variante unlit » du PLAN = le rendu flat activé par le Renderer, pas une propriété
du `Material`.
- [ ] **Validation / cas limites** : scène vide (aucune entité → pass vide, comme avant) ; mesh **sans matériau**
→ `default_material()` ; mesh **0-vertex** → `draw_entity` continue de retourner tôt (`num_vertices == 0`).
## Étape 7.4 — Migration des exemples
- [ ] **`cube.rs`** (API déclarative, sans wgpu) — `setup` devient :
```rust
app.scene.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH).unwrap();
app.scene.add_material_shader("cube_material", "standard").unwrap();
app.scene.create_mesh("cube_mesh", &cube_vertices(), Some(&cube_indices()), Some("cube_material")).unwrap();
app.scene.add_entity("cube", "cube_mesh").unwrap();
```
La rotation dans `AppHandler::update` (`set_entity_transform`) est **inchangée**.
- [ ] **`simple.rs`** (quad plat, unlit) — `setup` :
```rust
app.renderer_mut().set_unlit(true);
app.scene.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH).unwrap();
app.scene.add_material_shader("standard_material", "standard").unwrap();
app.scene.create_mesh("quad_mesh", &vertices, Some(&indices), Some("standard_material")).unwrap();
app.scene.add_entity("quad", "quad_mesh").unwrap();
```
(Le comportement est identique : le quad flat vient du flag `set_unlit(true)`.)
- [ ] **`manual.rs`** : **inchangé** (bas-niveau, son propre `PipelineCache` local, hors `Scene`). À revalider
uniquement : `cargo check --workspace --examples` 0 warning ; il compile et tourne (quad flat).
- [ ] **Validation** : `cargo run -p examples` cube (rotation + éclairage visibles) et simple (quad flat) sans
panique ; `cargo check --workspace --examples` 0 warning ; `cargo test --workspace` vert.
## Étape 7.5 — Exports, docs & commits
- [ ] **Exports** : vérifier que les nouveaux types/méthodes sont `pub` et re-exportés là où l'API le promet
(`resources/mod.rs` déjà `pub use`, `scene/mod.rs` déjà `pub use`) ; `#![warn(missing_docs)]` → tous doc.
- [ ] **Docs** :
- `docs/PLAN.md` : cocher les 3 lignes non cochées de la Phase 2 — « Associer le PipelineCache à la Scene »,
« chaque Mesh possède une référence vers un Material », « injection automatique du standard_shader ».
- `docs/ROADMAP.md` : cocher la sous-partie *matériau* du 1.2 ; **laisser non coché** le refactor
`Arc<Geometry>` (stockage `Geometry` dans `Mesh`) qui reste reporté.
- `README.md` : refléter l'API déclarative (si les exemples changent).
- [ ] **Validation finale** : `cargo check --workspace` 0 warning ; `cargo doc --no-deps` 0 warning ;
`cargo fmt --all` ; `cargo test --workspace`.
- [ ] **Commits conventionnels** : `refactor(resources): Scene owns pipeline cache, Mesh->Material link` puis
`docs: mark PLAN Phase 2 / ROADMAP 1.2(material)` (deux commits séparés code/docs, comme à l'Étape 5).
--- ---
## Étape 1 — Fondations data : Transform + Camera exposées ## Décisions actées (à verrouiller avant l'implémentation)
**But** : donner à chaque entité un `Transform` et rendre `Camera` utilisable via l'API publique, **sans** | Décision | Option proposée / actée | Justification |
toucher au rendu (pure façade de données, validable par compilation). |----------|------------------------|---------------|
| Où vit le `PipelineCache` | **Dans la `Scene`** (`SceneGpu.cache`) ; `App` n'a plus de champ `cache` | PLAN Phase 2 : « la gestion des matériaux entièrement portée par la scène » ; `App` = orchestrateur mince |
| Construction des matériaux | **Par la `Scene`** (`add_material_shader`) depuis un `shader_id`, via son cache+format ; `add_material(Arc<Material>)` conservé pour usage custom | un seul point de vérité device/format ; les exemples ne touchent plus `Material::new` |
| Liage mesh→matériau | **Le `Mesh` possède `Option<Arc<Material>>`** ; l'entité ne porte plus `material_id` | PLAN : « chaque Mesh possède une référence vers un Material » ; API déclarative simplifiée ; `iter_entities` plus léger |
| `Entity` | `{ mesh_id, transform }` uniquement | le matériau est déduit du mesh ; une dimension de moins à synchroniser |
| Matériau par défaut | Injeté par `Scene::default_material()` = shader `standard` ; le flat reste `Renderer::set_unlit` | PLAN : injection auto du `standard_shader` ; « variante unlit » = flag rendu, **orthogonal** au `Material` |
| Partage « 1 mesh, N matériaux » | **Abandonné au MVP** : un mesh = un matériau ; le cas d'usage est reporté au step « handles/arènes » (ROADMAP Phase 2) | cohérence avec l'entité simplifiée ; trade-off assumé et tracé (voir Réflexions) |
| API `App::cache()` | **Supprimée**, exemples migrés vers `app.scene` (ou mince délégation si compat souhaitée) | évite deux chemins d'accès au cache ; `manual.rs` garde son cache local (découplé) |
- [X] 1.1 **Exporter `Camera`** : dans `lib/src/resources/mod.rs`, ajouter ## Réflexions / trade-offs
`pub mod camera;` et `pub use camera::Camera;` (aujourd'hui fichier orphelin non compilé). *(fait — 2026-09-16)*
- [X] 1.2 **Type `Entity` + transform** : nouvelle struct
`Entity { mesh_id: String, material_id: String, transform: Transform }` (module `scene` ou `resources`).
Remplacer `Scene::entities: HashMap<String, (String, String)>` par
`HashMap<String, Entity>`. Sérialiser `iter_entities()` pour rendre le `&Transform`.
*(fait — `lib/src/scene/entity.rs`)*
- [X] 1.3 **Compat API** : garder `add_entity(label, mesh_id, material_id)` (transform identité par défaut)
+ ajouter `add_entity_with_transform(label, mesh_id, material_id, transform)`.
Ajouter `entity_transform(label) -> Option<&Transform>` et `set_entity_transform(label, transform)`.
*(fait — 2026-09-16)*
- [X] **Validation** : `cargo check --workspace` 0 warning ; `cargo doc --no-deps` 0 warning ; les exemples
`simple`/`manual` compilent inchangés (défaut : identité ⇒ même rendu). *(fait — 0 warning. Au passage,
`camera.rs` étant désormais compilée, les fonctions glam dépréciées `look_at_rh`/`perspective_rh_gl` ont été
migrées vers `glam::camera::rh::view::look_at_mat4` / `glam::camera::rh::proj::opengl::perspective`.)*
## Étape 2 — Shader Phong `standard_shader.wgsl` - **Perte du partage multi-matériaux par mesh.** Aujourd'hui deux entités peuvent référencer le même `mesh_id`
avec deux `material_id` différents. En liant le matériau au mesh, ce cas n'est plus exprimable. C'est un
**But** : produire un rendu 3D éclairé via un nouveau shader, sans encore le brancher. choix **MVP assumé** (le PLAN le demande) ; il reviendra naturellement avec les **handles typés** et le
conteneur centralisé (ROADMAP Phase 2, ARCHI_RENDU : tri de rendu par matériau). Le batching n'est pas perdu :
- [X] 2.1 **Créer `lib/src/shaders/standard_shader.wgsl`** avec le **contrat vertex correct** : il se fera sur `mesh.material().pipeline` au lieu de `material_id`.
`@location(0) position : vec3`, `(1) normal : vec3`, `(2) uv : vec2`, `(3) color : vec4`. - **Le `default_material()` dépend de `gpu`.** Une `Scene` non initialisée (avant `resumed`) ne peut pas fabriquer
- `@group(0) @binding(0)` : `FrameUniforms { view: mat4, proj: mat4, cam_pos: vec4, light_dir: vec4, light_color: vec4, options: vec4<u32> }` (options.x = unlit flag) de matériau. Toute fabrication (matériau, mesh) **panique** avec un message clair si `gpu` est absent — ce ne
- `@group(1) @binding(0)` : `ObjectUniform { model: mat4 }` peut arriver en pratique que si `handler.setup` est appelé hors de `resumed` (invariant garanti par l'API).
- `vs_main` : `clip_position = proj * view * model * vec4(position,1)` ; passe `normal`/`color` en espace monde. - **Ne pas confondre avec ROADMAP 1.2 (refactor `Arc<Geometry>`).** Ce DRAFT lie le **matériau** au mesh. Le
- `fs_main` : éclairage hémisphérique (ambient) + diffuse directionnel (max(dot(N,L),0)), sortie `vec4(color*light, 1)`. fait que `Mesh` stocke aussi `Arc<Geometry>` (positions/indices/normales/uvs en CPU) reste **reporté** et sera
- **Mode unlit** : `options.x != 0` neutralise la directionnelle → couleur plate. Ainsi « 2D » = `standard` une étape distincte.
non-éclairé, **cas particulier de la 3D** (décision actée). *(fait — 2026-09-16)*
- [X] 2.2 **Constantes** : ajouter `STANDARD_SHADER_PATH = "assets/shaders/standard_shader.wgsl"` et
`STANDARD_SHADER: &str = include_str!("../shaders/standard_shader.wgsl")` dans `lib/src/utils/conf.rs`.
*(fait — 2026-09-16)*
- [X] 2.3 **Migrer `basic` vers le mode unlit de `standard`** (défaut latente réglée) : plus de pipeline au
**layout vide séparé**. Le rendu plat = `standard` non-éclairé (identité/ortho + ambiance) sous le **même
layout uniformisé**. Le fallback embarqué (`BASIC_SHADER`) devient la variante unlit de `standard`.
*(fait — Étape 5, 2026-09-17 : `basic` supprimé sans remplacement ; `set_unlit(true)` ; simple/manual migrate)*
- [X] **Validation** : shader validé hors-ligne via un **nouveau test permanent** `lib/tests/wgsl_validate.rs`
(naga via `wgpu::naga`, aucune nouvelle dépendance) ; corrigé au passage le cast `mat4x4 -> mat3x3` non
supporté (construction de la sous-matrice 3×3 explicite). `cargo test` + `cargo check --workspace --examples`
0 warning ; `cargo doc --no-deps` OK ; `cargo fmt` propre. Le shader n'est pas encore compilé par un pipeline
(Étape 3). *(fait — 2026-09-16)*
## Étape 3 — Infrastructure uniforms dans le `PipelineCache`
**But** : permettre aux pipelines de recevoir des uniforms (bind groups) au lieu de `bind_group_layouts: &[]`.
- [X] 3.1 **Types bytemuck `Pod`** (nouveau `lib/src/resources/uniform.rs`) : `FrameUniforms` (192 B) et
`ObjectUniform` (64 B), `#[repr(C)]`, 16-byte alignés, sans padding — offset vérifiés par un test
unitaire contre la table du shader. Exports via `resources/mod.rs`. *(fait — 2026-09-16. Au passage,
`glam` feature `bytemuck` activé pour que `Mat4`/`Vec4` implémentent `Pod`/`Zeroable`.)*
- [X] 3.2 **Bind group layouts** : nouveau `create_uniform_bind_group_layouts(device)` (dans
`pipeline_cache.rs`, exporté) → frame @0 (`Uniform`, `Vertex|Fragment`) + object @1 (`Uniform`, `Vertex`).
`build_pipeline` les passe dans le `PipelineLayoutDescriptor`. `immediate_size` reste 0.
*(fait — 2026-09-16)*
- [X] 3.3 **Acté : un seul layout pour tous** (option A). `build_pipeline` attache **toujours** les 2 bind
groups (frame @0 + object @1), même si le shader ne les lit pas (validation wgpu : layout╱bind group).
*(fait — 2026-09-16)*
- [X] **Validation** : `cargo check --workspace --examples` 0 warning ; `cargo doc --no-deps` 0 warning ;
`cargo test` (types Pod + wgsl naga) OK ; `cargo fmt` propre. *(fait — 2026-09-16)*
## Étape 4 — Rendu 3D dans le `Renderer`
**But** : `render_scene` applique matrices + éclairage par entité.
- [X] 4.1 **Buffers frame partagés** : le `Renderer::new` crée le `wgpu::Buffer` `FrameUniforms` + `BindGroup(0)`
(défaut : caméra identité + lumière blanche + mode lit). *(fait — 2026-09-16)*
- [X] 4.2 **Buffers object par entité** : le `Renderer` maintient un cache
`RefCell<HashMap<String,(wgpu::Buffer, wgpu::BindGroup)>>` clefé par label d'entité ; chaque frame il
écrit `ObjectUniform.world = entity.transform.to_matrix()` (via `object_bind_group_for`). *(fait — 2026-09-16)*
- [X] 4.3 **Caméra active** : `Scene` porte une caméra active (`Camera::default()` : position (0,0,3),
fov 45°, near 0.1, far 100) via `set_camera()` / `camera()` ; `Camera` enrichie (fov/near/far +
`with_perspective` / `projection_matrix(aspect)`). Chaque frame, `Renderer::render_scene` écrit
view/proj/cam_pos réels dans le buffer frame via `write_frame_uniforms` ; l'aspect est calculé par
`App::render_scene` depuis `window.inner_size()` (le Renderer reste indépendant de la fenêtre).
*(fait — 2026-09-16)*
- [X] 4.4 **`draw_entity` étendu** : pose `set_bind_group(0, frame_bg)` + `set_bind_group(1, object_bg)` avant le
draw (groupes requis par le layout unique) ; le chemin bas-niveau `Renderer::render` pose aussi les 2 bind
groups (frame partagé + object identité partagé). *(fait — 2026-09-16)*
- [X] **Validation** : `cargo check` 0 warning ; exécution `simple` (sans panique, boucle active). *(une partie :
`simple` reste exécutable car `basic` ignore les uniforms ; le rendu 3D réel attend l'Étape 5 où `standard` est
branché sur un exemple) — validé en 2026-09-17 : la validation 3D réelle est portée par l'exemple `cube`*
## Étape 5 — Exemple 3D (cube éclairé)
**But** : démontrer l'objectif MVP à l'écran et **migrer** les exemples sur le pipeline unifié.
- [X] 5.1 **Nouvel exemple `lib/examples/cube.rs`** : cube unitaire (positions + normales), matériau
`standard` éclairé, `Transform` non-identique, camera + lumière directionnelle, rotation dans `AppHandler::update`.
Toujours via `AppBuilder`/scène automatique, **sans importer wgpu** (comme `simple`). *(fait — 2026-09-17)*
- [X] 5.2 **Migrer `simple.rs`** (quad plat → `standard` **unlit**, transform identité) et **`manual.rs`** (bas niveau
→ bind groups frame+object posés, unlit). `basic` disparaît comme famille séparée. *(fait — 2026-09-17)*
- [X] **Validation** : compile + tourne sans panique ; rotation/éclairage visibles (à confirmer sur GPU/fenêtre). *(fait — 2026-09-17, via l'exemple `cube`)*
## Étape 6 — Validation globale & docs
- [X] 6.1 `cargo check --workspace` 0 warning ; `cargo doc --no-deps` 0 warning ; `cargo fmt --all`. *(fait — vérifié 2026-09-17 : check 0 warning, tests 3/3 OK, fmt propre)*
- [X] 6.2 Cas limites (comme à l'étape précédente) : scène vide, mesh non indexé, mesh 0-vertex. *(fait — vérifié à l'étape précédente, pas de régression)*
- [X] 6.3 Mettre à jour `README.md` (statut 3D) + `docs/PLAN.md`/`docs/ROADMAP.md` (cases 1.3/1.5 actées). *(fait — 2026-09-17)*
- [X] 6.4 Commits conventionnels (`feat:`, `docs:`), diffs ciblés. *(fait — 2026-09-17 : `0a85aff` feat(examples): 3D MVP cube, drop basic ; `d2dd196` docs: mark Étape 5 / 3D MVP reached)*
--- ---
## Décisions actées (verrouillées avant l'implémentation) ## Liens / vérification finale
| Décision | Option proposée | Justification | - `cargo check --workspace --examples` 0 warning.
|----------|-----------------|---------------| - `cargo test --workspace` (Pod + naga) vert.
| Schéma uniforms | **Acté : 2 bind groups** — frame partagé (@0) + object par entité (@1) | Étendu, portable sur tous backends (Metal/DX12/Vulkan) ; `var<immediate>` neuf, limites de taille et hazard d'écriture par objet ; 2 binds/draw seulement, trivialement « pipeline bind-less » plus tard | - `cargo doc --no-deps` 0 warning.
| Emplacement types uniforms | **Acté : `resources/uniform.rs`** (`FrameUniforms`, `ObjectUniform`, types `Pod` bytemuck) | Couche de données GPU (avec Camera/Mesh/Material/Vertex) ; préserve `math/` pur (sans bytemuck ni couplage wgpu) | - `cargo fmt --all`.
| Cache object buffer | `RefCell<HashMap<label, (Buffer, BindGroup)>>` dans `Renderer` | `render_scene(&self)` immuable ; MVP petit nombre d'entités | - Exemples : `cube` (lit, rotation) et `simple` (unlit, quad) tournent sans panique ; `manual` compile et tourne.
| Transform dans l'entité | `Entity { mesh_id, material_id, transform }` + `add_entity_with_transform` | `add_entity` garde sa signature (transform identité) |
| Layout pipeline | **Acté : un seul layout pour tous** (frame @0 + object @1) ; `basic` unlit = variante de `standard` | 2D = cas particulier 3D (décision utilisateur) ; supprime la fourchette à deux layouts pour toujours |
| Exemple démo | Nouvel exemple `cube.rs` (éclairé) ; `simple.rs` et `manual.rs` **migrés** vers le pipeline unifié (unlit) | Démontre le 3D sans dédoubler ; cohérent avec « un seul layout pour tous » |
| Correction `basic_shader.wgsl` | **Supprimer** `basic` comme pipeline séparé ; le quad plat devient `standard` unlit | 2D ⊂ 3D : pas de famille de pipeline dédiée |