4a94ad4ac1
- 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.
115 lines
4.6 KiB
Rust
115 lines
4.6 KiB
Rust
//! Étape 5 — MVP 3D : un cube unitaire éclairé qui tourne, rendu automatiquement par la boucle `App`.
|
|
//!
|
|
//! 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**. 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::Geometry;
|
|
use wsg_lib::utils::WsgError;
|
|
|
|
/// Handler de démonstration : fait tourner le cube dans `update`.
|
|
struct Cube {
|
|
/// Angle de rotation cumulé (radians), incrémenté à chaque frame.
|
|
angle: f32,
|
|
}
|
|
|
|
/// 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
|
|
// 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.
|
|
let faces: [([f32; 3], [[f32; 3]; 4]); 6] = [
|
|
(
|
|
[0.0, 0.0, 1.0],
|
|
[[-s, -s, s], [s, -s, s], [s, s, s], [-s, s, s]],
|
|
), // +Z
|
|
(
|
|
[0.0, 0.0, -1.0],
|
|
[[s, -s, -s], [-s, -s, -s], [-s, s, -s], [s, s, -s]],
|
|
), // -Z
|
|
(
|
|
[1.0, 0.0, 0.0],
|
|
[[s, -s, -s], [s, s, -s], [s, s, s], [s, -s, s]],
|
|
), // +X
|
|
(
|
|
[-1.0, 0.0, 0.0],
|
|
[[-s, -s, s], [-s, s, s], [-s, s, -s], [-s, -s, -s]],
|
|
), // -X
|
|
(
|
|
[0.0, 1.0, 0.0],
|
|
[[-s, s, -s], [s, s, -s], [s, s, s], [-s, s, s]],
|
|
), // +Y
|
|
(
|
|
[0.0, -1.0, 0.0],
|
|
[[-s, -s, s], [s, -s, s], [s, -s, -s], [-s, -s, -s]],
|
|
), // -Y
|
|
];
|
|
|
|
let mut positions = Vec::with_capacity(24);
|
|
let mut normals = Vec::with_capacity(24);
|
|
for (normal, corners) in faces {
|
|
for corner in corners {
|
|
positions.push(corner);
|
|
normals.push(normal);
|
|
}
|
|
}
|
|
|
|
// 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]);
|
|
}
|
|
|
|
Geometry::new(positions)
|
|
.with_normals(normals)
|
|
.with_indices(indices)
|
|
}
|
|
|
|
impl AppHandler for Cube {
|
|
fn setup(&mut self, app: &mut wsg_lib::App) {
|
|
// 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();
|
|
app.scene
|
|
.add_material_shader("cube_material", "standard")
|
|
.unwrap();
|
|
app.scene
|
|
.create_mesh("cube_mesh", cube_geometry(), Some("cube_material"))
|
|
.unwrap();
|
|
app.scene.add_entity("cube", "cube_mesh").unwrap();
|
|
}
|
|
|
|
fn update(&mut self, app: &mut wsg_lib::App) {
|
|
// Rotation cumulée du cube (double axe pour un mouvement plus lisible).
|
|
self.angle += 0.02;
|
|
let base = *app
|
|
.scene
|
|
.entity_transform("cube")
|
|
.expect("cube entity present");
|
|
let mut transform = base;
|
|
transform.rotation =
|
|
Quat::from_rotation_y(self.angle) * Quat::from_rotation_x(self.angle * 0.3);
|
|
app.scene.set_entity_transform("cube", transform);
|
|
}
|
|
}
|
|
|
|
#[pollster::main]
|
|
async fn main() -> Result<(), WsgError> {
|
|
let app = AppBuilder::new().title("WSG Cube").build().await?;
|
|
app.run(Cube { angle: 0.0 })
|
|
}
|