//! # Scene Module — Resource Depot and Entity Graph (per ARCHI_APP.md) //! //! Defines `Scene`, the declarative layer of the WSG architecture. Users register resources (Meshes, Materials) by identifier before //! the render loop starts, then associate entities via labels. At runtime, Scene provides immutable access to these resources without exposing raw wgpu handles. //! //! ## Architecture Notes (per ARCHI_APP.md) //! - **La Recette**: Scene is central to the "App" facade workflow. In the Phase de Déclaration, users call add_mesh(), add_material(), and add_entity() //! to build the resource depot. During Phase d'Exécution, Renderer iterates Scene entities for rendering. //! - **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::{Geometry, Transform}; use crate::pipeline::PipelineCache; use crate::resources::{Camera, Lights, Material, Mesh, Texture}; use crate::scene::Entity; use glam::Vec3; 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 + 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` instances. Populated via `add_mesh()`. meshes: HashMap>, /// Map of material identifiers to owned `Arc` instances. Populated via `add_material()`. materials: HashMap>, /// Map of diffuse texture identifiers to owned `Arc` instances (Étape 10, D4). /// Populated via `add_texture()`; materials reference them via `add_material_texture()` by id. textures: HashMap>, /// Map of entity labels to `Entity` associations. Populated via `add_entity()` / `add_entity_with_transform()`. entities: HashMap, /// 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>>, /// Global light list (directional + point), uploaded into the frame uniforms each frame /// (Phase 4.2, Étape 12). Default = one white directional light along +Z (non-regression). lights: Lights, /// Ambient hemisphere color (rgb) used by the `standard` shader. Default = white. ambient: [f32; 3], /// Optional shadow-casting light index (DRAFT Étape 14, D1): the index (in the packed frame /// array: directionals, then points, then spots) of the single light that casts a shadow. /// `None` = shadows off (default, non-régression). Read each frame by `Renderer::render_scene` /// to compute the light `view_proj` and enable shadow sampling. shadow_caster: Option, } 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. 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(), textures: HashMap::new(), entities: HashMap::new(), camera: Camera::default(), gpu: None, default_material: RefCell::new(None), lights: Lights::new(), ambient: [1.0, 1.0, 1.0], shadow_caster: None, } } /// Attaches the GPU-facing pipeline context (device + queue + 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/textures/meshes/entities using `self`. Returns `&mut self` for chaining. /// Inputs: device — shared GPU device (Arc clone); queue — GPU command queue (used to build the /// geometry); format — surface texture output format. pub fn init_gpu( &mut self, device: Arc, queue: wgpu::Queue, format: wgpu::TextureFormat, ) -> &mut Self { let cache = PipelineCache::new(device.clone(), queue.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()) } /// Registers a diffuse texture in the Scene's resource depot under a unique identifier, so /// materials can reference it declaratively (Étape 10, D4). The texture is wrapped in `Arc` for /// zero-copy sharing across materials. Returns Ok(id) or Err(String) if the id already exists. /// Inputs: id (unique identifier), texture (GPU diffuse texture to register). pub fn add_texture(&mut self, id: &str, texture: Texture) -> Result { if self.textures.contains_key(id) { return Err(format!("Texture ID '{}' already exists.", id)); } self.textures.insert(id.to_string(), Arc::new(texture)); Ok(id.to_string()) } /// Retrieves a registered diffuse texture by its identifier, if present. Called by the user to /// read back a texture (or by internals when resolving material↔texture links). Étape 10 (D4). pub fn get_texture(&self, id: &str) -> Option<&Arc> { self.textures.get(id) } /// Builds and registers a Material from a shader id **and** a diffuse texture registered via /// [`Scene::add_texture`]. The material samples `texture_id` (Étape 10, D4). Returns Ok(id) or /// Err(String) if the material id exists or the texture id does not. Inputs: id (material id to /// register), shader_id (pipeline key), texture_id (existing texture id in this Scene). pub fn add_material_texture( &mut self, id: &str, shader_id: &str, texture_id: &str, ) -> Result { if self.materials.contains_key(id) { return Err(format!("Material ID '{}' already exists.", id)); } let texture = self .textures .get(texture_id) .ok_or_else(|| format!("Texture '{}' does not exist.", texture_id))? .clone(); let mut cache = self.gpu().cache.borrow_mut(); let material = Arc::new(Material::new_with_texture( self.gpu().format, shader_id, texture, &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. /// Since Étape 8 the mesh is declared from a CPU `Geometry` (DRAFT Étape 8, D4) instead of raw /// `&[Vertex]`. This builds the shared `Arc` and creates the GPU buffers via /// `Mesh::from_geometry(device, arc, ...)`, 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, geometry: Geometry, material: Option<&str>, ) -> Result { if self.meshes.contains_key(id) { return Err(format!("Mesh ID '{}' already exists.", id)); } let mut mesh = Mesh::from_geometry(self.device(), Arc::new(geometry), None); 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` /// 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 } /// Adds a directional light (direction **from the surface toward the light**, color, intensity). /// Lights are global to the scene and uploaded into the frame uniforms each frame (Phase 4.2, /// Étape 12). Returns `Err` if the scene would exceed `MAX_LIGHTS` (capacity is bounded; no /// dynamic UBO allocation). Inputs: dir (direction toward the light source), color (rgb), /// intensity (multiplier). pub fn add_directional_light( &mut self, dir: Vec3, color: [f32; 3], intensity: f32, ) -> Result<(), String> { if self.lights.len() >= crate::resources::MAX_LIGHTS { return Err(format!( "Cannot add another light: MAX_LIGHTS ({}) reached.", crate::resources::MAX_LIGHTS )); } self.lights .directional .push(crate::resources::lights::directional_light( dir, color, intensity, )); Ok(()) } /// Adds a point light (world position, color, intensity, attenuation radius). /// Returns `Err` if the scene would exceed `MAX_LIGHTS`. Inputs: pos (world position of the /// light), color (rgb), intensity (multiplier), radius (linear falloff to zero at this distance). pub fn add_point_light( &mut self, pos: Vec3, color: [f32; 3], intensity: f32, radius: f32, ) -> Result<(), String> { if self.lights.len() >= crate::resources::MAX_LIGHTS { return Err(format!( "Cannot add another light: MAX_LIGHTS ({}) reached.", crate::resources::MAX_LIGHTS )); } self.lights .point .push(crate::resources::lights::point_light( pos, color, intensity, radius, )); Ok(()) } /// Adds a spot light (world position, cone axis from the light toward the scene, color, /// intensity, attenuation radius, half-angle in radians). Returns `Err` if the scene would /// exceed `MAX_LIGHTS`. Inputs: pos (world position of the light), dir (cone axis, from the /// light toward the scene), color (rgb), intensity (multiplier), radius (linear falloff to /// zero at this distance), half_angle (cone half-angle in radians). pub fn add_spot_light( &mut self, pos: Vec3, dir: Vec3, color: [f32; 3], intensity: f32, radius: f32, half_angle: f32, ) -> Result<(), String> { if self.lights.len() >= crate::resources::MAX_LIGHTS { return Err(format!( "Cannot add another light: MAX_LIGHTS ({}) reached.", crate::resources::MAX_LIGHTS )); } self.lights.spot.push(crate::resources::lights::spot_light( pos, dir, color, intensity, radius, half_angle, )); Ok(()) } /// Replaces the scene's global light list. The `Scene` keeps ownership; the list is uploaded /// into the frame uniforms each frame. Used to reset or bulk-configure lighting. pub fn set_lights(&mut self, lights: Lights) { self.lights = lights; } /// Returns a reference to the scene's global light list (directional + point). /// Read by `Renderer::render_scene` each frame to upload the light array. pub fn lights(&self) -> &Lights { &self.lights } /// Selects the single shadow-casting light by **its index in the packed frame array** /// (directionals first, then point lights, then spots — same order as /// `Lights::into_frame_array`). `None` disables shadows (default, non-régression, Étape 14 D7). /// The light must be **directional or spot**; a point light index disables the shadow pass /// (cubemap shadows are out of scope, D6). Inputs: index — the light's packed-array index, or /// `None` to turn shadows off. pub fn set_shadow_caster(&mut self, index: Option) { self.shadow_caster = index; } /// Returns the index of the scene's shadow-casting light (`None` = shadows off). /// Read by `Renderer::render_scene` each frame to decide whether to run the shadow pass. pub fn shadow_caster(&self) -> Option { self.shadow_caster } /// Removes all lights (directional, point and spot). The fragment shader then contributes /// only the ambient term. Useful for flat look without toggling `unlit`. pub fn clear_lights(&mut self) { self.lights = Lights { directional: Vec::new(), point: Vec::new(), spot: Vec::new(), }; } /// Sets the ambient hemisphere color (rgb). Default is white. pub fn set_ambient(&mut self, color: [f32; 3]) { self.ambient = color; } /// Returns the scene's ambient hemisphere color (rgb). pub fn ambient(&self) -> [f32; 3] { self.ambient } /// 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. pub fn add_mesh(&mut self, id: &str, mesh: Arc) -> Result { if self.meshes.contains_key(id) { return Err(format!("Mesh ID '{}' already exists.", id)); } self.meshes.insert(id.to_string(), mesh); Ok(id.to_string()) } /// Registers a Material in the scene under a unique identifier. /// Inputs: id (unique key), material (Arc-wrapped Material instance). Returns Ok(id) on success or Err(String) if already exists. /// Called during scene initialization when building the resource depot. pub fn add_material(&mut self, id: &str, material: Arc) -> Result { if self.materials.contains_key(id) { return Err(format!("Material ID '{}' already exists.", id)); } self.materials.insert(id.to_string(), material); Ok(id.to_string()) } /// 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) 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 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, transform: Transform, ) -> Result { if !self.meshes.contains_key(mesh_id) { return Err(format!("Mesh '{}' does not exist.", mesh_id)); } self.entities .insert(label.to_string(), Entity::new(mesh_id, transform)); Ok(label.to_string()) } /// Retrieves a Mesh by its registered identifier. /// Called by Renderer during frame rendering to obtain vertex data for draw calls. pub fn get_mesh(&self, id: &str) -> Option<&Arc> { self.meshes.get(id) } /// Retrieves a Material by its registered identifier. /// Called by Renderer during frame rendering to obtain pipeline reference for draw calls. pub fn get_material(&self, id: &str) -> Option<&Arc> { self.materials.get(id) } /// 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, &Transform)> + '_ { self.entities.iter().map(|(label, entity)| { let mesh = self.meshes.get(entity.mesh_id()).unwrap(); // safe: add_entity validates existence (label.as_str(), mesh, entity.transform()) }) } /// Returns a reference to the transform of the entity with the given label, if it exists. /// Called by the user to read an entity's current placement during updates. pub fn entity_transform(&self, label: &str) -> Option<&Transform> { self.entities.get(label).map(|e| e.transform()) } /// Overwrites the transform of the entity with the given label. /// Returns true if the entity existed and was updated, false otherwise. /// Called by the user to move/rotate/scale an entity during `AppHandler::update`. pub fn set_entity_transform(&mut self, label: &str, transform: Transform) -> bool { match self.entities.get_mut(label) { Some(entity) => { entity.set_transform(transform); true } None => false, } } /// Removes an entity from the graph without freeing its underlying resources. /// The referenced Mesh and Material remain registered; only the association is dropped. /// Called during dynamic updates when an entity should be hidden or removed temporarily. pub fn remove_entity(&mut self, label: &str) -> bool { self.entities.remove(label).is_some() } /// Returns the number of registered entities in this scene. /// Called for diagnostic logging or culling decisions (e.g., skip rendering empty scenes). pub fn entity_count(&self) -> usize { self.entities.len() } }