171 lines
5.6 KiB
Rust
171 lines
5.6 KiB
Rust
//! **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,
|
||
})
|
||
}
|