//! **Emissive Materials** — demonstrates the emissive property of the standard material. //! //! Shows objects with varying emissive intensities. Without HDR, emissive values > 1.0 //! are clamped to white (LDR). With HDR, they produce true "glow" that can feed the //! bloom post-process. //! //! The scene contains 5 spheres with increasing emissive intensity (0.0 → 4.0), //! arranged in a row. A lit cube serves as a non-emissive reference. //! //! ## 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 | //! | `C` | Cycle emissive intensity (re-applies to all glow spheres) | //! //! ## Build & Run //! ```sh //! cargo run -p wsg-lib --example emissive //! ``` //! //! Run with `--features all-prims` if you don't have the default features. 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; /// Emissive intensities for the 5 glow spheres (left to right). const INTENSITIES: [f32; 5] = [0.0, 0.5, 1.0, 2.0, 4.0]; /// RGB colors for the 5 glow spheres (rainbow-ish). const COLORS: [[f32; 3]; 5] = [ [0.5, 0.5, 0.5], // gray (no glow) [1.0, 0.3, 0.1], // orange [1.0, 0.8, 0.0], // yellow [0.2, 1.0, 0.4], // green [0.3, 0.5, 1.0], // blue ]; struct EmissiveDemo { camera: CameraController, angle: f32, /// Which intensity preset to apply (0-4 maps to a multiplier). cycle_idx: usize, } impl AppHandler for EmissiveDemo { 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(); // Reference cube (non-emissive). app.scene .create_mesh("cube_mesh", cube(0.6), None) .unwrap(); let mut cube_tf = Transform::identity(); cube_tf.translation = Vec3::new(0.0, 0.3, 1.5); app.scene .add_entity_with_transform("cube_e", "cube_mesh", cube_tf) .unwrap(); // 5 glow spheres in a row. for i in 0..5 { let mat_id = format!("glow_mat_{}", i); let mesh_id = format!("glow_mesh_{}", i); let entity_id = format!("glow_e_{}", i); app.scene.add_material_shader(&mat_id, "standard").unwrap(); let c = COLORS[i]; let intensity = INTENSITIES[i]; app.scene .set_material_emissive(&mat_id, [c[0], c[1], c[2], intensity]) .unwrap(); app.scene .create_mesh(&mesh_id, icosphere(0.3, 3), Some(&mat_id)) .unwrap(); let x = (i as f32 - 2.0) * 0.9; let mut tf = Transform::identity(); tf.translation = Vec3::new(x, 0.4, 0.0); app.scene .add_entity_with_transform(&entity_id, &mesh_id, tf) .unwrap(); } // 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.88], 1.0) .unwrap(); app.scene.set_ambient([0.15, 0.15, 0.18]); // Camera. self.camera.yaw = 0.0; self.camera.pitch = 0.2; self.camera.distance = 5.5; self.camera.target = Vec3::new(0.0, 0.3, 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.0; self.camera.pitch = 0.2; self.camera.distance = 5.5; } self.camera.apply_to(app.scene.camera_mut()); // Exposure. 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"); } // C: cycle emissive intensity multiplier (1x → 2x → 0.5x → back). if app.input.key_pressed(KeyCode::KeyC) { self.cycle_idx = (self.cycle_idx + 1) % 3; let multiplier = match self.cycle_idx { 0 => 1.0, 1 => 2.0, _ => 0.5, }; for i in 0..5 { let mat_id = format!("glow_mat_{}", i); let c = COLORS[i]; let intensity = INTENSITIES[i] * multiplier; if let Ok(()) = app.scene.set_material_emissive(&mat_id, [c[0], c[1], c[2], intensity]) { eprintln!("emissive multiplier = {:.1}x", multiplier); } } } // Slow rotation. self.angle += 0.01; for i in 0..5 { let entity_id = format!("glow_e_{}", i); if let Some(base) = app.scene.entity_transform(&entity_id) { let mut tf = *base; tf.rotation = Quat::from_rotation_y(self.angle * (1.0 + i as f32 * 0.2)); app.scene.set_entity_transform(&entity_id, 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> { // HDR enabled so emissive > 1.0 produces true glow (not clamped to white). let app = AppBuilder::new() .title("WSG Emissive") .size(960, 640) .with_hdr(ToneMapper::Aces) .build() .await?; app.run(EmissiveDemo { camera: CameraController::default(), angle: 0.0, cycle_idx: 0, }) }