Scenario aspects — ready-made behaviors
Five ready-made aspects that make a 3D scene DO something without writing code: travel to a point,
run a waypoint route, spin, face a target, start a GLB animation. Built for the visual scene editor
(attach from the Aspects menu, pick targets in the viewport), they are ordinary aspects from code
too (node.aspect(MoveTo, { target })); for a behaviour of your own write an
Aspect.
- move an object to a point, or back and forth between two
- drive a car or a bus along a waypoint route
- spin a fan, a coin or a pickup continuously
- make a turret or a camera keep facing a target
- start a model's animation clip when the scene opens
- move a camera along a path while it looks at the hero
At a glance
const level = defineScene({
nodes: {
crate: {
mesh: { kind: 'box', size: 1 },
position: [0, 0.5, 0],
aspects: [use(MoveTo, { target: ref('dropPoint'), duration: 3, mode: 'pingpong' })],
},
dropPoint: { position: [4, 0.5, -2] }, // an Empty: no source block
fan: { mesh: { kind: 'box', size: [2, 0.1, 0.2] }, position: [0, 3, 0], aspects: [use(Spin, { speed: 180 })] },
turret: { mesh: { kind: 'box', size: 0.5 }, position: [-3, 0.25, 0], aspects: [use(LookAt, { target: ref('crate'), smoothing: 0.2 })] },
bus: {
model: asset('./bus.glb'),
aspects: [use(FollowPath, { path: ref('route'), duration: 12, forward: 'z' }), use(PlayAnimation, { clip: 'Drive' })],
},
route: {
children: {
stop1: { position: [0, 0, 0] },
stop2: { position: [8, 0, 2] },
stop3: { position: [5, 0, 8] },
},
},
},
})
const main = async () => {
const { nodes } = await level.open()
nodes.crate.moveTo.duration = 1.5 // every field is live
nodes.turret.facing.target = nodes.bus // LookAt's accessor is `facing`
}
main()Targets and waypoints are Empty nodes
The usual recipe pairs these aspects with Empty nodes (plain group nodes — see scene-files.md): place an Empty where something should go, then point the aspect at it. On a camera node, the same aspects become camera moves.
Motion runs per frame in play mode (update(dt)); times are in seconds, angles in degrees. Each
one's fields are live properties on its accessor — node.moveTo, node.followPath, node.spin,
node.facing, node.playAnimation — so code can change a duration or a target while it runs.
while a scene is being edited, the aspects are inert, but the movement ones draw their paths as editor gizmos (blue for movement, amber for look-at; overlay lines, not scene content), so a route can be seen and tuned without running the scene.
Move a node to a point: MoveTo
Travel from the node's starting position to a target node.
| Field | Default | Meaning |
|---|---|---|
target |
null |
Destination node (ref('path') in scene files — see scene-files.md for the scoped-upward resolution rules) |
duration |
2 |
Seconds for the full trip |
delay |
0 |
Seconds to wait before starting |
mode |
'once' |
'once' stop there · 'loop' restart from the start point · 'pingpong' back and forth |
easing |
'smooth' |
'smooth' ease-in-out · 'linear' constant speed |
The start is where the node is when the trip begins (its first frame with a target). The target is
tracked live — if it moves, the trip bends toward its new position (and once keeps the node pinned
to it after arriving).
crate: {
mesh: { kind: 'box', size: [1, 1, 1] },
position: [0, 0.5, 0],
aspects: [use(MoveTo, { target: ref('dropPoint'), duration: 3, mode: 'pingpong' })],
},
dropPoint: { position: [4, 0.5, -2] }, // an EmptyDrive along a waypoint route: FollowPath
Travel through the children of a path node, in scene-file order — add an Empty per waypoint.
| Field | Default | Meaning |
|---|---|---|
path |
null |
The node whose children are the waypoints |
duration |
6 |
Seconds for one full pass |
delay |
0 |
Seconds to wait before starting |
mode |
'loop' |
'loop' closed circuit (last → first) · 'once' stop at the last point · 'pingpong' back and forth along the open path |
orient |
true |
Turn to face the direction of travel |
forward |
'-z' |
Which local axis leads when orienting |
Speed is constant along the whole path (segment lengths are measured live, so dragging a waypoint
retimes the run correctly). GLB models usually face +z — flip forward if your model drives
backwards.
bus: {
model: asset('./bus.glb'),
aspects: [use(FollowPath, { path: ref('route'), duration: 12, forward: 'z' })],
},
route: {
position: [0, 0, 0],
children: {
stop1: { position: [0, 0, 0] },
stop2: { position: [8, 0, 2] },
stop3: { position: [5, 0, 8] },
},
},Spin continuously: Spin
Continuous rotation about a local axis — speed degrees per second (default 45; negative spins
the other way), axis 'x' | 'y' | 'z' (default 'y'). Composes in quaternion space, so it's
correct on pre-rotated nodes.
const scene = new Scene()
const fan = Mesh.box({ size: [2, 0.1, 0.2], position: [0, 3, 0] })
fan.aspect(Spin, { speed: 180 }) // 180°/s about its local Y
scene.add(Light.sun(), fan)
scene.open()Keep facing a target: LookAt {#lookat}
Keep the node facing a target node each frame.
| Field | Default | Meaning |
|---|---|---|
target |
null |
The node to face |
forward |
'-z' |
Which local axis points at the target |
smoothing |
0 |
0 snaps instantly; otherwise seconds of turn lag (bigger = slower) |
A turret tracking the player, a signpost, or — on a camera node — a camera that keeps a moving
hero framed while FollowPath drives the crane.
Play a model's animation on start: PlayAnimation
Start a GLB clip when the scene runs. Attach to a model: node.
| Field | Default | Meaning |
|---|---|---|
clip |
'' |
Clip name; empty = the model's first clip |
loop |
true |
true = the model's loop (model.anim.playLoop); false = played once, holding its last frame |
speed |
1 |
Playback rate |
const scene = defineScene({
nodes: {
hero: { model: asset('./hero.glb'), aspects: [use(PlayAnimation, { clip: 'Idle' })] }, // loops
door: { model: asset('./door.glb'), aspects: [use(PlayAnimation, { clip: 'Open', loop: false })] },
},
})
scene.open()In the editor its inspector card lists the model's clips in a dropdown and can Preview the clip
live while editing (and Stop a non-looping preview). For runtime control (switching clips from
your own code, seek, events) use model.anim directly — this aspect only presses play
when it attaches.
See also
- Scene files —
defineScene,use,ref, Empties, camera nodes - Aspects — writing your own behaviours
- Animator — clip playback from code