* feat(animation): add 2D sprite-sheet and Spine region animation Implement SpriteSheet/SpriteClip/SpriteAnim for frame animation with Quad binding, and a Spine .atlas + skeleton JSON subset (bones/slots/region attachments) that feeds the shared DrawItem2D queue via SpineAnim. Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com> * fix(animation): atlas page props, SpriteAnim getLoop, test polish Parse unindented Spine atlas page keys (size/format/...), move SpriteAnim getLoop out of line to avoid incomplete type, and harden sprite/spine tests. Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
16 KiB
动画模块
脚本入口: eve.Animation()
支持七类能力:
- Tween:标量/角度属性补间(delay、repeat、yoyo、缓动)
- 2D 帧动画:
SpriteSheet+SpriteClip+SpriteAnim(sprite sheet / 图集格子) - Spine(region 子集):
.atlas+ skeleton JSON →SpineAnim.collectDrawItems进 2D 队列 - 3D 骨骼动画播放:
AnimSkeleton+AnimClip,可用AnimPlayer、状态机AnimStateMachine、或 Motion Matching(MotionDatabase+MotionMatcher)驱动 - CPU 蒙皮:
AnimSkin从ModelData读取骨骼权重与 inverse-bind,按AnimPose世界矩阵做线性混合蒙皮 - 控制论程序动画:
ControlAnim(命名标量通道)与ControlPose(骨骼姿态跟踪),基于二阶 LTI / 闭式阻尼弹簧 / 单位质量 PD - 拖尾轨迹:
AnimTrail记录采样点并绘制淡出轨迹(2D 点或骨骼世界坐标投影)
基本用法(Tween)
local anim = eve.Animation();
local move = anim.newTween(0.6);
move.setFrom("x", 0);
move.setTo("x", 200);
move.setEase("outQuad");
move.start();
anim.update(dt);
基本用法(2D 帧动画)
local anim = eve.Animation();
local gfx = eve.Graphics();
local sheet = anim.newSpriteSheet();
sheet.setGrid(4, 2, 32, 48, 0, 0, 0, 0); // cols, rows, frameW, frameH, margin, spacing, ox, oy
local walk = anim.newSpriteClip("walk");
walk.setLoop(true);
walk.addFrame(0, 0.1);
walk.addFrame(1, 0.1);
walk.addFrame(2, 0.1);
walk.addFrame(3, 0.1);
local quad = gfx.newQuad(0, 0, 32, 48);
local player = anim.newSpriteAnim();
player.setSheet(sheet);
player.bindQuad(quad); // 每帧自动 setViewport
player.play(walk);
// eve_update:
anim.update(dt);
// 把 quad 挂到 Renderable2D.sprite.quad 即可换帧
基本用法(Spine region)
内置解析器支持 Spine .atlas + skeleton JSON 的 region 附件子集(骨骼 TRS、slot 附件切换)。Mesh / IK / path / deform 需自行接入官方 spine-cpp 插件。
local anim = eve.Animation();
local atlas = anim.newSpineAtlasFromFile("hero.atlas");
local data = anim.newSpineSkeletonDataFromFile("hero.json");
local sk = anim.newSpineSkeleton(data);
local spine = anim.newSpineAnim(sk);
spine.setAtlas(atlas);
spine.setPageTextureByName("hero.png", tex); // Graphics 纹理
spine.setPosition(400, 300);
spine.setFlipY(true); // 默认 true:Spine Y-up → 屏幕 Y-down
spine.play("idle");
// eve_update:
anim.update(dt);
// C++ / 自定义系统:spine.collectDrawItems(queue)
// 脚本可读:spine.getDrawSlotCount / getDrawSlotX/Y/Width/Height
基本用法(3D 状态机)
local anim = eve.Animation();
local sk = anim.newSkeleton();
local root = sk.addBone("root", -1);
local hip = sk.addBone("hip", root);
sk.setBindPosition(hip, 0, 1, 0);
local idle = anim.newClip("idle");
// idle.addPositionKey / addRotationKey ...
local walk = anim.newClip("walk");
local sm = anim.newStateMachine(sk);
sm.addState("Idle", idle);
sm.addState("Walk", walk);
sm.setEntry("Idle");
local t = sm.addTransition("Idle", "Walk", 0.15);
sm.addFloatCondition(t, "speed", ">", 0.5);
sm.setFloat("speed", 1.0);
sm.update(dt);
local pose = sm.getPose();
基本用法(Motion Matching)
local db = anim.newMotionDatabase(sk);
db.setRootBoneByName("mixamorig:Hips"); // Mixamo 等角色常用髋骨做轨迹根
db.addFeatureBoneByName("mixamorig:LeftFoot");
db.addFeatureBoneByName("mixamorig:RightFoot");
db.addClip(walk);
db.addClip(run);
db.bake();
local mm = anim.newMotionMatcher(sk, db);
mm.setDesiredVelocity(0, 3);
mm.setDesiredYaw(0);
mm.setSearchInterval(0.1);
mm.update(dt);
local pose = mm.getPose();
基本用法(控制论程序动画)
二阶动力学把目标当成输入 x,输出 y 满足:
[ \ddot y + k_1 \dot y + k_2 y = x + k_3 \dot x ]
其中 (k_1=\zeta/(\pi f)),(k_2=1/(2\pi f)^2),(k_3=r\zeta/(2\pi f))(t3ssel8r 参数化)。也可用闭式阻尼弹簧(Ryan Juckett)或单位质量 PD:(\tau=K_p(x-y)+K_d(\dot x-\dot y)),(K_p=\omega^2),(K_d=2\zeta\omega)。
local anim = eve.Animation();
local ca = anim.newControlAnim(3.0, 0.5, -1.0); // f, ζ, r
ca.setIntegrator("secondOrder"); // 或 "spring" / "pd"
ca.set("arm", 0);
ca.setTarget("arm", 1.2);
ca.update(dt);
local y = ca.get("arm");
local sk = anim.newSkeleton();
// ... addBone ...
local cp = anim.newControlPose(sk);
cp.setFrequency(4.0);
cp.setDamping(0.8);
cp.setResponse(1.0);
local target = anim.newPose(sk.getBoneCount());
sk.applyBindPose(target);
target.setLocalPosition(1, 0, 2, 0);
cp.setTargetPose(target);
cp.update(dt);
local pose = cp.getPose();
基本用法(拖尾轨迹)
local anim = eve.Animation();
local trail = anim.newTrail(64);
trail.setDuration(0.45);
trail.setWidth(4);
trail.setColor(1, 0.85, 0.35, 1);
trail.setFade(true);
trail.setStyle("line"); // 或 "points"
trail.setMinDistance(2);
// 每帧:写入采样 → update;渲染时 draw
trail.addPoint(x, y);
// 或从骨骼世界坐标投影(需先 pose.computeWorld(sk)):
// trail.sampleBone(pose, tipBone, "xy"); // plane: xy|xz|yz
// trail.setDrawScale(40, -40); // 世界单位 → 像素
// trail.setDrawOffset(400, 300);
trail.update(dt);
// eve_render:
trail.draw(gfx);
从 Mixamo / FBX 导入
// Assimp 路径(Model3D 解码后):
local model = model3d.newModelDataFromFile("Idle.fbx");
local sk = anim.newSkeletonFromModel(model);
local idle = anim.newClipFromModel(model, sk, 0);
// 或加载测试用紧凑 `.eva`(无网格关键帧,见 test/assets/mixamo/):
local sk2 = anim.newSkeletonFromEvaFile("test/assets/mixamo/Idle.eva");
local idle2 = anim.newClipFromEvaFile("test/assets/mixamo/Idle.eva");
// Mixamo 原地跑可补平面根运动:
run.applyPlanarRootMotion(sk.findBone("mixamorig:Hips"), 0, 300);
CPU 蒙皮(glTF / FBX 蒙皮网格)
local model = model3d.newModelDataFromFile("CesiumMan.gltf");
local sk = anim.newSkeletonFromModel(model);
local clip = anim.newClipFromModel(model, sk, 0);
local skin = anim.newSkinFromModel(model, 0, sk); // meshIndex 需 hasBones
local player = anim.newPlayer(sk);
player.play(clip);
player.update(dt);
local pose = player.getPose();
pose.computeWorld(sk);
skin.updateSkinnedPositions(pose); // 脚本可读 getSkinnedPositionX/Y/Z(i)
// 也可交给粒子:emitter.setSkinSource(skin, pose) 从皮肤表面发射
测试资源:scripts/download_skinned_character.sh 下载 Khronos CesiumMan(约 0.5 MB)到 test/assets/skinned/;CMake 选项 EVENGINE_DOWNLOAD_SKINNED_CHARACTER(默认 ON)会在构建 unit_test 时联网拉取。
对象关系与调用时机
Animation拥有 Tween / SpriteAnim / SpineAnim 注册表并统一update;3D 对象与AnimTrail由脚本持有,各自update(dt)。SpriteSheet定义图集格子;SpriteClip引用格子索引;SpriteAnim推进时间并可bindQuad。SpineAtlas+SpineSkeletonData为资源;SpineSkeleton为运行时姿态;SpineAnim采样动画并collectDrawItems。AnimSkeleton定义骨骼层级与 bind pose;AnimClip保存各骨 local TRS 关键帧。AnimPlayer/AnimStateMachine/MotionMatcher/ControlPose每帧写出AnimPose;AnimSkin用世界矩阵 + inverse-bind 做 CPU 蒙皮;渲染侧也可读取 local/world 同步调试骨骼。AnimTrail:每帧addPoint/sampleBone后update(dt),在eve_render调用draw(gfx)。- Motion Matching:先
MotionDatabase.bake(),再周期性搜索 + 交叉淡入。 ControlAnim/ControlPose:每帧更新目标后调用各自的update(dt);积分器字符串为secondOrder|spring|pd。
目标导向指南
做 UI 滑入动画
创建 Tween,给 x 设置 from/to,选择 outQuad,调用 start();每帧 anim.update(dt) 后读取 tween.get("x") 更新 UI 位置。
做 2D 角色走路帧动画
newSpriteSheet + setGrid(或 addFrame),newSpriteClip 按序 addFrame,newSpriteAnim 绑定 Quad 后 play;把该 Quad 赋给 Renderable2D.sprite.quad。
播放 Spine 角色(region)
加载 .atlas / .json,newSpineAnim,绑定 page 纹理,每帧 anim.update(dt);渲染侧调用 collectDrawItems 写入与地图/精灵同一 2D 队列。
做往返呼吸效果
设置 duration、repeat 和 setYoyo(true);颜色或缩放用多个命名属性并行插值。角度必须使用 setFromAngle / setToAngle,避免跨 360° 绕远路。
用状态机切换 Idle/Walk
addState 绑定 clip,addTransition + addFloatCondition/addTriggerCondition;每帧写参数并 sm.update(dt),从 getPose() 取姿态。
用 Motion Matching 跟手移动
把行走/奔跑等 clip 加入 MotionDatabase 并 bake;每帧设置 setDesiredVelocity / setDesiredYaw,调用 mm.update(dt)。
给武器或肢体加惯性/跟手感
用 newControlAnim 或 newControlPose,调低 ζ(欠阻尼)并设负的 r 可做预期回摆;调高 f 让跟踪更快。目标每帧变化时用 setTarget / setTargetPose,不要每帧 set(set 会清速度)。
给武器挥砍或移动目标加拖尾
newTrail(capacity),设 setDuration / setWidth / setColor;每帧在目标位置 addPoint(2D)或 sampleBone(pose, bone, "xy")(骨骼),再 update(dt);在 eve_render 调用 draw(gfx)。世界坐标可用 setDrawScale / setDrawOffset 映射到屏幕像素。
常见问题
- 创建 Tween 后忘记
start()。 - update 后不读取
get(property)写回对象。 - 普通标量接口插值角度导致跨 360° 绕行。
SpriteAnim未setSheet/bindQuad就期望自动换帧。- Spine 未
setPageTexture*导致collectDrawItems无贴图。 - 期望内置 Spine 解析 mesh/IK(当前仅 region;全量请用 spine-cpp 插件)。
- MotionMatcher 在
bake()之前调用search/update。 - 状态机/播放器持有的 skeleton、clip 被提前销毁。
ControlAnim.setIntegrator/ControlPose.setIntegrator传入未知字符串。- 把
ControlAnim.set当每帧追目标用(会清零速度,失去动力学感)。 AnimTrail.sampleBone前忘记pose.computeWorld(sk);或setStyle/ plane 字符串拼错(仅line|points与xy|xz|yz)。
API 快查
下列方法名来自当前 Squirrel 绑定;同一模块创建的辅助对象的方法也列在这里。
- Tween:
clearAll()、clearFinished()、evaluate()、get()、getActiveCount()、getDelay()、getDelta()、getDuration()、getEase()、getEasedProgress()、getElapsed()、getFrom()、getName()、getProgress()、getPropertyCount()、getPropertyName()、getRepeat()、getTo()、getTweenCount()、getYoyo()、has()、isActive()、isDelayed()、isFinished()、isPaused()、isRunning()、isStopped()、newTween()、pause()、reset()、resume()、setDelay()、setDelta()、setDeltaAngle()、setDuration()、setEase()、setFrom()、setFromAngle()、setRepeat()、setTo()、setToAngle()、setYoyo()、start()、stop()、update() - 2D 帧动画:
newSpriteSheet()、newSpriteClip()、newSpriteAnim()、getSpriteAnimCount() - Spine:
newSpineAtlas()、newSpineAtlasFromFile()、newSpineAtlasFromText()、newSpineSkeletonData()、newSpineSkeletonDataFromFile()、newSpineSkeletonDataFromJson()、newSpineSkeleton()、newSpineAnim()、getSpineAnimCount() SpriteSheet:addFrame()、setGrid()、clear()、getFrameCount()、findFrame()、getFrame*()、applyToQuad()SpriteClip:setName()、getName()、setLoop()、addFrame()、addFrameByName()、getDuration()、frameAtTime()SpriteAnim:setSheet()、play()、stop()、pause()、resume()、setSpeed()、setTime()、setLoop()、bindQuad()、applyToQuad()、getSheetFrame()、update()SpineAtlas:loadFromText()、loadFromFile()、getPage*()、findRegion()、getRegion*()SpineSkeletonData:loadFromJson()、loadFromFile()、findBone()、findSlot()、findAnimation()、getAnimationDuration()SpineSkeleton:setSkin()、setToSetupPose()、updateWorldTransform()、getBoneWorld*()、getSlotAttachmentName()SpineAnim:setAtlas()、setPageTexture()、setPageTextureByName()、play()、setPosition()、setScale()、setFlipY()、apply()、update()、getDrawSlot*()- 3D 工厂:
newSkeleton()、newClip()、newPose()、newPlayer()、newStateMachine()、newMotionDatabase()、newMotionMatcher()、newControlAnim()、newControlPose()、newSkinFromModel()、newTrail() AnimSkeleton:addBone()、getBoneCount()、getBoneName()、findBone()、getParent()、setBindPosition()、setBindRotation()、setBindScale()、getBind*()、applyBindPose()AnimClip:setName()、getName()、setDuration()、getDuration()、setLoop()、getLoop()、setSampleRate()、addPositionKey()、addRotationKey()、addScaleKey()、sample()、wrapTime()AnimPose:resize()、copyFrom()、blendFrom()、setLocal*()、getLocal*()、computeWorld()、getWorld*()、getWorldMatrixElement()AnimSkin:getVertexCount()、getBoneCount()、getSkeletonBone()、getSkinBoneName()、getInverseBindElement()、getBindPosition*()、getVertexBone()、getVertexWeight()、updateSkinnedPositions()、hasSkinnedPositions()、getSkinnedPosition*()AnimPlayer:play()、crossFade()、stop()、pause()、resume()、setSpeed()、setTime()、setLoop()、getPose()、update()AnimStateMachine:addState()、setEntry()、addTransition()、addFloatCondition()、addBoolCondition()、addTriggerCondition()、setExitTime()、setFloat()、setBool()、setTrigger()、getPose()、update()MotionDatabase:addFeatureBone()、addFeatureBoneByName()、addClip()、bake()、getFrameCount()、getFeatureSize()MotionMatcher:setDesiredVelocity()、setDesiredYaw()、setSearchInterval()、setBlendTime()、search()、update()、getPose()、getMatchedClipIndex()ControlAnim:setFrequency()、getFrequency()、setDamping()、getDamping()、setResponse()、getResponse()、setIntegrator()、getIntegrator()、set()、setTarget()、setTargetVelocity()、impulse()、has()、get()、getVelocity()、getTarget()、clear()、remove()、getPropertyCount()、getPropertyName()、update()ControlPose:setFrequency()、getFrequency()、setDamping()、getDamping()、setResponse()、getResponse()、setIntegrator()、getIntegrator()、setBoneWeight()、getBoneWeight()、setTargetPose()、snapToTarget()、getPose()、getTargetPose()、update()AnimTrail:setCapacity()、getCapacity()、setDuration()、getDuration()、setMinDistance()、getMinDistance()、setWidth()、getWidth()、setColor()、getColor*()、setFade()、getFade()、setStyle()、getStyle()、setDrawScale()、getDrawScale*()、setDrawOffset()、getDrawOffset*()、addPoint()、addPoint3()、sampleBone()、sampleBoneOffset()、clear()、update()、getPointCount()、getPoint*()、getPointAge()、getPointAlpha()、draw()
使用要点
- 模块对象和它创建的资源对象应保存在全局或实体状态中,不要在每帧重复创建。
- 带
update(dt)的系统应在eve_update调用;绘制方法应在eve_render调用。 - 参数约束、默认值和返回类型以对应模块头文件及
addFunc绑定为准;本文 API 快查与当前源码同步生成。
源码: src/modules/animation/
相关测试: 在 test/ 中搜索 animation。