examples: apply real texture assets to multi-mesh examples

- meshes/cube: procedural checkerboard -> uv_texture.jpg (8x8 UV grid)
- meshes/pbr: floor -> ground.jpeg, bump cube -> cave.jpg + caveNormal.jpg
  (normal map pre-encoded via sRGB OETF to cancel the GPU sRGB decode)
- lights/shadow: ground -> ground.jpeg, cube -> uv_texture.jpg
- effects/demo: ground -> ground.jpeg, cube -> uv_texture.jpg
- effects/fog: ground -> ground.jpeg (tiled 80x80), cubes -> stonewall.jpg
- effects/dof: ground -> ground.jpeg, cubes -> uv_texture.jpg
- cameras/culling: shared cube mesh -> uv_texture.jpg
- add lib/examples/assets/textures/ (19 assets, 6.5 MB)
- document assets + usage in examples READMEs, docs/user/examples.md,
  docs/user/meshes/materials.md (CARGO_MANIFEST_DIR pattern, sRGB caveat)
This commit is contained in:
Jérôme Bousquié
2026-09-26 10:49:13 +02:00
parent fecfdcd2d2
commit e5f3636b42
32 changed files with 341 additions and 104 deletions
+9 -2
View File
@@ -11,8 +11,8 @@ README (description + how to run): [`meshes/`](../../lib/examples/meshes/README.
| Example | Folder | What it shows | How to run | Corresponding page | | Example | Folder | What it shows | How to run | Corresponding page |
|---------|--------|---------------|------------|--------------------| |---------|--------|---------------|------------|--------------------|
| `simple` | meshes | A 2D quad with vertex colors, unlit mode (~30 lines) | `cargo run -p wsg-lib --example simple` | [Quickstart](quickstart.md), [Materials](meshes/materials.md) | | `simple` | meshes | A 2D quad with vertex colors, unlit mode (~30 lines) | `cargo run -p wsg-lib --example simple` | [Quickstart](quickstart.md), [Materials](meshes/materials.md) |
| `cube` | meshes | A rotating cube: point + spot light, checkerboard texture, procedural normal map, orbit/zoom | `cargo run -p wsg-lib --example cube` | [Meshes](meshes/meshes.md), [Materials](meshes/materials.md), [Lights](lights/lights.md) | | `cube` | meshes | A rotating cube: point + spot light, `uv_texture.jpg` UV-atlas texture | `cargo run -p wsg-lib --example cube` | [Meshes](meshes/meshes.md), [Materials](meshes/materials.md), [Lights](lights/lights.md) |
| `pbr` | meshes | A procedural PBR material (metal/roughness) + a checker diffuse | `cargo run -p wsg-lib --example pbr` | [Materials](meshes/materials.md) | | `pbr` | meshes | PBR materials (metal/roughness) + real textures: `cave.jpg` albedo, `caveNormal.jpg` normal map, `ground.jpeg` floor | `cargo run -p wsg-lib --example pbr` | [Materials](meshes/materials.md) |
| `import` | meshes | Wavefront **OBJ** import (CLI: file path as argument, procedural cube as fallback) | `cargo run -p wsg-lib --example import --features import-obj -- model.obj` | [Geometry sources](meshes/sources.md) | | `import` | meshes | Wavefront **OBJ** import (CLI: file path as argument, procedural cube as fallback) | `cargo run -p wsg-lib --example import --features import-obj -- model.obj` | [Geometry sources](meshes/sources.md) |
| `manual` | meshes | **Advanced**: the full manual workflow — buffers, pipelines, command encoding, no helpers | `cargo run -p wsg-lib --example manual` | [ARCHI_APP](../tech/ARCHI_APP.md), [FRAME_LOOP](../tech/FRAME_LOOP.md) | | `manual` | meshes | **Advanced**: the full manual workflow — buffers, pipelines, command encoding, no helpers | `cargo run -p wsg-lib --example manual` | [ARCHI_APP](../tech/ARCHI_APP.md), [FRAME_LOOP](../tech/FRAME_LOOP.md) |
| `shadow` | lights | Shadow mapping: the classic pitfall — the packed-index shadow caster | `cargo run -p wsg-lib --example shadow` | [Shadows](lights/shadows.md) | | `shadow` | lights | Shadow mapping: the classic pitfall — the packed-index shadow caster | `cargo run -p wsg-lib --example shadow` | [Shadows](lights/shadows.md) |
@@ -27,6 +27,13 @@ README (description + how to run): [`meshes/`](../../lib/examples/meshes/README.
| `fog` | effects | Distance fog, 3 modes switchable at runtime (linear / exponential / exp²) | `cargo run -p wsg-lib --example fog` | [Fog](effects/fog.md) | | `fog` | effects | Distance fog, 3 modes switchable at runtime (linear / exponential / exp²) | `cargo run -p wsg-lib --example fog` | [Fog](effects/fog.md) |
| `dof` | effects | Depth of field: Gaussian blur scaled by defocus distance, cinematic bokeh; focus presets 1-4 + continuous zoom | `cargo run -p wsg-lib --example dof` | [DoF](effects/dof.md) | | `dof` | effects | Depth of field: Gaussian blur scaled by defocus distance, cinematic bokeh; focus presets 1-4 + continuous zoom | `cargo run -p wsg-lib --example dof` | [DoF](effects/dof.md) |
> **Texture assets**: the multi-mesh examples use real image files from
> [`lib/examples/assets/textures/`](../../lib/examples/assets/textures/) (see the
> [Texture assets](../../lib/examples/README.md#texture-assets) section there for the full
> table): `uv_texture.jpg` (UV atlas visualization), `ground.jpeg` (tiled floor albedo),
> `stonewall.jpg`, and the `cave.jpg` + `caveNormal.jpg` albedo/normal pair. Paths are
> resolved against `CARGO_MANIFEST_DIR`, so the examples run from any working directory.
## The `manual` example: bypassing the helpers ## The `manual` example: bypassing the helpers
[`manual.rs`](../../lib/examples/meshes/manual.rs) renders a rotating cube with **no [`manual.rs`](../../lib/examples/meshes/manual.rs) renders a rotating cube with **no
+26 -2
View File
@@ -57,7 +57,8 @@ Four constructors:
| `Texture::from_file(device, queue, label, path)` | image file on disk | | `Texture::from_file(device, queue, label, path)` | image file on disk |
| `Texture::white_placeholder(device, queue)` | 1×1 white — used internally when a material has no texture | | `Texture::white_placeholder(device, queue)` | 1×1 white — used internally when a material has no texture |
You get `device`/`queue` in `setup()` via `app.context()`: You get `device`/`queue` in `setup()` via `app.context()` (clone them out of the
borrow before touching `app.scene` again — see the pattern in every textured example):
```rust ```rust
let (device, queue) = { let (device, queue) = {
@@ -70,7 +71,30 @@ app.scene.add_material_texture("ground_mat", "standard", "checker_texture").unwr
``` ```
The exact snippet (8×8 checkerboard + stripes generation) is in The exact snippet (8×8 checkerboard + stripes generation) is in
[`demo.rs`](../../../lib/examples/effects/demo.rs) and [`cube.rs`](../../../lib/examples/meshes/cube.rs). [`demo.rs`](../../../lib/examples/effects/demo.rs).
For **file textures** (the pattern used by `cube`, `demo`, `shadow`, `fog`, `dof`,
`culling` and `pbr`), load from `assets/textures/` and resolve the path against
`CARGO_MANIFEST_DIR` so the example works from any working directory:
```rust
const TEXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/assets/textures");
let texture = Texture::from_file(
&device,
&queue,
"uv_atlas",
&format!("{TEXTURES}/uv_texture.jpg"),
)
.unwrap();
```
The sampler is `Linear` + `Repeat`, so textures tile automatically when UVs exceed
[0,1] (e.g. the 80×80 fog floor tiles `ground.jpeg`).
> **Normal maps**: `Texture` is always `Rgba8UnormSrgb`, so the GPU sRGB-decodes on
> sample. A normal map is *linear* data — pre-encode its channels with the sRGB OETF
> before upload so the round-trip is the identity. See `load_normal_map` in
> [`pbr.rs`](../../../lib/examples/meshes/pbr.rs).
Two conditions for a texture to show up: Two conditions for a texture to show up:
1. the material is created via `add_material_texture` (otherwise the 1×1 white placeholder 1. the material is created via `add_material_texture` (otherwise the 1×1 white placeholder
+39 -5
View File
@@ -25,9 +25,42 @@ Examples gated behind a Cargo feature need the feature too:
cargo run -p wsg-lib --example import --features import-obj cargo run -p wsg-lib --example import --features import-obj
``` ```
All examples are **self-contained**: procedural textures, hard-coded geometries, All examples are **self-contained**: hard-coded geometries, and textures that are
no on-disk assets. All use the declarative API (`AppBuilder` + `AppHandler`) either procedural or shipped in [`assets/textures/`](assets/textures/). All use the
except `manual`, which demonstrates the low-level workflow instead. declarative API (`AppBuilder` + `AppHandler`) except `manual`, which demonstrates
the low-level workflow instead.
## Texture assets
A few examples (the multi-mesh / multi-effect ones) use real image files from
`assets/textures/`, loaded with `Texture::from_file`. The paths are resolved
against `CARGO_MANIFEST_DIR` at compile time, so the examples work from **any
working directory**:
```rust
const TEXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/assets/textures");
Texture::from_file(&device, &queue, "label", &format!("{TEXTURES}/ground.jpeg"))
```
Assets used by the examples:
| Asset | Size | Used by | Role |
|-------|------|---------|------|
| `uv_texture.jpg` | 437×438 | `cube`, `demo`, `shadow`, `dof`, `culling` | 8×8 UV atlas visualization (labelled cells + corner coordinates) — makes UV mapping and culling decisions explicit |
| `ground.jpeg` | 512×512 | `demo`, `pbr`, `shadow`, `fog`, `dof` | Seamless ground albedo, tiled via the `Repeat` sampler |
| `stonewall.jpg` | 300×225 | `fog` | Distinctive cube texture — the fog falloff reads clearly on it |
| `cave.jpg` + `caveNormal.jpg` | 600×450 | `pbr` | Albedo + normal-map pair for the PBR normal-mapping demo |
The remaining assets in the folder (`rock.jpg`, `seamlessRoad.jpg`, `stalag.jpg` /
`stalagNormal.jpg`, `stars1.jpg`, sprite/heightmap PNGs, …) are available for
experiments. Two notes:
- `Texture` uploads to `Rgba8UnormSrgb` — correct for **albedo** maps (the GPU
sRGB-decodes on sample). A **normal map** is linear data, so `pbr` pre-encodes
its channels with the sRGB OETF before upload (`load_normal_map`): the GPU
decode then restores the original values (EOTF∘OETF = identity).
- The texture sampler is `Linear` + `Repeat`, so any texture tiles automatically
when UVs exceed [0,1] (the 80×80 fog floor uses this to tile `ground.jpeg`).
> **Where do the files live?** Examples live in subfolders > **Where do the files live?** Examples live in subfolders
> (`examples/<folder>/<name>.rs`). Cargo only auto-discovers top-level > (`examples/<folder>/<name>.rs`). Cargo only auto-discovers top-level
@@ -56,6 +89,7 @@ except `manual`, which demonstrates the low-level workflow instead.
name = "my_example" name = "my_example"
path = "examples/<folder>/my_example.rs" path = "examples/<folder>/my_example.rs"
``` ```
3. Keep it **self-contained**: procedural textures, hard-coded geometries, no 3. Keep it **self-contained**: hard-coded geometries; textures are procedural
external assets. or come from `assets/textures/` (resolved via `CARGO_MANIFEST_DIR`, see
*Texture assets* above).
4. Document it in the folder's `README.md` (and in `docs/user/examples.md`). 4. Document it in the folder's `README.md` (and in `docs/user/examples.md`).
Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

+6 -4
View File
@@ -4,7 +4,7 @@ Examples where the **camera** drives what gets rendered.
| Example | Run command | What it shows | | Example | Run command | What it shows |
|---------|-------------|---------------| |---------|-------------|---------------|
| `culling` | `cargo run -p wsg-lib --example culling` | GPU-driven frustum culling: a 15×15 grid of cubes, off-frustum objects skipped | | `culling` | `cargo run -p wsg-lib --example culling` | GPU-driven frustum culling: a 15×15 grid of UV-atlas cubes, off-frustum objects skipped |
> All commands run from the repo root. > All commands run from the repo root.
@@ -16,9 +16,11 @@ objects changes — with **zero CPU cost** (the GPU decides in a compute pass).
## `culling` — GPU Frustum Culling ## `culling` — GPU Frustum Culling
A grid of **15×15 = 225 cubes** is placed on a large floor. The GPU-driven A grid of **15×15 = 225 cubes** is placed on a large floor. The shared cube
culling (compute shader) determines which cubes are visible in the camera mesh is textured with the `uv_texture.jpg` atlas — the colourful labelled
frustum and zeros their indirect draw args — **zero CPU cost**. cells make it obvious exactly which cubes the GPU draws and which it culls.
The GPU-driven culling (compute shader) determines which cubes are visible in
the camera frustum and zeros their indirect draw args — **zero CPU cost**.
```sh ```sh
cargo run -p wsg-lib --example culling cargo run -p wsg-lib --example culling
+23 -2
View File
@@ -39,9 +39,13 @@ use wsg_lib::app::AppBuilder;
use wsg_lib::camera::CameraController; use wsg_lib::camera::CameraController;
use wsg_lib::core::Transform; use wsg_lib::core::Transform;
use wsg_lib::mesh::{cube, plane}; use wsg_lib::mesh::{cube, plane};
use wsg_lib::resources::Texture;
use wsg_lib::AppHandler; use wsg_lib::AppHandler;
use wsg_lib::utils::WsgError; use wsg_lib::utils::WsgError;
/// Texture asset directory, resolved against the crate root so the example works from any CWD.
const TEXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/assets/textures");
/// Grid dimensions (15×15 = 225 cubes, fits within MAX_ENTITIES=256). /// Grid dimensions (15×15 = 225 cubes, fits within MAX_ENTITIES=256).
const GRID: usize = 15; const GRID: usize = 15;
/// Spacing between cubes (world units). /// Spacing between cubes (world units).
@@ -67,9 +71,26 @@ impl AppHandler for CullingDemo {
.unwrap(); .unwrap();
app.scene.add_entity("ground", "ground_mesh").unwrap(); app.scene.add_entity("ground", "ground_mesh").unwrap();
// One shared cube mesh (all entities reference the same GPU buffers). // One shared cube mesh (all entities reference the same GPU buffers), textured with
// the uv_texture.jpg atlas — the colourful labelled cells make it obvious exactly
// which cubes the GPU draws and which it culls.
let (device, queue) = {
let ctx = app.context();
(ctx.device.clone(), ctx.queue.clone())
};
let uv_tex = Texture::from_file(
&device,
&queue,
"uv_atlas",
&format!("{TEXTURES}/uv_texture.jpg"),
)
.unwrap();
app.scene.add_texture("uv_texture", uv_tex).unwrap();
app.scene app.scene
.create_mesh("cube_mesh", cube(0.5), None) .add_material_texture("cube_mat", "standard", "uv_texture")
.unwrap();
app.scene
.create_mesh("cube_mesh", cube(0.5), Some("cube_mat"))
.unwrap(); .unwrap();
// Place the grid of cubes. // Place the grid of cubes.
+10 -4
View File
@@ -19,7 +19,8 @@ the full showcase that combines everything.
## `demo` — Full Showcase ## `demo` — Full Showcase
Combines **all** effects: LOD primitives, procedural textures, lights Combines **all** effects: LOD primitives, file + procedural textures
(`ground.jpeg` floor, `uv_texture.jpg` cube, checker/stripe grids), lights
(directional + point + spot), shadows, HDR/ACES, exposure, emissive, bloom, culling. (directional + point + spot), shadows, HDR/ACES, exposure, emissive, bloom, culling.
```sh ```sh
@@ -144,8 +145,11 @@ identical, only the edges differ (stair-stepped vs smooth).
Demonstrates the 3 fog modes: **linear**, **exponential**, **exponential²**. Demonstrates the 3 fog modes: **linear**, **exponential**, **exponential²**.
The scene contains a row of cubes receding into the distance and scattered 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, spheres on a large floor plane. The floor is textured with `ground.jpeg`
creating the illusion of an infinite world. (tiled across 80×80 units via the `Repeat` sampler) and the cubes with
`stonewall.jpg` — the fog falloff reads clearly on the textured surfaces.
Fog blends objects toward a background color, creating the illusion of an
infinite world.
```sh ```sh
cargo run -p wsg-lib --example fog --features "all-prims" cargo run -p wsg-lib --example fog --features "all-prims"
@@ -165,7 +169,9 @@ while foreground and background blur according to their distance from the
focus plane. Creates a natural attention effect (cinematic style). 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 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. the sides, on a floor plane. The floor is textured with `ground.jpeg` (tiled)
and the cubes with the `uv_texture.jpg` atlas — bokeh blur reads much better
on textured surfaces. Focus presets at 3 m / 8 m / 15 m.
```sh ```sh
cargo run -p wsg-lib --example dof --features "all-prims" cargo run -p wsg-lib --example dof --features "all-prims"
+33 -6
View File
@@ -4,7 +4,8 @@
//! //!
//! * a **ground plane** plus one of each procedural primitive from `math::primitives` //! * a **ground plane** plus one of each procedural primitive from `math::primitives`
//! (`cube`, `uv_sphere`, `icosphere`, `cylinder`, `cone`, `torus`) placed around it, //! (`cube`, `uv_sphere`, `icosphere`, `cylinder`, `cone`, `torus`) placed around it,
//! * a **procedural texture** per mesh (checker / stripe grids, no assets on disk), //! * **textures per mesh**: the ground is a **file asset** (`ground.jpeg`) and the cube a
//! **UV atlas asset** (`uv_texture.jpg`), the rest stay procedural (checker / stripe grids),
//! * the **standard** Phong material wired to those textures, //! * the **standard** Phong material wired to those textures,
//! * an **orbital camera** driven live by the unified input state: //! * 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), //! hold the **left mouse button** and drag to orbit (yaw/pitch), the wheel zooms (distance),
@@ -43,6 +44,9 @@ use wsg_lib::camera::CameraController;
use wsg_lib::resources::Texture; use wsg_lib::resources::Texture;
use wsg_lib::utils::WsgError; use wsg_lib::utils::WsgError;
/// Texture asset directory, resolved against the crate root so the example works from any CWD.
const TEXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/assets/textures");
/// Generates an 8×8 RGBA checkerboard (white / brick) as raw bytes for `Texture::from_rgba8`. /// Generates an 8×8 RGBA checkerboard (white / brick) as raw bytes for `Texture::from_rgba8`.
fn checkerboard_rgba() -> Vec<u8> { fn checkerboard_rgba() -> Vec<u8> {
const SIZE: u32 = 8; const SIZE: u32 = 8;
@@ -109,13 +113,36 @@ impl AppHandler for Demo {
(ctx.device.clone(), ctx.queue.clone()) (ctx.device.clone(), ctx.queue.clone())
}; };
// 2. Procedural textures, one material per pattern. // 2. Textures: two **file assets** (ground + UV atlas) plus two procedural patterns.
// File paths are resolved against the crate root (`CARGO_MANIFEST_DIR`) so the
// example works from any CWD.
let ground_tex = Texture::from_file(
&device,
&queue,
"ground",
&format!("{TEXTURES}/ground.jpeg"),
)
.unwrap();
app.scene.add_texture("ground_texture", ground_tex).unwrap();
app.scene
.add_material_texture("ground_mat", "standard", "ground_texture")
.unwrap();
let uv_tex = Texture::from_file(
&device,
&queue,
"uv_atlas",
&format!("{TEXTURES}/uv_texture.jpg"),
)
.unwrap();
app.scene.add_texture("uv_texture", uv_tex).unwrap();
app.scene
.add_material_texture("uv_mat", "standard", "uv_texture")
.unwrap();
let checker = let checker =
Texture::from_rgba8(&device, &queue, 8, 8, &checkerboard_rgba(), "checker").unwrap(); Texture::from_rgba8(&device, &queue, 8, 8, &checkerboard_rgba(), "checker").unwrap();
app.scene.add_texture("checker_texture", checker).unwrap(); app.scene.add_texture("checker_texture", checker).unwrap();
app.scene
.add_material_texture("ground_mat", "standard", "checker_texture")
.unwrap();
app.scene app.scene
.add_material_texture("solid_mat", "standard", "checker_texture") .add_material_texture("solid_mat", "standard", "checker_texture")
.unwrap(); .unwrap();
@@ -140,7 +167,7 @@ impl AppHandler for Demo {
// packed into the mesh's single vertex/index buffers (D7). Zooming with the wheel // packed into the mesh's single vertex/index buffers (D7). Zooming with the wheel
// switches levels on the fly (asymmetric hysteresis, D4). // switches levels on the fly (asymmetric hysteresis, D4).
app.scene app.scene
.create_mesh("cube_mesh", cube(0.8), Some("solid_mat")) .create_mesh("cube_mesh", cube(0.8), Some("uv_mat"))
.unwrap(); .unwrap();
app.scene app.scene
.create_mesh_with_lod( .create_mesh_with_lod(
+30 -2
View File
@@ -35,9 +35,13 @@ use wsg_lib::app::AppBuilder;
use wsg_lib::camera::CameraController; use wsg_lib::camera::CameraController;
use wsg_lib::core::{DoFConfig, ToneMapper, Transform}; use wsg_lib::core::{DoFConfig, ToneMapper, Transform};
use wsg_lib::mesh::{cube, icosphere, plane}; use wsg_lib::mesh::{cube, icosphere, plane};
use wsg_lib::resources::Texture;
use wsg_lib::AppHandler; use wsg_lib::AppHandler;
use wsg_lib::utils::WsgError; use wsg_lib::utils::WsgError;
/// Texture asset directory, resolved against the crate root so the example works from any CWD.
const TEXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/assets/textures");
struct DoFDemo { struct DoFDemo {
camera: CameraController, camera: CameraController,
} }
@@ -48,9 +52,33 @@ impl AppHandler for DoFDemo {
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH) .register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
.unwrap(); .unwrap();
// Textured ground + cubes: bokeh blur reads much better on textured surfaces.
// ground.jpeg tiles across the 80×80 floor (Repeat sampler); uv_texture.jpg on the cubes.
let (device, queue) = {
let ctx = app.context();
(ctx.device.clone(), ctx.queue.clone())
};
let ground_tex =
Texture::from_file(&device, &queue, "ground", &format!("{TEXTURES}/ground.jpeg")).unwrap();
app.scene.add_texture("ground_texture", ground_tex).unwrap();
app.scene
.add_material_texture("ground_mat", "standard", "ground_texture")
.unwrap();
let uv_tex = Texture::from_file(
&device,
&queue,
"uv_atlas",
&format!("{TEXTURES}/uv_texture.jpg"),
)
.unwrap();
app.scene.add_texture("uv_texture", uv_tex).unwrap();
app.scene
.add_material_texture("cube_mat", "standard", "uv_texture")
.unwrap();
// Ground plane. // Ground plane.
app.scene app.scene
.create_mesh("ground_mesh", plane(80.0, 80.0, 1, 1), None) .create_mesh("ground_mesh", plane(80.0, 80.0, 1, 1), Some("ground_mat"))
.unwrap(); .unwrap();
app.scene app.scene
.add_entity_with_transform( .add_entity_with_transform(
@@ -62,7 +90,7 @@ impl AppHandler for DoFDemo {
// Row of cubes receding along -Z (distance ≈ 2 to 25 from camera at dist=8). // Row of cubes receding along -Z (distance ≈ 2 to 25 from camera at dist=8).
app.scene app.scene
.create_mesh("cube_mesh", cube(1.0), None) .create_mesh("cube_mesh", cube(1.0), Some("cube_mat"))
.unwrap(); .unwrap();
for i in 0..20 { for i in 0..20 {
+25 -2
View File
@@ -35,9 +35,13 @@ use wsg_lib::app::AppBuilder;
use wsg_lib::camera::CameraController; use wsg_lib::camera::CameraController;
use wsg_lib::core::{FogConfig, ToneMapper, Transform}; use wsg_lib::core::{FogConfig, ToneMapper, Transform};
use wsg_lib::mesh::{cube, icosphere, plane}; use wsg_lib::mesh::{cube, icosphere, plane};
use wsg_lib::resources::Texture;
use wsg_lib::AppHandler; use wsg_lib::AppHandler;
use wsg_lib::utils::WsgError; use wsg_lib::utils::WsgError;
/// Texture asset directory, resolved against the crate root so the example works from any CWD.
const TEXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/assets/textures");
struct FogDemo { struct FogDemo {
camera: CameraController, camera: CameraController,
} }
@@ -48,15 +52,34 @@ impl AppHandler for FogDemo {
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH) .register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
.unwrap(); .unwrap();
// Textured ground + cubes: the fog falloff reads much better on textured surfaces.
// ground.jpeg tiles across the 80×80 floor (Repeat sampler); stonewall.jpg on the cubes.
let (device, queue) = {
let ctx = app.context();
(ctx.device.clone(), ctx.queue.clone())
};
let ground_tex =
Texture::from_file(&device, &queue, "ground", &format!("{TEXTURES}/ground.jpeg")).unwrap();
app.scene.add_texture("ground_texture", ground_tex).unwrap();
app.scene
.add_material_texture("ground_mat", "standard", "ground_texture")
.unwrap();
let wall_tex =
Texture::from_file(&device, &queue, "wall", &format!("{TEXTURES}/stonewall.jpg")).unwrap();
app.scene.add_texture("wall_texture", wall_tex).unwrap();
app.scene
.add_material_texture("wall_mat", "standard", "wall_texture")
.unwrap();
// Large ground plane — will fade into fog at distance. // Large ground plane — will fade into fog at distance.
app.scene app.scene
.create_mesh("ground_mesh", plane(80.0, 80.0, 1, 1), None) .create_mesh("ground_mesh", plane(80.0, 80.0, 1, 1), Some("ground_mat"))
.unwrap(); .unwrap();
app.scene.add_entity("ground", "ground_mesh").unwrap(); app.scene.add_entity("ground", "ground_mesh").unwrap();
// Row of cubes receding into the distance. // Row of cubes receding into the distance.
app.scene app.scene
.create_mesh("cube_mesh", cube(1.0), None) .create_mesh("cube_mesh", cube(1.0), Some("wall_mat"))
.unwrap(); .unwrap();
for i in 0..15 { for i in 0..15 {
let z = -2.0 - i as f32 * 2.5; let z = -2.0 - i as f32 * 2.5;
+4 -2
View File
@@ -5,7 +5,7 @@ emissive materials.
| Example | Run command | What it shows | | Example | Run command | What it shows |
|---------|-------------|---------------| |---------|-------------|---------------|
| `shadow` | `cargo run -p wsg-lib --example shadow` | Shadow mapping in isolation (directional light, 4 objects on a floor) | | `shadow` | `cargo run -p wsg-lib --example shadow` | Shadow mapping in isolation (directional light, 4 objects on a textured floor) |
| `shadow_test` | `cargo run -p wsg-lib --example shadow_test` | Dedicated shadow test: one directional caster, cube on a ground slab, PCF-softened | | `shadow_test` | `cargo run -p wsg-lib --example shadow_test` | Dedicated shadow test: one directional caster, cube on a ground slab, PCF-softened |
| `spot_test` | `cargo run -p wsg-lib --example spot_test` | Isolated spot light: directed beam, penumbra, attenuation | | `spot_test` | `cargo run -p wsg-lib --example spot_test` | Isolated spot light: directed beam, penumbra, attenuation |
| `emissive` | `cargo run -p wsg-lib --example emissive` | Emissive materials (increasing intensities 0 → 4.0) | | `emissive` | `cargo run -p wsg-lib --example emissive` | Emissive materials (increasing intensities 0 → 4.0) |
@@ -18,7 +18,9 @@ emissive materials.
Four objects (cube, sphere, cone, cylinder) on a floor, lit by a directional Four objects (cube, sphere, cone, cylinder) on a floor, lit by a directional
light that casts shadows. Shadow quality is controlled by `ShadowConfig` light that casts shadows. Shadow quality is controlled by `ShadowConfig`
(map size, anti-acne bias). (map size, anti-acne bias). The floor is textured (`ground.jpeg`, tiled) and
the rotating cube uses the `uv_texture.jpg` UV atlas — the textures make the
shadow shapes and their movement clearly readable.
```sh ```sh
cargo run -p wsg-lib --example shadow cargo run -p wsg-lib --example shadow
+34 -2
View File
@@ -4,6 +4,10 @@
//! casts shadows. The shadow quality is controlled by `ShadowConfig` (map size, //! casts shadows. The shadow quality is controlled by `ShadowConfig` (map size,
//! depth/slope bias, ortho frustum radius). //! depth/slope bias, ortho frustum radius).
//! //!
//! The ground is textured (`ground.jpeg`, tiled via the Repeat sampler) and the
//! cube uses the `uv_texture.jpg` UV atlas — textured surfaces make the shadow
//! shapes and their movement clearly readable.
//!
//! ## Controls //! ## Controls
//! | Key | Action | //! | Key | Action |
//! |-----|--------| //! |-----|--------|
@@ -32,12 +36,16 @@ use wsg_lib::app::AppBuilder;
use wsg_lib::camera::CameraController; use wsg_lib::camera::CameraController;
use wsg_lib::core::{ShadowConfig, Transform}; use wsg_lib::core::{ShadowConfig, Transform};
use wsg_lib::mesh::{cone, cube, cylinder, icosphere, plane}; use wsg_lib::mesh::{cone, cube, cylinder, icosphere, plane};
use wsg_lib::resources::Texture;
use wsg_lib::AppHandler; use wsg_lib::AppHandler;
use wsg_lib::utils::WsgError; use wsg_lib::utils::WsgError;
/// Shadow map size — change to test quality (256, 512, 1024, 2048). /// Shadow map size — change to test quality (256, 512, 1024, 2048).
const SHADOW_MAP_SIZE: u32 = 1024; const SHADOW_MAP_SIZE: u32 = 1024;
/// Texture asset directory, resolved against the crate root so the example works from any CWD.
const TEXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/assets/textures");
/// Light directions to cycle through (normalized at runtime). /// Light directions to cycle through (normalized at runtime).
fn light_dirs() -> [Vec3; 3] { fn light_dirs() -> [Vec3; 3] {
[ [
@@ -59,15 +67,39 @@ impl AppHandler for ShadowDemo {
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH) .register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
.unwrap(); .unwrap();
// Textured ground + cube (assets make the shadows readable): ground.jpeg on the
// floor, uv_texture.jpg on the rotating cube. Paths resolve against CARGO_MANIFEST_DIR.
let (device, queue) = {
let ctx = app.context();
(ctx.device.clone(), ctx.queue.clone())
};
let ground_tex =
Texture::from_file(&device, &queue, "ground", &format!("{TEXTURES}/ground.jpeg")).unwrap();
app.scene.add_texture("ground_texture", ground_tex).unwrap();
app.scene
.add_material_texture("ground_mat", "standard", "ground_texture")
.unwrap();
let uv_tex = Texture::from_file(
&device,
&queue,
"uv_atlas",
&format!("{TEXTURES}/uv_texture.jpg"),
)
.unwrap();
app.scene.add_texture("uv_texture", uv_tex).unwrap();
app.scene
.add_material_texture("cube_mat", "standard", "uv_texture")
.unwrap();
// Large ground plane (receives shadows). // Large ground plane (receives shadows).
app.scene app.scene
.create_mesh("ground_mesh", plane(8.0, 8.0, 1, 1), None) .create_mesh("ground_mesh", plane(8.0, 8.0, 1, 1), Some("ground_mat"))
.unwrap(); .unwrap();
app.scene.add_entity("ground", "ground_mesh").unwrap(); app.scene.add_entity("ground", "ground_mesh").unwrap();
// Cube (casts + receives shadow). // Cube (casts + receives shadow).
app.scene app.scene
.create_mesh("cube_mesh", cube(0.8), None) .create_mesh("cube_mesh", cube(0.8), Some("cube_mat"))
.unwrap(); .unwrap();
let mut cube_tf = Transform::identity(); let mut cube_tf = Transform::identity();
cube_tf.translation = Vec3::new(0.8, 0.4, 0.0); cube_tf.translation = Vec3::new(0.8, 0.4, 0.0);
+15 -10
View File
@@ -6,8 +6,8 @@ PBR shading, file import, and the low-level (non-`App`) workflow.
| Example | Run command | What it shows | | Example | Run command | What it shows |
|---------|-------------|---------------| |---------|-------------|---------------|
| `simple` | `cargo run -p wsg-lib --example simple` | The minimal declarative workflow: a flat two-tone quad, **unlit**, rendered automatically | | `simple` | `cargo run -p wsg-lib --example simple` | The minimal declarative workflow: a flat two-tone quad, **unlit**, rendered automatically |
| `cube` | `cargo run -p wsg-lib --example cube` | The 3D MVP: a textured (checkerboard) cube, lit (directional + point + spot), spinning | | `cube` | `cargo run -p wsg-lib --example cube` | The 3D MVP: a textured (`uv_texture.jpg` UV atlas) cube, lit (directional + point + spot), spinning |
| `pbr` | `cargo run -p wsg-lib --example pbr` | PBR metallic/roughness + normal mapping | | `pbr` | `cargo run -p wsg-lib --example pbr` | PBR metallic/roughness + normal mapping (real `cave.jpg` albedo + `caveNormal.jpg`) |
| `import` | `cargo run -p wsg-lib --example import --features import-obj` | OBJ file import (non-graphical, prints stats to stdout) | | `import` | `cargo run -p wsg-lib --example import --features import-obj` | OBJ file import (non-graphical, prints stats to stdout) |
| `manual` | `cargo run -p wsg-lib --example manual` | The **advanced** workflow: `Context`/`Renderer`/`PipelineCache` driven by hand, without the `App` facade | | `manual` | `cargo run -p wsg-lib --example manual` | The **advanced** workflow: `Context`/`Renderer`/`PipelineCache` driven by hand, without the `App` facade |
@@ -37,12 +37,15 @@ No keys — static render.
## `cube` — The 3D MVP ## `cube` — The 3D MVP
A lit unit cube that rotates, **textured** with a procedural 8×8 checkerboard A lit unit cube that rotates, **textured** with the `uv_texture.jpg` asset — an
via the diffuse path (bind group `@group(2)`). Follows the declarative workflow 8×8 UV atlas visualization (labelled cells + corner coordinates) that makes
(like `simple`): `AppBuilder` + automatic scene, **no wgpu import**. The texture exactly where each face's UVs land visible. The texture is loaded from
is generated procedurally (RGBA bytes → `Texture::from_rgba8`) to stay `assets/textures/` via `Texture::from_file` (path resolved against
self-contained; the default camera at (0, 0, 3) frames the cube, and `CARGO_MANIFEST_DIR`), registered by id (`add_texture`) and bound through
`update()` rotates the entity via `set_entity_transform` each frame. `add_material_texture` (diffuse path, bind group `@group(2)`). Follows the
declarative workflow (like `simple`): `AppBuilder` + automatic scene, **no wgpu
import**; the default camera at (0, 0, 3) frames the cube, and `update()`
rotates the entity via `set_entity_transform` each frame.
```sh ```sh
cargo run -p wsg-lib --example cube cargo run -p wsg-lib --example cube
@@ -68,8 +71,10 @@ cargo run -p wsg-lib --example pbr
| `R` | Reset camera | | `R` | Reset camera |
Scene: 6 PBR materials (mirror metal, smooth plastic, rusty metal, ceramic, Scene: 6 PBR materials (mirror metal, smooth plastic, rusty metal, ceramic,
bump map, matte floor). The bump-map cube shows procedural sin-wave surface cave, textured floor). The floor is a 20×20 plane with the `ground.jpeg`
detail. albedo; the cave cube pairs `cave.jpg` (albedo) with `caveNormal.jpg`
(normal map, pre-encoded sRGB before upload — see the *Texture assets* section
in the parent README) and shows real surface detail under the light.
--- ---
+18 -22
View File
@@ -1,4 +1,5 @@
//! A lit unit cube that rotates, **textured** with a procedural checkerboard via the diffuse path //! A lit unit cube that rotates, **textured** with the `uv_texture.jpg` asset (an 8×8 UV atlas
//! visualization — each labelled cell shows exactly where a face's UVs land) via the diffuse path
//! (bind group `@group(2)`). //! (bind group `@group(2)`).
//! //!
//! A 3D mesh with Phong lighting on screen — the library's 3D showcase. //! A 3D mesh with Phong lighting on screen — the library's 3D showcase.
@@ -7,7 +8,9 @@
//! `add_material_shader`/`add_material_texture` + `create_mesh` + `add_entity`. The mesh //! `add_material_shader`/`add_material_texture` + `create_mesh` + `add_entity`. The mesh
//! is declared from a **`Geometry`** (positions, normals, indices). A texture is //! is declared from a **`Geometry`** (positions, normals, indices). A texture is
//! registered by id (`add_texture`) and a textured material bound to it (`add_material_texture`); //! registered by id (`add_texture`) and a textured material bound to it (`add_material_texture`);
//! the texture is generated *procedurally* (RGBA 8×8 checkerboard) to stay self-contained, no on-disk asset. //! here the texture is a **file asset** (`assets/textures/uv_texture.jpg`, loaded via
//! `Texture::from_file`) — the path is resolved against `CARGO_MANIFEST_DIR` so the example
//! works from any working directory.
//! The default active camera (`Scene::default`, position (0,0,3), fov 45°) frames the cube, and //! The default active camera (`Scene::default`, position (0,0,3), fov 45°) frames the cube, and
//! `AppHandler::update` rotates the entity via `set_entity_transform` each frame. //! `AppHandler::update` rotates the entity via `set_entity_transform` each frame.
use glam::{Quat, Vec3}; use glam::{Quat, Vec3};
@@ -17,27 +20,15 @@ use wsg_lib::mesh::cube;
use wsg_lib::resources::Texture; use wsg_lib::resources::Texture;
use wsg_lib::utils::WsgError; use wsg_lib::utils::WsgError;
/// Texture asset directory, resolved against the crate root so the example works from any CWD.
const TEXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/assets/textures");
/// Demo handler: rotates the textured cube in `update`. /// Demo handler: rotates the textured cube in `update`.
struct Cube { struct Cube {
/// Cumulative rotation angle (radians), incremented each frame. /// Cumulative rotation angle (radians), incremented each frame.
angle: f32, angle: f32,
} }
/// Generates a *procedural* RGBA 8×8 checkerboard (white/brick), no on-disk asset, to texture the
/// cube. Returned as a raw RGBA8 `Vec<u8>`, loadable via `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 { (255, 255, 255) } else { (190, 40, 40) };
rgba.extend_from_slice(&[r, g, b, 255]);
}
}
rgba
}
impl AppHandler for Cube { impl AppHandler for Cube {
fn setup(&mut self, app: &mut wsg_lib::App) { fn setup(&mut self, app: &mut wsg_lib::App) {
// Phong shader `standard` (carries the frame + object + texture bind groups). // Phong shader `standard` (carries the frame + object + texture bind groups).
@@ -45,17 +36,22 @@ impl AppHandler for Cube {
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH) .register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
.unwrap(); .unwrap();
// Builds the checkerboard texture with the Context's device/queue (via `app.context()`), then // Loads the `uv_texture.jpg` asset with the Context's device/queue (via `app.context()`), then
// registers it in the scene by id; a textured material is then bound to that id. // registers it in the scene by id; a textured material is then bound to that id.
let (device, queue) = { let (device, queue) = {
let ctx = app.context(); let ctx = app.context();
(ctx.device.clone(), ctx.queue.clone()) (ctx.device.clone(), ctx.queue.clone())
}; };
let texture = let texture = Texture::from_file(
Texture::from_rgba8(&device, &queue, 8, 8, &checkerboard_rgba(), "checker").unwrap(); &device,
app.scene.add_texture("checker_texture", texture).unwrap(); &queue,
"uv_atlas",
&format!("{TEXTURES}/uv_texture.jpg"),
)
.unwrap();
app.scene.add_texture("uv_texture", texture).unwrap();
app.scene app.scene
.add_material_texture("cube_material", "standard", "checker_texture") .add_material_texture("cube_material", "standard", "uv_texture")
.unwrap(); .unwrap();
app.scene app.scene
+69 -39
View File
@@ -3,12 +3,12 @@
//! Démonstration du workflow PBR Cook-Torrance (GGX + Smith + Schlick) avec IBL hémisphérique. //! Démonstration du workflow PBR Cook-Torrance (GGX + Smith + Schlick) avec IBL hémisphérique.
//! //!
//! ## Scène //! ## Scène
//! - Sol : plan 20×20, PBR matte (metallic=0, roughness=0.8) //! - Sol : plan 20×20, PBR matte + albedo `ground.jpeg` (metallic=0, roughness=0.8)
//! - Cube métal : metallic=1.0, roughness=0.1 → reflet spéculaire net (miroir) //! - Cube métal : metallic=1.0, roughness=0.1 → reflet spéculaire net (miroir)
//! - Cube plastique : metallic=0.0, roughness=0.4 → spéculaire large et doux //! - Cube plastique : metallic=0.0, roughness=0.4 → spéculaire large et doux
//! - Cube rouillé : metallic=0.8, roughness=0.7 → métal rugueux //! - Cube rouillé : metallic=0.8, roughness=0.7 → métal rugueux
//! - Sphere céramique : metallic=0.3, roughness=0.3 //! - Sphere céramique : metallic=0.3, roughness=0.3
//! - Cube normal map : bump procédural (sin wave) //! - Cube cave : albedo `cave.jpg` + normal map `caveNormal.jpg` (assets, normal map pré-encodée sRGB)
//! //!
//! ## Contrôles //! ## Contrôles
//! | Touche | Action | //! | Touche | Action |
@@ -51,18 +51,51 @@ impl AppHandler for PbrDemo {
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH) .register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
.unwrap(); .unwrap();
// Normal map procédurale 256×256 : bump sin(x)*sin(y). // Textures fichiers (assets/textures) : albedo du sol + albedo/normal cave.
let bump_map = make_bump_normal_map(&app.context().device, &app.context().queue); // La normal map est pré-encodée sRGB avant upload : `Texture` est toujours
app.scene.add_texture("bump_nm", bump_map).unwrap(); // `Rgba8UnormSrgb` (le GPU décode en sRGB à l'échantillonnage), et les données
// d'une normal map sont linéaires — l'encodage OETF compense la décodage EOTF
// (EOTF(OETF(x)) = x), sinon la perturbation serait visiblement faussée.
let (device, queue) = {
let ctx = app.context();
(ctx.device.clone(), ctx.queue.clone())
};
let ground_albedo = Texture::from_file(
&device,
&queue,
"ground",
&format!("{TEXTURES}/ground.jpeg"),
)
.unwrap();
app.scene.add_texture("ground_albedo", ground_albedo).unwrap();
let cave_albedo =
Texture::from_file(&device, &queue, "cave", &format!("{TEXTURES}/cave.jpg")).unwrap();
app.scene.add_texture("cave_albedo", cave_albedo).unwrap();
let cave_nm = load_normal_map(
&device,
&queue,
&format!("{TEXTURES}/caveNormal.jpg"),
"cave_nm",
);
app.scene.add_texture("cave_nm", cave_nm).unwrap();
// Matériaux PBR. // Matériaux PBR.
app.scene.add_material_pbr("floor", "standard", 0.0, 0.8).unwrap(); app.scene
.add_material_pbr_textured("floor", "standard", 0.0, 0.8, Some("ground_albedo"), None)
.unwrap();
app.scene.add_material_pbr("metal", "standard", 1.0, 0.1).unwrap(); app.scene.add_material_pbr("metal", "standard", 1.0, 0.1).unwrap();
app.scene.add_material_pbr("plastic", "standard", 0.0, 0.4).unwrap(); app.scene.add_material_pbr("plastic", "standard", 0.0, 0.4).unwrap();
app.scene.add_material_pbr("rust", "standard", 0.8, 0.7).unwrap(); app.scene.add_material_pbr("rust", "standard", 0.8, 0.7).unwrap();
app.scene.add_material_pbr("ceramic", "standard", 0.3, 0.3).unwrap(); app.scene.add_material_pbr("ceramic", "standard", 0.3, 0.3).unwrap();
app.scene app.scene
.add_material_pbr_textured("bump", "standard", 0.0, 0.5, None, Some("bump_nm")) .add_material_pbr_textured(
"cave",
"standard",
0.0,
0.6,
Some("cave_albedo"),
Some("cave_nm"),
)
.unwrap(); .unwrap();
// Sol (plan 20×20). // Sol (plan 20×20).
@@ -82,7 +115,7 @@ impl AppHandler for PbrDemo {
("c_metal", "metal", Vec3::new(-3.0, 0.5, 0.0)), ("c_metal", "metal", Vec3::new(-3.0, 0.5, 0.0)),
("c_plastic", "plastic", Vec3::new(-1.0, 0.5, 0.0)), ("c_plastic", "plastic", Vec3::new(-1.0, 0.5, 0.0)),
("c_rust", "rust", Vec3::new(1.0, 0.5, 0.0)), ("c_rust", "rust", Vec3::new(1.0, 0.5, 0.0)),
("c_bump", "bump", Vec3::new(3.0, 0.5, 0.0)), ("c_cave", "cave", Vec3::new(3.0, 0.5, 0.0)),
]; ];
for (id, mat, pos) in &cubes { for (id, mat, pos) in &cubes {
app.scene app.scene
@@ -126,7 +159,7 @@ impl AppHandler for PbrDemo {
self.camera.target = Vec3::new(0.0, 0.5, 0.0); self.camera.target = Vec3::new(0.0, 0.5, 0.0);
self.camera.apply_to(app.scene.camera_mut()); self.camera.apply_to(app.scene.camera_mut());
eprintln!("[PBR] Scene: 6 PBR materials (metal/plastic/rust/ceramic/bump/floor)"); eprintln!("[PBR] Scene: 6 PBR materials (metal/plastic/rust/ceramic/cave/floor)");
eprintln!("[PBR] Drag=orbit, Wheel=zoom, R=reset"); eprintln!("[PBR] Drag=orbit, Wheel=zoom, R=reset");
} }
@@ -149,37 +182,34 @@ impl AppHandler for PbrDemo {
} }
} }
/// Génère une normal map procédurale 256×256 : pattern sin(x*freq)*sin(y*freq) → bump. /// Texture asset directory, resolved against the crate root so the example works from any CWD.
/// Chaque pixel : normale perturbée encodée en RGB (nx*0.5+0.5, ny*0.5+0.5, nz*0.5+0.5) * 255. const TEXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/assets/textures");
fn make_bump_normal_map(device: &wgpu::Device, queue: &wgpu::Queue) -> Texture {
let size = 256u32;
let freq = 8.0;
let mut pixels: Vec<u8> = vec![0u8; (size * size * 4) as usize];
for y in 0..size { /// Charge une normal map depuis un fichier et l'upload en `Texture`.
for x in 0..size { ///
let u = x as f32 / size as f32; /// `Texture` est toujours `Rgba8UnormSrgb` : le GPU applique la EOTF sRGB à
let v = y as f32 / size as f32; /// l'échantillonnage. Une normal map est des données **linéaires** — on pré-encode
let h = (u * freq * std::f32::consts::PI).sin() /// donc chaque canal avec la OETF sRGB avant l'upload, pour que le round-trip
* (v * freq * std::f32::consts::PI).sin(); /// GPU soit l'identité (EOTF(OETF(x)) = x). Sans ce pré-encodage, la perturbation
let eps = 1.0 / size as f32; /// de normale serait visiblement faussée (valeurs compressées vers le noir).
let hx = ((u + eps) * freq * std::f32::consts::PI).sin() fn load_normal_map(device: &wgpu::Device, queue: &wgpu::Queue, path: &str, label: &str) -> Texture {
* (v * freq * std::f32::consts::PI).sin(); let bytes = std::fs::read(path).expect("normal map asset present in the repo");
let hy = (u * freq * std::f32::consts::PI).sin() let rgba = image::load_from_memory(&bytes).expect("valid image").to_rgba8();
* ((v + eps) * freq * std::f32::consts::PI).sin(); let encoded = rgba
let dhdx = (hx - h) / eps; .as_raw()
let dhdy = (hy - h) / eps; .iter()
let n = Vec3::new(-dhdx, -dhdy, 1.0).normalize(); .map(|&c| {
let idx = ((y * size + x) * 4) as usize; let v = c as f32 / 255.0;
pixels[idx] = ((n.x * 0.5 + 0.5) * 255.0).clamp(0.0, 255.0) as u8; let e = if v <= 0.0031308 {
pixels[idx + 1] = ((n.y * 0.5 + 0.5) * 255.0).clamp(0.0, 255.0) as u8; 12.92 * v
pixels[idx + 2] = ((n.z * 0.5 + 0.5) * 255.0).clamp(0.0, 255.0) as u8; } else {
pixels[idx + 3] = 255; 1.055 * v.powf(1.0 / 2.4) - 0.055
} };
} (e * 255.0).round().clamp(0.0, 255.0) as u8
})
Texture::from_rgba8(device, queue, size, size, &pixels, "bump_normal_map") .collect::<Vec<u8>>();
.expect("bump normal map creation failed") Texture::from_rgba8(device, queue, rgba.width(), rgba.height(), &encoded, label)
.expect("normal map upload failed")
} }
#[pollster::main] #[pollster::main]