235 lines
9.2 KiB
Rust
235 lines
9.2 KiB
Rust
//! **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,
|
||
})
|
||
}
|