骨骼动画与蒙皮
全库目录221
本文目录15
骨骼动画把高维的逐顶点运动压缩成少量关节变换,再用蒙皮权重恢复表面运动。要理解实现,关键是分清绑定姿态、当前姿态、局部空间和模型空间。
轨道与骨骼矩阵
骨架与关节
骨架是一棵层级树。引擎实际保存的是关节的局部变换、父索引和绑定信息。“骨骼”通常只是相邻关节之间的可视化连杆。
标准骨架有两个工程价值:
- 动画、动捕和 Retargeting 可以复用;
- 工具能依赖稳定的命名、拓扑、Root 和挂点。
Root 一般作为角色整体位移基准,Pelvis 负责身体重心和蹲起等局部运动。武器、特效和镜头挂点应有明确的附件关节,避免依赖易变化的网格节点。
局部姿态到模型空间
动画轨道通常保存关节相对父节点的平移、旋转、缩放。局部数据更容易压缩,并明确父子层级。骨长能否保持还取决于轨道:固定局部平移与单位缩放时,旋转动画保持骨长;平移或缩放轨道会主动改变长度。
对第 个关节:
从 Root 按父子顺序累积,就能得到全部关节的当前模型空间矩阵。若父节点还没计算,子节点结果一定错误,因此骨架常按拓扑顺序存储。
旋转应在局部空间插值。直接插值两个模型空间关节位置,可能让骨骼在中间帧伸缩或走不自然的直线。
轨道采样与层级累积
每个关节的轨道可以有独立关键帧时间。采样时先在当前轨道找到包围时间的两个关键帧,平移与缩放做线性插值,旋转使用最短弧 Slerp,再按拓扑顺序累积父变换:
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 表示尚未由对象变换消费的根变换。若角色控制器已经把它应用到实体位置,根节点累积应从单位变换或指定骨架根开始,否则位移会被计算两次。用原地待机和向前移动一米的片段分别检查实体轨迹与骨架根轨迹,可以确定这份位移的所有者。
绑定姿态与逆绑定矩阵
绑定姿态是蒙皮权重建立时的参考姿态。顶点在绑定时位于模型空间,但每个关节有自己的绑定模型空间变换 。
先把绑定姿态顶点从模型空间变到关节的绑定局部空间。当前姿态的模型空间矩阵 再把它带到当前姿态:
先用平移例检查这两步:绑定关节位于模型x=2,顶点位于x=3,逆绑定先得到局部x=1;当前关节移到x=5,再变回模型得到x=6。若省略逆绑定,会得到x=8,顶点把绑定偏移重复带入。绑定姿态下当前关节仍在x=2,输出回到x=3。
就是第 个关节的蒙皮矩阵。模型“炸开”常见原因是矩阵乘法顺序、行列约定、坐标系或逆绑定矩阵错误。
顶点蒙皮
线性混合蒙皮
一个顶点可以受多根关节影响。线性混合蒙皮(LBS)计算:
表面已经弯曲,法线也要跟着变。纯刚体骨骼常用加权旋转法线再归一化作为实时近似;混入非均匀缩放后,逐骨骼逆转置再加权并不等于混合线性映射的逆转置。若把每个顶点的权重视为局部常量,应针对该混合映射求逆转置,奇异情况还要回退。对强形变,重建变形后几何法线也有价值。
LBS 简单、稳定、硬件友好,但手腕等部位大角度扭转时容易出现 Candy Wrapper,弯曲关节也可能丢体积。常见改善:
- 增加 Twist 关节并重新分配权重;
- 使用 Corrective 形状目标;
- 双四元数蒙皮(DQS);
- 对关键关节使用局部 RBF/姿态驱动修形。
DQS 更能保持刚体旋转和体积,但对缩放、混合管线和资产支持要求更高。
GPU 蒙皮核心
下面的 HLSL 代码使用四个骨骼影响计算位置。SkinMatrices 已经是 ,因此顶点不需要再次进入绑定姿态空间:
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 或双四元数后插值。
更新与渲染顺序
常见执行顺序是:
- 推进动画时间并采样动画片段;
- 混合局部姿态;
- 执行 IK、约束和程序化修正;
- 累积模型空间姿态;
- 与物理或布料交换数据;
- 生成蒙皮 Palette;
- 提交渲染和运动向量。
运动向量必须使用当前与上一帧一致的变形结果。只保存对象变换而忽略上一帧骨骼姿态,会让蒙皮角色的 TAA 和运动模糊拖影。
验证方法
- 在绑定姿态验证 接近单位变换。
- 可视化骨架、关节 Axes、骨骼影响和权重和。
- 固定一个顶点,逐步打印每个关节的加权贡献。
- 比较 CPU/GPU 蒙皮输出和法线方向。
- 在 RenderDoc 检查骨骼缓冲、骨骼影响和顶点着色器输入。
- 对极限姿态、LOD 切换、运动向量和阴影 Pass 做回归。
相关主题
参考资料
- Khronos:glTF Skins:The Joint Matrices:全局关节变换、逆绑定矩阵及蒙皮权重。