ludic/packages/ludic.anim/tests/ozz_test.ludic
Orkuncakilkaya 9a56ee9b9d feat(ludic.anim): ozz-animation 0.17.0 underneath - a skin's skeleton and each clip built at load, sampled to 1e-4 of anim_mix (phase 19.1)
No bake and no new file: the skeleton comes from the skin's parents and rest pose (its own joints
and their ancestors - a kit holds several rigs), a clip from anim_read_doc's channels. Built by
native/build.sh from the pinned release; the Windows DLL imports KERNEL32 alone and passes there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 12:44:49 +03:00

82 lines
2.8 KiB
Text

# ozz_test.ludic - a skin and a clip built in ozz at load (phase 19.1): sampled, every rotation agrees
# with today's sampler (anim_mix) to 1e-4 a component, a joint no channel moves holds its rest, and
# a node outside the skin is no joint
import "ludic.render3d/r3d.ludic"
import "ludic.anim"
program OzzTest {
numbers float
const HALF: float = 0.70710678
function links_runtime() -> bool { return Input.key_pressed(0) }
function skin(render3d_st: mut Render3dState) -> Skin {
render3d_st.gltf_doc = Json.parse("{\"nodes\": [{\"name\": \"root\", \"children\": [1]}, {\"name\": \"bone\", \"translation\": [0, 1, 0], \"rotation\": [0, 0.2588190, 0, 0.9659258]}, {\"name\": \"other\"}], \"skins\": [{\"joints\": [0, 1]}]}")
return skin_load(render3d_st, 0)
}
# node 1 turns a quarter about Z over a second, in three keys, the middle one off the straight path
function turn() -> Clip {
let c = new Clip
c.name = "t"
c.n = 1
c.cnode = words(1)
c.cpath = words(1)
c.ckeys = words(1)
c.coff = words(1)
c.cvoff = words(1)
c.cnode[0] = 1
c.cpath[0] = AC_ROT
c.ckeys[0] = 3
c.times = floats(3)
c.times[1] = 0.4
c.times[2] = 1.0
c.vals = floats(12)
c.vals[3] = 1.0
c.vals[4] = 0.3
c.vals[6] = 0.3
c.vals[7] = 0.9055385
c.vals[10] = HALF
c.vals[11] = HALF
c.dur = 1.0
return c
}
# the largest component difference of two rotations, one turned to the other's sign first
function apart(a: floats, b: floats, i: int) -> float {
var d = 0.0
for k in 0 .. 4 { d += a[i * 4 + k] * b[i * 4 + k] }
var sgn = 1.0
if d < 0.0 { sgn = -1.0 }
var worst = 0.0
for k in 0 .. 4 { worst = Math.max(worst, Math.abs(a[i * 4 + k] - sgn * b[i * 4 + k])) }
return worst
}
test "sampled in ozz, a clip lands where anim_mix puts it, and a joint left alone holds its rest" (anim_st: mut AnimState, render3d_st: mut Render3dState) {
let sk = skin(render3d_st)
let c = turn()
let s = anim_oz_skel(sk)
expect(s != null)
expect_eq(anim_oz_joints(s), 2)
let clip = anim_oz_clip(anim_st, s, c)
expect(clip != null)
let ctx = anim_oz_ctx(s)
let rot = floats(12)
let pos = floats(9)
let want = floats(12)
let wpos = floats(9)
let hit = words(3)
var worst = 0.0
for k in 0 .. 61 {
let t = float(k) * 0.0371
expect(anim_oz_sample(ctx, clip, t, rot, pos, 3))
for i in 0 .. 3 { hit[i] = 0 }
anim_mix(anim_st, c, sk, t, 1.0, want, wpos, hit)
worst = Math.max(worst, apart(rot, want, 1))
}
print(`ozz: node 1 over 61 samples, worst component {worst}; the clip is {anim_oz_clip_bytes(clip)} bytes`)
expect(worst < 0.0001)
expect_near(rot[3], 1.0, 0.0001)
expect_near(pos[4], 1.0, 0.001)
anim_oz_ctx_free(ctx)
anim_oz_clip_free(clip)
anim_oz_skel_free(s)
}
}