UGUI 降低填充率技巧两则

UGUI 降低填充率技巧两则

Fill Rate(填充率)是指显卡每帧每秒能够渲染的像素数。在每帧绘制中,如果一个像素被反复绘制的次数越多,那么它占用的资源也必然更多。目前在移动设备上,FillRate 的压力主要来自半透明物体。因为多数情况下,半透明物体需要开启 Alpha Blend 且关闭 ZTest和 ZWrite,同时如果我们绘制像 alpha=0 这种实际上不会产生效果的颜色上去,也同样有 Blend 操作,这是一种极大的浪费。因此,今天我们为大家推荐两则UGUI 降低填充率的技巧,希望大家能受用。

这是侑虎科技第50篇原创文章,感谢作者钱康来供稿。欢迎转发分享,未经作者授权请勿转载。同时如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

作者博客:http://qiankanglai.me
知乎专栏:https://zhuanlan.zhihu.com/soulgame


在Unity中,与能直接看到的Verts/Tris/Batches数据不同,填充率并不能被直接统计到,但是我们可以通过查看OverDraw来大致查看:
UWA Tech Doc

对于UI来说,后者其实是很容易被忽视的热点(特别是对于中低端移动设备来说)。下面我就以具体两个例子为例,并探讨其解决思路。


滥用不可见组件

之前在Profile手头项目的时候发现红米上一个奇怪的现象:战斗界面维持60fps没问题;进入UI界面之后瞬间掉到45fps,甚至有的复杂界面掉到30fps。但战斗场景的Tris/Verts比UI高不少。

通过工具很方便的就定位到了瓶颈在于FillRate爆了,最后发现新手教学部分用了很多“不可见”的Image作为交互响应的控件;但这些东西虽然画上去没有效果,依然占用了显卡资源,特别是有很多大块的区域...找到问题之后就解决起来很方便:实现一个只在逻辑上响应Raycast但是不参与绘制的组件即可,改完之后帧率瞬间正常。

using UnityEngine;
using System.Collections;

namespace UnityEngine.UI
{
    public class Empty4Raycast : MaskableGraphic
    {
        protected Empty4Raycast()
        {
            useLegacyMeshGeneration = false;
        }

        protected override void OnPopulateMesh(VertexHelper toFill)
        {
            toFill.Clear();
        }
    }
}

这里顺便提一句,显卡资源消耗在没有到瓶颈的时候,大概是随着使用的增加正相关,但是到瓶颈之后很多时候是“崩盘”节奏。


Polygon Mode Sprites

在UI部分中我们会大量使用图片作为元素,如果图片边缘有大片留白就会和上面那个问题一样,产生很多无用填充。Unity和Texture Packer目前都支持了Polygon Mode,也就是说将原来的矩形Sprite用更加紧致的Polygon来描述,从而能更有效的利用空白空间(顺便也减小了打包出来的图资源)。

当然,目前Unity只在Sprite Render里支持了这个模式,在UGUI的Image中还无法正常使用。我自己实现了一个挂官方论坛UGUI Image with polygon sprites,Texture Packer作者也表示很感兴趣~
UWA Tech Doc

可以看到同样的一个图片,新的模式下顶点数变多了,但是绘制的范围变小了不少;同时在打包的时候图片也更加的紧致了,因为在不规则大图周围能塞进去不少小的元素。

下面这个脚本是针对Image的扩展,使其支持Polygon Mode Sprite...不过精力有限,只支持了Simple而且没做Preserve Aspect,有兴趣的朋友如果实现了别的模式还望多多交流(主要是Filled和Sliced下要自己重新划分三角形,想想就麻烦...)

using System.Collections.Generic;

namespace UnityEngine.UI
{
    [AddComponentMenu("UI/Effects/PolygonImage", 16)]
    [RequireComponent(typeof(Image))]
    public class PolygonImage : BaseMeshEffect
    {
        protected PolygonImage()
        { }

        // GC Friendly
        private static Vector3[] fourCorners = new Vector3[4];
        private static UIVertex vertice = new UIVertex();
        private RectTransform rectTransform = null;
        private Image image = null;
        public override void ModifyMesh(VertexHelper vh)
        {
            if (!isActiveAndEnabled) return;

            if (rectTransform == null)
            {
                rectTransform = GetComponent<RectTransform>();
            }
            if (image == null)
            {
                image = GetComponent<Image>();
            }
            if (image.type != Image.Type.Simple)
            {
                return;
            }
            Sprite sprite = image.overrideSprite;
            if (sprite == null || sprite.triangles.Length == 6)
            {
                // only 2 triangles
                return;
            }

            // Kanglai: at first I copy codes from Image.GetDrawingDimensions
            // to calculate Image's dimensions. But now for easy to read, I just take usage of corners.
            if (vh.currentVertCount != 4)
            {
                return;
            }

            rectTransform.GetLocalCorners(fourCorners);

            // Kanglai: recalculate vertices from Sprite!
            int len = sprite.vertices.Length;
            var vertices = new List<UIVertex>(len);
            Vector2 Center = sprite.bounds.center;
            Vector2 invExtend = new Vector2(1 / sprite.bounds.size.x, 1 / sprite.bounds.size.y);
            for (int i = 0; i < len; i++)
            {
                // normalize
                float x = (sprite.vertices[i].x - Center.x) * invExtend.x + 0.5f;
                float y = (sprite.vertices[i].y - Center.y) * invExtend.y + 0.5f;
                // lerp to position
                vertice.position = new Vector2(Mathf.Lerp(fourCorners[0].x, fourCorners[2].x, x), Mathf.Lerp(fourCorners[0].y, fourCorners[2].y, y));
                vertice.color = image.color;
                vertice.uv0 = sprite.uv[i];
                vertices.Add(vertice);
            }

            len = sprite.triangles.Length;
            var triangles = new List<int>(len);
            for (int i = 0; i < len; i++)
            {
                triangles.Add(sprite.triangles[i]);
            }

            vh.Clear();
            vh.AddUIVertexStream(vertices, triangles);
        }
    }
}

这个做法是用顶点数来换填充率,具体是否这么干还要看项目本身的瓶颈。这一点在官方论坛的帖子里我也和别人讨论过,这里就不再赘述了。

  • ugui 优化总结 R11; I Given 发表在 2018年10月16日 回复

    [...]如果sprite是中心镂空且切图为九宫格时,可以去除fill center,以减少over draw游戏中许多时候会使用一个透明的Image组件来监听点击事件或者屏蔽Image后面的按钮事件,空的Image可以解决这个问题,用起来也很方便,但是空的Image照旧会参与绘制,从而产生overdraw。解决办法是扩展Graphic组件来替换Image组件。 如果是只要点击区域,不要显示内容的。可以把空[...]

  • 会孵蛋的鱼 发表在 2018年02月28日 回复

  • wang 发表在 2017年08月08日 回复

    显示解决了问题, 但是ugui事件有问题。

    • 发表在 2018年01月06日 回复

      你是指第二种方法的点击问题吧?

  • 邹春毅 发表在 2016年09月21日 回复

    赞一个!

  • 卿圣 发表在 2016年09月19日 回复

    赞!

  • mrzivchu 发表在 2016年06月24日 回复

    nice