//! **WSG `demo`** — the final showcase example (Étape 15, sous-volt 15.C). //! //! Combines everything built throughout the library into one declarative scene: //! //! * a **ground plane** plus one of each procedural primitive from `math::primitives` //! (`cube`, `uv_sphere`, `icosphere`, `cylinder`, `cone`, `torus`) placed around it, //! * a **procedural texture** per mesh (checker / stripe grids, no assets on disk), //! * the **standard** Phong material wired to those textures, //! * an **orbital camera** driven live by the unified input state (Étape 15.B): //! moving the mouse orbits (yaw/pitch), the wheel zooms (distance), //! * `R` resets the view, keys `1`/`2`/`3` jump to front / side / top presets, //! * a **directional** light (the shadow caster) + a **point** light + a **spot** light, //! so the shadow of the cube and the colored light halos are all visible, //! * the primitives slowly rotate in `update`, so depth, lighting and shadows read clearly. //! //! Doc (this header) follows the English convention used for examples; internal comments stay //! concise and French where helpful. Run with: //! //! `cargo run -p wsg-lib --example demo` use glam::{Quat, Vec3}; use winit::keyboard::KeyCode; use wsg_lib::AppHandler; use wsg_lib::app::AppBuilder; use wsg_lib::math::{Transform, cone, cube, cylinder, icosphere, plane, torus, uv_sphere}; use wsg_lib::resources::{CameraController, Texture}; use wsg_lib::utils::WsgError; /// Generates an 8×8 RGBA checkerboard (white / brick) as raw bytes for `Texture::from_rgba8`. fn checkerboard_rgba() -> Vec { const SIZE: u32 = 8; let mut rgba = Vec::with_capacity((SIZE * SIZE * 4) as usize); for y in 0..SIZE { for x in 0..SIZE { let even = (x + y) % 2 == 0; let (r, g, b) = if even { (235, 235, 228) } else { (150, 90, 70) }; rgba.extend_from_slice(&[r, g, b, 255]); } } rgba } /// Generates a vertical stripe texture (blue / cyan), useful to make rotation visible on rounded /// bodies (sphere / cylinder) via the UV seams. fn stripes_rgba() -> Vec { const W: u32 = 32; const H: u32 = 16; let mut rgba = Vec::with_capacity((W * H * 4) as usize); for _y in 0..H { for x in 0..W { let band = (x / 4) % 2 == 0; let (r, g, b) = if band { (40, 90, 190) } else { (120, 210, 235) }; rgba.extend_from_slice(&[r, g, b, 255]); } } rgba } /// Demo handler: holds the orbital controller plus a slow rotation angle. struct Demo { camera: CameraController, angle: f32, } /// Horizontal radius at which the primitives sit around the origin. const ORBIT_RADIUS: f32 = 1.7; /// Vertical offset so the meshes stand on the ground plane (y = 0). const STAND_HEIGHT: f32 = 0.5; /// Lays out one primitive (already scaled/positioned) at an angle around the origin. fn place(label: &str, mesh: &str, app: &mut wsg_lib::App, index: usize) { let a = index as f32 / 6.0 * std::f32::consts::TAU; let mut tf = Transform::identity(); tf.translation = Vec3::new(a.cos() * ORBIT_RADIUS, STAND_HEIGHT, a.sin() * ORBIT_RADIUS); tf.rotation = Quat::from_rotation_y(a); // face the center app.scene .add_entity_with_transform(label, mesh, tf) .unwrap(); } impl AppHandler for Demo { fn setup(&mut self, app: &mut wsg_lib::App) { // 1. Shader + material base. app.scene .register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH) .unwrap(); let (device, queue) = { let ctx = app.context(); (ctx.device.clone(), ctx.queue.clone()) }; // 2. Procedural textures, one material per pattern. let checker = Texture::from_rgba8(&device, &queue, 8, 8, &checkerboard_rgba(), "checker").unwrap(); app.scene.add_texture("checker_texture", checker).unwrap(); app.scene .add_material_texture("ground_mat", "standard", "checker_texture") .unwrap(); app.scene .add_material_texture("solid_mat", "standard", "checker_texture") .unwrap(); let stripes = Texture::from_rgba8(&device, &queue, 32, 16, &stripes_rgba(), "stripes").unwrap(); app.scene.add_texture("stripes_texture", stripes).unwrap(); app.scene .add_material_texture("stripes_mat", "standard", "stripes_texture") .unwrap(); // 3. Ground plane (large, thin, textured). app.scene .create_mesh("ground_mesh", plane(9.0, 9.0, 1, 1), Some("ground_mat")) .unwrap(); app.scene.add_entity("ground", "ground_mesh").unwrap(); // 4. One mesh per primitive, each assigned to a textured (or stripe) material. app.scene .create_mesh("cube_mesh", cube(0.8), Some("solid_mat")) .unwrap(); app.scene .create_mesh("sphere_mesh", uv_sphere(0.55, 32, 20), Some("stripes_mat")) .unwrap(); app.scene .create_mesh("ico_mesh", icosphere(0.5, 2), Some("solid_mat")) .unwrap(); app.scene .create_mesh("cyl_mesh", cylinder(0.4, 0.9, 32), Some("stripes_mat")) .unwrap(); app.scene .create_mesh("cone_mesh", cone(0.45, 0.9, 32), Some("solid_mat")) .unwrap(); app.scene .create_mesh("torus_mesh", torus(0.42, 0.16, 24, 16), Some("solid_mat")) .unwrap(); place("cube_e", "cube_mesh", app, 0); place("sphere_e", "sphere_mesh", app, 1); place("ico_e", "ico_mesh", app, 2); place("cyl_e", "cyl_mesh", app, 3); place("cone_e", "cone_mesh", app, 4); place("torus_e", "torus_mesh", app, 5); // 5. Lights: a shadow-casting directional + a warm point + a green spot. // Start from the default list (directional +Z) so we keep it and add the rest. let toward_light = Vec3::new(1.0, 1.2, 1.0).normalize(); app.scene .add_directional_light(toward_light, [1.0, 0.98, 0.92], 1.5) .unwrap(); app.scene .add_point_light(Vec3::new(0.5, 1.6, 1.8), [1.0, 0.7, 0.3], 1.2, 6.0) .unwrap(); app.scene .add_spot_light( Vec3::new(-2.5, 2.2, 1.0), Vec3::new(2.5, -2.2, -1.0).normalize(), [0.3, 1.0, 0.5], 1.4, 8.0, 0.45, ) .unwrap(); // The warm directional light above casts shadows. It is packed at index 1: index 0 is // the default +Z directional light pre-loaded by `Lights::new()` (kept here for the // base lighting), so the demo's own light is the SECOND one in the packed array. app.scene.set_shadow_caster(Some(1)); app.scene.set_ambient([0.14, 0.14, 0.16]); // 6. Active camera, driven by the orbital controller (position, distance, preset target). self.camera.yaw = 0.6; self.camera.pitch = 0.35; self.camera.distance = 6.5; self.camera.target = Vec3::ZERO; self.camera.apply_to(app.scene.camera_mut()); } fn update(&mut self, app: &mut wsg_lib::App) { // ---- Orbital camera from unified input ---- // Moving the mouse orbits (yaw/pitch); the wheel zooms (distance). let (dx, dy) = app.input.mouse_delta(); self.camera.orbit(dx, dy); let (_, sy) = app.input.scroll_delta(); self.camera.zoom(sy); // R: reset the view. Keys 1/2/3: front / side / top presets. if app.input.key_pressed(KeyCode::KeyR) { // Keep the target but restore a pleasing default framing. self.camera.yaw = 0.6; self.camera.pitch = 0.35; self.camera.distance = 6.5; } if app.input.key_pressed(KeyCode::Digit1) { self.camera.yaw = 0.0; self.camera.pitch = 0.25; self.camera.distance = 6.5; } if app.input.key_pressed(KeyCode::Digit2) { self.camera.yaw = std::f32::consts::FRAC_PI_2; self.camera.pitch = 0.15; self.camera.distance = 6.5; } if app.input.key_pressed(KeyCode::Digit3) { self.camera.yaw = 0.0; self.camera.pitch = 1.25; self.camera.distance = 8.0; } self.camera.apply_to(app.scene.camera_mut()); // ---- Slow rotation of the primitives so lighting/shadow read clearly ---- self.angle += 0.008; let base = *app .scene .entity_transform("cube_e") .expect("cube entity present"); let mut tf = base; tf.rotation = Quat::from_rotation_y(self.angle) * Quat::from_rotation_x(self.angle * 0.4); app.scene.set_entity_transform("cube_e", tf); } } #[pollster::main] async fn main() -> Result<(), WsgError> { let app = AppBuilder::new().title("WSG Demo").build().await?; app.run(Demo { camera: CameraController::default(), angle: 0.0, }) }