//! **HDR + Tone Mapping** — demonstrates HDR rendering with exposure control. //! //! Shows the difference between ACES and Reinhard tone mapping curves, and how //! exposure affects the final image. A bright emissive sphere (intensity 3.0) //! demonstrates highlight rolloff: without HDR it would clip to white, with //! ACES it rolls off smoothly. //! //! ## 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 to 1.0 | //! //! ## Build & Run //! ```sh //! cargo run -p wsg-lib --example hdr //! ``` //! //! Note: tone mapper is selected at build time (pipeline compiled once). To compare //! ACES vs Reinhard, run twice with different flags or modify the source. 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; struct HdrDemo { camera: CameraController, angle: f32, } impl AppHandler for HdrDemo { 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(); // Lit cube (normal brightness, no emissive). app.scene .create_mesh("cube_mesh", cube(0.8), None) .unwrap(); let mut cube_tf = Transform::identity(); cube_tf.translation = Vec3::new(1.5, 0.4, 0.0); app.scene .add_entity_with_transform("cube_e", "cube_mesh", cube_tf) .unwrap(); // Bright sphere (emissive 3.0 — demonstrates HDR highlight rolloff). app.scene .add_material_shader("bright_mat", "standard") .unwrap(); app.scene .set_material_emissive("bright_mat", [1.0, 0.9, 0.7, 3.0]) .unwrap(); app.scene .create_mesh("bright_mesh", icosphere(0.4, 3), Some("bright_mat")) .unwrap(); let mut bright_tf = Transform::identity(); bright_tf.translation = Vec3::new(0.0, 0.5, 0.0); app.scene .add_entity_with_transform("bright_e", "bright_mesh", bright_tf) .unwrap(); // Dim sphere (emissive 0.3 — stays dark even at high exposure). app.scene .add_material_shader("dim_mat", "standard") .unwrap(); app.scene .set_material_emissive("dim_mat", [0.2, 0.4, 1.0, 0.3]) .unwrap(); app.scene .create_mesh("dim_mesh", icosphere(0.3, 3), Some("dim_mat")) .unwrap(); let mut dim_tf = Transform::identity(); dim_tf.translation = Vec3::new(-1.5, 0.4, 0.0); app.scene .add_entity_with_transform("dim_e", "dim_mesh", dim_tf) .unwrap(); // Strong 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.85], 2.0) .unwrap(); app.scene.set_ambient([0.1, 0.1, 0.12]); // Camera. self.camera.yaw = 0.3; self.camera.pitch = 0.25; self.camera.distance = 5.0; self.camera.target = Vec3::new(0.0, 0.4, 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.3; self.camera.pitch = 0.25; self.camera.distance = 5.0; } self.camera.apply_to(app.scene.camera_mut()); // Exposure control. if app.input.key_pressed(KeyCode::KeyE) { app.set_exposure(app.exposure() * 1.3); eprintln!("exposure = {:.3}", app.exposure()); } if app.input.key_pressed(KeyCode::KeyQ) { app.set_exposure(app.exposure() / 1.3); eprintln!("exposure = {:.3}", app.exposure()); } if app.input.key_pressed(KeyCode::Digit0) { app.set_exposure(1.0); eprintln!("exposure reset to 1.0"); } // Rotate the bright sphere to show specular highlights. self.angle += 0.008; let mut tf = *app .scene .entity_transform("bright_e") .expect("bright entity present"); tf.rotation = Quat::from_rotation_y(self.angle); app.scene.set_entity_transform("bright_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> { // ACES Filmic tone mapping — cinematic contrast with smooth highlight rolloff. // Change to ToneMapper::Reinhard to compare (flatter, less contrast). let app = AppBuilder::new() .title("WSG HDR (ACES)") .size(960, 640) .with_hdr(ToneMapper::Aces) .with_exposure(1.0) .build() .await?; app.run(HdrDemo { camera: CameraController::default(), angle: 0.0, }) }