一、骨骼、网格体、蒙皮——三个独立的概念
一个会动的角色由三层组成:
骨骼(Skeleton) → 层次化的"骨头"结构,定义运动学关系网格体(Mesh) → 顶点、三角面——角色的"皮肤"蒙皮(Skinning) → 连接骨骼和网格体的"胶水"——每个顶点被哪些骨头影响关键认知:骨骼和网格体是分开的。骨骼定义了动作(旋转、平移),网格体只是跟着变形。同一套骨骼可以配不同的身体(换个皮肤、换套衣服),同一套骨骼也可以配不同的动画(走、跑、跳)。
二、骨骼的层次结构
骨骼不是散装的——是父子层次树:
Root (Hips)├── Spine│ ├── Chest│ │ ├── Neck → Head│ │ ├── LeftShoulder → LeftArm → LeftHand → ...│ │ └── RightShoulder → RightArm → RightHand → ...│ └── ...└── LeftLeg → LeftFoot → ...每根骨头存储的是相对于父骨的变换(Local Transform)。世界空间中的位置通过从根向叶子逐级叠加计算:
WorldMatrix(Bone) = WorldMatrix(ParentBone) × LocalMatrix(Bone)这和场景图(第 6 篇)里父子 Transform 的计算一模一样——骨骼本质上就是一个层次化的场景图。
Bind Pose(绑定姿势):蒙皮时角色的参考姿势——通常是 T-Pose 或 A-Pose。顶点权重和骨骼位置都以这个姿势为参考。
三、蒙皮:顶点怎么跟着骨骼动
1 线性混合蒙皮(LBS / LBSM)
标准做法。每个顶点可被最多 N 根骨头影响(UE 默认最多 4 根,即 4-bone influence):
顶点在动画中的位置 = Σ (Weight_i × BoneMatrix_i × BindPoseInverse_i × VertexPos_BindPose) └──────────┬──────────┘ 从绑定空间变换到骨骼空间,再跟随骨骼运动简化理解:
float3 SkinnedPosition = float3(0, 0, 0);for (int i = 0; i < 4; i++) { float weight = Input.Weights[i]; // 第 i 根骨头对这个顶点的影响权重 float4x4 boneMatrix = BoneMatrices[Input.BoneIndices[i]]; SkinnedPosition += weight * mul(boneMatrix, float4(Input.Position, 1.0)).xyz;}每个顶点存:
BoneIndices[4]——哪 4 根骨头影响它BoneWeights[4]——每根骨头的权重(总和为 1.0)
Weight 和为 1.0 很重要——如果总和小于 1,蒙皮结果会在 BindPose 原点附近收缩;如果大于 1,顶点会过度拉伸。
2 蒙皮的经典问题
Candy Wrapper(糖纸效应): 关节弯曲时(如肘部),LBS 会让网格体在关节处”收缩”——像拧糖纸一样。因为 LBS 是对骨骼矩阵做线性插值,但旋转矩阵的线性插值不等于旋转的线性插值。
解决方案:Dual Quaternion Skinning(双四元数蒙皮)——用对偶四元数替代矩阵做插值。UE 支持在特定骨骼上启用(SkinCache 相关设置)。计算更贵,但关节弯曲处的体积保持更好。
四、动画数据:骨骼怎么动的
1 关键帧动画
动画 = 每根骨头每个时刻的 Transform。不存每帧——只存关键帧,中间帧由引擎插值:
Walk Animation(1 秒): Bone_LeftLeg: t=0.0 (前) → t=0.5 (后) → t=1.0 (前) Bone_RightLeg: t=0.0 (后) → t=0.5 (前) → t=1.0 (后)2 动画压缩
关键帧数据原始量很大——60 根骨头 × 每秒 30 帧 × 每帧位置+旋转+缩放。UE 的做法:
- 移除冗余关键帧(线性段中间帧去掉)
- 量化旋转(用 16-bit 整数替代 float 存四元数分量)
- 不同的骨骼用不同的压缩率(手指骨骼可以比脊柱压缩更多——它们移动幅度大但视觉上不明显)
3 Animation Sequence(UE 中的动画资产)
UAnimSequence 存储了动画数据。它不存完整的骨骼矩阵——只存相对于参考姿势的变化。运行时,AnimBlueprint 把多个 AnimSequence 混合(Blend)、叠加(Layered Blend)、IK 修正——产生最终的骨骼姿势。
五、动画状态机与 BlendSpace
1 动画状态机(State Machine)
在 UE 的 AnimBlueprint 里,状态机决定”当前在播哪个动画”:
[Idle] ──(速度>0)──→ [Walk] ──(速度>阈值)──→ [Run] ↑ │ │ └──(速度=0)──────────┴──(速度降低)───────────┘状态之间的过渡(Transition)有 Blend Duration——0.2 秒内从 Walk 淡入 Run,避免动作突变。
2 BlendSpace
不是离散的”Walk 是 300 速度,Run 是 600 速度”,而是连续的空间:
BlendSpace_1D(横轴 = Speed): 速度=0 → Idle 速度=200 → Walk 速度=400 → Jog 速度=600 → Sprint
速度=300 → Walk(50%) + Jog(50%) ← 引擎自动混合两个动画BlendSpace 让动作之间的过渡变得自然——玩家从走到跑到冲刺,动画混合是连续的,看不出来”切换点”。
六、UE 的动画管线总览
AnimSequence (动画数据资产) ↓AnimBlueprint (动画逻辑) ├── EventGraph → 从 Gameplay 获取速度、方向、是否在空中的状态 ├── AnimGraph (状态机 + BlendSpace + IK) │ ├── BlendSpace 根据速度混合 Walk/Run │ ├── Layered Blend 叠加上半身的攻击动画 │ └── IK 修正脚部位置(贴合地面) └── 输出:每帧的骨骼姿势(Pose) ↓ SkeletalMeshComponent::TickComponent ↓ GPU Skinning (Compute Shader 或 Vertex Shader 做蒙皮) ↓ 渲染关键:AnimBlueprint 在 CPU 上算出每根骨骼的 Transform,然后把最终的骨骼矩阵数组(BoneMatrices)上传到 GPU。GPU 的 Vertex Shader 拿这些矩阵对每个顶点做蒙皮。
七、总结
| 概念 | 一句话 |
|---|---|
| 骨骼 | 层次化的”骨头”,每根存相对于父骨的 Transform |
| 蒙皮 | 每个顶点被最多 4 根骨头加权影响——Weight 和为 1.0 |
| LBS | 线性混合蒙皮——标准做法,关节处有糖纸效应 |
| 关键帧 | 动画只存关键帧,引擎做插值 |
| 状态机 | 决定”什么时候播哪个动画” |
| BlendSpace | 根据参数(速度、方向)连续混合多个动画 |
| AnimBlueprint | UE 的动画逻辑层,输出每帧的骨骼姿势 → GPU 蒙皮 |
骨骼动画是图形学和 Gameplay 的交叉地带——渲染侧关心蒙皮矩阵怎么高效上传和计算,Gameplay 侧关心状态机和 BlendSpace 怎么组织。理解两端,才能搞定”角色为什么不动”或”角色为什么动错了”的 bug。