UE:论运行时动画录制的关键-正确获取骨骼数据与保存
© mengzhishanghun · 原创文章
首发于 博客园 · 禁止未经授权转载
核心问题
在 UE5.4 中实现运行时动画录制,最关键的两个问题是:
- 如何获取正确的骨骼数据 - 避免崩溃和数据不匹配
- 如何正确保存 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 运行时动画录制的核心在于:
-
正确获取骨骼数据:
- 使用
SkeletalMesh->GetRefSkeleton()而非 Skeleton 的 - 使用
GetRawBoneNum()而非GetNum() - 必须验证
BoneSpaceTransforms.Num() >= NumBones
- 使用
-
正确保存 AnimSequence:
- 通过
IAnimationDataController接口操作 - 使用
FScopedBracket管理作用域 - 调用
AddBoneCurve()而非AddBoneTrack() - 必须调用
NotifyPopulated()
- 通过
遵循这些原则,就能实现稳定可靠的动画录制系统。
引擎版本:Unreal Engine 5.4
参考源码:AnimSequence.cpp (2630-2692行)
感谢阅读,欢迎点赞、关注、收藏,有问题可在评论区交流。
如果本文对你有帮助,点击这里捐赠支持作者。

浙公网安备 33010602011771号