refactor examples
This commit is contained in:
+245
-17
@@ -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.
|
||||
|
||||
@@ -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,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
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user