Files
wsg/lib/examples/effects/dof.rs
T
Jérôme Bousquié e5f3636b42 examples: apply real texture assets to multi-mesh examples
- meshes/cube: procedural checkerboard -> uv_texture.jpg (8x8 UV grid)
- meshes/pbr: floor -> ground.jpeg, bump cube -> cave.jpg + caveNormal.jpg
  (normal map pre-encoded via sRGB OETF to cancel the GPU sRGB decode)
- lights/shadow: ground -> ground.jpeg, cube -> uv_texture.jpg
- effects/demo: ground -> ground.jpeg, cube -> uv_texture.jpg
- effects/fog: ground -> ground.jpeg (tiled 80x80), cubes -> stonewall.jpg
- effects/dof: ground -> ground.jpeg, cubes -> uv_texture.jpg
- cameras/culling: shared cube mesh -> uv_texture.jpg
- add lib/examples/assets/textures/ (19 assets, 6.5 MB)
- document assets + usage in examples READMEs, docs/user/examples.md,
  docs/user/meshes/materials.md (CARGO_MANIFEST_DIR pattern, sRGB caveat)
2026-09-26 10:49:13 +02:00

204 lines
7.2 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.
//! # Depth of Field Example (Étape 26)
//!
//! Demonstrates cinematic DoF: a row of cubes receding into the distance,
//! with the focus plane at a configurable depth. Cubes at the focus distance
//! stay sharp; those closer or farther blur proportionally.
//!
//! ## Pipeline
//! DoF operates in linear HDR space **after** bloom and **before** tone mapping:
//! 1. CoC pass: reads the depth buffer, linearizes to world distance, computes
//! per-pixel blur radius.
//! 2. Blur pass: 12-tap disc blur with variable radius (from CoC), producing
//! natural circular bokeh.
//!
//! ## Controls
//! | Key | Action |
//! |-----|--------|
//! | Drag (LMB) | Orbit camera |
//! | Wheel | Zoom |
//! | `1` | Cinematic preset (focus=8m, strong blur) |
//! | `2` | Subtle preset (focus=8m, gentle blur) |
//! | `3` | Focus at 3m (near cubes sharp, far blurred) |
//! | `4` | Focus at 15m (far cubes sharp, near blurred) |
//! | `5` | DoF OFF |
//! | `R` | Reset camera |
//!
//! ## Build & Run
//! ```sh
//! cargo run -p wsg-lib --example dof --features "all-prims"
//! ```
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::{DoFConfig, ToneMapper, Transform};
use wsg_lib::mesh::{cube, icosphere, plane};
use wsg_lib::resources::Texture;
use wsg_lib::AppHandler;
use wsg_lib::utils::WsgError;
/// Texture asset directory, resolved against the crate root so the example works from any CWD.
const TEXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/assets/textures");
struct DoFDemo {
camera: CameraController,
}
impl AppHandler for DoFDemo {
fn setup(&mut self, app: &mut wsg_lib::App) {
app.scene
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
.unwrap();
// Textured ground + cubes: bokeh blur reads much better on textured surfaces.
// ground.jpeg tiles across the 80×80 floor (Repeat sampler); uv_texture.jpg on the cubes.
let (device, queue) = {
let ctx = app.context();
(ctx.device.clone(), ctx.queue.clone())
};
let ground_tex =
Texture::from_file(&device, &queue, "ground", &format!("{TEXTURES}/ground.jpeg")).unwrap();
app.scene.add_texture("ground_texture", ground_tex).unwrap();
app.scene
.add_material_texture("ground_mat", "standard", "ground_texture")
.unwrap();
let uv_tex = Texture::from_file(
&device,
&queue,
"uv_atlas",
&format!("{TEXTURES}/uv_texture.jpg"),
)
.unwrap();
app.scene.add_texture("uv_texture", uv_tex).unwrap();
app.scene
.add_material_texture("cube_mat", "standard", "uv_texture")
.unwrap();
// Ground plane.
app.scene
.create_mesh("ground_mesh", plane(80.0, 80.0, 1, 1), Some("ground_mat"))
.unwrap();
app.scene
.add_entity_with_transform(
"ground",
"ground_mesh",
Transform::identity(),
)
.unwrap();
// Row of cubes receding along -Z (distance ≈ 2 to 25 from camera at dist=8).
app.scene
.create_mesh("cube_mesh", cube(1.0), Some("cube_mat"))
.unwrap();
for i in 0..20 {
let z = 3.0 - i as f32 * 1.5; // from z=3 (close) to z=-25.5 (far)
let mut tf = Transform::identity();
tf.translation = Vec3::new(0.0, 0.5, z);
app.scene
.add_entity_with_transform(&format!("cube_{i}"), "cube_mesh", tf)
.unwrap();
}
// A few spheres scattered to the sides for visual interest.
app.scene
.create_mesh("sphere_mesh", icosphere(0.7, 3), None)
.unwrap();
let sphere_positions = [
Vec3::new(2.5, 0.7, -2.0),
Vec3::new(-3.0, 0.7, -6.0),
Vec3::new(3.5, 0.7, -10.0),
Vec3::new(-2.0, 0.7, -14.0),
Vec3::new(2.0, 0.7, -18.0),
];
for (i, pos) in sphere_positions.iter().enumerate() {
let mut tf = Transform::identity();
tf.translation = *pos;
app.scene
.add_entity_with_transform(&format!("sphere_{i}"), "sphere_mesh", tf)
.unwrap();
}
// Directional light.
let light_dir = Vec3::new(-0.4, -1.0, -0.3).normalize();
app.scene
.add_directional_light(light_dir, [1.0, 0.95, 0.85], 1.2)
.unwrap();
app.scene.set_ambient([0.08, 0.08, 0.1]);
// Camera — positioned to look down the row of cubes.
self.camera.yaw = 0.0;
self.camera.pitch = 0.1;
self.camera.distance = 8.0;
self.camera.target = Vec3::new(0.0, 0.5, -8.0);
self.camera.apply_to(app.scene.camera_mut());
eprintln!("[DoF] Initial: Cinematic (focus=8m, aperture=0.3, max_blur=12)");
eprintln!("[DoF] Keys: 1=cinematic 2=subtle 3=focus 3m 4=focus 15m 5=off R=reset");
}
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);
// DoF presets.
if app.input.key_pressed(KeyCode::Digit1) {
app.renderer_mut()
.set_dof(Some(DoFConfig::cinematic(8.0)));
eprintln!("[DoF] → Cinematic (focus=8m, aperture=0.3, max_blur=12)");
}
if app.input.key_pressed(KeyCode::Digit2) {
app.renderer_mut()
.set_dof(Some(DoFConfig::subtle(8.0)));
eprintln!("[DoF] → Subtle (focus=8m, aperture=0.1, max_blur=8)");
}
if app.input.key_pressed(KeyCode::Digit3) {
app.renderer_mut()
.set_dof(Some(DoFConfig::new(3.0, 0.3, 12.0)));
eprintln!("[DoF] → Focus 3m (near sharp, far blurred)");
}
if app.input.key_pressed(KeyCode::Digit4) {
app.renderer_mut()
.set_dof(Some(DoFConfig::new(15.0, 0.3, 12.0)));
eprintln!("[DoF] → Focus 15m (far sharp, near blurred)");
}
if app.input.key_pressed(KeyCode::Digit5) {
app.renderer_mut().set_dof(None);
eprintln!("[DoF] → OFF");
}
if app.input.key_pressed(KeyCode::KeyR) {
self.camera.yaw = 0.0;
self.camera.pitch = 0.1;
self.camera.distance = 8.0;
self.camera.target = Vec3::new(0.0, 0.5, -8.0);
}
self.camera.apply_to(app.scene.camera_mut());
}
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> {
let app = AppBuilder::new()
.title("WSG — Depth of Field (Étape 26)")
.size(1280, 720)
.with_hdr(ToneMapper::Aces)
.with_dof(DoFConfig::cinematic(8.0))
.build()
.await?;
app.run(DoFDemo {
camera: CameraController::default(),
})
}