réorg doc
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
# Effects: HDR, Post-process & Showcase
|
||||
|
||||
Examples covering **HDR / tone mapping** and **post-process effects**, plus
|
||||
the full showcase that combines everything.
|
||||
|
||||
| Example | Run command | What it shows |
|
||||
|---------|-------------|---------------|
|
||||
| `demo` | `cargo run -p wsg-lib --example demo` | **Full showcase**: 6 LOD primitives, 3 lights, shadows, HDR/ACES, bloom, culling, orbital camera |
|
||||
| `bloom` | `cargo run -p wsg-lib --example bloom` | Post-process bloom (glow around bright areas) |
|
||||
| `hdr` | `cargo run -p wsg-lib --example hdr` | HDR + tone mapping (ACES) + runtime exposure control |
|
||||
| `msaa` | `cargo run -p wsg-lib --example msaa` | MSAA 4× (multisample anti-aliasing, smooth edges) |
|
||||
| `fog` | `cargo run -p wsg-lib --example fog --features "all-prims"` | Distance fog (3 modes: linear, exp, exp²) |
|
||||
| `dof` | `cargo run -p wsg-lib --example dof --features "all-prims"` | Depth of field (cinematic bokeh, focus presets) |
|
||||
|
||||
> All commands run from the repo root. All effects are **opt-in** — a disabled
|
||||
> effect allocates nothing and executes nothing.
|
||||
|
||||
---
|
||||
|
||||
## `demo` — Full Showcase
|
||||
|
||||
Combines **all** effects: LOD primitives, procedural textures, lights
|
||||
(directional + point + spot), shadows, HDR/ACES, exposure, emissive, bloom, culling.
|
||||
|
||||
```sh
|
||||
cargo run -p wsg-lib --example demo
|
||||
```
|
||||
|
||||
### Keys
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| Drag (LMB) | Orbit camera |
|
||||
| Wheel | Zoom |
|
||||
| `R` | Reset camera |
|
||||
| `1` / `2` / `3` | Presets: front / side / top |
|
||||
| `+` / `-` | Exposure ×1.3 / ÷1.3 |
|
||||
| `0` | Reset exposure |
|
||||
|
||||
---
|
||||
|
||||
## `bloom` — Post-process Bloom
|
||||
|
||||
Two emissive spheres (orange intensity 2.0, blue intensity 3.0) produce a
|
||||
visible halo. The cube and floor serve as reference (non-emissive).
|
||||
|
||||
Bloom is a 4-pass GPU pipeline: threshold → blur H → blur V → composite.
|
||||
|
||||
```sh
|
||||
cargo run -p wsg-lib --example bloom
|
||||
```
|
||||
|
||||
### Keys
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| Drag (LMB) | Orbit camera |
|
||||
| Wheel | Zoom |
|
||||
| `R` | Reset camera |
|
||||
| `+` / `-` | **Bloom threshold** +0.1 / −0.1 |
|
||||
| `[` / `]` | **Bloom intensity** +0.1 / −0.1 |
|
||||
| `I` / `O` | **Bloom radius** +0.5 / −0.5 |
|
||||
| `E` / `Q` | Exposure ×1.3 / ÷1.3 |
|
||||
| `0` | Reset exposure |
|
||||
|
||||
### What to observe
|
||||
|
||||
- **Low threshold** (0.0): the entire image "blooms" (very diffuse effect).
|
||||
- **High threshold** (2.0+): only the bright emissive spheres produce glow.
|
||||
- **Intensity 0.0**: no visible glow (even though the threshold extracts pixels).
|
||||
- **Large radius** (10+): the glow spreads over a large area.
|
||||
|
||||
---
|
||||
|
||||
## `hdr` — HDR + Tone Mapping
|
||||
|
||||
Demonstrates HDR rendering with the ACES Filmic curve. Three objects:
|
||||
|
||||
- **Cube**: normal lighting (no emissive) — LDR reference.
|
||||
- **Bright sphere** (emissive 3.0): without HDR, it would be clamped to white.
|
||||
With ACES, highlights "roll off" smoothly toward white.
|
||||
- **Dark sphere** (emissive 0.3): stays dark even at high exposure.
|
||||
|
||||
```sh
|
||||
cargo run -p wsg-lib --example hdr
|
||||
```
|
||||
|
||||
### Keys
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| Drag (LMB) | Orbit camera |
|
||||
| Wheel | Zoom |
|
||||
| `R` | Reset camera |
|
||||
| `E` | **Exposure ×1.3** (brighter) |
|
||||
| `Q` | **Exposure ÷1.3** (darker) |
|
||||
| `0` | Reset exposure to 1.0 |
|
||||
|
||||
### What to observe
|
||||
|
||||
- At exposure 1.0: the bright sphere is white but with detail (ACES rolloff).
|
||||
- At high exposure (E×E×E): the scene brightens, the bright sphere stays white
|
||||
(saturated), but the cube gains detail.
|
||||
- At low exposure (Q×Q): everything darkens, the bright sphere becomes orange
|
||||
(HDR values > 1.0 are compressed).
|
||||
|
||||
> **Note**: the tone mapper is compiled into the pipeline at build time. To
|
||||
> compare ACES vs Reinhard, change `ToneMapper::Aces` → `ToneMapper::Reinhard`
|
||||
> in the source.
|
||||
|
||||
---
|
||||
|
||||
## `msaa` — MSAA 4× (Anti-aliasing)
|
||||
|
||||
Demonstrates multisample anti-aliasing: object edges (cube, sphere) are smooth
|
||||
instead of "stair-stepped". The scene contains a cube (sharp edges), a sphere
|
||||
(curved silhouette), and a small cube near the camera (maximum aliasing).
|
||||
|
||||
```sh
|
||||
cargo run -p wsg-lib --example msaa
|
||||
```
|
||||
|
||||
### Keys
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| Drag (LMB) | Orbit camera |
|
||||
| Wheel | Zoom |
|
||||
| `R` | Reset camera |
|
||||
| `M` | Show sample count |
|
||||
|
||||
### To compare with/without MSAA
|
||||
|
||||
Remove the `.with_msaa(4)` line in the source and recompile: the scene is
|
||||
identical, only the edges differ (stair-stepped vs smooth).
|
||||
|
||||
> **Note**: MSAA is a build-time setting (multisample texture allocation). It
|
||||
> works independently of HDR: with HDR, the MSAA texture is `Rgba16Float` and
|
||||
> resolves into the HDR texture before bloom/TM.
|
||||
|
||||
---
|
||||
|
||||
## `fog` — Distance Fog
|
||||
|
||||
Demonstrates the 3 fog modes: **linear**, **exponential**, **exponential²**.
|
||||
The scene contains a row of cubes receding into the distance and scattered
|
||||
spheres on a large floor plane. Fog blends objects toward a background color,
|
||||
creating the illusion of an infinite world.
|
||||
|
||||
```sh
|
||||
cargo run -p wsg-lib --example fog --features "all-prims"
|
||||
```
|
||||
|
||||
**Keys**: `1` = linear, `2` = exp, `3` = exp², `4` = off, `R` = reset.
|
||||
|
||||
> Fog is applied in the main fragment shader (after lighting, before tone
|
||||
> mapping). It uses the Euclidean distance from the fragment to the camera.
|
||||
|
||||
---
|
||||
|
||||
## `dof` — Depth of Field (Cinematic Bokeh)
|
||||
|
||||
Demonstrates depth of field blur: an object at the focus plane stays sharp
|
||||
while foreground and background blur according to their distance from the
|
||||
focus plane. Creates a natural attention effect (cinematic style).
|
||||
|
||||
The scene contains 20 cubes in a row along Z (z=3 to z=-25.5) and 5 spheres to
|
||||
the sides, on a floor plane. Focus presets at 3 m / 8 m / 15 m.
|
||||
|
||||
```sh
|
||||
cargo run -p wsg-lib --example dof --features "all-prims"
|
||||
```
|
||||
|
||||
**Keys**: `1` = cinematic, `2` = subtle, `3` = focus 3 m, `4` = focus 15 m,
|
||||
`5` = off, `R` = reset.
|
||||
|
||||
> DoF operates in linear HDR (after bloom, before tone mapping). Two passes:
|
||||
> CoC (depth → per-pixel blur radius) then 12-tap disc blur with variable radius.
|
||||
@@ -0,0 +1,217 @@
|
||||
//! **Bloom** — demonstrates the bloom post-process with emissive materials.
|
||||
//!
|
||||
//! A glowing sphere (emissive intensity 2.0) produces a visible halo. The scene
|
||||
//! also contains a lit ground plane and a cube for reference.
|
||||
//!
|
||||
//! ## Controls
|
||||
//! | Key | Action |
|
||||
//! |-----|--------|
|
||||
//! | Drag (LMB) | Orbit camera |
|
||||
//! | Wheel | Zoom |
|
||||
//! | `R` | Reset camera |
|
||||
//! | `+` / `-` | Bloom threshold up/down |
|
||||
//! | `[` / `]` | Bloom intensity up/down |
|
||||
//! | `I` / `O` | Bloom radius up/down |
|
||||
//! | `E` | Exposure up (×1.3) |
|
||||
//! | `Q` | Exposure down (÷1.3) |
|
||||
//! | `0` | Reset exposure |
|
||||
//!
|
||||
//! ## Build & Run
|
||||
//! ```sh
|
||||
//! cargo run -p wsg-lib --example bloom
|
||||
//! ```
|
||||
|
||||
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::{BloomConfig, ToneMapper, Transform};
|
||||
use wsg_lib::mesh::{cube, icosphere, plane};
|
||||
use wsg_lib::AppHandler;
|
||||
use wsg_lib::utils::WsgError;
|
||||
|
||||
struct BloomDemo {
|
||||
camera: CameraController,
|
||||
angle: f32,
|
||||
/// Runtime bloom config (mirrors the App's internal state for display/adjustment).
|
||||
bloom: BloomConfig,
|
||||
}
|
||||
|
||||
impl AppHandler for BloomDemo {
|
||||
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(8.0, 8.0, 1, 1), None)
|
||||
.unwrap();
|
||||
app.scene.add_entity("ground", "ground_mesh").unwrap();
|
||||
|
||||
// Cube (lit, non-emissive — reference).
|
||||
app.scene
|
||||
.create_mesh("cube_mesh", cube(0.7), None)
|
||||
.unwrap();
|
||||
let mut cube_tf = Transform::identity();
|
||||
cube_tf.translation = Vec3::new(1.5, 0.35, 0.0);
|
||||
app.scene
|
||||
.add_entity_with_transform("cube_e", "cube_mesh", cube_tf)
|
||||
.unwrap();
|
||||
|
||||
// Glowing sphere (emissive intensity 2.0 → HDR bloom).
|
||||
app.scene
|
||||
.add_material_shader("glow_mat", "standard")
|
||||
.unwrap();
|
||||
app.scene
|
||||
.set_material_emissive("glow_mat", [1.0, 0.3, 0.05, 2.0])
|
||||
.unwrap();
|
||||
app.scene
|
||||
.create_mesh("glow_mesh", icosphere(0.35, 3), Some("glow_mat"))
|
||||
.unwrap();
|
||||
let mut glow_tf = Transform::identity();
|
||||
glow_tf.translation = Vec3::new(0.0, 0.5, 0.0);
|
||||
app.scene
|
||||
.add_entity_with_transform("glow_e", "glow_mesh", glow_tf)
|
||||
.unwrap();
|
||||
|
||||
// Second glow (blue, higher intensity for more dramatic bloom).
|
||||
app.scene
|
||||
.add_material_shader("blue_glow_mat", "standard")
|
||||
.unwrap();
|
||||
app.scene
|
||||
.set_material_emissive("blue_glow_mat", [0.2, 0.5, 1.0, 3.0])
|
||||
.unwrap();
|
||||
app.scene
|
||||
.create_mesh("blue_glow_mesh", icosphere(0.25, 3), Some("blue_glow_mat"))
|
||||
.unwrap();
|
||||
let mut blue_tf = Transform::identity();
|
||||
blue_tf.translation = Vec3::new(-1.5, 0.4, 0.0);
|
||||
app.scene
|
||||
.add_entity_with_transform("blue_glow_e", "blue_glow_mesh", blue_tf)
|
||||
.unwrap();
|
||||
|
||||
// Directional light (warm, from above-right).
|
||||
let light_dir = Vec3::new(1.0, 1.5, 0.8).normalize();
|
||||
app.scene
|
||||
.add_directional_light(light_dir, [1.0, 0.95, 0.88], 1.2)
|
||||
.unwrap();
|
||||
app.scene.set_ambient([0.12, 0.12, 0.15]);
|
||||
|
||||
// Camera.
|
||||
self.camera.yaw = 0.4;
|
||||
self.camera.pitch = 0.3;
|
||||
self.camera.distance = 5.0;
|
||||
self.camera.target = Vec3::new(0.0, 0.5, 0.0);
|
||||
self.camera.apply_to(app.scene.camera_mut());
|
||||
|
||||
// Sync bloom config from the App.
|
||||
if let Some(cfg) = app.bloom_config() {
|
||||
self.bloom = cfg.clone();
|
||||
}
|
||||
}
|
||||
|
||||
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.4;
|
||||
self.camera.pitch = 0.3;
|
||||
self.camera.distance = 5.0;
|
||||
}
|
||||
self.camera.apply_to(app.scene.camera_mut());
|
||||
|
||||
// Bloom threshold (+/-).
|
||||
if app.input.key_pressed(KeyCode::Equal) {
|
||||
self.bloom.threshold += 0.1;
|
||||
app.set_bloom_config(self.bloom.clone());
|
||||
eprintln!("bloom threshold = {:.2}", self.bloom.threshold);
|
||||
}
|
||||
if app.input.key_pressed(KeyCode::Minus) {
|
||||
self.bloom.threshold = (self.bloom.threshold - 0.1).max(0.0);
|
||||
app.set_bloom_config(self.bloom.clone());
|
||||
eprintln!("bloom threshold = {:.2}", self.bloom.threshold);
|
||||
}
|
||||
|
||||
// Bloom intensity ([/]).
|
||||
if app.input.key_pressed(KeyCode::BracketRight) {
|
||||
self.bloom.intensity += 0.1;
|
||||
app.set_bloom_config(self.bloom.clone());
|
||||
eprintln!("bloom intensity = {:.2}", self.bloom.intensity);
|
||||
}
|
||||
if app.input.key_pressed(KeyCode::BracketLeft) {
|
||||
self.bloom.intensity = (self.bloom.intensity - 0.1).max(0.0);
|
||||
app.set_bloom_config(self.bloom.clone());
|
||||
eprintln!("bloom intensity = {:.2}", self.bloom.intensity);
|
||||
}
|
||||
|
||||
// Bloom radius (I/O).
|
||||
if app.input.key_pressed(KeyCode::KeyI) {
|
||||
self.bloom.radius += 0.5;
|
||||
app.set_bloom_config(self.bloom.clone());
|
||||
eprintln!("bloom radius = {:.1}", self.bloom.radius);
|
||||
}
|
||||
if app.input.key_pressed(KeyCode::KeyO) {
|
||||
self.bloom.radius = (self.bloom.radius - 0.5).max(0.5);
|
||||
app.set_bloom_config(self.bloom.clone());
|
||||
eprintln!("bloom radius = {:.1}", self.bloom.radius);
|
||||
}
|
||||
|
||||
// Exposure (E/Q/0).
|
||||
if app.input.key_pressed(KeyCode::KeyE) {
|
||||
app.set_exposure(app.exposure() * 1.3);
|
||||
eprintln!("exposure = {:.2}", app.exposure());
|
||||
}
|
||||
if app.input.key_pressed(KeyCode::KeyQ) {
|
||||
app.set_exposure(app.exposure() / 1.3);
|
||||
eprintln!("exposure = {:.2}", app.exposure());
|
||||
}
|
||||
if app.input.key_pressed(KeyCode::Digit0) {
|
||||
app.set_exposure(1.0);
|
||||
eprintln!("exposure reset to 1.0");
|
||||
}
|
||||
|
||||
// Slow rotation of the glow spheres.
|
||||
self.angle += 0.01;
|
||||
let mut tf = *app
|
||||
.scene
|
||||
.entity_transform("glow_e")
|
||||
.expect("glow entity present");
|
||||
tf.rotation = Quat::from_rotation_y(self.angle);
|
||||
app.scene.set_entity_transform("glow_e", tf);
|
||||
|
||||
let mut tf2 = *app
|
||||
.scene
|
||||
.entity_transform("blue_glow_e")
|
||||
.expect("blue glow entity present");
|
||||
tf2.rotation = Quat::from_rotation_y(-self.angle * 0.7);
|
||||
app.scene.set_entity_transform("blue_glow_e", tf2);
|
||||
}
|
||||
|
||||
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 Bloom")
|
||||
.size(960, 640)
|
||||
.with_hdr(ToneMapper::Aces)
|
||||
.with_bloom(BloomConfig::default())
|
||||
.build()
|
||||
.await?;
|
||||
app.run(BloomDemo {
|
||||
camera: CameraController::default(),
|
||||
angle: 0.0,
|
||||
bloom: BloomConfig::default(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
//! **WSG `demo`** — the final showcase example.
|
||||
//!
|
||||
//! Combines everything built throughout the library into one declarative scene:
|
||||
//!
|
||||
//! * a **ground plane** plus one of each procedural primitive from `math::primitives`
|
||||
//! (`cube`, `uv_sphere`, `icosphere`, `cylinder`, `cone`, `torus`) placed around it,
|
||||
//! * a **procedural texture** per mesh (checker / stripe grids, no assets on disk),
|
||||
//! * the **standard** Phong material wired to those textures,
|
||||
//! * an **orbital camera** driven live by the unified input state:
|
||||
//! hold the **left mouse button** and drag to orbit (yaw/pitch), the wheel zooms (distance),
|
||||
//! * `R` resets the view, keys `1`/`2`/`3` jump to front / side / top presets,
|
||||
//! * a **directional** light (the shadow caster) + a **point** light + a **spot** light,
|
||||
//! so the shadow of the cube and the colored light halos are all visible,
|
||||
//! * the primitives slowly rotate in `update`, so depth, lighting and shadows read clearly,
|
||||
//! * **LOD** (Step 19): the rounded primitives are created with three levels each
|
||||
//! (`create_mesh_with_lod`, auto-decimated by halving targets); the CPU picks each entity's
|
||||
//! level from its projected screen size (with hysteresis) — zoom in/out with the wheel and
|
||||
//! the sphere/cylinder/cone/torus visibly lose detail as they shrink on screen.
|
||||
//! * **HDR + Tone Mapping** (Étape 20): the demo enables ACES Filmic tone mapping via
|
||||
//! `AppBuilder::with_hdr(ToneMapper::Aces)`. The main pass renders to an offscreen
|
||||
//! `Rgba16Float` texture, then a fullscreen TM pass compresses it to [0,1] and writes
|
||||
//! to the sRGB surface — highlights are softly rolled off instead of clipping to white.
|
||||
//! * **Exposure** (Étape 22, 6.1): keys `+` / `-` adjust the tone mapping exposure live
|
||||
//! (×1.3 / ÷1.3 per press), `0` resets to 1.0.
|
||||
//! * **Emissive** (Étape 22, 6.2): a small glowing orange sphere sits at the center
|
||||
//! (emissive intensity 2.0 → HDR glow, visible even in shadow).
|
||||
//!
|
||||
//! Doc (this header) follows the English convention used for examples; internal comments stay
|
||||
//! concise and French where helpful. Run with:
|
||||
//!
|
||||
//! `cargo run -p wsg-lib --example demo`
|
||||
|
||||
use glam::{Quat, Vec3};
|
||||
use winit::event::MouseButton;
|
||||
use winit::keyboard::KeyCode;
|
||||
use wsg_lib::AppHandler;
|
||||
use wsg_lib::app::AppBuilder;
|
||||
use wsg_lib::core::BloomConfig;
|
||||
use wsg_lib::core::ToneMapper;
|
||||
use wsg_lib::core::Transform;
|
||||
use wsg_lib::mesh::{cone, cube, cylinder, icosphere, plane, torus, uv_sphere};
|
||||
use wsg_lib::camera::CameraController;
|
||||
use wsg_lib::resources::Texture;
|
||||
use wsg_lib::utils::WsgError;
|
||||
|
||||
/// Generates an 8×8 RGBA checkerboard (white / brick) as raw bytes for `Texture::from_rgba8`.
|
||||
fn checkerboard_rgba() -> Vec<u8> {
|
||||
const SIZE: u32 = 8;
|
||||
let mut rgba = Vec::with_capacity((SIZE * SIZE * 4) as usize);
|
||||
for y in 0..SIZE {
|
||||
for x in 0..SIZE {
|
||||
let even = (x + y) % 2 == 0;
|
||||
let (r, g, b) = if even { (235, 235, 228) } else { (150, 90, 70) };
|
||||
rgba.extend_from_slice(&[r, g, b, 255]);
|
||||
}
|
||||
}
|
||||
rgba
|
||||
}
|
||||
|
||||
/// Generates a vertical stripe texture (blue / cyan), useful to make rotation visible on rounded
|
||||
/// bodies (sphere / cylinder) via the UV seams.
|
||||
fn stripes_rgba() -> Vec<u8> {
|
||||
const W: u32 = 32;
|
||||
const H: u32 = 16;
|
||||
let mut rgba = Vec::with_capacity((W * H * 4) as usize);
|
||||
for _y in 0..H {
|
||||
for x in 0..W {
|
||||
let band = (x / 4) % 2 == 0;
|
||||
let (r, g, b) = if band { (40, 90, 190) } else { (120, 210, 235) };
|
||||
rgba.extend_from_slice(&[r, g, b, 255]);
|
||||
}
|
||||
}
|
||||
rgba
|
||||
}
|
||||
|
||||
/// Demo handler: holds the orbital controller plus a slow rotation angle.
|
||||
struct Demo {
|
||||
camera: CameraController,
|
||||
angle: f32,
|
||||
/// Phase 3 black-window investigation: number of debug_dump calls already made.
|
||||
dbg: u32,
|
||||
}
|
||||
|
||||
/// Horizontal radius at which the primitives sit around the origin.
|
||||
const ORBIT_RADIUS: f32 = 1.7;
|
||||
/// Vertical offset so the meshes stand on the ground plane (y = 0).
|
||||
const STAND_HEIGHT: f32 = 0.5;
|
||||
|
||||
/// Lays out one primitive (already scaled/positioned) at an angle around the origin.
|
||||
fn place(label: &str, mesh: &str, app: &mut wsg_lib::App, index: usize) {
|
||||
let a = index as f32 / 6.0 * std::f32::consts::TAU;
|
||||
let mut tf = Transform::identity();
|
||||
tf.translation = Vec3::new(a.cos() * ORBIT_RADIUS, STAND_HEIGHT, a.sin() * ORBIT_RADIUS);
|
||||
tf.rotation = Quat::from_rotation_y(a); // face the center
|
||||
app.scene
|
||||
.add_entity_with_transform(label, mesh, tf)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
impl AppHandler for Demo {
|
||||
fn setup(&mut self, app: &mut wsg_lib::App) {
|
||||
// 1. Shader + material base.
|
||||
app.scene
|
||||
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
|
||||
.unwrap();
|
||||
|
||||
let (device, queue) = {
|
||||
let ctx = app.context();
|
||||
(ctx.device.clone(), ctx.queue.clone())
|
||||
};
|
||||
|
||||
// 2. Procedural textures, one material per pattern.
|
||||
let checker =
|
||||
Texture::from_rgba8(&device, &queue, 8, 8, &checkerboard_rgba(), "checker").unwrap();
|
||||
app.scene.add_texture("checker_texture", checker).unwrap();
|
||||
app.scene
|
||||
.add_material_texture("ground_mat", "standard", "checker_texture")
|
||||
.unwrap();
|
||||
app.scene
|
||||
.add_material_texture("solid_mat", "standard", "checker_texture")
|
||||
.unwrap();
|
||||
|
||||
let stripes =
|
||||
Texture::from_rgba8(&device, &queue, 32, 16, &stripes_rgba(), "stripes").unwrap();
|
||||
app.scene.add_texture("stripes_texture", stripes).unwrap();
|
||||
app.scene
|
||||
.add_material_texture("stripes_mat", "standard", "stripes_texture")
|
||||
.unwrap();
|
||||
|
||||
// 3. Ground plane (large, thin, textured).
|
||||
app.scene
|
||||
.create_mesh("ground_mesh", plane(9.0, 9.0, 1, 1), Some("ground_mat"))
|
||||
.unwrap();
|
||||
app.scene.add_entity("ground", "ground_mesh").unwrap();
|
||||
|
||||
// 4. One mesh per primitive, each assigned to a textured (or stripe) material.
|
||||
// The cube + ground stay single-level (tiny meshes — LOD would buy nothing); the
|
||||
// rounded primitives get three LOD levels each (Step 19): level 0 is the full mesh,
|
||||
// levels 1.. are auto-generated by quadric edge collapse at halving targets (D10), all
|
||||
// packed into the mesh's single vertex/index buffers (D7). Zooming with the wheel
|
||||
// switches levels on the fly (asymmetric hysteresis, D4).
|
||||
app.scene
|
||||
.create_mesh("cube_mesh", cube(0.8), Some("solid_mat"))
|
||||
.unwrap();
|
||||
app.scene
|
||||
.create_mesh_with_lod(
|
||||
"sphere_mesh",
|
||||
uv_sphere(0.55, 32, 20),
|
||||
Some("stripes_mat"),
|
||||
3,
|
||||
)
|
||||
.unwrap();
|
||||
app.scene
|
||||
.create_mesh_with_lod("ico_mesh", icosphere(0.5, 2), Some("solid_mat"), 3)
|
||||
.unwrap();
|
||||
app.scene
|
||||
.create_mesh_with_lod("cyl_mesh", cylinder(0.4, 0.9, 32), Some("stripes_mat"), 3)
|
||||
.unwrap();
|
||||
app.scene
|
||||
.create_mesh_with_lod("cone_mesh", cone(0.45, 0.9, 32), Some("solid_mat"), 3)
|
||||
.unwrap();
|
||||
app.scene
|
||||
.create_mesh_with_lod(
|
||||
"torus_mesh",
|
||||
torus(0.42, 0.16, 24, 16),
|
||||
Some("solid_mat"),
|
||||
3,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
place("cube_e", "cube_mesh", app, 0);
|
||||
place("sphere_e", "sphere_mesh", app, 1);
|
||||
place("ico_e", "ico_mesh", app, 2);
|
||||
place("cyl_e", "cyl_mesh", app, 3);
|
||||
place("cone_e", "cone_mesh", app, 4);
|
||||
place("torus_e", "torus_mesh", app, 5);
|
||||
|
||||
// 4b. Étape 22 (6.2): emissive demo — a small glowing sphere at the center.
|
||||
// The material has emissive = [1.0, 0.3, 0.05, 2.0] (orange, intensity 2.0 = HDR glow).
|
||||
// IMPORTANT: set emissive BEFORE create_mesh (the mesh captures the Arc at creation).
|
||||
app.scene
|
||||
.add_material_texture("glow_mat", "standard", "checker_texture")
|
||||
.unwrap();
|
||||
app.scene
|
||||
.set_material_emissive("glow_mat", [1.0, 0.3, 0.05, 2.0])
|
||||
.unwrap();
|
||||
app.scene
|
||||
.create_mesh("glow_mesh", icosphere(0.3, 3), Some("glow_mat"))
|
||||
.unwrap();
|
||||
let mut glow_tf = Transform::identity();
|
||||
glow_tf.translation = Vec3::new(0.0, 0.5, 0.0);
|
||||
app.scene
|
||||
.add_entity_with_transform("glow_e", "glow_mesh", glow_tf)
|
||||
.unwrap();
|
||||
|
||||
// 5. Lights: a shadow-casting directional + a warm point + a green spot.
|
||||
// Start from the default list (directional +Z) so we keep it and add the rest.
|
||||
let toward_light = Vec3::new(1.0, 1.2, 1.0).normalize();
|
||||
app.scene
|
||||
.add_directional_light(toward_light, [1.0, 0.98, 0.92], 1.5)
|
||||
.unwrap();
|
||||
app.scene
|
||||
.add_point_light(Vec3::new(0.5, 1.6, 1.8), [1.0, 0.7, 0.3], 1.2, 6.0)
|
||||
.unwrap();
|
||||
app.scene
|
||||
.add_spot_light(
|
||||
Vec3::new(-2.5, 2.2, 1.0),
|
||||
Vec3::new(2.5, -2.2, -1.0).normalize(),
|
||||
[0.3, 1.0, 0.5],
|
||||
1.4,
|
||||
8.0,
|
||||
0.45,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// The warm directional light above casts shadows. It is packed at index 1: index 0 is
|
||||
// the default +Z directional light pre-loaded by `Lights::new()` (kept here for the
|
||||
// base lighting), so the demo's own light is the SECOND one in the packed array.
|
||||
app.scene.set_shadow_caster(Some(1));
|
||||
app.scene.set_ambient([0.14, 0.14, 0.16]);
|
||||
|
||||
// 6. Active camera, driven by the orbital controller (position, distance, preset target).
|
||||
self.camera.yaw = 0.6;
|
||||
self.camera.pitch = 0.35;
|
||||
self.camera.distance = 6.5;
|
||||
self.camera.target = Vec3::ZERO;
|
||||
self.camera.apply_to(app.scene.camera_mut());
|
||||
}
|
||||
|
||||
fn update(&mut self, app: &mut wsg_lib::App) {
|
||||
// ---- Orbital camera from unified input ----
|
||||
// Classic arc-rotate: orbit ONLY while the left button is held (drag); the wheel zooms
|
||||
// without any button. Sensitivities use the library defaults (0.005 rad/px orbit, 0.9x
|
||||
// per wheel notch); tune them via `camera.orbit_sensitivity` / `camera.zoom_factor`.
|
||||
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);
|
||||
|
||||
// R: reset the view. Keys 1/2/3: front / side / top presets.
|
||||
if app.input.key_pressed(KeyCode::KeyR) {
|
||||
// Keep the target but restore a pleasing default framing.
|
||||
self.camera.yaw = 0.6;
|
||||
self.camera.pitch = 0.35;
|
||||
self.camera.distance = 6.5;
|
||||
}
|
||||
if app.input.key_pressed(KeyCode::Digit1) {
|
||||
self.camera.yaw = 0.0;
|
||||
self.camera.pitch = 0.25;
|
||||
self.camera.distance = 6.5;
|
||||
}
|
||||
if app.input.key_pressed(KeyCode::Digit2) {
|
||||
self.camera.yaw = std::f32::consts::FRAC_PI_2;
|
||||
self.camera.pitch = 0.15;
|
||||
self.camera.distance = 6.5;
|
||||
}
|
||||
if app.input.key_pressed(KeyCode::Digit3) {
|
||||
self.camera.yaw = 0.0;
|
||||
self.camera.pitch = 1.25;
|
||||
self.camera.distance = 8.0;
|
||||
}
|
||||
self.camera.apply_to(app.scene.camera_mut());
|
||||
|
||||
// ---- Étape 22 (6.1): exposure control ----
|
||||
// `+` / `-`: multiply/divide by 1.3 (visible step). `0`: reset to 1.0.
|
||||
if app.input.key_pressed(KeyCode::Equal) {
|
||||
app.set_exposure(app.exposure() * 1.3);
|
||||
}
|
||||
if app.input.key_pressed(KeyCode::Minus) {
|
||||
app.set_exposure(app.exposure() / 1.3);
|
||||
}
|
||||
if app.input.key_pressed(KeyCode::Digit0) {
|
||||
app.set_exposure(1.0);
|
||||
}
|
||||
|
||||
// ---- Slow rotation of the primitives so lighting/shadow read clearly ----
|
||||
self.angle += 0.008;
|
||||
let base = *app
|
||||
.scene
|
||||
.entity_transform("cube_e")
|
||||
.expect("cube entity present");
|
||||
let mut tf = base;
|
||||
tf.rotation = Quat::from_rotation_y(self.angle) * Quat::from_rotation_x(self.angle * 0.4);
|
||||
app.scene.set_entity_transform("cube_e", tf);
|
||||
}
|
||||
|
||||
fn render(&mut self, app: &mut wsg_lib::App, frame: &wsg_lib::core::Frame) {
|
||||
app.render_scene(frame.view());
|
||||
// Opt-in GPU readback (black-window investigation tooling): WSG_DEBUG_DUMP=N dumps the
|
||||
// first 8 slots of the transform/matrix/draw-args/bbox buffers for N frames (unset = silent,
|
||||
// non-numeric value = 3 frames). Note: orbiting/zooming this camera
|
||||
// can never cull the entity ring — the camera always looks at the origin, so each
|
||||
// entity's angular offset from the view axis is bounded by atan(1.7/6.1) ≈ 15.5°, under
|
||||
// the ~22° vertical half-FOV (verified 2026-09-22: 600 frames swept, GPU==CPU on all
|
||||
// 6000 cull verdicts, zero flips on the ring). Counts only flip to 0 for entities far
|
||||
// off-axis (e.g. behind the near plane) — see docs/user/gpu-driven.md.
|
||||
// Unset → 0 (the showcase stays silent); set but non-numeric (e.g. `WSG_DEBUG_DUMP=on`) → 3.
|
||||
let frames = match std::env::var("WSG_DEBUG_DUMP") {
|
||||
Ok(v) => v.parse::<u32>().ok().filter(|&n| n > 0).unwrap_or(3),
|
||||
Err(_) => 0,
|
||||
};
|
||||
if self.dbg < frames {
|
||||
self.dbg += 1;
|
||||
app.renderer().debug_dump(8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pollster::main]
|
||||
async fn main() -> Result<(), WsgError> {
|
||||
// Culling enabled here (Step 15, D8) to exercise the GPU path; it is OFF by default elsewhere.
|
||||
// HDR + ACES tone mapping (Étape 20): renders to an offscreen Rgba16Float texture, then
|
||||
// tone-maps to the sRGB surface. Without `.with_hdr(...)`, the demo would be LDR direct.
|
||||
let app = AppBuilder::new()
|
||||
.title("WSG Demo")
|
||||
.with_culling(true)
|
||||
.with_hdr(ToneMapper::Aces)
|
||||
.with_bloom(BloomConfig::default())
|
||||
.build()
|
||||
.await?;
|
||||
app.run(Demo {
|
||||
camera: CameraController::default(),
|
||||
angle: 0.0,
|
||||
dbg: 0,
|
||||
})
|
||||
}
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
//! **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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
//! **MSAA (Multi-Sample Anti-Aliasing)** — demonstrates 4× MSAA edge smoothing.
|
||||
//!
|
||||
//! Shows how MSAA eliminates the jagged "staircase" artifacts (aliasing) along
|
||||
//! sharp edges. The scene contains a cube (sharp edges), a sphere (curved surface),
|
||||
//! and a ground plane — all with high-contrast edges where aliasing is most visible.
|
||||
//!
|
||||
//! To compare with/without MSAA: remove the `.with_msaa(4)` line from the builder
|
||||
//! below and rebuild. The scene and lighting are identical — only the edge
|
||||
//! smoothness differs.
|
||||
//!
|
||||
//! ## Pipeline (MSAA + HDR)
|
||||
//! ```text
|
||||
//! Main pass → MSAA texture (4 samples, Rgba16Float)
|
||||
//! ↓ resolve (average 4 samples → 1)
|
||||
//! HDR texture (single sample)
|
||||
//! ↓
|
||||
//! Tone Mapping → surface
|
||||
//! ```
|
||||
//!
|
||||
//! ## Controls
|
||||
//! | Key | Action |
|
||||
//! |-----|--------|
|
||||
//! | Drag (LMB) | Orbit camera |
|
||||
//! | Wheel | Zoom |
|
||||
//! | `R` | Reset camera |
|
||||
//! | `M` | Toggle MSAA info (shows sample count) |
|
||||
//!
|
||||
//! ## Build & Run
|
||||
//! ```sh
|
||||
//! cargo run -p wsg-lib --example msaa
|
||||
//! ```
|
||||
|
||||
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::{Transform, ToneMapper};
|
||||
use wsg_lib::mesh::{cube, icosphere, plane};
|
||||
use wsg_lib::AppHandler;
|
||||
use wsg_lib::utils::WsgError;
|
||||
|
||||
struct MsaaDemo {
|
||||
camera: CameraController,
|
||||
show_info: bool,
|
||||
}
|
||||
|
||||
impl AppHandler for MsaaDemo {
|
||||
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(10.0, 10.0, 1, 1), None)
|
||||
.unwrap();
|
||||
app.scene.add_entity("ground", "ground_mesh").unwrap();
|
||||
|
||||
// Cube — sharp edges make aliasing very visible.
|
||||
app.scene
|
||||
.create_mesh("cube_mesh", cube(1.0), None)
|
||||
.unwrap();
|
||||
let mut cube_tf = Transform::identity();
|
||||
cube_tf.translation = Vec3::new(1.5, 0.5, 0.0);
|
||||
app.scene
|
||||
.add_entity_with_transform("cube", "cube_mesh", cube_tf)
|
||||
.unwrap();
|
||||
|
||||
// Sphere — curved surface, aliasing visible on the silhouette.
|
||||
app.scene
|
||||
.create_mesh("sphere_mesh", icosphere(0.6, 3), None)
|
||||
.unwrap();
|
||||
let mut sphere_tf = Transform::identity();
|
||||
sphere_tf.translation = Vec3::new(-1.5, 0.6, 0.0);
|
||||
app.scene
|
||||
.add_entity_with_transform("sphere", "sphere_mesh", sphere_tf)
|
||||
.unwrap();
|
||||
|
||||
// Small cube near the camera — very close edges, maximum aliasing.
|
||||
app.scene
|
||||
.create_mesh("small_cube_mesh", cube(0.3), None)
|
||||
.unwrap();
|
||||
let mut small_tf = Transform::identity();
|
||||
small_tf.translation = Vec3::new(0.0, 0.15, 1.5);
|
||||
app.scene
|
||||
.add_entity_with_transform("small_cube", "small_cube_mesh", small_tf)
|
||||
.unwrap();
|
||||
|
||||
// Directional light (strong, creates high-contrast edges).
|
||||
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.5)
|
||||
.unwrap();
|
||||
app.scene.set_ambient([0.08, 0.08, 0.1]);
|
||||
|
||||
// Camera.
|
||||
self.camera.yaw = 0.4;
|
||||
self.camera.pitch = 0.2;
|
||||
self.camera.distance = 4.0;
|
||||
self.camera.target = Vec3::new(0.0, 0.4, 0.0);
|
||||
self.camera.apply_to(app.scene.camera_mut());
|
||||
|
||||
// Print MSAA status.
|
||||
let sc = app.renderer().msaa_sample_count();
|
||||
eprintln!("[MSAA] sample_count = {} ({})", sc, if sc > 1 { "active" } else { "disabled" });
|
||||
}
|
||||
|
||||
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.4;
|
||||
self.camera.pitch = 0.2;
|
||||
self.camera.distance = 4.0;
|
||||
}
|
||||
self.camera.apply_to(app.scene.camera_mut());
|
||||
|
||||
// Toggle info display.
|
||||
if app.input.key_pressed(KeyCode::KeyM) {
|
||||
self.show_info = !self.show_info;
|
||||
let sc = app.renderer().msaa_sample_count();
|
||||
eprintln!("[MSAA] {}× {}", sc, if sc > 1 { "enabled" } else { "disabled (single sample)" });
|
||||
}
|
||||
}
|
||||
|
||||
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 MSAA 4×")
|
||||
.size(960, 640)
|
||||
.with_msaa(4) // ← Enable 4× MSAA (remove for comparison)
|
||||
.with_hdr(ToneMapper::Aces) // MSAA works with or without HDR
|
||||
.build()
|
||||
.await?;
|
||||
app.run(MsaaDemo {
|
||||
camera: CameraController::default(),
|
||||
show_info: false,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user