Objects

Everything you put in a 3D scene: meshes, groups, and the specialised drawables for crowds, lines, sprites and loaded models.

Meshes

Fourteen shorthand methods create a mesh from one flat options object that mixes the geometry's own parameters, a placement, and a material.

.box({ width: 2, height: 2, depth: 2, color: 'tomato', roughness: 0.3, position: [0, 1, 0] })
MethodMain parameters
boxwidth, height, depth, widthSegments, heightSegments, depthSegments
sphereradius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength
planewidth, height, widthSegments, heightSegments
cylinderradiusTop, radiusBottom, height, radialSegments, openEnded
coneradius, height, radialSegments, openEnded
torusradius, tube, radialSegments, tubularSegments, arc
torusKnotradius, tube, tubularSegments, radialSegments, p, q
circleradius, segments, thetaStart, thetaLength
ringinnerRadius, outerRadius, thetaSegments, thetaStart, thetaLength
capsuleradius, height, capSegments, radialSegments
polyhedronshape, radius, detail
extrudeshape, depth, bevel, bevelThickness, bevelSize
lathepoints, segments, phiStart, phiLength
tubepoints, radius, tubularSegments, radialSegments, closed

All angle parameters are degrees.

extrude takes the same path data the 2D Path node takes, so an existing outline becomes a solid with no new vocabulary:

.extrude({ shape: 'M 0 -60 L 52 40 L -52 40 Z', depth: 20, bevel: true, bevelSize: 2 })

Placement

Every mesh, light and group takes the same placement fields.

FieldTypeDescription
positionVector3InputPosition in the parent's space
rotationVector3Input | Euler3Euler rotation in degrees
quaternionQuaternionRotation as a quaternion. Wins over rotation
scaleVector3InputPer-axis scale, or one number for all three
lookAtVector3InputPoint to orient toward. Wins over both rotations
visiblebooleanHide without removing
castShadowbooleanCast into shadow maps
receiveShadowbooleanReceive shadows
renderOrdernumberDraw-order override for transparent sorting
keystringExplicit identity. See Animating

Material shorthand

The same options object also accepts the common material fields, so the usual case is one flat call:

.sphere({ radius: 1, color: 'cyan', roughness: 0.2, metalness: 0.8 })
.plane({ width: 10, height: 10, color: '#11141c', roughness: 0.85 })
.torus({ radius: 1.4, tube: 0.08, unlit: true, color: 'white' })

color, opacity, transparent, roughness, metalness, emissive, emissiveIntensity, map, normalMap, roughnessMap, metalnessMap, aoMap, alphaMap, envMapIntensity, wireframe, flatShading, side, vertexColors, depthWrite, depthTest, blending, alphaTest and toneMapped are all accepted this way.

unlit: true uses a basic material instead of standard, which is right for anything that should read as its own light source rather than being shaded by the scene.

For full control, pass a material built with Mat. See Materials and textures.

Groups

group() nests objects so they inherit a transform. Prefer the callback form, which cannot be left unbalanced.

new Graphics3D()
  .group({ position: [3, 0, 0], rotation: [0, 45, 0] }, g => g
    .sphere({ radius: 0.8, color: 'cyan' })
    .box({ width: 0.4, position: [0, 1.2, 0] }))

push() and pop() do the same thing imperatively. An unbalanced pair throws when the scene is drawn, rather than silently reparenting everything after it.

Explicit geometry and material

mesh() takes a geometry and a material as separate values, built with Geo and Mat. Hoist them and share them across meshes, since the renderer uploads a shared geometry to the GPU once no matter how many meshes reference it:

import { Geo, Mat } from 'motion-script';

const brick = Geo.box({ width: 1, height: 0.5, depth: 0.5 });
const red = Mat.standard({ color: 'tomato', roughness: 0.4 });

new Graphics3D()
  .mesh(brick, red, { position: [0, 0, 0] })
  .mesh(brick, red, { position: [1, 0, 0] })   // same GPU buffer, same shader

Geo also has entries the shorthand methods do not: tetrahedron, octahedron, icosahedron, dodecahedron, buffer, parametric, edges, wireframe and model.

Geo.buffer

Raw vertex data, for any mesh you can compute:

Geo.buffer({
  position: [/* x, y, z, x, y, z, ... */],
  index: [/* triangle indices */],
  computeNormals: true,
})

normal, uv and color are optional. Set staticData: true once the arrays stop changing so the renderer skips re-uploading them, and bump revision when you mutate an array in place.

Geo.parametric

A surface sampled over a grid, which is the friendlier form of Geo.buffer for waves, terrain and ribbons:

Geo.parametric({
  segments: [70, 70],
  vertex: (u, v) => {
    const x = (u - 0.5) * 20;
    const z = (v - 0.5) * 20;
    return { x, y: wave(x, z, phase()), z };
  },
  color: (u, v, p) => heightColor(p.y),
  computeNormals: true,
})

u and v both run from 0 to 1. The callbacks are evaluated into vertex buffers when you record the op, so the descriptor stays plain data. Per-vertex colour needs vertexColors: true on the material.

Geo.edges and Geo.wireframe

Derived line geometry from another geometry. edges gives the hard edges only, which is a clean outline. wireframe gives every triangle edge.

.line({
  geometry: Geo.edges(Geo.box({ width: 4, height: 4, depth: 4 })),
  mode: 'segments',
  color: 'white',
  opacity: 0.4,
})

Instances

Thousands of copies of one geometry in a single draw call. This is how you put a crowd on screen, since one mesh per copy will not keep up.

.instances(
  Geo.box({ width: 0.2 }),
  Mat.standard({ color: 'cyan' }),
  positions.map(p => ({ position: p, rotation: [0, p.x * 20, 0] })),
  { colors: tints },
)

The instance array's length is the instance count. Each entry is a full placement. colors tints per instance and needs a material that reads instance colour.

Lines

.line({ points: [[0, 0, 0], [1, 2, 0], [3, 1, 2]], color: '#6990DD', width: 2 })
.line({ geometry: Geo.edges(box), mode: 'segments', color: 'white', opacity: 0.4 })
PropTypeDefaultDescription
pointsVector3Input[]Explicit points. Give either this or geometry
geometryGeometry3DA geometry to draw as lines
mode'strip' | 'segments' | 'loop''strip'Connect in order, as disjoint pairs, or closed
colorColorShorthand for a line material
widthnumberLine width
dashedbooleanfalseUse a dashed line material

Points

A point cloud, one dot per vertex of the geometry:

.points(Geo.buffer({ position: cloud }), Mat.points({ size: 0.05, color: 'white' }))

Sprites

A camera-facing textured quad, which always presents flat to the viewer:

.sprite({ map: 'spark.png', position: [0, 2, 0], scale: 0.5 })

Models

Load a glTF, GLB or OBJ file:

.model({ src: 'robot.glb', position: [0, 0, 0], scale: 0.5 })
PropTypeDescription
srcstringPath in the asset manifest
animationModelAnimation3D | ModelAnimation3D[]Clips to sample
overrideRecord<string, Material3D>Replace materials by mesh or material name

Baked animation is sampled at an explicit time rather than advanced by a delta, which is what keeps a model frame-identical when you scrub:

.model({
  src: 'robot.glb',
  animation: [{ clip: 'Walk', time: walkTime(), weight: 1 }],
})

clip is a name or an index, and defaults to the first clip. Give several entries with weight values to cross-fade between them.