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:
+17
-24
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user