This commit is contained in:
Jérôme Bousquié
2026-09-25 20:06:10 +02:00
parent 24fbafc810
commit d4c2d93fc5
31 changed files with 988 additions and 881 deletions
+17
View File
@@ -0,0 +1,17 @@
# Meshes — user documentation
The **geometry side** of the scene: where the geometry comes from, how meshes and entities are
organized, and how objects look.
| Page | Topic |
|------|-------|
| [Meshes](meshes.md) | The three levels `Geometry` → `Mesh` → `Entity`; procedural primitives, custom geometry, `Transform`, mesh sharing |
| [Geometry sources](sources.md) | The `wsg::mesh` module: feature-gated procedural generators + file import (OBJ, glTF stub) |
| [Materials & textures](materials.md) | The `standard` shader, unlit mode, diffuse textures |
Example folder: [`lib/examples/meshes/`](../../../lib/examples/meshes/README.md)
(`simple`, `cube`, `pbr`, `import`, `manual`).
## Links
- [User documentation index](../README.md) · [Quickstart](../quickstart.md) · [Examples](../examples.md)
+100
View File
@@ -0,0 +1,100 @@
# 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()`:
```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) and [`cube.rs`](../../../lib/examples/meshes/cube.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)
+125
View File
@@ -0,0 +1,125 @@
# Meshes: geometries, entities and transforms
A displayed object in WSG goes through three levels:
```
Geometry (CPU, source of truth) ──► Mesh (GPU: vertex/index buffers) ──► Entity (placement in the scene)
```
- **`Geometry`**: raw CPU-side data — positions + optional normals/UVs/colors/indices.
- **`Mesh`**: GPU container (buffers uploaded once). It **retains** its `Arc<Geometry>` on the
CPU side, along with its material.
- **`Entity`**: a `mesh + Transform` association. This is the unit the engine draws. The same
`Mesh` can be shared by several entities (each with its own `Transform`).
## 1. Procedural primitives (the shortest path)
The `math::primitives` module provides ready-to-use `Geometry` generators
(positions + normals + UVs + indices):
| Function | Parameters | Result |
|----------|-----------|--------|
| `cube(size)` | side length | origin-centered cube, per-face normals |
| `plane(width, depth, seg_x, seg_z)` | dimensions + subdivisions | horizontal plane (Y-up), UVs |
| `uv_sphere(radius, sectors, stacks)` | radius + resolution | UV sphere (seam visible) |
| `icosphere(radius, subdivisions)` | radius + subdivisions | smooth sphere (normalized, seam-free) |
| `cylinder(radius, height, sectors)` | radius, height, resolution | centered cylinder |
| `cone(radius, height, sectors)` | radius, height, resolution | cone (base at the bottom when translated in Y) |
| `torus(major, minor, major_segments, minor_segments)` | radii + resolution | torus |
```rust
use wsg_lib::math::{cube, icosphere, torus};
app.scene.create_mesh("cube_mesh", cube(0.8), Some("solid_mat")).unwrap();
app.scene.create_mesh("sphere_mesh", icosphere(0.5, 2), Some("solid_mat")).unwrap();
```
## 2. Custom `Geometry` (your own mesh)
`Geometry` is a builder: positions are mandatory, everything else is optional
(sensible defaults are applied at upload — e.g. normal `[0,0,1]`, white color).
```rust
use wsg_lib::resources::Geometry;
let geometry = Geometry::new(vec![
[-0.5, 0.5, 0.0],
[ 0.5, 0.5, 0.0],
[ 0.5, -0.5, 0.0],
[-0.5, -0.5, 0.0],
])
.with_normals(vec![[0.0, 0.0, 1.0]; 4]) // required for lighting (Phong)
.with_colors(vec![
[1.0, 0.0, 0.0, 1.0],
[0.0, 1.0, 0.0, 1.0],
[0.0, 0.0, 1.0, 1.0],
[1.0, 1.0, 0.0, 1.0],
])
.with_indices(vec![0, 1, 2, 0, 2, 3]); // triangulation (without indices: triangle list)
```
Other attributes: `.with_uvs(vec![[u, v], …])` (required for textures — see
[Materials & textures](materials.md)). `geometry.validate()` checks the arrays for
consistency (aligned lengths, indices in range) before upload.
> **Indices**: `Vec<u16>` — a custom mesh must therefore stay under 65,536 vertices. The
> engine's primitives respect this limit.
## 3. Registering in the scene
```rust
// The mesh is built (GPU buffers) and bound to its material in one call.
// `material = None`: the scene will use its default material (`standard`) at render time.
app.scene.create_mesh("cube_mesh", geometry, Some("cube_material"))?;
// The entity references the mesh by its id (String IDs).
app.scene.add_entity("cube", "cube_mesh")?;
// …or with an explicit placement:
app.scene.add_entity_with_transform("cube", "cube_mesh", transform)?;
```
All these methods return `Result<_, String>` (unifying the typed errors is on the
horizon — see [ROADMAP](../../ROADMAP.md)).
## 4. Moving / animating: the `Transform`
Placement lives on the **entity** (not on the mesh): `Transform { translation: Vec3,
rotation: Quat, scale: Vec3 }`, converted to a world matrix by the engine every frame.
The snippet below is the animation from the [`cube`](../../../lib/examples/meshes/cube.rs) example:
```rust
fn update(&mut self, app: &mut wsg_lib::App) {
self.angle += 0.02;
let mut tf = *app.scene.entity_transform("cube").expect("entity present");
tf.rotation = Quat::from_rotation_y(self.angle) * Quat::from_rotation_x(self.angle * 0.3);
app.scene.set_entity_transform("cube", tf);
}
```
Other entity operations: `entity_transform(label)` (read), `remove_entity(label)` (hides
without freeing resources), `entity_count()`.
> **Rotation order**: `Quat` does not commute — `rot_y * rot_x` is not `rot_x * rot_y`.
> The order above (Y then X) gives a readable "top spinning" motion.
## 5. Mesh sharing
Create **one** mesh per geometry and as many entities as occurrences:
```rust
app.scene.create_mesh("rock_mesh", icosphere(0.3, 1), Some("rock_mat")).unwrap();
for i in 0..10 {
let label = format!("rock_{i}");
let mut tf = Transform::identity();
tf.translation = Vec3::new(i as f32 * 0.8, 0.15, 0.0);
app.scene.add_entity_with_transform(&label, "rock_mesh", tf).unwrap();
}
```
The GPU buffers are uploaded only once; only the world matrices differ.
## Links
- [User README](../README.md) · [Quickstart](../quickstart.md) · [Materials & textures](materials.md) · [Lights](../lights/lights.md)
- [Root README](../../../README.md) · [ARCHI_APP](../../tech/ARCHI_APP.md)
+113
View File
@@ -0,0 +1,113 @@
# Geometry sources: procedural generators and file import
The `wsg::mesh` module is the single entry point for **where the geometry comes from**:
procedural generators or file import.
## Procedural primitives
Each primitive family is behind a **feature** — you only compile what you need.
| Feature | Function | Description |
|---------|----------|-------------|
| `prim-cube` | `cube(size)` | Centered cube, 24 vertices, per-face normals |
| `prim-plane` | `plane(w, d, seg_x, seg_z)` | Horizontal XZ plane (normal +Y), subdivided |
| `prim-sphere` | `uv_sphere(r, sectors, stacks)` | Lat/long sphere, smooth normals |
| `prim-sphere` | `icosphere(r, subdivisions)` | Icosphere (subdivided icosahedron) |
| `prim-cylinder` | `cylinder(r, h, sectors)` | Cylinder (side + caps), analytic normals |
| `prim-cone` | `cone(r, h, sectors)` | Cone (apex + closed base) |
| `prim-torus` | `torus(major, minor, seg_maj, seg_min)` | Torus, smooth normals |
### Default features
```toml
# Your project's Cargo.toml
[dependencies]
wsg-lib = { path = "../lib" }
# Default: all primitives enabled (all-prims)
```
```toml
# Only compile the cube and the sphere:
wsg-lib = { path = "../lib", default-features = false, features = ["prim-cube", "prim-sphere"] }
```
### Usage
```rust
use wsg_lib::prelude::*;
let cube = cube(2.0);
let sphere = uv_sphere(1.0, 32, 16);
let ico = icosphere(1.0, 2);
// All return a Geometry (positions + normals + UVs + indices)
assert_eq!(cube.positions.len(), 24);
```
## File import
| Feature | Function | Format |
|---------|----------|--------|
| `import-obj` | `load_obj(path)` / `parse_obj(str)` | Wavefront OBJ |
| `import-gltf` | `load_gltf(path)` | glTF 2.0 / GLB (stub) |
### OBJ parser
Supports: `v`, `vn`, `vt`, `f` (3–4 vertices, fan triangulation).
If the file has no normals, they are **computed** (area-weighted).
```rust
use wsg_lib::mesh::{load_obj, parse_obj};
// From a file
let geom = load_obj("model.obj")?;
// From a string
let geom = parse_obj("v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n")?;
```
### Errors
```rust
use wsg_lib::mesh::import::MeshImportError;
match load_obj("missing.obj") {
Ok(geom) => { /* … */ }
Err(MeshImportError::Io(e)) => eprintln!("file not accessible: {e}"),
Err(MeshImportError::Parse(e)) => eprintln!("invalid syntax: {e}"),
Err(MeshImportError::Unsupported(e)) => eprintln!("unsupported feature: {e}"),
}
```
## From `Geometry` to the scene
The `mesh` module produces `Geometry` (CPU data). To render it, go through
`Scene::create_mesh`, which uploads it to the GPU:
```rust
use wsg_lib::prelude::*;
use wsg_lib::mesh::cube;
// In AppHandler::setup:
let geom = cube(1.0);
app.scene.create_mesh("my_mesh", geom, Some("my_mat"))?;
app.scene.add_entity("my_entity", "my_mesh")?;
```
## Example
```sh
cargo run -p wsg-lib --example import --features import-obj -- model.obj
```
## Conventions
- **Y-up**, centered on the origin (except `plane`: XZ plane at y=0)
- **Outward** normals
- UVs in [0,1]²
- **CCW** winding (front face)
## Links
- [User README](../README.md) · [Meshes](meshes.md) · [Materials & textures](materials.md) · [Examples](../examples.md)
- [Root README](../../../README.md)