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
|
-- Skeletal animation: skeleton + bones + tracks + rest + keyframe
|
||||||
-- + procedural + look-at constraint. Multi-channel animation (rot/pos/scl)
|
-- + procedural + look-at constraint. Multi-channel animation (rot/pos/scl)
|
||||||
-- + cascaded world-transform with scale propagation.
|
-- + cascaded world-transform with scale propagation.
|
||||||
@@ -420,6 +420,7 @@ function M.spawn(rig, pos)
|
|||||||
bone_animated = {},
|
bone_animated = {},
|
||||||
last_move_time = 0,
|
last_move_time = 0,
|
||||||
look_target = nil,
|
look_target = nil,
|
||||||
|
look_at_state = {}, -- {[bone_id] = { angle = X }} persistent rate-limit state
|
||||||
animations = {}, -- {[anim_id] = built_anim}
|
animations = {}, -- {[anim_id] = built_anim}
|
||||||
playing = {}, -- {[anim_id] = { t = 0, loop, speed }}
|
playing = {}, -- {[anim_id] = { t = 0, loop, speed }}
|
||||||
procedural = {}, -- {[name] = callback_fn}
|
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).
|
-- 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
|
if b.look_at and p.look_target ~= nil then
|
||||||
|
-- Compute bone-world-pos (scale-aware).
|
||||||
local px, py, pa, psx, psy
|
local px, py, pa, psx, psy
|
||||||
if b.parent then
|
if b.parent then
|
||||||
local pw = p.bone_state[b.parent.id].world
|
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 bone_world_y = py + (sin_a * lx * psx + cos_a * ly * psy)
|
||||||
local dx = p.look_target.x - bone_world_x
|
local dx = p.look_target.x - bone_world_x
|
||||||
local dy = p.look_target.y - bone_world_y
|
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.
|
-- Sprite-top convention: rot=0 means sprite top points up (-Y direction).
|
||||||
-- atan(dx, -dy) means: rot=0 when target is straight up (dy<0), rot=pi/2 when target is to the right.
|
-- 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)
|
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 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
|
if b.look_at_speed then
|
||||||
-- Rate-limited rotation: allow up to look_at_speed * dt per frame.
|
-- Rate-limit using persistent state (not current frame's bone_state.rot).
|
||||||
local delta = target_local - current
|
local delta = target_local - state.angle
|
||||||
-- Normalize delta to [-pi, pi] for shortest-path rotation.
|
|
||||||
while delta > math.pi do delta = delta - 2 * math.pi end
|
while delta > math.pi do delta = delta - 2 * math.pi end
|
||||||
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
|
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
|
||||||
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
|
else
|
||||||
next_value = target_local
|
-- Instant snap (no speed limit).
|
||||||
|
state.angle = target_local
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Apply rot_min/rot_max clamp (local-frame radians).
|
-- 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_min and state.angle < b.rest_rot_min then state.angle = 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_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
|
end
|
||||||
|
|
||||||
-- 3.c Run procedural callbacks at root-visit (once per puppet, before world-transform compute).
|
-- 3.c Run procedural callbacks at root-visit (once per puppet, before world-transform compute).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"id": "lib-core.puppet",
|
"id": "lib-core.puppet",
|
||||||
"version": "0.3.1",
|
"version": "0.3.2",
|
||||||
"api_min": "0.1",
|
"api_min": "0.1",
|
||||||
"deps": []
|
"deps": []
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user