# Materials & textures A **`Material`** describes a mesh's appearance: it references a shader (by id) and optionally a **diffuse texture**. Several materials pointing at the same shader share the same compiled GPU pipeline (the `PipelineCache` held by the scene). The engine ships a single shader: **`standard`** — multi-light Phong lighting (see [Lights](../lights/lights.md)), with an **unlit** mode for flat rendering. ## 1. Registering the shader ```rust app.scene .register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH) .unwrap(); ``` > **Note**: `STANDARD_SHADER_PATH` points to an optional file on disk; if it is missing > (the normal case for the embedded library), loading falls back to the shader **embedded at > compile time** (`include_str!`, byte-identical). The fallback message you may see is > therefore **expected and harmless**. For a custom shader: register your `.wgsl` file path under an id of your choice (it must expose the same bind groups as `standard` — frame @0, object @1, texture @2, shadow @3 — see [ARCHI_RENDU](../../tech/ARCHI_RENDU.md) and the [`shaders/standard_shader.wgsl`](../../../lib/src/shaders/standard_shader.wgsl) file). ## 2. Creating materials ```rust // Textureless material: the color comes from per-vertex colors (or white by default). app.scene.add_material_shader("mat", "standard").unwrap(); // Textured material: the texture must first be registered in the scene (below). app.scene.add_material_texture("mat_textured", "standard", "my_texture").unwrap(); ``` Binding a material to a mesh happens at mesh creation (see [Meshes](meshes.md)): ```rust app.scene.create_mesh("cube_mesh", cube(1.0), Some("mat_textured")).unwrap(); ``` A mesh created with `material = None` is rendered with the scene's **default material** (`standard`, built once then cached) — that is the behavior of the [`simple`](../../../lib/examples/meshes/simple.rs) example. ## 3. Diffuse textures `Texture` is a GPU image in `Rgba8UnormSrgb` (linear sampler, repeat addressing). Four constructors: | Constructor | Usage | |--------------|-------| | `Texture::from_rgba8(device, queue, w, h, rgba, label)` | raw RGBA8 bytes (procedural) | | `Texture::from_bytes(device, queue, label, bytes)` | encoded data (PNG/JPEG… via the `image` crate) | | `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()` (clone them out of the borrow before touching `app.scene` again — see the pattern in every textured example): ```rust let (device, queue) = { let ctx = app.context(); (ctx.device.clone(), ctx.queue.clone()) }; let texture = Texture::from_rgba8(&device, &queue, 8, 8, &my_rgba, "checker").unwrap(); app.scene.add_texture("checker_texture", texture).unwrap(); app.scene.add_material_texture("ground_mat", "standard", "checker_texture").unwrap(); ``` The exact snippet (8×8 checkerboard + stripes generation) is in [`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 is bound — no visual effect, no regression); 2. the `Geometry` carries **UVs** (`.with_uvs(…)`). Without UVs, sampling is constant. The procedural primitives (`uv_sphere`, `cube`, …) already provide them. ## 4. Unlit mode (flat / 2D rendering) "Flat" rendering (vertex colors as-is, no lighting) is a **renderer switch**, not a material: ```rust app.renderer_mut().set_unlit(true); // in setup() ``` This is the mode of the `simple` example (2D quad). In this mode the scene's lights are ignored; per-vertex colors (or white) are rendered directly. 2D is a special case of 3D: the single `standard` pipeline serves both. > `clear_lights()` (see [Lights](../lights/lights.md)) gives a similar result but keeps the lit > pipeline: only ambient stays active. Use it when you want to "turn off the lights" without > switching to unlit. ## Links - [User README](../README.md) · [Meshes](meshes.md) · [Lights](../lights/lights.md) · [Examples](../examples.md) - [Root README](../../../README.md) · [ARCHI_RENDU](../../tech/ARCHI_RENDU.md)