refactor(mesh): stockage CPU Arc<Geometry> (Étape 8, 8.1-8.5)

- geometry: ajoute le champ optionnel colors + constructeur new() et builder
  fluent (.with_normals/.with_uvs/.with_colors/.with_indices) + validate() et
  GeometryError (longueurs des tableaux optionnels + bornes des indices).
- geometry: ajoute to_vertices()/try_into_vertices() -> Vec<Vertex> (zip avec
  défauts : normale [0,0,1], UV [0,0], couleur blanche) (D6).
- mesh: remplace new()/with_material() par from_geometry(device, Arc<Geometry>,
  material); garde geometry: Arc<Geometry> (rétention CPU+GPU, D5) + nouveaux
  accesseurs geometry()/material()/set_material() (D4).
- scene: create_mesh(id, Geometry, Option<&str>) déclare un mesh depuis une
  Geometry; expose Geometry via math (D2) et un ré-export de convenance resources.
- exemples (cube/simple/manual) réécrits pour construire une Geometry.
This commit is contained in:
Jérôme Bousquié
2026-09-18 09:58:19 +02:00
parent 6f6b72ae8d
commit 4a94ad4ac1
8 changed files with 377 additions and 157 deletions
+17 -24
View File
@@ -4,12 +4,14 @@
//! On suit le workflow déclaratif (comme `simple`) : `AppBuilder` + scène automatique, **sans importer
//! 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).
//! Depuis l'Étape 8 (DRAFT 8.4/8.5) le mesh est déclaré à partir d'une **`Geometry`** (positions, normales,
//! indices) plutôt que d'un `&[Vertex]` brut : `Mesh` dérive ses buffers internes via `to_vertices()`.
//! 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 wsg_lib::AppHandler;
use wsg_lib::app::AppBuilder;
use wsg_lib::resources::Vertex;
use wsg_lib::resources::Geometry;
use wsg_lib::utils::WsgError;
/// Handler de démonstration : fait tourner le cube dans `update`.
@@ -18,12 +20,11 @@ struct Cube {
angle: f32,
}
/// Génère les sommets d'un cube unitaire centré à l'origine (arête de 1), une normale par face.
/// 24 sommets (4 par face) + 36 indices ; la couleur est blanche, l'UV est laissé à zéro (inutilisé
/// par `standard` pour un matériau sans texture).
fn cube_vertices() -> Vec<Vertex> {
/// Construit la `Geometry` d'un cube unitaire centré à l'origine (arête de 1), une normale par face.
/// 24 sommets (4 par face) + 36 indices ; la couleur est absente (défaut blanc opaque via
/// `Geometry::to_vertices`), l'UV est laissé à zéro (inutilisé par `standard` pour un matériau sans texture).
fn cube_geometry() -> Geometry {
let s = 0.5; // demi-arête
let color = [1.0, 1.0, 1.0, 1.0];
// Chaque face : (normale sortante, 4 coins). Le culling est désactivé par défaut (PrimitiveState
// par défaut), donc l'ordre d'enroulement n'affecte pas la visibilité ; seules les normales comptent
// pour l'éclairage.
@@ -54,28 +55,25 @@ fn cube_vertices() -> Vec<Vertex> {
), // -Y
];
let mut verts = Vec::with_capacity(24);
let mut positions = Vec::with_capacity(24);
let mut normals = Vec::with_capacity(24);
for (normal, corners) in faces {
for corner in corners {
verts.push(Vertex {
position: corner,
normal,
uv: [0.0, 0.0],
color,
});
positions.push(corner);
normals.push(normal);
}
}
verts
}
/// Génère les indices d'un cube à partir de ses 24 sommets (2 triangles par face, 36 indices).
fn cube_indices() -> Vec<u16> {
// 2 triangles par face, 36 indices.
let mut indices = Vec::with_capacity(36);
for face in 0..6u16 {
let b = face * 4;
indices.extend_from_slice(&[b, b + 1, b + 2, b, b + 2, b + 3]);
}
indices
Geometry::new(positions)
.with_normals(normals)
.with_indices(indices)
}
impl AppHandler for Cube {
@@ -90,12 +88,7 @@ impl AppHandler for Cube {
.add_material_shader("cube_material", "standard")
.unwrap();
app.scene
.create_mesh(
"cube_mesh",
&cube_vertices(),
Some(&cube_indices()),
Some("cube_material"),
)
.create_mesh("cube_mesh", cube_geometry(), Some("cube_material"))
.unwrap();
app.scene.add_entity("cube", "cube_mesh").unwrap();
}
+22 -34
View File
@@ -1,7 +1,9 @@
//! Workflow bas-niveau : utilisation directe du `Context`, `Renderer`, `PipelineCache`, `Mesh` et
//! `Material`, contournant la façade `App`. Rendu d'un quad plat (shader `standard` **unlit**) via la
//! boucle winit 0.30 (`EventLoop::run_app` + `ApplicationHandler`). La fenêtre et le GPU sont créés
//! dans `resumed()`, comme l'exigent winit 0.30 et la migration faite dans `app.rs`.
//! dans `resumed()`, comme l'exigent winit 0.30 et la migration faite dans `app.rs`. Depuis l'Étape 8
//! (DRAFT 8.5) le mesh est construit via `Mesh::from_geometry(device, Arc<Geometry>, None)` à partir
//! d'une `Geometry` (positions + couleurs par sommet) au lieu de `Mesh::new(device, &[Vertex], ..)`.
use std::sync::Arc;
use winit::application::ApplicationHandler;
use winit::dpi::LogicalSize;
@@ -12,9 +14,7 @@ use wsg_lib::core::Context;
use wsg_lib::core::Frame;
use wsg_lib::core::Renderer;
use wsg_lib::pipeline::PipelineCache;
use wsg_lib::resources::Material;
use wsg_lib::resources::Mesh;
use wsg_lib::resources::Vertex;
use wsg_lib::resources::{Geometry, Material, Mesh};
use wsg_lib::utils;
/// Application bas-niveau : détient les objets GPU + window, tous créés dans `resumed`.
@@ -70,36 +70,24 @@ impl ApplicationHandler for App {
// 3. Material : On utilise renderer.device() et renderer.format()
let material = Material::new(renderer.format(), "standard", &mut cache);
// Mesh : On utilise le device du renderer
let vertices = [
// Position (x,y,z) | Normale (x,y,z) | UV (u,v) | Couleur (r,g,b,a)
Vertex {
position: [-0.5, 0.5, 0.0],
normal: [0.0, 0.0, 1.0],
uv: [0.0, 0.0],
color: [1.0, 0.0, 0.0, 1.0],
}, // Haut-Gauche (Rouge)
Vertex {
position: [0.5, 0.5, 0.0],
normal: [0.0, 0.0, 1.0],
uv: [1.0, 0.0],
color: [0.0, 1.0, 0.0, 1.0],
}, // Haut-Droite (Vert)
Vertex {
position: [0.5, -0.5, 0.0],
normal: [0.0, 0.0, 1.0],
uv: [1.0, 1.0],
color: [0.0, 0.0, 1.0, 1.0],
}, // Bas-Droite (Bleu)
Vertex {
position: [-0.5, -0.5, 0.0],
normal: [0.0, 0.0, 1.0],
uv: [0.0, 1.0],
color: [1.0, 1.0, 0.0, 1.0],
}, // Bas-Gauche (Jaune)
];
let indices: [u16; 6] = [0, 1, 2, 0, 2, 3];
let mesh = Mesh::new(renderer.device(), &vertices, Some(&indices));
// Mesh : on utilise le device du renderer. Depuis l'Étape 8 le mesh est construit depuis une
// `Geometry` (positions + couleurs par sommet) via `Mesh::from_geometry` — le mesh garde aussi
// l'`Arc<Geometry>` côté CPU (rétention D5).
let geometry = Geometry::new(vec![
// Position (x,y,z) | Couleur (r,g,b,a) — normales/UV par défaut via to_vertices
[-0.5, 0.5, 0.0],
[0.5, 0.5, 0.0],
[0.5, -0.5, 0.0],
[-0.5, -0.5, 0.0],
])
.with_colors(vec![
[1.0, 0.0, 0.0, 1.0], // Haut-Gauche (Rouge)
[0.0, 1.0, 0.0, 1.0], // Haut-Droite (Vert)
[0.0, 0.0, 1.0, 1.0], // Bas-Droite (Bleu)
[1.0, 1.0, 0.0, 1.0], // Bas-Gauche (Jaune)
])
.with_indices(vec![0, 1, 2, 0, 2, 3]);
let mesh = Mesh::from_geometry(renderer.device(), Arc::new(geometry), None);
self.window = Some(window);
self.context = Some(context);
+23 -41
View File
@@ -4,11 +4,13 @@
//! 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. 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())`.
//! `add_material_shader` + `create_mesh` + `add_entity`, le matériau étant lié au mesh. Depuis l'Étape 8
//! (DRAFT 8.4/8.5) le mesh est déclaré à partir d'une **`Geometry`** : positions + couleurs par sommet
//! pour le quad unlit. 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::Vertex;
use wsg_lib::resources::Geometry;
use wsg_lib::utils::WsgError;
struct MonQuad;
@@ -21,45 +23,25 @@ impl AppHandler for MonQuad {
app.scene
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
.unwrap();
let vertices = [
Vertex {
position: [-0.5, 0.5, 0.0],
normal: [0.0, 0.0, 1.0],
uv: [0.0, 0.0],
color: [1.0, 0.0, 0.0, 1.0],
}, // Haut-Gauche (Rouge)
Vertex {
position: [0.5, 0.5, 0.0],
normal: [0.0, 0.0, 1.0],
uv: [1.0, 0.0],
color: [0.0, 1.0, 0.0, 1.0],
}, // Haut-Droite (Vert)
Vertex {
position: [0.5, -0.5, 0.0],
normal: [0.0, 0.0, 1.0],
uv: [1.0, 1.0],
color: [0.0, 0.0, 1.0, 1.0],
}, // Bas-Droite (Bleu)
Vertex {
position: [-0.5, -0.5, 0.0],
normal: [0.0, 0.0, 1.0],
uv: [0.0, 1.0],
color: [1.0, 1.0, 0.0, 1.0],
}, // Bas-Gauche (Jaune)
];
let indices: [u16; 6] = [0, 1, 2, 0, 2, 3];
app.scene
.add_material_shader("standard_material", "standard")
.unwrap();
app.scene
.create_mesh(
"quad_mesh",
&vertices,
Some(&indices),
Some("standard_material"),
)
.unwrap();
let geometry = Geometry::new(vec![
[-0.5, 0.5, 0.0], // Haut-Gauche
[0.5, 0.5, 0.0], // Haut-Droite
[0.5, -0.5, 0.0], // Bas-Droite
[-0.5, -0.5, 0.0], // Bas-Gauche
])
.with_normals(vec![[0.0, 0.0, 1.0]; 4])
.with_colors(vec![
[1.0, 0.0, 0.0, 1.0], // Haut-Gauche (Rouge)
[0.0, 1.0, 0.0, 1.0], // Haut-Droite (Vert)
[0.0, 0.0, 1.0, 1.0], // Bas-Droite (Bleu)
[1.0, 1.0, 0.0, 1.0], // Bas-Gauche (Jaune)
])
.with_indices(vec![0, 1, 2, 0, 2, 3]);
// Material par défaut : `None` laisse la Scene injecter son `standard` au rendu
// (`Scene::default_material`, DRAFT Étape 7.3.5) — on vérifie le chemin par défaut.
app.scene.create_mesh("quad_mesh", geometry, None).unwrap();
app.scene.add_entity("quad", "quad_mesh").unwrap();
}
}
+251 -8
View File
@@ -4,25 +4,268 @@
//! This module handles the core geometric representation used by meshes.
//!
//! ## Usage
//! - Stores vertex attributes (positions, normals, UVs)
//! - Stores vertex attributes (positions, normals, UVs, colors)
//! - Used by `Mesh` to define its vertex data
//! - Passed to shaders for rendering
//! - Converted to `resources::Vertex` buffers via `to_vertices()`
//!
//! ## Related Types
//! - `Geometry`: Main struct for vertex data storage
//! - Fields: positions, normals, uvs, indices
//! - `GeometryError`: Validation errors for inconsistent per-vertex arrays
//! - Fields: positions, normals, uvs, colors, indices
//!
//! ## Invariants
//! `positions` is required and non-empty. The optional per-vertex attributes
//! (`normals`, `uvs`, `colors`), when present, must have exactly the same length
//! as `positions`. `indices`, when present, must reference valid vertex indices.
//! Call `Geometry::validate()` (or `try_into_vertices()`) to enforce these before
//! uploading to the GPU.
use crate::resources::Vertex;
/// Validation error produced when a `Geometry` is inconsistent, i.e. its optional
/// per-vertex arrays (`normals`, `uvs`, `colors`) have a length different from
/// `positions`, or an index is out of bounds.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GeometryError {
/// No positions defined; a valid geometry must have at least one vertex.
EmptyPositions,
/// The normals array length differs from the positions array length.
NormalCountMismatch {
/// Number of positions.
positions: usize,
/// Number of normals.
normals: usize,
},
/// The UV array length differs from the positions array length.
UvCountMismatch {
/// Number of positions.
positions: usize,
/// Number of UVs.
uvs: usize,
},
/// The color array length differs from the positions array length.
ColorCountMismatch {
/// Number of positions.
positions: usize,
/// Number of colors.
colors: usize,
},
/// An index references a vertex that does not exist.
IndexOutOfBounds {
/// The offending index value.
index: u16,
/// Number of vertices in the geometry.
vertex_count: usize,
},
}
impl std::fmt::Display for GeometryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GeometryError::EmptyPositions => {
write!(
f,
"geometry has no positions; at least one vertex is required"
)
}
GeometryError::NormalCountMismatch { positions, normals } => {
write!(
f,
"normal count ({normals}) does not match position count ({positions})"
)
}
GeometryError::UvCountMismatch { positions, uvs } => {
write!(
f,
"UV count ({uvs}) does not match position count ({positions})"
)
}
GeometryError::ColorCountMismatch { positions, colors } => write!(
f,
"color count ({colors}) does not match position count ({positions})"
),
GeometryError::IndexOutOfBounds {
index,
vertex_count,
} => write!(
f,
"index {index} is out of bounds (vertex count is {vertex_count})"
),
}
}
}
impl std::error::Error for GeometryError {}
/// Represents the geometric data of a 3D mesh.
///
/// This struct stores the core vertex attributes that define a mesh's shape.
/// This struct stores the core vertex attributes that define a mesh's shape as
/// *scattered per-attribute arrays* (one Vec per attribute), as opposed to the
/// interleaved `resources::Vertex` layout used for GPU upload.
///
/// Construct a `Geometry` ergonomically with [`Geometry::new`] plus the fluent
/// builder methods ([`Geometry::with_normals`], [`Geometry::with_uvs`],
/// [`Geometry::with_colors`], [`Geometry::with_indices`]). Validate it with
/// [`Geometry::validate`], then produce GPU-ready vertices via
/// [`Geometry::to_vertices`].
#[derive(Debug, Clone)]
pub struct Geometry {
/// Vertex positions as an array of 3D coordinates
/// Vertex positions as an array of 3D coordinates. Required, non-empty.
pub positions: Vec<[f32; 3]>,
/// Optional vertex normals for lighting calculations
/// Optional vertex normals for lighting calculations. When present, must have
/// the same length as `positions`.
pub normals: Option<Vec<[f32; 3]>>,
/// Optional texture coordinates for UV mapping
/// Optional texture coordinates for UV mapping. When present, must have the
/// same length as `positions`.
pub uvs: Option<Vec<[f32; 2]>>,
/// Optional indices for indexed rendering
/// Optional per-vertex colors (RGBA). When present, must have the same length
/// as `positions`. Absent colors default to opaque white.
pub colors: Option<Vec<[f32; 4]>>,
/// Optional indices for indexed rendering. Each index must be `< positions.len()`.
pub indices: Option<Vec<u16>>,
}
impl Geometry {
/// Creates a geometry from vertex positions only; all optional attributes
/// (normals, UVs, colors, indices) start as `None`.
/// Chain builder methods to populate them:
/// ```
/// # use wsg_lib::math::Geometry;
/// let geo = Geometry::new(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]])
/// .with_normals(vec![[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]])
/// .with_indices(vec![0, 1]);
/// ```
pub fn new(positions: Vec<[f32; 3]>) -> Self {
Self {
positions,
normals: None,
uvs: None,
colors: None,
indices: None,
}
}
/// Sets the vertex normals and returns the geometry for chaining.
pub fn with_normals(mut self, normals: Vec<[f32; 3]>) -> Self {
self.normals = Some(normals);
self
}
/// Sets the texture coordinates and returns the geometry for chaining.
pub fn with_uvs(mut self, uvs: Vec<[f32; 2]>) -> Self {
self.uvs = Some(uvs);
self
}
/// Sets the per-vertex colors (RGBA) and returns the geometry for chaining.
pub fn with_colors(mut self, colors: Vec<[f32; 4]>) -> Self {
self.colors = Some(colors);
self
}
/// Sets the index buffer and returns the geometry for chaining.
pub fn with_indices(mut self, indices: Vec<u16>) -> Self {
self.indices = Some(indices);
self
}
/// Validates the geometry invariants:
/// - `positions` is non-empty;
/// - each optional per-vertex array, when present, has the same length as `positions`;
/// - each index, when present, references an existing vertex.
///
/// Returns `Ok(())` or a [`GeometryError`] describing the first inconsistency found.
pub fn validate(&self) -> Result<(), GeometryError> {
let count = self.positions.len();
if count == 0 {
return Err(GeometryError::EmptyPositions);
}
if let Some(normals) = &self.normals {
if normals.len() != count {
return Err(GeometryError::NormalCountMismatch {
positions: count,
normals: normals.len(),
});
}
}
if let Some(uvs) = &self.uvs {
if uvs.len() != count {
return Err(GeometryError::UvCountMismatch {
positions: count,
uvs: uvs.len(),
});
}
}
if let Some(colors) = &self.colors {
if colors.len() != count {
return Err(GeometryError::ColorCountMismatch {
positions: count,
colors: colors.len(),
});
}
}
if let Some(indices) = &self.indices {
for &index in indices {
if index as usize >= count {
return Err(GeometryError::IndexOutOfBounds {
index,
vertex_count: count,
});
}
}
}
Ok(())
}
/// Returns the optional index slice, when present.
/// Helper used by `Mesh` to upload the index buffer.
pub fn indices(&self) -> Option<&[u16]> {
self.indices.as_deref()
}
/// Returns the CPU vertices in interleaved `resources::Vertex` layout, suitable
/// for GPU upload. Assumes the geometry is valid; missing per-vertex attributes
/// are filled with defaults:
/// - position: required (must be present);
/// - normal: `[0, 0, 1]` (a geometry without normals lit by Phong will appear flat);
/// - uv: `[0, 0]`;
/// - color: `[1, 1, 1, 1]` (opaque white).
///
/// For a checked conversion that rejects inconsistent arrays, use
/// [`Geometry::try_into_vertices`].
pub fn to_vertices(&self) -> Vec<Vertex> {
self.positions
.iter()
.enumerate()
.map(|(i, &position)| Vertex {
position,
normal: self
.normals
.as_ref()
.and_then(|n| n.get(i))
.copied()
.unwrap_or([0.0, 0.0, 1.0]),
uv: self
.uvs
.as_ref()
.and_then(|u| u.get(i))
.copied()
.unwrap_or([0.0, 0.0]),
color: self
.colors
.as_ref()
.and_then(|c| c.get(i))
.copied()
.unwrap_or([1.0, 1.0, 1.0, 1.0]),
})
.collect()
}
/// Validates the geometry and, on success, returns the interleaved CPU vertices
/// (see [`Geometry::to_vertices`]). Returns a [`GeometryError`] describing the
/// first inconsistency if the geometry is invalid.
pub fn try_into_vertices(&self) -> Result<Vec<Vertex>, GeometryError> {
self.validate()?;
Ok(self.to_vertices())
}
}
+1 -1
View File
@@ -19,5 +19,5 @@ pub mod geometry;
pub mod transform;
// Re-exports
pub use geometry::Geometry;
pub use geometry::{Geometry, GeometryError};
pub use transform::Transform;
+43 -36
View File
@@ -1,25 +1,38 @@
//! # 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. 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`.
//! and remains valid across all frames until dropped. Since Étape 8 (DRAFT Étape 8.3), a Mesh also retains the
//! CPU geometry it was built from (`geometry: Arc<Geometry>`), giving meshes a shared, readable source of truth
//! for phases such as bounding-box culling and UV access. Since Étape 7, 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
//! during the render loop without borrow checker issues. The identifier serves as the `Handle<T>` key.
//! - **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.
//! - **Rétention CPU+GPU (DRAFT Étape 8, D5)**: `geometry` (CPU) and the vertex/index buffers (GPU) coexist.
//! The GPU buffers are uploaded once at creation; the `Arc<Geometry>` is kept for CPU-side computations
//! without re-uploading per frame.
//!
//! ## Construction (DRAFT Étape 8, D4)
//! The single canonical constructor is [`Mesh::from_geometry`]. The former `Mesh::new`/`Mesh::with_material`
//! (which took raw `&[Vertex]`) were removed in Étape 8: the `Scene` declares meshes from a `Geometry`, and
//! `Mesh` derives its interleaved vertices internally via `Geometry::to_vertices()`.
use crate::math::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>>`).
/// Persistent GPU geometry: vertex positions, optional indices, draw call counters, and the retained
/// CPU `Geometry` (Étape 8). Created once via `Mesh::from_geometry()` 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 {
/// Shared CPU geometry this mesh was built from (Étape 8, D5). Retained for CPU-side computation
/// (bounding boxes, UV access, normal queries) and shared across meshes with identical geometry.
geometry: Arc<Geometry>,
/// GPU buffer containing vertex attribute data (position, UV, color).
pub vertex_buffer: wgpu::Buffer,
/// Optional GPU buffer for indexed drawing. Present when the mesh uses index-based rendering instead of simple vertex iteration.
@@ -34,44 +47,30 @@ pub struct Mesh {
}
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, 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(
/// Canonical constructor (Étape 8, D4): builds GPU buffers from a shared CPU `Geometry`.
///
/// Inputs: device (GPU command source for buffer creation), geometry (shared CPU vertex data to
/// upload), material (optional appearance; `None` falls back to the Scene default at draw time).
///
/// Internal steps: 1) derive interleaved `Vertex` array via `geometry.to_vertices()`; 2) create the
/// vertex buffer (one `Vertex` per position); 3) if the geometry has indices, create the index buffer
/// and set `num_indices`, else leave it `None`.
///
/// The provided `geometry` is retained on the mesh (`geometry` accessor) alongside the uploaded GPU
/// buffers, so the CPU data remains readable for later phases without re-uploading each frame (DRAFT Étape 8, D5).
pub fn from_geometry(
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]>,
geometry: Arc<Geometry>,
material: Option<Arc<Material>>,
) -> Self {
let vertices = geometry.to_vertices();
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Mesh Vertex Buffer"),
contents: bytemuck::cast_slice(vertices),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
// Create optional index buffer and count indices if provided
let (index_buffer, num_indices) = if let Some(data) = indices {
let (index_buffer, num_indices) = if let Some(data) = geometry.indices() {
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Mesh Index Buffer"),
usage: wgpu::BufferUsages::INDEX,
@@ -81,7 +80,9 @@ impl Mesh {
} else {
(None, 0)
};
Self {
geometry,
vertex_buffer,
index_buffer,
num_vertices: vertices.len() as u32,
@@ -90,6 +91,12 @@ impl Mesh {
}
}
/// Returns a reference to the shared CPU geometry this mesh was built from (Étape 8, D5).
/// Read-only accessor for CPU-side queries (bounding boxes, UVs, normals).
pub fn geometry(&self) -> &Arc<Geometry> {
&self.geometry
}
/// 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>> {
+11 -5
View File
@@ -1,13 +1,15 @@
//! # Resources Module — Data Types
//!
//! Defines the three core data types that flow through the rendering pipeline: **Vertex** (CPU-side per-attribute
//! tuple), **Mesh** (GPU geometry container with vertex/index buffers), and **Material** (appearance descriptor
//! pairing shader ID with a compiled RenderPipeline). These are immutable after creation and consumed by Renderer
//! for draw calls.
//! Defines the core data types that flow through the rendering pipeline: **Geometry** (CPU-side scattered
//! vertex data, source of truth — re-exported here from `math` for convenience), **Vertex** (interleaved
//! CPU-side per-attribute tuple, the GPU upload contract), **Mesh** (GPU geometry container with vertex/index
//! buffers), and **Material** (appearance descriptor pairing shader ID with a compiled RenderPipeline).
//! These are immutable after creation and consumed by Renderer for draw calls.
//!
//! ## Interaction with Other Modules
//! - `pipeline_cache::build_pipeline()` reads Vertex field offsets to construct the vertex buffer layout.
//! - `mesh::new()` uploads Vertex arrays from CPU memory into GPU vertex buffers via DeviceExt::create_buffer_init().
//! - `mesh::from_geometry()` derives `Vertex` arrays from a `Geometry` and uploads them into GPU vertex
//! buffers via DeviceExt::create_buffer_init().
//! - `material::new()` requests RenderPipelines from PipelineCache during scene initialization.
pub mod camera;
@@ -22,3 +24,7 @@ pub use material::Material;
pub use mesh::Mesh;
pub use uniform::{FrameUniforms, ObjectUniform};
pub use vertex::Vertex;
// Convenience re-export of `math::Geometry` (Étape 8, D2) so examples can build meshes
// from `wsg_lib::resources::Geometry` without importing `math` separately.
pub use crate::math::Geometry;
+9 -8
View File
@@ -15,9 +15,9 @@
//! 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::math::{Geometry, Transform};
use crate::pipeline::PipelineCache;
use crate::resources::{Camera, Material, Mesh, Vertex};
use crate::resources::{Camera, Material, Mesh};
use crate::scene::Entity;
use std::cell::RefCell;
use std::collections::HashMap;
@@ -134,21 +134,22 @@ impl Scene {
}
/// 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.
/// 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,
vertices: &[Vertex],
indices: Option<&[u16]>,
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::new(self.device(), vertices, indices);
let mut mesh = Mesh::from_geometry(self.device(), Arc::new(geometry), None);
if let Some(name) = material {
let mat = self
.materials