diff --git a/lib/examples/cube.rs b/lib/examples/cube.rs index 039af10..0fcc00e 100644 --- a/lib/examples/cube.rs +++ b/lib/examples/cube.rs @@ -2,14 +2,14 @@ //! //! 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 +//! wgpu**. Depuis l'Étape 7 la scène possède son `PipelineCache` : on passe par `register_shader` + +//! `add_material_shader` + `create_mesh` + `add_entity` (le matériau est lié au mesh, plus de material_id). +//! 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::resources::Vertex; use wsg_lib::utils::WsgError; /// Handler de démonstration : fait tourner le cube dans `update`. @@ -80,25 +80,24 @@ fn cube_indices() -> Vec { 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() + // Shader Phong `standard` (porteur des bind groups frame + object). Depuis l'Étape 7 le + // PipelineCache vit dans la scène : `register_shader` / `add_material_shader` / `create_mesh` + // en sont la façade déclarative (le matériau est lié au mesh au moment du create_mesh). + app.scene .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") + .add_material_shader("cube_material", "standard") .unwrap(); + app.scene + .create_mesh( + "cube_mesh", + &cube_vertices(), + Some(&cube_indices()), + Some("cube_material"), + ) + .unwrap(); + app.scene.add_entity("cube", "cube_mesh").unwrap(); } fn update(&mut self, app: &mut wsg_lib::App) { diff --git a/lib/examples/simple.rs b/lib/examples/simple.rs index 69afdcc..7b59173 100644 --- a/lib/examples/simple.rs +++ b/lib/examples/simple.rs @@ -2,24 +2,23 @@ //! `AppBuilder` crée l'event loop puis `App::run` ouvre la fenêtre, construit le `Context`/`Renderer` //! et fait tourner la boucle update → render → present. Depuis la migration winit 0.30, le GPU n'existe //! qu'après `resumed` : c'est pourquoi l'enregistrement shader + la création mesh/matériau/entité vivent -//! dans le hook `AppHandler::setup`, appelé une fois le contexte prêt. La scène se rend automatiquement : -//! la méthode `render()` par défaut appelle `app.render_scene(frame.view())`. -use std::sync::Arc; +//! dans le hook `AppHandler::setup`, appelé une fois le contexte prêt. Depuis l'Étape 7 le PipelineCache +//! vit dans la scène (`Scene::init_gpu`, appelé dans `resumed`) : on passe par `register_shader` + +//! `add_material_shader` + `create_mesh` + `add_entity`, le matériau étant lié au mesh. La scène se rend +//! automatiquement : la méthode `render()` par défaut appelle `app.render_scene(frame.view())`. use wsg_lib::AppHandler; use wsg_lib::app::AppBuilder; -use wsg_lib::resources::{Material, Mesh, Vertex}; +use wsg_lib::resources::Vertex; use wsg_lib::utils::WsgError; struct MonQuad; impl AppHandler for MonQuad { fn setup(&mut self, app: &mut wsg_lib::App) { - let format = app.renderer().format(); - // 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.scene .register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH) .unwrap(); let vertices = [ @@ -50,20 +49,18 @@ impl AppHandler for MonQuad { ]; let indices: [u16; 6] = [0, 1, 2, 0, 2, 3]; - let mesh = Arc::new(Mesh::new( - app.renderer().device(), - &vertices, - Some(&indices), - )); - let material = Arc::new(Material::new(format, "standard", app.cache())); - - app.scene.add_mesh("quad_mesh", mesh).unwrap(); app.scene - .add_material("standard_material", material) + .add_material_shader("standard_material", "standard") .unwrap(); app.scene - .add_entity("quad", "quad_mesh", "standard_material") + .create_mesh( + "quad_mesh", + &vertices, + Some(&indices), + Some("standard_material"), + ) .unwrap(); + app.scene.add_entity("quad", "quad_mesh").unwrap(); } } diff --git a/lib/src/app.rs b/lib/src/app.rs index ad094cd..e176e93 100644 --- a/lib/src/app.rs +++ b/lib/src/app.rs @@ -8,8 +8,9 @@ //! ## Interaction with Other Modules //! - **core::context**: Consumes GPU hardware lifecycle via Context; acquires frames for rendering. //! - **core::renderer**: Delegates draw call execution to Renderer per frame. -//! - **pipeline::pipeline_cache**: Holds PipelineCache instance for shader/pipeline management. //! - **scene::scene**: Exposes Scene as mutable field so users can register resources and entities. +//! Since Étape 7 the `PipelineCache` lives *inside* the Scene (via `Scene::init_gpu`), owned and +//! used for material building there. //! - **utils::conf**: Provides default window title, dimensions, and embedded WGSL source. //! - **handler**: Defines the AppHandler trait that users implement for custom logic. //! @@ -23,7 +24,6 @@ use crate::AppHandler; use crate::core::{Context, Renderer}; -use crate::pipeline::PipelineCache; use crate::scene::Scene; use crate::utils::WsgError; use crate::utils::conf::{APP_DEFAULT_HEIGHT, APP_DEFAULT_TITLE, APP_DEFAULT_WIDTH}; @@ -38,9 +38,10 @@ use winit::window::{Window, WindowAttributes}; /// Encapsulates all five WGPU objects (Instance, Surface, Adapter, Device, Queue) plus the render loop. /// Users create an App via AppBuilder, then run it with their implementation of AppHandler. /// -/// The GPU-facing fields (`context`, `renderer`, `window`, `cache`) are created lazily when the -/// application is resumed (see `AppRunner`); they are only populated after `App::run` has started. -/// Access them through the `context()`, `renderer()`, `window()` and `cache()` accessors, which is +/// The GPU-facing fields (`context`, `renderer`, `window`) are created lazily when the application is +/// resumed (see `AppRunner`); they are only populated after `App::run` has started. The `PipelineCache` +/// is not a field here: since Étape 7 it lives in the `Scene`'s pipeline context (via `Scene::init_gpu`). +/// Access GPU resources through the `context()`, `renderer()` and `window()` accessors, which are /// guaranteed to work inside `AppHandler::setup`, `update` and `render`. pub struct App { /// Resource depot and entity graph — users register Meshes/Materials here during `AppHandler::setup`. @@ -59,8 +60,6 @@ pub struct App { renderer: Option, /// The OS-level window backing this application. Shared via Arc for multi-owner access. window: Option>, - /// Shader compilation cache — manages RenderPipelines keyed by shader_id. - cache: Option, } impl App { @@ -90,14 +89,6 @@ impl App { .expect("context not initialized yet — call app.run(handler) first") } - /// Returns a mutable reference to the shader compilation cache. - /// Panics if called before `App::run` has created the cache (i.e. before `resumed` fires). - pub fn cache(&mut self) -> &mut PipelineCache { - self.cache - .as_mut() - .expect("cache not initialized yet — call app.run(handler) first") - } - /// Returns a reference to the window backing this application. /// Panics if called before `App::run` has created the window (i.e. before `resumed` fires). pub fn window(&self) -> &Window { @@ -196,7 +187,6 @@ impl AppBuilder { context: None, renderer: None, window: None, - cache: None, }) } } @@ -243,11 +233,15 @@ impl ApplicationHandler for AppRunner { .configure(&context.adapter, self.width, self.height) .expect("Échec configuration surface"); let device = Arc::new(context.device.clone()); - let cache = PipelineCache::new(device); let renderer = Renderer::new(&context, format); + // Étape 7 (DRAFT 7.1) : the PipelineCache now lives in the Scene. We wire the GPU context + // (device + format + cache) into the Scene before setup so it can build materials/meshes. + let mut scene = Scene::new(); + scene.init_gpu(device, format); + let mut app = App { - scene: Scene::new(), + scene, title: self.title.clone(), width: self.width, height: self.height, @@ -255,7 +249,6 @@ impl ApplicationHandler for AppRunner { context: Some(context), renderer: Some(renderer), window: Some(window), - cache: Some(cache), }; // On laisse l'utilisateur enregistrer shaders/meshes/matériaux/entités une fois le GPU prêt. self.handler.setup(&mut app); diff --git a/lib/src/core/renderer.rs b/lib/src/core/renderer.rs index dcd7eb7..f05cf92 100644 --- a/lib/src/core/renderer.rs +++ b/lib/src/core/renderer.rs @@ -248,12 +248,18 @@ impl Renderer { ..Default::default() }); - for (label, mesh, material, transform) in scene.iter_entities() { + // Étape 7 (DRAFT 7.3.4) : the Material is resolved from the Mesh itself, falling back + // to the Scene's default material when the mesh carries none. + for (label, mesh, transform) in scene.iter_entities() { + let material = mesh + .material() + .cloned() + .unwrap_or_else(|| scene.default_material()); let object_bind_group = self.object_bind_group_for(label, transform); draw_entity( &mut render_pass, mesh, - material, + &material, &self.frame_bind_group, &object_bind_group, ); diff --git a/lib/src/resources/mesh.rs b/lib/src/resources/mesh.rs index 9b19aa1..220c98b 100644 --- a/lib/src/resources/mesh.rs +++ b/lib/src/resources/mesh.rs @@ -1,7 +1,8 @@ //! # Mesh Module — Persistent GPU Geometry Container //! //! Defines `Mesh`, a persistent GPU geometry container. Mesh data is uploaded to the GPU once at creation time -//! and remains valid across all frames until dropped. It holds no rendering knowledge—only raw geometric data. +//! and remains valid across all frames until dropped. Since Étape 7 (DRAFT Étape 7.2), a Mesh may also hold a +//! reference to the `Material` that draws it — the appearance lives on the Mesh rather than on the `Entity`. //! //! ## Architecture Notes (per ARCHI_APP.md) //! - **Identifiants**: Each Mesh is registered in Scene by string identifier, enabling dynamic access @@ -9,11 +10,15 @@ //! - **Phase de Déclaration**: Meshes are instantiated once in the declarative phase before the render loop begins. //! - **Performance**: Multiple entities can reference the same Mesh, reducing memory footprint for repeated geometry. +use crate::resources::Material; use crate::resources::vertex::Vertex; +use std::sync::Arc; use wgpu::util::DeviceExt; /// Persistent GPU geometry: vertex positions, optional indices, and draw call counters. /// Created once via `Mesh::new()` during scene setup; referenced by Renderer for every frame. +/// A Mesh optionally references the `Material` used to render it (`Option>`). +/// When `material()` is `None`, the `Scene` supplies its default material at draw time (DRAFT Étape 7.3.5). pub struct Mesh { /// GPU buffer containing vertex attribute data (position, UV, color). pub vertex_buffer: wgpu::Buffer, @@ -23,17 +28,42 @@ pub struct Mesh { pub num_vertices: u32, /// Number of indices in the index buffer. Used as `0..num_indices` for indexed draws. pub num_indices: u32, + /// The Material used to render this mesh. `None` until assigned; the Renderer falls back to the + /// Scene's default material when absent (DRAFT Étape 7.3.5). + material: Option>, } impl Mesh { /// Creates a new Mesh by uploading vertex and optional index data to GPU buffers. /// Inputs: device (GPU command source for buffer creation), vertices (CPU-side vertex array to upload), /// indices (optional CPU-side index array for indexed drawing). - /// Returns a Mesh with two GPU buffers ready for rendering. Called at scene initialization time only. + /// Returns a Mesh with two GPU buffers ready for rendering, without a material (falls back to the + /// Scene default at draw time). Called at scene initialization time only. /// Internal steps: 1) create_buffer_init for vertex data → /// 2) if indices provided: create_buffer_init for index data and set num_indices = len → /// else: set index_buffer = None and num_indices = 0. pub fn new(device: &wgpu::Device, vertices: &[Vertex], indices: Option<&[u16]>) -> Self { + Self::new_inner(device, vertices, indices, None) + } + + /// Creates a new Mesh (as `Mesh::new`) and immediately attaches a Material reference. + /// Convenience helper for the declarative workflow where geometry and appearance are declared together. + pub fn with_material( + device: &wgpu::Device, + vertices: &[Vertex], + indices: Option<&[u16]>, + material: Arc, + ) -> Self { + Self::new_inner(device, vertices, indices, Some(material)) + } + + /// Shared construction helper for `Mesh::new` / `Mesh::with_material`. + fn new_inner( + device: &wgpu::Device, + vertices: &[Vertex], + indices: Option<&[u16]>, + material: Option>, + ) -> Self { let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Mesh Vertex Buffer"), contents: bytemuck::cast_slice(vertices), @@ -56,6 +86,19 @@ impl Mesh { index_buffer, num_vertices: vertices.len() as u32, num_indices, + material, } } + + /// Returns a reference to the Material attached to this mesh, if any. + /// When `None`, the Renderer falls back to the Scene's default material at draw time. + pub fn material(&self) -> Option<&Arc> { + self.material.as_ref() + } + + /// Attaches (or replaces) the Material used to render this mesh. + /// Called by `Scene::create_mesh` during scene setup or by advanced users linking geometry manually. + pub fn set_material(&mut self, material: Arc) { + self.material = Some(material); + } } diff --git a/lib/src/scene/entity.rs b/lib/src/scene/entity.rs index 191de65..680598e 100644 --- a/lib/src/scene/entity.rs +++ b/lib/src/scene/entity.rs @@ -1,18 +1,19 @@ //! # Entity Module //! -//! Defines `Entity`, the renderable association between a Mesh and a Material together with its -//! own world-space `Transform`. Each entry of `Scene::entities` is an `Entity`: it references the -//! resource by identifier while carrying the per-entity placement data. +//! Defines `Entity`, the renderable association between a Mesh and its own world-space `Transform`. +//! Since Étape 7 (DRAFT Étape 7.3) the appearance (Material) lives **on the Mesh**, so an `Entity` only +//! references the mesh by identifier and carries the per-entity placement. Each entry of `Scene::entities` +//! is an `Entity`. //! //! ## Interaction with Other Modules //! - `scene::Scene` stores entities in a `HashMap` keyed by label. //! - `math::Transform` provides the placement (translation / rotation / scale) converted to a //! matrix during rendering. -//! - `resources::{Mesh, Material}` are the referenced render resources, resolved by `Scene`. +//! - `resources::Mesh` is the referenced render resource, resolved by `Scene`; its Material is read by the Renderer. use crate::math::Transform; -/// A renderable entity: a mesh + material pair with its own world-space transform. +/// A renderable entity: a mesh (with its own material) and a world-space transform. /// /// Entities are created through [`crate::scene::Scene::add_entity`] (identity transform) or /// [`crate::scene::Scene::add_entity_with_transform`]. Fields are exposed via accessors. @@ -20,23 +21,16 @@ use crate::math::Transform; pub struct Entity { /// Identifier of the referenced Mesh resource. mesh_id: String, - /// Identifier of the referenced Material resource. - material_id: String, /// World-space placement of this entity. transform: Transform, } impl Entity { - /// Creates a new entity associating a mesh and a material under the given transform. + /// Creates a new entity referencing a mesh under the given transform. /// Called internally by `Scene::add_entity*` after resource existence is validated. - pub fn new( - mesh_id: impl Into, - material_id: impl Into, - transform: Transform, - ) -> Self { + pub fn new(mesh_id: impl Into, transform: Transform) -> Self { Self { mesh_id: mesh_id.into(), - material_id: material_id.into(), transform, } } @@ -46,11 +40,6 @@ impl Entity { &self.mesh_id } - /// Returns the identifier of the referenced Material resource. - pub fn material_id(&self) -> &str { - &self.material_id - } - /// Returns a reference to this entity's world-space transform. pub fn transform(&self) -> &Transform { &self.transform diff --git a/lib/src/scene/scene.rs b/lib/src/scene/scene.rs index 3f6b7b3..6abc112 100644 --- a/lib/src/scene/scene.rs +++ b/lib/src/scene/scene.rs @@ -9,15 +9,35 @@ //! - **Identifiants**: All resource registration uses string identifiers (`Handle`/String pattern), guaranteeing memory safety //! and avoiding borrow checker issues during dynamic updates. //! - **Ergonomie**: Users interact only with entity-level operations (add/remove/get) rather than wgpu buffers/pipelines directly. +//! +//! ## Étape 7 — Pipeline context owned by the Scene (DRAFT Étape 7.1) +//! Since Étape 7 the Scene owns the GPU-facing material pipeline context (`SceneGpu` : device + format + `PipelineCache`) +//! instead of `App`. It can therefore build materials and meshes itself (`add_material_shader`, `create_mesh`) and inject +//! a default material for meshes that carry none (`default_material`). use crate::math::Transform; -use crate::resources::{Camera, Material, Mesh}; +use crate::pipeline::PipelineCache; +use crate::resources::{Camera, Material, Mesh, Vertex}; use crate::scene::Entity; +use std::cell::RefCell; use std::collections::HashMap; use std::sync::Arc; +/// GPU-facing context the Scene needs to build materials and meshes by itself. Held in +/// `Scene.gpu` and populated once by `Scene::init_gpu` after the `Context`/`Renderer` exist +/// (during `AppRunner::resumed`, before `AppHandler::setup`). The `cache` is interior-mutable +/// (`RefCell`) so a default material can be built lazily from an immutable `&Scene` at render time. +struct SceneGpu { + /// Shared GPU device used to create mesh buffers and compile pipelines. + device: Arc, + /// Surface texture output format, required to build fragment pipelines. + format: wgpu::TextureFormat, + /// Shader compilation cache: compiles/caches RenderPipelines keyed by shader_id + format. + cache: RefCell, +} + /// Resource depot and entity graph. Stores Meshes and Materials keyed by identifier strings, -/// maps entity labels to their associated `Entity` (mesh + material + transform) for rendering iteration, +/// maps entity labels to their associated `Entity` (mesh + 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 { @@ -30,21 +50,134 @@ pub struct Scene { /// 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, + /// Owned pipeline context (device + format + cache), `None` until `init_gpu` is called. + gpu: Option, + /// Lazily-built default `standard` material, cached so `default_material` costs O(1) after the + /// first call. Interior-mutable so it can be filled from an immutable `&Scene` (used by the Renderer). + default_material: RefCell>>, } impl Scene { /// 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. The GPU pipeline context is + /// empty (`gpu: None`) until `init_gpu` is called once the `Context`/`Renderer` exist. pub fn new() -> Self { Self { meshes: HashMap::new(), materials: HashMap::new(), entities: HashMap::new(), camera: Camera::default(), + gpu: None, + default_material: RefCell::new(None), } } + /// Attaches the GPU-facing pipeline context (device + format + `PipelineCache`) to this Scene, + /// enabling it to build materials and meshes itself. Called once during `AppRunner::resumed`, + /// just after the `Context`/`Renderer` are created and **before** `AppHandler::setup`, so setup + /// can register shaders/materials/meshes/entities using `self`. Returns `&mut self` for chaining. + /// Inputs: device — shared GPU device (Arc clone); format — surface texture output format. + pub fn init_gpu( + &mut self, + device: Arc, + format: wgpu::TextureFormat, + ) -> &mut Self { + let cache = PipelineCache::new(device.clone()); + self.gpu = Some(SceneGpu { + device, + format, + cache: RefCell::new(cache), + }); + self + } + + /// Returns the owned GPU pipeline context, panicking with a clear message if it has not been + /// initialized yet. Called internally whenever the Scene builds materials/meshes. + fn gpu(&self) -> &SceneGpu { + self.gpu + .as_ref() + .expect("scene pipeline not initialized yet — call Scene::init_gpu once after the GPU context is ready") + } + + /// Returns the Scene's shared GPU device (used to create mesh buffers and compile pipelines). + /// Panics if the Scene's pipeline context has not been initialized (i.e. outside `resumed`). + pub fn device(&self) -> &wgpu::Device { + self.gpu().device.as_ref() + } + + /// Returns the surface texture output format used to build fragment pipelines. + /// Panics if the Scene's pipeline context has not been initialized. + pub fn format(&self) -> wgpu::TextureFormat { + self.gpu().format + } + + /// Registers an external WGSL shader file path under a shader id in the Scene's pipeline cache. + /// Sugar for `cache().register_shader(id, path)` so callers never touch the cache directly. + /// Returns Ok(id) or Err(String) if the id is already registered. + pub fn register_shader(&mut self, id: &str, path: &str) -> Result { + self.gpu().cache.borrow_mut().register_shader(id, path) + } + + /// Builds and registers a Material from a shader id, using the Scene's pipeline context + /// (format + cache). This is the declarative way to declare an appearance without touching + /// `Material::new` or the `PipelineCache` directly. Returns Ok(id) or Err(String) if the id exists. + pub fn add_material_shader(&mut self, id: &str, shader_id: &str) -> Result { + if self.materials.contains_key(id) { + return Err(format!("Material ID '{}' already exists.", id)); + } + let mut cache = self.gpu().cache.borrow_mut(); + let material = Arc::new(Material::new(self.gpu().format, shader_id, &mut cache)); + drop(cache); + self.materials.insert(id.to_string(), material); + Ok(id.to_string()) + } + + /// Builds, (optionally) links to a Material, and registers a Mesh in one declarative call. + /// Creates the GPU buffers via `Mesh::new(device, ...)`, then — if `material` is `Some(name)` — + /// resolves that material id and attaches it to the mesh (`Mesh::set_material`). When `material` + /// is `None`, the mesh carries no material and the Scene's `default_material` is used at draw time. + /// Returns Ok(id) or Err(String) if the id exists or the named material does not. + pub fn create_mesh( + &mut self, + id: &str, + vertices: &[Vertex], + indices: Option<&[u16]>, + material: Option<&str>, + ) -> Result { + if self.meshes.contains_key(id) { + return Err(format!("Mesh ID '{}' already exists.", id)); + } + let mut mesh = Mesh::new(self.device(), vertices, indices); + if let Some(name) = material { + let mat = self + .materials + .get(name) + .ok_or_else(|| format!("Material '{}' does not exist.", name))? + .clone(); + mesh.set_material(mat); + } + self.meshes.insert(id.to_string(), Arc::new(mesh)); + Ok(id.to_string()) + } + + /// Returns the Scene's default material: the `standard` shader pipeline, built lazily on first + /// call and cached afterwards. Used by `Renderer::render_scene` for meshes that carry no material. + /// Note: the flat (unlit) look is *not* a property of this material — it is driven by the + /// orthogonal `Renderer::set_unlit` flag (DRAFT Étape 7.3.5). + pub fn default_material(&self) -> Arc { + if let Some(m) = self.default_material.borrow().as_ref() { + return m.clone(); + } + let material = Arc::new(Material::new( + self.gpu().format, + "standard", + &mut self.gpu().cache.borrow_mut(), + )); + *self.default_material.borrow_mut() = Some(material.clone()); + material + } + /// 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` @@ -82,44 +215,33 @@ impl Scene { Ok(id.to_string()) } - /// Associates an entity label with a mesh and material pair for rendering iteration, using an identity transform. - /// Inputs: label (entity identifier string), mesh_id (key into meshes map), material_id (key into materials map). - /// Returns Ok(label) on success or Err(String) if either referenced resource does not exist. + /// Associates an entity label with a mesh for rendering iteration, using an identity transform. + /// The appearance (Material) is read from the Mesh itself (or the Scene's default), so no + /// material_id is needed here (DRAFT Étape 7.3). + /// Inputs: label (entity identifier string), mesh_id (key into meshes map). + /// Returns Ok(label) on success or Err(String) if the referenced mesh does not exist. /// Called during scene initialization to build the renderable entity graph. - /// Internal steps: 1) validate mesh_id exists → 2) validate material_id exists → - /// 3) insert an `Entity` with identity transform into the entities HashMap. - pub fn add_entity( - &mut self, - label: &str, - mesh_id: &str, - material_id: &str, - ) -> Result { - self.add_entity_with_transform(label, mesh_id, material_id, Transform::identity()) + /// Internal steps: 1) validate mesh_id exists → 2) insert an `Entity` with identity transform. + pub fn add_entity(&mut self, label: &str, mesh_id: &str) -> Result { + self.add_entity_with_transform(label, mesh_id, Transform::identity()) } - /// Associates an entity label with a mesh and material pair together with an explicit world-space transform. - /// Inputs: label (entity identifier string), mesh_id (key into meshes map), material_id (key into materials map), - /// transform (world-space placement). Returns Ok(label) on success or Err(String) if either referenced resource does not exist. - /// Called during scene initialization to build the renderable entity graph. - /// Internal steps: 1) validate mesh_id exists → 2) validate material_id exists → - /// 3) insert the `Entity` into the entities HashMap. + /// Associates an entity label with a mesh together with an explicit world-space transform. + /// Inputs: label (entity identifier string), mesh_id (key into meshes map), + /// transform (world-space placement). Returns Ok(label) on success or Err(String) if the + /// referenced mesh does not exist. Called during scene initialization to build the renderable entity graph. + /// Internal steps: 1) validate mesh_id exists → 2) insert the `Entity` into the entities HashMap. pub fn add_entity_with_transform( &mut self, label: &str, mesh_id: &str, - material_id: &str, transform: Transform, ) -> Result { if !self.meshes.contains_key(mesh_id) { return Err(format!("Mesh '{}' does not exist.", mesh_id)); } - if !self.materials.contains_key(material_id) { - return Err(format!("Material '{}' does not exist.", material_id)); - } - self.entities.insert( - label.to_string(), - Entity::new(mesh_id, material_id, transform), - ); + self.entities + .insert(label.to_string(), Entity::new(mesh_id, transform)); Ok(label.to_string()) } @@ -135,15 +257,14 @@ impl Scene { self.materials.get(id) } - /// Iterates all entity associations, yielding (label, mesh_ref, material_ref, transform_ref) tuples. + /// Iterates all entity associations, yielding (label, mesh_ref, transform_ref) tuples. + /// The Material is **not** yielded here: since Étape 7 it is resolved from the Mesh + /// (`mesh.material()`) or the Scene's default at draw time (DRAFT Étape 7.3.4). /// Called by the orchestrator during each render pass to draw every entity in order. - pub fn iter_entities( - &self, - ) -> impl Iterator, &Arc, &Transform)> + '_ { + pub fn iter_entities(&self) -> impl Iterator, &Transform)> + '_ { self.entities.iter().map(|(label, entity)| { let mesh = self.meshes.get(entity.mesh_id()).unwrap(); // safe: add_entity validates existence - let mat = self.materials.get(entity.material_id()).unwrap(); // same invariant - (label.as_str(), mesh, mat, entity.transform()) + (label.as_str(), mesh, entity.transform()) }) }