153 lines
5.5 KiB
Rust
153 lines
5.5 KiB
Rust
//! 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::resources::{Camera, 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::math::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::math::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)
|
||
}
|