//! 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::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) }