fix(puppet): v0.3.2 — persistent look_at_state across frames
Previously the look-at-rate-limit read p.bone_state[bid].rot as 'current' and advanced from it. But bone_state.rot is reset-to-rest + keyframe- sampled at frame start, so 'current' was always near rest (~0) and the rate-limit only advanced one frame's worth (~7deg) per frame. Visual result: head/body rotation appeared capped at ~10deg. Fix: persistent p.look_at_state[bid] table carries angle across frames. Rate-limit operates on look_at_state.angle (not bone_state.rot). After rate-limit + clamp, value is written to bone_state.rot. Init: look_at_state[bid].angle = b.rest.rot on first encounter, so the first frame advances from rest pose toward target (matches substrate LookAtState init convention).
This commit is contained in:
37
init.lua
37
init.lua
@@ -1,4 +1,4 @@
|
||||
-- lib-core.puppet v0.3.1
|
||||
-- lib-core.puppet v0.3.2
|
||||
-- Skeletal animation: skeleton + bones + tracks + rest + keyframe
|
||||
-- + procedural + look-at constraint. Multi-channel animation (rot/pos/scl)
|
||||
-- + cascaded world-transform with scale propagation.
|
||||
@@ -420,6 +420,7 @@ function M.spawn(rig, pos)
|
||||
bone_animated = {},
|
||||
last_move_time = 0,
|
||||
look_target = nil,
|
||||
look_at_state = {}, -- {[bone_id] = { angle = X }} persistent rate-limit state
|
||||
animations = {}, -- {[anim_id] = built_anim}
|
||||
playing = {}, -- {[anim_id] = { t = 0, loop, speed }}
|
||||
procedural = {}, -- {[name] = callback_fn}
|
||||
@@ -894,6 +895,7 @@ update_bone_recursive = function(p, b, dt)
|
||||
|
||||
-- 3.b Apply look-at constraint if this bone is marked look_at (scale-aware).
|
||||
if b.look_at and p.look_target ~= nil then
|
||||
-- Compute bone-world-pos (scale-aware).
|
||||
local px, py, pa, psx, psy
|
||||
if b.parent then
|
||||
local pw = p.bone_state[b.parent.id].world
|
||||
@@ -907,34 +909,39 @@ update_bone_recursive = function(p, b, dt)
|
||||
local bone_world_y = py + (sin_a * lx * psx + cos_a * ly * psy)
|
||||
local dx = p.look_target.x - bone_world_x
|
||||
local dy = p.look_target.y - bone_world_y
|
||||
-- A.1: Substrate convention (puppet.c:748): sprite top extends in -Y direction at rot=0.
|
||||
-- atan(dx, -dy) means: rot=0 when target is straight up (dy<0), rot=pi/2 when target is to the right.
|
||||
-- Sprite-top convention: rot=0 means sprite top points up (-Y direction).
|
||||
-- atan(dx, -dy): rot=0 when target is straight up, rot=pi/2 when target is to the right.
|
||||
local world_angle = math.atan(dx, -dy)
|
||||
|
||||
-- A.4: Apply look_at_speed (rate-limited tracking) + rot_min/rot_max clamp (local frame).
|
||||
local target_local = world_angle - pa
|
||||
local current = p.bone_state[bid].rot
|
||||
local next_value
|
||||
|
||||
-- Persistent state for rate-limit (carries angle across frames).
|
||||
local state = p.look_at_state[bid]
|
||||
if state == nil then
|
||||
-- Initialize to bone's rest rotation so first frame advances from rest toward target.
|
||||
state = { angle = b.rest.rot or 0 }
|
||||
p.look_at_state[bid] = state
|
||||
end
|
||||
|
||||
if b.look_at_speed then
|
||||
-- Rate-limited rotation: allow up to look_at_speed * dt per frame.
|
||||
local delta = target_local - current
|
||||
-- Normalize delta to [-pi, pi] for shortest-path rotation.
|
||||
-- Rate-limit using persistent state (not current frame's bone_state.rot).
|
||||
local delta = target_local - state.angle
|
||||
while delta > math.pi do delta = delta - 2 * math.pi end
|
||||
while delta < -math.pi do delta = delta + 2 * math.pi end
|
||||
local max_step = b.look_at_speed * dt
|
||||
if delta > max_step then delta = max_step end
|
||||
if delta < -max_step then delta = -max_step end
|
||||
next_value = current + delta
|
||||
state.angle = state.angle + delta
|
||||
else
|
||||
next_value = target_local
|
||||
-- Instant snap (no speed limit).
|
||||
state.angle = target_local
|
||||
end
|
||||
|
||||
-- Apply rot_min/rot_max clamp (local-frame radians).
|
||||
if b.rest_rot_min and next_value < b.rest_rot_min then next_value = b.rest_rot_min end
|
||||
if b.rest_rot_max and next_value > b.rest_rot_max then next_value = b.rest_rot_max end
|
||||
if b.rest_rot_min and state.angle < b.rest_rot_min then state.angle = b.rest_rot_min end
|
||||
if b.rest_rot_max and state.angle > b.rest_rot_max then state.angle = b.rest_rot_max end
|
||||
|
||||
p.bone_state[bid].rot = next_value
|
||||
-- Write into bone_state (overrides keyframe-sampled rot).
|
||||
p.bone_state[bid].rot = state.angle
|
||||
end
|
||||
|
||||
-- 3.c Run procedural callbacks at root-visit (once per puppet, before world-transform compute).
|
||||
|
||||
Reference in New Issue
Block a user