Mesh
A renderable 3D node built from geometry: the primitive shapes (Mesh.box(), Mesh.sphere(), …)
and Mesh.from() for a custom Geometry. It is a Node (transform,
hierarchy, events, aspects). A GLB model is Model.load; the same geometry hundreds of
times is an InstancedMesh.
- add a cube, ball, cylinder, capsule or plane to a 3D scene
- make a flat floor or ground plane
- show your own procedural shape as a mesh
- change a mesh's colour or material
- draw hundreds of identical objects (trees, coins, crates) cheaply
- draw a marker or gizmo on top of everything
- turn a mesh's shadows on or off
Platforms: Mesh on every host; InstancedMesh on iOS, Android, desktop, the web player and
the le.codes viewer app — not on the web (Web Lite) or in headless tests, where it warns once and
draws nothing.
At a glance
const scene = new Scene()
const ground = Mesh.plane({
material: Material.lit({ color: '#556655' }),
normal: [0, 1, 0], // face up
scale: 20,
receiveShadows: true,
})
const crate = Mesh.box({
material: Material.lit({ color: '#b5854b' }),
size: [1, 1, 1],
position: [0, 0.5, 0],
castShadows: true,
})
scene.add(Light.sun(), ground, crate)
scene.open()Add a cube, ball, cylinder, capsule or plane: Mesh.box, Mesh.sphere, …
Add a box (cube), sphere (ball), cylinder, capsule or plane to a 3D scene with one call. All the
factories share the same base options and return a ready Mesh:
type MeshOptions = {
material?: Material // default Material.unlit()
position?: Vec3Like
eulerAngles?: Vec3Like // degrees
scale?: Vec3Like | number // number = uniform
name?: string
castShadows?: boolean // default true
receiveShadows?: boolean // default true
renderPriority?: number // 0 … 7, default 4 — see "Draw a marker on top of everything"
}
Mesh.box(options?: MeshOptions & {
size?: Vec3Like | number // full edge lengths in world units; default 1 (unit cube)
})
Mesh.sphere(options?: MeshOptions & {
radius?: number // default 0.5 (unit diameter)
widthSegments?: number // default 32
heightSegments?: number // default 32
})
Mesh.cylinder(options?: MeshOptions & {
radius?: number // default 0.5; height is fixed at 1 (Y: -0.5..0.5)
radiusTop?: number // default = radius (cone: set 0)
radiusBottom?: number // default = radius
edges?: number // default 32
smooth?: boolean // default true — smooth side normals; false = faceted
})
Mesh.capsule(options?: MeshOptions & {
radius?: number // default 0.5
length?: number // between the two cap centres; default 1 (0 = a sphere) — total height length + 2·radius
widthSegments?: number // around the axis; default 32
capSegments?: number // rings per hemisphere; default 8
})
Mesh.plane(options?: MeshOptions & {
normal?: Vec3Like // which way the 1×1 quad faces; default [0, 0, 1] (toward the camera)
})- Defaults are unit-sized and centred on the origin: box 1×1×1, sphere diameter 1, cylinder
height 1 / diameter 1, capsule along Y (2 m tall at the defaults), plane 1×1. Size them with
size(box),radius/length, orscale— the plane has no size option, onlyscale. box.sizeis baked into the geometry buffers;scaleis a node transform. Both draw the same, but a bakedsizeis what theShapeauto-box measures — either works, since the auto-box also multiplies by world scale.
A flat floor or ground plane: Mesh.plane({ normal })
A plane is a 1×1 quad standing upright and facing +Z by default. For a floor, point its normal up
and scale it — Mesh.plane({ normal: [0, 1, 0], scale: 20 }), as in At a glance.
A mesh from your own geometry: Mesh.from, setGeometry
new Mesh(geometry?: Geometry, material?: Material)
Mesh.from(geometry: Geometry, options?: MeshOptions): Mesh
mesh.setGeometry(geometry: Geometry): this // rebuild the buffers in place
mesh.geometry // Geometry | undefined — the buffers this mesh was built fromGeometry holds raw buffers (vertices, normals, indices, uv, and an optional uv1 — the
lightmap UV set, non-overlapping; the box and cylinder builders fill it, a plane's uv already
is one, see lightmap.md). Build one from the primitive
builders (Geometry.box(size), .sphere(opts), .cylinder(opts), .capsule(opts), .plane(opts)
— same options as the factories above) and reshape it, or construct it from your own
Float32Arrays — see Geometry:
const geo = Geometry.cylinder({ radiusTop: 0, edges: 6 }) // a hex cone
.scale(1, 3, 1) // mutates the vertex buffer
.translate(0, 1.5, 0) // move the pivot to the base
const spike = Mesh.from(geo, { material: Material.lit(), position: [2, 0, 0] })The buffers are uploaded when the mesh is built: editing the Geometry afterwards changes nothing
until mesh.setGeometry(geo) rebuilds them — the node, its transform, its material and its render
flags stay. geometry.kind = 'triangles' | 'edges' | 'vertices' switches the draw mode (lines /
points, see Geometry); set it before building. The geometry getter is what Shape's
auto-box measures for a collision shape (see physics).
Change a mesh's colour or material: mesh.material
mesh.material // slot 0 — get/set, never null on a Mesh
mesh.material.color = '#e33' // recolour the material it has (write-only)
mesh.setMaterial(material, index = 0): this // chainable, any slot
mesh.getMaterial(index = 0): Material | null // null if the slot is emptyconst scene = new Scene()
const ball = Mesh.sphere({ material: Material.lit({ color: '#3a7' }), position: [0, 1, 0] })
scene.add(Light.sun(), ball)
scene.open()
ball.material.color = '#e33' // recolour the material it has
ball.material = Material.lit({ color: '#888', metallic: 1 }) // or give it another onePrimitives have one material slot. Passing no material to a factory gives a default
Material.unlit() (flat, unlit, no lighting response). A material shared by several meshes
recolours all of them. The built-in materials and their options: Material.
Draw hundreds of copies: InstancedMesh
The same geometry hundreds or thousands of times (grass, crates, coins, rubble, a crowd of
identical props): one InstancedMesh with a transform per instance instead of one Mesh per
copy. It is hardware-instanced — the engine applies each instance's matrix, any regular material
works, and it costs one draw call per 64 instances (8 on WebGL), each chunk frustum-culled on its
own bounds. Not on the web (Web Lite) or in headless tests (the platform line above).
const scene = new Scene()
scene.add(Light.sun())
scene.open()
const crates = new InstancedMesh(Geometry.box(0.8), 500, {
material: Material.lit({ color: '#b5854b', roughness: 0.8 }),
castShadows: true,
})
for (let i = 0; i < 500; i++) {
crates.setAt(i, { position: [ (i % 25) * 1.2, 0.4, Math.floor(i / 25) * -1.2 ], rotation: Quat.fromEuler(0, i * 37, 0) })
}
crates.commit() // one upload for everything set since the last commit
scene.add(crates)
class Bob extends Aspect<'bob', InstancedMesh> {
private t = 0
update(dt: number) { // edit, then commit once per frame
this.t += dt
this.node.setAt(3, { position: [ 3.6, 1 + Math.sin(this.t), 0 ] }).commit()
}
}
crates.aspect(Bob)new InstancedMesh(geometry: Geometry, count: number, options?: MeshOptions) // count is fixed (≥ 1)
crates.count // the capacity (read-only)
crates.setAt(i, { position?, rotation?, eulerAngles?, scale? }) // local to the node; omitted = identity
crates.setMatrixAt(i, mat4) // column-major, local
crates.setMatrices(float32, offset = 0) // bulk, 16 floats each, from instance `offset`
crates.hideAt(i) // zero matrix — nothing is drawn for it
crates.matrixAt(i) // → Mat4 (a copy of what is stored)
crates.commit(): this // upload the dirty range
crates.dirty // anything uncommitted?
crates.material = Material.lit() // get/set, default Material.unlit()
crates.setShadows(cast: boolean, receive = cast): this // every instance at onceEvery instance starts hidden (a zero matrix) until you place it. rotation is a quaternion and wins
over eulerAngles; setAt / setMatrixAt / hideAt throw a RangeError outside
0 … count-1. Edits are buffered on the
JS side; nothing reaches the engine until commit(). The node's own transform applies on top of
every instance — move the InstancedMesh to move the whole field. Only the geometry's triangles,
normals and uv are used (not kind, uv1 or colors).
setAt's eulerAngles is converted to radians twice today — [0, 90, 0] turns an
instance by ~1.6°, not 90°. Pass rotation: Quat.fromEuler(x, y, z) (degrees) instead.
For debris that flies, bounces and dies on its own, use mesh particles instead
(Particles({ mesh }), see particles.md) — they simulate; InstancedMesh is for
things you place.
Shadows and culling: castShadows, receiveShadows, culling
mesh.castShadows = true // write-only; default true
mesh.receiveShadows = true // write-only; default true
mesh.culling = false // write-only; disable frustum culling (always draw)Also factory options (Mesh.box({ castShadows: false })); an InstancedMesh sets both shadow
flags with setShadows(cast, receive).
these are setters only — there is no readback. Set them at creation (the factory options) or keep your own flag.
Draw a marker on top of everything: renderPriority
mesh.renderPriority = 7 // write-only; coarse draw order 0 (first) … 7 (last), default 4renderPriority is Filament's renderable priority: inside one priority opaque draws sort
front-to-back and blended ones back-to-front, the priorities draw in order. Its use is an overlay —
a gizmo, a marker — whose material has depthTest: false (see material.md): in
priority 7 nothing drawn later can cover it. Also a factory option (Mesh.box({ renderPriority: 7 })).
Optional on the host: an older one warns once and keeps the default.
const scene = new Scene()
scene.add(Light.sun(), Mesh.box({ material: Material.lit(), position: [0, 0.5, 0] }))
scene.open()
const marker = Mesh.sphere({
material: Material.unlit({ color: '#ff0', depthTest: false }), // ignore what is in front
radius: 0.15,
position: [0, 0.5, 0], // inside the box, still visible
renderPriority: 7, // drawn last: nothing covers it
castShadows: false,
})
scene.add(marker)Pitfalls
// ✗ expecting the plane to lie flat by default
Mesh.plane({ scale: 10 }) // stands upright, facing +Z
// ✓ point the normal up for a floor
Mesh.plane({ scale: 10, normal: [0, 1, 0] })
// ✗ mutating a STORED transform copy (value semantics — see conventions)
const p = crate.position; p.y = 2
// ✓ (crate.position.y = 2 written directly compiles to this)
crate.y = 2See also
- Node — transform, hierarchy, events (inherited)
- Geometry — the buffers, the builders, geometry files
- Material — the built-in materials and your own shaders
- Model — GLB imports (the other renderable node kind)
- Physics & shapes —
Shapeauto-boxes frommesh.geometry - Conventions — units, degrees, value semantics