feat: add sample_animation with linear interpolation

Samples an animation at time t. Wraps t for loops. Finds the
keyframe segment containing t and linearly interpolates per-bone
angles. Returns frame with angles in degrees (intuitive for tests
and callers; internal storage uses radians).

Edge cases handled:
- t = 0 returns first-keyframe values
- t = duration returns last-keyframe values (no overshoot)
- t > duration with loop=true wraps via modulo
- Keyframes with disjoint bone-sets: each bone interpolated where
  available, snapped where only one side has it.
This commit is contained in:
Axel Meyer
2026-05-17 19:55:32 +02:00
parent e62671060e
commit c3bb096b5b

View File

@@ -178,4 +178,58 @@ function M.build_animation(anim_table, rig)
} }
end end
-- ====================================================================
-- Animation sampling
-- ====================================================================
function M.sample_animation(anim, t)
-- Wrap t for loops.
if anim.loop and t > anim.duration then
t = t % anim.duration
end
if t < 0 then t = 0 end
if t > anim.duration then t = anim.duration end
local kfs = anim.keyframes
-- Find segment [kf_i, kf_{i+1}] containing t.
local prev_kf, next_kf = kfs[1], kfs[1]
for i = 1, #kfs - 1 do
if t >= kfs[i].t and t <= kfs[i + 1].t then
prev_kf = kfs[i]
next_kf = kfs[i + 1]
break
end
end
-- If t equals last keyframe's t (or beyond and not looping), snap to last.
if t >= kfs[#kfs].t then
prev_kf = kfs[#kfs]
next_kf = kfs[#kfs]
end
-- Linear interpolation per bone present in either keyframe.
local frame = {}
local span = next_kf.t - prev_kf.t
local alpha = (span > 0) and ((t - prev_kf.t) / span) or 0
local all_bones = {}
for bid, _ in pairs(prev_kf.bones) do all_bones[bid] = true end
for bid, _ in pairs(next_kf.bones) do all_bones[bid] = true end
for bid, _ in pairs(all_bones) do
local prev_v = prev_kf.bones[bid]
local next_v = next_kf.bones[bid]
if prev_v and next_v then
frame[bid] = {
angle = math.deg(prev_v.angle * (1 - alpha) + next_v.angle * alpha),
}
elseif prev_v then
frame[bid] = { angle = math.deg(prev_v.angle) }
elseif next_v then
frame[bid] = { angle = math.deg(next_v.angle) }
end
end
return frame
end
return M return M