ludic/packages/ludic.anim
Orkuncakilkaya a465984c52 fix(render3d, runtime): a glTF document is freed whole, layouts found by number, block records reused
- Json.free_all (value_free_all): a parsed tree's nodes, lists and strings. render3d frees each
  glTF document that way at the next load; the names kept out of it are copies (a primitive's
  material, a skin's joints, an animation clip's name in ludic.anim) - a model's strings were
  ~640 KB left behind per load
- jp_number made a digits list per decimal in a document and never freed it
- gvk_layout_id matches a mesh's layout as numbers in a scratch made once, against the layouts
  known end to end; a new mesh no longer builds a key string
- gvk_mem_new puts a new block into the record of one given back rather than appending, so a
  buffer made again every few frames no longer grows the block lists

steady.ludic adds a glTF parsed and freed whole 200 times: 0 bytes (38,400 before the digits fix),
beside the buffer path and the frame, still 0. A model loaded and let go still keeps ~2 KB a round
(texture and buffer handles are not reused yet); it is bounded at 8 KB a round.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:36:42 +03:00
..
tests wip(0.S3): packages and examples migrated again from their pre-0.S sources in one run 2026-09-25 15:31:34 +03:00
bones.ludic feat(ludic.anim): skin_joint - a bone looked up in the skin's own joints, not the file's first of that name 2026-09-25 08:41:51 +03:00
clip.ludic refactor(packages): 0.R5 - a question no longer starts its state - 64 lazy starts ('if st.x == null { st.x = ... }' inside a getter) are the state's defaults, gear__ensure is gone, and ludic migrate state --tighten took mut off 208 package parameters: things_all, thing_find, gear_charge, gear_level, jobs_state and the rest read 2026-09-27 00:51:03 +03:00
index.ludic feat(ludic.anim): skin_joint - a bone looked up in the skin's own joints, not the file's first of that name 2026-09-25 08:41:51 +03:00
mix.ludic wip(0.S3): packages and examples migrated again from their pre-0.S sources in one run 2026-09-25 15:31:34 +03:00
package.ludic feat(packages): ludic.anim - glTF animation clips read out of the parsed document, sampled by halving, cross-faded two at a time and folded into a render3d Skin's pose; clips per rig by name, translations off unless asked, a late first key reported 2026-09-25 08:12:29 +03:00
pose.ludic wip(0.S3): packages and examples migrated again from their pre-0.S sources in one run 2026-09-25 15:31:34 +03:00
read.ludic fix(render3d, runtime): a glTF document is freed whole, layouts found by number, block records reused 2026-09-28 11:36:42 +03:00
README.md feat(ludic.anim): skin_joint - a bone looked up in the skin's own joints, not the file's first of that name 2026-09-25 08:41:51 +03:00

ludic.anim

The animation clips an animator keyed into a glTF, played on a render3d Skin and cross-faded two at a time. ludic.render3d parses no animations and offers a pose api (skin_set_quat, skin_set_offset, skin_pose) rather than a player, so the sampler lives here. Uses ludic_render3d and nothing else: it reads the document gltf_load just parsed and writes a Skin's pose. It asks the game nothing, so it has no port.

import "ludic.anim"
let m = gltf_load("assets/kit/hiker2", "hiker_m.gltf", "hiker_m")
anim_read_doc("assets/kit/hiker2")                 # straight after the load, before the next
let walk = anim_find("hiker_m_walk")
anim_play(sk, walk, t, idle, t2, 0.3)              # 70% walk, 30% idle, into the joint matrices

API

Clip { name, dur, n, cnode, cpath, ckeys, coff, cvoff, times, vals }, AC_ROT, AC_POS a clip, its channels flattened: a channel's keys are times[coff..coff+ckeys], its values 4 (rotation) or 3 (translation) per key from cvoff
anim_read_doc(dir) -> int every rotation and translation channel of every animation in gltf_doc, reading the .bin beside it in dir; how many clips were new
anim_find(name) -> Clip, anim_count(), anim_add(c) -> bool, anim_clear() the table, by name; a name already held is skipped on read and refused on add
anim_play(sk, a, ta, b, tb, blend) -> bool a at ta, cross-faded toward b at tb by blend (0 all a, 1 all b; b may be null), folded into the joint matrices
anim_mix(c, sk, t, w, rot, pos, hit), anim_to_pose(sk, rot, pos, hit), anim_key_at(c, ch, t) the pieces anim_play is made of, for a caller that mixes more than two
anim_set_positions(on), anim_positions() keyed translations applied or not (off: baking keys location on every bone, and rounding down an eighteen-bone chain is a collapsed animal)
skin_joint(sk, name), skin_joint_quiet(sk, name) a bone by name in the skin's OWN joints: a kit packs several rigs with the same bone names, and render3d's skin_find answers the file's first, which belongs to another rig (the pose is then stored and never drawn)
anim_first_key(c) -> float the earliest key; a read warns when it is not zero

Rules it keeps

  • Clips are named per rig. A channel addresses its FILE's node indices, two files number their nodes differently, and a name already held is skipped - so one shared name would hand the second body the first one's skeleton. Name them hiker_m_walk / hiker_f_walk, bear_walk.
  • Read straight after the load. gltf_doc is whichever file was parsed last, and gltf_load closes the .bin, which anim_read_doc reopens (a read through the closed handle is zeros).
  • Key from frame zero. A clip wraps on its duration and holds below its first key, so a clip keyed from frame 1 holds its opening pose for 1/30 s every cycle. anim_read_doc prints it.
  • The math. skin_pose takes a delta D in the model frame, local = (G_p^-1 D G_p) R_rest, and a channel gives the absolute local L, so D = G_p (L R_rest^-1) G_p^-1.
  • The scratch is made once and grown: a call is per actor per frame.

Tests

ludic test packages/ludic.anim

A two-bone skin made from a glTF document with no GPU: sampling, wrapping, the cross-fade, translations off by default, and clips read out of a .bin written to the temp directory.