//! **Bloom** — demonstrates the bloom post-process with emissive materials. //! //! A glowing sphere (emissive intensity 2.0) produces a visible halo. The scene //! also contains a lit ground plane and a cube for reference. //! //! ## Controls //! | Key | Action | //! |-----|--------| //! | Drag (LMB) | Orbit camera | //! | Wheel | Zoom | //! | `R` | Reset camera | //! | `+` / `-` | Bloom threshold up/down | //! | `[` / `]` | Bloom intensity up/down | //! | `I` / `O` | Bloom radius up/down | //! | `E` | Exposure up (×1.3) | //! | `Q` | Exposure down (÷1.3) | //! | `0` | Reset exposure | //! //! ## Build & Run //! ```sh //! cargo run -p wsg-lib --example bloom //! ``` 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::{BloomConfig, ToneMapper, Transform}; use wsg_lib::mesh::{cube, icosphere, plane}; use wsg_lib::AppHandler; use wsg_lib::utils::WsgError; struct BloomDemo { camera: CameraController, angle: f32, /// Runtime bloom config (mirrors the App's internal state for display/adjustment). bloom: BloomConfig, } impl AppHandler for BloomDemo { 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(8.0, 8.0, 1, 1), None) .unwrap(); app.scene.add_entity("ground", "ground_mesh").unwrap(); // Cube (lit, non-emissive — reference). app.scene .create_mesh("cube_mesh", cube(0.7), None) .unwrap(); let mut cube_tf = Transform::identity(); cube_tf.translation = Vec3::new(1.5, 0.35, 0.0); app.scene .add_entity_with_transform("cube_e", "cube_mesh", cube_tf) .unwrap(); // Glowing sphere (emissive intensity 2.0 → HDR bloom). app.scene .add_material_shader("glow_mat", "standard") .unwrap(); app.scene .set_material_emissive("glow_mat", [1.0, 0.3, 0.05, 2.0]) .unwrap(); app.scene .create_mesh("glow_mesh", icosphere(0.35, 3), Some("glow_mat")) .unwrap(); let mut glow_tf = Transform::identity(); glow_tf.translation = Vec3::new(0.0, 0.5, 0.0); app.scene .add_entity_with_transform("glow_e", "glow_mesh", glow_tf) .unwrap(); // Second glow (blue, higher intensity for more dramatic bloom). app.scene .add_material_shader("blue_glow_mat", "standard") .unwrap(); app.scene .set_material_emissive("blue_glow_mat", [0.2, 0.5, 1.0, 3.0]) .unwrap(); app.scene .create_mesh("blue_glow_mesh", icosphere(0.25, 3), Some("blue_glow_mat")) .unwrap(); let mut blue_tf = Transform::identity(); blue_tf.translation = Vec3::new(-1.5, 0.4, 0.0); app.scene .add_entity_with_transform("blue_glow_e", "blue_glow_mesh", blue_tf) .unwrap(); // Directional light (warm, from above-right). let light_dir = Vec3::new(1.0, 1.5, 0.8).normalize(); app.scene .add_directional_light(light_dir, [1.0, 0.95, 0.88], 1.2) .unwrap(); app.scene.set_ambient([0.12, 0.12, 0.15]); // Camera. self.camera.yaw = 0.4; self.camera.pitch = 0.3; self.camera.distance = 5.0; self.camera.target = Vec3::new(0.0, 0.5, 0.0); self.camera.apply_to(app.scene.camera_mut()); // Sync bloom config from the App. if let Some(cfg) = app.bloom_config() { self.bloom = cfg.clone(); } } 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.4; self.camera.pitch = 0.3; self.camera.distance = 5.0; } self.camera.apply_to(app.scene.camera_mut()); // Bloom threshold (+/-). if app.input.key_pressed(KeyCode::Equal) { self.bloom.threshold += 0.1; app.set_bloom_config(self.bloom.clone()); eprintln!("bloom threshold = {:.2}", self.bloom.threshold); } if app.input.key_pressed(KeyCode::Minus) { self.bloom.threshold = (self.bloom.threshold - 0.1).max(0.0); app.set_bloom_config(self.bloom.clone()); eprintln!("bloom threshold = {:.2}", self.bloom.threshold); } // Bloom intensity ([/]). if app.input.key_pressed(KeyCode::BracketRight) { self.bloom.intensity += 0.1; app.set_bloom_config(self.bloom.clone()); eprintln!("bloom intensity = {:.2}", self.bloom.intensity); } if app.input.key_pressed(KeyCode::BracketLeft) { self.bloom.intensity = (self.bloom.intensity - 0.1).max(0.0); app.set_bloom_config(self.bloom.clone()); eprintln!("bloom intensity = {:.2}", self.bloom.intensity); } // Bloom radius (I/O). if app.input.key_pressed(KeyCode::KeyI) { self.bloom.radius += 0.5; app.set_bloom_config(self.bloom.clone()); eprintln!("bloom radius = {:.1}", self.bloom.radius); } if app.input.key_pressed(KeyCode::KeyO) { self.bloom.radius = (self.bloom.radius - 0.5).max(0.5); app.set_bloom_config(self.bloom.clone()); eprintln!("bloom radius = {:.1}", self.bloom.radius); } // Exposure (E/Q/0). 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"); } // Slow rotation of the glow spheres. self.angle += 0.01; let mut tf = *app .scene .entity_transform("glow_e") .expect("glow entity present"); tf.rotation = Quat::from_rotation_y(self.angle); app.scene.set_entity_transform("glow_e", tf); let mut tf2 = *app .scene .entity_transform("blue_glow_e") .expect("blue glow entity present"); tf2.rotation = Quat::from_rotation_y(-self.angle * 0.7); app.scene.set_entity_transform("blue_glow_e", tf2); } 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 Bloom") .size(960, 640) .with_hdr(ToneMapper::Aces) .with_bloom(BloomConfig::default()) .build() .await?; app.run(BloomDemo { camera: CameraController::default(), angle: 0.0, bloom: BloomConfig::default(), }) }