版本检查:2017.3
-
难度:高级
本章讨论构建Unity UI的更广泛问题。
可以采取两种措施来减轻GPU片段管道的压力:
由于UI着色器通常是标准化的,因此最常见的问题是过度使用填充率过高。这通常是由于大量重叠的UI元素和/或具有占据屏幕的重要部分的多个UI元素。这两个问题都可能导致极高的透支度。
为了减少填充率过度使用并减少过度抽取,请考虑以下可能的补救措施。
需要最少重新设计现有UI元素的方法是简单地禁用播放器不可见的元素。最常见的情况是打开具有不透明背景的全屏UI。在这种情况下,可以禁用放置在全屏UI下方的任何UI元素。
最简单的方法是禁用包含不可见UI元素的根GameObject或GameObjects。有关其他解决方案,请参阅“ 禁用画布”部分。
最后,通过将其alpha设置为0来确保没有隐藏UI元素,因为该元素仍将被发送到GPU并且可能需要宝贵的渲染时间。如果UI元素不需要Graphic组件,您只需删除它,光线投射仍然可以工作。
为了减少重建和呈现UI所需的时间,保持UI对象的数量尽可能低是很重要的。尝试尽可能多地烘焙食物。例如,不要仅使用混合GameObject将色调更改为元素,而是通过材质属性来执行此操作。此外,不要创建像文件夹那样的游戏对象,除了组织场景之外没有其他目的。
如果打开具有不透明背景的全屏UI,则世界空间相机仍将在UI后面呈现标准3D场景。渲染器不知道全屏Unity UI会遮挡整个3D场景。
因此,如果打开一个完全全屏的UI,禁用任何和所有模糊的世界空间相机将通过简单地消除渲染3D世界的无用工作来帮助减少GPU压力。
如果UI未覆盖整个3D场景,您可能希望将场景渲染到纹理一次并使用它而不是连续渲染它。您将无法在3D场景中看到动画内容,但这在大多数情况下都是可以接受的。
注意:如果将画布设置为“屏幕空间 - 叠加”,则无论场景中活动的摄像机数量如何,都将绘制它。
许多“全屏”用户界面实际上并不掩盖整个3D世界,但却让世界的一小部分可见。在这些情况下,捕获渲染纹理中可见的世界部分可能更为理想。如果世界的可见部分在渲染纹理中“缓存”,则可以禁用实际的世界空间相机,并且可以在UI屏幕后面绘制缓存的渲染纹理以提供3D世界的冒名顶替版本。
设计人员通过合成创建UI非常常见 - 组合和分层标准背景和元素以创建最终UI。虽然这样做相对简单,并且对迭代非常友好,但由于Unity UI使用了透明渲染队列,因此它不具备高性能。
考虑一个带有背景,按钮和按钮上的一些文本的简单UI。因为透明队列中的对象是从后向前排序的,所以在像素落在文本字形内的情况下,GPU必须对背景的纹理,按钮的纹理,最后是文本图集的纹理进行采样,总计为三个样本。随着UI的复杂性增加,并且更多的装饰元素被分层到背景上,样本的数量可以迅速增加。
如果发现大的UI是填充率绑定的,那么最好的办法是创建专门的UI精灵,将UI的装饰/不变元素合并到其背景纹理中。这减少了必须彼此层叠以实现所需设计的元素数量,但是劳动密集型并且增加了项目纹理图集的大小。
将创建给定UI所需的分层元素数量缩减到专用UI精灵上的这一原则也可用于子元素。考虑具有滚动产品窗格的商店UI。每个产品UI元素都有边框,背景和一些图标来表示价格,名称和其他信息。
商店UI需要背景,但由于其产品在背景中滚动,因此产品元素无法合并到商店UI的背景纹理中。但是,产品UI元素的边框,价格,名称和其他元素可以合并到产品的背景中。根据图标的大小和数量,可以节省大量的填充率。
组合分层元素有几个缺点。专业元素无法再重复使用,需要额外的艺术家资源才能创建。添加大的新纹理可能会显着增加保存UI纹理所需的内存量,尤其是在未按需加载和卸载UI纹理的情况下。
Unity UI使用的内置着色器包含对屏蔽,剪切和许多其他复杂操作的支持。由于这种增加的复杂性,与较低端设备(如iPhone 4)上的简单Unity 2D着色器相比,UI着色器的性能较差。
如果针对低端设备的应用程序不需要屏蔽,剪切和其他“奇特”功能,则可以创建省略未使用操作的自定义着色器,例如此最小UI着色器:
着色器“UI /快速默认”
{
属性
{
[PerRendererData] _MainTex(“Sprite Texture”,2D)=“white”{}
_Color(“Tint”,Color)=(1,1,1,1)
}
SubShader
{
标签
{
“队列” =“透明”
“IgnoreProjector”= “真”
“RenderType”= “透明”
“预览类型” =“平面”
“CanUseSpriteAtlas”= “真”
}
剔除
点亮
ZWrite关闭
ZTest [unity_GUIZTestMode]
混合SrcAlpha OneMinusSrcAlpha
通过
{
CGPROGRAM
#pragma vertex vert
#pragma片段frag
#include“UnityCG.cginc”
#include“UnityUI.cginc”
struct appdata_t
{
float4 vertex:POSITION;
float4 color:COLOR;
float2 texcoord:TEXCOORD0;
};
struct v2f
{
float4 vertex:SV_POSITION;
fixed4颜色:COLOR;
half2 texcoord:TEXCOORD0;
float4 worldPosition:TEXCOORD1;
};
fixed4 _Color;
fixed4 _TextureSampleAdd;
v2f vert(appdata_t IN)
{
v2f OUT;
OUT.worldPosition = IN.vertex;
OUT.vertex = mul(UNITY_MATRIX_MVP,OUT.worldPosition);
OUT.texcoord = IN.texcoord;
#ifdef UNITY_HALF_TEXEL_OFFSET
OUT.vertex.xy + =(_ ScreenParams.zw-1.0)* float2(-1,1);
#万一
OUT.color = IN.color * _Color;
返回OUT;
}
sampler2D _MainTex;
fixed4 frag(v2f IN):SV_Target
{
return(tex2D(_MainTex,IN.texcoord)+ _TextureSampleAdd)* IN.color;
}
ENDCG
}
}
}
要显示任何UI,UI系统必须为屏幕上显示的每个UI组件构建几何。这包括运行动态布局代码,生成多边形以表示UI文本字符串中的字符,以及将尽可能多的几何体合并到单个网格中以最小化绘制调用。这是一个多步骤的过程,本指南开头的基础部分对此进行了详细介绍。
由于两个主要原因,Canvas重建可能会成为性能问题:
随着Canvas上元素数量的增加,这两个问题都会变得尖锐。
重要提示:每当给定Canvas上的任何可绘制UI元素发生更改时,Canvas必须重新运行批处理构建过程。此过程重新分析Canvas上的每个可绘制UI元素,无论它是否已更改。请注意,“更改”是影响UI对象外观的任何更改,包括分配给精灵渲染器的精灵,变换位置和比例,文本网格中包含的文本等。
Unity UI是从前到后构建的,层次结构中的对象顺序决定了它们的排序顺序。层次结构中较早的对象将在层次结构中稍后的对象后面考虑。通过从上到下遍历层次结构并收集使用相同材料,相同纹理且没有中间层的所有对象来构建批次。“中间层”是具有不同材料的图形对象,其边界框与两个其他可混合对象重叠,并且放置在两个可混合对象之间的层次结构中。中间层迫使批次被破坏。
如Unity UI概要分析工具部分所述,UI概要分析器和框架调试器可用于检查中间层的UI。这是一个可绘制对象插入另外两个可绘制对象的可绘制对象之间的情况。
当文本和精灵彼此靠近时,最常出现此问题:文本的边界框可以无形地与附近的精灵重叠,因为文本字形的多边形大部分都是透明的。这可以通过两种方式解决:
这两个操作都可以在Unity Editor中执行,Unity Frame Debugger打开并启用。通过简单地观察Unity Frame Debugger中可见的绘制调用的数量,可以找到最小化由于UI元素重叠而浪费的绘制调用的数量的顺序和位置。
除了最琐碎的情况之外,通常通过将元素移动到子画布或兄弟画布来分割画布通常是个好主意。
兄弟画布最适用于UI的某些部分必须将其绘制深度与UI的其余部分分开控制的情况,始终高于或低于其他层(例如教程箭头)。
在大多数其他情况下,Sub-canvas从他们的父Canvas继承其显示设置时更方便。
虽然乍一看似乎是将UI细分为多个Sub-canvas的最佳实践,但请记住,Canvas系统也不会将批处理组合在单独的画布中。高性能UI设计需要在最小化重建成本和最大限度地减少浪费的绘制调用之间取得平衡。
因为Canvas在其任何组成可绘制组件的任何时候都会重新更改,所以通常最好将任何非平凡的Canvas拆分为至少两个部分。此外,如果期望元素同时改变,最好尝试在同一画布上共同定位元素。一个例子可能是进度条和倒数计时器。这些都依赖于相同的底层数据,因此需要同时进行更新,因此它们应该放在同一个Canvas上。
在一个画布上,放置所有静态且不变的元素,例如背景和标签。这些将在首次显示Canvas时批量处理一次,然后不再需要重新加载。
在第二个画布上,放置所有“动态”元素 - 经常变化的元素。这将确保此Canvas主要重新调整脏元素。如果动态元素的数量变得非常大,则可能需要进一步将动态元素细分为一组不断变化的元素(例如,进度条,定时器读数,任何动画)以及仅偶尔改变的一组元素。
实际上这实际上相当困难,尤其是在将UI控件封装到预制件中时。许多UI选择通过将昂贵的控件拆分到子画布上来细分Canvas。
在Unity 5.2中,批处理代码基本上被重写,与Unity 4.6,5.0和5.1相比,性能更高。此外,在具有1个以上核心的设备上,Unity UI系统会将大部分处理移动到工作线程。通常,Unity 5.2减少了将UI积极地分成几十个子画布的需要。移动设备上的许多UI现在可以使用少至两个或三个画布来实现。
有关Unity 5.2中的优化的更多信息,请参阅此博客文章。
默认情况下,Unity UI使用Graphic Raycaster组件来处理输入事件,例如触摸事件和指针悬停事件。这通常由独立输入管理器组件处理。尽管名称如此,独立输入管理器仍然是一个“通用”输入管理器系统,它将处理指针和触摸。
在Unity 5.4之前,每个附加了图形射频场的活动Canvas将每帧运行一次光线投射,以检查指针的位置,只要当前没有可用的触摸输入。这将发生在任何平台; 没有鼠标的iOS和Android设备仍将查询鼠标的位置,并尝试发现哪个UI元素位于该位置下方,以确定是否需要发送任何悬停事件。
这浪费了CPU时间,并且目睹了占用Unity应用程序CPU帧时间的5%或更多。
Unity 5.4中已解决此问题。从5.4开始,没有鼠标的设备将不会查询鼠标位置,也不会执行不必要的光线投射。
如果使用早于5.4的Unity版本,强烈建议移动开发人员创建自己的Input Manager类。这可以像从Unity UI源复制Unity的标准输入管理器并注释掉ProcessMouseEvent方法以及对该方法的所有调用一样简单。
Graphic Raycaster是一个相对简单的实现,它迭代所有将“Raycast Target”设置为true的Graphic组件。对于每个Raycast目标,Raycaster执行一组测试。如果Raycast目标通过了所有测试,则会将其添加到命中列表中。
Raycast实现细节
测试是:
然后,按照深度对命中的Raycast目标列表进行排序,对反向目标进行过滤,并进行过滤以确保删除在摄像机后面呈现的元素(即在屏幕中不可见)。
如果在Graphic Raycaster的“阻挡对象”属性上设置了相应的标志,则Graphic Raycaster也可以将光线投射到3D或2D物理系统中。(从脚本开始,该属性被命名为blockingObjects。)
如果启用了2D或3D阻挡对象,那么在光线投射阻挡物理层上的2D或3D对象下方绘制的任何Raycast目标也将从命中列表中删除。
然后返回最终的命中列表。
光线投射优化技巧
鉴于必须由Graphic Raycaster测试所有Raycast目标,最佳做法是仅在必须接收指针事件的UI组件上启用“Raycast Target”设置。Raycast目标列表越小,必须遍历的层次越浅,每次Raycast测试的速度就越快。
对于具有必须响应指针事件的多个可绘制UI对象的复合UI控件,例如希望其背景和文本都改变颜色的按钮,通常最好将单个Raycast目标放在复合UI的根部控制。当该单个Raycast目标接收到指针事件时,它可以将事件转发到复合控件内的每个感兴趣的组件。
层次结构深度和光线投射过滤器
在搜索光线投射过滤器时,每个Graphic Raycast都会遍历Transform层次结构。此操作的成本与层次结构的深度成比例地线性增长。必须测试附加到层次结构中每个Transform的所有组件,看它们是否实现了ICanvasRaycastFilter,因此这不是一个便宜的操作。
有几个标准的Unity UI组件使用ICanvasRaycastFilter,例如CanvasGroup,Image,Mask和RectMask2D,因此这种遍历不能简单地删除。
子画布和OverrideSorting属性
Sub画布上的overrideSorting属性将导致Graphic Raycast测试停止攀爬变换层次结构。如果可以在不导致排序或光线投射检测问题的情况下启用它,则应该使用它来降低光线投射层次结构遍历的成本。