http://mp.weixin.qq.com/s/bybEHM9tF-jBPxxqXfrPOQ##

2017-05-09 虚幻引擎

Unreal Open Day 2017 活动上 Epic Games 开发者支持工程师郭春飚先生为到场的开发者介绍了在 Unreal Engine 4 中 UI 的优化技巧,以下是演讲实录。

大家好,我是 Epic Games 的开发者支持工程师郭春飚,今天给大家介绍的是 UE4 的 UI 优化经验。我们之前一直有接到国内开发者的一些抱怨,他们觉得在手机上面开了 UI 以后性能下降的很快,今天就专门给大家介绍一下怎么在 UE4 上做 UI 优化。本文介绍的 UI 优化方式,不仅适用于移动平台,在其它平台上(如 PC 和主机)对于复杂的 UI 系统也会有很大的性能提升。

文章目录:

1 UI 的基本概念

1.1 名词解释

1.2 渲染流程

1.3 性能指标

2 优化方案

2.1 游戏线程优化

2.1.1 Invalidation Box

2.1.2 可见性(Widget Visibility)

2.1.3 Widget Binding

2.2 渲染线程优化

2.2.1 合并批次

2.2.2 Retainer Box

2.2.3 事件驱动的 Retainer Box

2.2.4 切换材质

2.3 其它优化

2.3.1 C++ 开发

2.3.2 Manager Class

2.3.3 释放贴图内存

2.3.4 3D RTT 优化

2.3.5 新功能

3 效果测试

4 总结

1. UI的基本概念

1.1 名词解释

User Widget:对应一个用户界面。

Widget Tree:每一个 User Widget 都是存储成树状结构。

Panel Widget:不会渲染出来,用于对 Child Widget 进行布局,如 Canva Panel, Grid Panel, Horizontal Box 等。

Common Widget:用于渲染,会生成到最后的 Draw Elements 中,如 Button, Image, Text 等。

1.2 渲染流程

基本渲染流程示意图:

在游戏线程 (Game Thread),Slate Tick 每一帧会遍历两次 Widget Tree。

Prepass:从下到上遍历树,计算每一个Widget的理想尺寸 (Desired Size)。

OnPaint:从上到下遍历树,计算渲染所需的 Draw Elements 。这个过程中,会根据 Common Widget 的类型和参数生成相应的 Vertex Buffer,将 Common Widget 的 Render Transform 计算到 Vertex Buffer 中,并根据 Layer ID 和 Material 等信息进行批次合并。最后一个 User Widget 会生成1个或多个 Draw Element,并将 Draw Elements 传递给渲染线程进行渲染,其中每个 Draw Element 对应一个 Draw Call。

在渲染线程 (Render Thread),Slate 渲染分为两步:

Widget Render:执行 UI 的 RTT,如果使用了 Retainer Box,这里会将 Draw Elements 渲染到 Retainer Box 的 Render Target。

Slate Render:将 Draw Elements 渲染到 Back Buffer,如果使用了 Retainer Box,会将 Retainer Box 对应的 Texture Resource 渲染到 Back Buffer。

1.3 性能指标

Stat.Slate命令列举了一些主要的Slate性能参数:

Num Painted Widgets:在游戏线程执行 OnPaint 的 Widget 数量。

Num Batches:Draw Element(也即 Draw Call)数量。

Stat.Slate 会创建一个未优化的 UI,并且统计线程会将这个 UI 的性能数据算入 Slate 开销,因此表格中的时间数据和真实数据相差很大。建议通过如下命令查看统计线程变量的时间开销:

stat dumpave –num=120 –ms=0.5

三个关键指标的统计数据分别是:

Slate Tick:统计线程变量 STAT_SlateTickTime。

Slate Render:统计线程变量 STAT_SlateRenderingRTTime。

Widget Render:统计线程变量 FWidgetRenderer_DrawWindow。

如果希望在项目中实时调试性能,可以从统计线程直接获取数据,并做一个简单的调试面板进行查看。

游戏线程代码:

统计线程代码:

调试面板效果:

2 优化方案

2.1 游戏线程优化

2.1.1 Invalidation Box

使用 Invalidation Box 封装 User Widget,从而缓存 Slate Tick 数据,不需要每帧都进行计算。操作方式如下所示:

在 Invalidation Box 下的所有 Prepass 和 OnPaint 计算结果都会被缓存下来。如果某个 Child Widget 的渲染信息发生变化,就会通知 Invalidation Box 重新计算一次 Prepass 和 OnPaint 更新缓存信息。

下图演示了一种特殊情况,英雄图标是一个重复使用的 User Widget,每个都被封装进了 Invalidation Box。整个英雄列表是一个 Scroll Box,当 Scroll Box 上下滑动时,英雄图标对应 User Widget 的 Transform 信息也会发生变化。

此时可以勾选 Invalidation Box 对应的 Cache Relative Transforms,如下所示:

那么当 User Widget 的位置变化时,引擎不会去更新所有的 Draw Element(即 Vertex Buffer ),而会通过修改 Shader 参数(View * Projection Matrix)来反应位置变化。这种方式仅适用于位置变化,如果缩放发生变化,仍然需要重新计算 Draw Element。Cache Relative  Transforms 会在 Game Thread 增加少量额外的计算,确保需要使用时才勾选。

当某个 Widget 的渲染信息变化时,会通知所在的 Invalidation Box 重新缓存 Vertex Buffer。在一个复杂的 User Widget 中,Invalidation Box 频繁缓存整个 Widget Tree 会带来很高的性能开销,有两种方式可以解决这个问题。

第一种方式是拆分 Invalidation Box,根据 Widget 变化是否频繁将它们拆分到不同的 Invalidation Box 中。

有时由于布局的原因,不是很方便的划分不同的 Invalidation Box,那么可以使用第二种方式,将 Widget 设定成 Is Volatile,这样上层的 Invalidation Box 在缓存时就会排除这个 Widget,该 Widget 每帧都会 Tick 并计算 Prepass 和 OnPaint,但整体 Widget Tree 的缓存不会受到影响。

上图中的 LevelUpIcon,平时处于隐藏状态,当角色升级时会显示出来, LevelUpAnim 通过改变 Widget 的位置实现动画效果。当渲染这个 Image 时,由于位置一直在变化,会导致 Invalidation Box 每帧都在重新计算整个 Widget Tree 的 Cache,性能比较低。此时可以将这个 Widget 设定成 Is Volatile,从而提高性能。

编辑器中 Is Volatile 选项可以用于显式地设置 Volatile,用于提高 Invalidation Box 的性能。有时 Widget Binding 会隐式地将 Widget 标记成 Volatile,导致这个 Widget 每帧都会 Tick,从而降低性能。

每个 Widget 在 ComputeVolatility 函数中详细列举了哪些属性会导致影响 Draw Element(Vertex Buffer)。

文本 Widget 影响 Draw Element 的属性:

进度条 Widget 影响 Draw Element 的属性

如果在影响 Draw Element 的属性上使用了 Widget Binding,会导致引擎每帧都要 Tick 查询是否属性发生变化,从而判断是否需要更新 Draw Element,因此应该避免使用 Widget Binding。

可以通过 Slate.InvalidationDebugging 查看是否正确地设置了 Invalidation Box 和 Volatile。

绿线框:使用 Invalidation Box 缓存的 Widget。

蓝线框:Invalidation Box 勾选了 Cache Relative Transforms。

虚线框: 标记为 Volatile 的 Widget。

红线框:没有使用 Invalidation Box 的 Widget。

Slate.AlwaysInvalidate 命令可以强制 Invalidation Box 每帧更新缓存,可以用于测试是否会造成突然的卡顿。如果一个 User Widget 过于复杂,可以拆分成多个 Invalidation Box,将 Widget 按照更新频率的高低放入不同的 Invalidtion Box。

2.1.2 可见性(Widget Visibility)

Widget 可见性有 5 种:

Visible: 可见、可点击

HitTestInvisible: 可见、当前 Widget 不可点击、所有 Child Widget 不可点击

SelfHitTestInvisible: 可见、当前 Widget 不可点击、不影响 Child Widget

Hidden: 不可见、占用布局空间

Collapsed: 不可见、不占用布局空间

很多 Widget 默认属性是 Visible,需要手动设置成 HitTestInvisible 和 SelfHitTestInvisible。如果大量 Widget 设置成 Visible,那么引擎在点击响应时的效率就会大大下降,这也会增加游戏线程的开销。

Collapsed 不占用布局空间(Layout Space),因此在隐藏后不会进行 Prepass 的计算,性能优于 Hidden。

可以使用 Widget Reflector 帮助检查是否有错误设置的 Visibility 属性。

2.1.3 Widget Binding

在分析 Volatile 时提到过 Widget Binding 会导致 Volatile 从而降低 UI 性能。另外 Widget Binding 是每帧 Tick 执行,性能比较低。不建议在项目中使用这个功能,建议通过 C++(或蓝图)调用函数的方式传值。

RemoveFromViewport/AddToViewport 会销毁以及重新构建 User Widget,使用 Collapsed/SelfHitTestInvisible 可以得到更好的性能。

另外,在移动平台上建议将蓝图 Tick 中复杂的运算逻辑移动到 C++ 中。

2.2 渲染线程优化

2.2.1 合并批次

随着 GPU 的发展,Draw Call 的数量对于性能的影响也越来越小,很多情况下减少 Draw Call 并不能带来 FPS 的提升。但减少 Draw Call 可以减少对 GPU 的 API 调用,在移动端有助于控制手机发热。

A. Panel Widget

在 4.15 之前的引擎版本,Canvas Panel 不支持批次合并,建议不要使用 Canvas Panel,尽量使用 Grid Panel、Vertical Box、Horizontal Box 等支持合并批次的容器。

4.15 增加了对 Canvas Panel 合并批次的支持,开启方式位于 Project Settings 中:"Engine->Slate Settings->Constraint Canvas->Explicit Canvas Child ZOrder"。接着可以通过设定 Canvas Panel 的 Child Widget 的 ZOrder 属性,ZOrder 相同(渲染参数也相同)的会合并批次,比起 Grid Panel 和 Horizontal Box,Canvas Panel 没有额外的布局计算,OnPaint 效率会稍微高一些(游戏线程)。

B. 合并贴图

在 UE4 中的 Sprite 很方便地支持合并贴图的编辑和使用。

如果需要在逻辑代码中切换独立贴图和合并贴图,在 Manager Class 中,初始化独立贴图 (UTexture2D) 和合并贴图资源 (UPaperSprite),并创建 FSlateBrush,通过 SetResourceObject 将资源设置给 FSlateBrush。接着就可以通过开关变量控制传入 UImage::SetBrush 的参数。

在项目后期,如果需要将 User Widget 中的贴图全部替换成合并贴图,是一项很繁琐的工作。Epic Games 的 Dmitriy Dyomin 提供了一个思路方便快速地进行替换。

首先实现一个 Commandlet:

可以使用如下命令运行这个 Commandlet:

Commandlet 的具体功能:遍历所有的 Widget Blueprint Asset,使用 AssetRegistry 加载 Asset,并检查其中 UImage 和 UBorder 使用的 Texture,根据命名规则判断是否有对应的 Sprite Asset 存在。使用 AssetRegistry 将 Texture 替换成 Sprite,最后保存 Widget Blueprint Asset。

2.2.2 Retainer Box

通过合并批次和合并贴图的方式,UI 的 Draw Call 数量可能减少到比较低,但仍然会有很高的像素填充率。

在很多情况下,UI 不需要每帧都渲染,因此可以通过 Retainer Box 缓存渲染结果,每隔几帧更新一次。Retainer Box 的原理就是将 UI 渲染缓存在 Render Target上,再将  Render Target 渲染到屏幕。

下图中,我们将主界面的 UI 划分到 4 个 Retainer Box 中,通过间隔3帧更新一次的方式来渲染。

Retainer Box 区域应该尽量小,有助于提高渲染效率、降低显存使用。通常 Retainer Box 都应该包含 User Widget 的背景图,因为背景图有很大的像素填充率。

Retainer Box 会为每个 User Widget 实例创建一个 Render Target, 因此在不改动代码的情况下,重复使用的 User Widget 不要使用 Retainer Box。例如下图中,我们应该为 Scroll Box 所在的 User Widget 创建 Retainer Box,而不应该为 Scroll Box Item 所在的 User Widget 创建 Retainer Box。

下图演示了另外一种情况,B_HeroIcon 这个 User Widget 被重复用到了 HEROS 和 SOCIAL 等多个主界面中。Battle Breakers 是一个重 UI 的手机游戏,因此很难为所有的主界面分配 Retainer Box,这会占用大量的显存,当然我们也不希望为每个 B_HeroIcon 创建一个 Retainer Box。

此时可以通过扩展代码的方式实现更好的 Retainer Box 效果,假设我们知道该 B_HeroIcon 在画面中同时出现的上限是 20,那么可以创建一个包含 20 个 Render Target 的 Render Target Pool,使得不同的 Retainer Box 可以共享同一个 Render Target。

Retainer Box 会占用额外的显存,因此要控制使用量,将它优先分配给性能提升最大的 User Widget。一种情况是主界面的 User Widget,另一个种情况是使用共享 Render Target 后的大量频繁使用的 User Widget。

使用 Retainer Box 不但能提高渲染线程的效率,游戏线程的 Tick 也会相应的隔几帧执行一次。如果 Retainer Box 内部包含了可以点击的 Widget,那么需要将 Retainer Box 设置成 Visible,这样引擎会将点击测试区域映射到 Retainer Box 上。

持续表示的效果(如3D 角色、材质特效)可以从 Retainer Box 中分离出来,但需要注意像素填充率,也可以从特效设计的方面解决。

Invalidation Box 放置在 Retainer Box 上方没有意义,通常做法是在 Retainer Box 下层放一个 Invalidation Box。

在设定 Retainer Box 的 Phase Count 时需要全局考虑。例如下图表示每隔3帧更新一次 Retainer Box,并在第 0 帧更新:

下图表示每隔 5 帧更新一次,并在第 2 帧更新:

那么每隔15帧这两个 Retainer Box 就会在一帧内同时更新,导致帧数下降。

2.2.3 事件驱动的 Retainer Box

目前 Retainer Box 需要指定每隔几帧强制更新一次,但某些情况下 User Widget 不需要按照固定频率更新,只会在用户操作(且操作不频繁)时才更新。这种情况下就可以通过扩展 Retainer Box 来支持事件驱动的方式。

实现思路是继承 URetainerBox 和 SRetainerWidget,并在 PaintRetainedContent(在 4.16 之前的版本函数名是 OnTickRetainers)中判断是否有事件触发更新,如果需要更新则调用父类的 PaintRetainedContent,否则 return。

2.2.4 切换材质

UE4 提供了丰富的材质效果,在低端机上可以考虑关闭这些效果、或切换到低配材质以提升性能。

可以使用引擎提供的 DYNAMIC_MULTICAST 框架,将所有受影响的 Widget 绑定到一个开关变量上,实现整体切换。

2.3 其它优化

2.3.1 C++ 开发

除了 UI 动画这块存储结构设计的原因不能使用 C++ 实现,其它 UI 功能都可以用 C++ 实现。

第一步,实现一个 C++ 类 UWExpHeroIcon 继承自 UUserWidget

第二步,使用 Reparent Blueprint 修改父类为 UWExpHeroIcon

第三步,在编辑器中找到需要暴露的变量以及类型

第四步,在 C++ 中声明 BindWidget 变量,引擎会自动关联数据

2.3.2 Manager Class

建议在项目中创建一个 Manager Class,统一管理所有的 User Widget,并且统一管理所有的 UI 资源,比如 Brush、Font 等。Manager Class 可以是 C++ 或蓝图的形式。

2.3.3 释放贴图内存

释放贴图内存的一个前提是不要在编辑中设置贴图(下图中的 Image 项),而是通过程序进行手动的贴图加载、贴图设置、以及贴图销毁。不在编辑器中设置贴图,可以避免在 CDO(Class Default Object)中引用这个贴图对象。CDO 的引用会使得 SharedPtr 的引用计数至少为1,并且退出应用前不会销毁。

如果在 Editor 中设置了 Image 属性,同时又希望销毁这个贴图,Epic Games 的王弥提供了一个思路,可以在 Cook 阶段解除 UImage 和 UTexture 的引用关系,从而这个 User Widget 的 CDO 不会引用到 UTexture。

解除 Cook 阶段引用关系的代码如下所示:

加载贴图的代码如下所示:

释放贴图的代码如下所示:

2.3.4 3D RTT 优化

默认 SceneCaptureComponent2D 是每帧 Tick 的,通常情况下可以取消每帧更新图像:

动画的 Update 频率在手机上每秒 30 次就够了,因此可以通过蓝图设置 SceneCaptureComponent2D 的 Tick 间隔设置:

接着在蓝图里手动调用 Capture 即可:

另外 SceneCaptureComponent2D 的 Render Target 的尺寸不要太大,有助于提高性能。

2.3.5 新功能

我们在 Battle Breakers 中新增了两个调试命令,可能会在 4.17 版本合并到主干上。游戏界面:

使用 Slate.ShowOverdraw 查看 Pixel Overdraw:

使用 Slate.ShowBatching 查看批次:

3 效果测试

我们做了一个测试工程用于测试优化效果,下图中的 UI 有 800 多个 Widget:

测试机器是千元机,机器参数如下:

开启 Invalidation Box 后,Slate Tick 时间大幅降低,由于应用程序开启了 Mobile HDR,瓶颈在 GPU 上,因此 FPS 提升不大,如下所示:

下图可以方便对比 Invalidation Box, Retainer Box, 事件驱动的 Retainer Box 开启后性能参数的变化(可以看到渲染线程的提升对于 FPS 提升很大): 

4 总结

大部分的 UI 优化工作(比如说 Invalidation Box, Retainer Box)都是在项目后期( UI 基本开发完成后)再进行的。UE4 提供了很丰富的功能和调试工具,熟练掌握这些功能能够帮助开发者实现高性能的UI。

2017-05-09 虚幻引擎

Unreal Open Day 2017 活动上 Epic Games 开发者支持工程师郭春飚先生为到场的开发者介绍了在 Unreal Engine 4 中 UI 的优化技巧,以下是演讲实录。

大家好,我是 Epic Games 的开发者支持工程师郭春飚,今天给大家介绍的是 UE4 的 UI 优化经验。我们之前一直有接到国内开发者的一些抱怨,他们觉得在手机上面开了 UI 以后性能下降的很快,今天就专门给大家介绍一下怎么在 UE4 上做 UI 优化。本文介绍的 UI 优化方式,不仅适用于移动平台,在其它平台上(如 PC 和主机)对于复杂的 UI 系统也会有很大的性能提升。

文章目录:

1 UI 的基本概念

1.1 名词解释

1.2 渲染流程

1.3 性能指标

2 优化方案

2.1 游戏线程优化

2.1.1 Invalidation Box

2.1.2 可见性(Widget Visibility)

2.1.3 Widget Binding

2.2 渲染线程优化

2.2.1 合并批次

2.2.2 Retainer Box

2.2.3 事件驱动的 Retainer Box

2.2.4 切换材质

2.3 其它优化

2.3.1 C++ 开发

2.3.2 Manager Class

2.3.3 释放贴图内存

2.3.4 3D RTT 优化

2.3.5 新功能

3 效果测试

4 总结

 

1. UI的基本概念

1.1 名词解释

User Widget:对应一个用户界面。

Widget Tree:每一个 User Widget 都是存储成树状结构。

Panel Widget:不会渲染出来,用于对 Child Widget 进行布局,如 Canva Panel, Grid Panel, Horizontal Box 等。

Common Widget:用于渲染,会生成到最后的 Draw Elements 中,如 Button, Image, Text 等。

1.2 渲染流程

基本渲染流程示意图:

在游戏线程 (Game Thread),Slate Tick 每一帧会遍历两次 Widget Tree。

Prepass:从下到上遍历树,计算每一个Widget的理想尺寸 (Desired Size)。

OnPaint:从上到下遍历树,计算渲染所需的 Draw Elements 。这个过程中,会根据 Common Widget 的类型和参数生成相应的 Vertex Buffer,将 Common Widget 的 Render Transform 计算到 Vertex Buffer 中,并根据 Layer ID 和 Material 等信息进行批次合并。最后一个 User Widget 会生成1个或多个 Draw Element,并将 Draw Elements 传递给渲染线程进行渲染,其中每个 Draw Element 对应一个 Draw Call。

在渲染线程 (Render Thread),Slate 渲染分为两步:

Widget Render:执行 UI 的 RTT,如果使用了 Retainer Box,这里会将 Draw Elements 渲染到 Retainer Box 的 Render Target。

Slate Render:将 Draw Elements 渲染到 Back Buffer,如果使用了 Retainer Box,会将 Retainer Box 对应的 Texture Resource 渲染到 Back Buffer。

1.3 性能指标

Stat.Slate命令列举了一些主要的Slate性能参数:

Num Painted Widgets:在游戏线程执行 OnPaint 的 Widget 数量。

Num Batches:Draw Element(也即 Draw Call)数量。

Stat.Slate 会创建一个未优化的 UI,并且统计线程会将这个 UI 的性能数据算入 Slate 开销,因此表格中的时间数据和真实数据相差很大。建议通过如下命令查看统计线程变量的时间开销:

stat dumpave –num=120 –ms=0.5

三个关键指标的统计数据分别是:

Slate Tick:统计线程变量 STAT_SlateTickTime。

Slate Render:统计线程变量 STAT_SlateRenderingRTTime。

Widget Render:统计线程变量 FWidgetRenderer_DrawWindow。

如果希望在项目中实时调试性能,可以从统计线程直接获取数据,并做一个简单的调试面板进行查看。

游戏线程代码:

统计线程代码:

调试面板效果:


2 优化方案

2.1 游戏线程优化

2.1.1 Invalidation Box

使用 Invalidation Box 封装 User Widget,从而缓存 Slate Tick 数据,不需要每帧都进行计算。操作方式如下所示:

在 Invalidation Box 下的所有 Prepass 和 OnPaint 计算结果都会被缓存下来。如果某个 Child Widget 的渲染信息发生变化,就会通知 Invalidation Box 重新计算一次 Prepass 和 OnPaint 更新缓存信息。

下图演示了一种特殊情况,英雄图标是一个重复使用的 User Widget,每个都被封装进了 Invalidation Box。整个英雄列表是一个 Scroll Box,当 Scroll Box 上下滑动时,英雄图标对应 User Widget 的 Transform 信息也会发生变化。

此时可以勾选 Invalidation Box 对应的 Cache Relative Transforms,如下所示:

那么当 User Widget 的位置变化时,引擎不会去更新所有的 Draw Element(即 Vertex Buffer ),而会通过修改 Shader 参数(View * Projection Matrix)来反应位置变化。这种方式仅适用于位置变化,如果缩放发生变化,仍然需要重新计算 Draw Element。Cache Relative  Transforms 会在 Game Thread 增加少量额外的计算,确保需要使用时才勾选。

当某个 Widget 的渲染信息变化时,会通知所在的 Invalidation Box 重新缓存 Vertex Buffer。在一个复杂的 User Widget 中,Invalidation Box 频繁缓存整个 Widget Tree 会带来很高的性能开销,有两种方式可以解决这个问题。

第一种方式是拆分 Invalidation Box,根据 Widget 变化是否频繁将它们拆分到不同的 Invalidation Box 中。

有时由于布局的原因,不是很方便的划分不同的 Invalidation Box,那么可以使用第二种方式,将 Widget 设定成 Is Volatile,这样上层的 Invalidation Box 在缓存时就会排除这个 Widget,该 Widget 每帧都会 Tick 并计算 Prepass 和 OnPaint,但整体 Widget Tree 的缓存不会受到影响。

上图中的 LevelUpIcon,平时处于隐藏状态,当角色升级时会显示出来, LevelUpAnim 通过改变 Widget 的位置实现动画效果。当渲染这个 Image 时,由于位置一直在变化,会导致 Invalidation Box 每帧都在重新计算整个 Widget Tree 的 Cache,性能比较低。此时可以将这个 Widget 设定成 Is Volatile,从而提高性能。

编辑器中 Is Volatile 选项可以用于显式地设置 Volatile,用于提高 Invalidation Box 的性能。有时 Widget Binding 会隐式地将 Widget 标记成 Volatile,导致这个 Widget 每帧都会 Tick,从而降低性能。

每个 Widget 在 ComputeVolatility 函数中详细列举了哪些属性会导致影响 Draw Element(Vertex Buffer)。

文本 Widget 影响 Draw Element 的属性:

进度条 Widget 影响 Draw Element 的属性

如果在影响 Draw Element 的属性上使用了 Widget Binding,会导致引擎每帧都要 Tick 查询是否属性发生变化,从而判断是否需要更新 Draw Element,因此应该避免使用 Widget Binding。

可以通过 Slate.InvalidationDebugging 查看是否正确地设置了 Invalidation Box 和 Volatile。

绿线框:使用 Invalidation Box 缓存的 Widget。

蓝线框:Invalidation Box 勾选了 Cache Relative Transforms。

虚线框: 标记为 Volatile 的 Widget。

红线框:没有使用 Invalidation Box 的 Widget。

Slate.AlwaysInvalidate 命令可以强制 Invalidation Box 每帧更新缓存,可以用于测试是否会造成突然的卡顿。如果一个 User Widget 过于复杂,可以拆分成多个 Invalidation Box,将 Widget 按照更新频率的高低放入不同的 Invalidtion Box。

2.1.2 可见性(Widget Visibility)

Widget 可见性有 5 种:

Visible: 可见、可点击

HitTestInvisible: 可见、当前 Widget 不可点击、所有 Child Widget 不可点击

SelfHitTestInvisible: 可见、当前 Widget 不可点击、不影响 Child Widget

Hidden: 不可见、占用布局空间

Collapsed: 不可见、不占用布局空间

很多 Widget 默认属性是 Visible,需要手动设置成 HitTestInvisible 和 SelfHitTestInvisible。如果大量 Widget 设置成 Visible,那么引擎在点击响应时的效率就会大大下降,这也会增加游戏线程的开销。

Collapsed 不占用布局空间(Layout Space),因此在隐藏后不会进行 Prepass 的计算,性能优于 Hidden。

可以使用 Widget Reflector 帮助检查是否有错误设置的 Visibility 属性。


2.1.3 Widget Binding

在分析 Volatile 时提到过 Widget Binding 会导致 Volatile 从而降低 UI 性能。另外 Widget Binding 是每帧 Tick 执行,性能比较低。不建议在项目中使用这个功能,建议通过 C++(或蓝图)调用函数的方式传值。

RemoveFromViewport/AddToViewport 会销毁以及重新构建 User Widget,使用 Collapsed/SelfHitTestInvisible 可以得到更好的性能。

另外,在移动平台上建议将蓝图 Tick 中复杂的运算逻辑移动到 C++ 中。

2.2 渲染线程优化

2.2.1 合并批次

随着 GPU 的发展,Draw Call 的数量对于性能的影响也越来越小,很多情况下减少 Draw Call 并不能带来 FPS 的提升。但减少 Draw Call 可以减少对 GPU 的 API 调用,在移动端有助于控制手机发热。


A. Panel Widget

在 4.15 之前的引擎版本,Canvas Panel 不支持批次合并,建议不要使用 Canvas Panel,尽量使用 Grid Panel、Vertical Box、Horizontal Box 等支持合并批次的容器。

4.15 增加了对 Canvas Panel 合并批次的支持,开启方式位于 Project Settings 中:"Engine->Slate Settings->Constraint Canvas->Explicit Canvas Child ZOrder"。接着可以通过设定 Canvas Panel 的 Child Widget 的 ZOrder 属性,ZOrder 相同(渲染参数也相同)的会合并批次,比起 Grid Panel 和 Horizontal Box,Canvas Panel 没有额外的布局计算,OnPaint 效率会稍微高一些(游戏线程)。

B. 合并贴图

在 UE4 中的 Sprite 很方便地支持合并贴图的编辑和使用。

如果需要在逻辑代码中切换独立贴图和合并贴图,在 Manager Class 中,初始化独立贴图 (UTexture2D) 和合并贴图资源 (UPaperSprite),并创建 FSlateBrush,通过 SetResourceObject 将资源设置给 FSlateBrush。接着就可以通过开关变量控制传入 UImage::SetBrush 的参数。

在项目后期,如果需要将 User Widget 中的贴图全部替换成合并贴图,是一项很繁琐的工作。Epic Games 的 Dmitriy Dyomin 提供了一个思路方便快速地进行替换。

首先实现一个 Commandlet:

可以使用如下命令运行这个 Commandlet:

Commandlet 的具体功能:遍历所有的 Widget Blueprint Asset,使用 AssetRegistry 加载 Asset,并检查其中 UImage 和 UBorder 使用的 Texture,根据命名规则判断是否有对应的 Sprite Asset 存在。使用 AssetRegistry 将 Texture 替换成 Sprite,最后保存 Widget Blueprint Asset。

2.2.2 Retainer Box

通过合并批次和合并贴图的方式,UI 的 Draw Call 数量可能减少到比较低,但仍然会有很高的像素填充率。

在很多情况下,UI 不需要每帧都渲染,因此可以通过 Retainer Box 缓存渲染结果,每隔几帧更新一次。Retainer Box 的原理就是将 UI 渲染缓存在 Render Target上,再将  Render Target 渲染到屏幕。

下图中,我们将主界面的 UI 划分到 4 个 Retainer Box 中,通过间隔3帧更新一次的方式来渲染。

Retainer Box 区域应该尽量小,有助于提高渲染效率、降低显存使用。通常 Retainer Box 都应该包含 User Widget 的背景图,因为背景图有很大的像素填充率。

Retainer Box 会为每个 User Widget 实例创建一个 Render Target, 因此在不改动代码的情况下,重复使用的 User Widget 不要使用 Retainer Box。例如下图中,我们应该为 Scroll Box 所在的 User Widget 创建 Retainer Box,而不应该为 Scroll Box Item 所在的 User Widget 创建 Retainer Box。

下图演示了另外一种情况,B_HeroIcon 这个 User Widget 被重复用到了 HEROS 和 SOCIAL 等多个主界面中。Battle Breakers 是一个重 UI 的手机游戏,因此很难为所有的主界面分配 Retainer Box,这会占用大量的显存,当然我们也不希望为每个 B_HeroIcon 创建一个 Retainer Box。

此时可以通过扩展代码的方式实现更好的 Retainer Box 效果,假设我们知道该 B_HeroIcon 在画面中同时出现的上限是 20,那么可以创建一个包含 20 个 Render Target 的 Render Target Pool,使得不同的 Retainer Box 可以共享同一个 Render Target。

Retainer Box 会占用额外的显存,因此要控制使用量,将它优先分配给性能提升最大的 User Widget。一种情况是主界面的 User Widget,另一个种情况是使用共享 Render Target 后的大量频繁使用的 User Widget。

使用 Retainer Box 不但能提高渲染线程的效率,游戏线程的 Tick 也会相应的隔几帧执行一次。如果 Retainer Box 内部包含了可以点击的 Widget,那么需要将 Retainer Box 设置成 Visible,这样引擎会将点击测试区域映射到 Retainer Box 上。

持续表示的效果(如3D 角色、材质特效)可以从 Retainer Box 中分离出来,但需要注意像素填充率,也可以从特效设计的方面解决。

Invalidation Box 放置在 Retainer Box 上方没有意义,通常做法是在 Retainer Box 下层放一个 Invalidation Box。

在设定 Retainer Box 的 Phase Count 时需要全局考虑。例如下图表示每隔3帧更新一次 Retainer Box,并在第 0 帧更新:

下图表示每隔 5 帧更新一次,并在第 2 帧更新:

那么每隔15帧这两个 Retainer Box 就会在一帧内同时更新,导致帧数下降。

2.2.3 事件驱动的 Retainer Box

目前 Retainer Box 需要指定每隔几帧强制更新一次,但某些情况下 User Widget 不需要按照固定频率更新,只会在用户操作(且操作不频繁)时才更新。这种情况下就可以通过扩展 Retainer Box 来支持事件驱动的方式。

实现思路是继承 URetainerBox 和 SRetainerWidget,并在 PaintRetainedContent(在 4.16 之前的版本函数名是 OnTickRetainers)中判断是否有事件触发更新,如果需要更新则调用父类的 PaintRetainedContent,否则 return。

2.2.4 切换材质

UE4 提供了丰富的材质效果,在低端机上可以考虑关闭这些效果、或切换到低配材质以提升性能。

可以使用引擎提供的 DYNAMIC_MULTICAST 框架,将所有受影响的 Widget 绑定到一个开关变量上,实现整体切换。

2.3 其它优化

2.3.1 C++ 开发

除了 UI 动画这块存储结构设计的原因不能使用 C++ 实现,其它 UI 功能都可以用 C++ 实现。

第一步,实现一个 C++ 类 UWExpHeroIcon 继承自 UUserWidget

第二步,使用 Reparent Blueprint 修改父类为 UWExpHeroIcon

第三步,在编辑器中找到需要暴露的变量以及类型

第四步,在 C++ 中声明 BindWidget 变量,引擎会自动关联数据

2.3.2 Manager Class

建议在项目中创建一个 Manager Class,统一管理所有的 User Widget,并且统一管理所有的 UI 资源,比如 Brush、Font 等。Manager Class 可以是 C++ 或蓝图的形式。

2.3.3 释放贴图内存

释放贴图内存的一个前提是不要在编辑中设置贴图(下图中的 Image 项),而是通过程序进行手动的贴图加载、贴图设置、以及贴图销毁。不在编辑器中设置贴图,可以避免在 CDO(Class Default Object)中引用这个贴图对象。CDO 的引用会使得 SharedPtr 的引用计数至少为1,并且退出应用前不会销毁。

如果在 Editor 中设置了 Image 属性,同时又希望销毁这个贴图,Epic Games 的王弥提供了一个思路,可以在 Cook 阶段解除 UImage 和 UTexture 的引用关系,从而这个 User Widget 的 CDO 不会引用到 UTexture。

解除 Cook 阶段引用关系的代码如下所示:

加载贴图的代码如下所示:

释放贴图的代码如下所示:

2.3.4 3D RTT 优化

默认 SceneCaptureComponent2D 是每帧 Tick 的,通常情况下可以取消每帧更新图像:

动画的 Update 频率在手机上每秒 30 次就够了,因此可以通过蓝图设置 SceneCaptureComponent2D 的 Tick 间隔设置:

接着在蓝图里手动调用 Capture 即可:

另外 SceneCaptureComponent2D 的 Render Target 的尺寸不要太大,有助于提高性能。

2.3.5 新功能

我们在 Battle Breakers 中新增了两个调试命令,可能会在 4.17 版本合并到主干上。游戏界面:

使用 Slate.ShowOverdraw 查看 Pixel Overdraw:

使用 Slate.ShowBatching 查看批次:

3 效果测试

我们做了一个测试工程用于测试优化效果,下图中的 UI 有 800 多个 Widget:

测试机器是千元机,机器参数如下:

开启 Invalidation Box 后,Slate Tick 时间大幅降低,由于应用程序开启了 Mobile HDR,瓶颈在 GPU 上,因此 FPS 提升不大,如下所示:

下图可以方便对比 Invalidation Box, Retainer Box, 事件驱动的 Retainer Box 开启后性能参数的变化(可以看到渲染线程的提升对于 FPS 提升很大): 

4 总结

大部分的 UI 优化工作(比如说 Invalidation Box, Retainer Box)都是在项目后期( UI 基本开发完成后)再进行的。UE4 提供了很丰富的功能和调试工具,熟练掌握这些功能能够帮助开发者实现高性能的UI。

Unreal Engine 4 中的 UI 优化技巧相关推荐

  1. 网络推广软文浅谈网站优化过程中图片的优化技巧和注意事项!

    在网站优化的过程中,网络推广软文介绍到不仅要做好关键词的优化,也更要做好图片优化,在网站不断地利用图文模式时,做好图片优化对网站的排名提升也有很大的帮助.那么对于图片优化都有哪些技巧呢?下面网络推广软 ...

  2. Android产品研发(二十一)--Android中的UI优化

    转载请标明出处:一片枫叶的专栏 上一篇文章中我们讲解了Android产品研发过程中的代码Review.通过代码Review能够提高产品质量,增强团队成员之间的沟通,提高开发效率,所以良好的产品开发迭代 ...

  3. 如何做网络营销推广之网站SEO中title标签优化技巧!

    众所周知,title标签的优化也非常重要,因为它能帮助网站提升更多的权重.如何做网络营销推广表示,title标签SEO优化技术包括标题长度控制.关键词分布.关键字词频及关键字组合技巧等.下面如何做网络 ...

  4. Unreal Engine 虚幻引擎,性能优化

    一.Frame 帧 帧的时间并不是其他数字的总和,并不是其他时间的总和.原因就是,这些线程,它们都是不同的线程,是并行运行的,每个线程都是按照顺序的,它们需要上一个线程的内容和结果. 二.理想情况下, ...

  5. @程序员,一文掌握 Web 应用中的图片优化技巧!

    作者 | fecoder 责编 | 郭芮 这篇文章,我们将一起探讨,Web应用中能对图片进行什么样的优化,以及反思一些"负优化"手段. 为什么要对图片进行优化? 对于大多数前端工程 ...

  6. android中Listview的优化技巧

    2019独角兽企业重金招聘Python工程师标准>>> 减少内存中view对象的创建个数(明显提高效率)       复用历史缓存的view对象,减少Android内存消耗 减少子孩 ...

  7. 介绍Unreal Engine 4中的接口(Interface)使用C++和蓝图

    这个教程是从UE4 Wiki上整理而来. 在C++中直接使用Interface大家应该很熟悉.只是简单先定义一个个有虚函数的基类,然后在子类中实现相应的虚函数.像这样的虚函数的基类一般概念上叫接口.那 ...

  8. php中对象的遍历输出,PHP中的对象遍历技巧

    PHP中的对象遍历 对象的遍历,主要是指遍历对象中的,对外部可见属性.实际上就是用访问限制符public声明的属性,这点大家肯定很熟悉了.并且,在php中,遍历对象居然与遍历数组一样,都可以用使用fo ...

  9. 零基础Unreal Engine 4(UE4)图文笔记之准备篇(一)

    简介:十篇文章介绍虚幻4引擎的入门和基本内容,蓝图.材质.粒子效果.UI界面等 系列目录 准备篇 基本操作 材质入门 初步运行 简易交互门 多种开门方式 相对坐标.绝对坐标.世界坐标的含义 UI 粒子 ...

最新文章

  1. 怎么编写段错误(Segmentation fault)的程序
  2. 编译项目的时候,不会编译依赖的类库项目
  3. bootstrap table 的简单Demo
  4. hive java导入CVS
  5. 根据周次显示日期范围_Elasticsearch根据日期价格范围搜索酒店且排序
  6. Servlet-Response直接返回一个文件并且在浏览器上显示
  7. ADO.NET 【攻击及防御】
  8. wordpress如何防止发布文章时候自动清除P、br换行标签
  9. 程序员毕业实习报告,5000字
  10. 【T3】打印凭证没有任何反应
  11. 在椭球面上使用球面坐标系法做三重积分
  12. 华硕路由器,创建虚拟内存 U 盘读写速度不够,在设置里开启usb3.0
  13. python正则表达式提取字符串中的书名_使用python正则表达式从字符串中提取名称...
  14. 0 win10重装partition_win10换win7,U盘装机错误,Error:partition 0 ended too near怎么回事?怎么修?...
  15. Open_cv中常用函数的原型及参数解释(一)
  16. Kafka学习笔记: Kafka 百惑梳理
  17. Centos7 双网卡配置
  18. 为什么把文字图片放大后有一圈彩色的像素方块,而不是纯黑色的
  19. 全职宝妈卖出去5万多元的保暖袜子 只用这一招截流
  20. upload-labs靶场通关解密

热门文章

  1. eclipse和idea代码通用吗_25个JavaScript代码简写技巧(下篇)
  2. 服务器按ctrl alt delete没有用_详细教程——用PS制作直邮广告
  3. mysql filck_顺序全局id生成方案-flickr(转载)
  4. 【论文阅读】Deep Neural Networks for Learning Graph Representations | day14,15
  5. Python中import导入模块
  6. 转换到bot模式失败_《认知觉醒》:为什么越“努力”,会越失败
  7. python常用的库_这几个常用的python库你需要知道
  8. java嵌套类中的方法怎么调用_java类与嵌套嵌套后,怎么使用最外层的类建立对象后使用内部类的方法?...
  9. java应用程序怎样获取外接设备信号 通过usb
  10. redis介绍与使用