ludic migrate state packages <every example program> packages/ludic.lab/example/plate.ludic 1804 vars into 126 states, 64 into lets; 23498 edits in 460 files Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
148 lines
5.8 KiB
Text
148 lines
5.8 KiB
Text
# anim_test.ludic - clips on a two-bone skin made from a glTF document with no GPU: sampling,
|
|
# looping, the cross-fade, translations off by default, and reading clips out of a .bin
|
|
import "ludic.render3d/r3d.ludic"
|
|
import "ludic.anim"
|
|
program AnimTest {
|
|
numbers float
|
|
const HALF: float = 0.70710678
|
|
# never called: naming Input links the engine's runtime, which render3d's textures stand on
|
|
function links_runtime() -> bool { return Input.key_pressed(0) }
|
|
|
|
# a root and one child 1 m up, no inverse bind matrices, one skin of both
|
|
function skin(render3d_st: mut Render3dState) -> Skin {
|
|
render3d_st.gltf_doc = Json.parse("{\"nodes\": [{\"name\": \"root\", \"children\": [1]}, {\"name\": \"bone\", \"translation\": [0, 1, 0]}], \"skins\": [{\"joints\": [0, 1]}]}")
|
|
return skin_load(render3d_st, 0)
|
|
}
|
|
|
|
# one channel on node 1: identity at 0, a quarter turn about Z at 1 s
|
|
function turn(name: string) -> Clip {
|
|
let c = new Clip
|
|
c.name = name
|
|
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] = 2
|
|
c.times = floats(2)
|
|
c.times[1] = 1.0
|
|
c.vals = floats(8)
|
|
c.vals[3] = 1.0
|
|
c.vals[6] = HALF
|
|
c.vals[7] = HALF
|
|
c.dur = 1.0
|
|
return c
|
|
}
|
|
|
|
# where the child's origin lands after posing: rest is (0, 1, 0), a quarter turn of the root would move it
|
|
function pose_z(sk: Skin) -> float {
|
|
let q = floats(4)
|
|
q_load(q, sk.pose_r, 1)
|
|
return q[2]
|
|
}
|
|
|
|
test "a clip is found by name, once" (anim_st: mut AnimState) {
|
|
anim_clear(anim_st)
|
|
expect(anim_add(anim_st, turn("hiker_m_walk")))
|
|
expect(not anim_add(anim_st, turn("hiker_m_walk")))
|
|
expect(anim_add(anim_st, turn("hiker_f_walk")))
|
|
expect_eq(anim_count(anim_st), 2)
|
|
expect(anim_find(anim_st, "hiker_f_walk") != null)
|
|
expect(anim_find(anim_st, "bear_walk") == null)
|
|
}
|
|
|
|
test "a key pair is found by halving" {
|
|
let c = turn("t")
|
|
expect_eq(anim_key_at(c, 0, 0.5), 0)
|
|
expect_eq(anim_key_at(c, 0, 2.0), 0)
|
|
expect_eq(anim_key_at(c, 0, -1.0), 0)
|
|
}
|
|
|
|
test "playing samples between keys, and wraps on the duration" (anim_st: mut AnimState, render3d_st: mut Render3dState) {
|
|
let sk = skin(render3d_st)
|
|
let c = turn("t")
|
|
expect(anim_play(anim_st, sk, c, 0.0, null, 0.0, 0.0))
|
|
expect_near(pose_z(sk), 0.0, 0.001)
|
|
anim_play(anim_st, sk, c, 1.0 - 0.0001, null, 0.0, 0.0)
|
|
expect_near(pose_z(sk), HALF, 0.01)
|
|
anim_play(anim_st, sk, c, 0.5, null, 0.0, 0.0)
|
|
let mid = pose_z(sk)
|
|
anim_play(anim_st, sk, c, 1.5, null, 0.0, 0.0)
|
|
expect_near(pose_z(sk), mid, 0.001)
|
|
expect(mid > 0.3 and mid < 0.45)
|
|
}
|
|
|
|
test "a cross-fade lands between the two clips" (anim_st: mut AnimState, render3d_st: mut Render3dState) {
|
|
let sk = skin(render3d_st)
|
|
let a = turn("a")
|
|
let b = turn("b")
|
|
anim_play(anim_st, sk, a, 0.0, b, 0.999, 0.5)
|
|
let z = pose_z(sk)
|
|
expect(z > 0.3 and z < 0.45)
|
|
anim_play(anim_st, sk, a, 0.0, b, 0.999, 0.0)
|
|
expect_near(pose_z(sk), 0.0, 0.001)
|
|
}
|
|
|
|
test "a bone is found in the skin's own joints, not the file's first of that name" (render3d_st: mut Render3dState) {
|
|
render3d_st.gltf_doc = Json.parse("{\"nodes\": [{\"name\": \"bone\"}, {\"name\": \"root\", \"children\": [2]}, {\"name\": \"bone\"}], \"skins\": [{\"joints\": [1, 2]}]}")
|
|
let sk = skin_load(render3d_st, 0)
|
|
expect_eq(skin_joint(sk, "bone"), 2)
|
|
expect_eq(skin_joint_quiet(sk, "wing_l"), -1)
|
|
expect_eq(skin_joint_quiet(null, "bone"), -1)
|
|
}
|
|
|
|
test "no skin or no clip plays nothing" (anim_st: mut AnimState, render3d_st: mut Render3dState) {
|
|
expect(not anim_play(anim_st, null, turn("t"), 0.0, null, 0.0, 0.0))
|
|
expect(not anim_play(anim_st, skin(render3d_st), null, 0.0, null, 0.0, 0.0))
|
|
}
|
|
|
|
test "keyed translations are ignored until asked for" (anim_st: mut AnimState, render3d_st: mut Render3dState) {
|
|
let sk = skin(render3d_st)
|
|
let c = turn("t")
|
|
c.cpath[0] = AC_POS
|
|
c.vals[0] = 0.0
|
|
c.vals[1] = 3.0
|
|
c.vals[3] = 0.0
|
|
c.vals[4] = 3.0
|
|
anim_play(anim_st, sk, c, 0.0, null, 0.0, 0.0)
|
|
expect_near(sk.pose_t[4], 0.0, 0.0001)
|
|
anim_set_positions(anim_st, true)
|
|
anim_play(anim_st, sk, c, 0.0, null, 0.0, 0.0)
|
|
expect_near(sk.pose_t[4], 2.0, 0.0001)
|
|
}
|
|
|
|
function put_f(b: []byte, at: int, x: float) -> void {
|
|
let v = float_bits(x)
|
|
b[at] = v & 255
|
|
b[at + 1] = (v >> 8) & 255
|
|
b[at + 2] = (v >> 16) & 255
|
|
b[at + 3] = (v >> 24) & 255
|
|
}
|
|
|
|
test "clips are read out of a glTF's .bin, and a late first key is measured" (anim_st: mut AnimState, render3d_st: mut Render3dState) {
|
|
anim_clear(anim_st)
|
|
let dir = Os.temp_dir() + "/ludic-anim-test"
|
|
Fs.mkdir(dir)
|
|
let b = buffer(40)
|
|
put_f(b, 0, 0.0)
|
|
put_f(b, 4, 1.0)
|
|
put_f(b, 8 + 12, 1.0)
|
|
put_f(b, 8 + 24, HALF)
|
|
put_f(b, 8 + 28, HALF)
|
|
expect(Fs.write_bytes(dir + "/t.bin", b, 40))
|
|
render3d_st.gltf_doc = Json.parse("{\"buffers\": [{\"uri\": \"t.bin\"}], \"bufferViews\": [{\"byteOffset\": 0}, {\"byteOffset\": 8}], \"accessors\": [{\"bufferView\": 0, \"count\": 2, \"type\": \"SCALAR\"}, {\"bufferView\": 1, \"count\": 2, \"type\": \"VEC4\"}], \"animations\": [{\"name\": \"bear_walk\", \"samplers\": [{\"input\": 0, \"output\": 1}], \"channels\": [{\"sampler\": 0, \"target\": {\"node\": 1, \"path\": \"rotation\"}}, {\"sampler\": 0, \"target\": {\"node\": 1, \"path\": \"scale\"}}]}]}")
|
|
expect_eq(anim_read_doc(anim_st, render3d_st, dir), 1)
|
|
expect_eq(anim_read_doc(anim_st, render3d_st, dir), 0)
|
|
let c = anim_find(anim_st, "bear_walk")
|
|
expect(c != null)
|
|
expect_eq(c.n, 1)
|
|
expect_near(c.dur, 1.0, 0.0001)
|
|
expect_near(c.vals[7], HALF, 0.0001)
|
|
expect_near(anim_first_key(c), 0.0, 0.0001)
|
|
c.times[0] = 1.0 / 30.0
|
|
expect(anim_first_key(c) > 0.03)
|
|
}
|
|
}
|