Motion
Things move in this world by four mechanisms: keyframed motion (server-side, smooth in the viewer), llTargetOmega
(client-side spin), texture animation (client-side scrolling), and scripted position/colour/size changes on a
timer. Particles are covered in weather-and-light.md.
Keyframed motion
1. Keyframed rotation frames apply in the object's LOCAL frame
Tested: a yaw of 90 degrees, then a frame about x, turned the object about its own x. So a frame's rotation delta is
rock_i / rock_(i-1), and a loop of such deltas closes exactly (no drift over hours); deltas in the other
convention leave a residue every loop.
- Keyframed motion is smooth in the viewer, where a timer poking positions steps.
- See:
tools/grow_boat.py/tools/build_harbour.py(the boat),tools/build_gull.py(flight).
2. Build loops that close on themselves and never jolt
Put every phase at zero at the loop's start (use terms like sin w, 1 - cos w, and a gust as one sine over the whole loop), and rebuild the loop only at a boundary (on a timer of whole loops) so a new amplitude (a storm) never jolts it.
3. Rock about a pivot with absolute poses
For frame i with a world-frame tilt T_i: q_i = HROT * T_i, p_i = PIV + (HOME - PIV) * T_i; the keyframes are
[p_i - p_(i-1), q_i / q_(i-1), dt].
- See:
tools/build_sway.py.
4. Two objects sway as one if they run identical frames from the same wall-clock mark
A tree's bark and its leaves are separate objects. Both carry the same loop about the same pivot and START on the
same wall-clock mark (seconds into the minute from llGetTimestamp; a loop length that divides the minute keeps
the marks on loop boundaries). Rebuild once a minute: stop the motion, set the recorded REST pose, start a fresh
loop.
- Record the rest poses ONCE, before anything moves: a later
listreads mid-sway positions.
5. Looping keyframed motion costs every tick; gate it
OpenSim's keyframe timer moves each looping object about 20 times a second: 266 swaying objects were 16% of a core with nobody on the region. Move only what a visitor could see: each tree's halves decide at their shared mark, from the tree's ROOT, whether a visitor is within 80 m (so the halves agree), and something invisible (summer snow on branches) does not move at all. Idle cost fell to 3%, 12% with a visitor present.
- Measure cost only once every loop has actually started (a first reading of 3% was taken too early).
6. A weathervane turns by a keyframed rotation delta
llWind(ZERO_VECTOR) would give the region's wind, but it is a weak random field; read the region's one shared
wind instead (scripts.md). The head points where the wind comes FROM (atan2 + PI); turn by a keyframed rotation
delta over a couple of seconds (delta = target yaw - current yaw, since keyframed rotation is local).
7. Flight on keyframed motion
Heading from the path tangent, bank from the turn rate (clamped), pitch from the climb, a flare at the landing; the path is a Catmull-Rom spline through a loop. Birds come in to land from downwind and settle facing the wind.
- See:
tools/build_gull.py.
WorldBridge wander and behavior
8. Use many waypoints on a wander loop
With few waypoints keyframed motion interpolates the whole corner turn across the straight segment and the creature visibly spins; about 24 steps on the circle makes the turn gentle and continuous. (The module's default is 14.) The module now also modulates the loop's radius with low-frequency lobes so the path is smooth everywhere; a jagged random-radius loop made creatures "spin on a dime".
9. Tune facing by measurement
faceoffset (degrees) corrects a model whose forward axis is not +X. Tune it by polling the object's x, y and yaw
over time and comparing the movement heading with the object's yaw (the elephant needed -14 degrees, the zebra -5).
10. Stop the old motion before starting a new one
Re-applying a wander without stopping the previous keyframed motion left both timers running and the object
yo-yoed between two loops. ApplyWander stops the old one first.
11. Pause with Suspend(), not Pause()
KeyframeMotion.Pause() clears the running flag, and Resume() only restarts a running motion, so Pause then Resume
froze the creature for good. Suspend() halts the timer but keeps the flag, so Resume() works.
- See:
worldbridge/WorldBridgeModule.cs(BehaviorTick).
12. Followers ride the leader's exact path
wander with leader (a uuid) and trail (waypoints behind) seeds the loop from the leader, so a follower walks
single file in its footsteps (same cx, cy, radius and steps required). behavior jitters every creature's
phase so a herd desynchronises.
WorldBridge roam: creatures that wander a real landscape
roam (worldbridge/Roam.cs) supersedes wander + behavior for creatures. Body:
{uuid, walk_anim, pauses:[{anim,kind}], speed, faceoffset, clear, max_slope, height, body_len, cx, cy, radius, leader, leg_min, leg_max, pause_min, pause_max}; {uuid, off:true}; {status:true}; {regrid:true}.
tools/spawn_herd.py is a complete example.
17. Build the walk grid from physics, not from names or boxes
One downward physics ray per 1 m cell, from the body's height to the ground: anything solid met above a step's height blocks the cell; phantom ground cover (ferns, tussocks, shrubs) never blocks. A chamfer distance field gives each creature its own clearance. Measure slope as the ground's gradient across about 4 m: a steepest-1-m-step measure called a third of a gentle, bumpy range too steep and stranded the elephants.
18. Physics fills in after the prims
A grid built the moment the region loads sees nothing solid ("0 solid" in the log). Wait about 75 s after load, and treat a grid that found nothing solid as stale after two minutes.
19. Journeys, ground and gait
Dijkstra over the cells this creature can use; a destination 10-28 m off, mostly ahead and away from recent
stops, turning back before the range's edge; string-pulled, Chaikin-smoothed, resampled every metre. Each keyframe
sits the body on the ground's tangent plane under its four feet: pitched with the slope along its length AND rolled
with it across its width (body_len, body_wid), both eased along the path. Pitch alone left a herd standing
level across a hillside. Land exactly on the journey's end, and if a busy server has run the keyframes slow, wait
for it rather than snapping.
- Stride: give each creature a speed so that speed x walk-cycle = the body travel that holds a planted foot
STILL on the ground. That is the foot's sweep while planted divided by the fraction of the cycle it is planted,
not the sweep itself: for a walk (planted 75% of the cycle) the sweep is a quarter short, and the planted feet
slide backward. Measure it with
tools/bench_stride.py <animesh.json> <pose fn>, which prints the sweep, the still-foot travel, and the front/hind balance (the front-swing factor that makes all four feet agree).
20. Never stuck
A blocked start cell steps to the nearest usable one. If no normal destination can be reached (a creature spawned into a pocket between a boulder and a steep bank stood there half an hour, and its followers with it), plan again with relaxed limits and accept any cell over 3 m away. A follower whose leader has stood still for long goes its own way. Log repeated planning failures instead of retrying silently.
20b. Predators, prey and the chase
A roamer names the kinds it is wary_of (a zebra: the lions) and the kinds it hunts (a lioness: zebras, with a
hunt_chance per journey). The wary move off at flee_speed with flee_anim from a predator walking their way
(within 1.4 x flee_dist) or one that comes very close; a hunter now and then STALKS, walking to a point short of the
nearest of its prey. Give it charge_anim, charge_speed and charge_dist and the stalk ends in a CHARGE: stopped
within charge_dist of its prey, it runs at where the prey stands, and the prey, seeing it come, runs too; the
charger stops where the prey stood (a pursuit with no kill). Who notices whom: a predator walking is noticed at
1.4 x the prey's flee_dist, one lying up or STALKING only at half of it (a stalk creeps), one CHARGING at 1.6 x.
Before the stalk crept, every stalk ended with the prey long gone and no charge: they saw it walking at 17 m. The gallops are solved by the same IK as the
walks (tools/ik_gait.py with a short stance and high folds): a ROTARY gallop for cats (the hind feet land one after
the other, then the fore in the opposite order), a TRANSVERSE one for horses and zebras (the fore in the same order).
- See:
worldbridge/Roam.cs(StalkTarget,ChargeTarget, the charge inArrive),tools/spawn_herd.py(GALLOP_CAT,GALLOP_EQ,charge=).
21. A creature carries its own roam
Roam registers a creature by uuid, and a copy rezzed out of a gift box has a new uuid, so it stood still. An object
that enters the scene holding a notecard named roam in its root prim now registers itself: the notecard is the
roam body as JSON (no uuid), its animations may be NAMES of animation items in the root prim, and its range is
centred where it was put down (radius 16 unless the notecard says). roam_notecard {uuid} writes that notecard
from a live roamer, with this world's keys (cx, cy, water, wary_of, hunts, leader) removed and its
animations named. Details and an example: worldbridge.md, "A creature that carries its own roam".
- A hand-written notecard may give
waterwithout adrink_anim: the creature walks down, pauses at the edge as anywhere, and goes home (it used to stay on the trip's wide grid for good).
Spin and scroll
13. One mesh prim spins as a whole with llTargetOmega
A waterwheel needs no linkset: it is one mesh turned by llTargetOmega (client-side). Bake the yaw into the
vertices so the spin axis means the same in local and region terms.
- See:
tools/build_waterwheel.py.
14. A positive texture-animation rate scrolls toward -u
llSetTextureAnim(ANIM_ON | SMOOTH | LOOP, ALL_SIDES, 1, 1, 1.0, 1.0, rate) scrolls TOWARD -u for a positive rate.
Pass a NEGATIVE rate to run water streaks downstream along +u.
- Learned: the first water ribbon flowed uphill.
- One prim per speed: a slow pool and fast falls are separate ribbons.
- See:
tools/waterribbon.py,tools/build_vein.py.
15. Texture animation of a single frame is not held
See textures.md: to show one cell of an atlas, use repeats and offset; animate only where motion is wanted.
Rigid bodies from unlinked prims
16. Apply one shared transform to every part
When a thing is several unlinked prims (an early rowboat), bake each prim's offset from the thing's centre and its base rotation into its script, and apply one shared rock (position AND rotation) to all of them every tick. Check rigidity by reading back the distance between two parts over time (it held at 3.60 m while the bow-to-stern height difference swung 0.34 m).
Checking motion
- A single screenshot cannot show motion: take two about half a cycle apart (verification.md).
- For offline animations, verify a GIF animates by the mean difference between CONSECUTIVE frames, not frames mirrored about the middle of a ping-pong loop; and do not trust a whole-frame metric when the thing that changes is a few percent of the pixels (a season change read as "1.5, too similar" while the forest was plainly ablaze).