服务热线
131-1198-7613
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 总结... 34
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:
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。
2024-03-20
网页设计,是根据企业希望向浏览者传递的信息(包括产品、服务、理念、文化),进行网站功能策划,然后进行···
2024-03-19
网页设计,是根据企业希望向浏览者传递的信息(包括产品、服务、理念、文化),进行网站功能策划,然后进行···
2024-03-19
网页设计,是根据企业希望向浏览者传递的信息(包括产品、服务、理念、文化),进行网站功能策划,然后进行···
2024-03-19
网页设计,是根据企业希望向浏览者传递的信息(包括产品、服务、理念、文化),进行网站功能策划,然后进行···