Compare commits

..

2 Commits

Author SHA1 Message Date
Jérôme Bousquié d2dd1967cd docs: mark Étape 5 / 3D MVP reached (PLAN, ROADMAP, DRAFT, README) 2026-09-17 10:40:43 +02:00
Jérôme Bousquié 0a85aff17b feat(examples): 3D MVP cube via standard shader, drop basic (Étape 5) 2026-09-17 10:40:43 +02:00
19 changed files with 273 additions and 158 deletions
+14 -10
View File
@@ -2,7 +2,7 @@
WSG is a Rust library that wraps [wgpu](https://github.com/gfx-rs/wgpu) and [winit](https://crates.io/crates/winit) for simple GPU drawing. It groups the five core wgpu objects (Instance, Surface, Adapter, Device, Queue) behind a single `Context`, adds small building blocks (`Mesh`, `Material`, `PipelineCache`, `Frame`), and exposes the low-level primitives for advanced users. WSG is a Rust library that wraps [wgpu](https://github.com/gfx-rs/wgpu) and [winit](https://crates.io/crates/winit) for simple GPU drawing. It groups the five core wgpu objects (Instance, Surface, Adapter, Device, Queue) behind a single `Context`, adds small building blocks (`Mesh`, `Material`, `PipelineCache`, `Frame`), and exposes the low-level primitives for advanced users.
> **Status: unstable development version.** The manual workflow below is fully working, and the high-level "declarative" workflow (automatic `App` scene rendering) works for flat/NDC drawing. The GPU-driven two-pass pipeline described in the architecture docs is **not implemented yet** — see [Status](#status) and [Roadmap](#roadmap). > **Status: unstable development version.** The manual workflow below is fully working, the high-level "declarative" workflow (automatic `App` scene rendering) works for flat/NDC drawing, and the **3D MVP is reached** : the `cube` example (Étape 5) renders a rotating, Phong-lit cube through `App::render_scene`. The GPU-driven two-pass pipeline described in the architecture docs is **not implemented yet** — see [Status](#status) and [Roadmap](#roadmap).
## Status ## Status
@@ -12,9 +12,9 @@ WSG is a Rust library that wraps [wgpu](https://github.com/gfx-rs/wgpu) and [win
| `App` / `AppBuilder` / `AppHandler` event-loop facade | ✅ Working — window, events, frame presentation, and **automatic scene rendering** (the per-frame view is exposed via `Frame::view()`) | | `App` / `AppBuilder` / `AppHandler` event-loop facade | ✅ Working — window, events, frame presentation, and **automatic scene rendering** (the per-frame view is exposed via `Frame::view()`) |
| `Scene` resource/entity registry | ✅ Working — the engine renders every registered entity automatically in one batched render pass (`App::render_scene`) | | `Scene` resource/entity registry | ✅ Working — the engine renders every registered entity automatically in one batched render pass (`App::render_scene`) |
| GPU-driven two-pass pipeline (Compute → indirect draw) | 📋 Roadmap — spec in [docs/tech/ARCHI_CPU_GPU.md](docs/tech/ARCHI_CPU_GPU.md) | | GPU-driven two-pass pipeline (Compute → indirect draw) | 📋 Roadmap — spec in [docs/tech/ARCHI_CPU_GPU.md](docs/tech/ARCHI_CPU_GPU.md) |
| 3D infrastructure (uniform bind groups, MVP + camera in the pipeline) | ✅ Working at the engine level — the `Renderer` uploads per-frame camera matrices (active `Camera`) and per-entity world matrices to shared uniform buffers every frame; the bundled `basic` shader still ignores them, so visible 3D awaits wiring `standard_shader.wgsl` to an example | | 3D infrastructure (uniform bind groups, MVP + camera in the pipeline) | ✅ Working — the `Renderer` uploads per-frame camera matrices (active `Camera`) and per-entity world matrices to shared uniform buffers every frame; the **MVP is reached** (Étape 5) : the `cube` example renders a rotating Phong-lit cube via the `standard` shader |
Note: the bundled `basic_shader.wgsl` treats vertex positions as already in NDC space, so what you can see today is flat, untransformed drawing (e.g. a colored quad) — not a 3D scene. The Phong-lit `standard_shader.wgsl` exists and validates, and the uniform plumbing (bind groups + per-frame camera + per-entity world matrices) is in place, but it is not yet bound to a visible example. Note: the `standard_shader.wgsl` (Phong, with an explicit **unlit** mode) is now the **single** shader the library ships. The old `basic_shader.wgsl` was removed as a separate pipeline family (Étape 5) : flat 2D drawing is the unlit variant of `standard` (`Renderer::set_unlit(true)` or `app.renderer_mut().set_unlit(true)`, DRAFT « 2D ⊂ 3D »). See the `cube` example (3D, lit) and the `simple` example (2D, unlit).
## What it does ## What it does
@@ -39,12 +39,14 @@ fn main() {
let format = context.configure(&context.adapter, 800, 600).expect("surface config failed"); let format = context.configure(&context.adapter, 800, 600).expect("surface config failed");
// Renderer + shader cache (falls back to the embedded shader if the file is missing) // Renderer + shader cache (falls back to the embedded shader if the file is missing)
let renderer = Renderer::new(&context, format); // `set_unlit(true)` selects flat 2D rendering (the quad below is drawn in NDC space, unlit).
let mut renderer = Renderer::new(&context, format);
renderer.set_unlit(true);
let mut cache = PipelineCache::new(Arc::new(context.device.clone())); let mut cache = PipelineCache::new(Arc::new(context.device.clone()));
cache.register_shader("basic", utils::BASIC_SHADER_PATH).unwrap(); cache.register_shader("standard", utils::STANDARD_SHADER_PATH).unwrap();
// Material + mesh // Material + mesh
let material = Material::new(renderer.format(), "basic", &mut cache); let material = Material::new(renderer.format(), "standard", &mut cache);
let vertices: [Vertex; 4] = [ let vertices: [Vertex; 4] = [
Vertex { position: [-0.5, 0.5, 0.0], normal: [0.0, 0.0, 1.0], uv: [0.0, 0.0], color: [1.0, 0.0, 0.0, 1.0] }, Vertex { position: [-0.5, 0.5, 0.0], normal: [0.0, 0.0, 1.0], uv: [0.0, 0.0], color: [1.0, 0.0, 0.0, 1.0] },
Vertex { position: [ 0.5, 0.5, 0.0], normal: [0.0, 0.0, 1.0], uv: [1.0, 0.0], color: [0.0, 1.0, 0.0, 1.0] }, Vertex { position: [ 0.5, 0.5, 0.0], normal: [0.0, 0.0, 1.0], uv: [1.0, 0.0], color: [0.0, 1.0, 0.0, 1.0] },
@@ -92,9 +94,10 @@ async fn main() -> Result<(), wsg_lib::utils::WsgError> {
let app = AppBuilder::new().build().await?; let app = AppBuilder::new().build().await?;
// Register your scene once (string IDs), then App renders it automatically each frame: // Register your scene once (string IDs), then App renders it automatically each frame:
// app.cache.register_shader("basic", wsg_lib::utils::BASIC_SHADER_PATH)?; // app.renderer_mut().set_unlit(true); // select flat 2D rendering (optional)
// app.cache.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)?;
// app.scene.add_mesh("quad", Arc::new(mesh))?; // app.scene.add_mesh("quad", Arc::new(mesh))?;
// app.scene.add_material("mat", Arc::new(Material::new(app.renderer.format(), "basic", &mut app.cache)))?; // app.scene.add_material("mat", Arc::new(Material::new(app.renderer.format(), "standard", &mut app.cache)))?;
// app.scene.add_entity("my_quad", "quad", "mat")?; // app.scene.add_entity("my_quad", "quad", "mat")?;
app.run(MyGame) app.run(MyGame)
@@ -140,10 +143,11 @@ pollster = "0.4" # only if you use the async AppBuilder
| Action | Command | | Action | Command |
|--------|---------| |--------|---------|
| Build everything | `cargo build --workspace` | | Build everything | `cargo build --workspace` |
| Run the 3D MVP example | `cargo run -p wsg-lib --example cube` |
| Run the working example | `cargo run -p wsg-lib --example manual` | | Run the working example | `cargo run -p wsg-lib --example manual` |
| Check everything (incl. examples) | `cargo check --all-targets` | | Check everything (incl. examples) | `cargo check --all-targets` |
The `manual` example is the reference for the low-level workflow. The `simple` example (App facade) registers a colored quad and renders it automatically through the declarative path — it draws a scene without importing wgpu. The `manual` example is the reference for the low-level workflow. The `simple` example (App facade) registers a colored quad and renders it automatically through the declarative path — it draws a scene without importing wgpu. The `cube` example (Étape 5) demonstrates the 3D MVP: a rotating Phong-lit cube, also through the declarative path and without importing wgpu.
## Documentation ## Documentation
@@ -160,6 +164,6 @@ The architecture docs live in `docs/tech/` and are written in **French**. Each d
1. ✅ **Scene auto-rendering** — `App::render_scene` iterates registered entities and draws them in one encoder/submit per frame; the frame view is exposed to `AppHandler::render` for custom draws. (Done 2026-09-16.) 1. ✅ **Scene auto-rendering** — `App::render_scene` iterates registered entities and draws them in one encoder/submit per frame; the frame view is exposed to `AppHandler::render` for custom draws. (Done 2026-09-16.)
2. **GPU-driven two-pass pipeline** — Compute Pass (world matrices + frustum culling) filling an indirect draw buffer, single `draw_indexed_indirect` (see ARCHI_CPU_GPU). 2. **GPU-driven two-pass pipeline** — Compute Pass (world matrices + frustum culling) filling an indirect draw buffer, single `draw_indexed_indirect` (see ARCHI_CPU_GPU).
3. **CPU→GPU transform sync** — persistent transform buffers with ring (triple) buffering. 3. **CPU→GPU transform sync** — persistent transform buffers with ring (triple) buffering.
4. **Real 3D pipeline** — MVP uniforms + camera support in the vertex shader. *(Engine-side plumbing done 2026-09-16: uniform bind groups, per-frame active camera matrices, per-entity world matrices; visible 3D awaits wiring `standard_shader.wgsl` to an example — Étape 5.)* 4. ✅ **Real 3D pipeline (MVP atteint)** — MVP uniforms + camera support in the vertex shader. *(Engine plumbing done 2026-09-16 ; Étape 5, 2026-09-17 : `standard` branché sur l'exemple `cube` — un cube unitaire éclairé (Phong) qui tourne, rendu automatiquement par `App::render_scene`. Retrait de `basic` : le 2D plat = variante unlit de `standard` via `Renderer::set_unlit`.)*
5. **Typed resource handles** — keep String IDs for the MVP (current design, source of truth in `Scene`); slotmap-based generational handles (`ARCHI_ARENES.md`) are deferred to a later performance pass. 5. **Typed resource handles** — keep String IDs for the MVP (current design, source of truth in `Scene`); slotmap-based generational handles (`ARCHI_ARENES.md`) are deferred to a later performance pass.
6. **Error unification** — replace `Result<_, String>` in `Scene`/`PipelineCache` with typed errors. 6. **Error unification** — replace `Result<_, String>` in `Scene`/`PipelineCache` with typed errors.
+20 -17
View File
@@ -31,12 +31,15 @@
- `standard_shader.wgsl` validé par naga (test permanent) mais **pas encore branché** sur un pipeline d'exemple. - `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. - `basic` ignore encore les uniforms → les exemples `simple`/`manual` tournent mais le rendu reste plat.
**Prochaine session — reprendre à (dans l'ordre)** **Réalisé (2026-09-17) — cette étape est terminée.** MVP 3D atteint : le cube éclairé tourne à l'écran.
1. **Étape 5** : créer `lib/examples/cube.rs` (cube unitaire + `standard` éclairé + rotation, via `AppBuilder` sans wgpu), puis migrer `simple`/`manual` (2.3 + 5.2 : `basic` → variante unlit de `standard`). - **Étape 5 (5.1 + 5.2)** : nouvel exemple `lib/examples/cube.rs` (cube unitaire + normales, matériau `standard`
2. **Validation réelle** Étape 4/5 : exécuter les exemples, confirmer rotation/éclairage sur GPU/fenêtre. éclairé, camera par défaut + lumière directionnelle, rotation dans `AppHandler::update`, via `AppBuilder` sans
3. **Étape 6** : cas limites + update `README.md`/`PLAN.md`/`ROADMAP.md` (cases 1.3/1.5/2.3) + commits. 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, Étape 5 et Étape 6 restent **non cochées** ci-dessous = état exact. > Les cases 2.3, Étape 4-validation et Étape 5 (5.1/5.2/validation) sont désormais **cochées** ci-dessous = état exact.
--- ---
@@ -76,10 +79,10 @@ toucher au rendu (pure façade de données, validable par compilation).
- [X] 2.2 **Constantes** : ajouter `STANDARD_SHADER_PATH = "assets/shaders/standard_shader.wgsl"` et - [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`. `STANDARD_SHADER: &str = include_str!("../shaders/standard_shader.wgsl")` dans `lib/src/utils/conf.rs`.
*(fait — 2026-09-16)* *(fait — 2026-09-16)*
- [ ] 2.3 **Migrer `basic` vers le mode unlit de `standard`** (défaut latente réglée) : plus de pipeline au - [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 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`. layout uniformisé**. Le fallback embarqué (`BASIC_SHADER`) devient la variante unlit de `standard`.
*(bloqué : dépend des bind groups du `PipelineCache`, infra de l'Étape 3)* *(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` - [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 (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` supporté (construction de la sous-matrice 3×3 explicite). `cargo test` + `cargo check --workspace --examples`
@@ -122,26 +125,26 @@ toucher au rendu (pure façade de données, validable par compilation).
- [X] 4.4 **`draw_entity` étendu** : pose `set_bind_group(0, frame_bg)` + `set_bind_group(1, object_bg)` avant le - [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 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)* groups (frame partagé + object identité partagé). *(fait — 2026-09-16)*
- [ ] **Validation** : `cargo check` 0 warning ; exécution `simple` (sans panique, boucle active). *(une partie : - [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 `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)* 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é) ## Étape 5 — Exemple 3D (cube éclairé)
**But** : démontrer l'objectif MVP à l'écran et **migrer** les exemples sur le pipeline unifié. **But** : démontrer l'objectif MVP à l'écran et **migrer** les exemples sur le pipeline unifié.
- [ ] 5.1 **Nouvel exemple `lib/examples/cube.rs`** : cube unitaire (positions + normales), matériau - [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`. `standard` éclairé, `Transform` non-identique, camera + lumière directionnelle, rotation dans `AppHandler::update`.
Toujours via `AppBuilder`/scène automatique, **sans importer wgpu** (comme `simple`). Toujours via `AppBuilder`/scène automatique, **sans importer wgpu** (comme `simple`). *(fait — 2026-09-17)*
- [ ] 5.2 **Migrer `simple.rs`** (quad plat → `standard` **unlit**, transform identité) et **`manual.rs`** (bas niveau - [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. → bind groups frame+object posés, unlit). `basic` disparaît comme famille séparée. *(fait — 2026-09-17)*
- [ ] **Validation** : compile + tourne sans panique ; rotation/éclairage visibles (à confirmer sur GPU/fenêtre). - [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 ## Étape 6 — Validation globale & docs
- [ ] 6.1 `cargo check --workspace` 0 warning ; `cargo doc --no-deps` 0 warning ; `cargo fmt --all`. - [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)*
- [ ] 6.2 Cas limites (comme à l'étape précédente) : scène vide, mesh non indexé, mesh 0-vertex. - [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)*
- [ ] 6.3 Mettre à jour `README.md` (statut 3D) + `docs/PLAN.md`/`docs/ROADMAP.md` (cases 1.3/1.5 actées). - [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)*
- [ ] 6.4 Commits conventionnels (`feat:`, `docs:`), diffs ciblés. - [ ] 6.4 Commits conventionnels (`feat:`, `docs:`), diffs ciblés.
--- ---
+6 -4
View File
@@ -11,15 +11,17 @@ generated: { by: human:jerome, at: 2026-07-31T00:00:00Z }
Ce plan définit les étapes prioritaires pour finaliser l'architecture actuelle. L'objectif est de rendre l'API intuitive pour l'utilisateur standard tout en conservant la puissance de contrôle pour l'utilisateur avancé. Ce plan définit les étapes prioritaires pour finaliser l'architecture actuelle. L'objectif est de rendre l'API intuitive pour l'utilisateur standard tout en conservant la puissance de contrôle pour l'utilisateur avancé.
> **Statut réel (à jour au 2026-09-16).** Ce plan couvre la phase de *consolidation* passée ; la source > **Statut réel (à jour au 2026-09-17).** Ce plan couvre la phase de *consolidation* passée ; la source
> de vérité sur l'état actuel est **README.md** et le code. Depuis la révision du 2026-09-14, l'étape > de vérité sur l'état actuel est **README.md** et le code. Depuis la révision du 2026-09-14, l'étape
> **« Scene auto-render »** a été réalisée : le rendu de la `Scene` est **automatisé** en une seule > **« Scene auto-render »** a été réalisée : le rendu de la `Scene` est **automatisé** en une seule
> passe groupée via `App::render_scene(frame.view())` (appelée par défaut dans `AppHandler::render`), > passe groupée via `App::render_scene(frame.view())` (appelée par défaut dans `AppHandler::render`),
> et `simple.rs` (API `AppBuilder`, sans `winit`/`wgpu`) déclare un quad rendu automatiquement. > et `simple.rs` (API `AppBuilder`, sans `winit`/`wgpu`) déclare un quad rendu automatiquement.
> Le même jour (Étape 3 + 4, 2026-09-16) l'**infrastructure 3D** est en place : bind groups uniformes > Le même jour (Étape 3 + 4, 2026-09-16) l'**infrastructure 3D** est en place : bind groups uniformes
> partagés (frame + object), caméra active dans la `Scene` (`Scene::set_camera`/`camera()`) écrite dans > partagés (frame + object), caméra active dans la `Scene` (`Scene::set_camera`/`camera()`) écrite dans
> le buffer frame chaque frame, matrices monde par entité. L'éclairage visible (`standard_shader.wgsl` > le buffer frame chaque frame, matrices monde par entité. **Étape 5 (2026-09-17) : MVP 3D atteint** —
> branché sur un exemple) reste une étape suivante. > l'exemple `cube` rend un cube unitaire éclairé (Phong) en rotation via `App::render_scene` ; le
> shader `basic` est supprimé, le 2D plat devient la **variante unlit** de `standard`
> (`Renderer::set_unlit` / `app.renderer_mut().set_unlit(true)`).
## Phase 1 : Finalisation et Nettoyage de l'Existant (Priorité Absolue) ## Phase 1 : Finalisation et Nettoyage de l'Existant (Priorité Absolue)
@@ -59,7 +61,7 @@ Une fois la plomberie encapsulée, nous devons rendre l'assemblage des objets co
### Gestion des Matériaux et Shaders ### Gestion des Matériaux et Shaders
- [ ] S'assurer que chaque Mesh possède une référence vers un Material (à l'heure actuelle le lien est porté par l'entité `(mesh_id, material_id)` de la Scene, pas par le Mesh lui-même). - [ ] S'assurer que chaque Mesh possède une référence vers un Material (à l'heure actuelle le lien est porté par l'entité `(mesh_id, material_id)` de la Scene, pas par le Mesh lui-même).
- [ ] Implémenter le comportement par défaut : si aucun matériau n'est assigné, le moteur injecte automatiquement le `basic_shader` (non implémenté). - [ ] Implémenter le comportement par défaut : si aucun matériau n'est assigné, le moteur injecte automatiquement le `standard_shader` (variante unlit) (non implémenté).
## Phase 3 : Documentation et Interface (API "User-Friendly") ## Phase 3 : Documentation et Interface (API "User-Friendly")
+15 -16
View File
@@ -24,15 +24,14 @@ generated: { by: human:jerome, at: 2026-07-31T00:00:00Z }
> initialement non branchés au pipeline — **désormais branchés** (caméra active + matrices monde écrites > 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). > chaque frame, Étape 4.3, 2026-09-16 ; voir §1.1/1.5 ci-dessous).
> **Étape suivante (prochaine itération) — « 3D + éclairage Phong » (ROADMAP 1.3 + 1.5).** > **Étape suivante (résolue 2026-09-17).** « 3D + éclairage Phong » (ROADMAP 1.3 + 1.5) est **atteinte** :
> Le rendu automatique est aujourd'hui **plat** : le `basic_shader.wgsl` interprète les positions comme > le rendu automatique n'est plus plat. L'infrastructure (Étapes 3+4, 2026-09-16) — `standard_shader.wgsl`
> déjà en NDC, sans matrice monde/vue/projection ni lumière. **Une grande partie de l'infrastructure est > Phong (matrice `projection * view * world` + lumière directionnelle), uniform buffers branchés
> déjà en place (Étapes 3+4, 2026-09-16)** : le `standard_shader.wgsl` Phong (matrice > (frame : view/proj/cam_pos + lumière ; par mesh : `world` dérivé du `Transform`), `Renderer` écrivant
> `projection * view * world` + lumière directionnelle) existe et valide ; les uniform buffers sont > chaque frame la caméra active et la matrice monde de chaque entité — est **branchée** sur l'exemple
> branchés (frame : view/proj/cam_pos + lumière ; par mesh : `world` dérivé du `Transform`) ; le `Renderer` > `cube` (Étape 5, 2026-09-17) : un cube unitaire éclairé qui tourne à l'écran via `App::render_scene`.
> écrit chaque frame la caméra active (via `Scene::set_camera`/`camera()`) et la matrice monde de chaque > Le shader `basic` est supprimé : le 2D plat devient la variante **unlit** de `standard`
> entité. **Reste à faire** pour un mesh 3D éclairé à l'écran : brancher `standard` sur un exemple et > (`Renderer::set_unlit(true)`). Objectif MVP **atteint**.
> ajouter un mesh de test (cube). Objectif MVP : **un mesh 3D éclairé à l'écran**.
--- ---
@@ -50,23 +49,23 @@ generated: { by: human:jerome, at: 2026-07-31T00:00:00Z }
- [x] Fonctions `view_matrix()` et `projection_matrix(fov, aspect, near, far)` (Étape 4.3 : `projection_matrix(aspect)` utilise fov/near/far stockés) - [x] Fonctions `view_matrix()` et `projection_matrix(fov, aspect, near, far)` (Étape 4.3 : `projection_matrix(aspect)` utilise fov/near/far stockés)
### 1.2 Geometry & Mesh ### 1.2 Geometry & Mesh
- [ ] Créer struct `Geometry` (math/geometry.rs) : - [x] Créer struct `Geometry` (math/geometry.rs) — **fait** :
- [ ] `positions: Vec<[f32; 3]>` (obligatoire) - [x] `positions: Vec<[f32; 3]>` (obligatoire)
- [ ] `indices: Option<Vec<u16>>` (optionnel) - [x] `indices: Option<Vec<u16>>` (optionnel)
- [ ] `normals: Option<Vec<[f32; 3]>>` (pour Phong) - [x] `normals: Option<Vec<[f32; 3]>>` (pour Phong) — plus `uvs: Option<Vec<[f32; 2]>>`
- [ ] Refactorer `Mesh` pour contenir : - [ ] Refactorer `Mesh` pour contenir :
- [ ] `geometry: Arc<Geometry>` - [ ] `geometry: Arc<Geometry>`
- [ ] `vertex_buffer: wgpu::Buffer` - [ ] `vertex_buffer: wgpu::Buffer`
- [ ] `index_buffer: Option<wgpu::Buffer>` - [ ] `index_buffer: Option<wgpu::Buffer>`
- [ ] `transform: Transform` (état CPU) - [ ] `transform: Transform` (état CPU)
- [ ] Ajouter un mesh de test (cube unitaire) en exemple - [x] Ajouter un mesh de test (cube unitaire) en exemple — **fait** (helper `cube_geometry` dans l'exemple `cube`, Étape 5, 2026-09-17)
### 1.3 Shader Phong Minimal ### 1.3 Shader Phong Minimal
- [x] Créer `standard_shader.wgsl` (Étape 2, 2026-09-16) : - [x] Créer `standard_shader.wgsl` (Étape 2, 2026-09-16) :
- [x] Vertex shader : projection * view * world * position - [x] Vertex shader : projection * view * world * position
- [x] Fragment shader : éclairage directionnel (+ hémisphérique) - [x] Fragment shader : éclairage directionnel (+ hémisphérique)
- [x] Uniforms : `view`, `proj`, `cam_pos`, `light_dir`, `light_color`, `options` - [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) — `standard` n'est pas encore branché sur un exemple - [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)
### 1.4 Scene avec identifiants (MVP : String IDs) ### 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] `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
@@ -78,7 +77,7 @@ generated: { by: human:jerome, at: 2026-07-31T00:00:00Z }
- [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 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] 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] `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 — **à faire** (Étape 5 : brancher `standard` sur un exemple + mesh cube) - [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`)
--- ---
+122
View File
@@ -0,0 +1,122 @@
//! Étape 5 — MVP 3D : un cube unitaire éclairé qui tourne, rendu automatiquement par la boucle `App`.
//!
//! Démonstration de l'objectif MVP du ROADMAP 1.3 + 1.5 : un mesh 3D avec éclairage Phong à l'écran.
//! On suit le workflow déclaratif (comme `simple`) : `AppBuilder` + scène automatique, **sans importer
//! wgpu**. La seule nouveauté déclarative est l'enregistrement du shader `standard` (Phong) au lieu de
//! `basic`. La caméra active par défaut (`Scene::default`, position (0,0,3), fov 45°) cadre le cube, et
//! `AppHandler::update` fait tourner l'entité via `set_entity_transform` chaque frame.
use glam::Quat;
use std::sync::Arc;
use wsg_lib::AppHandler;
use wsg_lib::app::AppBuilder;
use wsg_lib::resources::{Material, Mesh, Vertex};
use wsg_lib::utils::WsgError;
/// Handler de démonstration : fait tourner le cube dans `update`.
struct Cube {
/// Angle de rotation cumulé (radians), incrémenté à chaque frame.
angle: f32,
}
/// Génère les sommets d'un cube unitaire centré à l'origine (arête de 1), une normale par face.
/// 24 sommets (4 par face) + 36 indices ; la couleur est blanche, l'UV est laissé à zéro (inutilisé
/// par `standard` pour un matériau sans texture).
fn cube_vertices() -> Vec<Vertex> {
let s = 0.5; // demi-arête
let color = [1.0, 1.0, 1.0, 1.0];
// Chaque face : (normale sortante, 4 coins). Le culling est désactivé par défaut (PrimitiveState
// par défaut), donc l'ordre d'enroulement n'affecte pas la visibilité ; seules les normales comptent
// pour l'éclairage.
let faces: [([f32; 3], [[f32; 3]; 4]); 6] = [
(
[0.0, 0.0, 1.0],
[[-s, -s, s], [s, -s, s], [s, s, s], [-s, s, s]],
), // +Z
(
[0.0, 0.0, -1.0],
[[s, -s, -s], [-s, -s, -s], [-s, s, -s], [s, s, -s]],
), // -Z
(
[1.0, 0.0, 0.0],
[[s, -s, -s], [s, s, -s], [s, s, s], [s, -s, s]],
), // +X
(
[-1.0, 0.0, 0.0],
[[-s, -s, s], [-s, s, s], [-s, s, -s], [-s, -s, -s]],
), // -X
(
[0.0, 1.0, 0.0],
[[-s, s, -s], [s, s, -s], [s, s, s], [-s, s, s]],
), // +Y
(
[0.0, -1.0, 0.0],
[[-s, -s, s], [s, -s, s], [s, -s, -s], [-s, -s, -s]],
), // -Y
];
let mut verts = Vec::with_capacity(24);
for (normal, corners) in faces {
for corner in corners {
verts.push(Vertex {
position: corner,
normal,
uv: [0.0, 0.0],
color,
});
}
}
verts
}
/// Génère les indices d'un cube à partir de ses 24 sommets (2 triangles par face, 36 indices).
fn cube_indices() -> Vec<u16> {
let mut indices = Vec::with_capacity(36);
for face in 0..6u16 {
let b = face * 4;
indices.extend_from_slice(&[b, b + 1, b + 2, b, b + 2, b + 3]);
}
indices
}
impl AppHandler for Cube {
fn setup(&mut self, app: &mut wsg_lib::App) {
let format = app.renderer().format();
// Shader Phong `standard` (porteur des bind groups frame + object) au lieu de `basic`.
app.cache()
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
.unwrap();
let mesh = Arc::new(Mesh::new(
app.renderer().device(),
&cube_vertices(),
Some(&cube_indices()),
));
let material = Arc::new(Material::new(format, "standard", app.cache()));
app.scene.add_mesh("cube_mesh", mesh).unwrap();
app.scene.add_material("cube_material", material).unwrap();
app.scene
.add_entity("cube", "cube_mesh", "cube_material")
.unwrap();
}
fn update(&mut self, app: &mut wsg_lib::App) {
// Rotation cumulée du cube (double axe pour un mouvement plus lisible).
self.angle += 0.02;
let base = *app
.scene
.entity_transform("cube")
.expect("cube entity present");
let mut transform = base;
transform.rotation =
Quat::from_rotation_y(self.angle) * Quat::from_rotation_x(self.angle * 0.3);
app.scene.set_entity_transform("cube", transform);
}
}
#[pollster::main]
async fn main() -> Result<(), WsgError> {
let app = AppBuilder::new().title("WSG Cube").build().await?;
app.run(Cube { angle: 0.0 })
}
+9 -6
View File
@@ -1,7 +1,7 @@
//! Workflow bas-niveau : utilisation directe du `Context`, `Renderer`, `PipelineCache`, `Mesh` et //! Workflow bas-niveau : utilisation directe du `Context`, `Renderer`, `PipelineCache`, `Mesh` et
//! `Material`, contournant la façade `App`. Rendu d'un quad plat éclairé via la boucle winit 0.30 //! `Material`, contournant la façade `App`. Rendu d'un quad plat (shader `standard` **unlit**) via la
//! (`EventLoop::run_app` + `ApplicationHandler`). La fenêtre et le GPU sont créés dans `resumed()`, //! boucle winit 0.30 (`EventLoop::run_app` + `ApplicationHandler`). La fenêtre et le GPU sont créés
//! comme l'exigent winit 0.30 et la migration faite dans `app.rs`. //! dans `resumed()`, comme l'exigent winit 0.30 et la migration faite dans `app.rs`.
use std::sync::Arc; use std::sync::Arc;
use winit::application::ApplicationHandler; use winit::application::ApplicationHandler;
use winit::dpi::LogicalSize; use winit::dpi::LogicalSize;
@@ -59,13 +59,16 @@ impl ApplicationHandler for App {
let device = Arc::new(context.device.clone()); let device = Arc::new(context.device.clone());
let mut cache = PipelineCache::new(device); let mut cache = PipelineCache::new(device);
cache cache
.register_shader("basic", utils::BASIC_SHADER_PATH) .register_shader("standard", utils::STANDARD_SHADER_PATH)
.unwrap(); .unwrap();
let renderer = Renderer::new(&context, format); // Rendu 2D plat : `standard` en mode unlit (les bind groups frame+object sont posés par
// draw_entity, la matrice frame par défaut est l'identité → positions NDC inchangées).
let mut renderer = Renderer::new(&context, format);
renderer.set_unlit(true);
// 3. Material : On utilise renderer.device() et renderer.format() // 3. Material : On utilise renderer.device() et renderer.format()
let material = Material::new(renderer.format(), "basic", &mut cache); let material = Material::new(renderer.format(), "standard", &mut cache);
// Mesh : On utilise le device du renderer // Mesh : On utilise le device du renderer
let vertices = [ let vertices = [
+9 -5
View File
@@ -16,9 +16,11 @@ impl AppHandler for MonQuad {
fn setup(&mut self, app: &mut wsg_lib::App) { fn setup(&mut self, app: &mut wsg_lib::App) {
let format = app.renderer().format(); let format = app.renderer().format();
// Enregistrement du shader, création du matériau et du mesh du quad (sans importer wgpu). // Exemple 2D plat : le shader `standard` en mode **unlit** (options.x = 1) renvoie la couleur
// du vertex telle quelle. Ainsi le 2D est un cas particulier du 3D — un seul pipeline pour tous.
app.renderer_mut().set_unlit(true);
app.cache() app.cache()
.register_shader("basic", wsg_lib::utils::BASIC_SHADER_PATH) .register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
.unwrap(); .unwrap();
let vertices = [ let vertices = [
Vertex { Vertex {
@@ -53,12 +55,14 @@ impl AppHandler for MonQuad {
&vertices, &vertices,
Some(&indices), Some(&indices),
)); ));
let material = Arc::new(Material::new(format, "basic", app.cache())); let material = Arc::new(Material::new(format, "standard", app.cache()));
app.scene.add_mesh("quad_mesh", mesh).unwrap(); app.scene.add_mesh("quad_mesh", mesh).unwrap();
app.scene.add_material("basic_material", material).unwrap();
app.scene app.scene
.add_entity("quad", "quad_mesh", "basic_material") .add_material("standard_material", material)
.unwrap();
app.scene
.add_entity("quad", "quad_mesh", "standard_material")
.unwrap(); .unwrap();
} }
} }
+10
View File
@@ -72,6 +72,16 @@ impl App {
.expect("renderer not initialized yet — call app.run(handler) first") .expect("renderer not initialized yet — call app.run(handler) first")
} }
/// Returns a mutable reference to the GPU renderer.
/// Panics if called before `App::run` has created the renderer (i.e. before `resumed` fires).
/// Callers can configure the renderer here, e.g. `app.renderer_mut().set_unlit(true)` in `setup`
/// to select flat 2D rendering (DRAFT Étape 5).
pub fn renderer_mut(&mut self) -> &mut Renderer {
self.renderer
.as_mut()
.expect("renderer not initialized yet — call app.run(handler) first")
}
/// Returns a reference to the GPU hardware context. /// Returns a reference to the GPU hardware context.
/// Panics if called before `App::run` has created the context (i.e. before `resumed` fires). /// Panics if called before `App::run` has created the context (i.e. before `resumed` fires).
pub fn context(&self) -> &Context { pub fn context(&self) -> &Context {
+35 -5
View File
@@ -55,6 +55,10 @@ pub struct Renderer {
/// entity label. Needed because `render_scene(&self, &Scene)` is immutable; the model matrix is /// entity label. Needed because `render_scene(&self, &Scene)` is immutable; the model matrix is
/// rewritten each frame for every entity. /// rewritten each frame for every entity.
object_cache: RefCell<HashMap<String, (wgpu::Buffer, wgpu::BindGroup)>>, object_cache: RefCell<HashMap<String, (wgpu::Buffer, wgpu::BindGroup)>>,
/// Flat (unlit) rendering flag, exposed via [`Renderer::set_unlit`]. When true, `options.x` of the
/// `FrameUniforms` is set to 1 so the `standard` shader returns vertex colors as-is — flat 2D
/// rendering is thus a special case of the 3D lit path (DRAFT Étape 5). Defaults to `false` (lit).
unlit: bool,
} }
impl Renderer { impl Renderer {
@@ -72,14 +76,12 @@ impl Renderer {
// Shared frame uniforms: identity camera + white directional light, lit mode by default. // Shared frame uniforms: identity camera + white directional light, lit mode by default.
// Values become meaningful once an active camera is wired (Étape 4.3); for now the default // Values become meaningful once an active camera is wired (Étape 4.3); for now the default
// is a coherent scene when a shader actually reads them, and irrelevant to shaders that don't. // is a coherent scene when a shader actually reads them, and irrelevant to shaders that don't.
let default_frame = FrameUniforms::default();
let frame_buffer = device.create_buffer(&wgpu::BufferDescriptor { let frame_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("frame uniform buffer"), label: Some("frame uniform buffer"),
size: FRAME_UNIFORMS_SIZE, size: FRAME_UNIFORMS_SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false, mapped_at_creation: false,
}); });
queue.write_buffer(&frame_buffer, 0, bytemuck::bytes_of(&default_frame));
let frame_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { let frame_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("frame bind group"), label: Some("frame bind group"),
layout: &frame_layout, layout: &frame_layout,
@@ -109,7 +111,7 @@ impl Renderer {
}], }],
}); });
Self { let renderer = Self {
queue, queue,
device, device,
format, format,
@@ -118,7 +120,35 @@ impl Renderer {
frame_bind_group, frame_bind_group,
shared_object_bind_group, shared_object_bind_group,
object_cache: RefCell::new(HashMap::new()), object_cache: RefCell::new(HashMap::new()),
} unlit: false,
};
// Seed the shared frame buffer with an identity camera + current unlit flag so the low-level
// `render` path (which has no window/camera) sees coherent values before `render_scene` runs.
renderer.write_default_frame_uniforms();
renderer
}
/// Writes the shared per-frame uniform buffer using an identity camera (view = proj = identity)
/// and the current [`Renderer::set_unlit`] flag. This is the initial state for the low-level
/// `render` path, which is independent of any window and therefore has no camera or aspect ratio.
/// Called at construction and whenever the renderer transitions between lit and unlit mode.
fn write_default_frame_uniforms(&self) {
let frame = FrameUniforms {
options: [if self.unlit { 1 } else { 0 }, 0, 0, 0],
..FrameUniforms::default()
};
self.queue
.write_buffer(&self.frame_buffer, 0, bytemuck::bytes_of(&frame));
}
/// Toggles flat (unlit) rendering. When true, the `standard` shader returns vertex colors as-is
/// (`options.x = 1`), so flat 2D rendering is a special case of the 3D lit path (DRAFT Étape 5 :
/// « 2D ⊂ 3D »). Rewrites the shared frame buffer immediately so the low-level `render` path picks
/// up the change ; the `render_scene` path reads the flag each frame in `write_frame_uniforms`.
/// Inputs: unlit — true for flat rendering, false (default) for Phong-lit rendering.
pub fn set_unlit(&mut self, unlit: bool) {
self.unlit = unlit;
self.write_default_frame_uniforms();
} }
/// Rewrites the shared per-frame uniform buffer from the scene's active camera and the current /// Rewrites the shared per-frame uniform buffer from the scene's active camera and the current
@@ -135,7 +165,7 @@ impl Renderer {
cam_pos: camera.position.extend(1.0), cam_pos: camera.position.extend(1.0),
light_dir: Vec4::new(0.0, 0.0, 1.0, 0.0), light_dir: Vec4::new(0.0, 0.0, 1.0, 0.0),
light_color: Vec4::ONE, light_color: Vec4::ONE,
options: [0, 0, 0, 0], options: [if self.unlit { 1 } else { 0 }, 0, 0, 0],
}; };
self.queue self.queue
.write_buffer(&self.frame_buffer, 0, bytemuck::bytes_of(&frame)); .write_buffer(&self.frame_buffer, 0, bytemuck::bytes_of(&frame));
+1 -1
View File
@@ -22,7 +22,7 @@
//! ```ignore //! ```ignore
//! use wsg_lib::core::{Context, Renderer}; //! use wsg_lib::core::{Context, Renderer};
//! use wsg_lib::resources::{Mesh, Material, Vertex}; //! use wsg_lib::resources::{Mesh, Material, Vertex};
//! use wsg_lib::utils::BASIC_SHADER; //! use wsg_lib::utils::STANDARD_SHADER;
//! ``` //! ```
// Warn if a public API item has no rustdoc comment, keeping API coverage at 100%. // Warn if a public API item has no rustdoc comment, keeping API coverage at 100%.
+2 -2
View File
@@ -6,11 +6,11 @@ The `pipeline` module contains the shader compilation cache that avoids duplicat
| File | Responsibility | | File | Responsibility |
|------|---------------| |------|---------------|
| **pipeline_cache** | PipelineCache — maps (shader_id, format) keys to compiled RenderPipelines. Loads WGSL from disk or falls back to embedded BASIC_SHADER constant. Creates pipelines on-demand via build_pipeline(). | | **pipeline_cache** | PipelineCache — maps (shader_id, format) keys to compiled RenderPipelines. Loads WGSL from disk or falls back to embedded STANDARD_SHADER constant. Creates pipelines on-demand via build_pipeline(). |
## Interaction with Other Modules ## Interaction with Other Modules
- **utils::conf**: Provides BASIC_SHADER_PATH (disk path) and BASIC_SHADER (embedded fallback). - **utils::conf**: Provides STANDARD_SHADER_PATH (disk path) and STANDARD_SHADER (embedded fallback).
- **resources::vertex**: Vertex struct field offsets define the CPU-side layout that build_pipeline() uses as the vertex buffer contract. - **resources::vertex**: Vertex struct field offsets define the CPU-side layout that build_pipeline() uses as the vertex buffer contract.
- **resources::material**: Material::new() calls get_or_create() during construction to obtain a shared RenderPipeline. - **resources::material**: Material::new() calls get_or_create() during construction to obtain a shared RenderPipeline.
+1 -1
View File
@@ -6,7 +6,7 @@
//! //!
//! ## Interaction with Other Modules //! ## Interaction with Other Modules
//! - `material` calls `get_or_create()` during its own construction to obtain a shared RenderPipeline. //! - `material` calls `get_or_create()` during its own construction to obtain a shared RenderPipeline.
//! - `conf::BASIC_SHADER` provides fallback WGSL source when an external file is not found. //! - `conf::STANDARD_SHADER` provides fallback WGSL source when an external file is not found.
//! - `vertex::Vertex` defines the CPU-side layout that `build_pipeline()` uses as the vertex buffer contract. //! - `vertex::Vertex` defines the CPU-side layout that `build_pipeline()` uses as the vertex buffer contract.
pub mod pipeline_cache; pub mod pipeline_cache;
+5 -5
View File
@@ -7,7 +7,7 @@
//! //!
//! ## Interaction with Other Modules //! ## Interaction with Other Modules
//! - **Material** calls `get_or_create()` during its own construction to obtain a shared RenderPipeline. //! - **Material** calls `get_or_create()` during its own construction to obtain a shared RenderPipeline.
//! - **conf::BASIC_SHADER** provides fallback WGSL source when an external file is not found. //! - **conf::STANDARD_SHADER** provides fallback WGSL source when an external file is not found.
//! - **vertex::Vertex** defines the CPU-side layout that `build_pipeline` uses as the vertex buffer contract. //! - **vertex::Vertex** defines the CPU-side layout that `build_pipeline` uses as the vertex buffer contract.
//! //!
//! ## Technical Points //! ## Technical Points
@@ -16,7 +16,7 @@
//! - **Batching**: Multiple Materials with the same shader_id share one pipeline, enabling material-level batching in Renderer. //! - **Batching**: Multiple Materials with the same shader_id share one pipeline, enabling material-level batching in Renderer.
use crate::resources::Vertex; use crate::resources::Vertex;
use crate::utils::BASIC_SHADER; use crate::utils::STANDARD_SHADER;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
@@ -79,7 +79,7 @@ impl PipelineCache {
device, device,
pipelines: HashMap::new(), pipelines: HashMap::new(),
// Maps shader IDs to file paths on disk for WGSL loading in load_shader(). // Maps shader IDs to file paths on disk for WGSL loading in load_shader().
// When a path exists, it reads from it; otherwise falls back to BASIC_SHADER constant. // When a path exists, it reads from it; otherwise falls back to STANDARD_SHADER constant.
shader_paths: HashMap::new(), shader_paths: HashMap::new(),
} }
} }
@@ -139,13 +139,13 @@ impl PipelineCache {
pipeline_arc pipeline_arc
} }
/// Loads a WGSL shader module: reads from disk first, falls back to the embedded BASIC_SHADER constant. /// Loads a WGSL shader module: reads from disk first, falls back to the embedded STANDARD_SHADER constant.
/// Inputs: device (GPU command source for shader compilation), path (file path or shader_id string). /// Inputs: device (GPU command source for shader compilation), path (file path or shader_id string).
/// Returns a compiled wgpu::ShaderModule. Called internally by `get_or_create()` when compiling a new pipeline. /// Returns a compiled wgpu::ShaderModule. Called internally by `get_or_create()` when compiling a new pipeline.
fn load_shader(&self, device: &wgpu::Device, path: &str) -> wgpu::ShaderModule { fn load_shader(&self, device: &wgpu::Device, path: &str) -> wgpu::ShaderModule {
let source = std::fs::read_to_string(path).unwrap_or_else(|_| { let source = std::fs::read_to_string(path).unwrap_or_else(|_| {
println!("Shader not found: {}, falling back to default", path); println!("Shader not found: {}, falling back to default", path);
BASIC_SHADER.to_string() STANDARD_SHADER.to_string()
}); });
device.create_shader_module(wgpu::ShaderModuleDescriptor { device.create_shader_module(wgpu::ShaderModuleDescriptor {
+1 -1
View File
@@ -14,4 +14,4 @@ The `resources` module defines three immutable data types that flow through the
- **pipeline**: build_pipeline() reads Vertex field offsets to construct VertexBufferLayout attributes array. - **pipeline**: build_pipeline() reads Vertex field offsets to construct VertexBufferLayout attributes array.
- **scene**: Scene stores Arc<Mesh> and Arc<Material> instances keyed by identifier strings. - **scene**: Scene stores Arc<Mesh> and Arc<Material> instances keyed by identifier strings.
- **utils**: Mesh creation uses BASIC_SHADER fallback when external shader files are missing. - **utils**: PipelineCache uses the embedded STANDARD_SHADER fallback when external shader files are missing.
+12 -25
View File
@@ -2,39 +2,24 @@
## Overview ## Overview
Contains WGSL shader source files used by the PipelineCache module. These are loaded at runtime from disk when referenced by their registered ID in PipelineCache.register_shader(). If a file is missing, PipelineCache falls back to the embedded BASIC_SHADER constant defined in utils::conf. Contains WGSL shader source files used by the PipelineCache module. These are loaded at runtime from
disk when referenced by their registered ID in PipelineCache.register_shader(). If a file is missing,
PipelineCache falls back to the embedded STANDARD_SHADER constant defined in utils::conf.
Depuis l'Étape 5, il n'existe plus qu'**un seul shader** : `standard_shader.wgsl` (Phong). L'ancien
`basic_shader.wgsl` a été supprimé comme pipeline séparé — le rendu 2D plat est désormais la **variante
unlit** de `standard` (décision actée dans le DRAFT : « 2D ⊂ 3D »).
## Files ## Files
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| **basic_shader.wgsl** | Legacy flat/unlit vertex/fragment shader pair (vs_main / fs_main) with position, uv, and color attributes. Scheduled to be replaced by the unlit variant of `standard_shader.wgsl` (DRAFT Étape 2.3 / 5). |
| **standard_shader.wgsl** | Standard (Phong) vertex/fragment shader — ambient + directional diffuse with an explicit unlit mode. Carries the full uniform contract (frame @group(0) + object @group(1)). | | **standard_shader.wgsl** | Standard (Phong) vertex/fragment shader — ambient + directional diffuse with an explicit unlit mode. Carries the full uniform contract (frame @group(0) + object @group(1)). |
## Shader Contract (basic_shader.wgsl)
The WGSL shader defines:
- `@vertex fn vs_main(model: VertexInput) -> VertexOutput` — vertex entry point
- `@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4<f32>` — fragment entry point writing RGBA output
### Vertex Input Layout
| Location | Attribute | Type | Offset (bytes) |
|----------|-----------|------|----------------|
| 0 | position | vec3<f32> | 0 |
| 1 | uv | vec2<f32> | 12 |
| 2 | color | vec3<f32> | 24 |
**Note**: This shader uses a 39-byte vertex stride (3+2+3 floats). It does NOT include normal data or alpha channel interpolation — it outputs fully opaque geometry with per-vertex color passthrough. This differs from the full `Vertex` struct layout (56 bytes with normal + alpha) defined in resources::Vertex; if a full shader matching the Vertex struct is needed, extend this shader accordingly.
> **Statut** : ce shader n'est plus un pipeline séparé ; il est destiné à disparaître au profit de la variante
> unlit de `standard_shader.wgsl` (DRAFT Étape 2.3 / Étape 5, « un seul layout pour tous »).
## Shader Contract (standard_shader.wgsl) ## Shader Contract (standard_shader.wgsl)
`standard_shader.wgsl` est le shader unifié (Phong) de WSG. Il corrige le défaut latente de `basic` `standard_shader.wgsl` est le shader unifié (Phong) de WSG. Il expose les deux bind groups partagés
(contrat vertex incomplet) et expose les deux bind groups partagés par tout matériau. par tout matériau (Étape 3 : un seul layout pour tous).
### Vertex Input Layout (56-byte stride — correspond au `resources::Vertex`) ### Vertex Input Layout (56-byte stride — correspond au `resources::Vertex`)
@@ -57,4 +42,6 @@ The WGSL shader defines:
### Mode unlit ### Mode unlit
Un flag `options.x != 0` neutralise la directionnelle et renvoie la couleur du vertex telle quelle Un flag `options.x != 0` neutralise la directionnelle et renvoie la couleur du vertex telle quelle
(couleur plate). Ainsi le rendu 2D plat est un **cas particulier** de la 3D éclairée. (couleur plate). Côté API, `Renderer::set_unlit(true)` (ou `app.renderer_mut().set_unlit(true)`)
positionne ce flag dans les frame uniforms. Ainsi le rendu 2D plat est un **cas particulier** de la 3D
éclairée.
-42
View File
@@ -1,42 +0,0 @@
//! # Basic Shader Module
//!
//! Default vertex/fragment shader pair used by PipelineCache when no external .wgsl file is found.
//! This shader implements a simple unlit rendering path: passes through position and color attributes
//! from VertexInput to fragment output, producing flat-colored geometry without lighting calculations.
//!
//! ## Shader Contract
//! Must define entry points matching PipelineCache::build_pipeline():
//! - @vertex fn vs_main(model: VertexInput) -> VertexOutput
//! - model.position → @location(0), vec3<f32>, offset 0 bytes in vertex buffer
//! - model.uv → @location(1), vec2<f32>, offset 12 bytes in vertex buffer
//! - model.color → @location(2), vec3<f32>, offset 24 bytes in vertex buffer
//! - @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4<f32>
//! - Writes RGBA output where alpha is hardcoded to 1.0 (fully opaque).
//!
//! ## Technical Notes
//! - No normal or UV interpolation — this is an unlit shader that directly outputs the per-vertex color.
//! - The clip_position is computed as vec4<f32>(position, 1.0), assuming position is already in NDC space.
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) uv: vec2<f32>,
@location(2) color: vec3<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) color: vec3<f32>,
};
@vertex
fn vs_main(model: VertexInput) -> VertexOutput {
var out: VertexOutput;
out.clip_position = vec4<f32>(model.position, 1.0);
out.color = model.color; // On transmet la couleur au fragment shader
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(in.color, 1.0);
}
+2 -2
View File
@@ -6,11 +6,11 @@ The `utils` module defines two leaf concepts that other modules consume but have
| File | Responsibility | | File | Responsibility |
|------|---------------| |------|---------------|
| **conf** | Shared constants for shader paths (BASIC_SHADER_PATH) and embedded WGSL source code (BASIC_SHADER). Centralized here so all submodules import from one place instead of duplicating literal strings. Enables PipelineCache to fall back to an embedded default shader when the file-based one is missing. | | **conf** | Shared constants for shader paths (STANDARD_SHADER_PATH) and embedded WGSL source code (STANDARD_SHADER). Centralized here so all submodules import from one place instead of duplicating literal strings. Enables PipelineCache to fall back to an embedded default shader when the file-based one is missing. |
| **error** | WsgError enum — application-level error type mapping specific wgpu failure modes to user-friendly messages via thiserror. Every variant maps a GPU initialization or rendering failure to a recoverable or fatal outcome. | | **error** | WsgError enum — application-level error type mapping specific wgpu failure modes to user-friendly messages via thiserror. Every variant maps a GPU initialization or rendering failure to a recoverable or fatal outcome. |
## Interaction with Other Modules ## Interaction with Other Modules
- **pipeline::pipeline_cache**: load_shader() reads BASIC_SHADER_PATH from disk; falls back to BASIC_SHADER if unreadable. - **pipeline::pipeline_cache**: load_shader() reads STANDARD_SHADER_PATH from disk; falls back to STANDARD_SHADER if unreadable.
- **core::context**: Returns WsgError variants from all fallible methods (new, configure, begin_frame). - **core::context**: Returns WsgError variants from all fallible methods (new, configure, begin_frame).
- **core::renderer**: Does not use errors directly — panics on invalid state rather than returning Result. - **core::renderer**: Does not use errors directly — panics on invalid state rather than returning Result.
+6 -13
View File
@@ -6,26 +6,19 @@
//! Also provides application defaults for window title, width, and height used by AppBuilder. //! Also provides application defaults for window title, width, and height used by AppBuilder.
//! //!
//! ## Interaction with Other Modules //! ## Interaction with Other Modules
//! - **pipeline_cache::load_shader()** reads `BASIC_SHADER_PATH` from disk; if unreadable, falls back to `BASIC_SHADER`. //! - **pipeline_cache::load_shader()** reads `STANDARD_SHADER_PATH` from disk; if unreadable, falls back to `STANDARD_SHADER`.
//! - Both constants are compile-time values (`include_str!`) ensuring the fallback shader is always available even without external files. //! - Both constants are compile-time values (`include_str!`) ensuring the fallback shader is always available even without external files.
//! - **app::AppBuilder** reads APP_DEFAULT_TITLE, APP_DEFAULT_WIDTH, and APP_DEFAULT_HEIGHT for default window configuration. //! - **app::AppBuilder** reads APP_DEFAULT_TITLE, APP_DEFAULT_WIDTH, and APP_DEFAULT_HEIGHT for default window configuration.
/// Path to the default WGSL shader file on disk (runtime). Used by PipelineCache::load_shader() for file-based loading.
pub const BASIC_SHADER_PATH: &str = "assets/shaders/basic_shader.wgsl";
/// The basic WGSL shader source code, embedded at compile time via `include_str!`.
/// Serves as a fallback when `BASIC_SHADER_PATH` cannot be read at runtime.
pub const BASIC_SHADER: &str = include_str!("../shaders/basic_shader.wgsl");
/// Path to the standard (Phong) WGSL shader file on disk (runtime). Used by PipelineCache::load_shader() /// Path to the standard (Phong) WGSL shader file on disk (runtime). Used by PipelineCache::load_shader()
/// once standardized (Étape 3) : this shader carries the full uniform contract (frame + object bind groups) /// for file-based loading. This is the unified pipeline shader (Étape 3 : un seul layout pour tous) :
/// and supports an unlit mode so flat 2D rendering is a special case of the 3D lit path. /// it carries the full uniform contract (frame + object bind groups) and supports an unlit mode so flat
/// 2D rendering is a special case of the 3D lit path. The `basic` family was removed (Étape 5).
pub const STANDARD_SHADER_PATH: &str = "assets/shaders/standard_shader.wgsl"; pub const STANDARD_SHADER_PATH: &str = "assets/shaders/standard_shader.wgsl";
/// The standard (Phong) WGSL shader source code, embedded at compile time via `include_str!`. /// The standard (Phong) WGSL shader source code, embedded at compile time via `include_str!`.
/// Not yet compiled by any pipeline (Étape 2 : shader seul, non branché). Becomes the unified /// Serves as the fallback when `STANDARD_SHADER_PATH` cannot be read at runtime. Because every
/// pipeline shader once the uniform infrastructure exists (Étape 3). The unlit variant is the /// pipeline uses the unified layout (frame @0 + object @1), this is the only shader the library ships.
/// replacement for the flat `basic` family.
pub const STANDARD_SHADER: &str = include_str!("../shaders/standard_shader.wgsl"); pub const STANDARD_SHADER: &str = include_str!("../shaders/standard_shader.wgsl");
/// Default application title displayed in the OS taskbar/window decorations. /// Default application title displayed in the OS taskbar/window decorations.
+3 -3
View File
@@ -5,7 +5,7 @@
//! Both are consumed by other modules but have no internal dependencies on them. //! Both are consumed by other modules but have no internal dependencies on them.
//! //!
//! ## Interaction with Other Modules //! ## Interaction with Other Modules
//! - `pipeline_cache` loads shaders from disk using conf::BASIC_SHADER_PATH; falls back to BASIC_SHADER. //! - `pipeline_cache` loads shaders from disk using conf::STANDARD_SHADER_PATH; falls back to STANDARD_SHADER.
//! - `context` returns WsgError variants from all fallible methods (new, configure, begin_frame). //! - `context` returns WsgError variants from all fallible methods (new, configure, begin_frame).
//! - `renderer` does not use errors directly (panics on invalid state rather than returning Result). //! - `renderer` does not use errors directly (panics on invalid state rather than returning Result).
@@ -13,6 +13,6 @@ pub mod conf;
pub mod error; pub mod error;
// Re-exports // Re-exports
pub use conf::BASIC_SHADER; pub use conf::STANDARD_SHADER;
pub use conf::BASIC_SHADER_PATH; pub use conf::STANDARD_SHADER_PATH;
pub use error::WsgError; pub use error::WsgError;