diff --git a/docs/user/examples.md b/docs/user/examples.md index 95e689f..6eaa686 100644 --- a/docs/user/examples.md +++ b/docs/user/examples.md @@ -11,8 +11,8 @@ README (description + how to run): [`meshes/`](../../lib/examples/meshes/README. | 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) | -| `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) | -| `pbr` | meshes | A procedural PBR material (metal/roughness) + a checker diffuse | `cargo run -p wsg-lib --example pbr` | [Materials](meshes/materials.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 | 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) | | `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) | @@ -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) | | `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 [`manual.rs`](../../lib/examples/meshes/manual.rs) renders a rotating cube with **no diff --git a/docs/user/meshes/materials.md b/docs/user/meshes/materials.md index b2ecc7e..45e2d44 100644 --- a/docs/user/meshes/materials.md +++ b/docs/user/meshes/materials.md @@ -57,7 +57,8 @@ Four constructors: | `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 | -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 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 -[`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: 1. the material is created via `add_material_texture` (otherwise the 1×1 white placeholder diff --git a/lib/examples/README.md b/lib/examples/README.md index 8b7b3fa..0dfa138 100644 --- a/lib/examples/README.md +++ b/lib/examples/README.md @@ -25,9 +25,42 @@ Examples gated behind a Cargo feature need the feature too: cargo run -p wsg-lib --example import --features import-obj ``` -All examples are **self-contained**: procedural textures, hard-coded geometries, -no on-disk assets. All use the declarative API (`AppBuilder` + `AppHandler`) -except `manual`, which demonstrates the low-level workflow instead. +All examples are **self-contained**: hard-coded geometries, and textures that are +either procedural or shipped in [`assets/textures/`](assets/textures/). All use the +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 > (`examples//.rs`). Cargo only auto-discovers top-level @@ -56,6 +89,7 @@ except `manual`, which demonstrates the low-level workflow instead. name = "my_example" path = "examples//my_example.rs" ``` -3. Keep it **self-contained**: procedural textures, hard-coded geometries, no - external assets. +3. Keep it **self-contained**: hard-coded geometries; textures are procedural + 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`). diff --git a/lib/examples/assets/textures/cave.jpg b/lib/examples/assets/textures/cave.jpg new file mode 100644 index 0000000..7623c4c Binary files /dev/null and b/lib/examples/assets/textures/cave.jpg differ diff --git a/lib/examples/assets/textures/caveNormal.jpg b/lib/examples/assets/textures/caveNormal.jpg new file mode 100644 index 0000000..4daf783 Binary files /dev/null and b/lib/examples/assets/textures/caveNormal.jpg differ diff --git a/lib/examples/assets/textures/earthDouble.png b/lib/examples/assets/textures/earthDouble.png new file mode 100644 index 0000000..beb3de3 Binary files /dev/null and b/lib/examples/assets/textures/earthDouble.png differ diff --git a/lib/examples/assets/textures/fleche.jpg b/lib/examples/assets/textures/fleche.jpg new file mode 100644 index 0000000..c7aa4b6 Binary files /dev/null and b/lib/examples/assets/textures/fleche.jpg differ diff --git a/lib/examples/assets/textures/ground.jpeg b/lib/examples/assets/textures/ground.jpeg new file mode 100644 index 0000000..f32ca86 Binary files /dev/null and b/lib/examples/assets/textures/ground.jpeg differ diff --git a/lib/examples/assets/textures/rock.jpg b/lib/examples/assets/textures/rock.jpg new file mode 100644 index 0000000..df5e1de Binary files /dev/null and b/lib/examples/assets/textures/rock.jpg differ diff --git a/lib/examples/assets/textures/sc-snowflakes0.png b/lib/examples/assets/textures/sc-snowflakes0.png new file mode 100644 index 0000000..a4d0c95 Binary files /dev/null and b/lib/examples/assets/textures/sc-snowflakes0.png differ diff --git a/lib/examples/assets/textures/sc-snowflakes1.png b/lib/examples/assets/textures/sc-snowflakes1.png new file mode 100644 index 0000000..8ebf1f3 Binary files /dev/null and b/lib/examples/assets/textures/sc-snowflakes1.png differ diff --git a/lib/examples/assets/textures/sc-snowflakes2.png b/lib/examples/assets/textures/sc-snowflakes2.png new file mode 100644 index 0000000..ee20e67 Binary files /dev/null and b/lib/examples/assets/textures/sc-snowflakes2.png differ diff --git a/lib/examples/assets/textures/seamlessRoad.jpg b/lib/examples/assets/textures/seamlessRoad.jpg new file mode 100644 index 0000000..325151c Binary files /dev/null and b/lib/examples/assets/textures/seamlessRoad.jpg differ diff --git a/lib/examples/assets/textures/spriteAtlas.png b/lib/examples/assets/textures/spriteAtlas.png new file mode 100644 index 0000000..493d3aa Binary files /dev/null and b/lib/examples/assets/textures/spriteAtlas.png differ diff --git a/lib/examples/assets/textures/stalag.jpg b/lib/examples/assets/textures/stalag.jpg new file mode 100644 index 0000000..25b46a3 Binary files /dev/null and b/lib/examples/assets/textures/stalag.jpg differ diff --git a/lib/examples/assets/textures/stalagNormal.jpg b/lib/examples/assets/textures/stalagNormal.jpg new file mode 100644 index 0000000..ee266ef Binary files /dev/null and b/lib/examples/assets/textures/stalagNormal.jpg differ diff --git a/lib/examples/assets/textures/stars1.jpg b/lib/examples/assets/textures/stars1.jpg new file mode 100644 index 0000000..25840a0 Binary files /dev/null and b/lib/examples/assets/textures/stars1.jpg differ diff --git a/lib/examples/assets/textures/stonewall.jpg b/lib/examples/assets/textures/stonewall.jpg new file mode 100644 index 0000000..e85da74 Binary files /dev/null and b/lib/examples/assets/textures/stonewall.jpg differ diff --git a/lib/examples/assets/textures/testTexture.jpg b/lib/examples/assets/textures/testTexture.jpg new file mode 100644 index 0000000..88bee75 Binary files /dev/null and b/lib/examples/assets/textures/testTexture.jpg differ diff --git a/lib/examples/assets/textures/uv_texture.jpg b/lib/examples/assets/textures/uv_texture.jpg new file mode 100644 index 0000000..c220724 Binary files /dev/null and b/lib/examples/assets/textures/uv_texture.jpg differ diff --git a/lib/examples/assets/textures/worldHeightMapDouble.png b/lib/examples/assets/textures/worldHeightMapDouble.png new file mode 100644 index 0000000..2273640 Binary files /dev/null and b/lib/examples/assets/textures/worldHeightMapDouble.png differ diff --git a/lib/examples/cameras/README.md b/lib/examples/cameras/README.md index 48cae26..fa178a6 100644 --- a/lib/examples/cameras/README.md +++ b/lib/examples/cameras/README.md @@ -4,7 +4,7 @@ Examples where the **camera** drives what gets rendered. | 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. @@ -16,9 +16,11 @@ objects changes — with **zero CPU cost** (the GPU decides in a compute pass). ## `culling` — GPU Frustum Culling -A grid of **15×15 = 225 cubes** is placed on a large floor. The GPU-driven -culling (compute shader) determines which cubes are visible in the camera -frustum and zeros their indirect draw args — **zero CPU cost**. +A grid of **15×15 = 225 cubes** is placed on a large floor. The shared cube +mesh is textured with the `uv_texture.jpg` atlas — the colourful labelled +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 cargo run -p wsg-lib --example culling diff --git a/lib/examples/cameras/culling.rs b/lib/examples/cameras/culling.rs index 8473291..cb93361 100644 --- a/lib/examples/cameras/culling.rs +++ b/lib/examples/cameras/culling.rs @@ -39,9 +39,13 @@ use wsg_lib::app::AppBuilder; use wsg_lib::camera::CameraController; use wsg_lib::core::Transform; use wsg_lib::mesh::{cube, plane}; +use wsg_lib::resources::Texture; use wsg_lib::AppHandler; 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). const GRID: usize = 15; /// Spacing between cubes (world units). @@ -67,9 +71,26 @@ impl AppHandler for CullingDemo { .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 - .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(); // Place the grid of cubes. diff --git a/lib/examples/effects/README.md b/lib/examples/effects/README.md index 9478c2f..542fa5d 100644 --- a/lib/examples/effects/README.md +++ b/lib/examples/effects/README.md @@ -19,7 +19,8 @@ the full showcase that combines everything. ## `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. ```sh @@ -144,8 +145,11 @@ identical, only the edges differ (stair-stepped vs smooth). Demonstrates the 3 fog modes: **linear**, **exponential**, **exponential²**. The scene contains a row of cubes receding into the distance and scattered -spheres on a large floor plane. Fog blends objects toward a background color, -creating the illusion of an infinite world. +spheres on a large floor plane. The floor is textured with `ground.jpeg` +(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 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). 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 cargo run -p wsg-lib --example dof --features "all-prims" diff --git a/lib/examples/effects/demo.rs b/lib/examples/effects/demo.rs index e1b8937..2643dd7 100644 --- a/lib/examples/effects/demo.rs +++ b/lib/examples/effects/demo.rs @@ -4,7 +4,8 @@ //! //! * a **ground plane** plus one of each procedural primitive from `math::primitives` //! (`cube`, `uv_sphere`, `icosphere`, `cylinder`, `cone`, `torus`) placed around it, -//! * a **procedural texture** per mesh (checker / stripe grids, no assets on disk), +//! * **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, //! * 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), @@ -43,6 +44,9 @@ use wsg_lib::camera::CameraController; use wsg_lib::resources::Texture; 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`. fn checkerboard_rgba() -> Vec { const SIZE: u32 = 8; @@ -109,13 +113,36 @@ impl AppHandler for Demo { (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 = Texture::from_rgba8(&device, &queue, 8, 8, &checkerboard_rgba(), "checker").unwrap(); app.scene.add_texture("checker_texture", checker).unwrap(); - app.scene - .add_material_texture("ground_mat", "standard", "checker_texture") - .unwrap(); app.scene .add_material_texture("solid_mat", "standard", "checker_texture") .unwrap(); @@ -140,7 +167,7 @@ impl AppHandler for Demo { // packed into the mesh's single vertex/index buffers (D7). Zooming with the wheel // switches levels on the fly (asymmetric hysteresis, D4). app.scene - .create_mesh("cube_mesh", cube(0.8), Some("solid_mat")) + .create_mesh("cube_mesh", cube(0.8), Some("uv_mat")) .unwrap(); app.scene .create_mesh_with_lod( diff --git a/lib/examples/effects/dof.rs b/lib/examples/effects/dof.rs index 45f80cb..4cad77e 100644 --- a/lib/examples/effects/dof.rs +++ b/lib/examples/effects/dof.rs @@ -35,9 +35,13 @@ use wsg_lib::app::AppBuilder; use wsg_lib::camera::CameraController; use wsg_lib::core::{DoFConfig, ToneMapper, Transform}; use wsg_lib::mesh::{cube, icosphere, plane}; +use wsg_lib::resources::Texture; use wsg_lib::AppHandler; 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 { camera: CameraController, } @@ -48,9 +52,33 @@ impl AppHandler for DoFDemo { .register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH) .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. 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(); app.scene .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). app.scene - .create_mesh("cube_mesh", cube(1.0), None) + .create_mesh("cube_mesh", cube(1.0), Some("cube_mat")) .unwrap(); for i in 0..20 { diff --git a/lib/examples/effects/fog.rs b/lib/examples/effects/fog.rs index c43ad58..46aad7c 100644 --- a/lib/examples/effects/fog.rs +++ b/lib/examples/effects/fog.rs @@ -35,9 +35,13 @@ use wsg_lib::app::AppBuilder; use wsg_lib::camera::CameraController; use wsg_lib::core::{FogConfig, ToneMapper, Transform}; use wsg_lib::mesh::{cube, icosphere, plane}; +use wsg_lib::resources::Texture; use wsg_lib::AppHandler; 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 { camera: CameraController, } @@ -48,15 +52,34 @@ impl AppHandler for FogDemo { .register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH) .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. 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(); app.scene.add_entity("ground", "ground_mesh").unwrap(); // Row of cubes receding into the distance. app.scene - .create_mesh("cube_mesh", cube(1.0), None) + .create_mesh("cube_mesh", cube(1.0), Some("wall_mat")) .unwrap(); for i in 0..15 { let z = -2.0 - i as f32 * 2.5; diff --git a/lib/examples/lights/README.md b/lib/examples/lights/README.md index 4a88ff3..9ece932 100644 --- a/lib/examples/lights/README.md +++ b/lib/examples/lights/README.md @@ -5,7 +5,7 @@ emissive materials. | 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 | | `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) | @@ -18,7 +18,9 @@ emissive materials. Four objects (cube, sphere, cone, cylinder) on a floor, lit by a directional 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 cargo run -p wsg-lib --example shadow diff --git a/lib/examples/lights/shadow.rs b/lib/examples/lights/shadow.rs index 0dfc7d4..b6bb70e 100644 --- a/lib/examples/lights/shadow.rs +++ b/lib/examples/lights/shadow.rs @@ -4,6 +4,10 @@ //! casts shadows. The shadow quality is controlled by `ShadowConfig` (map size, //! 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 //! | Key | Action | //! |-----|--------| @@ -32,12 +36,16 @@ 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::resources::Texture; 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; +/// 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). fn light_dirs() -> [Vec3; 3] { [ @@ -59,15 +67,39 @@ impl AppHandler for ShadowDemo { .register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH) .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). 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(); app.scene.add_entity("ground", "ground_mesh").unwrap(); // Cube (casts + receives shadow). app.scene - .create_mesh("cube_mesh", cube(0.8), None) + .create_mesh("cube_mesh", cube(0.8), Some("cube_mat")) .unwrap(); let mut cube_tf = Transform::identity(); cube_tf.translation = Vec3::new(0.8, 0.4, 0.0); diff --git a/lib/examples/meshes/README.md b/lib/examples/meshes/README.md index 1bff906..6368c8f 100644 --- a/lib/examples/meshes/README.md +++ b/lib/examples/meshes/README.md @@ -6,8 +6,8 @@ PBR shading, file import, and the low-level (non-`App`) workflow. | 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 | -| `cube` | `cargo run -p wsg-lib --example cube` | The 3D MVP: a textured (checkerboard) cube, lit (directional + point + spot), spinning | -| `pbr` | `cargo run -p wsg-lib --example pbr` | PBR metallic/roughness + normal mapping | +| `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 (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) | | `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 -A lit unit cube that rotates, **textured** with a procedural 8×8 checkerboard -via the diffuse path (bind group `@group(2)`). Follows the declarative workflow -(like `simple`): `AppBuilder` + automatic scene, **no wgpu import**. The texture -is generated procedurally (RGBA bytes → `Texture::from_rgba8`) to stay -self-contained; the default camera at (0, 0, 3) frames the cube, and -`update()` rotates the entity via `set_entity_transform` each frame. +A lit unit cube that rotates, **textured** with the `uv_texture.jpg` asset — an +8×8 UV atlas visualization (labelled cells + corner coordinates) that makes +exactly where each face's UVs land visible. The texture is loaded from +`assets/textures/` via `Texture::from_file` (path resolved against +`CARGO_MANIFEST_DIR`), registered by id (`add_texture`) and bound through +`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 cargo run -p wsg-lib --example cube @@ -68,8 +71,10 @@ cargo run -p wsg-lib --example pbr | `R` | Reset camera | Scene: 6 PBR materials (mirror metal, smooth plastic, rusty metal, ceramic, -bump map, matte floor). The bump-map cube shows procedural sin-wave surface -detail. +cave, textured floor). The floor is a 20×20 plane with the `ground.jpeg` +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. --- diff --git a/lib/examples/meshes/cube.rs b/lib/examples/meshes/cube.rs index 635a867..3572547 100644 --- a/lib/examples/meshes/cube.rs +++ b/lib/examples/meshes/cube.rs @@ -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)`). //! //! 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 //! 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`); -//! 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 //! `AppHandler::update` rotates the entity via `set_entity_transform` each frame. use glam::{Quat, Vec3}; @@ -17,27 +20,15 @@ use wsg_lib::mesh::cube; use wsg_lib::resources::Texture; 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`. struct Cube { /// Cumulative rotation angle (radians), incremented each frame. 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`, loadable via `Texture::from_rgba8`. -fn checkerboard_rgba() -> Vec { - 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 { fn setup(&mut self, app: &mut wsg_lib::App) { // 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) .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. let (device, queue) = { let ctx = app.context(); (ctx.device.clone(), ctx.queue.clone()) }; - let texture = - Texture::from_rgba8(&device, &queue, 8, 8, &checkerboard_rgba(), "checker").unwrap(); - app.scene.add_texture("checker_texture", texture).unwrap(); + let texture = Texture::from_file( + &device, + &queue, + "uv_atlas", + &format!("{TEXTURES}/uv_texture.jpg"), + ) + .unwrap(); + app.scene.add_texture("uv_texture", texture).unwrap(); app.scene - .add_material_texture("cube_material", "standard", "checker_texture") + .add_material_texture("cube_material", "standard", "uv_texture") .unwrap(); app.scene diff --git a/lib/examples/meshes/pbr.rs b/lib/examples/meshes/pbr.rs index 7f7b993..8f1787e 100644 --- a/lib/examples/meshes/pbr.rs +++ b/lib/examples/meshes/pbr.rs @@ -3,12 +3,12 @@ //! Démonstration du workflow PBR Cook-Torrance (GGX + Smith + Schlick) avec IBL hémisphérique. //! //! ## 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 plastique : metallic=0.0, roughness=0.4 → spéculaire large et doux //! - Cube rouillé : metallic=0.8, roughness=0.7 → métal rugueux //! - 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 //! | Touche | Action | @@ -51,18 +51,51 @@ impl AppHandler for PbrDemo { .register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH) .unwrap(); - // Normal map procédurale 256×256 : bump sin(x)*sin(y). - let bump_map = make_bump_normal_map(&app.context().device, &app.context().queue); - app.scene.add_texture("bump_nm", bump_map).unwrap(); + // Textures fichiers (assets/textures) : albedo du sol + albedo/normal cave. + // La normal map est pré-encodée sRGB avant upload : `Texture` est toujours + // `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. - 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("plastic", "standard", 0.0, 0.4).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_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(); // Sol (plan 20×20). @@ -82,7 +115,7 @@ impl AppHandler for PbrDemo { ("c_metal", "metal", Vec3::new(-3.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_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 { app.scene @@ -126,7 +159,7 @@ impl AppHandler for PbrDemo { self.camera.target = Vec3::new(0.0, 0.5, 0.0); 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"); } @@ -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. -/// 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. -fn make_bump_normal_map(device: &wgpu::Device, queue: &wgpu::Queue) -> Texture { - let size = 256u32; - let freq = 8.0; - let mut pixels: Vec = vec![0u8; (size * size * 4) as usize]; +/// 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"); - for y in 0..size { - for x in 0..size { - let u = x as f32 / size as f32; - let v = y as f32 / size as f32; - let h = (u * freq * std::f32::consts::PI).sin() - * (v * freq * std::f32::consts::PI).sin(); - let eps = 1.0 / size as f32; - let hx = ((u + eps) * freq * std::f32::consts::PI).sin() - * (v * freq * std::f32::consts::PI).sin(); - let hy = (u * freq * std::f32::consts::PI).sin() - * ((v + eps) * freq * std::f32::consts::PI).sin(); - let dhdx = (hx - h) / eps; - let dhdy = (hy - h) / eps; - let n = Vec3::new(-dhdx, -dhdy, 1.0).normalize(); - let idx = ((y * size + x) * 4) as usize; - pixels[idx] = ((n.x * 0.5 + 0.5) * 255.0).clamp(0.0, 255.0) as u8; - pixels[idx + 1] = ((n.y * 0.5 + 0.5) * 255.0).clamp(0.0, 255.0) as u8; - pixels[idx + 2] = ((n.z * 0.5 + 0.5) * 255.0).clamp(0.0, 255.0) as u8; - pixels[idx + 3] = 255; - } - } - - Texture::from_rgba8(device, queue, size, size, &pixels, "bump_normal_map") - .expect("bump normal map creation failed") +/// Charge une normal map depuis un fichier et l'upload en `Texture`. +/// +/// `Texture` est toujours `Rgba8UnormSrgb` : le GPU applique la EOTF sRGB à +/// l'échantillonnage. Une normal map est des données **linéaires** — on pré-encode +/// donc chaque canal avec la OETF sRGB avant l'upload, pour que le round-trip +/// GPU soit l'identité (EOTF(OETF(x)) = x). Sans ce pré-encodage, la perturbation +/// de normale serait visiblement faussée (valeurs compressées vers le noir). +fn load_normal_map(device: &wgpu::Device, queue: &wgpu::Queue, path: &str, label: &str) -> Texture { + let bytes = std::fs::read(path).expect("normal map asset present in the repo"); + let rgba = image::load_from_memory(&bytes).expect("valid image").to_rgba8(); + let encoded = rgba + .as_raw() + .iter() + .map(|&c| { + let v = c as f32 / 255.0; + let e = if v <= 0.0031308 { + 12.92 * v + } else { + 1.055 * v.powf(1.0 / 2.4) - 0.055 + }; + (e * 255.0).round().clamp(0.0, 255.0) as u8 + }) + .collect::>(); + Texture::from_rgba8(device, queue, rgba.width(), rgba.height(), &encoded, label) + .expect("normal map upload failed") } #[pollster::main]