Files
wsg/lib/examples/demo.rs
T
Jérôme Bousquié 3dd372410f update demos et docs
2026-09-21 12:01:28 +02:00

235 lines
9.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! **WSG `demo`** — the final showcase example.
//!
//! 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:
//! hold the **left mouse button** and drag to orbit (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::event::MouseButton;
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<u8> {
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<u8> {
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 ----
// Classic arc-rotate: orbit ONLY while the left button is held (drag); the wheel zooms
// without any button. Sensitivities use the library defaults (0.005 rad/px orbit, 0.9x
// per wheel notch); tune them via `camera.orbit_sensitivity` / `camera.zoom_factor`.
let (dx, dy) = app.input.mouse_delta();
if app.input.mouse_button_held(MouseButton::Left) {
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,
})
}