réorg doc

This commit is contained in:
Jérôme Bousquié
2026-09-25 19:08:20 +02:00
parent 7e88390006
commit 24fbafc810
34 changed files with 684 additions and 383 deletions
+124
View File
@@ -0,0 +1,124 @@
# Lights, Shadows & Emissive
Examples covering **lighting**: shadow mapping, isolated light types, and
emissive materials.
| Example | Run command | What it shows |
|---------|-------------|---------------|
| `shadow` | `cargo run -p wsg-lib --example shadow` | Shadow mapping in isolation (directional light, 4 objects on a floor) |
| `shadow_test` | `cargo run -p wsg-lib --example shadow_test` | Dedicated shadow test: one directional caster, cube on a ground slab, PCF-softened |
| `spot_test` | `cargo run -p wsg-lib --example spot_test` | Isolated spot light: directed beam, penumbra, attenuation |
| `emissive` | `cargo run -p wsg-lib --example emissive` | Emissive materials (increasing intensities 0 → 4.0) |
> All commands run from the repo root.
---
## `shadow` — Shadow Mapping
Four objects (cube, sphere, cone, cylinder) on a floor, lit by a directional
light that casts shadows. Shadow quality is controlled by `ShadowConfig`
(map size, anti-acne bias).
```sh
cargo run -p wsg-lib --example shadow
```
### Keys
| Key | Action |
|-----|--------|
| Drag (LMB) | Orbit camera |
| Wheel | Zoom |
| `R` | Reset camera |
| `1` | Front view |
| `2` | Side view |
| `3` | **Top view** (see shadow shapes clearly) |
| `L` | Change light direction (3 presets) |
### What to observe
- The cube rotates slowly → its shadow moves on the floor.
- The sphere has a smooth shadow/light transition (soft terminator).
- The cone produces a distinct triangular shadow.
- In top view (`3`), you see the exact shape of projected shadows.
- Shadow map size (1024 default) determines resolution: modify
`SHADOW_MAP_SIZE` at the top of the file to test 256 (pixelated) or 2048 (sharp).
---
## `shadow_test` — Dedicated Shadow Mapping Test
A single **directional** light is configured as the shadow caster
(`Scene::set_shadow_caster(Some(0))`). The cube sits on a large thin ground
slab, so its silhouette is projected as a crisp PCF-softened shadow. With a
small ambient term the shadow is clearly visible and the light/shadow
directions are easy to read:
1. the **blocker** (cube) casts a directional shadow that stretches along the
ground opposite the light direction — the light sits at the camera's
front-right and low-ish, so the shadow runs clearly across the ground to
the left of the cube,
2. the shadow edge is **softened** by 3×3 PCF (no hard jagged border),
3. the lit faces are bright while the shadowed ground stays near-ambient,
proving the depth comparison is applied per-pixel.
```sh
cargo run -p wsg-lib --example shadow_test
```
---
## `spot_test` — Isolated Spot Light
**Only** a spot light is on (the default directional light is removed via
`clear_lights()`) and the ambient is deliberately **very low**. The rotating
cube therefore appears nearly black except where the spot's cone reaches it —
you clearly see:
1. a **directed beam** (not an omni halo like the point light),
2. a **smoothed edge** (penumbra) at the cone's limit,
3. the lighting that **follows the cube** as it rotates (the cone is fixed in
world space).
```sh
cargo run -p wsg-lib --example spot_test
```
---
## `emissive` — Emissive Materials
Five spheres in a row with increasing emissive intensities:
| Sphere | Color | Intensity | Effect |
|--------|-------|-----------|--------|
| 1 | Gray | 0.0 | No glow (reference) |
| 2 | Orange | 0.5 | Slight glow |
| 3 | Yellow | 1.0 | Visible glow |
| 4 | Green | 2.0 | HDR glow (beyond 1.0) |
| 5 | Blue | 4.0 | Intense glow (saturation) |
With HDR, intensities > 1.0 produce a true "glow" (values exceed [0,1] in
linear space). Without HDR, they would be clamped to white.
```sh
cargo run -p wsg-lib --example emissive
```
### Keys
| Key | Action |
|-----|--------|
| Drag (LMB) | Orbit camera |
| Wheel | Zoom |
| `R` | Reset camera |
| `E` / `Q` | Exposure ×1.3 / ÷1.3 |
| `0` | Reset exposure |
| `C` | **Cycle emissive multiplier** (1× → 2× → 0.5× → …) |
### What to observe
- Sphere 1 (intensity 0) is simply lit by the directional light.
- Spheres 2-5 glow with their own light, independent of scene lighting.
- `C` doubles or halves all intensities simultaneously (to see the HDR effect).
+197
View File
@@ -0,0 +1,197 @@
//! **Emissive Materials** — demonstrates the emissive property of the standard material.
//!
//! Shows objects with varying emissive intensities. Without HDR, emissive values > 1.0
//! are clamped to white (LDR). With HDR, they produce true "glow" that can feed the
//! bloom post-process.
//!
//! The scene contains 5 spheres with increasing emissive intensity (0.0 → 4.0),
//! arranged in a row. A lit cube serves as a non-emissive reference.
//!
//! ## Controls
//! | Key | Action |
//! |-----|--------|
//! | Drag (LMB) | Orbit camera |
//! | Wheel | Zoom |
//! | `R` | Reset camera |
//! | `E` | Exposure up (×1.3) |
//! | `Q` | Exposure down (÷1.3) |
//! | `0` | Reset exposure |
//! | `C` | Cycle emissive intensity (re-applies to all glow spheres) |
//!
//! ## Build & Run
//! ```sh
//! cargo run -p wsg-lib --example emissive
//! ```
//!
//! Run with `--features all-prims` if you don't have the default features.
use glam::{Quat, Vec3};
use winit::event::MouseButton;
use winit::keyboard::KeyCode;
use wsg_lib::app::AppBuilder;
use wsg_lib::camera::CameraController;
use wsg_lib::core::{ToneMapper, Transform};
use wsg_lib::mesh::{cube, icosphere, plane};
use wsg_lib::AppHandler;
use wsg_lib::utils::WsgError;
/// Emissive intensities for the 5 glow spheres (left to right).
const INTENSITIES: [f32; 5] = [0.0, 0.5, 1.0, 2.0, 4.0];
/// RGB colors for the 5 glow spheres (rainbow-ish).
const COLORS: [[f32; 3]; 5] = [
[0.5, 0.5, 0.5], // gray (no glow)
[1.0, 0.3, 0.1], // orange
[1.0, 0.8, 0.0], // yellow
[0.2, 1.0, 0.4], // green
[0.3, 0.5, 1.0], // blue
];
struct EmissiveDemo {
camera: CameraController,
angle: f32,
/// Which intensity preset to apply (0-4 maps to a multiplier).
cycle_idx: usize,
}
impl AppHandler for EmissiveDemo {
fn setup(&mut self, app: &mut wsg_lib::App) {
app.scene
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
.unwrap();
// Ground.
app.scene
.create_mesh("ground_mesh", plane(10.0, 10.0, 1, 1), None)
.unwrap();
app.scene.add_entity("ground", "ground_mesh").unwrap();
// Reference cube (non-emissive).
app.scene
.create_mesh("cube_mesh", cube(0.6), None)
.unwrap();
let mut cube_tf = Transform::identity();
cube_tf.translation = Vec3::new(0.0, 0.3, 1.5);
app.scene
.add_entity_with_transform("cube_e", "cube_mesh", cube_tf)
.unwrap();
// 5 glow spheres in a row.
for i in 0..5 {
let mat_id = format!("glow_mat_{}", i);
let mesh_id = format!("glow_mesh_{}", i);
let entity_id = format!("glow_e_{}", i);
app.scene.add_material_shader(&mat_id, "standard").unwrap();
let c = COLORS[i];
let intensity = INTENSITIES[i];
app.scene
.set_material_emissive(&mat_id, [c[0], c[1], c[2], intensity])
.unwrap();
app.scene
.create_mesh(&mesh_id, icosphere(0.3, 3), Some(&mat_id))
.unwrap();
let x = (i as f32 - 2.0) * 0.9;
let mut tf = Transform::identity();
tf.translation = Vec3::new(x, 0.4, 0.0);
app.scene
.add_entity_with_transform(&entity_id, &mesh_id, tf)
.unwrap();
}
// Directional light.
let light_dir = Vec3::new(0.5, 1.0, 0.5).normalize();
app.scene
.add_directional_light(light_dir, [1.0, 0.95, 0.88], 1.0)
.unwrap();
app.scene.set_ambient([0.15, 0.15, 0.18]);
// Camera.
self.camera.yaw = 0.0;
self.camera.pitch = 0.2;
self.camera.distance = 5.5;
self.camera.target = Vec3::new(0.0, 0.3, 0.0);
self.camera.apply_to(app.scene.camera_mut());
}
fn update(&mut self, app: &mut wsg_lib::App) {
// Orbit camera.
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);
if app.input.key_pressed(KeyCode::KeyR) {
self.camera.yaw = 0.0;
self.camera.pitch = 0.2;
self.camera.distance = 5.5;
}
self.camera.apply_to(app.scene.camera_mut());
// Exposure.
if app.input.key_pressed(KeyCode::KeyE) {
app.set_exposure(app.exposure() * 1.3);
eprintln!("exposure = {:.2}", app.exposure());
}
if app.input.key_pressed(KeyCode::KeyQ) {
app.set_exposure(app.exposure() / 1.3);
eprintln!("exposure = {:.2}", app.exposure());
}
if app.input.key_pressed(KeyCode::Digit0) {
app.set_exposure(1.0);
eprintln!("exposure reset to 1.0");
}
// C: cycle emissive intensity multiplier (1x → 2x → 0.5x → back).
if app.input.key_pressed(KeyCode::KeyC) {
self.cycle_idx = (self.cycle_idx + 1) % 3;
let multiplier = match self.cycle_idx {
0 => 1.0,
1 => 2.0,
_ => 0.5,
};
for i in 0..5 {
let mat_id = format!("glow_mat_{}", i);
let c = COLORS[i];
let intensity = INTENSITIES[i] * multiplier;
if let Ok(()) = app.scene.set_material_emissive(&mat_id, [c[0], c[1], c[2], intensity]) {
eprintln!("emissive multiplier = {:.1}x", multiplier);
}
}
}
// Slow rotation.
self.angle += 0.01;
for i in 0..5 {
let entity_id = format!("glow_e_{}", i);
if let Some(base) = app.scene.entity_transform(&entity_id) {
let mut tf = *base;
tf.rotation = Quat::from_rotation_y(self.angle * (1.0 + i as f32 * 0.2));
app.scene.set_entity_transform(&entity_id, tf);
}
}
}
fn render(&mut self, app: &mut wsg_lib::App, frame: &wsg_lib::core::Frame) {
app.render_scene(frame.view());
}
}
#[pollster::main]
async fn main() -> Result<(), WsgError> {
// HDR enabled so emissive > 1.0 produces true glow (not clamped to white).
let app = AppBuilder::new()
.title("WSG Emissive")
.size(960, 640)
.with_hdr(ToneMapper::Aces)
.build()
.await?;
app.run(EmissiveDemo {
camera: CameraController::default(),
angle: 0.0,
cycle_idx: 0,
})
}
+201
View File
@@ -0,0 +1,201 @@
//! **Shadow Mapping** — demonstrates the directional shadow map system.
//!
//! A cube and a sphere sit on a ground plane, lit by a directional light that
//! casts shadows. The shadow quality is controlled by `ShadowConfig` (map size,
//! depth/slope bias, ortho frustum radius).
//!
//! ## Controls
//! | Key | Action |
//! |-----|--------|
//! | Drag (LMB) | Orbit camera |
//! | Wheel | Zoom |
//! | `R` | Reset camera |
//! | `1` | Front view |
//! | `2` | Side view |
//! | `3` | Top view (see shadow shape clearly) |
//! | `L` | Move light (cycles 3 directions) |
//!
//! ## Shadow Config
//! The shadow map parameters are set at build time (the shadow map texture is
//! allocated once). To test different resolutions, modify `SHADOW_MAP_SIZE` below
//! and re-run.
//!
//! ## Build & Run
//! ```sh
//! cargo run -p wsg-lib --example shadow
//! ```
use glam::{Quat, Vec3};
use winit::event::MouseButton;
use winit::keyboard::KeyCode;
use wsg_lib::app::AppBuilder;
use wsg_lib::camera::CameraController;
use wsg_lib::core::{ShadowConfig, Transform};
use wsg_lib::mesh::{cone, cube, cylinder, icosphere, plane};
use wsg_lib::AppHandler;
use wsg_lib::utils::WsgError;
/// Shadow map size — change to test quality (256, 512, 1024, 2048).
const SHADOW_MAP_SIZE: u32 = 1024;
/// Light directions to cycle through (normalized at runtime).
fn light_dirs() -> [Vec3; 3] {
[
Vec3::new(1.0, 1.2, 0.8).normalize(),
Vec3::new(-0.8, 1.0, 0.5).normalize(),
Vec3::new(0.3, 0.6, -1.0).normalize(),
]
}
struct ShadowDemo {
camera: CameraController,
angle: f32,
light_idx: usize,
}
impl AppHandler for ShadowDemo {
fn setup(&mut self, app: &mut wsg_lib::App) {
app.scene
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
.unwrap();
// Large ground plane (receives shadows).
app.scene
.create_mesh("ground_mesh", plane(8.0, 8.0, 1, 1), None)
.unwrap();
app.scene.add_entity("ground", "ground_mesh").unwrap();
// Cube (casts + receives shadow).
app.scene
.create_mesh("cube_mesh", cube(0.8), None)
.unwrap();
let mut cube_tf = Transform::identity();
cube_tf.translation = Vec3::new(0.8, 0.4, 0.0);
app.scene
.add_entity_with_transform("cube_e", "cube_mesh", cube_tf)
.unwrap();
// Sphere (smooth shadow terminator).
app.scene
.create_mesh("sphere_mesh", icosphere(0.45, 3), None)
.unwrap();
let mut sphere_tf = Transform::identity();
sphere_tf.translation = Vec3::new(-0.8, 0.45, 0.3);
app.scene
.add_entity_with_transform("sphere_e", "sphere_mesh", sphere_tf)
.unwrap();
// Cone (distinctive shadow shape).
app.scene
.create_mesh("cone_mesh", cone(0.4, 0.8, 24), None)
.unwrap();
let mut cone_tf = Transform::identity();
cone_tf.translation = Vec3::new(0.0, 0.4, -0.9);
app.scene
.add_entity_with_transform("cone_e", "cone_mesh", cone_tf)
.unwrap();
// Cylinder.
app.scene
.create_mesh("cyl_mesh", cylinder(0.3, 0.7, 24), None)
.unwrap();
let mut cyl_tf = Transform::identity();
cyl_tf.translation = Vec3::new(-0.5, 0.35, -0.7);
app.scene
.add_entity_with_transform("cyl_e", "cyl_mesh", cyl_tf)
.unwrap();
// Directional light (shadow caster).
let dirs = light_dirs();
let light_dir = dirs[0];
app.scene
.add_directional_light(light_dir, [1.0, 0.95, 0.88], 1.5)
.unwrap();
// The light is at index 1 (index 0 is the default +Z light from Lights::new()).
app.scene.set_shadow_caster(Some(1));
app.scene.set_ambient([0.15, 0.15, 0.18]);
// Camera.
self.camera.yaw = 0.5;
self.camera.pitch = 0.4;
self.camera.distance = 5.0;
self.camera.target = Vec3::ZERO;
self.camera.apply_to(app.scene.camera_mut());
}
fn update(&mut self, app: &mut wsg_lib::App) {
// Orbit camera.
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);
// Camera presets.
if app.input.key_pressed(KeyCode::KeyR) {
self.camera.yaw = 0.5;
self.camera.pitch = 0.4;
self.camera.distance = 5.0;
}
if app.input.key_pressed(KeyCode::Digit1) {
self.camera.yaw = 0.0;
self.camera.pitch = 0.2;
self.camera.distance = 5.0;
}
if app.input.key_pressed(KeyCode::Digit2) {
self.camera.yaw = std::f32::consts::FRAC_PI_2;
self.camera.pitch = 0.15;
self.camera.distance = 5.0;
}
if app.input.key_pressed(KeyCode::Digit3) {
self.camera.yaw = 0.0;
self.camera.pitch = 1.4;
self.camera.distance = 6.0;
}
self.camera.apply_to(app.scene.camera_mut());
// L: cycle light direction.
if app.input.key_pressed(KeyCode::KeyL) {
let dirs = light_dirs();
self.light_idx = (self.light_idx + 1) % dirs.len();
let new_dir = dirs[self.light_idx];
eprintln!("light direction: {:?}", new_dir);
// Note: changing the light direction at runtime requires re-packing
// the lights buffer. For this demo, we just print the direction —
// the shadow frustum is computed from the light each frame.
}
// Slow rotation of the cube to show shadow movement.
self.angle += 0.005;
if let Some(base) = app.scene.entity_transform("cube_e") {
let mut tf = *base;
tf.rotation = Quat::from_rotation_y(self.angle);
app.scene.set_entity_transform("cube_e", tf);
}
}
fn render(&mut self, app: &mut wsg_lib::App, frame: &wsg_lib::core::Frame) {
app.render_scene(frame.view());
}
}
#[pollster::main]
async fn main() -> Result<(), WsgError> {
// Shadow config: 1024² map, default biases.
// Try map_size = 256 to see blocky shadows, or 2048 for sharper ones.
let app = AppBuilder::new()
.title("WSG Shadow")
.size(960, 640)
.with_shadow_config(ShadowConfig {
map_size: SHADOW_MAP_SIZE,
..Default::default()
})
.build()
.await?;
app.run(ShadowDemo {
camera: CameraController::default(),
angle: 0.0,
light_idx: 0,
})
}
+153
View File
@@ -0,0 +1,153 @@
//! Dedicated test for **shadow mapping**.
//!
//! A single **directional** light is configured as the shadow caster
//! (`Scene::set_shadow_caster(Some(0))`). The cube sits on a large thin ground
//! slab, so its silhouette is projected as a crisp PCF-softened shadow. With a
//! small ambient term the shadow is clearly visible and the light/shadow
//! directions are easy to read:
//!
//! 1. the **blocker** (cube) casts a directional shadow that stretches along
//! the ground opposite the light direction. The light sits at the camera's
//! front-right and low-ish, so its shadow runs clearly across the ground to
//! the left of the cube and is easy to see,
//! 2. the shadow edge is **softened** by 3×3 PCF (no hard jagged border),
//! 3. the lit faces are bright while the shadowed ground stays near-ambient,
//! proving the depth comparison is applied per-pixel.
//!
//! Run with: `cargo run -p wsg-lib --example shadow_test`
use glam::Vec3;
use wsg_lib::camera::Camera;
use wsg_lib::resources::Geometry;
use wsg_lib::utils::WsgError;
/// Shadow handler: a fixed scene (ground slab + cube blocker) lit by one
/// shadow-casting directional light.
struct ShadowTest;
/// Axis-aligned box geometry (24 vertices / 36 indices, per-face normals + uvs).
fn box_geometry(hx: f32, hy: f32, hz: f32) -> Geometry {
let faces: [([f32; 3], [[f32; 3]; 4]); 6] = [
(
[0.0, 0.0, 1.0],
[[-hx, -hy, hz], [hx, -hy, hz], [hx, hy, hz], [-hx, hy, hz]],
), // +Z
(
[0.0, 0.0, -1.0],
[
[hx, -hy, -hz],
[-hx, -hy, -hz],
[-hx, hy, -hz],
[hx, hy, -hz],
],
), // -Z
(
[1.0, 0.0, 0.0],
[[hx, -hy, -hz], [hx, hy, -hz], [hx, hy, hz], [hx, -hy, hz]],
), // +X
(
[-1.0, 0.0, 0.0],
[
[-hx, -hy, hz],
[-hx, hy, hz],
[-hx, hy, -hz],
[-hx, -hy, -hz],
],
), // -X
(
[0.0, 1.0, 0.0],
[[-hx, hy, -hz], [hx, hy, -hz], [hx, hy, hz], [-hx, hy, hz]],
), // +Y
(
[0.0, -1.0, 0.0],
[
[-hx, -hy, hz],
[hx, -hy, hz],
[hx, -hy, -hz],
[-hx, -hy, -hz],
],
), // -Y
];
let mut positions = Vec::with_capacity(24);
let mut normals = Vec::with_capacity(24);
let mut uvs = Vec::with_capacity(24);
let quad_uvs = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
for (normal, corners) in faces {
for (i, corner) in corners.iter().enumerate() {
positions.push(*corner);
normals.push(normal);
uvs.push(quad_uvs[i]);
}
}
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]);
}
Geometry::new(positions)
.with_normals(normals)
.with_uvs(uvs)
.with_indices(indices)
}
impl wsg_lib::AppHandler for ShadowTest {
fn setup(&mut self, app: &mut wsg_lib::App) {
app.scene
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
.unwrap();
app.scene.add_material_shader("mat", "standard").unwrap();
// Ground slab (thin, wide) lying with its top at y = 0.
app.scene
.create_mesh("ground_mesh", box_geometry(5.0, 0.05, 5.0), Some("mat"))
.unwrap();
app.scene
.add_entity_with_transform(
"ground",
"ground_mesh",
wsg_lib::core::Transform::identity(),
)
.unwrap();
// Blocker cube centred at the origin, standing on the ground (bottom at y = 0).
app.scene
.create_mesh("cube_mesh", box_geometry(0.5, 0.5, 0.5), Some("mat"))
.unwrap();
let mut cube_tf = wsg_lib::core::Transform::identity();
cube_tf.translation = Vec3::new(0.0, 0.5, 0.0);
app.scene
.add_entity_with_transform("cube", "cube_mesh", cube_tf)
.unwrap();
// One directional light only: replace the default list.
app.scene.clear_lights();
// Direction "from surface toward the light": the light sits up and to the +x side
// (the camera's right), at a lowish elevation. Its shadow is then cast toward -x,
// running clearly across the ground to the left of the cube. A steeper or more
// frontal light would push the shadow tight against the cube's base or behind it,
// where it is occluded by the cube from this elevated front-right view.
let toward_light = Vec3::new(1.0, 0.5, 0.0).normalize();
app.scene
.add_directional_light(toward_light, [1.0, 0.98, 0.92], 1.6)
.unwrap();
// Make this directional light (packed index 0) the shadow caster.
app.scene.set_shadow_caster(Some(0));
// Small ambient so the shadowed side of the ground stays readable.
app.scene.set_ambient([0.12, 0.12, 0.14]);
// Slightly elevated view so both the cube and its ground shadow are framed.
app.scene
.set_camera(Camera::new(Vec3::new(3.4, 2.6, 3.4), Vec3::ZERO, Vec3::Y));
}
}
#[pollster::main]
async fn main() -> Result<(), WsgError> {
let app = wsg_lib::app::AppBuilder::new()
.title("WSG Shadow Test")
.build()
.await?;
app.run(ShadowTest)
}
+84
View File
@@ -0,0 +1,84 @@
//! Test dedicated to **spot lights**.
//!
//! In this example, **only** a spot light is on (the default directional light is
//! removed via `clear_lights()`) and the ambient is deliberately **very low**. The cube therefore
//! appears nearly black except where the spot's cone reaches it: you clearly see
//!
//! 1. a **directed beam** (not an omni halo like the point light),
//! 2. a **smoothed edge** (penumbra) at the cone's limit,
//! 3. the lighting that **follows the cube** as it rotates (the cone is fixed in world space).
//!
//! Run with: `cargo run -p wsg-lib --example spot_test`
use glam::{Quat, Vec3};
use wsg_lib::AppHandler;
use wsg_lib::app::AppBuilder;
use wsg_lib::mesh::cube;
use wsg_lib::utils::WsgError;
/// Test handler: cube rotating slowly on two axes, lit **only** by a spot.
struct SpotTest {
angle_x: f32,
angle_y: f32,
}
impl AppHandler for SpotTest {
fn setup(&mut self, app: &mut wsg_lib::App) {
app.scene
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
.unwrap();
app.scene.add_material_shader("mat", "standard").unwrap();
app.scene
.create_mesh("cube_mesh", cube(1.0), Some("mat"))
.unwrap();
app.scene.add_entity("cube", "cube_mesh").unwrap();
// Remove the default directional light to isolate the spot.
app.scene.clear_lights();
// Near-zero ambient: the cube is black outside the beam, the cone stands out.
app.scene.set_ambient([0.03, 0.03, 0.03]);
// The spot is above/behind the camera, aimed at the origin (the cube).
// World position (0, 2, 3), cone axis toward (0,0,0).
let spot_pos = Vec3::new(0.0, 2.0, 3.0);
let spot_dir = (Vec3::ZERO - spot_pos).normalize(); // points at the cube
app.scene
.add_spot_light(
spot_pos,
spot_dir,
[1.0, 0.9, 0.6], // warm tint
2.0, // intensity
10.0, // attenuation radius (wide, the cube is at ~3.6)
0.45, // half-angle (~26°) — wide enough to cover the cube
)
.unwrap();
}
fn update(&mut self, app: &mut wsg_lib::App) {
// Slow rotation on two axes (X and Y): the cone is fixed in world space,
// so a fixed region of the cube stays lit while the cube rotates.
// The two axes let you see the beam's effect on the 6 faces without
// a favored orientation (a Y rotation alone would leave the +Y/-Y faces fixed).
self.angle_x += 0.007;
self.angle_y += 0.011;
let base = *app
.scene
.entity_transform("cube")
.expect("cube entity present");
let mut transform = base;
// Y * X composition: the X axis rotates in the frame already oriented by Y,
// which gives a precession motion (all vertices pass in front of
// the cone in turn).
transform.rotation =
Quat::from_rotation_y(self.angle_y) * Quat::from_rotation_x(self.angle_x);
app.scene.set_entity_transform("cube", transform);
}
}
#[pollster::main]
async fn main() -> Result<(), WsgError> {
let app = AppBuilder::new().title("WSG Spot Test").build().await?;
app.run(SpotTest {
angle_x: 0.0,
angle_y: 0.0,
})
}