//! É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). //! 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::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, } /// 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 { 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. 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 verts = 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, }); } } verts } /// Génère les indices d'un cube à partir de ses 24 sommets (2 triangles par face, 36 indices). fn cube_indices() -> Vec { 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 } 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_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) { // 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 }) }