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

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

TypeScript
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, or scale — the plane has no size option, only scale.
  • box.size is baked into the geometry buffers; scale is a node transform. Both draw the same, but a baked size is what the Shape auto-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

TypeScript
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 from

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

TypeScript
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

TypeScript
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 empty
TypeScript
const 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 one

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

TypeScript
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)
TypeScript
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 once

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

Note

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

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

Note

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

TypeScript
mesh.renderPriority = 7      // write-only; coarse draw order 0 (first) … 7 (last), default 4

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

TypeScript
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

TypeScript
// ✗ 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 = 2

See 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 — Shape auto-boxes from mesh.geometry
  • Conventions — units, degrees, value semantics