载入中...
搜索中...
未找到
动画模块

动画模块

脚本入口: eve.Animation()

支持九类能力:

  1. **Tween**:标量/角度属性补间(delay、repeat、yoyo、缓动)——兼容保留
  2. **Motion(LitMotion 风格)**:typed float/Vec2/Vec3 补间,Builder + Push Bind + Handle
  3. **2D 帧动画**:SpriteSheet + SpriteClip + SpriteAnim(sprite sheet / 图集格子)
  4. **Spine(region 子集)**:.atlas + skeleton JSON → SpineAnim.collectDrawItems 进 2D 队列
  5. **3D 骨骼动画播放与动画图**:AnimSkeleton + AnimClip,可用 AnimPlayer、AnimGraph、状态机 AnimStateMachine、或 Motion Matching(MotionDatabase + MotionMatcher)驱动
  6. **CPU 蒙皮**:AnimSkin 从 ModelData 读取骨骼权重与 inverse-bind,按 AnimPose 世界矩阵做线性混合蒙皮
  7. **控制论程序动画**:ControlAnim(命名标量通道)与 ControlPose(骨骼姿态跟踪),基于二阶 LTI / 闭式阻尼弹簧 / 单位质量 PD
  8. **拖尾轨迹**:AnimTrail 记录采样点并绘制淡出轨迹(2D 点或骨骼世界坐标投影)
  9. **程序化骨骼**:DynamicBoneSolver 提供弹簧骨、碰撞、风场和距离休眠;FootIKSolver 提供地面探测、脚掌对齐、锁足和骨盆补偿
  10. **受击晃动与平衡恢复**:PhysicalBalancePose 把世界冲量叠到姿态上,倒立摆 + PD 让角色主动回正(物理模块只注入冲量,不反向依赖)

Motion(LitMotion 风格 Push 补间)

C++ 入口(Phase 1):Animation::motion / motionVec2 / motionVec3 返回 Builder; bind(sink) 时入库播放。Sink 由调用方提供(FloatPointerSink / 自定义 IMotionFloatSink),跨模块写回不要让 animation 直接 include 上层。

float x = 0.f;
FloatPointerSink sink(&x);
auto handle = anim->motion(0.f, 200.f, 0.6f)
.ease("outQuad")
.delay(0.1f)
.loops(2, MotionLoopMode::Yoyo)
.bind(sink)
.expect("spawn");
anim->advance(step); // 与 Tween 共用 SimulationStep 泵
anim->motions().complete(handle); // 跳到终点并触发 onComplete
anim->motions().cancel(handle); // 取消并触发 onCancel
float x
Definition AnimClip.cpp:738
PrimitiveHandle handle
float step
Definition TreeMesh.cpp:314

无 sink 时可 run(),再用 motions().floatValue(handle) 拉取当前值。

Sequence(Phase 2)

auto seq = anim->sequence();
seq.append(std::move(anim->motion(0.f, 1.f, 0.3f).ease("outQuad").to(sinkX))).expect("append");
seq.appendInterval(0.1f).expect("gap");
seq.join(std::move(anim->motion(0.f, 1.f, 0.3f).ease("linear").to(sinkY))).expect("join");
seq.insert(0.05f, std::move(anim->motion(1.f, 0.f, 0.2f).to(sinkZ))).expect("insert");
auto playback = seq.run().expect("run");
anim->advance(step);
playback.complete().expect("complete");

脚本:

local b = anim.newMotion(0, 100, 0.5);
b.ease("outQuad");
b.loops(2, "yoyo");
b.cancelOnError(true);
local h = b.run();
print(h.value());
h.complete();
print(anim.getMotionCount());
local seq = anim.newMotionSequence();
seq.append(anim.newMotion(0, 1, 0.3));
seq.appendInterval(0.1);
seq.join(anim.newMotion(0, 1, 0.3));
print(seq.cursor());
print(seq.itemCount());
local sh = seq.run();
print(sh.childCount());

Ease 扩展:in/out/inOut + Back / Elastic / Bounce。

Punch / Shake(有限时长阻尼正弦;to/strength 为振幅)与 Color/Quat:

anim->punch(0.f, 12.f, 0.4f).frequency(18).dampingRatio(0.f).bind(sinkX);
anim->shake(0.f, 0.2f, 0.5f).frequency(20).seed(7).bind(sinkX);
anim->motionColor(MotionColor{0,0,0,1}, MotionColor{1,0,0,1}, 0.3f).bind(colorSink);
anim->motionQuat(MotionQuat{0,0,0,1}, MotionQuat{0,1,0,0}, 0.3f).bind(quatSink);

脚本:

local p = anim.newMotionPunch(0, 12, 0.4);
p.frequency(18);
p.dampingRatio(0.0);
local h = p.run();
local s = anim.newMotionShake(0, 0.2, 0.5);
s.frequency(20);
s.seed(7);
s.run();

基本用法(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);

程序化骨骼与 Foot IK

Dynamic Bone 在基础动画采样后修改 Pose,适合头发、裙摆、尾巴和挂件。链、碰撞体及 Skeleton 均为 solver 内部状态;传入的 Skeleton 和地面查询对象为 borrowed,必须比 solver 活得更久。update 使用固定子步并限制单帧最大步数,非有限时间步会被忽略。

local dynamic = anim.newDynamicBoneSolver(skeleton);
// 发卡/马尾推荐:setupHairChain 带上 tip 延伸与自碰撞默认值
local hair = anim.setupHairChain(dynamic, "hair_root", "hair_tip");
anim.setupHairHeadCollider(dynamic, "head", 0.12);
dynamic.setGlobalGravity(0, -9.81, 0);
dynamic.setExternalForce(windX, windY, windZ);
dynamic.update(pose, dt);
local feet = anim.newFootIKSolver(skeleton);
feet.setPelvisBone(skeleton.findBone("hips"));
feet.configureLeftLeg(upperLeft, lowerLeft, footLeft, 0.03);
feet.configureRightLeg(upperRight, lowerRight, footRight, 0.03);
feet.configureLeftToe(toeLeft, 0.5);
feet.configureRightToe(toeRight, 0.5);
feet.setFootLockEnabled(true);
feet.setGroundQuery(groundQuery);
feet.apply(pose, dt);

setGroundQuery 接收实现地面探测契约的运行时对象;没有 provider 或 provider 返回 Unavailable 时,solver 保留脚本通过 setLeftContact / setRightContact 提供的手动 接触点。调用顺序应为动画采样、Foot IK、Dynamic Bone,最后计算世界 Pose 与蒙皮。

基本用法(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 即可换帧

独立 PNG 序列可在运行时自动合并为共享图集,不需要先用外部工具合图; 每帧会自动扩展 1 像素边缘,避免线性过滤、缩放和旋转时采样到相邻帧:

local sheet = anim.newSpriteSheetFromSequence(
gfx, "assets/frame_{n}.png", 1, 64, 8);
local burst = anim.newSpriteClip("burst");
burst.addRange(0, 63, 24.0); // inclusive range, 24 FPS
local quad = gfx.newQuad(0, 0, 128, 128);
local player = anim.newSpriteAnim();
player.setSheet(sheet);
player.bindQuad(quad);
player.play(burst); // player.playReverse(burst) 可倒放
player.setSpeed(0.5); // 支持负数;0 冻结时间

consumeLooped() / consumeCompleted() 用于每帧消费一次性事件, getLoopCount() 返回本次播放以来累计跨过的循环边界数。

播放速度还可以由关键点曲线控制。曲线值与 setSpeed() 的基础倍率相乘, 曲线时间独立推进,因此首个关键点为 0 也不会把播放永久卡住:

player.setSpeed(1.0);
player.addSpeedCurveKey(0.0, 0.2);
player.addSpeedCurveKey(1.2, 2.4);
player.addSpeedCurveKey(2.6, 0.35);
player.addSpeedCurveKey(4.0, 0.2);
player.setSpeedCurveLoop(true);
// clearSpeedCurve / resetSpeedCurve / getSpeedCurveValue

关键点之间采用线性插值;如需平滑 S 曲线,可用更多采样关键点逼近。 也可调用 setSpeedCurveInterpolation("linear"|"smooth"|"cubic") 选择插值。

序列加载器会按 alpha 自动裁掉透明边缘,保留每帧原始尺寸和偏移; player.bindSprite(sprite) 会同步这些布局信息,避免裁边动画抖动。相同加载参数会复用缓存图集, 可用 getSpriteSequenceCacheCount/Bytes 查看数量与估算显存。

Aseprite 与 TexturePacker 的 JSON Hash 格式可通过 newSpriteSheetFromAtlasJson(gfx, texturePath, jsonPath) 导入;当前明确拒绝 rotated frame。

基本用法(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();

可组合 3D Animation Graph

AnimGraph 用稳定整数句柄连接节点,支持共享子图单帧缓存、普通混合、 additive、逐骨骼 mask 分层、one-shot,以及 1D/2D blend space。现有 AnimPlayer 和 AnimStateMachine 保持兼容,适合简单控制器;复杂角色建议使用图。

local graph = anim.newGraph(sk);
local idleNode = graph.addClip(idle);
local walkNode = graph.addClip(walk);
local runNode = graph.addClip(run);
local locomotion = graph.addBlendSpace1D();
graph.addBlendSpace1DPoint(locomotion, 0.0, idleNode);
graph.addBlendSpace1DPoint(locomotion, 2.0, walkNode);
graph.addBlendSpace1DPoint(locomotion, 6.0, runNode);
graph.setPosition1D(locomotion, speed);
local fireNode = graph.addClip(fire);
local fireLayer = graph.addOneShot(locomotion, fireNode, 0.08, 0.12);
graph.clearBoneMask(fireLayer);
graph.setBoneMask(fireLayer, sk.findBone("Spine"), 1.0, true);
graph.setRoot(fireLayer);
graph.trigger(fireLayer);
// eve_update:
graph.update(dt);
local pose = graph.getPose();

addLayer(base, overlay, weight) 默认 mask 全为 0,须显式设置参与骨骼; addAdditive(base, delta, weight) 默认作用于全身;默认参考是 identity(样本本身就是局部空间 delta),可用 setAdditiveReference(node, "bind"|"identity") 改为相对 bind pose,并用 getAdditiveReference(node) 读取当前参考。Additive clip 应以 identity 姿态为参考:位移为差值、旋转为差值四元数、缩放以 1 为基准。 one-shot 的 shot 输入当前应是 clip 节点,用该 clip 的时长决定结束和淡出。

基本用法(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();

原地动画必须在 bake() 前用 clip.applyPlanarRootMotion(rootBone, vx, vz) 补入标定的移动速度;恒定的单关键帧 root 轨道也会补齐终点。数据库对循环的平面 位移累计整周期行程,避免把回到起点误当作反向速度。setDesiredVelocity 输入 世界 XZ 速度,setDesiredYaw 为绕 Y 轴的弧度,正向是 (sin(yaw), cos(yaw))。

匹配器以当前播放时刻为连续候选。setIgnoreRadius 保留当前时刻邻域内的连续播放,也覆盖循环接缝。 setPlayRateRange(minimum, maximum) 返回 {ok,message},配置 Motion Matching 播放速率的闭区间。可变特征布局会按 UE Pose Search 的规则,累加查询与选中姿势中 所有未归一化轨迹速度通道的长度,以二者比值作为播放速率并夹到该区间;搜索节流仍按 模拟时间推进。getPlayRateMinimum() / getPlayRateMaximum() 返回当前速率区间, getPlayRate() 返回当前实际速率,默认区间为 [1,1]。参数必须满足 有限的 0 < minimum <= maximum <= 10;失败时保留原区间和当前速率。

setPoseReselectHistory(seconds) 复制 UE PoseSearch 的短期姿势历史:正时间步会记录 当前最接近的烘焙姿势,在指定时间内禁止把它重新选为跳转目标,但不会阻止当前姿势继续播放。 getPoseReselectHistory() 返回当前期限。零会清空并关闭历史;脚本返回的 {ok,message} 必须检查。候选只需严格优于继续播放成本即可切换, 不会再额外施加非源自 UE 的百分比改善门槛。 若物理由角色控制器负责,先复制 getPose(),再从渲染副本移除平面 root 位移; 不要修改匹配器持有的姿态。参考 examples/climbing-motion-matching。

Orientation warping

eve.OrientationWarping() owns interpolation state and borrows a skeleton after configure(skeleton, rootBone, spineBones, ikBones), which returns {ok,message}. getSkeleton() / getRootBone() / getSpineBoneCount() / getIkBoneCount() read that configuration. apply(pose, locomotionX, locomotionZ, animatedX, animatedZ, dt) yaws the copied display pose so authored planar velocity aligns with locomotion velocity. Root receives (1 - getDistributedAlpha()) * angle; remaining yaw is split across the spine list. Optional IK bones restore their pre-warp world orientation. Speeds use metres/second. Magnitudes above getAngleThreshold() (default 135°) or below getMinRootMotionSpeed() (0.1 m/s) target zero. setRotationInterpSpeed / getRotationInterpSpeed match Unreal FInterpTo; zero snaps. setEnabled / isEnabled gate pose mutation: disabled apply calls still update the target/smoothed angle but leave the pose unchanged.

Call this on a copy of MotionMatcher.getPose(), never on the matcher pose itself: the next search must keep reading unwarped matching features. Native OrientationWarping uses checked Result and does not retain the pose. See examples/climbing-motion-matching.

基本用法(控制论程序动画)

二阶动力学把目标当成输入 x,输出 y 满足:

[ \ddot y + k_1