refactor(resources): Scene owns pipeline cache, Mesh->Material link

Étape 7 (DRAFT): the PipelineCache moves into the Scene (SceneGpu holds
device + format + cache, wired via Scene::init_gpu in AppRunner::resumed),
so App no longer owns a cache field. Mesh now holds Option<Arc<Material>>
(with material()/set_material()/with_material()); Entity drops material_id
({mesh_id, transform}); iter_entities yields (label, &Mesh, &Transform);
Renderer::render_scene resolves the material from the mesh or the Scene's
lazy default_material(). New declarative Scene helpers: register_shader,
add_material_shader, create_mesh. cube/simple examples migrated; manual
remains low-level and unchanged.
This commit is contained in:
Jérôme Bousquié
2026-09-17 13:15:30 +02:00
parent df546a4ac9
commit 62b5cac11b
7 changed files with 261 additions and 113 deletions
+18 -19
View File
@@ -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<u16> {
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) {
+14 -17
View File
@@ -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();
}
}
+12 -19
View File
@@ -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<Renderer>,
/// The OS-level window backing this application. Shared via Arc for multi-owner access.
window: Option<Arc<Window>>,
/// Shader compilation cache — manages RenderPipelines keyed by shader_id.
cache: Option<PipelineCache>,
}
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<H: AppHandler> ApplicationHandler for AppRunner<H> {
.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<H: AppHandler> ApplicationHandler for AppRunner<H> {
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);
+8 -2
View File
@@ -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,
);
+45 -2
View File
@@ -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<Arc<Material>>`).
/// 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<Arc<Material>>,
}
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<Material>,
) -> 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<Arc<Material>>,
) -> 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<Material>> {
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<Material>) {
self.material = Some(material);
}
}
+8 -19
View File
@@ -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<String, Entity>` 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<String>,
material_id: impl Into<String>,
transform: Transform,
) -> Self {
pub fn new(mesh_id: impl Into<String>, 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
+156 -35
View File
@@ -9,15 +9,35 @@
//! - **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::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<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 + 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<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>>>,
}
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<wgpu::Device>,
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<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())
}
/// 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<String, String> {
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<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`
@@ -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<String, String> {
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<String, String> {
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<String, String> {
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<Item = (&str, &Arc<Mesh>, &Arc<Material>, &Transform)> + '_ {
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
let mat = self.materials.get(entity.material_id()).unwrap(); // same invariant
(label.as_str(), mesh, mat, entity.transform())
(label.as_str(), mesh, entity.transform())
})
}