c2cbd7fadb
Implement shadow mapping for directional lights: - Scene::set_shadow_caster(Option<usize>) selects the shadow-casting light by packed frame-array index (None disables; point lights rejected at render). - Lights::get(index) resolves a packed index across the directional/point/spot lists. - Renderer allocates a shadow depth map, comparison sampler, group-3 bind groups, shadow uniform buffer and shadow pipeline; render_scene does a depth-only shadow pass before the main pass; compute_shadow_light_view_proj builds an orthographic light-space frustum from the scene radius. - standard_shader: shadow_light_index/light_view_proj/shadow_params uniforms, @group(3) depth map + comparison sampler, 3x3 PCF compute_shadow(). - shadow_shader: path/vertex shader with attribute layout matching the shared vertex buffer (only position consumed). - shadow_test example: directional shadow caster casts a PCF-softened shadow onto a ground slab; documented in examples README.
495 lines
24 KiB
Rust
495 lines
24 KiB
Rust
//! # 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<T>`/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<wgpu::Device>,
|
|
/// 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<PipelineCache>,
|
|
}
|
|
|
|
/// 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<Mesh>` instances. Populated via `add_mesh()`.
|
|
meshes: HashMap<String, Arc<Mesh>>,
|
|
/// Map of material identifiers to owned `Arc<Material>` instances. Populated via `add_material()`.
|
|
materials: HashMap<String, Arc<Material>>,
|
|
/// Map of diffuse texture identifiers to owned `Arc<Texture>` instances (Étape 10, D4).
|
|
/// Populated via `add_texture()`; materials reference them via `add_material_texture()` by id.
|
|
textures: HashMap<String, Arc<Texture>>,
|
|
/// 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,
|
|
/// Owned pipeline context (device + format + cache), `None` until `init_gpu` is called.
|
|
gpu: Option<SceneGpu>,
|
|
/// 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<Option<Arc<Material>>>,
|
|
/// 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<usize>,
|
|
}
|
|
|
|
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<wgpu::Device>,
|
|
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<String, String> {
|
|
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<String, String> {
|
|
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<String, String> {
|
|
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<Texture>> {
|
|
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<String, String> {
|
|
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<Geometry>` 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<String, String> {
|
|
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<Material> {
|
|
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<usize>) {
|
|
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<usize> {
|
|
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<Mesh>) -> Result<String, String> {
|
|
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<Material>) -> Result<String, String> {
|
|
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<String, String> {
|
|
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<String, String> {
|
|
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<Mesh>> {
|
|
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<Material>> {
|
|
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<Item = (&str, &Arc<Mesh>, &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()
|
|
}
|
|
}
|