# 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) } }