UE:论运行时动画录制的关键-正确获取骨骼数据与保存

© mengzhishanghun · 原创文章
首发于 博客园 · 禁止未经授权转载


核心问题

在 UE5.4 中实现运行时动画录制,最关键的两个问题是:

  1. 如何获取正确的骨骼数据 - 避免崩溃和数据不匹配
  2. 如何正确保存 AnimSequence - 使用引擎标准 API

本文直击核心,提供最简洁有效的解决方案。


一、获取正确的骨骼数据

核心原则:三个关键 API 的正确使用

// ✅ 正确:使用 SkeletalMesh 的 RefSkeleton
USkeletalMesh* SkeletalMesh = SkeletalMeshComponent->GetSkeletalMeshAsset();
const FReferenceSkeleton& RefSkeleton = SkeletalMesh->GetRefSkeleton();

// ✅ 正确:使用 GetRawBoneNum() 获取实际骨骼数
const int32 NumBones = RefSkeleton.GetRawBoneNum();

// ✅ 正确:运行时变换数据
const TArray<FTransform>& BoneSpaceTransforms = SkeletalMeshComponent->GetBoneSpaceTransforms();

错误示范与后果

错误代码 问题 后果
Skeleton->GetReferenceSkeleton() 包含 ControlRig 虚拟骨骼 崩溃:FKControlRig.cpp:126
RefSkeleton.GetNum() 返回全部骨骼(含虚拟) 数组越界
GetBoneNames() 后再遍历 RefSkeleton 骨骼名重复添加 数据混乱

关键 API 对比表

数据源 方法 返回值示例 是否正确
Skeleton->GetReferenceSkeleton().GetNum() 全部骨骼 161 (89实际+72虚拟)
SkeletalMesh->GetRefSkeleton().GetNum() 全部骨骼 161
SkeletalMesh->GetRefSkeleton().GetRawBoneNum() 实际骨骼 89
SkeletalMeshComponent->GetBoneSpaceTransforms().Num() 运行时变换 89 ✅ 用于对比验证

引擎源码依据

AnimSequence.cpp 第 2674-2678 行(官方实现)

const int32 NumBones = RefSkeleton.GetRawBoneNum();  // ← 引擎使用 GetRawBoneNum()
const TArray<FTransform> BoneSpaceTransforms = MeshComponent->GetBoneSpaceTransforms();

check(BoneSpaceTransforms.Num() >= NumBones);  // ← 引擎期望这个条件成立

完整初始化代码

void ATestActor::StartRecording(int _FrameSize, ACharacter* _TargetActor)
{
    TargetActor = _TargetActor;
    SkeletalMeshComponent = TargetActor->GetMesh();

    // 1. 获取正确的 RefSkeleton(SkeletalMesh 的,不是 Skeleton 的)
    USkeletalMesh* SkeletalMesh = SkeletalMeshComponent->GetSkeletalMeshAsset();
    const FReferenceSkeleton& RefSkeleton = SkeletalMesh->GetRefSkeleton();

    // 2. 使用 GetRawBoneNum() 获取实际骨骼数
    const int32 NumBones = RefSkeleton.GetRawBoneNum();

    // 3. 获取运行时变换数据(用于验证)
    const TArray<FTransform>& BoneSpaceTransforms = SkeletalMeshComponent->GetBoneSpaceTransforms();

    // 4. 安全性检查(引擎期望这个条件)
    if (BoneSpaceTransforms.Num() < NumBones)
    {
        UE_LOG(LogTemp, Error, TEXT("骨骼数据不匹配:BoneSpaceTransforms(%d) < RawBoneNum(%d)"),
            BoneSpaceTransforms.Num(), NumBones);
        return;
    }

    // 5. 填充骨骼名称(单次遍历)
    BoneNames.Empty();
    for (int32 BoneIndex = 0; BoneIndex < NumBones; ++BoneIndex)
    {
        BoneNames.Add(RefSkeleton.GetBoneName(BoneIndex));
    }

    // 6. 初始化数据数组
    TrackDataArray.SetNum(NumBones);
    IsRecording = true;
}

关键点说明

为什么必须用 SkeletalMesh 的 RefSkeleton?

// ❌ 错误:Skeleton 的 RefSkeleton 包含 ControlRig 虚拟骨骼
const FReferenceSkeleton& RefSkeleton =
    SkeletalMesh->GetSkeleton()->GetReferenceSkeleton();  // 161个骨骼

// ✅ 正确:SkeletalMesh 的 RefSkeleton 是实际骨骼层级
const FReferenceSkeleton& RefSkeleton =
    SkeletalMesh->GetRefSkeleton();  // 配合 GetRawBoneNum() 使用

Skeleton 中的虚拟骨骼示例

  • root_CONTROL - ControlRig 根控制器
  • ik_hand_l_CONTROL - IK 手部控制器
  • 这些骨骼在 BoneSpaceTransforms不存在

为什么必须用 GetRawBoneNum()?

// GetNum() vs GetRawBoneNum() 的区别
RefSkeleton.GetNum();         // 返回全部:RawBones + VirtualBones = 161
RefSkeleton.GetRawBoneNum();  // 返回实际:RawBones = 89

验证方法

UE_LOG(LogTemp, Warning, TEXT("RefSkeleton.GetNum() = %d"), RefSkeleton.GetNum());
UE_LOG(LogTemp, Warning, TEXT("RefSkeleton.GetRawBoneNum() = %d"), RefSkeleton.GetRawBoneNum());
UE_LOG(LogTemp, Warning, TEXT("BoneSpaceTransforms.Num() = %d"), BoneSpaceTransforms.Num());

// 正确结果应该是:
// GetNum() = 161
// GetRawBoneNum() = 89
// BoneSpaceTransforms.Num() = 89

二、正确保存 AnimSequence

核心原则:使用 IAnimationDataController

UE5 废弃了直接修改 AnimSequence 的方式,必须通过 IAnimationDataController 接口。

标准保存流程(5 个关键步骤)

void ATestActor::SpawnAnimAsset()
{
    // === 1. 创建资产对象 ===
    UPackage* Package = CreatePackage(*AssetPath);
    UAnimSequence* AnimSeq = NewObject<UAnimSequence>(Package, UAnimSequence::StaticClass(),
        *AssetName, RF_Public | RF_Standalone | RF_MarkAsRootSet);

    // 绑定 Skeleton
    USkeletalMesh* SkeletalMesh = SkeletalMeshComponent->GetSkeletalMeshAsset();
    AnimSeq->SetSkeleton(SkeletalMesh->GetSkeleton());

    // === 2. 获取 Controller(唯一修改接口)===
    IAnimationDataModel* Model = AnimSeq->GetDataModel();
    TScriptInterface<IAnimationDataController> Controller = Model->GetController();

    // === 3. 初始化模型(使用 FScopedBracket)===
    Controller->InitializeModel();
    IAnimationDataController::FScopedBracket ScopedBracket(Controller,
        LOCTEXT("RecordAnimation", "Recording Animation"));

    // === 4. 设置帧率和帧数 ===
    // 计算实际帧率(关键:不能用固定值)
    float RecordingDuration = GetWorld()->GetTimeSeconds() - RecordingStartTime;
    int32 NumFrames = TrackDataArray[0].PosKeys.Num();
    float ActualFrameRate = (float)NumFrames / RecordingDuration;
    ActualFrameRate = FMath::Clamp(ActualFrameRate, 24.0f, 120.0f);

    Controller->SetFrameRate(FFrameRate(FMath::RoundToInt(ActualFrameRate), 1));
    Controller->SetNumberOfFrames(NumFrames);

    // === 5. 添加骨骼轨道和关键帧 ===
    for (int32 BoneIndex = 0; BoneIndex < BoneNames.Num(); ++BoneIndex)
    {
        const FName& BoneName = BoneNames[BoneIndex];
        const FRawAnimSequenceTrack& Track = TrackDataArray[BoneIndex];

        // 5.1 添加骨骼曲线
        Controller->AddBoneCurve(BoneName);

        // 5.2 准备 Scale 数据(引擎要求 Pos/Rot/Scale 数量一致)
        TArray<FVector3f> ScaleKeys;
        ScaleKeys.Init(FVector3f(1.0f, 1.0f, 1.0f), Track.PosKeys.Num());

        // 5.3 设置关键帧
        Controller->SetBoneTrackKeys(BoneName, Track.PosKeys, Track.RotKeys, ScaleKeys);
    }

    // 5.4 通知数据填充完成(必须调用)
    Controller->NotifyPopulated();

    // === 6. 启用根骨骼运动(可选)===
    AnimSeq->bEnableRootMotion = true;
    AnimSeq->RootMotionRootLock = ERootMotionRootLock::RefPose;

    // === 7. 保存资产 ===
    FAssetRegistryModule::AssetCreated(AnimSeq);
    Package->MarkPackageDirty();

    FString PackageFileName = FPackageName::LongPackageNameToFilename(
        AssetPath, FPackageName::GetAssetPackageExtension());
    FSavePackageArgs SaveArgs;
    bool bSaved = UPackage::SavePackage(Package, AnimSeq, *PackageFileName, SaveArgs);
}

API 对比:UE4 vs UE5

功能 UE4(已弃用) UE5(标准)
添加轨道 AddNewRawTrack() Controller->AddBoneCurve()
设置关键帧 直接修改 RawAnimationData Controller->SetBoneTrackKeys()
作用域管理 OpenBracket() / CloseBracket() FScopedBracket (RAII)
完成通知 Controller->NotifyPopulated()

关键步骤详解

步骤 3:为什么使用 FScopedBracket?

// ❌ UE4 旧方式(容易忘记 CloseBracket)
Controller->OpenBracket(LOCTEXT("AddTracks", "Adding Tracks"));
// ... 添加数据
Controller->CloseBracket();  // 如果中途 return 会泄漏

// ✅ UE5 标准方式(RAII 自动管理)
{
    IAnimationDataController::FScopedBracket ScopedBracket(Controller, LOCTEXT(...));
    // ... 添加数据
}  // 离开作用域自动调用 CloseBracket

步骤 4:为什么不能用固定帧率?

// ❌ 错误:固定 30fps
Controller->SetFrameRate(FFrameRate(30, 1));
// 如果游戏运行在 60fps,录制的动画会以 2 倍速播放!

// ✅ 正确:动态计算
float ActualFrameRate = (float)NumFrames / RecordingDuration;
Controller->SetFrameRate(FFrameRate(FMath::RoundToInt(ActualFrameRate), 1));

步骤 5.4:NotifyPopulated() 的作用

Controller->NotifyPopulated();  // ← 必须调用!

作用

  • 通知引擎数据填充完成
  • 触发内部缓存更新
  • 验证数据完整性
  • 不调用会导致动画无法播放

三、完整代码示例

头文件 (TestActor.h)

#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "TestActor.generated.h"

UCLASS()
class TEST03_API ATestActor : public AActor
{
    GENERATED_BODY()

public:
    UFUNCTION(BlueprintCallable)
    void StartRecording(int _FrameSize, ACharacter* _TargetActor = nullptr);

    UFUNCTION(BlueprintCallable)
    void EndRecording();

private:
    virtual void Tick(float DeltaTime) override;
    void SpawnAnimAsset();

    UPROPERTY()
    ACharacter* TargetActor;

    UPROPERTY()
    USkeletalMeshComponent* SkeletalMeshComponent;

    TArray<FName> BoneNames;
    TArray<FRawAnimSequenceTrack> TrackDataArray;

    bool IsRecording = false;
    float RecordingStartTime;
    FTransform InitialActorTransform;
};

Tick 录制逻辑

void ATestActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    if (IsRecording)
    {
        const TArray<FTransform>& BoneSpaceTransforms = SkeletalMeshComponent->GetBoneSpaceTransforms();

        // 计算相对位移(用于根骨骼运动)
        FTransform CurrentActorTransform = TargetActor->GetActorTransform();
        FTransform RelativeActorTransform = CurrentActorTransform.GetRelativeTransform(InitialActorTransform);

        for (int i = 0; i < BoneNames.Num(); ++i)
        {
            FTransform FinalBoneTransform;

            if (i == 0)  // 根骨骼
            {
                // 烘焙世界位移到根骨骼
                FinalBoneTransform = BoneSpaceTransforms[0] * RelativeActorTransform;
            }
            else
            {
                FinalBoneTransform = BoneSpaceTransforms[i];
            }

            TrackDataArray[i].PosKeys.Add(FVector3f(FinalBoneTransform.GetLocation()));
            TrackDataArray[i].RotKeys.Add(FQuat4f(FinalBoneTransform.GetRotation()));
            TrackDataArray[i].ScaleKeys.Add(FVector3f(FinalBoneTransform.GetScale3D()));
        }
    }
}

四、核心技术点总结

骨骼数据获取(3 个必须)

步骤 API 错误会导致
1. 数据源 SkeletalMesh->GetRefSkeleton() 使用 Skeleton 会包含虚拟骨骼
2. 骨骼数 RefSkeleton.GetRawBoneNum() 使用 GetNum() 会越界
3. 验证 BoneSpaceTransforms.Num() >= NumBones 不检查会崩溃

AnimSequence 保存(5 个关键)

步骤 API 作用
1. 获取 Controller Model->GetController() 唯一修改接口
2. 作用域管理 FScopedBracket RAII 自动清理
3. 添加轨道 AddBoneCurve() UE5 标准 API
4. 设置数据 SetBoneTrackKeys() Pos/Rot/Scale 数量必须一致
5. 完成通知 NotifyPopulated() 必须调用

五、调试检查清单

录制前检查

UE_LOG(LogTemp, Warning, TEXT("=== 骨骼数据诊断 ==="));
UE_LOG(LogTemp, Warning, TEXT("RefSkeleton.GetRawBoneNum() = %d"), RefSkeleton.GetRawBoneNum());
UE_LOG(LogTemp, Warning, TEXT("RefSkeleton.GetNum() = %d"), RefSkeleton.GetNum());
UE_LOG(LogTemp, Warning, TEXT("BoneSpaceTransforms.Num() = %d"), BoneSpaceTransforms.Num());

// 正确结果:RawBoneNum == BoneSpaceTransforms.Num()

保存前检查

UE_LOG(LogTemp, Warning, TEXT("生成动画: 骨骼=%d, 帧数=%d, 帧率=%.2f fps"),
    BoneNames.Num(), NumFrames, ActualFrameRate);

// 检查数据完整性
for (int32 i = 0; i < BoneNames.Num(); ++i)
{
    if (TrackDataArray[i].PosKeys.Num() != NumFrames)
    {
        UE_LOG(LogTemp, Error, TEXT("骨骼 %s 数据不完整"), *BoneNames[i].ToString());
    }
}

六、引擎源码参考位置

AnimSequence.cpp 关键函数

函数 行号 作用
CreateAnimation(SkeletalMeshComponent*) 2659-2692 标准骨骼数据获取
CreateAnimation(SkeletalMesh*) 2630-2657 使用参考姿态
GetBonePose() 1598-1715 播放时数据提取

查看方法

文件路径:Engine/Source/Runtime/Engine/Private/Animation/AnimSequence.cpp
关键行:const int32 NumBones = RefSkeleton.GetRawBoneNum();  // 第 2674 行

结论

UE5.4 运行时动画录制的核心在于:

  1. 正确获取骨骼数据

    • 使用 SkeletalMesh->GetRefSkeleton() 而非 Skeleton 的
    • 使用 GetRawBoneNum() 而非 GetNum()
    • 必须验证 BoneSpaceTransforms.Num() >= NumBones
  2. 正确保存 AnimSequence

    • 通过 IAnimationDataController 接口操作
    • 使用 FScopedBracket 管理作用域
    • 调用 AddBoneCurve() 而非 AddBoneTrack()
    • 必须调用 NotifyPopulated()

遵循这些原则,就能实现稳定可靠的动画录制系统。


引擎版本:Unreal Engine 5.4
参考源码:AnimSequence.cpp (2630-2692行)


感谢阅读,欢迎点赞、关注、收藏,有问题可在评论区交流。
如果本文对你有帮助,点击这里捐赠支持作者。

posted @ 2025-11-06 19:10  mengzhishanghun  阅读(55)  评论(0)    收藏  举报