返回文章列表

骨骼动画渲染管线深度分析

2026-07-23

开始写作..# 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 在三个层面实现并行:

  1. 组件级并行:通过 Task Graph(FParallelAnimationEvaluationTask)将不同 SkeletalMeshComponent 的动画求值分发到工作线程。默认开启(a.ParallelAnimEvaluation=1)。

  2. 单骨骼 SIMD:使用 ISPC(Intel SPDP Program Compiler)对单个骨骼的 FTransform::Multiply 进行 SIMD 向量化。虽然骨骼间是串行的,但每根骨骼内部的四元数乘法、向量旋转等运算是 SIMD 的。

  3. 顶点级并行(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 的核心算法:

  1. 寻找连通面片壳:从某个三角形出发,找到所有共享顶点的连通三角形集合。
  2. 合并相同骨骼的壳:如果两个壳的骨骼集合合并后不超过 MaxBonesPerChunk,则合并。
  3. 拆分超限的壳:如果骨骼数超过限制,创建新 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 时完成,因为:

  1. 拆分算法复杂ChunkSkinnedVertices 涉及连通面片搜索,计算量大,不适合在运行时做。
  2. 平台相关:不同平台的 MaxGPUSkinBones 不同,需要按目标平台 Cook 出不同的 Section 配置。
  3. 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 中完成骨骼蒙皮
GPUSkinCacheCompute 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 已经在多个层面对缩放进行了优化:

  1. 动画压缩ShouldCompressScale() 检测缩放是否等于绑定姿态,如果是则不存储。FilterTrivialScaleKeys() 过滤恒定缩放关键帧。

  2. 压缩编码ACF_Identity 编码模式下,恒定缩放占用 0 字节。

  3. 运行时提取:如果 ScaleKeys.Num() == 0,提取变换时使用默认缩放 (1,1,1),跳过缩放插值。

9.2 去除缩放的性能收益分析

结论:在缩放为 (1,1,1) 的正常情况下,去除缩放的收益极小。

原因:

  1. FTransform 固定大小:不论是否有缩放,FTransform 都是 48 字节,内存布局不变。

  2. 乘法开销极小FTransform::Multiply 的快速路径中,缩放只有 2 条 SIMD 指令:

    // 快速路径中的缩放计算
    OutTransform->Scale3D = VectorMultiply(ScaleA, ScaleB);  // 1 条 SIMD 乘法
    OutTransform->Translation = VectorAdd(
        VectorQuaternionRotateVector(QuatB, VectorMultiply(TranslateA, ScaleB)),  // ScaleB 参与
        TranslateB
    );
    
  3. GPU 骨骼矩阵不变:骨骼上传到 GPU 的 FMatrix3x4(48 字节)包含缩放分量,不论缩放是否为 (1,1,1)。

  4. 顶点着色器不变:顶点着色器的矩阵乘法是固定的,不会因为缩放为 (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(FGPUMorphAccumulateCSFGPUMorphNormalizeCS)计算顶点偏移量,写入 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
AnimDynamicsTG_PrePhysics骨骼 CS上游 Pose修改后的 CS Pose
FillComponentSpaceTransformsTG_PrePhysics骨骼 CS→LocalCS PoseLocal 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.cppFillComponentSpaceTransforms
Engine/Source/Runtime/Engine/Private/SkinnedAsset.ispcISPC SIMD 骨骼乘法
Engine/Source/Runtime/AnimGraphRuntime/Public/BoneControllers/AnimNode_SkeletalControlBase.hSkeletalControl 基类定义
Engine/Source/Runtime/AnimGraphRuntime/Private/BoneControllers/AnimNode_SkeletalControlBase.cppIK 执行流程、LocalBlendCSBoneTransforms
Engine/Source/Runtime/Engine/Public/Rendering/SkeletalMeshLODRenderData.hFSkelMeshRenderSection 定义
Engine/Source/Developer/MeshUtilities/Private/SkeletalMeshTools.cppChunkSkinnedVertices 骨骼拆分算法
Engine/Source/Developer/MeshUtilities/Private/MeshUtilities.cppSection 创建
Engine/Source/Runtime/Engine/Private/SkeletalMeshRenderData.cppGetMaxBonesPerSection、RequiresCPUSkinning
Engine/Source/Runtime/Engine/Private/GPUSkinVertexFactory.cppGetMaxGPUSkinBones
Engine/Source/Runtime/Engine/Private/SkeletalRenderGPUSkin.cppGPU 蒙皮技术选择、骨骼/布料数据上传
Engine/Source/Runtime/Engine/Private/SkeletalRenderCPUSkin.cppCPU 蒙皮路径
Engine/Source/Runtime/Engine/Private/GPUSkinCache.cppSkin Cache Compute Shader
Engine/Source/Runtime/Engine/Private/PhysicsEngine/PhysAnim.cpp物理混合 (PerformBlendPhysicsBones)
Engine/Source/Runtime/Engine/Private/SkeletalMeshComponentPhysics.cppClothTick、EndPhysicsTick
Engine/Source/Runtime/Core/Public/Math/TransformVectorized.hFTransform 定义、VQS 乘法
Engine/Source/Runtime/Core/Private/Math/TransformVectorized.cppFTransform::Multiply 实现、负缩放检查
Engine/Source/Runtime/Engine/Private/Animation/AnimCompress.cpp动画压缩、缩放过滤
Engine/Source/Runtime/Engine/Private/Animation/AnimationUtils.cppExtractTransformFromTrack
Engine/Config/BaseEngine.iniMaxSkinBones 配置
.