Files
Jérôme Bousquié 5ae978da23 doc
2026-09-21 10:07:32 +02:00

4.9 KiB

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
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).

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). 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

// 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).

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 example:

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:

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.