Optimizing Unity UI(四):Fill-rate, Canvases and input

版本检查: 2017.3-难度: 高级

本章讨论了构建UnityUI的更广泛问题。

Remediating fill-rate issues 补救填充率问题

为了减少GPU碎片管道的压力,可以采取两种行动:

  • 减少碎片着色器的复杂性。有关详细信息,请参阅“UI shaders and low-spec devices”一节。
  • 减少必须采样的像素数。

由于UI着色器通常是标准化的,最常见的问题就是过度使用填充率。这最常见的原因是大量重叠的UI元素和/或占用屏幕重要部分的多个UI元素。这两个问题都可能导致极高水平的透支。

为了减少填充率的过度使用和减少透支,请考虑以下可能的补救措施。

Eliminating invisible UI

最不需要重新设计现有UI元素的方法是简单地禁用对播放机不可见的元素。适用于此的最常见的情况是打开背景不透明的全屏UI。在这种情况下,放置在全屏UI下的任何UI元素都可以被禁用。

最简单的方法是禁用根GameObject或包含不可见UI元素的GameObjects。有关替代解决方案,请参阅Disabling Canvases部分。

最后,通过将UI元素的alpha设置为0,确保不隐藏UI元素,因为元素仍将被发送到GPU,并且可能会花费宝贵的呈现时间。如果UI元素不需要图形组件,您可以简单地删除它,光线投射仍然有效。

Simplify UI structure

为了减少重建和呈现UI所需的时间,必须尽可能减少UI对象的数量。尽可能多地烘焙东西。例如,不要只使用混合GameObject来将色调更改为元素,而是通过材质属性来实现这一点。另外,不要创建像文件夹一样的游戏对象,除了组织你的场景没有别的目的。

Disabling invisible camera output

如果一个具有不透明背景的全屏用户界面被打开,世界空间相机仍将呈现UI背后的标准3D场景。渲染器不知道全屏Unity用户界面会模糊整个3D场景。

因此,如果一个完全的全屏用户界面被打开,禁用任何和所有模糊的世界空间相机将有助于减少GPU的压力,简单地消除无用的工作,渲染3D世界。

如果UI没有覆盖整个3D场景,您可能希望将场景渲染一次并使用它,而不是不断地呈现它。你将失去在3D场景中看到动画内容的可能性,但这在大多数情况下应该是可以接受的。

注意:如果画布被设置为“Screen Space – Overlay”,那么它将被绘制,而不管在场景中活动的摄像机的数量。

Majority-obscured cameras

许多“full-screen”UI实际上并不掩盖整个3D世界,而是使世界的一小部分可见。在这些情况下,仅捕捉在渲染纹理中可见的世界部分可能更理想。如果世界的可见部分在渲染纹理中“cached”,则可以禁用实际世界空间摄影机,并且可以在UI屏幕后面绘制缓存的渲染纹理,以提供3D世界的说明性版本。

Composition-based UIs

这是非常常见的设计师创建UI通过组合和分层的标准背景和元素,以创建最终的UI。虽然这样做相对简单,并且对迭代非常友好,但由于UnityUI使用了透明的呈现队列,这是不符合性能的。

考虑一个简单的UI,它有一个背景,一个按钮和一些文本在按钮上。由于透明队列中的对象是从后到前排序的,在像素落入文本字形的情况下,GPU必须对背景的纹理进行采样,然后是按钮的纹理,最后是文本地图集的纹理,总共需要三个样本。随着UI的复杂性的增加,以及更多的装饰元素被层叠在背景上,样本的数量会迅速增加。

如果发现一个大型UI是填充率绑定的,最好的方法是创建专门的UI精灵,将UI的许多装饰/不变元素合并到其背景纹理中。这减少了元素的数量,这些元素必须彼此分层才能实现所需的设计,但这是劳动密集型的,并且增加了项目纹理地图集的大小。

将创建给定UI所需的分层元素的数量浓缩到专用UI精灵上的原则也可用于子元素。考虑一个带有产品滚动窗格的商店UI。每个产品UI元素都有一个边框、一个背景和一些图标来表示价格、名称和其他信息。

存储UI将需要一个背景,但由于其产品在后台滚动,产品元素不能合并到存储UI的背景纹理中。但是,可以将产品UI元素的边框、价格、名称和其他元素合并到产品的背景中。根据图标的大小和数量,填充率节省可以相当可观。

组合分层元素有几个缺点。专业元素不能再被重用,需要额外的艺术家资源来创建。添加大的新纹理可能会显著增加容纳UI纹理所需的内存数量,特别是在UI纹理没有按需加载和卸载的情况下。

UI shaders and low-spec devices

Unity用户界面使用的内置着色器包含了对掩蔽、裁剪和许多其他复杂操作的支持。由于这种增加的复杂性,UI着色器的性能不如简单的Unity2D着色器在诸如iphone 4这样的低端设备上。

如果针对低端设备的应用程序不需要掩蔽、裁剪和其他“fancy”功能,则可以创建一个自定义着色器来省略未使用的操作,例如这个最小的UI着色器:

Shader "UI/Fast-Default"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)
    }

    SubShader
    {
        Tags
        { 
            "Queue"="Transparent" 
            "IgnoreProjector"="True" 
            "RenderType"="Transparent" 
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Cull Off
        Lighting Off
        ZWrite Off
        ZTest [unity_GUIZTestMode]
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment 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    : 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);
                #endif
                
                OUT.color = IN.color * _Color;
                return OUT;
            }

            sampler2D _MainTex;
            fixed4 frag(v2f IN) : SV_Target
            {
                return (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
            }
        ENDCG
        }
    }
}

UI Canvas rebuilds

要显示任何UI,UI系统必须为屏幕上表示的每个UI组件构造几何.这包括运行动态布局代码,生成多边形以表示UI文本字符串中的字符,以及将尽可能多的几何图形合并到单个网格中,以最小化绘制调用。这是一个多步骤的过程,在本指南开头的基础部分详细描述。

由于两个主要原因,画布重构可能成为性能问题:

  • 如果画布上可绘制UI元素的数量很大,那么计算批处理本身就会变得非常昂贵。这是因为对元素进行排序和分析的成本与画布上可绘制的UI元素的数量成线性关系。
  • 如果画布经常被弄脏,那么可能会花费过多的时间来刷新更改相对较少的画布。

随着画布上元素数量的增加,这两个问题都变得尖锐起来。

重要提示:当给定画布上的任何可绘制UI元素发生更改时,画布必须重新运行批处理构建过程。此过程重新分析画布上的每个可绘制UI元素,不管它是否已更改。请注意,“change”是指影响UI对象外观的任何更改,包括分配给sprite呈现器的sprite、转换位置和比例、文本网格中包含的文本等。

Child order

UnityUI是从后到前构造的,对象在hierarchy中的顺序决定了它们的排序顺序。hierarchy中较早的对象在层次结构的后面被认为是对象后面的对象。批次是通过从上到下的hierarchy和收集所有使用相同的材料,同样的纹理和没有中间层的对象来构建的。“中间层”是一个具有不同材料的图形对象,其边界框重叠于两个其他可击对象,并放置在两个可击对象之间的层次结构中。中间层迫使批次破碎。

正如在UnityUI分析工具部分中所讨论的,UI分析器和框架调试器可以用于检查UI的中间层。在这种情况下,一个可绘制对象会在另两个其他可绘制对象之间进行交互,否则这些对象就会变得易碎。

这个问题最常见的情况是文本和精灵相互靠近:文本的边界框可以在附近的SPTER中不可见地重叠,因为文字字形的多边形很大程度上是透明的。可以通过两种方式解决这一问题:

  • 重新排序可绘制对象,使可击对象不被非可击对象插入;也就是说,将不可击对象移动到可击对象的上方或下面。
  • 调整物体的位置以消除不可见的重叠空间。

这两个操作都可以在UnityEditor中执行,同时还可以打开并启用UnityFrameDebugger。通过简单地观察在UnityFrameworkDebugger中可见的绘图调用的数量,就可以找到一个顺序和位置,以最小化由于UI元素重叠而浪费的绘制调用的数量。

Splitting Canvases

除了最简单的情况外,在所有情况下,将一个画布分开通常是一个好主意,要么将元素移动到子画布上,要么将元素移动到同级画布上。

兄弟画布最好是在这样的情况下使用,即UI的某些部分必须与UI的其他部分分开控制其绘制深度,使其始终在其他层之上或下面(例如教程箭头)。

在大多数其他情况下,子画布是更方便的,因为他们继承他们的显示设置从他们的父画布。

虽然乍一看,将UI细分为多个子画布是最好的做法,但请记住,画布系统也不会在不同的画布上合并批次。Performant UI设计需要在最小化重建成本和尽量减少浪费的绘图调用之间取得平衡。

General guidelines

因为一个画布在任何一个组成的可绘制组件发生变化时都会重新批次,所以通常最好将任何非平凡的画布分割成至少两个部分。此外,如果期望元素同时发生变化,最好尝试在同一画布上共同定位元素。例如,一个进度条和一个倒计时器。它们都依赖于相同的底层数据,因此需要同时更新,因此它们应该放在相同的画布上。

在一个画布上,放置所有静态和不变的元素,例如背景和标签。这些将批处理一次,当画布第一次显示,然后将不再需要重新批处理之后。

在第二个画布上,放置所有的“dynamic”元素-那些频繁变化的元素。这将确保这个画布主要是脏元素。如果动态元素的数量增长非常大,则可能需要进一步将动态元素细分为一组不断变化的元素(例如进度条、计时器读出、任何动画)和一组仅偶尔发生变化的元素。

实际上,这在实践中是相当困难的,特别是当将UI控件封装到预制件中时。许多UI选择细分画布,将更昂贵的控件分割到子画布上。

Unity 5.2 and Optimized Batching

在Unity5.2中,批处理代码被大量重写,与Unity4.6、5.0和5.1相比性能要好得多。此外,在具有多个核心的设备上,UnityUI系统将大部分处理移到工作线程上。一般来说,Unity5.2减少了将UI分割成数十个子画布的需要。移动设备上的许多UI现在只需两三张画布就可以表演。

有关Unity5.2中的优化的更多信息,请参见 this blog post.

Input and raycasting in Unity UI

默认情况下,UnityUI使用 Graphic Raycaster 组件来处理输入事件,例如触摸事件和指针悬停事件。这通常由独立输入管理器组件来处理。尽管名称,独立的输入管理器意味着是一个“通用”的输入管理器系统,并将处理指针和触摸。

Erroneous mouse input detection on mobile (5.3)

在Unity 5.4之前,每个活动Canvas与一个Graphic Raycaster将运行每帧一次光线投射,以检查指针的位置,只要目前没有可用的触摸输入。不管平台如何,iOS和Android设备都会出现这种情况;没有鼠标的iOS和Android设备仍然会查询鼠标的位置,并试图发现哪个UI元素位于该位置之下,以确定是否需要发送任何悬停事件。

这是对CPU时间的浪费,并且已经见证了使用Unity 应用程序的CPU帧时间的5%或更多。

这一问题在Unity 5.4中得到解决。从5.4开始,没有鼠标的设备不会查询鼠标位置,也不会执行不必要的光线投射。

如果使用的版本 older than 5.4,强烈建议移动开发人员创建自己的输入管理器类。这可以简单到从UnityUI源复制UnitedInputManager的标准输入管理器,并注释掉ProcessMouseEvent方法以及对该方法的所有调用。

Raycast optimization

Graphic Raycaster是一个相对简单的实现,它迭代所有具有“Raycast Target”设置为true的图形组件。对于每个Raycast目标,Raycaster执行一组测试。如果一个Raycast目标通过了它的所有测试,那么它将被添加到命中列表中。

Raycast implementation details

The tests are:

  • 如果Raycast目标处于活动状态,启用并绘制 (i.e. has geometry)
  • 如果输入点位于连接Raycast目标的RectTransform中
  • 如果Raycast目标具有或是任何ICanvasRaycast过滤器组件的子级(在任何深度),并且该Raycast过滤器组件允许Raycast。

然后,按深度对命中的Raycast目标列表进行排序,对反向目标进行过滤,并对其进行过滤,以确保在摄像机后面呈现的元素(即在屏幕中不可见)被移除。

如果在图形Raycaster的“阻塞对象”属性上设置了相应的标志,则图形Raycaster也可以将射线投射到3D或2D物理系统中。(在脚本中,该属性名为Block ingObjects。)

如果启用了2D或3D阻挡对象,则在光线投射阻挡物理层上的2D或3D对象下方绘制的任何Raycast目标也将从命中列表中删除。

然后返回最后的点击列表。

Raycasting optimization tips

鉴于所有Raycast目标必须由图形Raycaster进行测试,最好的做法是只在必须接收指针事件的UI组件上启用“Raycast目标”设置。Raycast目标列表越小,必须遍历的层次越浅,每个Raycast测试的速度就越快。

对于具有必须响应指针事件的多个可绘制UI对象的复合UI控件,例如希望其背景和文本都更改颜色的按钮,通常最好在复合UI控件的根处放置一个Raycast Target。当单个Raycast Target接收到指针事件时,它可以将事件转发给复合控件中的每个感兴趣的组件。

Hierarchy depth and raycast filters

当搜索光线投射过滤器时,每个图形Raycast都会遍历转换层次结构直到根。此操作的成本与层次结构的深度成正比增长。所有连接到层次结构中每个转换的组件都必须进行测试,以确定它们是否实现了ICanvasRayCastFilter,因此这不是一个廉价的操作。

有几个标准的UnitedUI组件可以使用ICanvasRayCastFilter,例如CanvasGroup、Image、MASK和RectMask2D,因此这种遍历不能被忽略。

Sub-canvases and the OverrideSorting property

子画布上的overrideSorting属性将导致图形Raycast测试停止攀升转换层次结构。如果可以启用它而不引起排序或射线广播检测问题,则应该使用它来降低射线广播层次结构遍历的成本。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值