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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ab5584f421
commit
c446c07a4c
9 changed files with 530 additions and 0 deletions
69
packages/ludic.anim/mix.ludic
Normal file
69
packages/ludic.anim/mix.ludic
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# mix.ludic - sample one clip at a time into per-node buffers, mixed in at a weight. `rot` is 4 per
|
||||
# node, `pos` 3 per node, and `hit` says which nodes have received a value (bit 0 a rotation, bit 1
|
||||
# a translation), so a node the clips never mention stays at rest
|
||||
var q_a: floats = null
|
||||
var q_b: floats = null
|
||||
var q_cur: floats = null
|
||||
|
||||
export function anim_mix(c: Clip, sk: Skin, t: float, w: float, rot: floats, pos: floats, hit: words) -> void {
|
||||
if c == null or sk == null { return }
|
||||
if q_a == null {
|
||||
q_a = floats(4)
|
||||
q_b = floats(4)
|
||||
q_cur = floats(4)
|
||||
}
|
||||
var tt = t
|
||||
if c.dur > 0.0 { tt = t % c.dur }
|
||||
for ch in 0 .. c.n {
|
||||
let node = c.cnode[ch]
|
||||
if node < 0 or node >= sk.n_nodes { continue }
|
||||
let n = c.ckeys[ch]
|
||||
let o = c.coff[ch]
|
||||
let i0 = anim_key_at(c, ch, tt)
|
||||
var i1 = i0 + 1
|
||||
if i1 > n - 1 { i1 = n - 1 }
|
||||
var f = 0.0
|
||||
if i1 > i0 {
|
||||
let span = c.times[o + i1] - c.times[o + i0]
|
||||
if span > 0.000001 { f = Math.clamp((tt - c.times[o + i0]) / span, 0.0, 1.0) }
|
||||
}
|
||||
if c.cpath[ch] == AC_ROT { mix_rot(c, ch, node, i0, i1, f, w, rot, hit) } else { mix_pos(c, ch, node, i0, i1, f, w, pos, hit) }
|
||||
}
|
||||
}
|
||||
|
||||
# Across clips the first contributor lands and the rest are nlerped onto it by their share, which
|
||||
# is what makes a cross-fade rather than a jump. Test the ROTATION bit, not the whole word: glTF
|
||||
# writes a node's translation before its rotation, and `hit == 0` then skipped every rotation.
|
||||
function mix_rot(c: Clip, ch: int, node: int, i0: int, i1: int, f: float, w: float, rot: floats, hit: words) -> void {
|
||||
let vo = c.cvoff[ch]
|
||||
for e in 0 .. 4 {
|
||||
q_a[e] = c.vals[vo + i0 * 4 + e]
|
||||
q_b[e] = c.vals[vo + i1 * 4 + e]
|
||||
}
|
||||
q_nlerp(q_a, q_a, q_b, f)
|
||||
if (hit[node] & 1) == 0 {
|
||||
q_store(rot, node, q_a)
|
||||
hit[node] = hit[node] | 1
|
||||
} else {
|
||||
q_load(q_cur, rot, node)
|
||||
q_nlerp(q_cur, q_cur, q_a, w)
|
||||
q_store(rot, node, q_cur)
|
||||
}
|
||||
}
|
||||
|
||||
function mix_pos(c: Clip, ch: int, node: int, i0: int, i1: int, f: float, w: float, pos: floats, hit: words) -> void {
|
||||
let vo = c.cvoff[ch]
|
||||
let ax = Math.lerp(c.vals[vo + i0 * 3], c.vals[vo + i1 * 3], f)
|
||||
let ay = Math.lerp(c.vals[vo + i0 * 3 + 1], c.vals[vo + i1 * 3 + 1], f)
|
||||
let az = Math.lerp(c.vals[vo + i0 * 3 + 2], c.vals[vo + i1 * 3 + 2], f)
|
||||
if (hit[node] & 2) == 0 {
|
||||
pos[node * 3] = ax
|
||||
pos[node * 3 + 1] = ay
|
||||
pos[node * 3 + 2] = az
|
||||
hit[node] = hit[node] | 2
|
||||
} else {
|
||||
pos[node * 3] = Math.lerp(pos[node * 3], ax, w)
|
||||
pos[node * 3 + 1] = Math.lerp(pos[node * 3 + 1], ay, w)
|
||||
pos[node * 3 + 2] = Math.lerp(pos[node * 3 + 2], az, w)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue