//! # Depth of Field Example (Étape 26) //! //! Demonstrates cinematic DoF: a row of cubes receding into the distance, //! with the focus plane at a configurable depth. Cubes at the focus distance //! stay sharp; those closer or farther blur proportionally. //! //! ## Pipeline //! DoF operates in linear HDR space **after** bloom and **before** tone mapping: //! 1. CoC pass: reads the depth buffer, linearizes to world distance, computes //! per-pixel blur radius. //! 2. Blur pass: 12-tap disc blur with variable radius (from CoC), producing //! natural circular bokeh. //! //! ## Controls //! | Key | Action | //! |-----|--------| //! | Drag (LMB) | Orbit camera | //! | Wheel | Zoom | //! | `1` | Cinematic preset (focus=8m, strong blur) | //! | `2` | Subtle preset (focus=8m, gentle blur) | //! | `3` | Focus at 3m (near cubes sharp, far blurred) | //! | `4` | Focus at 15m (far cubes sharp, near blurred) | //! | `5` | DoF OFF | //! | `R` | Reset camera | //! //! ## Build & Run //! ```sh //! cargo run -p wsg-lib --example dof --features "all-prims" //! ``` use glam::Vec3; use winit::event::MouseButton; use winit::keyboard::KeyCode; use wsg_lib::app::AppBuilder; use wsg_lib::camera::CameraController; use wsg_lib::core::{DoFConfig, ToneMapper, Transform}; use wsg_lib::mesh::{cube, icosphere, plane}; use wsg_lib::AppHandler; use wsg_lib::utils::WsgError; struct DoFDemo { camera: CameraController, } impl AppHandler for DoFDemo { fn setup(&mut self, app: &mut wsg_lib::App) { app.scene .register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH) .unwrap(); // Ground plane. app.scene .create_mesh("ground_mesh", plane(80.0, 80.0, 1, 1), None) .unwrap(); app.scene .add_entity_with_transform( "ground", "ground_mesh", Transform::identity(), ) .unwrap(); // Row of cubes receding along -Z (distance ≈ 2 to 25 from camera at dist=8). app.scene .create_mesh("cube_mesh", cube(1.0), None) .unwrap(); for i in 0..20 { let z = 3.0 - i as f32 * 1.5; // from z=3 (close) to z=-25.5 (far) let mut tf = Transform::identity(); tf.translation = Vec3::new(0.0, 0.5, z); app.scene .add_entity_with_transform(&format!("cube_{i}"), "cube_mesh", tf) .unwrap(); } // A few spheres scattered to the sides for visual interest. app.scene .create_mesh("sphere_mesh", icosphere(0.7, 3), None) .unwrap(); let sphere_positions = [ Vec3::new(2.5, 0.7, -2.0), Vec3::new(-3.0, 0.7, -6.0), Vec3::new(3.5, 0.7, -10.0), Vec3::new(-2.0, 0.7, -14.0), Vec3::new(2.0, 0.7, -18.0), ]; for (i, pos) in sphere_positions.iter().enumerate() { let mut tf = Transform::identity(); tf.translation = *pos; app.scene .add_entity_with_transform(&format!("sphere_{i}"), "sphere_mesh", tf) .unwrap(); } // Directional light. let light_dir = Vec3::new(-0.4, -1.0, -0.3).normalize(); app.scene .add_directional_light(light_dir, [1.0, 0.95, 0.85], 1.2) .unwrap(); app.scene.set_ambient([0.08, 0.08, 0.1]); // Camera — positioned to look down the row of cubes. self.camera.yaw = 0.0; self.camera.pitch = 0.1; self.camera.distance = 8.0; self.camera.target = Vec3::new(0.0, 0.5, -8.0); self.camera.apply_to(app.scene.camera_mut()); eprintln!("[DoF] Initial: Cinematic (focus=8m, aperture=0.3, max_blur=12)"); eprintln!("[DoF] Keys: 1=cinematic 2=subtle 3=focus 3m 4=focus 15m 5=off R=reset"); } 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); // DoF presets. if app.input.key_pressed(KeyCode::Digit1) { app.renderer_mut() .set_dof(Some(DoFConfig::cinematic(8.0))); eprintln!("[DoF] → Cinematic (focus=8m, aperture=0.3, max_blur=12)"); } if app.input.key_pressed(KeyCode::Digit2) { app.renderer_mut() .set_dof(Some(DoFConfig::subtle(8.0))); eprintln!("[DoF] → Subtle (focus=8m, aperture=0.1, max_blur=8)"); } if app.input.key_pressed(KeyCode::Digit3) { app.renderer_mut() .set_dof(Some(DoFConfig::new(3.0, 0.3, 12.0))); eprintln!("[DoF] → Focus 3m (near sharp, far blurred)"); } if app.input.key_pressed(KeyCode::Digit4) { app.renderer_mut() .set_dof(Some(DoFConfig::new(15.0, 0.3, 12.0))); eprintln!("[DoF] → Focus 15m (far sharp, near blurred)"); } if app.input.key_pressed(KeyCode::Digit5) { app.renderer_mut().set_dof(None); eprintln!("[DoF] → OFF"); } if app.input.key_pressed(KeyCode::KeyR) { self.camera.yaw = 0.0; self.camera.pitch = 0.1; self.camera.distance = 8.0; self.camera.target = Vec3::new(0.0, 0.5, -8.0); } self.camera.apply_to(app.scene.camera_mut()); } 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> { let app = AppBuilder::new() .title("WSG — Depth of Field (Étape 26)") .size(1280, 720) .with_hdr(ToneMapper::Aces) .with_dof(DoFConfig::cinematic(8.0)) .build() .await?; app.run(DoFDemo { camera: CameraController::default(), }) }