feat(renderer): active camera wired to frame uniforms (Étape 4.3)

- Camera enrichie: fov/near/far stockés, Default (pos (0,0,3), 45°, near 0.1,
  far 100), with_perspective(), projection_matrix(aspect) depuis les params
  stockés (au lieu de les passer en argument).
- Scene porte une caméra active: set_camera()/camera() (défaut Camera::default).
- Renderer::render_scene(view, scene, aspect) écrit chaque frame view/proj/
  cam_pos réels dans le buffer frame (write_frame_uniforms) avant de dessiner;
  le Renderer garde le handle du frame_buffer. Le chemin bas-niveau render()
  conserve les valeurs par défaut (identité).
- App::render_scene calcule l'aspect depuis window.inner_size() (le Renderer
  reste indépendant de la fenêtre).

Docs synchronisées: DRAFT (4.3 coche), README (statut 3D-infra + quick ref),
PLAN (caméras), ROADMAP (1.1/1.3/1.5/2.3).

Validation: check workspace+examples 0 warning, test (Pod + wgsl) OK, doc 0
warning, fmt propre. Le rendu 3D visible attend Étape 5 (brancher standard).
This commit is contained in:
Jérôme Bousquié
2026-09-16 17:25:50 +02:00
parent c1e07b42b4
commit f10e249898
8 changed files with 159 additions and 52 deletions
+5 -5
View File
@@ -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 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 ## 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. - **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. - **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**. 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 | ✅ | | Material | Struct | Shader ID → RenderPipeline | ✅ |
| Mesh / Vertex | Struct | GPU geometry container / CPU-side vertex tuple | ✅ | | Mesh / Vertex | Struct | GPU geometry container / CPU-side vertex tuple | ✅ |
| Frame | Struct | Per-frame RAII wrapper (surface texture + view) | ✅ | | 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 ## 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.) 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. 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. 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.
+8 -4
View File
@@ -93,14 +93,18 @@ toucher au rendu (pure façade de données, validable par compilation).
- [X] 4.2 **Buffers object par entité** : le `Renderer` maintient un cache - [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 `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)* écrit `ObjectUniform.world = entity.transform.to_matrix()` (via `object_bind_group_for`). *(fait — 2026-09-16)*
- [ ] 4.3 **Caméra active** : ajouter `scene.set_active_camera(Camera)` / `scene.active_camera() -> Option<&Camera>` ; - [X] 4.3 **Caméra active** : `Scene` porte une caméra active (`Camera::default()` : position (0,0,3),
écrire view/proj (avec aspect de la fenêtre) dans le buffer frame chaque frame. *(non fait — laisse le fov 45°, near 0.1, far 100) via `set_camera()` / `camera()` ; `Camera` enrichie (fov/near/far +
`FrameUniforms::default()` : simple/manual tournent toujours via `basic` qui ignore ces uniforms)* `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 - [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 : - [ ] **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 4.3)* `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é) ## Étape 5 — Exemple 3D (cube éclairé)
+7 -1
View File
@@ -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 > **« 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
> 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) ## 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. - [ ] **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. - [ ] **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 ## Check-list de Vérification pour le LLM d'Assistance
+28 -24
View File
@@ -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 > 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`). > (`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. > - `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).** > **É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 > 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 > déjà en NDC, sans matrice monde/vue/projection ni lumière. **Une grande partie de l'infrastructure est
> 3D et éclairée : créer `standard_shader.wgsl` (Phong : matrice `projection * view * world` + lumière > déjà en place (Étapes 3+4, 2026-09-16)** : le `standard_shader.wgsl` Phong (matrice
> directionnelle), ajouter les uniform buffers (frame : view/proj/light ; par mesh : world matrix dérivée > `projection * view * world` + lumière directionnelle) existe et valide ; les uniform buffers sont
> du `Transform`) et les brancher dans `Renderer::render_scene` et `Material`, puis exposer `Camera`/ > branchés (frame : view/proj/cam_pos + lumière ; par mesh : `world` dérivé du `Transform`) ; le `Renderer`
> `Transform` à la `Scene` (caméra active) et ajouter un mesh de test (cube) à l'exemple. Objectif MVP : > écrit chaque frame la caméra active (via `Scene::set_camera`/`camera()`) et la matrice monde de chaque
> **un mesh 3D éclairé à l'écran**. > 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 ### 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] `glam = "0.33"` ajouté (`lib/Cargo.toml`) — déjà présent, utilisé par `math/transform.rs` et `resources/camera.rs`
- [x] `slotmap` **retiré** — décision prise : **String IDs pour le MVP** ; slotmap reporté à l'étape "handles typés" (voir Notes de Décision) - [x] `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`) : - [x] Module `math/` / `transform.rs`:
- [ ] Struct `Transform { translation: Vec3, rotation: Quat, scale: Vec3 }` - [x] Struct `Transform { translation: Vec3, rotation: Quat, scale: Vec3 }`
- [ ] Méthode `to_matrix() -> Mat4` pour calculer la matrice locale - [x] Méthode `to_matrix() -> Mat4` pour calculer la matrice locale
- [ ] Struct `Camera { position: Vec3, target: Vec3, up: Vec3 }` : resources/camera.rs - [x] Struct `Camera { position: Vec3, target: Vec3, up: Vec3 }` : resources/camera.rs — enrichi en Étape 4.3 (fov/near/far + `with_perspective`)
- [ ] Fonctions `view_matrix()` et `projection_matrix(fov, aspect, near, far)` - [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) : - [ ] 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 - [ ] Ajouter un mesh de test (cube unitaire) en exemple
### 1.3 Shader Phong Minimal ### 1.3 Shader Phong Minimal
- [ ] Créer `standard_shader.wgsl` : - [x] Créer `standard_shader.wgsl` (Étape 2, 2026-09-16) :
- [ ] Vertex shader : projection * view * world * position - [x] Vertex shader : projection * view * world * position
- [ ] Fragment shader : éclairage hémisphérique + diffuse avec une lumière directionnelle - [x] Fragment shader : éclairage directionnel (+ hémisphérique)
- [ ] Uniforms : `view_matrix`, `proj_matrix`, `world_matrix`, `light_dir`, `light_color` - [x] Uniforms : `view`, `proj`, `cam_pos`, `light_dir`, `light_color`, `options`
- [ ] Mettre à jour `Material` pour supporter les uniforms du shader Phong - [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) ### 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
- [x] Méthodes : `add_mesh()`, `get_mesh()`, `add_material()`, `add_entity()`, `iter_entities()`, `remove_entity()` - [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 - [ ] **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 ### 1.5 Rendu du Prototype
- [ ] Uniform buffer pour la frame : `view_matrix`, `proj_matrix`, `light_dir` - [x] Uniform buffer pour la frame : `view`, `proj`, `cam_pos`, `light_dir` (Étapes 3+4) — écrit chaque frame depuis la caméra active
- [ ] Uniform buffer par mesh : `world_matrix` (calculée sur CPU pour le MVP) - [x] Uniform buffer par mesh : `world` (calculée sur CPU depuis `transform.to_matrix()`, Étape 4.2)
- [ ] `Renderer::render()` itère sur les meshes de la Scene et dessine chacun - [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 - [ ] 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 - [ ] `Scene::iter_entities()` → pour le render loop
### 2.3 Camera dans la Scene ### 2.3 Camera dans la Scene
- [ ] Intégrer `Camera` comme ressource de la Scene - [x] Intégrer `Camera` comme ressource de la Scene (Étape 4.3 : `Scene::set_camera` / `camera()`, caméra active unique)
- [ ] Permettre plusieurs caméras (actuelle/inactive) - [ ] Permettre plusieurs caméras (actuelle/inactive) et une sélection par identifiant (`scene.set_active_camera(camera_id)`)
- [ ] Exposer API : `scene.set_active_camera(camera_id)` - [ ] Exposer une caméra orbitale contrôlable (exemple final, Phase 5)
--- ---
+7 -1
View File
@@ -125,8 +125,14 @@ impl App {
/// Called automatically each frame by the default `AppHandler::render`, or manually by users /// Called automatically each frame by the default `AppHandler::render`, or manually by users
/// who override `render` to control drawing themselves. /// who override `render` to control drawing themselves.
/// Inputs: view — the frame's texture view acting as the color attachment target. /// 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) { 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);
} }
} }
+35 -3
View File
@@ -23,8 +23,9 @@ use crate::core::Frame;
use crate::math::Transform; use crate::math::Transform;
use crate::pipeline::create_uniform_bind_group_layouts; use crate::pipeline::create_uniform_bind_group_layouts;
use crate::resources::uniform::{FRAME_UNIFORMS_SIZE, OBJECT_UNIFORM_SIZE}; use crate::resources::uniform::{FRAME_UNIFORMS_SIZE, OBJECT_UNIFORM_SIZE};
use crate::resources::{FrameUniforms, Material, Mesh, ObjectUniform}; use crate::resources::{Camera, FrameUniforms, Material, Mesh, ObjectUniform};
use crate::scene::Scene; use crate::scene::Scene;
use glam::Vec4;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
@@ -43,6 +44,9 @@ pub struct Renderer {
format: wgpu::TextureFormat, format: wgpu::TextureFormat,
/// Bind group layout for the per-object uniforms (group 1) — must match every pipeline layout. /// Bind group layout for the per-object uniforms (group 1) — must match every pipeline layout.
object_layout: wgpu::BindGroupLayout, 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`). /// Shared per-frame uniform buffer + bind group (camera + lights). Written each frame (`render_scene`).
frame_bind_group: wgpu::BindGroup, frame_bind_group: wgpu::BindGroup,
/// Shared per-object bind group (identity model) used by the low-level `render` path. /// Shared per-object bind group (identity model) used by the low-level `render` path.
@@ -110,12 +114,33 @@ impl Renderer {
device, device,
format, format,
object_layout, object_layout,
frame_buffer,
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()),
} }
} }
/// 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, /// 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). /// 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). /// Inputs: view (TextureView color attachment target), mesh (geometry to render), material (shader+pipeline).
@@ -163,8 +188,15 @@ impl Renderer {
/// This avoids allocating a separate encoder and render pass per entity (which the low-level /// 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 /// `render` does), minimizing GPU submissions. Called automatically each frame by the default
/// `AppHandler::render` through `App::render_scene`. /// `AppHandler::render` through `App::render_scene`.
/// Inputs: view — the frame's texture view color attachment; scene — the scene whose entities are drawn. /// Inputs: view — the frame's texture view color attachment; scene — the scene whose entities are
pub fn render_scene(&self, view: &wgpu::TextureView, scene: &Scene) { /// 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 let mut encoder = self
.device .device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { .create_command_encoder(&wgpu::CommandEncoderDescriptor {
+45 -11
View File
@@ -15,9 +15,18 @@
use glam::{Mat4, Vec3}; 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. /// 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)] #[derive(Debug, Clone)]
pub struct Camera { pub struct Camera {
/// Position of the camera in world space /// Position of the camera in world space
@@ -26,37 +35,62 @@ pub struct Camera {
pub target: Vec3, pub target: Vec3,
/// Up vector defining the camera's orientation /// Up vector defining the camera's orientation
pub up: Vec3, 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 { 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 { pub fn new(position: Vec3, target: Vec3, up: Vec3) -> Self {
Self { Self {
position, position,
target, target,
up, 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. /// Computes the view matrix for this camera.
/// ///
/// # Returns /// # Returns
/// A `Mat4` representing the view transformation matrix /// A `Mat4` representing the view transformation matrix (world → view space)
pub fn view_matrix(&self) -> Mat4 { pub fn view_matrix(&self) -> Mat4 {
glam::camera::rh::view::look_at_mat4(self.position, self.target, self.up) 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 /// # Parameters
/// - `fov`: Field of view in radians /// - `aspect`: Aspect ratio of the viewport (width / height)
/// - `aspect`: Aspect ratio of the viewport
/// - `near`: Near clipping plane distance
/// - `far`: Far clipping plane distance
/// ///
/// # Returns /// # Returns
/// A `Mat4` representing the projection transformation matrix /// A `Mat4` representing the projection transformation matrix (view → clip space)
pub fn projection_matrix(&self, fov: f32, aspect: f32, near: f32, far: f32) -> Mat4 { pub fn projection_matrix(&self, aspect: f32) -> Mat4 {
glam::camera::rh::proj::opengl::perspective(fov, aspect, near, far) glam::camera::rh::proj::opengl::perspective(self.fov, aspect, self.near, self.far)
} }
} }
+24 -3
View File
@@ -11,13 +11,14 @@
//! - **Ergonomie**: Users interact only with entity-level operations (add/remove/get) rather than wgpu buffers/pipelines directly. //! - **Ergonomie**: Users interact only with entity-level operations (add/remove/get) rather than wgpu buffers/pipelines directly.
use crate::math::Transform; use crate::math::Transform;
use crate::resources::{Material, Mesh}; use crate::resources::{Camera, Material, Mesh};
use crate::scene::Entity; use crate::scene::Entity;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
/// Resource depot and entity graph. Stores Meshes and Materials keyed by identifier strings, /// 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. /// Created once during application setup; entities are added before the render loop starts.
pub struct Scene { pub struct Scene {
/// Map of mesh identifiers to owned `Arc<Mesh>` instances. Populated via `add_mesh()`. /// 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>>, materials: HashMap<String, Arc<Material>>,
/// Map of entity labels to `Entity` associations. Populated via `add_entity()` / `add_entity_with_transform()`. /// Map of entity labels to `Entity` associations. Populated via `add_entity()` / `add_entity_with_transform()`.
entities: HashMap<String, Entity>, 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 { 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. /// Called at application startup before any resource registration.
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
meshes: HashMap::new(), meshes: HashMap::new(),
materials: HashMap::new(), materials: HashMap::new(),
entities: 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. /// 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. /// 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. /// Called during scene initialization when building the resource depot.