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

171 lines
5.6 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.
//! **HDR + Tone Mapping** — demonstrates HDR rendering with exposure control.
//!
//! Shows the difference between ACES and Reinhard tone mapping curves, and how
//! exposure affects the final image. A bright emissive sphere (intensity 3.0)
//! demonstrates highlight rolloff: without HDR it would clip to white, with
//! ACES it rolls off smoothly.
//!
//! ## 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 to 1.0 |
//!
//! ## Build & Run
//! ```sh
//! cargo run -p wsg-lib --example hdr
//! ```
//!
//! Note: tone mapper is selected at build time (pipeline compiled once). To compare
//! ACES vs Reinhard, run twice with different flags or modify the source.
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;
struct HdrDemo {
camera: CameraController,
angle: f32,
}
impl AppHandler for HdrDemo {
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();
// Lit cube (normal brightness, no emissive).
app.scene
.create_mesh("cube_mesh", cube(0.8), None)
.unwrap();
let mut cube_tf = Transform::identity();
cube_tf.translation = Vec3::new(1.5, 0.4, 0.0);
app.scene
.add_entity_with_transform("cube_e", "cube_mesh", cube_tf)
.unwrap();
// Bright sphere (emissive 3.0 — demonstrates HDR highlight rolloff).
app.scene
.add_material_shader("bright_mat", "standard")
.unwrap();
app.scene
.set_material_emissive("bright_mat", [1.0, 0.9, 0.7, 3.0])
.unwrap();
app.scene
.create_mesh("bright_mesh", icosphere(0.4, 3), Some("bright_mat"))
.unwrap();
let mut bright_tf = Transform::identity();
bright_tf.translation = Vec3::new(0.0, 0.5, 0.0);
app.scene
.add_entity_with_transform("bright_e", "bright_mesh", bright_tf)
.unwrap();
// Dim sphere (emissive 0.3 — stays dark even at high exposure).
app.scene
.add_material_shader("dim_mat", "standard")
.unwrap();
app.scene
.set_material_emissive("dim_mat", [0.2, 0.4, 1.0, 0.3])
.unwrap();
app.scene
.create_mesh("dim_mesh", icosphere(0.3, 3), Some("dim_mat"))
.unwrap();
let mut dim_tf = Transform::identity();
dim_tf.translation = Vec3::new(-1.5, 0.4, 0.0);
app.scene
.add_entity_with_transform("dim_e", "dim_mesh", dim_tf)
.unwrap();
// Strong 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.85], 2.0)
.unwrap();
app.scene.set_ambient([0.1, 0.1, 0.12]);
// Camera.
self.camera.yaw = 0.3;
self.camera.pitch = 0.25;
self.camera.distance = 5.0;
self.camera.target = Vec3::new(0.0, 0.4, 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.3;
self.camera.pitch = 0.25;
self.camera.distance = 5.0;
}
self.camera.apply_to(app.scene.camera_mut());
// Exposure control.
if app.input.key_pressed(KeyCode::KeyE) {
app.set_exposure(app.exposure() * 1.3);
eprintln!("exposure = {:.3}", app.exposure());
}
if app.input.key_pressed(KeyCode::KeyQ) {
app.set_exposure(app.exposure() / 1.3);
eprintln!("exposure = {:.3}", app.exposure());
}
if app.input.key_pressed(KeyCode::Digit0) {
app.set_exposure(1.0);
eprintln!("exposure reset to 1.0");
}
// Rotate the bright sphere to show specular highlights.
self.angle += 0.008;
let mut tf = *app
.scene
.entity_transform("bright_e")
.expect("bright entity present");
tf.rotation = Quat::from_rotation_y(self.angle);
app.scene.set_entity_transform("bright_e", 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> {
// ACES Filmic tone mapping — cinematic contrast with smooth highlight rolloff.
// Change to ToneMapper::Reinhard to compare (flatter, less contrast).
let app = AppBuilder::new()
.title("WSG HDR (ACES)")
.size(960, 640)
.with_hdr(ToneMapper::Aces)
.with_exposure(1.0)
.build()
.await?;
app.run(HdrDemo {
camera: CameraController::default(),
angle: 0.0,
})
}