This commit is contained in:
Jérôme Bousquié
2026-09-25 13:43:59 +02:00
parent 9614156848
commit 8ece89ccba
22 changed files with 1998 additions and 245 deletions
+39
View File
@@ -18,6 +18,7 @@ cargo run -p wsg-lib --example <nom>
| `shadow` | Shadow mapping (ombre portée directionnelle) |
| `culling` | Culling GPU-driven (grille 15×15, objets hors frustum ignorés) |
| `msaa` | MSAA 4× (anti-aliasing multi-échantillons, arêtes lisses) |
| `fog` | Brouillard de distance (3 modes : linéaire, exp, exp²) |
| `manual` | Workflow bas niveau (Context + Renderer + PipelineCache) |
| `import` | Import de fichier OBJ (non graphique, stdout) |
@@ -251,6 +252,44 @@ identique, seules les arêtes diffèrent (escaler vs lisse).
---
## `fog` — Brouillard de distance
Démontre les 3 modes de brouillard : **linéaire**, **exponentiel**, **exponentiel²**.
La scène contient une rangée de cubes qui s'éloignent et des sphères dispersées sur
un grand plan au sol. Le brouillard fond les objets vers une couleur de fond,
créant l'illusion d'un monde infini.
```sh
cargo run -p wsg-lib --example fog --features "all-prims"
```
**Touches** : `1` = linéaire, `2` = exp, `3` = exp², `4` = désactivé, `R` = reset.
> Le brouillard est appliqué dans le shader fragment principal (après l'éclairage,
> avant le tone mapping). Il utilise la distance euclidienne du fragment à la caméra.
---
## `dof` — Depth of Field (bokeh cinématique)
Démontre le flou de profondeur de champ : un objet au centre reste net tandis que
le premier et arrière-plan se flouent selon leur distance au plan de mise au point.
Crée un effet d'attention naturelle (type cinématique).
La scène contient un cube de focus au centre, des sphères en premier plan (proches)
et des cubes en arrière-plan (loin), sur un plan au sol.
```sh
cargo run -p wsg-lib --example dof --features "all-prims"
```
**Touches** : `1` = cinématique, `2` = subtil, `3` = focus 2m, `4` = focus 10m, `5` = off, `R` = reset.
> DoF opère en HDR linéaire (après bloom, avant tone mapping). Deux passes :
> CoC (depth → rayon de flou par pixel) puis blur disque 12-taps à rayon variable.
---
## `manual` — Workflow bas niveau
Démontre l'API **sans** la façade `App` : utilisation directe de `Context`,
+175
View File
@@ -0,0 +1,175 @@
//! # 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::AppHandler;
use wsg_lib::utils::WsgError;
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();
// Ground plane.
app.scene
.create_mesh("ground_mesh", plane(80.0, 80.0, 1, 1), None)
.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), None)
.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(),
})
}
+163
View File
@@ -0,0 +1,163 @@
//! # Fog Example (Étape 25)
//!
//! Demonstrates distance fog: objects fade into the fog color as they recede,
//! creating the illusion of an infinite world (Skyrim/GTA pattern).
//!
//! The scene has a row of cubes receding into the distance and scattered spheres,
//! all sitting on a large ground plane. Switch fog modes with number keys to
//! compare the three attenuation curves.
//!
//! ## Pipeline
//! Fog is applied in the main pass fragment shader (after lighting, before tone
//! mapping). It uses the fragment's world-space distance to the camera and
//! blends the final color toward `fog_color`.
//!
//! ## Controls
//! | Key | Action |
//! |-----|--------|
//! | Drag (LMB) | Orbit camera |
//! | Wheel | Zoom |
//! | `1` | Linear fog (near=5, far=30) |
//! | `2` | Exponential fog (density=0.04) |
//! | `3` | Exponential² fog (density=0.06) — best for masking |
//! | `4` | Fog OFF |
//! | `R` | Reset camera |
//!
//! ## Build & Run
//! ```sh
//! cargo run -p wsg-lib --example fog --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::{FogConfig, ToneMapper, Transform};
use wsg_lib::mesh::{cube, icosphere, plane};
use wsg_lib::AppHandler;
use wsg_lib::utils::WsgError;
struct FogDemo {
camera: CameraController,
}
impl AppHandler for FogDemo {
fn setup(&mut self, app: &mut wsg_lib::App) {
app.scene
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
.unwrap();
// Large ground plane — will fade into fog at distance.
app.scene
.create_mesh("ground_mesh", plane(80.0, 80.0, 1, 1), None)
.unwrap();
app.scene.add_entity("ground", "ground_mesh").unwrap();
// Row of cubes receding into the distance.
app.scene
.create_mesh("cube_mesh", cube(1.0), None)
.unwrap();
for i in 0..15 {
let z = -2.0 - i as f32 * 2.5;
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();
}
// Scattered spheres at various distances.
app.scene
.create_mesh("sphere_mesh", icosphere(0.8, 3), None)
.unwrap();
let positions = [
Vec3::new(3.0, 0.8, -5.0),
Vec3::new(-4.0, 0.8, -10.0),
Vec3::new(5.0, 0.8, -15.0),
Vec3::new(-3.0, 0.8, -20.0),
Vec3::new(0.0, 0.8, -30.0),
];
for (i, pos) in 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.5, -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.15;
self.camera.distance = 8.0;
self.camera.target = Vec3::new(0.0, 0.5, -8.0);
self.camera.apply_to(app.scene.camera_mut());
// Print initial fog status.
eprintln!("[Fog] Initial: Exponential² (density=0.06)");
eprintln!("[Fog] Keys: 1=linear 2=exp 3=exp² 4=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);
// Fog mode switching.
if app.input.key_pressed(KeyCode::Digit1) {
app.renderer_mut()
.set_fog(Some(FogConfig::linear([0.7, 0.75, 0.85], 5.0, 30.0)));
eprintln!("[Fog] → Linear (near=5, far=30)");
}
if app.input.key_pressed(KeyCode::Digit2) {
app.renderer_mut()
.set_fog(Some(FogConfig::exponential([0.7, 0.75, 0.85], 0.04)));
eprintln!("[Fog] → Exponential (density=0.04)");
}
if app.input.key_pressed(KeyCode::Digit3) {
app.renderer_mut()
.set_fog(Some(FogConfig::exponential2([0.7, 0.75, 0.85], 0.06)));
eprintln!("[Fog] → Exponential² (density=0.06)");
}
if app.input.key_pressed(KeyCode::Digit4) {
app.renderer_mut().set_fog(None);
eprintln!("[Fog] → OFF");
}
if app.input.key_pressed(KeyCode::KeyR) {
self.camera.yaw = 0.0;
self.camera.pitch = 0.15;
self.camera.distance = 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 — Fog (3 modes)")
.size(1024, 640)
.with_fog(FogConfig::exponential2([0.7, 0.75, 0.85], 0.06))
.with_hdr(ToneMapper::Aces)
.build()
.await?;
app.run(FogDemo {
camera: CameraController::default(),
})
}
+1 -1
View File
@@ -63,7 +63,7 @@ impl ApplicationHandler for App {
// Flat 2D rendering: `standard` in unlit mode (the frame+object bind groups are set by
// draw_entity, the default frame matrix is the identity → NDC positions unchanged).
let mut renderer = Renderer::new(&context, format, 800, 600, &ShadowConfig::default(), None, None, None);
let mut renderer = Renderer::new(&context, format, 800, 600, &ShadowConfig::default(), None, None, None, None, None);
renderer.set_unlit(true);
// 3. Material: uses renderer.device() and renderer.format()