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 itsArc<Geometry>on the CPU side, along with its material.Entity: amesh + Transformassociation. This is the unit the engine draws. The sameMeshcan be shared by several entities (each with its ownTransform).
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:
Quatdoes not commute —rot_y * rot_xis notrot_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.