Unreal Engine 5 Character and Animation Optimizations
Unreal Engine 5 Character and Animation Optimizations
这是一篇视频记录,视频地址:Unreal Engine 5 Character and Animation Optimizations | Unreal Fest 2024
Profiling
Commands
- Stat FPS

- Stat UNIT

- Stat UNITGRAPH

- Stat ANIM

- ShowDebug ANIMATION

Size Map
检测当前资源引用到的资源大小,可分别查看内存中的和磁盘中。

MemReport
MemReport -full 输出当前内存情况,到Saved/Profiling/MemReports/文件夹中。
Control Rig
在Control Rig的类设置中,打开以下选项:

这样可以实时看到整个Control Rig花的时间:

以及每个节点的消耗和调用次数:

GPU Visualiser

Insights
- Channels
- Animation
- AssetLoadTime
- Stat Named Events
- Open Insights After Trace
Animation Sequence
Audit
Content Audit Animation Data
- Animation Tracks:检查是否有不需要的节点被烘培进动画了,比如把脸部节点也加入到动画中了,但是该动画不需要脸部节点运动。
- Animation Curves:删除不必要的曲线

Animation Track
检查动画轨道数量,数量在66个左右是非常适合Gameplay的。
使用Asset Action Utility编写一个简单的方法查询该动画的轨道数量,代码源文件,代码如下:

这样可以右键动画切片时,看到该方法,运行即可:

输出如下:

Animation Import Setting
导入动画切片时勾选上以下三个选项:

设置全部导入配置,在/Engine/Conifg/BaseEditorPerProjectUserSettings.ini文件中的/Script/UnrealEd.FbxAnimSequenceImportData章节配置:

Audit Example
以下原数据:

减去不必要轨道和曲线后:

Anim Graph
可以在动画蓝图中查看,动画传输的数据,如果除了动画数据还有其他数据在传输,可以看到有其他颜色的线,这样可以用来排查是否有无用数据在传输。

Compression
- Animation Compression Library(ACL)
- Default >= 5.3
- Unreal Marketplace 4.25 - 5.2
Level Of Detail (LOD)
根据距离来减少蒙皮的面数,同时也可以减少骨骼数量、蒙皮权重等。

在骨骼蒙皮文件中查看LOD的索引:

创建SkeletalMeshLODSettings配置表设置LOD的细节:

主要设置以下几个选项:

Screen Size
这是用控制LOD切换的参数,他决定了在屏幕上,模型占据多大比例时切换到特定的LOD级别。ScreenSize 是一个介于0到1之间的浮点值,表示模型在屏幕上的相对大小。
值为1时,表示模型占据整个屏幕高度。
值为0.5时,表示模型占据屏幕高度的一半。
值为0时,表示模型在屏幕上不可见。
LOD切换逻辑:
当模型的屏幕大小(基于其包围盒)小于或等于 ScreenSize 时,引擎会切换到对应的LOD级别。
例如,如果 ScreenSize 设置为0.5,当模型在屏幕上的大小小于或等于屏幕高度的一半时,引擎会使用该LOD级别。
可以使用命令A.VisualizeLODs 1 查看模型在场景中的ScreenSize,如下图:

LOD Hysteresis

在一些特殊的情况下,LOD会一直切换,即使相机是固定的。为了解决频繁的切换造成的性能和抖动的问题,为LOD切换引入了一个缓冲区域。具体来说逻辑如下:
当模型从高 LOD 切换到低 LOD 时,切换的实际阈值会比配置的
ScreenSize更低。当模型从低 LOD 切换回高 LOD 时,切换的实际阈值会比配置的
ScreenSize更高。
假设 ScreenSize 是 LOD 切换的阈值,LODHysteresis 是一个介于 0 到 1 之间的值,用于调整切换的缓冲范围。
从高 LOD 切换到低 LOD: 实际切换阈值 =
ScreenSize× (1 -LODHysteresis)从低 LOD 切换回高 LOD: 实际切换阈值 =
ScreenSize× (1 +LODHysteresis)
例如:
如果 ScreenSize 为 0.5,LODHysteresis 为 0.1:
从高 LOD 切换到低 LOD 的阈值是
0.5 × (1 - 0.1) = 0.45。从低 LOD 切换回高 LOD 的阈值是
0.5 × (1 + 0.1) = 0.55。
这意味着:
当模型的屏幕大小从大于 0.5 减小到 0.45 时,才会从高 LOD 切换到低 LOD。
当模型的屏幕大小从小于 0.5 增加到 0.55 时,才会从低 LOD 切换回高 LOD。
Bone List
设置骨骼赛选Bone Filer Action Option:
- Remove the joints specified and children
- Only Keep the joints specified and parents
一个简单的设置例子:
- LOD 01
- Remove the joints specified and children
- Face Joints
- Remove the joints specified and children
- LOD 02
- Only Keep the joints specified and parents
- Core Skeleton Keep
- Only Keep the joints specified and parents
Reduction Settings

这里有很多设置,核心讲几个比较重要的设置:
- Remap Morph Targets:关闭。
Morph Targets是实现模型变形的技术,通过调整顶点的位置来实现面部表情、肌肉、布料等效果。在启用LOD时,引擎会减少模型顶点的数量,那么使用顶点做的变形就需要重新映射顶点。 - Max Bone Influences:降低到3和4。最小的可以降低到0。
- Geometry
- Enforce Bone Boundaries: 保留骨骼边界的完整性,避免动画变形问题。
- Merge Coincident Vertices Bones:合并重合的顶点和骨骼,优化模型数据。
- Volumetric Correction:保持模型的体积不变,避免形状失真。
- Lock Mesh Edges:锁定模型边缘,保留硬边和轮廓特征。
- Lock Vertex Colour Boundaries:保留顶点颜色的完整性,确保颜色信息正确。
- Improve Triangles for Cloth:优化布料模拟的三角形,提高模拟质量。
Animation Blueprint
Multi-threading
启用多线程运行动画蓝图。
确保在Project Settings->Engine->General Settings中启用以下选项:

在每个动画蓝图的中的Class Settings中启用以下选项:

Warning

在使用Root Motion时,多线处理会失效,因为计算根节点位移实在Game线程中。
将所有更新的逻辑移动到Blueprint Thread Safe Update Aniamtion,并且确保该函数调用的函数是BlueprintThreadSafe启动该选项,获取参数使用Property Access,详情可参考:文章
多线程在使用Root Motion时是会失效,是因为CMC组件是在Game线程处理角色的位置。
这篇文章中提到一种解决方案:
引擎默认是开启动画多线程的,引起多线程失效一般都是有root motion问题(RootMotionFromEverything,RootMotionFromMontagesOnly),RootMotionFromEverything会强制动画在主线程更新完,RootMotionFromMontagesOnly如果当前在播蒙太奇,而且蒙太奇有root motion曲线,也会引起多线程失效,因为移动组件要提取加速度。
我们的优化手段是,动画组件支持是否强制多线程,只有非常近的怪物,才会让有root motion的时候在主线程update,远处的带有root motion的,强制多线程更新,保存起来root motion的曲线数据,让移动组件在下一帧的时候提取上一帧的root motion,经过一段时间的观察,表现还不错。只有哪些root motion非常快的,才会有一点点滑步,而且是远处的,基本看不出来。这一步的优化,让主线程清净了许多。
Fast Path
Fast Path(快速路径) 是动画蓝图(Animation Blueprint)中的一种自动优化机制,旨在提高动画系统的运行效率。它通过减少不必要的计算和逻辑处理,使动画蓝图能够以更高的性能运行。
启动该机制前提是打开Project Settings->Engine->General Settings->Optimize Anim Blueprint Member Variable Access选项。
查看节点是否启用了该机制,可以查看蓝图节点上是否有闪电标识如下图:

在动画蓝图中开启Warn About Blueprint Usage选项,可以提示开发者使用的节点是否合理。

Fast Path 是引擎自动管理的功能,开发者无法手动启用或禁用它。然而,可以通过以下方式确保动画蓝图能够充分利用 Fast Path:
简化动画蓝图逻辑:
- 尽量避免在动画蓝图中使用复杂的逻辑(如自定义事件、复杂的变量计算等)。
- 使用简单的节点(如状态机、混合节点、骨骼控制器等)来实现动画逻辑。
- 直接调用本地参数,而不是调用引用对象的参数,使用
Property Access访问参数。
避免使用不支持
Fast Path的节点:- 某些节点(如蓝图函数调用、自定义事件等)可能会导致
Fast Path被禁用。 - 尽量使用动画系统内置的节点来实现功能。
- 不要在
AnimGraph中使用And和Or节点
- 不要在
AnimGraph中使用乘法
- 某些节点(如蓝图函数调用、自定义事件等)可能会导致
优化动画蓝图结构:
- 将复杂的逻辑移到
Anim Instance或Character Blueprint中处理,而不是直接在动画蓝图中实现。
- 将复杂的逻辑移到
AnimGraph Functions
在AnimGrap中很多计算都不能使用,那应该怎么正确的在AnimGrap中使用计算呢?
Functions on nodes
使用绑定方法,如下图:

Call Function Node
在AnimGrap中直接调用方法,使用的是UAnimGraphNode_CallFunction,并且可以设置触发的时机。

没有具体使用过该节点,发现只要是在AnimInstance中的方法且开启了BlueprintThreadSafe,都可以在这里选择出来。
Functions Must Be Thread Safe
函数必须打开多线程安全

Use Case - Fast Path With Logic In AnimGraph
以下方式不能触发Fast Path自动优化机制:

可以修改为:

AnimGraph LODs
Node LODs
可以设置节点上LOD来控制节点是否运行:

比如下图的叠加动画,当前角色LOD值大于填的值,就不会执行该节点

Predicted LOD Level

使用预测LOD来控制动画播放,比如下图:

Post Process ABP LOD
在骨骼蒙皮文件中可以对动画后处理蓝图进行设置LOD

AnimGraph Practices
Space Conversion Bundles
尽量减少使用空间转化的节点,把需要同一个空间计算的节点放在一起。

State Machine Settings

Max Transitions Per Frame 和 Max Transitions Requests可以适当的减少。
Cached Pose
多使用缓存姿势:

不要在状态机里再套状态机,可以把所有状态机放在最上层,然后把他们的姿势存起来再使用。
Skeleton Pose Update
Visibility Base Anim Tick
在SkeletonMeshComponent->Details->Optimization中设置:

可以设置为Only Tick Pose when Rendered,表现如下图:

统一修改Project/Config/DefaultEngine.ini文件中:
[/Script/Engine.SkeletalMeshComponent]
VisibilityBaseAnimTickOption=OnlyTickPoseWhenRendered
这只能修改到SkeletalMeshComponent,并不能修改Skeletal Mesh Actors。如果想要全部修改的话,只有修改引擎文件:
SkinnedMeshComponent.cpp构造函数中VisibilityBasedAnimTickOption的默认值SkeletalMeshActor.cpp构造函数中SkeletalMeshComponent->VisibilityBasedAnimTickOption的默认值
No Skeleton Update
在一些场景中人物不需要更新骨骼,可以启用USkeletalMeshComponent->bNoSkeletonUpdate选项,停止更新骨骼,同时动画和物理模拟也不会更新了。

可以使用一个触发盒来启用该选项,离开该盒子后就关闭该选项。
Update Rate Optimizations
打开以下两个选项:

可以看到更新的频率:

不同颜色的表示:
绿色:
- 表示骨骼网格体正在以 最高频率 更新(例如每帧更新)。
- 这意味着该骨骼网格体没有被优化,或者优化被禁用。
黄色:
- 表示骨骼网格体正在以 较低的频率 更新(例如每两帧更新一次)。
- 这是
URO的典型优化状态,表示骨骼网格体的更新频率被降低以节省性能。
红色:
- 表示骨骼网格体 完全停止更新。
- 这意味着骨骼网格体的更新被跳过,通常是因为它被认为对当前帧的渲染不重要。
蓝色:
- 表示骨骼网格体正在以 最低频率 更新(例如每四帧更新一次)。
- 这是更激进的优化状态,适用于对性能要求极高的场景。
如果你不想使用C++来编写更新频率的逻辑,可以使用下面这个插件:

以下是简单的一个示例根据LOD数值控制刷新率:

下面是一个简单控制刷新率的例子,没有进行插值的效果如下:

在打开插值过后,可以看到20帧刷新一次的跟每帧刷新一次的没有太大的区别:

Budgeted Skeletal Mesh Component
插件Animation Budget Allocator

使用Skeletal Mesh Component Budgeted替换所有的Skeletal Mesh Component

然后再使用命令a.Budget.Enabled 1启用

这样就可以看到当前场景中的Skeletal Mesh Component Budgeted使用情况:

可以手动设置动画的预算为1ms:

查看场景中的表现:

Texture
Texture Size
图片的倍数:

Size Limit
可以设置贴图中的Maximum Texture Size来控制纹理最大分辨率,它决定了纹理在导入或运行时可以被压缩到的最大尺寸。

以下是一个4k图片设置对比:

统一修改在文件Engine\Config\BaseDeviceProfiles.ini - GlobalDefaults DeviceProfile:

设置Texture Group在Texture -> Level of Detail -> Texture Group Character :

可以多选贴图使用Property Matrices同时设置。
Micro Detail
细节纹理容易在压缩纹理之后丢失掉,看起来会很糊。

所以我们可以将这些细节纹理用Shader的方式呈现,比如在材质中Tiling这些细节纹理,而不是将细节纹理烘焙在主纹理上。

Compression
The Cost of aphla
一般导入是以下格式,不过需要手动检查以下该图片是否需要透明通道,如果不需要就需要启用以下这个选项:

以下是有透明通道和没有透明通道的对比:

压缩算法的编码不同导致储存大小不同,对比如下图。如果我们的磁盘与内存真的不够,就需要重点考虑哪些纹理确实不需要Alpha通道,哪些纹理不需要高精度的颜色表现了。

G8
有时候我们只需要单通道纹理,这时候G8压缩方法就比较好,这个方法会只留下Red通道。所有这些压缩方法可以在纹理的细节面板中调整。

Oodle
我们在打开纹理编辑器的时候可能就已经见过这个藏在角落的Oodle面板了,但这个东西到底有什么用?

这个面板可以让我们调整相关的纹理编码参数:

当我们调整出了一个看起来很满意的参数之后就可以在BaseEngine配置文件中修改全局默认值了。

Channel Packing
通道合成 一个很常见的例子就是将AO遮罩、粗糙度遮罩和金属度遮罩合并成一张RGB纹理。我们甚至还可以加多一张Emissive或者别的什么遮罩放在Alpha通道里,随我们喜欢。这样我们就可以在材质中减少纹理采样,优化纹理复杂度。 
在权衡了纹理质量的前提下,想要追求极致优化,可以合成法线纹理。首先我们需要将法线纹理的压缩设置改回默认,然后在材质中这么写。这样我们就可以往法线纹理的B通道中加东西了。

注意!这种方案非常影响纹理的质量,下图是结果,请权衡好质量与优化。

Masks RGB
蒙版-大部分人会以为只能往RGBA纹理中塞4种蒙版,但其实我们可以往里面塞9种蒙版!

Skeletal Mesh Components
Bounds
骨骼网格体组件还有更多参数设置。我们在更新骨骼网格体时会更新它的包围盒,包围盒的更新我们也可以进行一些调整。执行ShowFlag.Bounds 1可以显示组件的包围盒,这个可以帮助我们Debug。

一般可以设置以下几个:
上文提到过,骨骼网格体可以跳过更新帧,然后通过插值对骨骼变形进行平滑。而包围盒的更新也可以跳过更新帧,并且不在平滑后更新。将包围盒更新模式调整成
Skip Bounds Update When Interpolating即可。因为遮蔽剔除和相机剔除都是用包围盒计算的,假如包围盒进入剔除范围,这个组件就不会被渲染。骨骼网格体组件可以固定包围盒(
Component Use Fixed Skel Bounds),但存在一定的风险,它可能会在应该渲染的时候不渲染。当然,如果确定我们的角色不会超过这个包围盒,我们就可以放心使用固定包围盒。
如果我们采用了模块化角色方案,可以调整成使用父项骨骼网格体的包围盒模式(
Use Parent Bounds/Bounds From Leader Pose Component)。

以上这些设置能在USkeletalMeshComponent找到:

Render Static
假如我们有一个骨骼网格体,但是它在游戏内不需要动画,这时候这个Render Static选项就很有用。并且这个选项可以在游戏内自由调节。

Nanite Skeleton
现在Nanite骨骼网格体还没出,但我们可以用点小伎俩。比如我们可以导入一个只有单个三角面的骨架(因为虚幻不允许导入没有面与蒙皮的骨骼网格体),然后将这个三角面关掉。

导入一些静态网格体,静态网格体可以用Nanite,然后再将他们绑定在骨架上。

这样我们就得到了一个“Nanite骨骼网格体”,这非常适用于各种刚性的骨骼网格体上。

URO(Update Rate Optimization)
URO通过降低非关键角色的动画更新频率实现性能优化,核心原理是根据距离相机的远近或LOD等级跳过部分动画帧计算。例如,远处角色可能每4帧更新一次动画,而非每帧更新。其工作机制依赖FAnimUpdateRateManager管理更新参数,可通过以下方式配置:
距离阈值模式
根据角色在屏幕空间的大小(MaxDistanceFactor)设置更新频率。默认阈值表为{0.24f, 0.12f},当角色占屏面积大于0.24时每帧更新,0.12~0.24之间每2帧更新,小于0.12时每3帧更新。可通过C++自定义阈值表:
TArray<float> Thresholds = {0.5f, 0.3f, 0.1f}; // 距离因子阈值
AnimUpdateRateParams->BaseVisibleDistanceFactorThesholds = Thresholds;
LOD映射模式
为不同LOD等级指定跳过帧数。例如,LOD0(最高细节)每帧更新,LOD1每3帧更新,LOD2每5帧更新:
AnimUpdateRateParams->bShouldUseLodMap = true;
AnimUpdateRateParams->LODToFrameSkipMap.Add(0, 0); // LOD0:不跳帧
AnimUpdateRateParams->LODToFrameSkipMap.Add(1, 2); // LOD1:跳2帧(每3帧更新)
插值补偿
跳帧可能导致动画卡顿,URO通过MaxEvalRateForInterpolation参数启用插值。默认值为4,即跳帧次数≤3时自动插值过渡,确保视觉流畅度。
适用场景:开放世界游戏中大量NPC或远处角色,可降低主线程CPU占用30%以上
VisibilityBasedAnimTickOption
可见性动画Tick控制,该枚举控制动画组件在角色不可见时的Tick行为,直接影响动画逻辑更新和骨骼矩阵刷新,共4种模式:
| 模式 | 行为描述 | 性能影响 | 适用场景 |
|---|---|---|---|
| AlwaysTickPoseAndRefreshBones | 始终Tick动画逻辑并刷新骨骼矩阵,无视可见性 | 最高(每帧全更新) | 玩家角色、关键交互NPC |
| AlwaysTickPose | 始终Tick逻辑,但仅可见时刷新矩阵 | 中等(逻辑持续运行) | 需要保持动画连续性的敌人 |
| OnlyTickMontagesWhenNotRendered | 不可见时仅Tick蒙太奇逻辑,其他动画暂停 | 较低 | 过场动画角色、非战斗NPC |
| OnlyTickPoseWhenRendered | 不可见时完全停止Tick和矩阵刷新 | 最低 | 背景角色、屏幕外物体 |
典型问题:若设置为OnlyTickPoseWhenRendered,不可见时动画通知(Notify)和IK逻辑会失效,导致技能特效不触发或角色位置偏移。服务器端通常设为OnlyTickMontagesWhenNotRendered,仅保留蒙太奇逻辑以节省资源。
VisibilityBasedAnimTickOption&&URO
两者结合可最大化性能收益:
- 近处角色:关闭URO(每帧更新)+
AlwaysTickPoseAndRefreshBones,保证动画精度。 - 中距离角色:启用URO(每2~3帧更新)+
AlwaysTickPose,平衡性能与连续性。 - 远处/屏幕外角色:URO(每4~6帧更新)+
OnlyTickPoseWhenRendered,最低资源消耗。
注意事项:URO的距离阈值与LOD模式不可同时生效,需根据项目需求选择;VisibilityBasedAnimTickOption需避免过度优化导致逻辑断裂,例如关键NPC禁用OnlyTickPoseWhenRendered。
bUpdateOverlapsOnAnimationFinalize
这个是当动画组件最终完成后,会跑一次迭代这个actor下所有要产生的overlap事件的所有组件,这个耗时很高,大部分情况下,都是没必要的,让移动组件产生overlap事件就足够了,默认情况下我们都关闭,子类有须要的时候才打开。
Blueprints
Tick
Option
Turn it Off by Default
- Blueprint

- Component

- Blueprint
Turn it Off by Default - Source Code
- Actor.cpp - PrimaryActorTick.bStartWithTickEnabled = false;
- ActorComponent.cpp - PrimaryComponentTick.bStartWithTickEnabled = false;
- Controller.cpp - PrimaryActorTick.bStartWithTickEnabled = true;
Tick Rate

Source Code:
- Actor.cpp - PrimaryActorTick.TickInterval = 1.0f;
- ActorComponent.cpp - PrimaryComponentTick.TickInterval = 1.0f;
dumpTicks
执行dumpTicks指令可以输出现在在Tick的事件有哪些。

References
引用如果处理不当也会出现大问题,这会导致资产拖着一堆引用,导致加载不畅或者内存占用高。当我们打开Size Map就能看到资产的硬引用,加载第三人称角色蓝图意味着要加载上Size Map上显示的这些资产。

举一个反面案例,这是硬引用处理不当导致的。加载角色的时候加载多了一些载具相关的东西。

这是因为角色蓝图里有一个载具类引用的变量,或者Cast To了载具类。

Interface
如何处理Cast就很重要了,首先如果我们确实需要用到某个类,放心Cast,比如角色与动画蓝图之间的Cast。

但如果我们真的需要用到类里面的逻辑怎么办?可以试试用接口。

然后在蓝图中实现它即可。

Soft Reference

软引用能做的东西不多,并且我们在使用它之前得将它加载成硬引用。Blocking节点会阻塞游戏线程,而Async节点会异步完成加载任务。

Warning
要注意的是,以上这些节点加载出来的引用会卡在蓝图的生命周期里,不会被垃圾收集处理,在加载完,完成了对应的任务之后,推荐用一个Set Object Reference (by ref)节点清掉这个引用,让垃圾收集处理它。
Parent Class Component Assignments
我们一般会将角色蓝图的子蓝图作为角色的变体,修改里面的网格体和里面的一些参数。这个例子是用Quinn的主蓝图生成了一个Mannequin的子蓝图。

当我们打开子蓝图的Size Map就会发现,它加载上了主蓝图中的Quinn骨骼网格体。

这种情况下,需要将主蓝图中的网格体或者其他一些硬引用去掉。

然后再创建两个子蓝图,分别对应Quinn和Mannequin。这样两个子蓝图就会只加载自己对应的引用资产。
