Files
wsg/lib/examples/cube.rs
T

123 lines
4.4 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**. 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
//! `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::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<Vertex> {
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<u16> {
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) {
let format = app.renderer().format();
// Shader Phong `standard` (porteur des bind groups frame + object) au lieu de `basic`.
app.cache()
.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")
.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 })
}