← Paracosm·Docs/Handbook
Contents · Rigged creatures (animesh) on the viewer's skeleton

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

2. The skin block (decoded from a real viewer upload pulled out of Asset.db, then matched exactly)

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.

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.

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.

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

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

  1. Map joints to stock names. A table from your rig's joint names to stock bone names (elephant_gait.BENTO, zebra_rig.BENTO_ZEBRA are examples). For a .dae destined for the viewer's uploader, tools/relabel_dae_bento.py renames joints in place (names only; weights are index-based). Check each name exists in tools/avatar_skeleton.xml; sl_skeleton.closed_joint_set raises on an unknown bone.
  2. 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.
  3. Check against the decoded reference. animesh_skin.validate_against_ref(skin, "assets/firestorm-ref-skin.json") compares invBind and altInvBind for bones the reference shares (bind_shape is mesh-dependent). tools/build_animesh_data.py does this before writing.
  4. Bench it. tools/render_inworld.py: load(json), deform(names, pF, V, W, pose) and strip(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.py prints the rest reconstruction error). A pose that reads right here still has to be seen in the viewer once.
  5. 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}; bento maps rig names to stock names. It refuses a pose that does not loop and round-trips the file. tools/verify_anim.py decodes independently the way the viewer does (dequantise, recover w = sqrt(1 - x^2 - y^2 - z^2)).
  6. Spawn and play. wb("spawn_mesh", file=..., x=..., y=..., z=..., texture=...), then wb("store_anim", file=..., name=...) and wb("play_anim", uuid=..., anim=..., name=...), then save_world. Optionally wander and behavior (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

Superseded notes you will find in this repo

These older statements are wrong or obsolete. Do not follow them.