163 lines
13 KiB
Markdown
163 lines
13 KiB
Markdown
# DRAFT — Plan d'implémentation : « 3D + éclairage Phong »
|
||
|
||
> **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
|
||
> 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**,
|
||
> rendu automatiquement par la boucle `App` (Scene auto-render déjà en place).
|
||
>
|
||
> **État de départ vérifié.**
|
||
> - Rendu automatique fonctionnel mais **plat** : `basic_shader.wgsl` pose les positions telles quelles
|
||
> (`vec4(position, 1.0)`), aucune matrice, aucune uniform, aucun éclairage.
|
||
> - `Renderer::render_scene` parcourt `iter_entities()` en une passe (`&self`, `&Scene`), sans transform.
|
||
> - `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`) ;
|
||
> `Transform`/`Geometry` exportés via `math`.
|
||
> - `PipelineCache::build_pipeline` : `bind_group_layouts: &[]`, `immediate_size: 0` — aucun binding.
|
||
> - Défaut latente : `basic_shader.wgsl` déclare `@location(1) uv`, `(2) color` alors que le
|
||
> `VertexBufferLayout` réel expose `(1) normal`, `(2) uv`, `(3) color`.
|
||
|
||
---
|
||
|
||
## Point d'étape — 2026-09-16 (fin de session, reprise sur autre machine)
|
||
|
||
**État : infra posée, MVP 3D pas encore atteint.** Dernier commit : `f10e249`
|
||
(`feat(renderer): active camera wired to frame uniforms (Étape 4.3)`), dépôt propre.
|
||
|
||
**Fait et committé (bases pour la reprise)**
|
||
- É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.
|
||
- `standard_shader.wgsl` validé par naga (test permanent) mais **pas encore branché** sur un pipeline d'exemple.
|
||
- `basic` ignore encore les uniforms → les exemples `simple`/`manual` tournent mais le rendu reste plat.
|
||
|
||
**Réalisé (2026-09-17) — cette étape est terminée.** MVP 3D atteint : le cube éclairé tourne à l'écran.
|
||
- **É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.
|
||
|
||
---
|
||
|
||
## Étape 1 — Fondations data : Transform + Camera exposées
|
||
|
||
**But** : donner à chaque entité un `Transform` et rendre `Camera` utilisable via l'API publique, **sans**
|
||
toucher au rendu (pure façade de données, validable par compilation).
|
||
|
||
- [X] 1.1 **Exporter `Camera`** : dans `lib/src/resources/mod.rs`, ajouter
|
||
`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`
|
||
|
||
**But** : produire un rendu 3D éclairé via un nouveau shader, sans encore le brancher.
|
||
|
||
- [X] 2.1 **Créer `lib/src/shaders/standard_shader.wgsl`** avec le **contrat vertex correct** :
|
||
`@location(0) position : vec3`, `(1) normal : vec3`, `(2) uv : vec2`, `(3) color : vec4`.
|
||
- `@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)
|
||
- `@group(1) @binding(0)` : `ObjectUniform { model: mat4 }`
|
||
- `vs_main` : `clip_position = proj * view * model * vec4(position,1)` ; passe `normal`/`color` en espace monde.
|
||
- `fs_main` : éclairage hémisphérique (ambient) + diffuse directionnel (max(dot(N,L),0)), sortie `vec4(color*light, 1)`.
|
||
- **Mode unlit** : `options.x != 0` neutralise la directionnelle → couleur plate. Ainsi « 2D » = `standard`
|
||
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)
|
||
|
||
| Décision | Option proposée | Justification |
|
||
|----------|-----------------|---------------|
|
||
| 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 |
|
||
| 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) |
|
||
| Cache object buffer | `RefCell<HashMap<label, (Buffer, BindGroup)>>` dans `Renderer` | `render_scene(&self)` immuable ; MVP petit nombre d'entités |
|
||
| 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 |
|