骨骼动画渲染管线深度分析
开始写作..# UE5 骨骼动画渲染管线深度分析
一、概述
本文梳理了 Unreal Engine 5.4 骨骼动画渲染管线的完整流程,从动画数据存储、骨骼驱动顶点的数学原理、骨骼优化体系、Section 概念、GPU 蒙皮技术,到 IK / 物理混合 / 布料模拟在管线中的作用环节,形成一个完整的知识体系。
二、骨骼如何驱动顶点
2.1 ReferenceToLocal 矩阵
骨骼驱动顶点的核心是一个称为 ReferenceToLocal 的矩阵数组。每根骨骼在每一帧都会计算一个变换矩阵,顶点通过骨骼索引查找这个矩阵来完成形变。
最终顶点位置 = Σ (骨骼矩阵[bone_idx_i] × 静态顶点位置 × 权重_i)
其中"ReferenceToLocal"的含义是:将骨骼从**参考姿态(绑定姿态,Bind Pose)变换到当前局部空间(Local Space)**的矩阵。
2.2 变换空间的层级
UE5 的骨骼变换存在于多个空间中:
| 空间 | 含义 | 存储位置 |
|---|---|---|
| BoneSpace(局部空间) | 相对于父骨骼的变换 | 动画文件存储的就是这个 |
| ComponentSpace(组件空间) | 相对于骨骼根节点的变换 | 运行时通过层级乘法计算 |
| WorldSpace(世界空间) | 相对于场景的变换 | ComponentSpace × ComponentToWorld |
骨骼变换的层级关系决定了计算顺序:
BoneSpace[root] → ComponentSpace[root]
BoneSpace[child] × ComponentSpace[parent] → ComponentSpace[child]
2.3 动画数据存储
动画序列(AnimSequence)存储的是每根骨骼在 BoneSpace(局部空间) 下的变换:
struct FRawAnimSequenceTrack {
TArray<FVector3f> PosKeys; // 位置关键帧
TArray<FQuat4f> RotKeys; // 旋转关键帧
TArray<FVector3f> ScaleKeys; // 缩放关键帧(可为空)
};
ScaleKeys 为空时,提取变换时使用默认缩放 (1,1,1)。这意味着没有缩放的动画不会存储缩放数据,但运行时的 FTransform 结构体仍然是固定 48 字节。
2.4 FTransform 结构
FTransform 采用 VQS(Vector-Quaternion-Scale)表示法,固定 48 字节:
Rotation (FQuat) : 16 bytes (4×float, SIMD aligned)
Translation(FVector) : 16 bytes (4×float, SIMD aligned, w=unused)
Scale3D (FVector) : 16 bytes (4×float, SIMD aligned, w=unused)
Total : 48 bytes
不论动画是否包含缩放,运行时 FTransform 的大小始终是 48 字节。
三、骨骼优化体系——三级过滤
UE5 通过三级过滤来减少需要计算的骨骼数量:
3.1 第一级:ActiveBoneIndices(Section 级别)
每个 FSkelMeshRenderSection 持有一个 BoneMap,记录该 Section 的顶点实际引用了哪些骨骼。只有出现在某个 Section 的 BoneMap 中的骨骼才需要上传到 GPU 的骨骼缓冲区。
3.2 第二级:RequiredBones(LOD 级别)
ComputeRequiredBones() 根据 LOD 索引构建当前 LOD 需要的骨骼列表。低 LOD 会丢弃不重要的骨骼(如面部小骨骼),减少动画求值和层级乘法的计算量。
// 伪代码
RequiredBones = LOD.RequiredBones // LOD 配置的骨骼列表
∪ VirtualBones // 虚拟骨骼
∪ PhysicsBones // 物理体控制的骨骼
∪ SocketBones // Socket 挂载点的骨骼
EnsureParentsPresent(RequiredBones) // 确保父骨骼都在列表中
3.3 第三级:BoneMap(Section 级别,最细粒度)
在渲染时,每个 Section 只上传自己的 BoneMap 对应的骨骼矩阵到 GPU,而不是全部骨骼。这是骨骼优化最细的粒度。
全部骨骼 (可能 500+)
└─ RequiredBones (LOD 过滤后, 可能 200)
└─ ActiveBoneIndices (Section 激活, 可能 100)
└─ Section.BoneMap (单个 Section, 可能 30-80)
四、并行计算策略
4.1 骨骼层级乘法的串行性
骨骼的父子依赖关系使得 FillComponentSpaceTransforms 本质上是串行的——子骨骼的组件空间变换依赖于父骨骼的组件空间变换:
// SkinnedAsset.cpp - FillComponentSpaceTransforms
OutComponentSpaceTransforms[0] = InBoneSpaceTransforms[0]; // Root
for (int32 i = 1; i < RequiredBones.Num(); i++) {
int32 BoneIndex = RequiredBones[i];
int32 ParentIndex = GetRefSkeleton().GetParentIndex(BoneIndex);
FTransform::Multiply(
&OutComponentSpaceTransforms[i], // Out
&InBoneSpaceTransforms[i], // 当前骨骼 Local
&OutComponentSpaceTransforms[ParentIndex] // 父骨骼 CS
);
}
4.2 UE 的并行策略
UE 在三个层面实现并行:
-
组件级并行:通过 Task Graph(
FParallelAnimationEvaluationTask)将不同 SkeletalMeshComponent 的动画求值分发到工作线程。默认开启(a.ParallelAnimEvaluation=1)。 -
单骨骼 SIMD:使用 ISPC(Intel SPDP Program Compiler)对单个骨骼的
FTransform::Multiply进行 SIMD 向量化。虽然骨骼间是串行的,但每根骨骼内部的四元数乘法、向量旋转等运算是 SIMD 的。 -
顶点级并行(GPU):GPU 蒙皮完全并行——每个顶点独立计算,互不依赖。
4.3 并行任务流程
主线程: TickComponent
├─ 派发 FParallelAnimationEvaluationTask → 工作线程
│ ├─ Update_AnyThread (动画节点更新)
│ ├─ Evaluate_AnyThread (动画节点求值 + IK)
│ └─ FillComponentSpaceTransforms (骨骼层级乘法)
│
└─ FParallelAnimationCompletionTask → 主线程
├─ Swap buffers (交换双缓冲)
└─ PostAnimEvaluation (后处理)
五、Section 概念详解
5.1 什么是 Section
FSkelMeshRenderSection 是骨骼网格体 LOD 的基本渲染单元。一个 Section 包含:
struct FSkelMeshRenderSection {
uint16 MaterialIndex; // 使用的材质索引
uint32 BaseIndex; // 索引缓冲区偏移
uint32 NumTriangles; // 三角形数量
uint32 BaseVertexIndex; // 顶点缓冲区偏移
uint32 NumVertices; // 顶点数量
TArray<FBoneIndexType> BoneMap; // 该 Section 顶点引用的骨骼列表
int32 MaxBoneInfluences; // 每个顶点最大骨骼影响数
FClothingSectionData ClothingData; // 布料数据
// ...
};
一个 Section = 一次 Draw Call = 一个 VertexFactory + 一份骨骼缓冲区。
5.2 Section 的创建过程
Section 在 Cook / 构建阶段通过两步拆分创建:
原始网格数据
│
▼ 步骤1: 按材质拆分
│ 每个材质 ID 对应一个 Chunk
│ Chunk[0]: 材质0的所有顶点
│ Chunk[1]: 材质1的所有顶点
│ ...
│
▼ 步骤2: 按骨骼数量拆分 (ChunkSkinnedVertices)
│ 如果一个 Chunk 的骨骼数 > MaxGPUSkinBones
│ → 找到连通的面片壳 (polygon patch)
│ → 将骨骼数超限的 Chunk 拆分为多个子 Chunk
│ → 每个子 Chunk 的骨骼数 ≤ MaxGPUSkinBones
│
▼ 最终: 每个 Chunk → 一个 Section
ChunkSkinnedVertices 的核心算法:
- 寻找连通面片壳:从某个三角形出发,找到所有共享顶点的连通三角形集合。
- 合并相同骨骼的壳:如果两个壳的骨骼集合合并后不超过
MaxBonesPerChunk,则合并。 - 拆分超限的壳:如果骨骼数超过限制,创建新 Chunk,记录
ChunkedParentSectionIndex。
5.3 Section 与 Draw Call 的关系
LOD 0
├─ Section 0: 材质0, 骨骼{0,1,2,...,80} → Draw Call 1
├─ Section 1: 材质1, 骨骼{0,5,10,...,60} → Draw Call 2
├─ Section 2: 材质0, 骨骼{81,82,...,150} → Draw Call 3 (骨骼拆分)
└─ Section 3: 材质2, 骨骼{0,3,7,...,40} → Draw Call 4
Section 数量 = Draw Call 数量。减少 Section 数量是优化骨骼网格体渲染性能的关键。
六、MaxGPUSkinBones 与 MaxBonesPerChunk
6.1 两个值的区别
| 值 | 含义 | 来源 | 确认方式 |
|---|---|---|---|
| MaxGPUSkinBones | 平台 GPU 能力上限 | 项目设置 MaxSkinBones,按平台配置 | 从 URendererSettings 读取 |
| MaxBonesPerChunk | 网格实际数据中的最大骨骼数 | 遍历所有 Section 的 BoneMap.Num() 取最大值 | GetMaxBonesPerSection() |
6.2 MaxGPUSkinBones 的确认
配置在 BaseEngine.ini 中:
MaxSkinBones=(Default=65536, PerPlatform=(("Mobile", 256)))
运行时通过 GetMaxGPUSkinBones() 确认,经过多层校验:
GetMaxGPUSkinBones(TargetPlatform)
│
├── 1. 读取项目设置 MaxSkinBones
│ Desktop → 65536, Mobile → 256
│
├── 2. 如果有旧版 cvar Compat.MAX_GPUSKIN_BONES,取两者较小值
│
├── 3. 不能低于 MAX_TOTAL_INFLUENCES(=12)
│
└── 4. 如果不支持 16bit 骨骼索引 → 最多 256
硬件上限 GHardwareMaxGPUSkinBones = 65536(uint16 最大值)。
6.3 MaxBonesPerChunk 的确认
int32 FSkeletalMeshRenderData::GetMaxBonesPerSection(int32 MinLODIdx) const
{
int32 MaxBonesPerSection = 0;
for (int32 LODIndex = MinLODIdx; LODIndex < LODRenderData.Num(); ++LODIndex)
{
for (int32 SectionIndex = 0; SectionIndex < RenderSections.Num(); ++SectionIndex)
{
MaxBonesPerSection = FMath::Max(
MaxBonesPerSection,
RenderData.RenderSections[SectionIndex].BoneMap.Num()
);
}
}
return MaxBonesPerSection;
}
6.4 两者在不同阶段的关系
Cook 阶段:
MaxGPUSkinBones(platform) → 作为拆分阈值传给 ChunkSkinnedVertices
→ 保证每个 Section.BoneMap.Num() ≤ MaxGPUSkinBones
运行时:
MaxGPUSkinBones = GetMaxGPUSkinBones() // 当前平台
MaxBonesPerChunk = GetMaxBonesPerSection() // 网格数据
if (MaxBonesPerChunk > MaxGPUSkinBones)
→ CPU 蒙皮回退
else
→ GPU 蒙皮
正常情况下 Cook 完成后 MaxBonesPerChunk ≤ MaxGPUSkinBones,只有跨平台迁移(如桌面 Cook 的网格在手机上运行)才可能出现回退。
七、Cook 时 vs 运行时
7.1 为什么区分 Cook 和运行时
骨骼拆分(Section 创建)在 Cook 时完成,因为:
- 拆分算法复杂:
ChunkSkinnedVertices涉及连通面片搜索,计算量大,不适合在运行时做。 - 平台相关:不同平台的
MaxGPUSkinBones不同,需要按目标平台 Cook 出不同的 Section 配置。 - DDC 缓存:骨骼拆分结果缓存在 Derived Data Cache 中,DDC Key 包含
MaxGPUSkinBones。
7.2 CPU 蒙皮仍然使用 Cook 时的 Section
CPU 蒙皮路径同样使用 Cook 时创建的 Section 和 BoneMap:
// SkeletalRenderCPUSkin.cpp
const FBoneIndexType* BoneMap = Section.BoneMap.GetData();
const FMatrix44f BoneMatrix0 = ReferenceToLocal[BoneMap[BoneIndices[INFLUENCE_0]]];
// 逐顶点蒙皮...
CPU 蒙皮没有骨骼数量限制(理论上 MAX_int32),但仍然受限于 Cook 时按目标平台拆分的 Section 结构。
八、GPU 蒙皮技术
8.1 三种 GPU 蒙皮技术
UE5 定义了三种 GPU 蒙皮技术(ESkeletalMeshGPUSkinTechnique):
| 技术 | 实现方式 | 平台支持 | 说明 |
|---|---|---|---|
| Inline | 顶点着色器内联蒙皮 | 所有平台(含移动端) | 默认方式,每个顶点在 VS 中完成骨骼蒙皮 |
| GPUSkinCache | Compute Shader 预蒙皮 | SM5+(桌面端) | 先用 CS 将蒙皮结果写入缓冲区,VS 直接读取,避免重复蒙皮(阴影、GBuffer 等) |
| MeshDeformer | 自定义形变器 | SM5+ | 通过 Deformer Graph 自定义蒙皮逻辑 |
8.2 关键澄清:GPU 蒙皮 ≠ Compute Shader
一个常见误解是 GPU 蒸皮需要 Compute Shader。实际上:
- Inline 模式(默认,所有平台):骨骼蒙皮在顶点着色器中完成,不需要 Compute Shader。移动端 ES3.1 完全支持。
- GPUSkinCache 模式(可选,桌面端):使用 Compute Shader 预蒙皮到缓冲区,避免多 Pass(阴影、GBuffer)时的重复计算。移动端有回退逻辑(MALI 64K texel buffer 限制时回退到 Inline)。
- GPU Morph Target:需要 Compute Shader(SM5+),但有 CPU 回退路径。
Section 的骨骼拆分限制(MaxGPUSkinBones)是顶点着色器常量缓冲区的限制,与 Compute Shader 无关。
8.3 移动端 GPU 蒙皮
移动端默认使用 Inline 模式(顶点着色器蒙皮)。MaxGPUSkinBones 在移动端为 256,这意味着:
- 移动端 Section 更可能被拆分(骨骼数超过 256 就拆)
- 一个材质可能对应多个 Section(不是 1:1 关系)
- 顶点着色器中骨骼索引使用 uint8 存储(最多 256),除非支持 16bit 索引
// GetGPUSkinTechnique - 移动端回退逻辑
if (FeatureLevel == ERHIFeatureLevel::ES3_1) {
if (RenderData.LODRenderData[LODIndex].GetNumVertices() * 3 >= (64 * 1024))
GPUSkinTechnique = ESkeletalMeshGPUSkinTechnique::Inline; // 回退到顶点着色器
}
九、骨骼缩放的性能分析
9.1 缩放优化的已有机制
UE 已经在多个层面对缩放进行了优化:
-
动画压缩:
ShouldCompressScale()检测缩放是否等于绑定姿态,如果是则不存储。FilterTrivialScaleKeys()过滤恒定缩放关键帧。 -
压缩编码:
ACF_Identity编码模式下,恒定缩放占用 0 字节。 -
运行时提取:如果
ScaleKeys.Num() == 0,提取变换时使用默认缩放 (1,1,1),跳过缩放插值。
9.2 去除缩放的性能收益分析
结论:在缩放为 (1,1,1) 的正常情况下,去除缩放的收益极小。
原因:
-
FTransform 固定大小:不论是否有缩放,
FTransform都是 48 字节,内存布局不变。 -
乘法开销极小:
FTransform::Multiply的快速路径中,缩放只有 2 条 SIMD 指令:// 快速路径中的缩放计算 OutTransform->Scale3D = VectorMultiply(ScaleA, ScaleB); // 1 条 SIMD 乘法 OutTransform->Translation = VectorAdd( VectorQuaternionRotateVector(QuatB, VectorMultiply(TranslateA, ScaleB)), // ScaleB 参与 TranslateB ); -
GPU 骨骼矩阵不变:骨骼上传到 GPU 的
FMatrix3x4(48 字节)包含缩放分量,不论缩放是否为 (1,1,1)。 -
顶点着色器不变:顶点着色器的矩阵乘法是固定的,不会因为缩放为 (1,1,1) 而跳过。
9.3 真正的性能陷阱:负缩放
FTransform::Multiply 中有一个关键检查:
void FTransform::Multiply(TTransform* Out, const TTransform* A, const TTransform* B) {
if (Private_AnyHasNegativeScale(A->Scale3D, B->Scale3D)) {
// 慢速路径:矩阵乘法
MultiplyUsingMatrixWithScale(Out, A, B);
} else {
// 快速路径:VQS 乘法
Out->Rotation = QuatB * QuatA;
Out->Scale3D = ScaleA * ScaleB;
Out->Translation = B.Rotate(ScaleB * A.Translation) + B.Translation;
}
}
负缩放会触发 MultiplyUsingMatrixWithScale,该函数将 FTransform 转换为 4×4 矩阵进行乘法,然后再提取出 Q/S/T,开销远大于 VQS 快速路径。
避免负缩放是比去除缩放更有意义的优化。
十、IK / 物理 / 布料在管线中的位置
10.1 完整管线时序
TG_PrePhysics TG_EndPhysics TG_PostPhysics 渲染线程
┌──────────────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐
│ PrimaryComponentTick │ │EndPhysicsTick│ │ ClothTick │ │ UpdateDynamic │
│ │ │ │ │ │ │ Data │
│ ① 动画求值 │──►│ ③ 物理混合 │──►│ ④ 布料模拟 │──►│ ⑤ 骨骼/布料/Morph│
│ (AnimGraph) │ │ │ │ │ │ 上传GPU │
│ ② IK/SkeletalControl │ │ │ │ │ │ ⑥ GPU 蒙皮渲染 │
└──────────────────────┘ └──────────────┘ └──────────────┘ └─────────────────┘
骨骼级 (Bone) 骨骼级 (Bone) 顶点级 (Vertex) GPU阶段
10.2 IK / SkeletalControl — 骨骼级操作
所有 IK(FABRIK、TwoBoneIK、HandIK、CCDIK 等)和 AnimDynamics 都是 FAnimNode_SkeletalControlBase 的子类,作用在 ComponentSpace(组件空间)Pose 上。
执行机制:
// AnimNode_SkeletalControlBase::EvaluateComponentSpace_AnyThread
void EvaluateComponentSpace_AnyThread(FComponentSpacePoseContext& Output) {
// 1. 执行上游节点(动画混合等)
EvaluateComponentPose_AnyThread(Output);
// 2. IK 实现计算要修改的骨骼变换
EvaluateSkeletalControl_AnyThread(Output, BoneTransforms);
// ↑ 各 IK 节点实现此虚函数,输出 TArray<FBoneTransform>
// 3. 按 Alpha 混合回 Pose
Output.Pose.LocalBlendCSBoneTransforms(BoneTransforms, BlendWeight);
// ↑ 直接修改 ComponentSpace Pose
}
- 输入:上游 AnimGraph 产生的 ComponentSpace Pose
- 输出:修改后的 ComponentSpace Pose(直接覆盖/混合特定骨骼的变换)
- 后续:修改后的 CS Pose 会被转换回 LocalSpace,存入
BoneSpaceTransforms
10.3 物理混合 — 骨骼级操作
在 TG_EndPhysics 阶段执行(EndPhysicsTickComponent),在动画求值之后:
EndPhysicsTickComponent
├─ SyncComponentToRBPhysics() // 同步动画结果到物理体
└─ BlendInPhysicsInternal() // 物理体结果混合回骨骼
├─ PerformBlendPhysicsBones (工作线程)
│ └─ 遍历骨骼,若有物理体控制该骨骼
│ → 用物理体结果覆盖骨骼变换
│ → 最终骨骼 = lerp(动画骨骼, 物理骨骼, PhysicsWeight)
└─ FinalizeBoneTransform (主线程)
典型场景:角色 ragdoll、死亡物理、物理驱动的附属物(如披风骨骼链)。
10.4 布料模拟 — 顶点级操作
布料是特殊的——它在 ClothTickFunction(TG_PrePhysics ~ TG_PostPhysics)中运行,在 IK + 物理混合之后:
TickClothing → UpdateClothStateAndSimulate
├─ 输入: BoneSpaceTransforms (已被 IK + 物理修改过的最终骨骼变换)
├─ 布料物理模拟 (Chaos/NvCloth)
└─ 输出: FClothSimulData
├─ Positions: TArray<FVector3f> (每个布料顶点的位置)
└─ Normals: TArray<FVector3f> (每个布料顶点的法线)
布料输出的是顶点级数据,不经过骨骼。在渲染时通过布料着色器数据和骨骼蒙皮结果混合:
// 有布料的顶点
skinned_pos = bone_matrix * static_pos; // 骨骼蒙皮结果
final_pos = lerp(skinned_pos, cloth_pos, cloth_blend_weight); // 和布料模拟结果混合
10.5 Morph Target — 顶点级操作
Morph Target 在渲染线程处理,有两种路径:
- GPU 路径(SM5+):使用 Compute Shader(
FGPUMorphAccumulateCS→FGPUMorphNormalizeCS)计算顶点偏移量,写入MorphVertexBuffer。 - CPU 路径:在 CPU 蒸皮时直接叠加 Morph 偏移量。
Morph 偏移量在顶点着色器中与骨骼蒙皮结果叠加:
final_pos = bone_matrix * (static_pos + morph_delta);
十一、骨骼级操作 vs 顶点级操作
11.1 分类总结
| 操作 | 作用阶段 | 数据级别 | 作用对象 | 输出 |
|---|---|---|---|---|
| 动画序列 / 混合 | TG_PrePhysics | 骨骼 Local | 动画数据 | Local Pose |
| IK (FABRIK 等) | TG_PrePhysics | 骨骼 CS | 上游 Pose | 修改后的 CS Pose |
| AnimDynamics | TG_PrePhysics | 骨骼 CS | 上游 Pose | 修改后的 CS Pose |
| FillComponentSpaceTransforms | TG_PrePhysics | 骨骼 CS→Local | CS Pose | Local Pose |
| 物理混合 | TG_EndPhysics | 骨骼 Local | 动画骨骼 + 物理体 | 混合后骨骼 |
| 布料模拟 | TG_PrePhysics~PostPhysics | 顶点级 | 最终骨骼变换 | 顶点位置/法线 |
| Morph Target | 渲染线程 | 顶点级 | 权重 + 静态顶点 | 顶点偏移量 |
| GPU 蒙皮 | 渲染线程 / GPU | 顶点级 | 骨骼矩阵 + 顶点 | 最终屏幕顶点 |
11.2 核心逻辑
骨骼级操作(IK、物理、AnimDynamics)
│
│ 修改 BoneSpaceTransforms
│
▼
顶点级操作(布料、Morph、GPU 蒙皮)
│
│ 使用骨骼变换作为输入
│
▼
最终渲染
骨骼是"因",顶点是"果"。所有骨骼级操作在游戏线程完成,修改 BoneSpaceTransforms;所有顶点级操作在渲染线程/GPU 完成,使用游戏线程传过来的骨骼数据作为输入。
时序保证:IK → 物理混合 → 布料 → 渲染,后者总能看到前者的结果。
唯一的例外是布料——它不经过骨骼,直接输出顶点位置,在着色器里和骨骼蒙皮结果做混合。这是因为布料的形变无法用骨骼来表达(一块布料可能有上千个模拟顶点,但只有几根骨骼驱动)。
十二、完整管线流程图
┌─────────────────────────────────────────────────────────────────────┐
│ 游戏线程 │
│ │
│ TG_PrePhysics │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ PrimaryComponentTick │ │
│ │ ├─ TickAnimation → TickAnimInstances │ │
│ │ └─ ParallelAnimationEvaluation (工作线程) │ │
│ │ ├─ Update_AnyThread (状态机/混合权重) │ │
│ │ ├─ Evaluate_AnyThread (动画节点树求值) │ │
│ │ │ ├─ Asset Player (动画序列播放) │ │
│ │ │ ├─ Blend Nodes (混合空间/层混合) │ │
│ │ │ ├─ State Machine (状态机) │ │
│ │ │ └─ SkeletalControl (IK/AnimDynamics) ← 骨骼CS │ │
│ │ ├─ FillComponentSpaceTransforms (骨骼层级乘法) │ │
│ │ │ └─ ISPC SIMD (单骨骼 FTransform::Multiply) │ │
│ │ └─ ParallelDuplicateAndInterpolate (URO 插值) │ │
│ │ │ │
│ │ CompleteParallelAnimationEvaluation (主线程) │ │
│ │ ├─ SwapBuffers │ │
│ │ └─ PostAnimEvaluation → PostUpdateAnimation │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ BoneSpaceTransforms (最终骨骼变换) │
│ TG_EndPhysics │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ EndPhysicsTickComponent │ │
│ │ ├─ SyncComponentToRBPhysics (动画→物理体) │ │
│ │ └─ BlendInPhysicsInternal (物理体→骨骼) ← 骨骼级 │ │
│ │ └─ PerformBlendPhysicsBones (并行) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ BoneSpaceTransforms (物理混合后) │
│ TG_PostPhysics │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ClothTick │ │
│ │ └─ TickClothing → UpdateClothStateAndSimulate │ │
│ │ ├─ 输入: BoneSpaceTransforms (最终骨骼) │ │
│ │ └─ 输出: FClothSimulData (顶点位置/法线) ← 顶点级 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ 创建 FDynamicSkelMeshObjectDataGPUSkin│
│ ├─ ReferenceToLocal (骨骼矩阵) │
│ ├─ MorphTargetWeights │
│ ├─ ClothingSimData (布料数据) │
│ └─ ActiveMorphTargets │
└─────────────────────────────────────────────────────────────────────┘
│
▼ 发送到渲染线程
┌─────────────────────────────────────────────────────────────────────┐
│ 渲染线程 / GPU │
│ │
│ UpdateDynamicData_RenderThread │
│ ├─ UpdateMorphVertexBuffer (GPU Compute / CPU) │
│ │ └─ FGPUMorphAccumulateCS → FGPUMorphNormalizeCS │
│ │ → MorphVertexBuffer (顶点偏移量) │
│ │ │
│ ├─ UpdateBufferData (每 Section) │
│ │ └─ UpdateBoneData(ReferenceToLocal, Section.BoneMap) │
│ │ → 骨骼矩阵 Buffer (FMatrix3x4 × BoneMap.Num()) │
│ │ │
│ ├─ ClothShaderData Setup (每 Section) │
│ │ └─ UpdateClothSimulationData(Positions, Normals) │
│ │ → 布料顶点 Buffer │
│ │ │
│ └─ Skin Cache (可选, Compute Shader) │
│ └─ 预蒙皮 → SkinCacheVertexBuffer │
│ │
│ GPU 渲染 │
│ ├─ 顶点着色器 (Inline 模式): │
│ │ final_pos = bone_matrix × (static_pos + morph_delta) │
│ │ if (has_cloth) final_pos = lerp(final_pos, cloth_pos, w) │
│ │ │
│ └─ 或从 SkinCache 直接读取预蒙皮结果 │
└─────────────────────────────────────────────────────────────────────┘
参考文件索引
| 文件 | 关键内容 |
|---|---|
Engine/Source/Runtime/Engine/Private/Components/SkeletalMeshComponent.cpp | 并行动画求值、TickGroup 配置、PostAnimEvaluation |
Engine/Source/Runtime/Engine/Private/SkinnedAsset.cpp | FillComponentSpaceTransforms |
Engine/Source/Runtime/Engine/Private/SkinnedAsset.ispc | ISPC SIMD 骨骼乘法 |
Engine/Source/Runtime/AnimGraphRuntime/Public/BoneControllers/AnimNode_SkeletalControlBase.h | SkeletalControl 基类定义 |
Engine/Source/Runtime/AnimGraphRuntime/Private/BoneControllers/AnimNode_SkeletalControlBase.cpp | IK 执行流程、LocalBlendCSBoneTransforms |
Engine/Source/Runtime/Engine/Public/Rendering/SkeletalMeshLODRenderData.h | FSkelMeshRenderSection 定义 |
Engine/Source/Developer/MeshUtilities/Private/SkeletalMeshTools.cpp | ChunkSkinnedVertices 骨骼拆分算法 |
Engine/Source/Developer/MeshUtilities/Private/MeshUtilities.cpp | Section 创建 |
Engine/Source/Runtime/Engine/Private/SkeletalMeshRenderData.cpp | GetMaxBonesPerSection、RequiresCPUSkinning |
Engine/Source/Runtime/Engine/Private/GPUSkinVertexFactory.cpp | GetMaxGPUSkinBones |
Engine/Source/Runtime/Engine/Private/SkeletalRenderGPUSkin.cpp | GPU 蒙皮技术选择、骨骼/布料数据上传 |
Engine/Source/Runtime/Engine/Private/SkeletalRenderCPUSkin.cpp | CPU 蒙皮路径 |
Engine/Source/Runtime/Engine/Private/GPUSkinCache.cpp | Skin Cache Compute Shader |
Engine/Source/Runtime/Engine/Private/PhysicsEngine/PhysAnim.cpp | 物理混合 (PerformBlendPhysicsBones) |
Engine/Source/Runtime/Engine/Private/SkeletalMeshComponentPhysics.cpp | ClothTick、EndPhysicsTick |
Engine/Source/Runtime/Core/Public/Math/TransformVectorized.h | FTransform 定义、VQS 乘法 |
Engine/Source/Runtime/Core/Private/Math/TransformVectorized.cpp | FTransform::Multiply 实现、负缩放检查 |
Engine/Source/Runtime/Engine/Private/Animation/AnimCompress.cpp | 动画压缩、缩放过滤 |
Engine/Source/Runtime/Engine/Private/Animation/AnimationUtils.cpp | ExtractTransformFromTrack |
Engine/Config/BaseEngine.ini | MaxSkinBones 配置 |
| . |