Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f10e249898 | |||
| c1e07b42b4 |
Generated
+3
@@ -488,6 +488,9 @@ name = "glam"
|
||||
version = "0.33.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f22fb22f065b308be0d8724e3706c7fa3fc2a6c7d6899df4cad7860e7a75436"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glow"
|
||||
|
||||
@@ -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()`) |
|
||||
| `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) |
|
||||
| 3D transforms (MVP uniforms, camera in the pipeline) | 📋 Roadmap — the bundled shader draws positions straight to NDC today |
|
||||
| 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 |
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## What it does
|
||||
|
||||
@@ -108,7 +108,7 @@ async fn main() -> Result<(), wsg_lib::utils::WsgError> {
|
||||
|
||||
- **Manager layer (`Context`)** — owns the GPU hardware lifecycle (Instance → Surface → Adapter → Device → Queue). Created once at startup; `configure()` sets up the swapchain, `Frame` wraps each frame's surface texture + view.
|
||||
- **Executor layer (`Renderer`)** — binds a `Material` pipeline + `Mesh` buffers into a RenderPass and submits the commands. Rendering a whole `Scene` (`render_scene`) batches all entities into **one encoder + one submit per frame**; the low-level `render` still allocates one per object.
|
||||
- **Supporting pieces** — `PipelineCache` (shader → compiled RenderPipeline, `Arc`-shared), `Material`, `Mesh`/`Vertex`, `Scene` (string-ID registry), `Camera`/`Transform` (types only, not yet used by the pipeline).
|
||||
- **Supporting pieces** — `PipelineCache` (shader → compiled RenderPipeline, `Arc`-shared), `Material`, `Mesh`/`Vertex`, `Scene` (string-ID registry), `Camera`/`Transform` (active camera wired to the frame uniforms, Étape 4.3).
|
||||
|
||||
The planned target architecture — a GPU-driven two-pass pipeline (Compute Pass: world matrices + frustum culling → Indirect Draw Buffer, then a single `draw_indexed_indirect` per frame) — is specified in [docs/tech/ARCHI_APP.md](docs/tech/ARCHI_APP.md) and [docs/tech/ARCHI_CPU_GPU.md](docs/tech/ARCHI_CPU_GPU.md) but is **not implemented yet**.
|
||||
|
||||
@@ -125,7 +125,7 @@ The planned target architecture — a GPU-driven two-pass pipeline (Compute Pass
|
||||
| Material | Struct | Shader ID → RenderPipeline | ✅ |
|
||||
| Mesh / Vertex | Struct | GPU geometry container / CPU-side vertex tuple | ✅ |
|
||||
| Frame | Struct | Per-frame RAII wrapper (surface texture + view) | ✅ |
|
||||
| Camera / Transform | Struct | Camera & transform math | 📋 Types only, not in the pipeline |
|
||||
| Camera / Transform | Struct | Camera & transform math | ✅ Active camera + transform wired to per-frame uniforms (Étape 4.3) |
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -160,6 +160,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.)
|
||||
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.
|
||||
4. **Real 3D pipeline** — MVP uniforms + camera support in the vertex shader.
|
||||
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.)*
|
||||
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.
|
||||
|
||||
+30
-23
@@ -70,34 +70,41 @@ toucher au rendu (pure façade de données, validable par compilation).
|
||||
|
||||
**But** : permettre aux pipelines de recevoir des uniforms (bind groups) au lieu de `bind_group_layouts: &[]`.
|
||||
|
||||
- [ ] 3.1 **Types bytemuck `Pod`** (nouveau `lib/src/resources/uniform.rs`, ou `math/uniform.rs`) :
|
||||
- `#[repr(C)] #[derive(Pod, Zeroable, Copy, Clone)] FrameUniforms` (voir 2.1)
|
||||
- `#[repr(C)] #[derive(...)] ObjectUniform { model: Mat4 }`
|
||||
- (alignement 16 octets : utiliser `Vec4`/tableaux pour éviter le padding). Exporter via le `mod.rs` concerné.
|
||||
- [ ] 3.2 **Bind group layouts** : dans `build_pipeline`, créer 2 `BindGroupLayout`
|
||||
(frame @0 + object @1, chacun avec un buffer uniform `Vertex`/`Fragment`/`Vertex|Fragment` selon usage) et les
|
||||
passer dans `PipelineLayoutDescriptor.bind_group_layouts`. `immediate_size` reste 0 (pas de `var<immediate>`).
|
||||
- [ ] 3.3 **Acté : un seul layout pour tous** (option A). `build_pipeline` attache **toujours** les 2 bind groups
|
||||
(frame @0 + object @1). Plus de famille `basic` au layout vide : tout matériau partage le même layout
|
||||
uniformisé. `manual`/quad plat migrent (Étape 5).
|
||||
- [ ] **Validation** : `cargo check` 0 warning ; `cargo doc` 0 warning (types documentés, `missing_docs` actif).
|
||||
- [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é.
|
||||
|
||||
- [ ] 4.1 **Buffers frame partagés** : créer le `wgpu::Buffer` `FrameUniforms` + `BindGroup(0)` dans
|
||||
`Renderer::new` (ou à la 1re frame). Écrire chaque frame : view/proj (caméra active) + lumière.
|
||||
- [ ] 4.2 **Buffers object par entité** : `Renderer` maintient un cache
|
||||
`RefCell<HashMap<String, (wgpu::Buffer, wgpu::BindGroup)>>` clefé par label d'entité (créé à la 1re rencontre),
|
||||
car `render_scene(&self, &Scene)` est immuable. Chaque frame : écrire `ObjectUniform.world = entity.transform.to_matrix()` + `set_bind_group(1, ...)`.
|
||||
- [ ] 4.3 **Caméra active** : ajouter `scene.set_active_camera(Camera)` / `scene.active_camera() -> Option<&Camera>`.
|
||||
Calcul du `proj` avec l'aspect de la fenêtre (`window.inner_size()` accessible via `App.window`).
|
||||
- [ ] 4.4 **`draw_entity` étendu** : `set_bind_group(0, frame_bg)` + `set_bind_group(1, object_bg)` avant le draw,
|
||||
pour **tout** matériau (layout unique). Le chemin bas-niveau `Renderer::render` pose aussi les 2 bind groups
|
||||
(frame partagé + object du mesh appelant).
|
||||
- [ ] **Validation** : `cargo check` 0 warning ; exécution `simple` (sans panique, boucle active) ;
|
||||
`manual` non-régressif (chemin bas-niveau).
|
||||
- [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)*
|
||||
- [ ] **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)*
|
||||
|
||||
## Étape 5 — Exemple 3D (cube éclairé)
|
||||
|
||||
|
||||
+7
-1
@@ -16,6 +16,10 @@ Ce plan définit les étapes prioritaires pour finaliser l'architecture actuelle
|
||||
> **« 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`),
|
||||
> 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
|
||||
> 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`
|
||||
> branché sur un exemple) reste une étape suivante.
|
||||
|
||||
## Phase 1 : Finalisation et Nettoyage de l'Existant (Priorité Absolue)
|
||||
|
||||
@@ -74,7 +78,9 @@ Une fois les phases 1 à 3 validées, nous pourrons introduire :
|
||||
|
||||
- [ ] **Système de Lumières** : Ajout de buffers d'uniformes dans le PipelineCache.
|
||||
- [ ] **Textures** : Intégration d'un module de chargement d'images et de BindGroups.
|
||||
- [ ] **Caméras** : Gestion des matrices de projection/vue dans la Scene.
|
||||
- [X] **Caméras** : Gestion des matrices de projection/vue dans la Scene *(fait 2026-09-16, Étape 4.3 —
|
||||
`Scene::set_camera`/`camera()` porte une caméra active ; `render_scene` écrit view/proj/cam_pos réels
|
||||
dans le buffer frame chaque frame, aspect calculé depuis la fenêtre)*.
|
||||
|
||||
## Check-list de Vérification pour le LLM d'Assistance
|
||||
|
||||
|
||||
+28
-24
@@ -20,16 +20,19 @@ generated: { by: human:jerome, at: 2026-07-31T00:00:00Z }
|
||||
> est exposée (`Frame::view()`), `render()` dessine la scène en une passe groupée
|
||||
> (`App::render_scene`) et la présentation est automatique dans `App::run` (exemple `simple`).
|
||||
> - `Scene` avec identifiants **String** (décision prise — voir tableau Notes de Décision) : 🚧 enregistrement seul.
|
||||
> - `Camera` / `Transform` et `glam` : types et mathématiques présents (`math/`, `resources/camera.rs`), non branchés au pipeline.
|
||||
> - `Camera` / `Transform` et `glam` : types et mathématiques présents (`math/`, `resources/camera.rs`),
|
||||
> initialement non branchés au pipeline — **désormais branchés** (caméra active + matrices monde écrites
|
||||
> chaque frame, Étape 4.3, 2026-09-16 ; voir §1.1/1.5 ci-dessous).
|
||||
|
||||
> **Étape suivante (prochaine itération) — « 3D + éclairage Phong » (ROADMAP 1.3 + 1.5).**
|
||||
> Le rendu automatique est aujourd'hui **plat** : le `basic_shader.wgsl` interprète les positions comme
|
||||
> déjà en NDC, sans matrice monde/vue/projection ni lumière. L'étape suivante rend la scène réellement
|
||||
> 3D et éclairée : créer `standard_shader.wgsl` (Phong : matrice `projection * view * world` + lumière
|
||||
> directionnelle), ajouter les uniform buffers (frame : view/proj/light ; par mesh : world matrix dérivée
|
||||
> du `Transform`) et les brancher dans `Renderer::render_scene` et `Material`, puis exposer `Camera`/
|
||||
> `Transform` à la `Scene` (caméra active) et ajouter un mesh de test (cube) à l'exemple. Objectif MVP :
|
||||
> **un mesh 3D éclairé à l'écran**.
|
||||
> déjà en NDC, sans matrice monde/vue/projection ni lumière. **Une grande partie de l'infrastructure est
|
||||
> déjà en place (Étapes 3+4, 2026-09-16)** : le `standard_shader.wgsl` Phong (matrice
|
||||
> `projection * view * world` + lumière directionnelle) existe et valide ; les uniform buffers sont
|
||||
> branchés (frame : view/proj/cam_pos + lumière ; par mesh : `world` dérivé du `Transform`) ; le `Renderer`
|
||||
> écrit chaque frame la caméra active (via `Scene::set_camera`/`camera()`) et la matrice monde de chaque
|
||||
> entité. **Reste à faire** pour un mesh 3D éclairé à l'écran : brancher `standard` sur un exemple et
|
||||
> ajouter un mesh de test (cube). Objectif MVP : **un mesh 3D éclairé à l'écran**.
|
||||
|
||||
---
|
||||
|
||||
@@ -40,11 +43,11 @@ generated: { by: human:jerome, at: 2026-07-31T00:00:00Z }
|
||||
### 1.1 Dépendances & Mathématiques
|
||||
- [x] `glam = "0.33"` ajouté (`lib/Cargo.toml`) — déjà présent, utilisé par `math/transform.rs` et `resources/camera.rs`
|
||||
- [x] `slotmap` **retiré** — décision prise : **String IDs pour le MVP** ; slotmap reporté à l'étape "handles typés" (voir Notes de Décision)
|
||||
- [ ] Créer module `math/` (ou `transform.rs`) :
|
||||
- [ ] Struct `Transform { translation: Vec3, rotation: Quat, scale: Vec3 }`
|
||||
- [ ] Méthode `to_matrix() -> Mat4` pour calculer la matrice locale
|
||||
- [ ] Struct `Camera { position: Vec3, target: Vec3, up: Vec3 }` : resources/camera.rs
|
||||
- [ ] Fonctions `view_matrix()` et `projection_matrix(fov, aspect, near, far)`
|
||||
- [x] Module `math/` / `transform.rs`:
|
||||
- [x] Struct `Transform { translation: Vec3, rotation: Quat, scale: Vec3 }`
|
||||
- [x] Méthode `to_matrix() -> Mat4` pour calculer la matrice locale
|
||||
- [x] Struct `Camera { position: Vec3, target: Vec3, up: Vec3 }` : resources/camera.rs — enrichi en Étape 4.3 (fov/near/far + `with_perspective`)
|
||||
- [x] Fonctions `view_matrix()` et `projection_matrix(fov, aspect, near, far)` (Étape 4.3 : `projection_matrix(aspect)` utilise fov/near/far stockés)
|
||||
|
||||
### 1.2 Geometry & Mesh
|
||||
- [ ] Créer struct `Geometry` (math/geometry.rs) :
|
||||
@@ -59,22 +62,23 @@ generated: { by: human:jerome, at: 2026-07-31T00:00:00Z }
|
||||
- [ ] Ajouter un mesh de test (cube unitaire) en exemple
|
||||
|
||||
### 1.3 Shader Phong Minimal
|
||||
- [ ] Créer `standard_shader.wgsl` :
|
||||
- [ ] Vertex shader : projection * view * world * position
|
||||
- [ ] Fragment shader : éclairage hémisphérique + diffuse avec une lumière directionnelle
|
||||
- [ ] Uniforms : `view_matrix`, `proj_matrix`, `world_matrix`, `light_dir`, `light_color`
|
||||
- [ ] Mettre à jour `Material` pour supporter les uniforms du shader Phong
|
||||
- [x] Créer `standard_shader.wgsl` (Étape 2, 2026-09-16) :
|
||||
- [x] Vertex shader : projection * view * world * position
|
||||
- [x] Fragment shader : éclairage directionnel (+ hémisphérique)
|
||||
- [x] Uniforms : `view`, `proj`, `cam_pos`, `light_dir`, `light_color`, `options`
|
||||
- [x] Mettre à jour `Material` / pipeline pour supporter les uniforms du shader Phong (bind group layouts frame+object, Étape 3) — `standard` n'est pas encore branché sur un exemple
|
||||
|
||||
### 1.4 Scene avec identifiants (MVP : String IDs)
|
||||
- [x] `Scene` implémentée avec **String IDs** (`HashMap<String, Arc<Mesh>>`, `...Material`, entités) — état actuel validé ; décision : rester en String IDs pour le MVP
|
||||
- [x] Méthodes : `add_mesh()`, `get_mesh()`, `add_material()`, `add_entity()`, `iter_entities()`, `remove_entity()`
|
||||
- [x] Caméra active dans la `Scene` : `set_camera()` / `camera()` (Étape 4.3)
|
||||
- [ ] **Reporté (étape "Handles typés")** : migrer vers `slotmap` générationnel (`MeshId`/`MaterialId`) quand l'éviction/les performances le justifieront
|
||||
|
||||
### 1.5 Rendu du Prototype
|
||||
- [ ] Uniform buffer pour la frame : `view_matrix`, `proj_matrix`, `light_dir`
|
||||
- [ ] Uniform buffer par mesh : `world_matrix` (calculée sur CPU pour le MVP)
|
||||
- [ ] `Renderer::render()` itère sur les meshes de la Scene et dessine chacun
|
||||
- [ ] Exemple fonctionnel : un cube éclairé tourne à l'écran
|
||||
- [x] Uniform buffer pour la frame : `view`, `proj`, `cam_pos`, `light_dir` (Étapes 3+4) — écrit chaque frame depuis la caméra active
|
||||
- [x] Uniform buffer par mesh : `world` (calculée sur CPU depuis `transform.to_matrix()`, Étape 4.2)
|
||||
- [x] `Renderer::render_scene()` itère sur les entités de la Scene et dessine chacune (liaison bind groups frame+object)
|
||||
- [ ] Exemple fonctionnel : un cube éclairé tourne à l'écran — **à faire** (Étape 5 : brancher `standard` sur un exemple + mesh cube)
|
||||
|
||||
---
|
||||
|
||||
@@ -94,9 +98,9 @@ generated: { by: human:jerome, at: 2026-07-31T00:00:00Z }
|
||||
- [ ] `Scene::iter_entities()` → pour le render loop
|
||||
|
||||
### 2.3 Camera dans la Scene
|
||||
- [ ] Intégrer `Camera` comme ressource de la Scene
|
||||
- [ ] Permettre plusieurs caméras (actuelle/inactive)
|
||||
- [ ] Exposer API : `scene.set_active_camera(camera_id)`
|
||||
- [x] Intégrer `Camera` comme ressource de la Scene (Étape 4.3 : `Scene::set_camera` / `camera()`, caméra active unique)
|
||||
- [ ] Permettre plusieurs caméras (actuelle/inactive) et une sélection par identifiant (`scene.set_active_camera(camera_id)`)
|
||||
- [ ] Exposer une caméra orbitale contrôlable (exemple final, Phase 5)
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -11,5 +11,5 @@ wgpu = "30.0.0" # Vérifiez la version la plus récente
|
||||
winit = "0.30.13" # For window management — pinned to match examples
|
||||
thiserror = "2"
|
||||
bytemuck = { version = "1.25.0", features = ["derive"] }
|
||||
glam = "0.33"
|
||||
glam = { version = "0.33", features = ["bytemuck"] } # feature requis pour Pod/Zeroable sur Mat4/Vec4 (uniform.rs)
|
||||
pollster = { version="1.0.1", features = ["macro"] }
|
||||
|
||||
+7
-1
@@ -125,8 +125,14 @@ impl App {
|
||||
/// Called automatically each frame by the default `AppHandler::render`, or manually by users
|
||||
/// who override `render` to control drawing themselves.
|
||||
/// Inputs: view — the frame's texture view acting as the color attachment target.
|
||||
///
|
||||
/// The viewport aspect ratio (needed for the active camera's perspective projection, Étape 4.3)
|
||||
/// is derived here from the window's current inner size, so the `Renderer` stays independent of
|
||||
/// the windowing backend.
|
||||
pub fn render_scene(&self, view: &wgpu::TextureView) {
|
||||
self.renderer().render_scene(view, &self.scene);
|
||||
let size = self.window().inner_size();
|
||||
let aspect = size.width as f32 / size.height.max(1) as f32;
|
||||
self.renderer().render_scene(view, &self.scene, aspect);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+169
-13
@@ -20,12 +20,21 @@
|
||||
|
||||
use crate::core::Context;
|
||||
use crate::core::Frame;
|
||||
use crate::resources::{Material, Mesh};
|
||||
use crate::math::Transform;
|
||||
use crate::pipeline::create_uniform_bind_group_layouts;
|
||||
use crate::resources::uniform::{FRAME_UNIFORMS_SIZE, OBJECT_UNIFORM_SIZE};
|
||||
use crate::resources::{Camera, FrameUniforms, Material, Mesh, ObjectUniform};
|
||||
use crate::scene::Scene;
|
||||
use glam::Vec4;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// The Executor layer of the architecture. Holds shared references to Device and Queue from Context,
|
||||
/// plus the surface texture format. Executes WGPU rendering commands by binding Materials and Meshes
|
||||
/// into RenderPasses during each frame. Does not own raw hardware resources (they are Arc-cloned from Context).
|
||||
///
|
||||
/// Since Étape 3 every pipeline declares the two uniform bind groups (frame @0 + object @1), the
|
||||
/// Renderer owns the matching GPU buffers and `BindGroup`s and binds them around every draw call.
|
||||
pub struct Renderer {
|
||||
/// GPU command submission queue — holds an Arc clone from Context; shared with other Context users.
|
||||
queue: wgpu::Queue,
|
||||
@@ -33,22 +42,105 @@ pub struct Renderer {
|
||||
device: wgpu::Device,
|
||||
/// Surface texture output format — stored here so it can be passed to PipelineCache on Material creation.
|
||||
format: wgpu::TextureFormat,
|
||||
/// Bind group layout for the per-object uniforms (group 1) — must match every pipeline layout.
|
||||
object_layout: wgpu::BindGroupLayout,
|
||||
/// Shared per-frame uniform buffer handle — kept so the camera matrices can be rewritten each
|
||||
/// frame (`render_scene`) and shipped to the GPU before the frame bind group is used.
|
||||
frame_buffer: wgpu::Buffer,
|
||||
/// Shared per-frame uniform buffer + bind group (camera + lights). Written each frame (`render_scene`).
|
||||
frame_bind_group: wgpu::BindGroup,
|
||||
/// Shared per-object bind group (identity model) used by the low-level `render` path.
|
||||
shared_object_bind_group: wgpu::BindGroup,
|
||||
/// Per-entity object uniform buffers + bind groups, lazily created on first encounter and keyed by
|
||||
/// entity label. Needed because `render_scene(&self, &Scene)` is immutable; the model matrix is
|
||||
/// rewritten each frame for every entity.
|
||||
object_cache: RefCell<HashMap<String, (wgpu::Buffer, wgpu::BindGroup)>>,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
/// Creates a Renderer by cloning Device and Queue Arc references from the Context, plus capturing the surface format.
|
||||
/// Creates a Renderer by cloning Device and Queue Arc references from the Context, plus capturing
|
||||
/// the surface format, and allocates the shared frame + object uniform buffers and their bind groups.
|
||||
/// Inputs: context (borrowed reference to Context providing GPU resource handles), format (surface texture format).
|
||||
/// Returns a new Renderer instance sharing the same underlying GPU resources as Context.
|
||||
/// Called once at application startup during scene setup. The Renderer shares these resources via Arc;
|
||||
/// Context retains ownership and can continue using them after this call.
|
||||
pub fn new(context: &Context, format: wgpu::TextureFormat) -> Self {
|
||||
let queue: wgpu::Queue = context.queue.clone();
|
||||
let device: wgpu::Device = context.device.clone();
|
||||
let [frame_layout, object_layout] = create_uniform_bind_group_layouts(&device);
|
||||
|
||||
// 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
|
||||
// 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 {
|
||||
label: Some("frame uniform buffer"),
|
||||
size: FRAME_UNIFORMS_SIZE,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
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 {
|
||||
label: Some("frame bind group"),
|
||||
layout: &frame_layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: frame_buffer.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
|
||||
// Shared per-object bind group (identity model) for the low-level `render` path.
|
||||
let object_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("shared object uniform buffer"),
|
||||
size: OBJECT_UNIFORM_SIZE,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let identity_object = ObjectUniform {
|
||||
model: glam::Mat4::IDENTITY,
|
||||
};
|
||||
queue.write_buffer(&object_buffer, 0, bytemuck::bytes_of(&identity_object));
|
||||
let shared_object_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("shared object bind group"),
|
||||
layout: &object_layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: object_buffer.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
|
||||
Self {
|
||||
queue: context.queue.clone(),
|
||||
device: context.device.clone(),
|
||||
queue,
|
||||
device,
|
||||
format,
|
||||
object_layout,
|
||||
frame_buffer,
|
||||
frame_bind_group,
|
||||
shared_object_bind_group,
|
||||
object_cache: RefCell::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Rewrites the shared per-frame uniform buffer from the scene's active camera and the current
|
||||
/// viewport aspect, then returns the frame bind group wired to that buffer. Called at the start of
|
||||
/// every `render_scene` so the GPU sees the latest camera matrices and camera position (Étape 4.3).
|
||||
///
|
||||
/// The directional light stays at the `FrameUniforms::default()` values (white, along +Z) — scene
|
||||
/// lighting configuration is a later step; only the camera-driven fields are derived from `camera`.
|
||||
/// Inputs: camera (the scene's active camera), aspect (viewport width / height).
|
||||
fn write_frame_uniforms(&self, camera: &Camera, aspect: f32) {
|
||||
let frame = FrameUniforms {
|
||||
view: camera.view_matrix(),
|
||||
proj: camera.projection_matrix(aspect),
|
||||
cam_pos: camera.position.extend(1.0),
|
||||
light_dir: Vec4::new(0.0, 0.0, 1.0, 0.0),
|
||||
light_color: Vec4::ONE,
|
||||
options: [0, 0, 0, 0],
|
||||
};
|
||||
self.queue
|
||||
.write_buffer(&self.frame_buffer, 0, bytemuck::bytes_of(&frame));
|
||||
}
|
||||
|
||||
/// Orchestrates rendering of a single object: binds Material pipeline + Mesh vertex data into a RenderPass,
|
||||
/// then submits commands to the GPU queue for execution. Called per-frame by the orchestrator (main.rs).
|
||||
/// Inputs: view (TextureView color attachment target), mesh (geometry to render), material (shader+pipeline).
|
||||
@@ -81,7 +173,13 @@ impl Renderer {
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
draw_entity(&mut render_pass, mesh, material);
|
||||
draw_entity(
|
||||
&mut render_pass,
|
||||
mesh,
|
||||
material,
|
||||
&self.frame_bind_group,
|
||||
&self.shared_object_bind_group,
|
||||
);
|
||||
}
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
}
|
||||
@@ -90,8 +188,15 @@ impl Renderer {
|
||||
/// This avoids allocating a separate encoder and render pass per entity (which the low-level
|
||||
/// `render` does), minimizing GPU submissions. Called automatically each frame by the default
|
||||
/// `AppHandler::render` through `App::render_scene`.
|
||||
/// Inputs: view — the frame's texture view color attachment; scene — the scene whose entities are drawn.
|
||||
pub fn render_scene(&self, view: &wgpu::TextureView, scene: &Scene) {
|
||||
/// Inputs: view — the frame's texture view color attachment; scene — the scene whose entities are
|
||||
/// drawn; aspect — the viewport aspect ratio (width/height), used to build the camera's perspective
|
||||
/// projection.
|
||||
///
|
||||
/// Before drawing, the shared frame uniform buffer is rewritten from `scene.camera()` so the GPU
|
||||
/// receives the active camera's view/projection matrices and position for this frame (Étape 4.3).
|
||||
pub fn render_scene(&self, view: &wgpu::TextureView, scene: &Scene, aspect: f32) {
|
||||
self.write_frame_uniforms(scene.camera(), aspect);
|
||||
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
@@ -113,8 +218,15 @@ impl Renderer {
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
for (_label, mesh, material, _transform) in scene.iter_entities() {
|
||||
draw_entity(&mut render_pass, mesh, material);
|
||||
for (label, mesh, material, transform) in scene.iter_entities() {
|
||||
let object_bind_group = self.object_bind_group_for(label, transform);
|
||||
draw_entity(
|
||||
&mut render_pass,
|
||||
mesh,
|
||||
material,
|
||||
&self.frame_bind_group,
|
||||
&object_bind_group,
|
||||
);
|
||||
}
|
||||
}
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
@@ -138,18 +250,62 @@ impl Renderer {
|
||||
pub fn format(&self) -> wgpu::TextureFormat {
|
||||
self.format
|
||||
}
|
||||
|
||||
/// Returns the per-entity object bind group for `label`, creating its uniform buffer on first
|
||||
/// encounter and rewriting the model matrix each call. Since `render_scene(&self)` is immutable,
|
||||
/// the lazily-populated cache is interior-mutable (`RefCell`). Étape 4.2.
|
||||
/// Inputs: label (entity identifier used as cache key), transform (world placement to upload).
|
||||
/// Returns an owned (cheaply Arc-cloned) reference handle to the object bind group (group 1).
|
||||
fn object_bind_group_for(&self, label: &str, transform: &Transform) -> wgpu::BindGroup {
|
||||
let mut cache = self.object_cache.borrow_mut();
|
||||
let (buffer, bind_group) = cache.entry(label.to_string()).or_insert_with(|| {
|
||||
let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("object uniform buffer"),
|
||||
size: OBJECT_UNIFORM_SIZE,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("object bind group"),
|
||||
layout: &self.object_layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: buffer.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
(buffer, bind_group)
|
||||
});
|
||||
// Rewrite the model matrix every frame so entity transforms can update (e.g. rotation).
|
||||
let object_uniforms = ObjectUniform {
|
||||
model: transform.to_matrix(),
|
||||
};
|
||||
self.queue
|
||||
.write_buffer(buffer, 0, bytemuck::bytes_of(&object_uniforms));
|
||||
bind_group.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Binds a Material pipeline and Mesh buffers into an active render pass and issues the draw call.
|
||||
/// Shared by `Renderer::render` and `Renderer::render_scene` to avoid duplicated draw logic.
|
||||
/// Binds a Material pipeline, the two uniform bind groups, and Mesh buffers into an active render
|
||||
/// pass and issues the draw call. Shared by `Renderer::render` and `Renderer::render_scene`.
|
||||
/// The frame (group 0) and object (group 1) bind groups are **required** by every pipeline layout
|
||||
/// (Étape 3 : un seul layout pour tous) — they must be bound even if the shader does not read them.
|
||||
/// Draws indexed geometry when an index buffer exists, otherwise falls back to a non-indexed draw.
|
||||
/// Inputs: pass (active render pass), mesh (geometry to draw), material (pipeline to bind).
|
||||
fn draw_entity(pass: &mut wgpu::RenderPass<'_>, mesh: &Mesh, material: &Material) {
|
||||
/// Inputs: pass (active render pass), mesh (geometry to draw), material (pipeline to bind),
|
||||
/// frame_bind_group (shared per-frame uniforms), object_bind_group (per-entity/identity model).
|
||||
fn draw_entity(
|
||||
pass: &mut wgpu::RenderPass<'_>,
|
||||
mesh: &Mesh,
|
||||
material: &Material,
|
||||
frame_bind_group: &wgpu::BindGroup,
|
||||
object_bind_group: &wgpu::BindGroup,
|
||||
) {
|
||||
if mesh.num_vertices == 0 {
|
||||
// No vertices — nothing to render.
|
||||
return;
|
||||
}
|
||||
pass.set_pipeline(&material.pipeline);
|
||||
pass.set_bind_group(0, frame_bind_group, &[]);
|
||||
pass.set_bind_group(1, object_bind_group, &[]);
|
||||
pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
|
||||
if let Some(index_buffer) = &mesh.index_buffer {
|
||||
pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16);
|
||||
|
||||
@@ -11,4 +11,4 @@
|
||||
|
||||
pub mod pipeline_cache;
|
||||
// Re-exports
|
||||
pub use pipeline_cache::PipelineCache;
|
||||
pub use pipeline_cache::{PipelineCache, create_uniform_bind_group_layouts};
|
||||
|
||||
@@ -21,6 +21,44 @@ use crate::utils::BASIC_SHADER;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Creates the two bind group layouts shared by **every** pipeline (Étape 3 — décision actée
|
||||
/// « un seul layout pour tous »). Both buffers are `Uniform`, 16-byte aligned, no dynamic offset.
|
||||
/// Matching CPU types: `FrameUniforms` (192 B) and `ObjectUniform` (64 B) in `resources::uniform`.
|
||||
/// Returns `[frame_layout, object_layout]` in renderer binding order.
|
||||
///
|
||||
/// - `index 0` : per-frame uniforms (view/proj/light/options), visible in both shader stages.
|
||||
/// - `index 1` : per-object uniforms (model matrix), visible in the vertex stage only.
|
||||
pub fn create_uniform_bind_group_layouts(device: &wgpu::Device) -> [wgpu::BindGroupLayout; 2] {
|
||||
[
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("frame_uniform_layout"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
}),
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("object_uniform_layout"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
/// Shader pipeline cache: maps (shader_id, format) keys to compiled RenderPipelines.
|
||||
/// Ensures each unique shader+format combination is compiled at most once; subsequent requests return cached instances.
|
||||
pub struct PipelineCache {
|
||||
@@ -156,12 +194,16 @@ impl PipelineCache {
|
||||
],
|
||||
};
|
||||
|
||||
// Pipeline layout — defines bind group bindings (empty here; no uniform buffers used).
|
||||
// wgpu 30: `immediate_size` replaces `push_constant_ranges`.
|
||||
// Pipeline layout — the two uniform bind groups (frame @0 + object @1) are attached
|
||||
// to EVERY pipeline (Étape 3, décision actée « un seul layout pour tous »), even if a
|
||||
// given shader does not read them. `immediate_size` stays 0 (no var<immediate> used).
|
||||
let bind_group_layouts = create_uniform_bind_group_layouts(device);
|
||||
let layout_refs: Vec<Option<&wgpu::BindGroupLayout>> =
|
||||
bind_group_layouts.iter().map(Some).collect();
|
||||
let render_pipeline_layout =
|
||||
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("render_pipeline_layout"),
|
||||
bind_group_layouts: &[],
|
||||
bind_group_layouts: &layout_refs,
|
||||
immediate_size: 0, // no var<immediate> used
|
||||
});
|
||||
|
||||
|
||||
+45
-11
@@ -15,9 +15,18 @@
|
||||
|
||||
use glam::{Mat4, Vec3};
|
||||
|
||||
/// Default vertical field of view in radians (45°).
|
||||
pub const DEFAULT_FOV: f32 = 45.0_f32.to_radians();
|
||||
/// Near clipping plane distance used by the default perspective projection.
|
||||
pub const DEFAULT_NEAR: f32 = 0.1;
|
||||
/// Far clipping plane distance used by the default perspective projection.
|
||||
pub const DEFAULT_FAR: f32 = 100.0;
|
||||
|
||||
/// Represents a 3D camera for viewing the scene.
|
||||
///
|
||||
/// The camera defines the viewpoint and projection settings for rendering.
|
||||
/// The camera defines the viewpoint (position/target/up), the projection parameters (fov, near, far)
|
||||
/// and can produce the view and projection matrices uploaded each frame to the `FrameUniforms` buffer
|
||||
/// (Étape 4.3). Use `Scene::set_camera` to install it as the scene's active camera.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Camera {
|
||||
/// Position of the camera in world space
|
||||
@@ -26,37 +35,62 @@ pub struct Camera {
|
||||
pub target: Vec3,
|
||||
/// Up vector defining the camera's orientation
|
||||
pub up: Vec3,
|
||||
/// Vertical field of view in radians (used by the perspective projection).
|
||||
pub fov: f32,
|
||||
/// Near clipping plane distance (used by the perspective projection).
|
||||
pub near: f32,
|
||||
/// Far clipping plane distance (used by the perspective projection).
|
||||
pub far: f32,
|
||||
}
|
||||
|
||||
impl Default for Camera {
|
||||
/// Default camera : positioned at (0, 0, 3) looking at the origin with a 45° vertical fov,
|
||||
/// near 0.1 and far 100. Good enough to frame a unit-cube scene out of the box.
|
||||
fn default() -> Self {
|
||||
Self::new(Vec3::new(0.0, 0.0, 3.0), Vec3::ZERO, Vec3::Y)
|
||||
}
|
||||
}
|
||||
|
||||
impl Camera {
|
||||
/// Creates a new camera with specified position, target, and up vector.
|
||||
/// Creates a new perspective camera with the default fov/near/far.
|
||||
/// Inputs: position (world-space eye point), target (world-space look-at point), up (view up vector).
|
||||
/// Adjust the projection via [`Camera::with_perspective`] if the defaults don't fit.
|
||||
pub fn new(position: Vec3, target: Vec3, up: Vec3) -> Self {
|
||||
Self {
|
||||
position,
|
||||
target,
|
||||
up,
|
||||
fov: DEFAULT_FOV,
|
||||
near: DEFAULT_NEAR,
|
||||
far: DEFAULT_FAR,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the perspective projection parameters and returns the camera for chaining.
|
||||
/// Inputs: fov (vertical field of view in radians), near (near plane), far (far plane).
|
||||
pub fn with_perspective(mut self, fov: f32, near: f32, far: f32) -> Self {
|
||||
self.fov = fov;
|
||||
self.near = near;
|
||||
self.far = far;
|
||||
self
|
||||
}
|
||||
|
||||
/// Computes the view matrix for this camera.
|
||||
///
|
||||
/// # Returns
|
||||
/// A `Mat4` representing the view transformation matrix
|
||||
/// A `Mat4` representing the view transformation matrix (world → view space)
|
||||
pub fn view_matrix(&self) -> Mat4 {
|
||||
glam::camera::rh::view::look_at_mat4(self.position, self.target, self.up)
|
||||
}
|
||||
|
||||
/// Computes the projection matrix for this camera.
|
||||
/// Computes the perspective projection matrix for this camera using its stored fov/near/far.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `fov`: Field of view in radians
|
||||
/// - `aspect`: Aspect ratio of the viewport
|
||||
/// - `near`: Near clipping plane distance
|
||||
/// - `far`: Far clipping plane distance
|
||||
/// - `aspect`: Aspect ratio of the viewport (width / height)
|
||||
///
|
||||
/// # Returns
|
||||
/// A `Mat4` representing the projection transformation matrix
|
||||
pub fn projection_matrix(&self, fov: f32, aspect: f32, near: f32, far: f32) -> Mat4 {
|
||||
glam::camera::rh::proj::opengl::perspective(fov, aspect, near, far)
|
||||
/// A `Mat4` representing the projection transformation matrix (view → clip space)
|
||||
pub fn projection_matrix(&self, aspect: f32) -> Mat4 {
|
||||
glam::camera::rh::proj::opengl::perspective(self.fov, aspect, self.near, self.far)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
pub mod camera;
|
||||
pub mod material;
|
||||
pub mod mesh;
|
||||
pub mod uniform;
|
||||
pub mod vertex;
|
||||
|
||||
// Re-exports
|
||||
pub use camera::Camera;
|
||||
pub use material::Material;
|
||||
pub use mesh::Mesh;
|
||||
pub use uniform::{FrameUniforms, ObjectUniform};
|
||||
pub use vertex::Vertex;
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
//! # Uniform Module — GPU Buffer Data Types
|
||||
//!
|
||||
//! Defines the CPU-side `Pod` (plain old data) structs that are uploaded to GPU uniform buffers.
|
||||
//! Their memory layout must match **exactly** the WGSL uniforms declared in `standard_shader.wgsl`
|
||||
//! (see the "Uniform Contract" section of that file) — 16-byte alignment (std140), no padding.
|
||||
//!
|
||||
//! Two bind groups are shared by every pipeline (single-layout decision, Étape 3) :
|
||||
//! - `@group(0) @binding(0)` : `FrameUniforms` (per-frame : camera + lights) → 192 bytes
|
||||
//! - `@group(1) @binding(0)` : `ObjectUniform` (per-entity model matrix) → 64 bytes
|
||||
//!
|
||||
//! ## Interaction with Other Modules
|
||||
//! - `pipeline_cache::build_pipeline()` creates the two bind group layouts matching these types.
|
||||
//! - `Renderer` allocates the buffers and `BindGroup`s from these types and writes them each frame.
|
||||
//! - `standard_shader.wgsl` consumes them (layout identical to these structs).
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use glam::{Mat4, Vec4};
|
||||
|
||||
/// Byte size of the per-frame uniform buffer (`FrameUniforms`).
|
||||
pub const FRAME_UNIFORMS_SIZE: u64 = std::mem::size_of::<FrameUniforms>() as u64;
|
||||
/// Byte size of the per-object uniform buffer (`ObjectUniform`).
|
||||
pub const OBJECT_UNIFORM_SIZE: u64 = std::mem::size_of::<ObjectUniform>() as u64;
|
||||
|
||||
/// Per-frame GPU uniforms : camera matrices + directional light + options.
|
||||
///
|
||||
/// Mirrors the WGSL `FrameUniforms` struct in `standard_shader.wgsl` (offset table there).
|
||||
/// 192 bytes, 16-byte aligned, no padding — `Pod` for direct `bytes_of` upload.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Pod, Zeroable)]
|
||||
pub struct FrameUniforms {
|
||||
/// Camera view matrix (world → view space). Offset 0.
|
||||
pub view: Mat4,
|
||||
/// Camera projection matrix (view → clip space). Offset 64.
|
||||
pub proj: Mat4,
|
||||
/// Camera world position (`.xyz` used). Offset 128.
|
||||
pub cam_pos: Vec4,
|
||||
/// Directional light direction : points **from the surface toward the light**. Offset 144.
|
||||
pub light_dir: Vec4,
|
||||
/// Directional light color (`.rgb` used). Offset 160.
|
||||
pub light_color: Vec4,
|
||||
/// Options. `options[0]` = unlit flag (1 → flat color, no directional lighting). Offset 176.
|
||||
pub options: [u32; 4],
|
||||
}
|
||||
|
||||
impl Default for FrameUniforms {
|
||||
/// Sensible defaults : identity camera, white light along +Z, *lit* mode.
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
view: Mat4::IDENTITY,
|
||||
proj: Mat4::IDENTITY,
|
||||
cam_pos: Vec4::ZERO,
|
||||
light_dir: Vec4::new(0.0, 0.0, 1.0, 0.0),
|
||||
light_color: Vec4::ONE,
|
||||
options: [0, 0, 0, 0],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-object GPU uniforms : the entity's world-space model matrix.
|
||||
///
|
||||
/// Mirrors the WGSL `ObjectUniform` struct. 64 bytes, `Pod`.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Pod, Zeroable, Default)]
|
||||
pub struct ObjectUniform {
|
||||
/// Model matrix (object → world space). Offset 0.
|
||||
pub model: Mat4,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::mem::offset_of;
|
||||
use std::mem::{align_of, size_of};
|
||||
|
||||
#[test]
|
||||
fn frame_uniforms_layout_matches_wgsl() {
|
||||
// The offsets below must match the offset table in standard_shader.wgsl.
|
||||
assert_eq!(size_of::<FrameUniforms>(), 192);
|
||||
assert_eq!(align_of::<FrameUniforms>(), 16);
|
||||
|
||||
let f = FrameUniforms::default();
|
||||
assert_eq!(offset_of!(FrameUniforms, view), 0);
|
||||
assert_eq!(offset_of!(FrameUniforms, proj), 64);
|
||||
assert_eq!(offset_of!(FrameUniforms, cam_pos), 128);
|
||||
assert_eq!(offset_of!(FrameUniforms, light_dir), 144);
|
||||
assert_eq!(offset_of!(FrameUniforms, light_color), 160);
|
||||
assert_eq!(offset_of!(FrameUniforms, options), 176);
|
||||
// Default is lit mode (unlit flag cleared).
|
||||
assert_eq!(f.options[0], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_uniform_layout_matches_wgsl() {
|
||||
assert_eq!(size_of::<ObjectUniform>(), 64);
|
||||
assert_eq!(align_of::<ObjectUniform>(), 16);
|
||||
assert_eq!(offset_of!(ObjectUniform, model), 0);
|
||||
}
|
||||
}
|
||||
+24
-3
@@ -11,13 +11,14 @@
|
||||
//! - **Ergonomie**: Users interact only with entity-level operations (add/remove/get) rather than wgpu buffers/pipelines directly.
|
||||
|
||||
use crate::math::Transform;
|
||||
use crate::resources::{Material, Mesh};
|
||||
use crate::resources::{Camera, Material, Mesh};
|
||||
use crate::scene::Entity;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Resource depot and entity graph. Stores Meshes and Materials keyed by identifier strings,
|
||||
/// and maps entity labels to their associated `Entity` (mesh + material + transform) for rendering iteration.
|
||||
/// maps entity labels to their associated `Entity` (mesh + material + transform) for rendering iteration,
|
||||
/// and holds the scene's active `Camera` used to build the per-frame view/projection matrices (Étape 4.3).
|
||||
/// Created once during application setup; entities are added before the render loop starts.
|
||||
pub struct Scene {
|
||||
/// Map of mesh identifiers to owned `Arc<Mesh>` instances. Populated via `add_mesh()`.
|
||||
@@ -26,19 +27,39 @@ pub struct Scene {
|
||||
materials: HashMap<String, Arc<Material>>,
|
||||
/// Map of entity labels to `Entity` associations. Populated via `add_entity()` / `add_entity_with_transform()`.
|
||||
entities: HashMap<String, Entity>,
|
||||
/// Active camera used for rendering. Read each frame by `Renderer::render_scene` to compute the
|
||||
/// view/projection matrices written into the frame uniform buffer. Replaced via `set_camera()`.
|
||||
camera: Camera,
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
/// Creates an empty scene with no registered resources or entities.
|
||||
/// Creates an empty scene with no registered resources or entities and a default camera
|
||||
/// (`Camera::default()` : position (0,0,3), looking at origin, 45° perspective).
|
||||
/// Called at application startup before any resource registration.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
meshes: HashMap::new(),
|
||||
materials: HashMap::new(),
|
||||
entities: HashMap::new(),
|
||||
camera: Camera::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces the scene's active camera. The new camera is used from the next frame onward by
|
||||
/// `Renderer::render_scene` to build the view/projection matrices and the camera position.
|
||||
/// Inputs: camera — the new camera configuration. Call during setup or `AppHandler::update`
|
||||
/// to move/re-orient the view (e.g. orbit or FPS controls).
|
||||
pub fn set_camera(&mut self, camera: Camera) {
|
||||
self.camera = camera;
|
||||
}
|
||||
|
||||
/// Returns a reference to the scene's active camera.
|
||||
/// Called by users to read the current camera (e.g. to move it based on input) and internally by
|
||||
/// `Renderer::render_scene` to upload its matrices.
|
||||
pub fn camera(&self) -> &Camera {
|
||||
&self.camera
|
||||
}
|
||||
|
||||
/// Registers a Mesh in the scene under a unique identifier.
|
||||
/// Inputs: id (unique key), mesh (Arc-wrapped Mesh instance). Returns Ok(id) on success or Err(String) if already exists.
|
||||
/// Called during scene initialization when building the resource depot.
|
||||
|
||||
Reference in New Issue
Block a user