← ALL ARTICLES
动画系统6 MIN READ

骨骼动画与蒙皮

全库目录221
本文目录15

骨骼动画把高维的逐顶点运动压缩成少量关节变换,再用蒙皮权重恢复表面运动。要理解实现,关键是分清绑定姿态、当前姿态、局部空间和模型空间。

轨道与骨骼矩阵

骨架与关节

骨架是一棵层级树。引擎实际保存的是关节的局部变换、父索引和绑定信息。“骨骼”通常只是相邻关节之间的可视化连杆。

标准骨架有两个工程价值:

  • 动画、动捕和 Retargeting 可以复用;
  • 工具能依赖稳定的命名、拓扑、Root 和挂点。

Root 一般作为角色整体位移基准,Pelvis 负责身体重心和蹲起等局部运动。武器、特效和镜头挂点应有明确的附件关节,避免依赖易变化的网格节点。

局部姿态到模型空间

动画轨道通常保存关节相对父节点的平移、旋转、缩放。局部数据更容易压缩,并明确父子层级。骨长能否保持还取决于轨道:固定局部平移与单位缩放时,旋转动画保持骨长;平移或缩放轨道会主动改变长度。

对第 ii 个关节:

Mimodel(t)=Mparent(i)model(t)Milocal(t)M_i^{model}(t)=M_{parent(i)}^{model}(t)M_i^{local}(t)

从 Root 按父子顺序累积,就能得到全部关节的当前模型空间矩阵。若父节点还没计算,子节点结果一定错误,因此骨架常按拓扑顺序存储。

旋转应在局部空间插值。直接插值两个模型空间关节位置,可能让骨骼在中间帧伸缩或走不自然的直线。

轨道采样与层级累积

每个关节的轨道可以有独立关键帧时间。采样时先在当前轨道找到包围时间的两个关键帧,平移与缩放做线性插值,旋转使用最短弧 Slerp,再按拓扑顺序累积父变换:

cpp
for (JointIndex joint : skeleton.topologicalOrder) {
    TRS local = SampleLocalTRS(clip.track[joint], time);
    Matrix4 localMatrix = Compose(local.translation,
                                  local.rotation,
                                  local.scale);
    Matrix4 modelMatrix = skeleton.HasParent(joint)
        ? modelPose[skeleton.Parent(joint)] * localMatrix
        : rootMotion * localMatrix;
    modelPose[joint] = modelMatrix;
    skinPalette[joint] = modelMatrix * inverseBind[joint];
}

输入轨道时间应先按动画片段的循环或限制范围规则归一化。四元数点积小于 0 时翻转其中一个端点,才能沿同一旋转的短弧插值。没有轨道的关节使用参考姿态局部变换。最终矩阵的顺序必须与导入时的坐标和矩阵约定一致;在绑定姿态时间点,若两份矩阵处于相同模型空间且未叠加额外根运动,modelMatrix * inverseBind 应接近单位矩阵,这是最直接的层级与逆绑定检查。

代码中的 rootMotion 表示尚未由对象变换消费的根变换。若角色控制器已经把它应用到实体位置,根节点累积应从单位变换或指定骨架根开始,否则位移会被计算两次。用原地待机和向前移动一米的片段分别检查实体轨迹与骨架根轨迹,可以确定这份位移的所有者。

绑定姿态与逆绑定矩阵

绑定姿态是蒙皮权重建立时的参考姿态。顶点在绑定时位于模型空间,但每个关节有自己的绑定模型空间变换 BiB_i。

Bi−1B_i^{-1} 先把绑定姿态顶点从模型空间变到关节的绑定局部空间。当前姿态的模型空间矩阵 Mi(t)M_i(t) 再把它带到当前姿态:

Si(t)=Mi(t)Bi−1S_i(t)=M_i(t)B_i^{-1}

先用平移例检查这两步:绑定关节位于模型x=2,顶点位于x=3,逆绑定先得到局部x=1;当前关节移到x=5,再变回模型得到x=6。若省略逆绑定,会得到x=8,顶点把绑定偏移重复带入。绑定姿态下当前关节仍在x=2,输出回到x=3。

Si(t)S_i(t) 就是第 ii 个关节的蒙皮矩阵。模型“炸开”常见原因是矩阵乘法顺序、行列约定、坐标系或逆绑定矩阵错误。

顶点蒙皮

线性混合蒙皮

一个顶点可以受多根关节影响。线性混合蒙皮(LBS)计算:

p′=∑i=1nwiSi(t)p,∑iwi=1\mathbf{p}'=\sum_{i=1}^{n}w_iS_i(t)\mathbf{p},\qquad \sum_i w_i=1

表面已经弯曲,法线也要跟着变。纯刚体骨骼常用加权旋转法线再归一化作为实时近似;混入非均匀缩放后,逐骨骼逆转置再加权并不等于混合线性映射的逆转置。若把每个顶点的权重视为局部常量,应针对该混合映射求逆转置,奇异情况还要回退。对强形变,重建变形后几何法线也有价值。

LBS 简单、稳定、硬件友好,但手腕等部位大角度扭转时容易出现 Candy Wrapper,弯曲关节也可能丢体积。常见改善:

  • 增加 Twist 关节并重新分配权重;
  • 使用 Corrective 形状目标;
  • 双四元数蒙皮(DQS);
  • 对关键关节使用局部 RBF/姿态驱动修形。

DQS 更能保持刚体旋转和体积,但对缩放、混合管线和资产支持要求更高。

GPU 蒙皮核心

下面的 HLSL 代码使用四个骨骼影响计算位置。SkinMatrices 已经是 Mi(t)Bi−1M_i(t)B_i^{-1},因此顶点不需要再次进入绑定姿态空间:

hlsl
float4 SkinPosition(
    float3 positionOS,
    uint4 jointIndices,
    float4 jointWeights)
{
    float weightSum = dot(jointWeights, float4(1.0, 1.0, 1.0, 1.0));
    float4 weights = jointWeights / max(weightSum, 1e-5);
    float4 source = float4(positionOS, 1.0);
    float4 skinned = 0.0;

    [unroll]
    for (uint i = 0; i < 4; ++i)
        skinned += mul(SkinMatrices[jointIndices[i]], source)
                 * weights[i];

    return skinned;
}

生产资产应在导入阶段处理零权重和越界索引。运行时归一化可以防止小误差,但零权重仍会得到零位置,越界索引仍会访问无效数据。导入器要给零权重顶点分配有效默认骨骼,并在删骨后重映射索引。

蒙皮权重的实际约束

每个顶点影响骨骼数越多,顶点着色器读取和矩阵运算越多。移动端常限制为 4 个骨骼影响;高端角色可能允许 8 个,但需要实测。

导入或发布时应检查:

  • 权重和是否接近 1;
  • 是否引用不存在或被裁剪的关节;
  • 微小无意义权重是否清理;
  • 关节弯曲处的权重梯度是否连续;
  • LOD 删骨后权重是否正确转移到保留父骨骼。

只在绑定姿态检查权重不够。应使用肩、肘、腕、胯、膝等极限测试姿态观察塌陷、拉伸和穿插。

变形数据提交

CPU 与 GPU 蒙皮

CPU 蒙皮在 CPU 计算变形顶点,再上传动态顶点缓冲。优点是 CPU 侧容易访问最终几何,也能兼容部分碰撞或旧管线;缺点是计算和上传带宽随顶点数增长。

GPU 蒙皮将骨骼矩阵表放进常量缓冲、结构化缓冲或纹理,再由顶点或计算着色器读取。具体布局取决于骨骼数量、复用通道与平台限制。它减少 CPU 顶点工作,但 GPU 每个相关 Pass 都可能重复蒙皮。

计算着色器蒙皮可以先把结果写入缓冲,供深度、阴影、GBuffer 等 Pass 复用。代价是额外显存、写带宽、屏障和调度。是否更快取决于角色数量、Pass 数和平台。

骨骼矩阵表与分段

一次 Draw 能访问的关节数受常量空间和平台限制。导入器可能按骨骼集合拆网格 Section,使每段只包含有限 Palette。

拆分会增加 Draw Call。工具应同时考虑:

  • 每个 Section 的骨骼数量;
  • 材质 Submesh;
  • 顶点在 Section 边界的复制;
  • LOD 和影子 Pass 是否需要全部骨骼。

动画纹理

大量同类角色可把每帧关节矩阵、双四元数或已蒙皮顶点烘焙到纹理/缓冲。实例只保存动画片段、时间、帧索引和混合参数。

两种常见方案:

  • 骨骼 Animation 纹理:存关节变换,顶点着色器仍按权重蒙皮;
  • 顶点 Animation 纹理:直接存每帧顶点位置/法线,运行时采样更直接,但数据量大且不易重新组合骨骼动作。

时间插值需要采样相邻帧。两个动画片段混合会增加双份读取。直接线性插值矩阵会破坏正交性,较稳妥的做法是分别存 TRS 或双四元数后插值。

更新与渲染顺序

常见执行顺序是:

  1. 推进动画时间并采样动画片段;
  2. 混合局部姿态;
  3. 执行 IK、约束和程序化修正;
  4. 累积模型空间姿态;
  5. 与物理或布料交换数据;
  6. 生成蒙皮 Palette;
  7. 提交渲染和运动向量。

运动向量必须使用当前与上一帧一致的变形结果。只保存对象变换而忽略上一帧骨骼姿态,会让蒙皮角色的 TAA 和运动模糊拖影。

验证方法

  • 在绑定姿态验证 MiBi−1M_iB_i^{-1} 接近单位变换。
  • 可视化骨架、关节 Axes、骨骼影响和权重和。
  • 固定一个顶点,逐步打印每个关节的加权贡献。
  • 比较 CPU/GPU 蒙皮输出和法线方向。
  • 在 RenderDoc 检查骨骼缓冲、骨骼影响和顶点着色器输入。
  • 对极限姿态、LOD 切换、运动向量和阴影 Pass 做回归。

相关主题

参考资料

  • Khronos:glTF Skins:The Joint Matrices:全局关节变换、逆绑定矩阵及蒙皮权重。