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:
+18
-19
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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
@@ -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
@@ -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())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user