Kinematics

Forward Kinematic(FK)

正向运动学 (FK) 是一种根据运动链的关节角度计算 末端执行器(例如手)位置和方向的方法。它从根节点(Root)开始,沿着骨骼层级向下遍历,累积应用每个关节的变换。

数学基础

FK 的核心是矩阵变换。每个关节(Bone/Joint)都有一个相对于其父关节的局部变换矩阵(Local Transformation Matrix)。

1. 局部变换 (Local Transformation)

对于第 $i$ 个关节,其局部变换矩阵 $M_{local, i}$ 通常由平移(Translation)、旋转(Rotation)和缩放(Scale)组成:

$$ M_{local, i} = T_i \cdot R_i \cdot S_i $$

其中:

  • $R_i$:由关节角度(例如欧拉角或四元数)决定的旋转矩阵。
  • $T_i$:由骨骼长度或偏移量决定的平移矩阵。

2. 全局变换 (Global Transformation)

为了得到关节在世界坐标系(或模型空间)中的位置和方向,需要将其局部变换乘以父关节的全局变换矩阵。

$$ M_{global, i} = M_{global, i-1} \cdot M_{local, i} $$

其中 $M_{global, i-1}$ 是父关节的全局变换矩阵。

对于根节点(Root),$M_{global, root} = M_{world} \cdot M_{local, root}$。

3. 顶点蒙皮 (Vertex Skinning)

最终计算出的 $M_{global, i}$ 用于驱动网格顶点:

$$ V_{world} = \sum w_i \cdot M_{global, i} \cdot V_{rest} $$

其中 $w_i$ 是顶点受该骨骼影响的权重,$V_{rest}$是rest pose(bind pose)下的顶点位置。

Inverse Kinematic(IK)

逆向运动学 (IK) 是 FK 的逆过程。它计算将 末端执行器(End Effector)放置在特定目标位置所需的关节角度。

CCD (Cyclic Coordinate Descent)

CCD (循环坐标下降) 是一种迭代算法,通过一次优化一个关节来解决 IK 问题。它从末端执行器(End Effector)的父关节开始,逐个遍历到根关节,调整每个关节的角度以尽可能减小末端执行器与目标位置的距离。

原理

对于每个关节 $J_i$,我们定义两个向量:

  1. 当前向量 (Current Vector, $V_c$):从关节 $J_i$ 指向当前末端执行器位置的向量。
  2. 目标向量 (Target Vector, $V_t$):从关节 $J_i$ 指向目标位置的向量。

目标是旋转关节 $J_i$,使得 $V_c$ 与 $V_t$ 对齐。

  • 旋转轴 (Rotation Axis):$axis = V_c \times V_t$ (归一化)。
  • 旋转角度 (Rotation Angle):$\theta = \arccos(\frac{V_c \cdot V_t}{|V_c| |V_t|})$。

算法流程

  1. 检查末端执行器是否已到达目标(在误差范围内)。
  2. 如果未到达,从倒数第二个关节(末端执行器的父节点)开始,向根节点遍历。
  3. 对于每个关节,计算将末端执行器指向目标所需的旋转,并应用该旋转。
  4. 如果需要,应用关节限制(Constraints)。
  5. 重复上述步骤,直到达到最大迭代次数或满足误差要求。

伪代码

def SolveIK_CCD(chain, target_pos, max_iterations=10, tolerance=0.01):
    # chain: 关节列表,chain[-1] 是末端执行器 (End Effector)
    # target_pos: 目标位置
    
    for iter in range(max_iterations):
        # 检查末端执行器是否已到达目标
        current_end_pos = chain[-1].global_position
        if distance(current_end_pos, target_pos) < tolerance:
            return True # 已收敛
            
        # 从末端执行器的父关节开始,反向遍历到根关节
        for i in range(len(chain) - 2, -1, -1):
            joint = chain[i]
            end_effector = chain[-1]
            
            # 1. 计算向量
            # 关节到末端执行器的向量
            to_end = normalize(end_effector.global_position - joint.global_position)
            # 关节到目标的向量
            to_target = normalize(target_pos - joint.global_position)
            
            # 2. 计算旋转角度和轴
            # 点积得到余弦值 -> 角度
            cos_theta = dot(to_end, to_target)
            # 限制在 [-1, 1] 以防浮点误差导致 acos 越界
            cos_theta = clamp(cos_theta, -1.0, 1.0)
            angle = acos(cos_theta)
            
            # 极小角度忽略,防止抖动
            if angle < 0.0001:
                continue
                
            # 叉积得到旋转轴
            axis = cross(to_end, to_target)
            axis = normalize(axis)
            
            # 3. 应用旋转
            # 在关节局部空间或全局空间应用旋转
            # 注意:实际实现中通常需要将全局轴转换为局部轴
            joint.rotate(axis, angle)
            
            # 4. (可选) 应用关节约束
            # joint.clamp_rotation()
            
            # 更新链中受影响关节的全局位置(FK)
            update_chain_positions(chain)
            
    return False # 未能完全收敛

优缺点

  • 优点
    • 实现简单:算法逻辑直观,易于编码。
    • 计算成本低:每次迭代只涉及简单的向量运算,不需要构建雅可比矩阵(Jacobian)或求逆。
    • 数值稳定:不易出现奇异性问题。
  • 缺点
    • 不自然:倾向于过度旋转靠近末端执行器的关节,导致“卷曲”现象。
    • 收敛慢:对于某些配置(如完全伸展),收敛速度可能较慢。

FABRIK

FABRIK (Forward And Backward Reaching Inverse Kinematics) 是一种启发式迭代算法,它直接作用于关节位置(Positions)而不是关节角度(Angles)。它将 IK 问题视为寻找一条通过所有关节位置的折线,使其满足骨骼长度约束。

原理

假设关节位置为 $P_0, P_1, \dots, P_n$,其中 $P_0$ 是根(Root),$P_n$ 是末端执行器(End Effector)。$d_i = |P_{i+1} - P_i|$ 是第 $i$ 根骨骼的长度。

算法的核心思想是在两条直线之间迭代:

  1. 前向 (Forward):从目标开始,将末端执行器拉向目标,并随之调整整条链。
  2. 后向 (Backward):将根节点拉回其原始位置,并随之调整整条链。

算法流程

  1. 计算距离:检查根节点到目标的距离是否大于总骨骼长度。
    • 如果大于:目标不可达,直接拉直关节链指向目标。
    • 如果小于:进入迭代循环。
  2. 前向传递 (Forward Reaching)
    • 将末端执行器 $P_n$ 移至目标位置 $T$:$P_n' = T$。
    • 从 $P_{n-1}$ 向根部遍历到 $P_0$:
      • 将 $P_i$ 移动到连接 $P_{i+1}'$ 和 $P_i$ 的直线上,且距离 $P_{i+1}'$ 为 $d_i$ 处。
      • 公式:$P_i' = P_{i+1}' + \frac{P_i - P_{i+1}'}{|P_i - P_{i+1}'|} \cdot d_i$。
  3. 后向传递 (Backward Reaching)
    • 将根节点 $P_0$ 移回其原始固定位置 $Origin$:$P_0'' = Origin$。
    • 从 $P_1$ 向末端遍历到 $P_n$:
      • 将 $P_i$ 移动到连接 $P_{i-1}''$ 和 $P_i$ 的直线上,且距离 $P_{i-1}''$ 为 $d_{i-1}$ 处。
      • 公式:$P_i'' = P_{i-1}'' + \frac{P_i' - P_{i-1}''}{|P_i' - P_{i-1}''|} \cdot d_{i-1}$。
  4. 重复:重复步骤 2 和 3,直到末端执行器与目标的距离小于阈值或达到最大迭代次数。

伪代码

def SolveIK_FABRIK(chain, target_pos, tolerance=0.01, max_iter=10):
    # chain: 关节位置列表 [p0, p1, ..., pn]
    # lengths: 骨骼长度列表 [d0, d1, ..., dn-1]
    # origin: 根节点原始位置 p0
    
    # 0. 预计算骨骼长度
    lengths = [distance(chain[i], chain[i+1]) for i in range(len(chain)-1)]
    total_length = sum(lengths)
    root_pos = chain[0]
    
    # 1. 检查是否可达
    if distance(root_pos, target_pos) > total_length:
        # 目标不可达:拉直
        dir = normalize(target_pos - root_pos)
        for i in range(1, len(chain)):
            chain[i] = chain[i-1] + dir * lengths[i-1]
        return
        
    # 2. 迭代求解
    diff = distance(chain[-1], target_pos)
    iter_count = 0
    
    while diff > tolerance and iter_count < max_iter:
        # --- Forward Reaching (从末端向根部) ---
        chain[-1] = target_pos # 设置末端到目标
        for i in range(len(chain)-2, -1, -1):
            # 找到 Pi 到 Pi+1 的方向
            dir = normalize(chain[i] - chain[i+1])
            # 将 Pi 放在距离 Pi+1 为 lengths[i] 的位置
            chain[i] = chain[i+1] + dir * lengths[i]
            
        # --- Backward Reaching (从根部向末端) ---
        chain[0] = root_pos # 将根重置回原点
        for i in range(1, len(chain)):
            # 找到 Pi 到 Pi-1 的方向
            dir = normalize(chain[i] - chain[i-1])
            # 将 Pi 放在距离 Pi-1 为 lengths[i-1] 的位置
            chain[i] = chain[i-1] + dir * lengths[i-1]
            
        diff = distance(chain[-1], target_pos)
        iter_count += 1
        
    # 注意FABRIK算法原生是操作位置,旋转需要额外计算
    # 可以类似CCD的算法,在每次更新关节位置的同时计算关节旋转

从位置推导旋转

FABRIK 原生只输出位置 $P_i$。为了驱动骨骼动画,我们需要更新骨骼的旋转矩阵(或四元数)。

基本思路: 对于每一根骨骼,计算其在 FABRIK 求解前后的方向向量,找出对齐这两个向量所需的旋转,并将其应用到骨骼的当前旋转上。

步骤

  1. 对于骨骼 $i$(连接 $J_i$ 和 $J_{i+1}$):
    • 旧方向 $V_{old} = \text{normalize}(P_{i+1}^{old} - P_i^{old})$ (或者直接用骨骼的轴向,如 $(0,1,0)$ 变换后的方向)。
    • 新方向 $V_{new} = \text{normalize}(P_{i+1}^{new} - P_i^{new})$。
  2. 计算旋转 $Q_{delta}$:
    • 将 $V_{old}$ 旋转到 $V_{new}$ 的最短路径旋转(Shortest Arc Rotation)。
    • 轴 $axis = V_{old} \times V_{new}$。
    • 角 $\theta = \arccos(V_{old} \cdot V_{new})$。
  3. 更新全局旋转:
    • $R_{global, i}^{new} = Q_{delta} \cdot R_{global, i}^{old}$。
  4. 计算局部旋转(用于更新层级):
    • $R_{local, i}^{new} = (R_{global, i-1}^{new})^{-1} \cdot R_{global, i}^{new}$。

伪代码

def UpdateRotations(chain, new_positions):
    # chain: 骨骼列表,包含当前的 global_rotation 和 position
    # new_positions: FABRIK 计算出的新位置列表
    
    # 假设 chain[i] 是节点,bone_axis 是骨骼在局部空间的指向 (例如 Vector3.UP)
    
    for i in range(len(chain) - 1):
        # 1. 获取旧的骨骼方向 (Global)
        # 方式 A: 使用旧位置
        # old_dir = normalize(chain[i+1].position - chain[i].position)
        # 方式 B: 使用旋转 (更准确,如果骨骼有初始弯曲)
        old_dir = chain[i].global_rotation * chain[i].bone_axis
        
        # 2. 获取新的骨骼方向 (从 FABRIK 位置)
        new_dir = normalize(new_positions[i+1] - new_positions[i])
        
        # 3. 计算对齐旋转 (FromToRotation / Shortest Arc)
        delta_rotation = FromToRotation(old_dir, new_dir)
        
        # 4. 更新全局旋转
        chain[i].global_rotation = delta_rotation * chain[i].global_rotation
        
        # 5. 更新局部旋转 (相对于父节点)
        if i > 0:
            parent_inv = inverse(chain[i-1].global_rotation)
            chain[i].local_rotation = parent_inv * chain[i].global_rotation
        else:
            # 根节点
            chain[i].local_rotation = chain[i].global_rotation
            
    # 最后一个关节通常继承父关节旋转或保持不变
    chain[-1].global_rotation = chain[-2].global_rotation
    chain[-1].local_rotation = Identity() 

优缺点

  • 优点
    • 收敛速度快:通常比 CCD 迭代次数少,且单次迭代开销极低。
    • 平滑:结果通常比 CCD 更自然,没有“卷曲”问题。
    • 无奇异性:不会像基于 Jacobian 的方法那样遇到万向节锁或奇异性问题。
  • 缺点
    • 仅位置:原生算法只处理位置,不处理旋转约束(需要额外步骤将位置转回旋转)。
    • 断链:在迭代过程中骨骼实际上是分离的,只有在收敛后才看起来是连接的。

Two-Bone IK

Two-Bone IK (双骨骼 IK) 是一种专门用于解决由两根骨骼组成的运动链(例如:上臂+前臂,大腿+小腿)的解析解算法。与 CCD 或 FABRIK 等迭代算法不同,Two-Bone IK 可以直接通过几何计算得出精确解。

原理

给定根关节 (A)、中间关节 (B)、末端执行器 (C) 和目标位置 (T)。

  • $a$:骨骼 AB 的长度。
  • $c$:骨骼 BC 的长度。
  • $b$:根关节 A 到目标 T 的距离。

我们可以将问题视为在三维空间中构建一个三角形 ABT。根据余弦定理 (Law of Cosines),我们可以求出三角形的内角,从而确定关节的角度。

核心步骤

  1. 计算内角:利用余弦定理计算关节 A 和关节 B 处所需的弯曲角度。
  2. 旋转根关节 (Root Rotation)
    • 先将骨骼链旋转到指向目标的方向。
    • 再根据计算出的内角 $\alpha$ 调整根关节,使其指向正确的中间位置。
  3. 旋转中间关节 (Middle Rotation):根据内角 $\beta$ 旋转中间关节。
  4. 极向量 (Pole Vector):为了确定关节弯曲的方向(例如膝盖是向外弯还是向前弯),需要引入一个参考向量(Pole Vector)来固定旋转平面。

数学推导

余弦定理

$$ c^2 = a^2 + b^2 - 2ab \cos(\gamma) $$

由此可得内角:

  • 中间关节角 (在 B 处的外角) $\phi$:

    $$ \cos(\alpha_{inner}) = \frac{a^2 + c^2 - b^2}{2ac} $$

    关节实际弯曲角度通常是 $180^\circ - \alpha_{inner}$。

  • 根关节调整角 (在 A 处的角) $\theta$:

    $$ \cos(\theta) = \frac{a^2 + b^2 - c^2}{2ab} $$

算法流程

  1. 验证距离
    • 如果 $b > a + c$:目标不可达(太远),拉直手臂指向目标。
    • 如果 $b < |a - c|$:目标不可达(太近,缩到最短)。
  2. 计算角度:使用 acos 计算 $\theta$ 和 $\alpha$。
  3. 应用旋转
    • Root (A):首先旋转 A 使得 AC 连线指向 T。然后绕着旋转平面法线旋转 $\theta$。
    • Mid (B):绕着旋转平面法线旋转 $\phi$。
  4. 处理极向量 (Pole Vector)
    • 旋转平面由 A、T 和 Pole Vector 决定。确保关节 B 位于该平面上。

伪代码

def SolveTwoBoneIK(root, mid, end, target_pos, pole_vector):
    a = length(mid.position - root.position) # 上臂长
    c = length(end.position - mid.position)  # 前臂长
    b = length(target_pos - root.position)   # 根到目标距离
    
    # 1. 距离限制
    if b > a + c:
        # 目标太远,直接指向
        dir = normalize(target_pos - root.position)
        root.look_at(target_pos)
        mid.look_at(target_pos) # 拉直
        return
        
    # 2. 余弦定理计算角度
    # 根节点处的内角 (theta)
    cos_root = (a*a + b*b - c*c) / (2*a*b)
    angle_root = acos(clamp(cos_root, -1, 1))
    
    # 中间节点处的内角 (phi, 注意这是内角,实际旋转可能需要补角)
    cos_mid = (a*a + c*c - b*b) / (2*a*c)
    angle_mid = acos(clamp(cos_mid, -1, 1))
    
    # 3. 构建坐标系和旋转
    # 目标方向
    target_dir = normalize(target_pos - root.position)
    
    # 旋转平面的法线 (由 目标方向 和 极向量 确定)
    plane_normal = normalize(cross(target_dir, pole_vector))
    
    # --- 应用旋转到 Root ---
    # 先将根指向目标
    # 再绕法线旋转 angle_root,使上臂抬起
    # Quaternion.AxisAngle(axis, angle)
    root_rot_offset = Quaternion.AxisAngle(plane_normal, angle_root)
    # 假设 look_at 会将骨骼主轴对齐到 target_dir,副轴对齐到 plane_normal
    root.rotation = root_rot_offset * LookAt(target_dir, plane_normal)
    
    # --- 应用旋转到 Mid ---
    # 中间关节旋转通常是绕着同样的法线弯曲
    # 弯曲角度取决于骨骼定义的 rest pose,通常是 180 - angle_mid
    bend_angle = PI - angle_mid 
    mid.local_rotation = Quaternion.AxisAngle(local_bend_axis, bend_angle)

优缺点

  • 优点
    • 极其高效:解析解,无迭代,常数时间复杂度 $O(1)$。
    • 精确:只要在可达范围内,结果绝对精确。
    • 稳定:不会像迭代法那样出现抖动。
  • 缺点
    • 局限性:仅适用于 2 根骨骼的链。
    • 极向量:必须手动指定极向量来控制弯曲方向,否则结果可能翻转。

Retargeting

动画重定向是将动画数据从一个角色(源)传输到另一个角色(目标)的过程,通常具有不同的骨骼比例或拓扑结构。

基础算法:基于 Bind Pose 的相对旋转 (Relative Rotation)

最基础的重定向方法是转移“相对于初始姿态 (Bind Pose / T-Pose) 的旋转变化量”,而不是直接复制旋转值。

1. 原理

假设源角色 (Source) 和目标角色 (Target) 的骨骼定义不同(例如 Source 的手臂自然下垂是 0 度,而 Target 的手臂平举是 0 度)。直接复制旋转会导致错误的姿态。 我们需要计算 Source 从 Bind Pose 到 Current Pose 的“Delta Rotation”,并将这个 Delta 应用到 Target 的 Bind Pose 上。

2. 数学推导

对于任意关节 $i$:

  1. 计算源角色的局部旋转增量 ($Q_{delta}$): $$ Q_{delta} = (Q_{src\_bind}^{-1} \cdot Q_{src\_current}) $$ 这里 $Q$ 均为局部空间旋转 (Local Rotation)。
  2. 应用到目标角色: $$ Q_{tgt\_current} = Q_{tgt\_bind} \cdot Q_{delta} $$

3. 坐标系不一致

如果源和目标的骨骼坐标轴定义不同(例如 Source 的 X 轴指向子骨骼,Target 的 Y 轴指向子骨骼),简单的 Bind Pose 相对旋转可能不够。需要引入坐标基变换。

解决方案: 在 Retargeting 初始化阶段,计算一个 重定向矩阵 (Retargeting Map / Mount Rotation) $M_{map}$,用于修正坐标系差异。

$$ Q_{tgt\_current} = Q_{tgt\_bind} \cdot M_{map} \cdot Q_{delta} \cdot M_{map}^{-1} $$

或者更简单地,在逻辑层确保两者都处于相同的“虚拟坐标系”中(例如都映射到标准的 T-Pose 空间)。

4. 根节点位移

对于根节点(通常是 Hips/Pelvis),除了旋转还需要处理位移 (Position)。由于身高腿长不同,直接复制位移会导致“滑步”或“浮空”。

$$ P_{tgt} = P_{tgt\_bind} + (P_{src} - P_{src\_bind}) \cdot S $$

其中 $S$ 是缩放因子,通常 $S = \frac{\text{Target Leg Length}}{\text{Source Leg Length}}$ 或 $S = \frac{\text{Target Height}}{\text{Source Height}}$。

常见问题与挑战

  • 用例
    • 将动作捕捉 (MoCap) 数据应用于游戏角色。
    • 在游戏中的不同角色之间重用动画。
  • 挑战
    • 滑步 (Foot Sliding):脚在应该着地时移动。即使缩放了位移,也常需 IK 修正。
    • 自碰撞 (Self-Collision):由于体型不同,肢体穿过身体。
    • 末端执行器可达性 (End-Effector Reach):小角色可能无法像高个子角色那样够到同一个架子。

Animation Blending

动画混合(Animation Blending)是将多个动画片段(Clips)的采样结果组合在一起,以生成最终姿态的过程。它是现代动画系统的核心,用于实现平滑过渡和复杂的动作组合。

1. 线性插值与混合 (Linear Interpolation)

最基本的混合是在两个姿态(Pose A 和 Pose B)之间进行插值。

混合公式

对于骨骼 $i$ 的局部变换(位移 $T$、旋转 $R$、缩放 $S$),给定混合权重 $w \in [0, 1]$:

  • 位移/缩放 (Translation/Scale): 使用线性插值 (LERP)。 $$ T_{out} = \text{Lerp}(T_A, T_B, w) = (1-w)T_A + wT_B $$
  • 旋转 (Rotation): 使用球面线性插值 (SLERP) 或归一化线性插值 (NLERP) 处理四元数。 $$ R_{out} = \text{Slerp}(Q_A, Q_B, w) $$ 注意:必须确保 $Q_A \cdot Q_B \ge 0$(点积非负),如果为负,需反转其中一个四元数,以走最短路径。

2. 混合类型 (Blending Types)

Cross-Fading (淡入淡出)

在一段时间内,将动画 A 的权重从 1 减至 0,同时将动画 B 的权重从 0 增至 1。

  • 通常用于状态切换(State Machine Transitions),如 Idle $\to$ Walk。
  • 平滑函数:权重变化通常不是线性的,而是使用 SmoothStep 或 Ease-In/Out 函数以获得更自然的过渡。

Blend Trees (混合树)

混合树用于根据参数(Parameters)连续混合多个动画,而不是简单的 A 到 B 过渡。

1D Blending

根据单个参数(如“速度”)在多个动画间插值。

  • 示例:Idle (0) $\to$ Walk (1) $\to$ Run (2)。
  • 如果输入速度为 1.5,则混合 50% Walk 和 50% Run。

2D Blending (Direct / Cartesian / Freeform)

根据两个参数(如“水平速度”和“垂直速度”)混合。

  • 重心坐标 (Barycentric Coordinates): 常用于 Freeform Directional 混合(如移动扫射 Strafe)。将动画样本视为 2D 空间中的点(Delaunay 三角剖分),输入参数作为采样点。计算采样点在三角形内的重心坐标 $(u, v, w)$ 作为混合权重。 $$ Pose_{final} = u \cdot Pose_A + v \cdot Pose_B + w \cdot Pose_C $$ 其中 $u+v+w=1$。

Additive Blending (叠加混合)

将一个动画的“变化量”叠加到另一个动画上。常用于在行走/奔跑时添加呼吸、受伤、射击或表情动作。

  • 原理: 叠加动画不是存储绝对姿态,而是存储相对于参考姿态(Reference Pose,通常是 Bind Pose 或动画第一帧)的差值 (Delta)。
  • 计算
    1. 计算差值:$D = A_{additive} \ominus A_{ref}$
      • 对于位移:$D_T = T_{add} - T_{ref}$
      • 对于旋转:$D_R = Q_{add} \cdot Q_{ref}^{-1}$
    2. 应用叠加:$Final = Base \oplus (D \times weight)$
      • 对于位移:$T_{final} = T_{base} + D_T \cdot w$
      • 对于旋转:$Q_{final} = \text{Slerp}(Identity, D_R, w) \cdot Q_{base}$ (或者 $Q_{final} = D_R^w \cdot Q_{base}$,注意旋转顺序通常是 Post-rotation)

3. 骨骼遮罩 (Avatar Masking)

仅对身体的特定部分应用混合。

  • 例如:下半身播放“跑步”动画,上半身播放“挥手”动画。
  • 实现:在混合阶段,根据骨骼层级设置权重。若骨骼在遮罩内,权重为 1,否则为 0(或使用过渡权重)。

4. 惯性混合 (Inertial Blending) / Inertialization

一种高性能的过渡技术,用于替代传统的 Cross-Fading。

  • 问题:Cross-Fading 需要同时计算两个动画图(Source 和 Target),开销加倍。
  • 方案:在切换瞬间,立刻停止 Source 动画,开始 Target 动画。计算 Source 和 Target 在切换瞬间的速度/加速度差值,生成一个衰减的“惯性偏移”叠加在 Target 上。
  • 公式: $$ Pose(t) = Pose_{target}(t) + \text{Offset}(t) $$ $$ \text{Offset}(t) \approx A t^5 + B t^4 + \dots $$ (使用五次多项式或其他衰减函数拟合位置和速度的连续性)
  • 优点:无需同时采样两个动画,响应极快,过渡自然。