//! # Exemple PBR — Metallic/Roughness + Normal Mapping (Étape 27) //! //! Démonstration du workflow PBR Cook-Torrance (GGX + Smith + Schlick) avec IBL hémisphérique. //! //! ## Scène //! - Sol : plan 20×20, PBR matte (metallic=0, roughness=0.8) //! - Cube métal : metallic=1.0, roughness=0.1 → reflet spéculaire net (miroir) //! - Cube plastique : metallic=0.0, roughness=0.4 → spéculaire large et doux //! - Cube rouillé : metallic=0.8, roughness=0.7 → métal rugueux //! - Sphere céramique : metallic=0.3, roughness=0.3 //! - Cube normal map : bump procédural (sin wave) //! //! ## Contrôles //! | Touche | Action | //! |--------|--------| //! | Drag (LMB) | Orbite caméra | //! | Molette | Zoom | //! | `R` | Reset caméra | //! //! ## Lancement //! ```bash //! cargo run -p wsg-lib --example pbr //! ``` 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::{ToneMapper, Transform}; use wsg_lib::mesh::{cube, icosphere, plane}; use wsg_lib::resources::Texture; use wsg_lib::AppHandler; use wsg_lib::utils::WsgError; struct PbrDemo { camera: CameraController, } impl Default for PbrDemo { fn default() -> Self { Self { camera: CameraController::default(), } } } impl AppHandler for PbrDemo { fn setup(&mut self, app: &mut wsg_lib::App) { app.scene .register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH) .unwrap(); // Normal map procédurale 256×256 : bump sin(x)*sin(y). let bump_map = make_bump_normal_map(&app.context().device, &app.context().queue); app.scene.add_texture("bump_nm", bump_map).unwrap(); // Matériaux PBR. app.scene.add_material_pbr("floor", "standard", 0.0, 0.8).unwrap(); app.scene.add_material_pbr("metal", "standard", 1.0, 0.1).unwrap(); app.scene.add_material_pbr("plastic", "standard", 0.0, 0.4).unwrap(); app.scene.add_material_pbr("rust", "standard", 0.8, 0.7).unwrap(); app.scene.add_material_pbr("ceramic", "standard", 0.3, 0.3).unwrap(); app.scene .add_material_pbr_textured("bump", "standard", 0.0, 0.5, None, Some("bump_nm")) .unwrap(); // Sol (plan 20×20). app.scene .create_mesh("floor_mesh", plane(1.0, 1.0, 1, 1), Some("floor")) .unwrap(); { let mut tf = Transform::identity(); tf.translation = Vec3::new(0.0, 0.0, 0.0); tf.scale = Vec3::new(20.0, 1.0, 20.0); app.scene.add_entity_with_transform("floor", "floor_mesh", tf).unwrap(); } // Cubes. app.scene.create_mesh("cube_mesh", cube(1.0), None).unwrap(); let cubes: [(&str, &str, Vec3); 4] = [ ("c_metal", "metal", Vec3::new(-3.0, 0.5, 0.0)), ("c_plastic", "plastic", Vec3::new(-1.0, 0.5, 0.0)), ("c_rust", "rust", Vec3::new(1.0, 0.5, 0.0)), ("c_bump", "bump", Vec3::new(3.0, 0.5, 0.0)), ]; for (id, mat, pos) in &cubes { app.scene .create_mesh(&format!("{id}_mesh"), cube(1.0), Some(mat)) .unwrap(); let mut tf = Transform::identity(); tf.translation = *pos; app.scene .add_entity_with_transform(id, &format!("{id}_mesh"), tf) .unwrap(); } // Sphere céramique. app.scene .create_mesh("sphere_mesh", icosphere(0.5, 4), Some("ceramic")) .unwrap(); { let mut tf = Transform::identity(); tf.translation = Vec3::new(0.0, 0.5, -3.0); app.scene .add_entity_with_transform("s_ceramic", "sphere_mesh", tf) .unwrap(); } // Lumières. app.scene .add_directional_light(Vec3::new(-1.0, 2.0, 1.0).normalize(), [1.0, 0.95, 0.9], 2.0) .unwrap(); app.scene .add_point_light(Vec3::new(0.0, 3.0, 2.0), [0.3, 0.5, 1.0], 8.0, 5.0) .unwrap(); // Ambiance (IBL hémisphérique). app.scene.set_ambient([0.3, 0.35, 0.4]); // Caméra. self.camera.yaw = 0.0; self.camera.pitch = 0.3; self.camera.distance = 8.0; self.camera.target = Vec3::new(0.0, 0.5, 0.0); self.camera.apply_to(app.scene.camera_mut()); eprintln!("[PBR] Scene: 6 PBR materials (metal/plastic/rust/ceramic/bump/floor)"); eprintln!("[PBR] Drag=orbit, Wheel=zoom, R=reset"); } fn update(&mut self, app: &mut wsg_lib::App) { // Orbite caméra. 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); self.camera.apply_to(app.scene.camera_mut()); // R = reset. if app.input.key_pressed(KeyCode::KeyR) { self.camera = CameraController::default(); self.camera.target = Vec3::new(0.0, 0.5, 0.0); self.camera.apply_to(app.scene.camera_mut()); } } } /// Génère une normal map procédurale 256×256 : pattern sin(x*freq)*sin(y*freq) → bump. /// Chaque pixel : normale perturbée encodée en RGB (nx*0.5+0.5, ny*0.5+0.5, nz*0.5+0.5) * 255. fn make_bump_normal_map(device: &wgpu::Device, queue: &wgpu::Queue) -> Texture { let size = 256u32; let freq = 8.0; let mut pixels: Vec = vec![0u8; (size * size * 4) as usize]; for y in 0..size { for x in 0..size { let u = x as f32 / size as f32; let v = y as f32 / size as f32; let h = (u * freq * std::f32::consts::PI).sin() * (v * freq * std::f32::consts::PI).sin(); let eps = 1.0 / size as f32; let hx = ((u + eps) * freq * std::f32::consts::PI).sin() * (v * freq * std::f32::consts::PI).sin(); let hy = (u * freq * std::f32::consts::PI).sin() * ((v + eps) * freq * std::f32::consts::PI).sin(); let dhdx = (hx - h) / eps; let dhdy = (hy - h) / eps; let n = Vec3::new(-dhdx, -dhdy, 1.0).normalize(); let idx = ((y * size + x) * 4) as usize; pixels[idx] = ((n.x * 0.5 + 0.5) * 255.0).clamp(0.0, 255.0) as u8; pixels[idx + 1] = ((n.y * 0.5 + 0.5) * 255.0).clamp(0.0, 255.0) as u8; pixels[idx + 2] = ((n.z * 0.5 + 0.5) * 255.0).clamp(0.0, 255.0) as u8; pixels[idx + 3] = 255; } } Texture::from_rgba8(device, queue, size, size, &pixels, "bump_normal_map") .expect("bump normal map creation failed") } #[pollster::main] async fn main() -> Result<(), WsgError> { let app = AppBuilder::new() .title("WSG — PBR Metallic/Roughness") .size(1280, 720) .with_hdr(ToneMapper::Aces) .build() .await?; app.run(PbrDemo::default()) }