下面我将详细讲解Unity引擎骨骼动画的实现原理,并给出核心代码逻辑实现(伪代码和C#示例),让你既理解底层原理,也能动手实现一个简化版的骨骼动画系统。
一、Unity骨骼动画实现原理
1. 基本概念
- 骨骼(Skeleton/Bone):一组层级结构的Transform节点(通常是GameObject),每个节点代表一根骨头。
- 蒙皮网格(Skinned Mesh):网格的每个顶点都绑定到一组骨骼,并有权重。
- 动画数据(AnimationClip):记录每根骨骼在每一帧的变换(位置、旋转、缩放)曲线。
- 动画采样:在某一时刻,根据动画曲线计算每根骨骼的变换,并应用到骨骼Transform上。
- 蒙皮变形:根据骨骼的变换,实时计算网格顶点的新位置,实现角色的动态变形。
2. 工作流程
- 动画采样:每帧根据当前时间,从AnimationClip中采样出每根骨骼的变换。
- 骨骼变换应用:将采样结果应用到骨骼Transform(或矩阵)。
- 蒙皮计算:用骨骼的变换矩阵驱动网格顶点,计算最终顶点位置。
- 渲染:用变形后的顶点渲染角色。
3. Unity的实现要点
- Unity的Animator/Animation系统自动完成上述流程。
- SkinnedMeshRenderer组件负责蒙皮变形,支持CPU和GPU两种方式。
- 动画采样和骨骼变换在CPU完成,蒙皮变形可在CPU或GPU完成。
二、简化版骨骼动画系统代码实现
下面用C#伪代码和Unity风格代码,演示一个简化版的骨骼动画系统(不依赖Animator/Animation组件)。
1. 数据结构定义
// 骨骼节点
class Bone
{
public string name;
public Bone parent;
public List<Bone> children = new List<Bone>();
public Matrix4x4 localMatrix; // 本地变换
public Matrix4x4 worldMatrix; // 世界变换
public Matrix4x4 bindPose; // 绑定姿势逆矩阵
}
// 动画关键帧
class Keyframe
{
public float time;
public Vector3 position;
public Quaternion rotation;
public Vector3 scale;
}
// 每根骨骼的动画曲线
class BoneAnimationCurve
{
public string boneName;
public List<Keyframe> keyframes;
}
// 动画片段
class AnimationClip
{
public List<BoneAnimationCurve> boneCurves;
public float length;
}
2. 动画采样与骨骼变换应用
// 采样某一时刻的骨骼动画
void SampleAnimation(AnimationClip clip, List<Bone> skeleton, float time)
{
foreach (var curve in clip.boneCurves)
{
Bone bone = FindBoneByName(skeleton, curve.boneName);
if (bone == null) continue;
// 查找当前时间点的关键帧区间
Keyframe k0, k1 = FindKeyframes(curve.keyframes, time);
float t = (time - k0.time) / (k1.time - k0.time);
// 插值计算
Vector3 pos = Vector3.Lerp(k0.position, k1.position, t);
Quaternion rot = Quaternion.Slerp(k0.rotation, k1.rotation, t);
Vector3 scale = Vector3.Lerp(k0.scale, k1.scale, t);
// 生成本地变换矩阵
bone.localMatrix = Matrix4x4.TRS(pos, rot, scale);
}
// 递归更新世界矩阵
UpdateBoneWorldMatrix(skeleton[0], Matrix4x4.identity);
}
// 递归更新世界矩阵
void UpdateBoneWorldMatrix(Bone bone, Matrix4x4 parentMatrix)
{
bone.worldMatrix = parentMatrix * bone.localMatrix;
foreach (var child in bone.children)
UpdateBoneWorldMatrix(child, bone.worldMatrix);
}
3. 蒙皮变形(CPU版)
// 顶点数据
class SkinnedVertex
{
public Vector3 position;
public int[] boneIndices; // 影响此顶点的骨骼索引
public float[] weights; // 对应权重
}
// 蒙皮变形
void SkinMesh(List<SkinnedVertex> vertices, List<Bone> skeleton, out Vector3[] skinnedPositions)
{
skinnedPositions = new Vector3[vertices.Count];
for (int i = 0; i < vertices.Count; ++i)
{
Vector3 skinnedPos = Vector3.zero;
for (int j = 0; j < vertices[i].boneIndices.Length; ++j)
{
int boneIdx = vertices[i].boneIndices[j];
float weight = vertices[i].weights[j];
Bone bone = skeleton[boneIdx];
// 变换:骨骼世界矩阵 * 绑定姿势逆矩阵 * 原始顶点
Matrix4x4 skinMatrix = bone.worldMatrix * bone.bindPose;
skinnedPos += (skinMatrix.MultiplyPoint3x4(vertices[i].position)) * weight;
}
skinnedPositions[i] = skinnedPos;
}
}
三、Unity骨骼动画系统的核心流程总结
- 动画采样:AnimationClip.SampleAnimation/Animator采样动画曲线,计算每根骨骼的本地变换。
- 骨骼变换:递归计算每根骨骼的世界矩阵。
- 蒙皮变形:SkinnedMeshRenderer用骨骼矩阵变换顶点,实现网格变形。
- 渲染:用变形后的顶点渲染角色。
四、Unity实际用法(参考)
// Unity中,骨骼动画流程通常如下:
Animator animator = GetComponent<Animator>();
// Animator自动采样动画,驱动骨骼Transform
// SkinnedMeshRenderer自动用骨骼Transform驱动网格变形
五、参考资料
下面我将继续深入,补充Unity骨骼动画系统的底层实现细节、GPU蒙皮原理、性能优化、以及更完整的代码示例。内容分为以下几个部分:
一、Unity骨骼动画底层实现细节
1. 动画数据的组织
- Unity的AnimationClip内部,每根骨骼的每个属性(位置、旋转、缩放)都对应一条曲线(AnimationCurve)。
- 曲线数据在内存中是紧凑存储的,采样时通过二分查找定位关键帧,然后插值。
2. 骨骼Transform的驱动
- Animator/Animation组件每帧采样动画曲线,直接写入骨骼GameObject的Transform(localPosition、localRotation、localScale)。
- Unity会自动递归刷新Transform的世界矩阵,保证骨骼层级的正确性。
3. SkinnedMeshRenderer的工作
- SkinnedMeshRenderer组件会在渲染前,收集所有骨骼Transform的世界矩阵,和每根骨骼的bindPose(绑定姿势逆矩阵)相乘,得到最终的骨骼变换矩阵(skin matrix)。
- 这些矩阵会传递给CPU或GPU,用于顶点变形。
二、GPU蒙皮原理
1. 为什么用GPU蒙皮
- CPU蒙皮在顶点数和骨骼数较多时,性能瓶颈明显。
- GPU蒙皮将顶点变形计算放到顶点着色器,大幅提升效率。
2. 实现流程
- 骨骼矩阵上传:每帧将所有骨骼的skin matrix打包成数组,作为Uniform上传到GPU。
- 顶点数据:每个顶点存储最多4个骨骼索引和对应权重。
- 顶点着色器:在Shader中,按权重混合骨骼矩阵,变换顶点位置。
3. Shader伪代码
// 顶点着色器伪代码
float4 skinnedPos = float4(0,0,0,0);
for (int i = 0; i < 4; ++i)
{
int boneIdx = boneIndices[i];
float weight = boneWeights[i];
float4x4 boneMatrix = _BoneMatrices[boneIdx];
skinnedPos += mul(boneMatrix, float4(vertexPos, 1.0)) * weight;
}
三、性能优化建议
- 减少骨骼数量:只保留必要的骨骼,合并不动的骨骼。
- 减少顶点影响骨骼数:通常限制为4个骨骼/顶点,超过则归一化前4大权重。
- 动画压缩:导出动画时,移除冗余关键帧,压缩曲线精度。
- Animator Culling:不可见或远距离角色关闭Animator,减少采样和Transform刷新。
- 批量动画(DOTS Animation):大规模NPC用DOTS Animation或GPU Instancing。
四、完整代码示例(简化版)
1. 骨骼动画采样与蒙皮(C#伪代码)
// 假设有骨骼树skeleton,动画片段clip,网格顶点vertices
float time = ...; // 当前动画时间
// 1. 采样动画,更新骨骼本地变换
SampleAnimation(clip, skeleton, time);
// 2. 递归更新骨骼世界矩阵
UpdateBoneWorldMatrix(skeleton[0], Matrix4x4.identity);
// 3. 蒙皮变形
Vector3[] skinnedPositions;
SkinMesh(vertices, skeleton, out skinnedPositions);
// 4. 更新Mesh顶点
mesh.vertices = skinnedPositions;
mesh.RecalculateNormals();
2. GPU蒙皮Shader(Unity HLSL片段)
// 属性
uniform float4x4 _BoneMatrices[64]; // 假设最多64根骨骼
struct appdata
{
float4 vertex : POSITION;
float4 boneWeights : BLENDWEIGHT;
uint4 boneIndices : BLENDINDICES;
};
struct v2f
{
float4 pos : SV_POSITION;
};
v2f vert(appdata v)
{
float4 skinnedPos = float4(0,0,0,0);
for (int i = 0; i < 4; ++i)
{
int idx = v.boneIndices[i];
float w = v.boneWeights[i];
skinnedPos += mul(_BoneMatrices[idx], v.vertex) * w;
}
v2f o;
o.pos = UnityObjectToClipPos(skinnedPos);
return o;
}
五、Unity骨骼动画系统的完整流程图
[AnimationClip曲线数据]
│
▼
[采样动画曲线]
│
▼
[写入骨骼Transform]
│
▼
[递归刷新骨骼世界矩阵]
│
▼
[SkinnedMeshRenderer收集骨骼矩阵]
│
▼
[CPU或GPU蒙皮变形]
│
▼
[渲染]
六、进阶话题
- 动画融合(Blending):多个动画Clip按权重混合,采样后插值骨骼变换。
- 动画遮罩(Mask):只影响部分骨骼(如只播放上半身动画)。
- 动画事件:在特定帧触发脚本逻辑。
- 物理与动画混合(Ragdoll/IK):动画驱动骨骼,物理或IK可覆盖部分骨骼。
七、参考资料
下面我将继续深入,补充以下内容:
- 动画融合(Blending)与遮罩(Mask)原理与实现
- 骨骼动画与物理/IK混合(如Ragdoll、Foot IK)
- Unity源码级流程梳理(Animator/SkinnedMeshRenderer)
- 进阶优化与实际开发建议
1. 动画融合(Blending)与遮罩(Mask)
1.1 动画融合原理
- 动画融合是指将多个动画片段(如“跑步”“挥手”)按权重混合,得到最终骨骼变换。
- 融合通常在同一骨骼树上进行,按权重对每根骨骼的本地变换(位置、旋转、缩放)插值。
- 旋转插值用Slerp,位置/缩放用Lerp。
伪代码示例
// 假设有两个动画clipA, clipB,融合权重weightA, weightB
foreach (var bone in skeleton)
{
// 采样两个动画
TransformData tA = SampleBone(clipA, bone, timeA);
TransformData tB = SampleBone(clipB, bone, timeB);
// 融合
Vector3 pos = Vector3.Lerp(tA.position, tB.position, weightB);
Quaternion rot = Quaternion.Slerp(tA.rotation, tB.rotation, weightB);
Vector3 scale = Vector3.Lerp(tA.scale, tB.scale, weightB);
bone.localMatrix = Matrix4x4.TRS(pos, rot, scale);
}
1.2 动画遮罩原理
- 动画遮罩允许只对部分骨骼应用某个动画(如只让上半身挥手,下半身继续跑步)。
- 遮罩本质是为每根骨骼分配一个权重(0~1),融合时只对被遮罩的骨骼插值。
伪代码示例
foreach (var bone in skeleton)
{
float mask = GetMaskWeight(bone); // 0=不受影响,1=完全受影响
TransformData tA = SampleBone(clipA, bone, timeA);
TransformData tB = SampleBone(clipB, bone, timeB);
Vector3 pos = Vector3.Lerp(tA.position, tB.position, mask);
Quaternion rot = Quaternion.Slerp(tA.rotation, tB.rotation, mask);
Vector3 scale = Vector3.Lerp(tA.scale, tB.scale, mask);
bone.localMatrix = Matrix4x4.TRS(pos, rot, scale);
}
1.3 Unity中的实现
- Animator Controller的Blend Tree、Layer、Avatar Mask等功能,底层就是上述原理。
- 每一层可以有自己的遮罩和权重,最终递归融合。
2. 骨骼动画与物理/IK混合
2.1 动画与物理(Ragdoll)混合
- 动画驱动骨骼,物理(如Ragdoll)可在特定时刻接管骨骼。
- 混合时,通常对部分骨骼(如脊柱、四肢)用物理结果,其他用动画。
伪代码
foreach (var bone in skeleton)
{
if (IsRagdollBone(bone))
bone.localMatrix = ragdollResult[bone];
else
bone.localMatrix = animationResult[bone];
}
2.2 动画与IK混合(如Foot IK)
- 动画采样后,可用IK算法(如CCD、FABRIK)调整部分骨骼(如脚、手)以适应地形或目标。
- Unity的Animator支持Foot IK,底层就是在动画采样后对脚部骨骼做IK修正。
3. Unity源码级流程梳理
3.1 Animator/Animation系统
- Animator/Animation每帧会:
- 采样所有活跃动画片段(Clip),得到每根骨骼的本地变换。
- 按Blend Tree、Layer、Mask等规则融合变换。
- 写入骨骼GameObject的Transform。
- 递归刷新Transform的世界矩阵。
3.2 SkinnedMeshRenderer
- SkinnedMeshRenderer在渲染前:
- 收集所有骨骼Transform的worldMatrix。
- 乘以bindPose,得到skin matrix。
- 传递给CPU或GPU做蒙皮变形。
3.3 相关源码入口(C++/C#)
- Animator/Animation:
Animator::Update
,Animation::Sample
- SkinnedMeshRenderer:
SkinnedMeshRenderer::UpdateSkinning
- Unity C# API:
AnimationClip.SampleAnimation
,Animator.Play
,SkinnedMeshRenderer.bones
4. 进阶优化与开发建议
- 动画融合优化:只融合活跃骨骼,避免全骨骼全插值。
- 遮罩优化:遮罩权重为0的骨骼直接跳过插值。
- GPU蒙皮:大部分项目建议开启GPU蒙皮,提升性能。
- 骨骼重用:同一骨骼树可被多个SkinnedMeshRenderer复用(如换装)。
- 动画事件与同步:事件触发要注意与动画帧同步,避免延迟。
- 大规模角色:用DOTS Animation或自研批量动画系统。
5. 参考资料与推荐阅读
下面我将继续深入,补充以下内容:
1. 动画融合(Blend Tree)底层实现原理
1.1 Blend Tree的本质
Blend Tree 是 Unity 动画系统中用于实现多动画平滑过渡的结构。其本质是多动画Clip的加权融合,权重由参数(如速度、方向、手势等)动态控制。
- 一维Blend Tree:如“走-跑”之间的平滑过渡,权重由速度参数决定。
- 二维Blend Tree:如“前后左右移动”融合,权重由两个参数(如水平、垂直速度)决定。
1.2 Blend Tree融合算法
Blend Tree会根据参数,计算每个子动画的权重,然后对每根骨骼的本地变换做加权融合。
伪代码:
// 假设有N个动画Clip,每个有权重weights[i]
foreach (var bone in skeleton)
{
Vector3 pos = Vector3.zero;
Quaternion rot = Quaternion.identity;
Vector3 scale = Vector3.zero;
float totalWeight = 0f;
for (int i = 0; i < N; ++i)
{
TransformData t = SampleBone(clip[i], bone, time[i]);
float w = weights[i];
pos += t.position * w;
scale += t.scale * w;
if (i == 0)
rot = t.rotation;
else
rot = Quaternion.Slerp(rot, t.rotation, w / (totalWeight + w));
totalWeight += w;
}
pos /= totalWeight;
scale /= totalWeight;
bone.localMatrix = Matrix4x4.TRS(pos, rot, scale);
}
注意:旋转的加权融合不能直接线性插值,通常用分步Slerp或四元数插值加权算法。
2. IK算法(如Foot IK)原理与代码
2.1 IK(逆向运动学)简介
- 正向运动学(FK):已知每个关节的角度,计算末端位置。
- 逆向运动学(IK):已知末端目标位置,反推每个关节的角度。
2.2 常用IK算法
- CCD(Cyclic Coordinate Descent):迭代调整每个关节,使末端靠近目标。
- FABRIK(Forward And Backward Reaching IK):正反两次迭代,收敛更快。
2.3 CCD IK伪代码(2D/3D通用)
void CCD_IK(List<Bone> chain, Vector3 target, int maxIter = 10, float threshold = 0.01f)
{
for (int iter = 0; iter < maxIter; ++iter)
{
for (int i = chain.Count - 2; i >= 0; --i)
{
Vector3 toEnd = chain.Last().position - chain[i].position;
Vector3 toTarget = target - chain[i].position;
float angle = Vector3.Angle(toEnd, toTarget);
Vector3 axis = Vector3.Cross(toEnd, toTarget).normalized;
Quaternion rot = Quaternion.AngleAxis(angle, axis);
chain[i].rotation = rot * chain[i].rotation;
// 更新后续骨骼的位置
UpdateForward(chain, i);
}
if ((chain.Last().position - target).magnitude < threshold)
break;
}
}
实际应用中,Unity的Animator可以自动处理Foot IK,但自定义IK可用于手部、触手等特殊需求。
3. Unity骨骼动画系统的源码级流程(简要)
3.1 Animator/Animation
- Animator::Update(C++):采样所有活跃动画,融合,写入Transform。
- AnimationClip::SampleAnimation(C#):采样单个Clip,写入Transform。
- Blend Tree/Layer/Mask:C++层实现,融合时递归遍历骨骼树,按权重插值。
3.2 SkinnedMeshRenderer
- SkinnedMeshRenderer::UpdateSkinning(C++):收集骨骼矩阵,传递给CPU/GPU。
- 骨骼矩阵上传:
SkinnedMeshRenderer.bones
(C#)返回Transform数组,sharedMesh.bindposes
返回bindPose数组。
3.3 Shader蒙皮
- Unity内置Standard/URP/HDRP Shader都支持GPU蒙皮,骨骼矩阵通过Uniform数组传递。
4. 实际开发中的高级技巧
4.1 动画融合优化
- 只融合活跃骨骼:如Blend Tree中,未被遮罩的骨骼可跳过插值。
- 分层融合:如上半身/下半身分层,减少不必要的融合计算。
4.2 动画压缩与重定向
- 动画压缩:导出时减少关键帧、量化曲线,减小内存和IO压力。
- 动画重定向(Retargeting):不同骨骼结构间动画复用,Unity用Avatar实现。
4.3 动画事件与同步
- 动画事件:在AnimationClip中插入事件,运行时自动回调C#方法。
- 同步问题:多角色同步动画时,需统一采样时间,避免帧差。
4.4 DOTS Animation与大规模角色
- DOTS Animation:基于ECS的批量动画系统,适合大规模NPC。
- 自研批量动画:可用Job System+Burst并行采样和蒙皮。
5. 参考资料与推荐阅读
- Unity官方文档:Blend Tree
- Unity官方文档:Inverse Kinematics
- Unity官方文档:DOTS Animation
- Unity官方文档:动画事件
- Unity官方文档:Avatar与Retargeting
- IK算法原理与实现