Rigged creatures (animesh) on the viewer's skeleton
An animesh is a rigged mesh object that plays animations on its own copy of the viewer's avatar skeleton. OpenSim
supports it fully: the flag is PrimitiveBaseShape.AnimeshEnabled (sent as the mesh-flags extra parameter, and it
needs a mesh sculpt type), and an object animation is SceneObjectPart.AddAnimation(animId, name), the same call
as llStartObjectAnimation. WorldBridge's spawn_mesh sets the flag whenever the meshdata carries a skin.
Getting the viewer to deform the mesh correctly was the hardest problem in this project. It took many hours of
headless verification that all passed while the viewer showed collapsed sheets, then a decoded reference upload,
then a second bug that the bench could not see because it shared the encoder's mistake. The section below is the
authoritative result. Where older notes in this repo disagree (see the end of this chapter), follow this chapter
and the current code in tools/animesh_skin.py and tools/sl_skeleton.py.
The authoritative rules
1. The viewer's skeleton is fixed
A "custom skeleton" is the stock bones MOVED by joint-position overrides. The uploader refuses unknown joint names
("Model has an unknown joint(s)") and the viewer cannot drive them. Map each of your joints to a stock name (legs,
arms, and the Bento extras: mHindLimb*, mWing*, mTail*, mFace*).
- The full list of bone names, with their default positions and parents, is the viewer's
character/avatar_skeleton.xml, vendored here astools/avatar_skeleton.xml. - An
.animthat targets a custom name (for examplehip0) silently does not animate.
2. The skin block (decoded from a real viewer upload pulled out of Asset.db, then matched exactly)
- Vertices are normalised to [-0.5, 0.5] by
spawn_mesh. bind_shape_matrix= the UN-normalisation: a diagonal of the extent plus the centre as translation. (Emitting identity here was the whole "collapse" bug.)inverse_bind_matrix= translate(-p) of each joint's rest position, in real metres.alt_inverse_bind_matrix= translate(p - p_parent): this is the joint-position override (what the uploader's "include joint positions" checkbox writes).- Matrices are built as numpy column-vector matrices and emitted TRANSPOSED (the viewer stores row-vector).
- Emit native Z-up, or the creature lies on its side (the uploader auto-rights an uploaded mesh; a programmatic spawn gets no such help).
- WorldBridge also writes
pelvis_offset(0.0 unless the skin givespelvisOffset) whenever analt_inverse_bind_matrixis present, because a real viewer upload always carries it with the override. See CONFLICTS.md for the two recorded explanations of the "front legs at the rear hip" bug.
3. The override is an offset from the VIEWER'S parent, so the hierarchy must come from the viewer's own file
Take every parent from avatar_skeleton.xml (read by tools/sl_skeleton.py), never from a table written from
memory.
- Learned: the first encoder hung
mHipLeft/mHipRightfrommChest; the viewer hangs them frommPelvis. So each front leg rendered at pelvis + (hip - chest): the front feet at the rear hip, 1.5 m out, seen in the viewer as "legs spread out" for a day. - Other parents that differ from intuition:
mHindLimb*hang frommHindLimbsRoot(20 cm behind the pelvis),mFaceEar*frommFaceRoot,mFaceChinfrommFaceJaw,mTorsoviamSpine1/mSpine2,mChestviamSpine3/mSpine4. - Close the joint set under the true hierarchy and place every inserted ancestor at zero length on its nearest
placed ancestor, so nothing rests on the human default positions (which appearance parameters can also reshape).
sl_skeleton.closed_joint_setandplace_intermediatesdo this;animesh_skin.build_skincalls them.
4. A bench must model the foreign system from the foreign system's files
tools/render_inworld.py once placed every joint where the MESH expected it, under the encoder's own parent table,
so it agreed with the encoder's mistake by construction: rest error 0.00002 while in-world was 1.56 m wrong.
Rewritten to do forward kinematics the viewer's way (each skinned joint at its parent plus the override it was sent,
under the viewer's hierarchy; every other joint at its default position), it measures the old elephant 1.56 m out
and the rebuilt one 16 micrometres.
5. Prove a rig path on two bones first
tools/make_bend_box.py puts a 4 x 2 x 1 orientation box, subdivided into 1 m squares, on mHipRight (the -X half)
and mHipLeft (the +X half), both hinged at the origin, with only the inner two quarters blended (smoothstep over
x in [-1, 1]), and a 6 s 0-90-0 fold. Seen in the viewer: one half stands up, red +X end on top, and the tip keeps
its shape. With bones at identity rest rotation, rotY(-a) swings +X toward +Z, as the math says.
6. Every chain must be a chain in the viewer's skeleton
An animated bone carries only its viewer descendants, so a rig chain mapped onto bones that are siblings in the viewer's skeleton will not move together, however the overrides place them.
- Front legs: the arm chain.
mCollar*hangs frommChest(let the encoder place it at zero length), thenmShoulder*,mElbow*,mWrist*. ThemHip*bones hang from the pelvis, so a spine or chest motion would leave front legs mapped there behind. - Hind legs:
mHindLimb1-3(undermHindLimbsRoot). - Face bones are mostly SIBLINGS under
mFaceRoot. The longest true face chain ismFaceJaw->mFaceTeethLower->mFaceTongueBase->mFaceTongueTip, which carries an elephant's trunk. LeavemFaceRootunanimated on the head so the ears (its children) follow the head rather than the trunk. (Learned: the first trunk ended onmFaceChin, which hangs from the jaw, so the tip never followed the trunk.) - Put a quadruped's neck joint at the WITHERS. Placed two-thirds up the neck, the lower neck is weighted to the chest and the head cannot reach the grass.
- An elephant grazes with its trunk (sweeping the ground, then a J-curl up to the mouth), not by dropping its
head. Keep the chest level while grazing, because the front legs hang from it. See
elephant_gait._graze_elephant. tools/spawn_rig_test.pyputs one of each creature in a paddock beside the bend box for a look in the viewer.
7. .anim export
Local delta rotations per joint, quantised to U16. Guard loop continuity (t = 1 must equal t = 0) and round-trip decode. Priority 4. A bone absent from the animation holds its rest.
- See:
tools/build_walk_anim.py(export_anim),tools/verify_anim.py.
8. Two ways into the world, one requirement
The world path is store_anim + play_anim on WorldBridge. The viewer path is Firestorm's mesh uploader with
"include joint positions" ticked (free on a standalone grid). Both need the same true-hierarchy offsets: the
uploader computes each override from the .dae's node hierarchy, so a .dae must nest its joints exactly as the
viewer's skeleton does.
9. The viewer hangs an animesh from its rig origin, and ignores the prim's scale
- It draws the skeleton's origin at the prim's position, while the server places a prim by its bounding-box
centre. A rig whose origin is at its soles floats by half its height.
animesh_skin.build_skincentres every rig on its box before encoding. - It does not scale an animesh with its prim. One rig spawned at size 2 and at size 6 rendered identically, one
sunk and one floating; a "calf" spawned small was full grown. Bake each body's size into its own rig
(
tools/animesh_scale.py: vertices, inverse-bind and override translations, bind shape) and spawn it at its native extent..animfiles are rotations only, so they fit any size. - Learned from two screenshots and a two-size test in the viewer; no headless bench shows either.
10. A source model's rest pose can be wrong
The zebra model came with its head turned about 50 degrees to one side. Put the head joint where the turn begins
and counter-turn the head bone as a constant rest correction in every pose (elephant_gait._zebra_head). Search
the angle on the bench until the muzzle sits on the centreline: 62 degrees here, not the 49 the joint alone
suggests, because the skin blend near the joint lags.
The pipeline, tool by tool
- Map joints to stock names. A table from your rig's joint names to stock bone names (
elephant_gait.BENTO,zebra_rig.BENTO_ZEBRAare examples). For a.daedestined for the viewer's uploader,tools/relabel_dae_bento.pyrenames joints in place (names only; weights are index-based). Check each name exists intools/avatar_skeleton.xml;sl_skeleton.closed_joint_setraises on an unknown bone. - Build the skin.
animesh_skin.build_skin(bento_names, world_pos_zup, weights, verts_zup, tris, rgb, textured)returns(meshdata_dict, extent):bento_names: distinct stock names in the order your weight indices use.world_pos_zup: rest world position (metres, Z-up) of each named bone.weights: per vertex[[bone_index, w], ...], at most 4.verts_zup: N x 8 (position, normal, u, v).tris:[[a, b, c], ...].- It appends the missing ancestors with zero-length placements and writes one submesh plus
skin{joints, bindShape, invBind, altInvBind, weights}. WorldBridge writes the vertex weights into submesh 0 only, so an animesh here is a single-face mesh.
- Check against the decoded reference.
animesh_skin.validate_against_ref(skin, "assets/firestorm-ref-skin.json")comparesinvBindandaltInvBindfor bones the reference shares (bind_shape is mesh-dependent).tools/build_animesh_data.pydoes this before writing. - Bench it.
tools/render_inworld.py:load(json),deform(names, pF, V, W, pose)andstrip(pose_fn, out, ...)render side or front strips with a controlled camera. Deforming with an empty pose must reproduce the bind vertices (python tools/render_inworld.pyprints the rest reconstruction error). A pose that reads right here still has to be seen in the viewer once. - Write the animation.
build_walk_anim.export_anim(pose_fn, out, duration, nkeys=24, loop=True, bento=map):pose_fn(t)for t in [0, 1] returns{rig_joint: 4x4 local delta rotation};bentomaps rig names to stock names. It refuses a pose that does not loop and round-trips the file.tools/verify_anim.pydecodes independently the way the viewer does (dequantise, recover w = sqrt(1 - x^2 - y^2 - z^2)). - Spawn and play.
wb("spawn_mesh", file=..., x=..., y=..., z=..., texture=...), thenwb("store_anim", file=..., name=...)andwb("play_anim", uuid=..., anim=..., name=...), thensave_world. Optionallywanderandbehavior(motion.md).
The minimal test, end to end
python tools/make_uv_testrect.py # orientation box mesh + atlas (assets/uv-testrect.*)
python tools/make_j2c_pillow.py assets/uv-testrect.png assets/uv-testrect-mct0.j2c
python tools/make_bend_box.py --bench # skin + 6 s fold .anim + bench output
python tools/spawn_bend_box.py # spawn, store_anim, play_anim, save_world
--bench prints the rest error under the viewer hierarchy, the tip's rigidity at full fold (the maximum change in
distance between tip vertices), and the tip end's centre at full fold (want about [0, 0, 2]). Then look in the
viewer: one half should stand up with the red +X end on top. (Paths inside these tools point at the original
checkout; adjust them first.)
Craft notes from building the creatures
- Artist weights beat procedural ones where they exist. A procedural bone-segment reweight dented the
elephant's torso; its transferred artist weights were cleaner. Where there were none (the zebra),
tools/zebra_rig.pyweights by distance to the bone segments with an inverse-power falloff over the four nearest joints, then smooths the weights over mesh adjacency (a Laplacian pass), which removed rippling at the shoulder junction. - A loop must be whole harmonics. A half-frequency term (a trunk curl at half the walk frequency) left a 0.11
rotation error at the loop point.
export_animrefuses such a pose. - Walk cycles: a naive sine hip slides the planted foot (skating). A stance/swing cycle with a LINEAR hip sweep during stance tracks the foot back at the body's speed; a cubic Hermite swing whose end slopes match the stance rate stops the foot slamming down at the plant.
- Review motion at the joints at maximum bend, not from static eight-frame strips.
- Custom rigs from other tools: Firestorm's uploader rejects unknown joint names (the skin and joint-position checkboxes grey out); relabel to stock names first, and the uploader then reports "0 conflicts".
- Wind the triangles to agree with the normals, and check it. Swapping two coordinate axes (to change which way is up) is a reflection: it reverses every triangle's winding while normals carried through the same map still point outward. Renders that do not cull back faces look fine; the viewer culls the OUTSIDE and the creature shows its inside. Swap two index columns of every triangle along with the axes, and have the exporter refuse a mesh whose winding agrees with its normals on fewer than 95 % of its triangles.
- Hair is its own mesh, linked on. A mane sculpted into the body read as lumps and spilled onto the forelegs.
As hair it works: locks grown from the bare skin (flattened tapering tubes, wavy, drooping at the tip; a short
standing under-layer for volume and a long falling over-layer for the fringe), a strand texture whose U runs
across a lock and V along it, rigged on the SAME joints and landmarks as the body, every vertex of a lock weighted
at its ROOT (
zebra_rig.pyreads per-vertexanchors: by its own position a hanging tip takes the nearest leg bone), then LINKED into the body: a script in the body callsosForceCreateLink(mane, TRUE). The child's rigged mesh rides the root's skeleton, Roam moves the root, andlistshows one object. Scale the child's rig by the BODY's factor so both keep one skeleton (animesh_scale.scale_animesh). - Look at what a creature is PLAYING before judging its shape (
anims {uuid}): a lying pose caught in a still reads as a deformed walk.
Superseded notes you will find in this repo
These older statements are wrong or obsolete. Do not follow them.
- PLAN.md: the "F = (x, z, -y)" mesh frame, the per-species claims that the sagittal axis is rotZ for the elephant
and rotX for the zebra, the hand-written Bento PARENT table with
mHip*undermChest("front legs -> mChest"), and the two "viewer-side unknowns" (transpose and keyframe interpretation; transposed is correct). tools/animesh_skin.pymodule docstring (its first paragraph still describes the F-frame); the code below it uses identity (native Z-up), as its FRAME comment explains.tools/render_inworld.pymodule docstring ("skins in the mesh F-frame (X=forward, Y=up, Z=left)"); the FK inbone_worldsis the current, viewer-faithful version.tools/build_walk_anim.py: theINWORLD_SKIPcomment block about spine bones.