réorg doc
This commit is contained in:
@@ -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).
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user