Fill-rate, Canvases and input 【译】

翻译自https://unity3d.com/cn/learn/tutorials/topics/best-practices/fill-rate-canvases-and-input?playlist=30089#anchor-fn-2

这一章讨论一下在构建Unity UI时容易碰到的问题。

解决填充率问题

有两种方法可以减轻GPU片段管线的压力:
* 减少片段shader的复杂度
- 更多详细信息,请看“UI Shader和低端设备”部分。
* 减少必须被采样的像素点的数量。

由于UI shader 通常是标准化的,最常见的问题就是填充率使用过度问题。这通常是因为UI元素的大量重叠,和许多UI元素占据屏幕的重要部分。这两个问题可能会导致非常严重的过度绘制(overdraw)。

为了减轻填充率的段度使用,减少overdraw,可以考虑下面的一些可能的补救措施。

消除看不见的UI

重构UI元素改动做少的方法就是将玩家看不到的元素disable掉。最常见的适合这种方式的情况是打开了一个背景不透明的全屏UI。这种情况下,在全屏UI下的所有元素都应该被disable。

最简单的方式是disable根GameObjec或者包含不可见元素的GameObject。有关其他的决绝方案,请参阅Disabling Canvas Renderers部分。

禁止看不见的相机输出

如果一个背景不透明的全屏UI被打开,空间相机依旧会在UI下面渲染3D场景。渲染器不知道全屏的UI将会遮盖整个3D场景。

因此,如果一个全屏UI被打开,关闭所有空间相机,可以通过减少3D世界的无用渲染,来降低GPU的渲染压力。
注意:如果一个Canvas被设置“Screen Space - Overlay”,它的渲染和场景激活的相机数量无关。

大量被遮盖的相机

许多全屏UI不会完全遮盖整个3D场景,而是保持场景的一部分可见。这种情况,最好的办法是将可见的部分渲染到一张render texture中,如果场景的可见部分被缓存到render texture中,那么实际的世界空间相机可以被disable掉,缓存的render texture 可以放到UI屏幕的下面,模拟3D场景。

基于组合的UI

使用组合的方式创建UI是设计者经常使用的方式,通过对UI元素和背景组合和分层来创建最终的UI。虽然这样相对来说做很简单,并对迭代很友好,但是由于Unity UI 使用了透明渲染队列(transparent rendering queue),这么做的效率并不高。

假设有一个简单UI,它有一个背景,一个按钮,按钮上有一些文本。渲染一个在文字内的像素,GPU必须先采样背景图片,再采样按钮的图片,最后才是问题图集的图片,总共进行了3次采样。随着UI复杂度的增加,并且更多的装饰UI被添加到背景上,采样的数量也会不断增加。

如果一个大的UI被有很高的填充率,解决这个问题的最好方法是创建一些UI sprite,尽可能多的将变化的/不变得UI元素合并到背景图中。这种方式减少遮挡UI元素的数量来实现减少填充率,但这会增加工作量,以及工程中纹理图集的数量。

一个减少层叠元素的原则是,为给定的UI创建一个特定的sprite,同时这个sprite也可以被子元素使用。假设一个商店UI有一个商品的滚动列表。每一个商品的UI元素包含一个边框,一个背景,和一些显示价格,名字,和其他信息的图标。

这个商店UI需要一个背景,但是由于商品会在滚动条上滚动,商品元素不能合并到背景中。但是边框,价格,名字和其他商品UI元素可以合并到商品的背景中。根据图标的大小和数量,填充率可以节省很多。

合并不同层的UI有一些缺点。特定的元素很难被重用,并且需要美术去创建额外的资源。大量的新的纹理会显著的增大UI贴图的内存数量,尤其是UI 贴图没有合理的加载和释放。

UI shaders 和 低端设备

Unity UI使用的内置shader支持遮罩,剔除,以及许多其他复杂操作。因为添加这些复杂操作,与Unity 2D shader相比,在一些像iphone一样低端机上,效率可能会低一些。

如果遮罩,裁剪和一些其他的高级特性在低端设备的程序上不被需要,可以创建一些自定义shader来忽略这些无用的操作,如下面这个精简的着色器:

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
        }
    }
}

Canvas 重建

对于任何UI的显示,UI系统必须为屏幕上的每一个组件创建几何图形。这包括运行动态布局的代码,生成表示UI文本字符的多边形,已经尽可能多的去将几何图合并到一个mesh中来减少drawcall。这是一个多步的过程,在基础部分已经介绍了。

Canvas的重建会影响性能主要是以下两个原因:

  • 如果一个Canvas上有许多可现实的UI元素,那么计算它自身的合批的开销非常大。这是因为排序和分析UI元素的开销与Canvas中可显示元素的数量大于线性增长关系(more-than-linearly )。
  • 如果一个Canvas频繁的被标记为dirty,那用来刷新它的时间会比那些变化相对较少的Canvas多。
    随着canvas内的UI元素变多,这两个问题会变得更加严重。

重要提醒:Canvas上的UI元素在任何时候发生变化时,都会重新制定一次重新合批。这个过程会重新分析Canvas上的每一个元素,无论它有没有变化。注意,“变化”是指任何可以影响UI部件显示的变化,包括sprite被分配给sprite render, transform的坐标和缩放,文本网格中的文本等。

子级顺序

Unity UI 是从后向前构建,不见在hierarchy 中的顺序就是它的排序顺序。离hierarchy越近的物体,会被挡在后面。合批在hierarchy中从上到下遍历,并收集相同材质和相同贴图并且不包含中间层【1】的所有部件。中间层会强制阻断合批。

在UnityFrameDebuger部分讨论过,frame debuger可以检测中间层的UI。这种情况是一个可显示的元素插入到两个可合批的元素之间。

这个问题经常是在文本和sprite离得很近的时候:文本的边框可以显示的覆盖到附近的sprite,因为很多文本的多边形都是透明的。这可以用两种方式解决:

  • 对可绘制元素重新排序,使可合并的元素中不会插入不能合批的对象;即把不能合批的元素移动到可合批的对象的前面和后面。
  • 移动object的位置来消除不可见的重叠区域。

这两种操作都可以在Editor下,Frame Debugger开启的情况下进行。简单的通过观察Frame Debugger中的drawcall数量,就可以去找到合适的顺序和位置来最小化因为遮挡而产生的drawcall。

拆分Canvas

除了少数情况外,通常拆分Canvas是一个好的方法,可以通过移动元素到子Canvas或者同级的Canvas的方式实现。

当一部分UI的绘制深度和其他部分不同,绘制在其他层的上面或者下面(如 教程中的箭头)时,使用同级Canvas比较合适。

其他情况下子Canvas更加方便,因为他们从父级上继承了一些显示的设置。

看上去分割成很多个子Canvas是一个好方法,但是记住,Canvas系统不会垮Canvas进行合批。高效的UI设计需要平衡最小的重建消耗以及最小的drawcall开销。

一般准则

因为当组成Canvas的组件在任何时候发生变化都会导致Canvas重建,通常将一些重要的(non-trivial)Canvas分成两部分。另外,最好将同时变化的元素放到相同的Canvas上。进度条和倒计时就是这样的例子。它们以来相同的数据,所以它们会在相同时间更新,因为他们应该被放置在相同的Canvas上。

在一个Canvas上放置所有静态的不会变的元素,如标签和背景,它们只会在第一次显示时合批一次,之后就不需要再重新合批。

在第二Canvas上,放置所有动态元素 —— 变化很频繁的元素。确保这个Cavnas将重新合批所有dirty的元素。如果动态元素的数量非常多,可以把这些元素再细分为不断变化(如进度条,定时读书器,动画)的集合和偶尔变化的集合。

这实际比较难操作,尤其是当UI元素被封装到预设中。许多UI选择把Canvas中开销昂贵的组件移动到子Canvas中。

Unity 5.2 和 优化合批

Unity 5.2的合批代码实质上是重写的,比起Unity 4.6, 5.0 和 5.1,它有更好的性能表现。另外,在多核的设备上,Unity UI系统将大部分操作移动到工作线程中。Unity 5.2减少了切分大量子canvas的必要。在手机设备上,两道三个Canvas绘制UI界面可以很高效。

更多关于Unity 5.2的优化的信息可以在这个博客上找到。

Unity UI 中的输入和射线检测

Unity UI 默认是使用 Graphic Raycaster 组件去处理输入事件,如触摸事件,指针悬停(pointer-hover)事件。这通常是有 Standalone Input Manager组件处理。 Standalone Input Manager指的是“万能(universal)”输入管理系统,它会处理点击和触摸事件。

移动端错误的鼠标事件检测(5.3)

5.4版本之前,每个active的挂有Graphic Raycaster 的Cavnas会在每一帧检测指针的位置,即使没有可用的点击输入。各个平台都会执行这个操作。IOS和安卓设备即使没有鼠标,依旧会检测鼠标的位置,并且会去查找哪个元素在那个位置下面【2】。

这是对CPU时间的浪费,它被验证至少会占用Unity应用程序5%的 CPU 帧时间。

这个问题在5.4的版本之后被解决了。5.4版本之后,没有鼠标的设备不会去查询鼠标位置,也不会执行无用的射线检测。

如果正在使用5.4之前的版本,强烈建议移动端开发者创建一个自定义的Input Manager类。可以简单的复制Unity 标准的Input Manager的代码,然后注释掉ProcessMouseEvent方法和调用它的地方。

射线检测优化

Graphic Raycaster是一个相对简单的实现,它遍历所有Raycast Target设置为true的Graphic组件。对每一个Raycast Target,Raycaster 会执行一系列的检测。如果Raycast Target通过所有检测,那它将会被加入到点击列表中。

射线检测的具体细节

检测是:

  • 如果Raycast Target是active,enable的,并且是被绘制的(如含有几何体)
  • 如果输入的点落在Raycast Target所在的RectTransform之中。
  • 如果Raycast Target或者它的子级(在任何深度)有ICanvasRaycastFilter组件,并且这个Raycast Filter允许检测。

点击到的Raycast Target列表按照深度排序,过滤反向的对象,确保不被相机渲染的元素(如在屏幕上不可见)被移除掉。

Graphic Raycaster也可能相3D或者2D物理系统发射射线,如果Graphic Raycaster的“Blocking Objects”属性被设置为true。(脚本手册中,属性名称为blockingObjects)

如果2D或者3Dblocking objects 设置为true,在射线拦截(raycast-blocking)的物理层2D或者3D对象下的Raycast Target,将被从点击列表中移除。

最后将点击列表返回。

射线检测优化技巧

所有设置了 Raycast Target的Graphic组件都会被Graphic Raycaster检测,最好只将需要接受点击事件的UI组件的 Raycast Target属性设置为true。越少的 Raycast Target,遍历的层级越浅,每一个射线测试将会越快。

对于包含多个可绘制UI元素的组合UI,它们必须对点击事件有回应,如按钮的背景和文字颜色变化,通常放一个单独的Raycast Target在组合UI的根节点下。当单个的Raycast Target收到点击事件,它可以把时间传递给每个对这个事件感兴趣的组件。

Hierarchy 深度和射线过滤

每一次对图形的射线检测都是从当前节点一直遍历到根节点,查找射线过滤器。这种操作的开销与hierarchy中的深度呈正比。在hierarchy中每个Transform挂载的所有组件都需要去检测是否继承ICanvasRaycastFilter,这个操作开销不小。

这有一些标准的Unity UI组件继承了ICanvasRaycastFilter,如CanvasGroup,Mask,RectMask2D,所以这些便利不能简单的取消。

子Canvas和OverrideSorting属性

子Canvas的overrideSorting属性可以阻止射线检测向上级遍历。如果它的开启不影响排序和点击检测,它可以用来减少层级遍历的开销。

尾注

  1. 中间层是指一个包含不同材质的图形对象,它覆盖到了两个可以比合批的对象并且它的层级在两个可合批对象之间。

  2. 任何hover时间需要发送时,都会被发现。

你可能感兴趣的:(Fill-rate, Canvases and input 【译】)