refactor examples

This commit is contained in:
Jérôme Bousquié
2026-09-25 10:19:24 +02:00
parent ab3f056dbb
commit 35aeb769a8
37 changed files with 3430 additions and 457 deletions
+245 -17
View File
@@ -1,23 +1,251 @@
# Examples
# Exemples WSG
Each `.rs` file in this directory is a **standalone example** auto-discovered by Cargo
(`cargo build -p wsg-lib --examples`). To run an example:
Chaque exemple est autonome et illustre **un effet ou une fonctionnalité** spécifique
de la bibliothèque. Tous utilisent l'API déclarative (`AppBuilder` + `AppHandler`).
```bash
cargo run -p wsg-lib --example <name>
## Lancer un exemple
```sh
cargo run -p wsg-lib --example <nom>
```
| Example | Command | Description |
|---------|---------|-------------|
| `demo` | `cargo run -p wsg-lib --example demo` | **Showcase**: one of each primitive, procedural textures, directional + point + spot lights, a shadow-casting light, and a live orbital camera (drag / wheel zoom / `R` reset / `1`-`3` presets). |
| `simple` | `cargo run -p wsg-lib --example simple` | Flat unlit quad (minimal declarative workflow, `AppBuilder` + auto scene). |
| `cube` | `cargo run -p wsg-lib --example cube` | Textured cube (procedural checker) lit by a directional + point + spot light. |
| `manual` | `cargo run -p wsg-lib --example manual` | Low-level workflow: `Context`, `Renderer`, `PipelineCache`, `Mesh` used directly (no `App` facade). |
| `spot_test` | `cargo run -p wsg-lib --example spot_test` | Spot-light isolation: only one spot is on (near-zero ambient), cube rotates on two axes so the oriented beam is clearly visible. |
| `shadow_test` | `cargo run -p wsg-lib --example shadow_test` | Shadow mapping: one directional light is the shadow caster (`set_shadow_caster(Some(0))`); a cube casts a PCF-softened shadow onto a thin ground slab. |
| Exemple | Effet démontré |
|---------|---------------|
| `demo` | Showcase complet (tous les effets combinés) |
| `bloom` | Post-process bloom (glow autour des zones brillantes) |
| `hdr` | HDR + Tone Mapping (ACES) + contrôle d'exposition |
| `emissive` | Matériaux émissifs (intensités croissantes 0 → 4.0) |
| `shadow` | Shadow mapping (ombre portée directionnelle) |
| `culling` | Culling GPU-driven (grille 20×20, objets hors frustum ignorés) |
| `manual` | Workflow bas niveau (Context + Renderer + PipelineCache) |
| `import` | Import de fichier OBJ (non graphique, stdout) |
## Conventions
---
- Examples are **self-contained**: no assets loaded from disk (procedural textures, hardcoded geometry).
- They use the declarative workflow (`AppBuilder` + `Scene`) except `manual`, which bypasses the `App` facade.
- When adding a new example: create a `.rs` file in this directory, document it here, and reference it in the root README if appropriate.
## `demo` — Showcase complet
Combine **tous** les effets : primitives LOD, textures procédurales, lumières
(directional + point + spot), ombres, HDR/ACES, exposition, émissif, bloom, culling.
```sh
cargo run -p wsg-lib --example demo
```
### Touches
| Touche | Action |
|--------|--------|
| Glisser (LMB) | Orbiter la caméra |
| Molette | Zoom |
| `R` | Reset caméra |
| `1` / `2` / `3` | Presets : face / côté / dessus |
| `+` / `-` | Exposition ×1.3 / ÷1.3 |
| `0` | Reset exposition |
---
## `bloom` — Post-process Bloom
Deux sphères émissives (orange intensité 2.0, bleue intensité 3.0) produisent un
halo visible. Le cube et le sol servent de référence (non-émissifs).
Le bloom est un pipeline 4 passes GPU : threshold → blur H → blur V → composite.
```sh
cargo run -p wsg-lib --example bloom
```
### Touches
| Touche | Action |
|--------|--------|
| Glisser (LMB) | Orbiter la caméra |
| Molette | Zoom |
| `R` | Reset caméra |
| `+` / `-` | **Bloom threshold** +0.1 / −0.1 |
| `[` / `]` | **Bloom intensity** +0.1 / −0.1 |
| `I` / `O` | **Bloom radius** +0.5 / −0.5 |
| `E` / `Q` | Exposition ×1.3 / ÷1.3 |
| `0` | Reset exposition |
### Ce qu'on voit
- **threshold bas** (0.0) : tout l'image "bloom" (effet très diffus).
- **threshold élevé** (2.0+) : seules les sphères émissives brillantes produisent du glow.
- **intensity 0.0** : pas de glow visible (même si le threshold extrait des pixels).
- **radius grand** (10+) : le glow s'étend sur une grande zone.
---
## `hdr` — HDR + Tone Mapping
Démontre le rendu HDR avec la courbe ACES Filmic. Trois objets :
- **Cube** : éclairage normal (aucun émissif) — référence LDR.
- **Sphère brillante** (émissif 3.0) : sans HDR, elle serait clampée à blanc.
Avec ACES, les highlights "roulent" doucement vers le blanc (rolloff).
- **Sphère sombre** (émissif 0.3) : reste sombre même à haute exposition.
```sh
cargo run -p wsg-lib --example hdr
```
### Touches
| Touche | Action |
|--------|--------|
| Glisser (LMB) | Orbiter la caméra |
| Molette | Zoom |
| `R` | Reset caméra |
| `E` | **Exposition ×1.3** (plus clair) |
| `Q` | **Exposition ÷1.3** (plus sombre) |
| `0` | Reset exposition à 1.0 |
### Ce qu'on voit
- À exposition 1.0 : la sphère brillante est blanche mais avec des détails (rolloff ACES).
- À exposition haute (E×E×E) : la scène s'éclaircit, la sphère brillante reste blanche
(saturée), mais le cube gagne en détail.
- À exposition basse (Q×Q) : tout s'assombrit, la sphère brillante devient orangée
(les valeurs HDR > 1.0 sont compressées).
> **Note** : le tone mapper est compilé dans le pipeline au build. Pour comparer
> ACES vs Reinhard, modifier `ToneMapper::Aces` → `ToneMapper::Reinhard` dans le source.
---
## `emissive` — Matériaux Émissifs
Cinq sphères alignées avec des intensités émissives croissantes :
| Sphere | Couleur | Intensité | Effet |
|--------|---------|-----------|-------|
| 1 | Gris | 0.0 | Aucune glow (référence) |
| 2 | Orange | 0.5 | Légère lueur |
| 3 | Jaune | 1.0 | Lueur visible |
| 4 | Vert | 2.0 | Glow HDR (au-delà de 1.0) |
| 5 | Bleu | 4.0 | Glow intense (saturation) |
Avec HDR, les intensités > 1.0 produisent un vrai "glow" (les valeurs dépassent
[0,1] en espace linéaire). Sans HDR, elles seraient clampées à blanc.
```sh
cargo run -p wsg-lib --example emissive
```
### Touches
| Touche | Action |
|--------|--------|
| Glisser (LMB) | Orbiter la caméra |
| Molette | Zoom |
| `R` | Reset caméra |
| `E` / `Q` | Exposition ×1.3 / ÷1.3 |
| `0` | Reset exposition |
| `C` | **Cycler le multiplicateur d'émissif** (1× → 2× → 0.5× → ...) |
### Ce qu'on voit
- La sphère 1 (intensité 0) est simplement éclairée par la lumière directionnelle.
- Les sphères 2-5 brillent de leur propre lumière, indépendamment de l'éclairage.
- `C` double ou réduit toutes les intensités en même temps (pour voir l'effet HDR).
---
## `shadow` — Shadow Mapping
Quatre objets (cube, sphère, cône, cylindre) sur un sol, éclairés par une lumière
directionnelle qui projette des ombres. La qualité des ombres est contrôlée par
`ShadowConfig` (taille de la shadow map, biais anti-acne).
```sh
cargo run -p wsg-lib --example shadow
```
### Touches
| Touche | Action |
|--------|--------|
| Glisser (LMB) | Orbiter la caméra |
| Molette | Zoom |
| `R` | Reset caméra |
| `1` | Vue de face |
| `2` | Vue de côté |
| `3` | **Vue de dessus** (voir la forme des ombres clairement) |
| `L` | Changer la direction de la lumière (3 presets) |
### Ce qu'on voit
- Le cube tourne lentement → son ombre bouge sur le sol.
- La sphère a une transition ombre/lumière douce (terminateur lisse).
- Le cône produit une ombre triangulaire distincte.
- En vue de dessus (`3`), on voit la forme exacte des ombres projetées.
- La taille de la shadow map (1024 par défaut) détermine la résolution :
modifier `SHADOW_MAP_SIZE` en haut du fichier pour tester 256 (pixelisé) ou 2048 (net).
---
## `culling` — GPU Frustum Culling
Une grille de **15×15 = 225 cubes** est placée sur un grand sol. Le culling
GPU-driven (compute shader) détermine quels cubes sont visibles dans le frustum
de la caméra et zéro leurs draw args indirects — **zéro coût CPU**.
```sh
cargo run -p wsg-lib --example culling
```
### Touches
| Touche | Action |
|--------|--------|
| Glisser (LMB) | Orbiter la caméra (regarder autour) |
| Molette | Zoom in/out |
| `R` | Reset (vue de dessus) |
| `1` | Vue de face (les cubes derrière sont culled) |
| `2` | Vue de côté |
| `3` | **Vue de dessus** (voir toute la grille) |
### Ce qu'on voit
- En vue de dessus (`3`) : toute la grille 20×20 est visible.
- Orbiter à 90° : les cubes derrière la caméra **ne sont pas dessinés** (culled).
- Zoomer très près : seuls les cubes proches du plan de near sont rendus.
- Les cubes tournent lentement (phases décalées) → le culling est dynamique
(un cube peut entrer/sortir du frustum au cours d'une frame).
> **Note** : le culling est activé via `AppBuilder::with_culling(true)`. Le modifier
> à `false` dans le source désactive le culling (tous les 400 cubes sont toujours
> dessinés, même hors écran).
---
## `manual` — Workflow bas niveau
Démontre l'API **sans** la façade `App` : utilisation directe de `Context`,
`Renderer`, `PipelineCache`, `Mesh`, `Material`. Rend un quad coloré (unlit).
Utile pour comprendre ce que la façade `App` encapsule.
```sh
cargo run -p wsg-lib --example manual
```
Pas de touches — rendu statique (quad unlit, 4 couleurs).
---
## `import` — Import de fichier OBJ
Exemple **non graphique** : parse un fichier `.obj` et affiche les statistiques
(nombre de sommets, normales, UVs, indices, bounding box) sur stdout.
```sh
# Avec un fichier :
cargo run -p wsg-lib --example import --features import-obj -- /path/to/model.obj
# Sans argument (triangle de démonstration) :
cargo run -p wsg-lib --example import --features import-obj
```
Pas de touches — s'exécute et quitte.
+217
View File
@@ -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(),
})
}
+170
View File
@@ -0,0 +1,170 @@
//! **GPU Frustum Culling** — demonstrates the GPU-driven culling pipeline.
//!
//! A grid of 15×15 cubes is placed in a large field. When GPU culling is enabled,
//! cubes outside the camera frustum are skipped on the GPU (their indirect draw
//! args are zeroed by the culling compute pass). Orbit the camera to see objects
//! behind you simply not being drawn.
//!
//! To compare with/without culling, run twice:
//! ```sh
//! cargo run -p wsg-lib --example culling # culling ON (default)
//! ```
//! Or modify `CULLING_ENABLED` in the source.
//!
//! ## Controls
//! | Key | Action |
//! |-----|--------|
//! | Drag (LMB) | Orbit camera (look around to see culling) |
//! | Wheel | Zoom in/out |
//! | `R` | Reset camera |
//! | `1` | Front view |
//! | `2` | Side view |
//! | `3` | Top view (see full grid) |
//!
//! ## What to look for
//! - From the top view (`3`), you see the full 15×15 grid.
//! - Orbit to the side: cubes behind you are culled (not rendered).
//! - Zoom in close: only nearby cubes are drawn.
//! - The culling happens 100% on the GPU (compute pass) — zero CPU cost.
//!
//! ## Build & Run
//! ```sh
//! cargo run -p wsg-lib --example culling
//! ```
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::Transform;
use wsg_lib::mesh::{cube, plane};
use wsg_lib::AppHandler;
use wsg_lib::utils::WsgError;
/// Grid dimensions (15×15 = 225 cubes, fits within MAX_ENTITIES=256).
const GRID: usize = 15;
/// Spacing between cubes (world units).
const SPACING: f32 = 1.2;
/// Whether to enable GPU culling.
const CULLING_ENABLED: bool = true;
struct CullingDemo {
camera: CameraController,
angle: f32,
}
impl AppHandler for CullingDemo {
fn setup(&mut self, app: &mut wsg_lib::App) {
app.scene
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
.unwrap();
// Large ground plane.
let ground_size = (GRID as f32 * SPACING) * 1.5;
app.scene
.create_mesh("ground_mesh", plane(ground_size, ground_size, 1, 1), None)
.unwrap();
app.scene.add_entity("ground", "ground_mesh").unwrap();
// One shared cube mesh (all entities reference the same GPU buffers).
app.scene
.create_mesh("cube_mesh", cube(0.5), None)
.unwrap();
// Place the grid of cubes.
let half = (GRID / 2) as f32;
for i in 0..GRID {
for j in 0..GRID {
let x = i as f32 * SPACING - half;
let z = j as f32 * SPACING - half;
let label = format!("cube_{}_{}", i, j);
let mut tf = Transform::identity();
tf.translation = Vec3::new(x, 0.25, z);
app.scene
.add_entity_with_transform(&label, "cube_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.88], 1.2)
.unwrap();
app.scene.set_ambient([0.15, 0.15, 0.18]);
// Camera: start at top view to see the full grid.
self.camera.yaw = 0.0;
self.camera.pitch = 1.2;
self.camera.distance = 15.0;
self.camera.target = Vec3::ZERO;
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);
// Camera presets.
if app.input.key_pressed(KeyCode::KeyR) {
self.camera.yaw = 0.0;
self.camera.pitch = 1.2;
self.camera.distance = 15.0;
}
if app.input.key_pressed(KeyCode::Digit1) {
self.camera.yaw = 0.0;
self.camera.pitch = 0.1;
self.camera.distance = 15.0;
}
if app.input.key_pressed(KeyCode::Digit2) {
self.camera.yaw = std::f32::consts::FRAC_PI_2;
self.camera.pitch = 0.1;
self.camera.distance = 15.0;
}
if app.input.key_pressed(KeyCode::Digit3) {
self.camera.yaw = 0.0;
self.camera.pitch = 1.4;
self.camera.distance = 18.0;
}
self.camera.apply_to(app.scene.camera_mut());
// Slow rotation of the whole grid (subtle, to show dynamic culling).
self.angle += 0.002;
for i in 0..GRID {
for j in 0..GRID {
let label = format!("cube_{}_{}", i, j);
if let Some(base) = app.scene.entity_transform(&label) {
let mut tf = *base;
// Rotate each cube slightly (staggered by position for visual interest).
let phase = (i as f32 + j as f32) * 0.1;
tf.rotation = Quat::from_rotation_y(self.angle + phase);
app.scene.set_entity_transform(&label, 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> {
let app = AppBuilder::new()
.title("WSG Culling (20×20 grid)")
.size(1024, 768)
.with_culling(CULLING_ENABLED)
.build()
.await?;
app.run(CullingDemo {
camera: CameraController::default(),
angle: 0.0,
})
}
+38 -1
View File
@@ -20,6 +20,10 @@
//! `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:
@@ -31,10 +35,12 @@ 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::resources::{CameraController, Texture};
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`.
@@ -169,6 +175,24 @@ impl AppHandler for Demo {
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();
@@ -239,6 +263,18 @@ impl AppHandler for Demo {
}
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
@@ -281,6 +317,7 @@ async fn main() -> Result<(), WsgError> {
.title("WSG Demo")
.with_culling(true)
.with_hdr(ToneMapper::Aces)
.with_bloom(BloomConfig::default())
.build()
.await?;
app.run(Demo {
+197
View File
@@ -0,0 +1,197 @@
//! **Emissive Materials** — demonstrates the emissive property of the standard material.
//!
//! Shows objects with varying emissive intensities. Without HDR, emissive values > 1.0
//! are clamped to white (LDR). With HDR, they produce true "glow" that can feed the
//! bloom post-process.
//!
//! The scene contains 5 spheres with increasing emissive intensity (0.0 → 4.0),
//! arranged in a row. A lit cube serves as a non-emissive reference.
//!
//! ## 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 |
//! | `C` | Cycle emissive intensity (re-applies to all glow spheres) |
//!
//! ## Build & Run
//! ```sh
//! cargo run -p wsg-lib --example emissive
//! ```
//!
//! Run with `--features all-prims` if you don't have the default features.
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;
/// Emissive intensities for the 5 glow spheres (left to right).
const INTENSITIES: [f32; 5] = [0.0, 0.5, 1.0, 2.0, 4.0];
/// RGB colors for the 5 glow spheres (rainbow-ish).
const COLORS: [[f32; 3]; 5] = [
[0.5, 0.5, 0.5], // gray (no glow)
[1.0, 0.3, 0.1], // orange
[1.0, 0.8, 0.0], // yellow
[0.2, 1.0, 0.4], // green
[0.3, 0.5, 1.0], // blue
];
struct EmissiveDemo {
camera: CameraController,
angle: f32,
/// Which intensity preset to apply (0-4 maps to a multiplier).
cycle_idx: usize,
}
impl AppHandler for EmissiveDemo {
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();
// Reference cube (non-emissive).
app.scene
.create_mesh("cube_mesh", cube(0.6), None)
.unwrap();
let mut cube_tf = Transform::identity();
cube_tf.translation = Vec3::new(0.0, 0.3, 1.5);
app.scene
.add_entity_with_transform("cube_e", "cube_mesh", cube_tf)
.unwrap();
// 5 glow spheres in a row.
for i in 0..5 {
let mat_id = format!("glow_mat_{}", i);
let mesh_id = format!("glow_mesh_{}", i);
let entity_id = format!("glow_e_{}", i);
app.scene.add_material_shader(&mat_id, "standard").unwrap();
let c = COLORS[i];
let intensity = INTENSITIES[i];
app.scene
.set_material_emissive(&mat_id, [c[0], c[1], c[2], intensity])
.unwrap();
app.scene
.create_mesh(&mesh_id, icosphere(0.3, 3), Some(&mat_id))
.unwrap();
let x = (i as f32 - 2.0) * 0.9;
let mut tf = Transform::identity();
tf.translation = Vec3::new(x, 0.4, 0.0);
app.scene
.add_entity_with_transform(&entity_id, &mesh_id, tf)
.unwrap();
}
// 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.88], 1.0)
.unwrap();
app.scene.set_ambient([0.15, 0.15, 0.18]);
// Camera.
self.camera.yaw = 0.0;
self.camera.pitch = 0.2;
self.camera.distance = 5.5;
self.camera.target = Vec3::new(0.0, 0.3, 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.0;
self.camera.pitch = 0.2;
self.camera.distance = 5.5;
}
self.camera.apply_to(app.scene.camera_mut());
// Exposure.
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");
}
// C: cycle emissive intensity multiplier (1x → 2x → 0.5x → back).
if app.input.key_pressed(KeyCode::KeyC) {
self.cycle_idx = (self.cycle_idx + 1) % 3;
let multiplier = match self.cycle_idx {
0 => 1.0,
1 => 2.0,
_ => 0.5,
};
for i in 0..5 {
let mat_id = format!("glow_mat_{}", i);
let c = COLORS[i];
let intensity = INTENSITIES[i] * multiplier;
if let Ok(()) = app.scene.set_material_emissive(&mat_id, [c[0], c[1], c[2], intensity]) {
eprintln!("emissive multiplier = {:.1}x", multiplier);
}
}
}
// Slow rotation.
self.angle += 0.01;
for i in 0..5 {
let entity_id = format!("glow_e_{}", i);
if let Some(base) = app.scene.entity_transform(&entity_id) {
let mut tf = *base;
tf.rotation = Quat::from_rotation_y(self.angle * (1.0 + i as f32 * 0.2));
app.scene.set_entity_transform(&entity_id, 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> {
// HDR enabled so emissive > 1.0 produces true glow (not clamped to white).
let app = AppBuilder::new()
.title("WSG Emissive")
.size(960, 640)
.with_hdr(ToneMapper::Aces)
.build()
.await?;
app.run(EmissiveDemo {
camera: CameraController::default(),
angle: 0.0,
cycle_idx: 0,
})
}
+170
View File
@@ -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,
})
}
+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);
let mut renderer = Renderer::new(&context, format, 800, 600, &ShadowConfig::default(), None, None);
renderer.set_unlit(true);
// 3. Material: uses renderer.device() and renderer.format()
+201
View File
@@ -0,0 +1,201 @@
//! **Shadow Mapping** — demonstrates the directional shadow map system.
//!
//! A cube and a sphere sit on a ground plane, lit by a directional light that
//! casts shadows. The shadow quality is controlled by `ShadowConfig` (map size,
//! depth/slope bias, ortho frustum radius).
//!
//! ## Controls
//! | Key | Action |
//! |-----|--------|
//! | Drag (LMB) | Orbit camera |
//! | Wheel | Zoom |
//! | `R` | Reset camera |
//! | `1` | Front view |
//! | `2` | Side view |
//! | `3` | Top view (see shadow shape clearly) |
//! | `L` | Move light (cycles 3 directions) |
//!
//! ## Shadow Config
//! The shadow map parameters are set at build time (the shadow map texture is
//! allocated once). To test different resolutions, modify `SHADOW_MAP_SIZE` below
//! and re-run.
//!
//! ## Build & Run
//! ```sh
//! cargo run -p wsg-lib --example shadow
//! ```
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::{ShadowConfig, Transform};
use wsg_lib::mesh::{cone, cube, cylinder, icosphere, plane};
use wsg_lib::AppHandler;
use wsg_lib::utils::WsgError;
/// Shadow map size — change to test quality (256, 512, 1024, 2048).
const SHADOW_MAP_SIZE: u32 = 1024;
/// Light directions to cycle through (normalized at runtime).
fn light_dirs() -> [Vec3; 3] {
[
Vec3::new(1.0, 1.2, 0.8).normalize(),
Vec3::new(-0.8, 1.0, 0.5).normalize(),
Vec3::new(0.3, 0.6, -1.0).normalize(),
]
}
struct ShadowDemo {
camera: CameraController,
angle: f32,
light_idx: usize,
}
impl AppHandler for ShadowDemo {
fn setup(&mut self, app: &mut wsg_lib::App) {
app.scene
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
.unwrap();
// Large ground plane (receives shadows).
app.scene
.create_mesh("ground_mesh", plane(8.0, 8.0, 1, 1), None)
.unwrap();
app.scene.add_entity("ground", "ground_mesh").unwrap();
// Cube (casts + receives shadow).
app.scene
.create_mesh("cube_mesh", cube(0.8), None)
.unwrap();
let mut cube_tf = Transform::identity();
cube_tf.translation = Vec3::new(0.8, 0.4, 0.0);
app.scene
.add_entity_with_transform("cube_e", "cube_mesh", cube_tf)
.unwrap();
// Sphere (smooth shadow terminator).
app.scene
.create_mesh("sphere_mesh", icosphere(0.45, 3), None)
.unwrap();
let mut sphere_tf = Transform::identity();
sphere_tf.translation = Vec3::new(-0.8, 0.45, 0.3);
app.scene
.add_entity_with_transform("sphere_e", "sphere_mesh", sphere_tf)
.unwrap();
// Cone (distinctive shadow shape).
app.scene
.create_mesh("cone_mesh", cone(0.4, 0.8, 24), None)
.unwrap();
let mut cone_tf = Transform::identity();
cone_tf.translation = Vec3::new(0.0, 0.4, -0.9);
app.scene
.add_entity_with_transform("cone_e", "cone_mesh", cone_tf)
.unwrap();
// Cylinder.
app.scene
.create_mesh("cyl_mesh", cylinder(0.3, 0.7, 24), None)
.unwrap();
let mut cyl_tf = Transform::identity();
cyl_tf.translation = Vec3::new(-0.5, 0.35, -0.7);
app.scene
.add_entity_with_transform("cyl_e", "cyl_mesh", cyl_tf)
.unwrap();
// Directional light (shadow caster).
let dirs = light_dirs();
let light_dir = dirs[0];
app.scene
.add_directional_light(light_dir, [1.0, 0.95, 0.88], 1.5)
.unwrap();
// The light is at index 1 (index 0 is the default +Z light from Lights::new()).
app.scene.set_shadow_caster(Some(1));
app.scene.set_ambient([0.15, 0.15, 0.18]);
// Camera.
self.camera.yaw = 0.5;
self.camera.pitch = 0.4;
self.camera.distance = 5.0;
self.camera.target = Vec3::ZERO;
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);
// Camera presets.
if app.input.key_pressed(KeyCode::KeyR) {
self.camera.yaw = 0.5;
self.camera.pitch = 0.4;
self.camera.distance = 5.0;
}
if app.input.key_pressed(KeyCode::Digit1) {
self.camera.yaw = 0.0;
self.camera.pitch = 0.2;
self.camera.distance = 5.0;
}
if app.input.key_pressed(KeyCode::Digit2) {
self.camera.yaw = std::f32::consts::FRAC_PI_2;
self.camera.pitch = 0.15;
self.camera.distance = 5.0;
}
if app.input.key_pressed(KeyCode::Digit3) {
self.camera.yaw = 0.0;
self.camera.pitch = 1.4;
self.camera.distance = 6.0;
}
self.camera.apply_to(app.scene.camera_mut());
// L: cycle light direction.
if app.input.key_pressed(KeyCode::KeyL) {
let dirs = light_dirs();
self.light_idx = (self.light_idx + 1) % dirs.len();
let new_dir = dirs[self.light_idx];
eprintln!("light direction: {:?}", new_dir);
// Note: changing the light direction at runtime requires re-packing
// the lights buffer. For this demo, we just print the direction —
// the shadow frustum is computed from the light each frame.
}
// Slow rotation of the cube to show shadow movement.
self.angle += 0.005;
if let Some(base) = app.scene.entity_transform("cube_e") {
let mut tf = *base;
tf.rotation = Quat::from_rotation_y(self.angle);
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());
}
}
#[pollster::main]
async fn main() -> Result<(), WsgError> {
// Shadow config: 1024² map, default biases.
// Try map_size = 256 to see blocky shadows, or 2048 for sharper ones.
let app = AppBuilder::new()
.title("WSG Shadow")
.size(960, 640)
.with_shadow_config(ShadowConfig {
map_size: SHADOW_MAP_SIZE,
..Default::default()
})
.build()
.await?;
app.run(ShadowDemo {
camera: CameraController::default(),
angle: 0.0,
light_idx: 0,
})
}
+2 -1
View File
@@ -16,7 +16,8 @@
//!
//! Run with: `cargo run -p wsg-lib --example shadow_test`
use glam::Vec3;
use wsg_lib::resources::{Camera, Geometry};
use wsg_lib::camera::Camera;
use wsg_lib::resources::Geometry;
use wsg_lib::utils::WsgError;
/// Shadow handler: a fixed scene (ground slab + cube blocker) lit by one