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

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


核心问题

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

  1. 如何获取正确的骨骼数据 - 避免崩溃和数据不匹配

  2. 如何正确保存 AnimSequence - 使用引擎标准 API

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


一、获取正确的骨骼数据

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


// ✅ 正确:使用 SkeletalMesh 的 RefSkeletonUSkeletalMesh* 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 = 161RefSkeleton.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:为什么不能用固定帧率?


// ❌ 错误:固定 30fpsController->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

  1. 正确保存 AnimSequence

- 通过 IAnimationDataController 接口操作

- 使用 FScopedBracket 管理作用域

- 调用 AddBoneCurve() 而非 AddBoneTrack()

- 必须调用 NotifyPopulated()

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


引擎版本:Unreal Engine 5.4

参考源码:AnimSequence.cpp (2630-2692行)


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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/958032.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

a-menu 当设置折叠状态如何穿透悬浮菜单样式

效果antReset.css .ant-menu-submenu .ant-menu-submenu-popup .ant-menu .ant-menu-light {border: 1px solid #173808 !important; }/* 直接针对 popup 整体背景 */ .ant-menu-submenu-popup {background-color: #17…

attention论文及Transformer工作原理概述

attention论文及Transformer工作原理概述Posted on 2025-11-06 19:09 wsg_blog 阅读(0) 评论(0) 收藏 举报attention论文及Transformer工作原理概述

kamailio+rtpengine对sdp的处理

概述 使用kamailio+rtpengine的过程中,默认会使用rtpengine处理sdp信息,同时又需要对sdp信息定制,就需要对cfg配置流程中做特殊处理才能实现。 环境 CentOS 7.9 kamailio:5.8.3-bullseye docker rtpengine:mr13.1.1…

软工团队项目第一次作业

软工团队项目第一次作业作业所属课程 https://edu.cnblogs.com/campus/fzu/202501SoftwareEngineering/作业要求 https://edu.cnblogs.com/campus/fzu/202501SoftwareEngineering/homework/13573作业的目标 团队展示+选…

低代码权限管理安全合规指南:守住数据安全的 “最后一道防线”

随着数据安全法、个人信息保护法的落地,企业对系统权限管理的合规要求越来越高。低代码平台作为企业数字化的核心工具,其权限管理不仅要保障数据不泄露、操作不越权,还要满足行业监管和法律法规的要求。 很多企业误…

2025-11-06

2025-11-06CF补题 Problem - 515C - Codeforces(1400)(string+a little factorial) 这题妙在把各个数字阶乘转换成仅含有2 3 5 7 数字,然后直接求解 要对每个数的阶乘进行换算[!tip]9 is 7!*8*9=7!*3!*3!*2!8 is …

低代码权限管理常见场景解决方案:精准适配不同业务需求

低代码平台的核心优势是 “快速适配多元业务”,而权限管理作为保障业务安全的关键,必须跟着场景走。很多企业在设置权限时,容易陷入 “一刀切” 的误区 —— 用一套权限配置应对所有业务场景,结果要么出现 “权限不…

不适用模型的简易ai交互页面

不适用模型的简易ai交互页面 一.形式import streamlit as st st.title("测试标题") st.divider() prompt=st.chat_input("请输入你的问题") if prompt:#如果问题不为空才输出答案st.chat_message(…

关于waybar状态栏颜文字乱码问题

也就差个字体的事: sudo pacman -S nerd-fonts

自己的火印

/*** Modified by Noivelist,* Luogu:https://www.luogu.com.cn/user/700335* Marsoj:http://marsoj.com/user/252* “倘若梦境醒来,执念破去,我们再谈救赎”* Working on Project: [ ]**/

P10277 [USACO24OPEN] Bessies Interview S 题解

P10277 [USACO24OPEN] Bessies Interview S 题解P10277 [USACO24OPEN] Bessies Interview S 题解 题目传送门 我的博客 思路 首先这道题第一问非常好做。只需要按照题目描述的那样模拟即可。即用优先队列存每个奶牛的面…

基于AIGC的图表狐深度评测:自然语言生成专业级统计图表的高效的技术实现

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

AI 时代的数据库进化论 —— 从向量到混合检索

AI 时代的数据库进化论 —— 从向量到混合检索说明:本文只是关于数据库发展趋势的个人见解,没有特别深入的向量和混合检索的实现原理,属于很浅显易懂的科普类文章,几乎不需要任何背景知识,大家可以放心阅读。 关于…

深入解析:操作系统基础:了解进程、线程、协程,理解I/O模型(阻塞/非阻塞,同步/异步)。

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

vue 3.x 前端导出功能

首页安装插件 npm install xlsx 在当前页面中引入import * as XLSX from xlsx点击事件<a-button :disabled="orderList.length === 0" :size="small" @click="exportExcel"><…

最高法-合同目的的认定

最高法-合同目的的认定2025-11-06 18:48 wwx的个人博客 阅读(0) 评论(0) 收藏 举报(2023)最高法民申2327号 天府某公司、西藏某公司等技术服务合同纠纷民事申请再审审查民事裁定书 本院认为: (一)关于涉案合…

2025年恒温恒湿机标杆厂家最新推荐:中焓环境,档案室恒湿机/精密恒温恒湿机/吊顶恒温恒湿机/档案室恒温恒湿机,定义环境控制精准新标准

随着社会对文物保存、精密制造、数据中心运维及工业生产的环境要求日益严苛,恒温恒湿设备已从特定领域专用设备,扩展至博物馆、档案馆、数据中心、医药、电子等多个关键行业。2025年,市场需求预计将持续增长,但随之…

2025年恒温恒湿厂家及恒湿设备标杆之选:中焓环境,适配机房/档案室/展柜等场景

随着各行业对环境温湿度精准控制需求的不断提升,尤其是机房、档案室、展柜等特殊场景对环境稳定性要求趋严,以及环保与节能理念在设备领域的深入渗透,恒温恒湿机、恒湿机等相关设备已从专业领域逐步拓展至更多行业应…