Files
wsg/lib/examples/meshes/pbr.rs
T
Jérôme Bousquié 24fbafc810 réorg doc
2026-09-25 19:08:20 +02:00

195 lines
6.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! # 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<u8> = 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())
}