Git中配置UnityMergeTool方法

在Unity安装路径下/Editor/Data/Toolsmergespecfile.txt中配置Fallback的合并工具tortoiseGit Merge

  • use “%programs%/TortoiseGit/bin/TortoiseGitMerge.exe” -base:”%b” -mine:”%l” -theirs:”%r” -merged:”%d”

在项目工程.git文件夹中找到config配置文件,加如下配置:
[merge]
tool = unityyamlmerge

[mergetool “unityyamlmerge”]
trustExitCode = false
cmd = ‘Unity安装路径/Editor/Data/Tools/UnityYAMLMerge.exe’ merge -p $BASE $REMOTE $LOCAL $MERGED

冲突时使用此命令:git mergetool

Unity线性空间和伽马空间(转)

为什么我会想起来写这篇博客呢?缘起于项目中做PBR时,美术来问出贴图是要出线性空间贴图还是伽马空间贴图,经过一番纠结与查证后,项目终于切到了线性空间,而贴图则是能出线性空间优先出线性空间,出不了的出伽马空间也行。至于为什么也行,就是本篇博客所要讲述的内容。

首先要来解决第一个问题,什么是伽马空间,什么是线性空间?线性空间好理解,颜色按照线性渐变的空间即是线性空间,我在网上看到一个举例很有趣,想象一个纯黑墨水的池子,往里面滴一滴白色颜料,随着白色颜料不断的滴入,墨池会越来越白直至变成白色,而记录每次滴入颜料后墨池的颜色变化,即是一个从黑到白的颜色线性渐变过程。既然有线性渐变,那么肯定就有非线性渐变,而伽马空间正是这样一个颜色非线性渐变的空间。为什么会发生这样的事呢?

一个流传甚广的版本表示,这是由CRT显示器引起的。由于CRT显示器对于输入的电压和显示的亮度并不呈线性关系,而是一个类似幂律曲线的关系,一般来说,这个曲线的指数部分称作伽马值,为2.5。显示器有这样的特性后,一个正常颜色输入进去显示出来会变暗,我自己拉了个图来说明下

这是个

y=x^{2.5}

的幂律曲线,如果颜色输入值为0.5,那么输出值大概为0.176左右,更接近黑色。所以输入的颜色在输出时会变暗。为了解决这个问题,图片在被采集时会做一个逆向操作,如果颜色为0.5的话先逆向

0.5^{1/2.5} \approx 0.757

然后

0.757^{2.5}

就会回到0.5了。这里我们用到了两个伽马值,1/2.5和2.5,他们分别称为encoding gamma和display gamma,通过下图展示他们的用处。

encoding gamma通常在图片生成时(比如说拍照拍出来的照片,PS新建的图片等)就已经存在,而display gamma又是显示器自带的特性(当然现在的显示器不再是CRT,所以display gamma可能不是2.5,不过为了兼容性厂商还是会把以前的2.5伽马值加入),他俩相乘称作end-to-end gamma,如果是1的话那么真实场景被捕捉的亮度和显示的亮度是成比例的。然而,Real Time Rendering一书指出了乘积为1的问题。一是我们人眼看到的真实场景的亮度与显示器所能显示的亮度差了好几个数量级,说白了显示器所能显示的颜色精度根本达不到真实场景的精度;二是周围环境影响,我们的视野在看真实场景时是由真实场景所填充的,而在看显示器时视野除了被虚拟场景包围,还会被真实场景包围。这样两个差别导致了end-to-end gamma是1的话并不能保证显示的亮度和原始场景的亮度是一致的。书中推荐,电影院那种漆黑的环境为1.5,在明亮的室内为1.125。

我们通常用的sRGB标准的encoding gamma大概为0.45(1/2.2),这是为了配合2.5的display gamma,因为0.45 * 2.5 = 1.125。当然,显示器的display gamma大部分值还是设为2.2(这里有个网站,可以看在不同的伽马值下图片所表现的不同准确的伽玛 2.2 及预设 5 种伽玛值设定),这样1/2.2*2.2 = 1。

当然冯乐乐前辈还提出了来自其他领域对于伽马的解释,以此来论证伽马值存在的必然,不管如何,伽马空间中的颜色并非线性渐变,而是呈一条曲线变化。

那么我们在做PBR时,为什么要纠结到底用伽马空间还是线性空间呢?PBR全称为Physically Based Rendering,既然是基于物理的渲染,那么我们做渲染时对于贴图采样出来的值必定要是和真实环境下相同的值才行,而采用了伽马空间的话贴图中颜色会被encoding gamma所改变,shader中采样出来的颜色值和真实环境下的值是不一样的,这样怎么能称为基于物理的渲染呢?

我们以人的皮肤渲染来举例子,皮肤贴图的r通道的值通常会高于其余两个通道的值,那么在伽马校正后(即对原始值做一个伽马次方的操作),这种差异会被进一步的放大,再做光照计算,你会发现r通道的值提升的异常的高。

上图左边是线性正确值,右边是渲染时带着伽马值,那么提升光的亮度会迅速的曝光。

而当这种差异被拉大后,你会发现在眼皮轮廓的地方会出现蓝黑色的痕迹,如下图


所以在做PBR渲染时,使用线性空间是非常有必要的。

而我们纠结的点在于,把Unity引擎切到线性空间的话,之前所有美术的资源都是在伽马空间下制作的,会不会有问题?实际上是有问题的。

当我们把Unity从伽马空间切换到线性空间时,引擎里面我们需要勾选一个东西,这样伽马空间的资源也能使用了。

勾选了图中的sRGB后,其实引擎为我们做了一个工作,在采样这张图片的时候会调用OpenGL ES3.0里的sRGB Sampler接口,将贴图中被encoding gamma所改变的值还原,这样我们在shader中做的任何计算就是基于物体在真实场景中的颜色了。算完以后当我们要把颜色输出到显示器时,显示器因为自带display gamma,我们无法抹去这个东西,所以引擎又为我们做了一件事,调用OpenGL ES3.0里的sRGB Frame Buffer接口,将计算得出的最终结果用encoding gamma算好,用以抵消display gamma的影响。

那么在线性空间底下使用伽马空间资源会有什么问题呢?透明混合会出问题。我们知道透明混合的时候dst color在frame buffer中,而颜色在线性空间下进入了frame buffer引擎会调用sRGB Frame Buffer接口做一个pow0.45的操作,而透明混合时明显需要线性空间的颜色,因为src color还没进frame buffer,没做过pow4.5的操作。所以这里在把dst color从frame buffer拿出来时,会做一个pow2.2的操作回到线性空间,然后做透明混合,得出结果后再做一遍pow0.45。

假设src color的某个分量为1,alpha为0.5;dst color某个分量为0,那么根据正常的计算为:

res = src * alpha + dst * (1-alpha) = 0.5

而有了sRGB Sampler和sRGB Frame Buffer后:

res = (src ^{2.2} * alpha + dst ^ {2.2} * (1-alpha)) ^ {0.45} = 0.5 ^ {0.45} \approx 0.732

差异在此产生,并且随着混合次数的增多,差异会越来越大。

理论上最好的解决方案是美术直接在线性空间下制作资源,如官方说的在PhotoShop设置中选择“用灰度系数混合RGB颜色”,参数设置为1。

或者参考网上的解决方案【Unity补完计划】Unity线性空间(Linear)下Alpha的混合问题Unity手机线性空间下的透明混合(上),但他们都有额外的消耗,对于性能不是那么富裕的项目就显得无能为力了。

参考
Unite 2018 | 浅谈伽玛和线性颜色空间
聊聊Unity的Gamma校正以及线性工作流
【图形学】我理解的伽马校正(Gamma Correction)
Chapter 24. The Importance of Being Linear

原文链接:https://www.jianshu.com/p/964ffc5d3b9c

Unity安装Android SDK

在使用Unity打包apk时,有时会期望设置Android API level

Unity的默认设置是使用安装的最高版本,例如Unity2019安装的是Android 29

如果期望使用别的SDK打包会出现提示

Unity提示使用SDK Manager安装自己需要的版本

在External Tools中找到SDK安装的目录,在Unity的安装目录中找到”Editor\Data\PlaybackEngines\AndroidPlayer\SDK\tools\bin”文件夹,sdkmanager.bat就是我们要找的SDK Manager

在该目录运行命令行工具,例如sdkmanager –help

执行安装:

1sdkmanager platforms;android-28

 安装完成重启Unity就可以选择新的API打包

使用MaterialPropertyBlock来替换Material属性操作[转]

在Unite 2017 中曾提到了使用材质属性块的优化建议,笔者为此做了特别的研究、测试和验证,并将实验结论和测试工程在此分享,希望能对大家的研发和优化有所帮助。


一、官方文档

Unite 2017 国外技术专场中,Arturo Núñez在《Shader性能与优化专题》中的原话是:

Use MaterialPropertyBlock Is faster to set properties using a MaterialPropertyBlock rather than material.SetFloat(); Material.SetColor();

首先,我特意查找了下关于MaterialPropertyBlock的官方文档,文档是这样说的:材质属性块被用于Graphics.DrawMesh和Renderer.SetPropertyBlock两个API,当我们想要绘制许多相同材质但不同属性的对象时可以使用它。例如你想改变每个绘制网格的颜色,但是它却不会改变渲染器的状态。

我们来看看Renderer这个类,它包含了Material,SharedMaterial这两个属性;GetPropertyBlock,SetPropertyBlock这两个函数,其中两个属性是用来访问和改变材质的,而两个函数是用来设置和获取材质属性块的。我们知道,当我们操作材质共性时,可以使用SharedMaterial属性,改变这个属性,那么所有使用此材质的物件都将会改变,而我们需要改变单一材质时,需要使用Material属性,而在第一次使用Material时其实是会生成一份材质拷贝的,即Material(Instance)。

二、实验

首先声明两个数组,一个用来保存操作材质,另一个用来保存操作材质属性块。

GameObject[] listObj = null;
GameObject[] listProp = null;

然后在Start函数中做初始化工作,我们在屏幕左侧空间生成ObjCount个球体Sphere,用来处理材质,在屏幕右侧空间生成ObjCount个球体Sphere,用来处理材质属性块。

void Start () {
        colorID = Shader.PropertyToID("_Color");
        prop = new MaterialPropertyBlock();
        var obj = Resources.Load("Perfabs/Sphere") as GameObject;
        listObj = new GameObject[objCount];
        listProp = new GameObject[objCount];
        for (int i = 0; i < objCount; ++i)
        {
            int x = Random.Range(-6,-2);
            int y = Random.Range(-4, 4);
            int z = Random.Range(-4, 4);
            GameObject o = Instantiate(obj);
            o.name = i.ToString();
            o.transform.localPosition = new Vector3(x,y,z);
            listObj[i] = o;
        }
        for (int i = 0; i < objCount; ++i)
        {
            int x = Random.Range(2, 6);
            int y = Random.Range(-4, 4);
            int z = Random.Range(-4, 4);
            GameObject o = Instantiate(obj);
            o.name = (objCount + i).ToString();
            o.transform.localPosition = new Vector3(x, y, z);
            listProp[i] = o;
        }
    }

然后我们在Update函数中响应我们的操作,这里我使用按键上下健位来操作。

void Update () {
        if (Input.GetKeyDown(KeyCode.DownArrow))
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < objCount; ++i)
            {
                float r = Random.Range(0, 1f);
                float g = Random.Range(0, 1f);
                float b = Random.Range(0, 1f);
                listObj[i].GetComponent<Renderer>().material.SetColor("_Color", new Color(r, g, b, 1));
            }
            sw.Stop();     
            UnityEngine.Debug.Log(string.Format("material total: {0:F4} ms", (float)sw.ElapsedTicks *1000 / Stopwatch.Frequency));
        }
        if (Input.GetKeyDown(KeyCode.UpArrow))
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < objCount; ++i)
            {
                float r = Random.Range(0, 1f);
                float g = Random.Range(0, 1f);
                float b = Random.Range(0, 1f);
                listProp[i].GetComponent<Renderer>().GetPropertyBlock(prop);
                prop.SetColor(colorID, new Color(r, g, b, 1));
                listProp[i].GetComponent<Renderer>().SetPropertyBlock(prop);             
            }
            sw.Stop();
            UnityEngine.Debug.Log(string.Format("MaterialPropertyBlock total: {0:F4} ms", (float)sw.ElapsedTicks * 1000 / Stopwatch.Frequency));
        }
    }

这时,我们再来看一下对比数据:

从结果对比来看,确实使用材质属性块要快于使用材质,其消耗将近是操作材质耗时的四分之一。同时不管是材质还是材质属性块,第一次操作比后面的操作耗时要大。尤其是材质,可见在第一次使用材质改变属性操作时,其拷贝操作消耗还是非常大的。

当然上面的代码还是有优化空间的,因为每次去获取Renderer组件时都是GetComponent的形式来获取的,我们可以在Start时将其保存一下。null

Renderer[] listRender = null;
Renderer[] listRenderProp = null;

...
listRender[i] = o.GetComponent<Renderer>();
...
listRenderProp[i] = o.GetComponent<Renderer>();
...

再来看下运行对比数据:

同时我也通过Profiler的Memory模块,切换进Detailed选项,对其进行采样,可以发现在Sence Memory下面会有Material的拷贝(材质操作导致,而材质属性操作不会)。这也验证了操作材质时会有实例化存在,而使用材质属性块则不存在实例化。

三、游戏中处理

正如官方文档介绍材质属性块一样,Unity地形引擎正是使用材质属性块来绘制树的,所有的树使用的是相同材质,但是每棵树有不同的颜色、缩放和风因子。对于大场景大世界来说,我们肯定是动态加载地图的,这个时候我们可以配合GPU Instance来进一步提高性能,使用GPU Instance有两个优点:1)省去实体对象本身的开销;2)减少DrawCall的作用,同时还能减少动态合批的CPU开销和静态合批的内存开销,可谓一举多得。遗憾的是只能在Open GL ES 3.0以上的设备上使用。对于一些游戏中存在自定义皮肤颜色玩法的,材质属性块的优势就可以发挥出来了:当你想让100个不同玩家同屏时,如果使用材质操作颜色属性,那么首先就存在100份材质拷贝的实例;其次,材质操作属性本身就比材质属性块操作要慢那么点,在性能优化中一毫秒的优化就是胜利,那么这里一毫秒那里一毫秒,累积起来就不得了了。

四、相关工程

Arturo Núñez 的shader性能与优化的工程下载地址:
https://github.com/ArturoNereu/ShaderProfilingAndOptimization

本次测试工程:
https://pan.baidu.com/s/1qXPGhTa

原文地址:https://blog.uwa4d.com/archives/1983.html

Unity UI 优化学习总结(转)

Unity UI 基础

Canvas(画布),和名字一样,是 UI 绘制的地方,Unity 的渲染系统用其来提供一个可绘制的分层几何。负责将ui几何合批成适合的网格,提交绘制命令给 Unity 的图形系统,这整个过程叫做 rebatch 或者 batch build。当 Canvas 其子节点下包含 Canvas Renderer 的节点需要进行 rebatch 的时候,就会被标记为脏。

Sub-canvas 是嵌套在 Canvas 里的 Canvas,它与其父节点是隔离的,Sub-Canvas 的 rebuild 不会逼迫父节点重建几何,反之亦然,不过还是存在一些边界情况。。比如父节点变化导致子节点大小变化。

Graphic 是 unity 的 ui C# 基类,大部分 unity ui 都是 通过继承 MaskableGraphic 类来实现的,后者比前者多了可遮罩(Maskbale)的能力。该类让 ui 描述并创建出自身需要绘制的网格。

Layout 组件为 ui 提供了布局管理能力,也就是控制 RectTransform 的 位置和大小。它不会受到Graphics的影响。

Graphci 与 Layout 都依赖于 CanvasUpdateRegistry 类。它会定位 Graphic 与 Layout 是否需要更新并加入更新队列,在所在 Canvas 的 willRenderCanvases 事件被触发时对队列中的对象执行真正的更新。这些更新被称作 rebuild。不要和上面的 rebatch 弄混了。

Batch building 的过程,Canvas 会将其 ui 元素生成的 mesh 组合并生成合适的绘制命令给 Unity 渲染系统。并且过程的结果会被缓存并重用,直到 Canvas 重新被标记为脏。这会在组合的网格发生变化时发生。Canvas 会根据子节点里带有 Canvas Renderer 组件的 ui 来生成 网格,但是不包括 Sub-Canvas 的子节点,也就是说每个 Canvas 单独负责自身的 Batch building。Batch building 的过程会对根据深度、重叠测试、材质等条件对各个 Mesh 进行排序、分组、合并,这个过程是多线程的,在移动端(核心少)与桌面端(核心多)会呈现相当大的差异。

Rebuild 的过程就是布局与网格(这里指的不是最终 rebatch 生成的网格,而是 Graphic 子类或者说 ui 生成的网格)重新计算的过程。CanvasUpdateRegistry 会维护若干队列,里面有标记了 Layout 或者 Graphic 脏标记的 ui 节点,在 PerformUpdate 方法里依次对各个队列节点作如下处理

  1. Layout 进行更新(会改变节点 RectTransform ,该组件会在 Graphic 更新时被引用)
  2. 进行裁剪(官方文档这里举例说 Mask,但事实上内置ui实现了 IClipper 接口的只有 RectMask2D,Mask 是通过更新 stencil buffer然后ui都会测试的原理来实现的)
  3. 然后对 Graphic 进行更新(更新节点上的 Canvas Render 网格、材质、贴图)

这样所有 ui 节点的网格、材质、贴图就确定了。之后 Canvas 会从 Canvas Render 中取出这些东西,并重新生成网格(绘制顺序实际上是在这里确定的)与绘制命令交给渲染系统(也就是 rebatch)。事实上上述可以进一步细分成prelayoy、layout、postlayout等等。细节可以直接看 源码 。

Profile UI 性能热点

任何的性能优化,第一步都是剖析性能热点,常见的 ui 性能热点有

  • GPU 填充率过高
  • Canvas batch 的 Rebuild 花费时间过长
  • 过多的 Canvas batch Rebuild
  • 顶点生成过多

Unity 的 UI 性能剖析工具,有两种,一种是编辑器自带的,一种是第三方外部工具,第三方工具往往结果更准确,但是有平台、编译模式等等限制,这里只说 Unity 内置剖析工具。

可以观察在 Unity Profiler 的 CPU Usage 里 PostLateUpdate.PlayerUpdate 下 UIEvent.WillRenderCanvas 的时间片占比来确定当前 UI 的 CPU开销。

SendWillRenderCanvas 即为 rebuild 的过程,Canvas 则为 batch build 的过程。也可以更省事,直接看 profiler 里的 UI 与 UI Detail 查看 rebuild Layout 与 Render 的开销,还有就是 Canvas 的自身或者累计的(即包括 Sub-Canvas) 的 batch 次数、顶点与GameObject 次数,甚至还有合批中断原因。还会显示触发的ui事件。不过 batch viewer(也就是profiler 下面那一坨)只能在编辑器运行模式下使用。

此外还可以使用 GPU profiler 查看 Transparent 部分开销来确定 UI 的 GPU 使用情况,从而排查填充率,确定 overdraw 等问题。

除了使用 Profiler 之外,还可以选择使用 Unity Frame Debugger(帧调试器),它会完整地显示某一帧的渲染过程,ui 当然也在其中,而且与 Profiler 不同,在非编辑器运行模式下也能使用。ui 的 drawCall 所在位置会受 Canvas render Mode 的影响, overlay 的模式会显示在 Canvas.RenderOverlays 组,否则则会在相关联的摄影机 Render.TransparentGeometry 组下。可以看出 ui 是在透明队列中绘制的,也就是说要按照深度从后往前绘制才能满足透明的需求。所以 Canvas 在 batch 的过程需要按深度排序 Canavs render,之后会检查拥有相同材质贴图(也就是有相同渲染状态,这种可以用一个drawCall 搞定)的 Canavs render,检查他们之间是否有不同材质且与他们发生重叠的 Canavs render。如果有则无法将它们合批,要拆成两个 batch,原因是如果这时候强行把他们合批,都无法满足绘制透明物体需要从后往前绘制的需求,会导致不正确的透明绘制结果。此外这里还要注意 hierarchy 中的顺序也是排序的依据或者说深度。

收集到结果之后就可以开始分析啦~~

  • Canvas.BuildBatch 或者 Canvas::UpdateBatches 占用过多 CPU 时间,说明 batch build 过度,需要考虑拆分成多个 Canvas。
  • 填充率过高,则考虑优化 overdraw。
  • WillRenderCanvas 大部分时间花在 IndexedSet_Sort 或者 CanvasUpdateRegistry_SortLayoutList 方法上(代表了过多的 Layout rebuild,不一定只是这两方法,具体看源码),则考虑减少布局组件的使用,UI Profiler 中 Layout 也是同理。
  • UI Profiler 中 Graphics 或者PopulateMesh 之类的方法开销占比大,则说明 Graphic 的 rebuild 是性能热点。这个要结合具体组件分析。
  • 如果发现 Canvas.SendWillRenderCanvases 每帧都被调用,那就要找找是什么导致了 Canvas 的频繁 rebuild,考虑通过拆分 Canvas 来缓解。

填充率、Canvas、输入

填充率

UI 节省 GPU 片元管线的负载的方法主要集中在两点

  • 减少复杂片元着色器的复杂度
  • 减少像素的采样数目

因为实际项目大部分的时候使用的都是 unity 的标准 ui 材质,所以主要的问题还是出在后者,也就是填充率过高上,ui占据过多的屏幕,或者合批数目太多都会导致过多的 overdraw 从而产生填充率高的问题。有如下解决方法

  • 隐藏不可见ui,因为项目里经常有全屏覆盖的ui,而且隐藏 Canvas 很简单,所以这个是相当简单实在的方法,不过要注意的是单纯控制透明度为0的确能“隐藏”ui,但是 ui 的绘制开销不会变。但是 ui 是通过 Graphics(勾选了 Raycast Target)来找到响应目标的,所以如果有只想响应点击但是不想显示的情况,可以通过调整透明度隐藏,然后勾选 Canvas Render 上的 Cull Transparent Mesh 来达到裁剪完全透明 ui 的目的。
  • 简化ui结构,尽量让美术把图整体切出来,这样相当于人力“合批”了 ui,从而消除了 overdraw,需要整体改变色调不要通过叠图混合颜色的方式来做,要用材质或者控制顶点色来搞。那种为了屏幕适配或者合理分层的节点而没有显示作用的节点要搞成空节点(即不带 Graphic 的节点)。
  • 关闭不可见的摄影机,这个不仅可以用于 UI 的优化,大部分游戏除了 ui 还会有实际场景,当有覆盖全屏的 ui 时,将那些不可见图形(ui 或者 3d 场景)的输出摄影机关了,相当于把他们的绘制开销完全砍了,可以简单直观有效地减小 GPU 压力 以及 drawCall,甚至有时对于非全屏ui,也可以通过截图(可能还要模糊)铺满背景的方式来硬怼出全屏ui,从而可以关闭摄影机。这种优化和上面隐藏 ui 的优化根据时一样的,但是应用范围更广。
  • 使用简单的shader,Unity 的默认 ui shader 为了通用使用了 stencil buffer 之类的东西,可以针对应用场景将 shader 替换成更简单的自定义 shader,除了采样 sprite 啥事不做之类的。

UI Canvas 的 rebuild

Canvas 的 rebuild 产生性能问题的原因主要有两种

  • Canvas 下 ui 元素过多,导致大量 ui 元素在合批过程的分析、排序开销过大,这个过程的复杂度是大于 O(n) 的,所以会 ui 元素数目影响很大。
  • Canvas 频繁地被标记为脏,即使 Canvas 只有很少的变动,但还是会刷新从而花费用于刷新

所以要尽可能地将会频繁刷新的 ui 拆分成多个Canvas,因为哪怕 Canvas 下一个 ui 元素的变动都会导致整个Canvas 的 重新合批(这里说得是合批(batch),单个 ui 元素的变动(指会对 Cannvasvas Rednder 产生影响,无论是材质、贴图、网格)一般不会导致其他 ui 的 rebuild)。不过也不能过多,因为 Canvas 相互之间是不会合批的。“动静分离”是 Unity Canvas 拆分的一个指导依据,将频繁刷新的部分,比如进度条,还有长时间静态的部分,比如各种背景,拆成两部分,这样前者的刷新就不会影响后者。

上面讲到在两个 ui 元素之间插入不可合批(拥有不同渲染状态,材质或者贴图不同)的且与它们重叠的 ui 元素,就会打断 ui 的合批,他们会被拆成三份网格,从而增加 drawcall。因为排序是按照 hierarchy 下的顺序来的,所以可以通过调整层级顺序来解决。此外也可以通过调整 ui 大小来恢复合批,或者将贴图打成一张图集从而能共享渲染状态而合批。这三种方法分别是通过消除 插入元素之间 重叠 不可合批 这三个条件来达到合批的,总之只要知道打断合批或者说成功合批的条件,就能针对性地进行优化。

可以通过 profiler 或者帧调试器来确定合批打断原因,具体方法上文已提及,此处不赘述。

Unity UI 的 输入与 Raycasting

Unity 的 UI 输入流程是 EventSystem 接受各类来自输入模块的 event,然后传递给各个 Graphics Raycaster,然后 Graphics Raycaster 找到其下所有勾选了 RayCaster Target 的且 active 的 Graphics,然后检测输入坐标是否命中了 ui 的 RectTransform 描述的节点,是的话,沿着层级向上遍历,对实现了 ICanvasRaycastFilter 的节点检查命中,如果命中就加入列表,知道没有父节点或者走到了勾选了 overrideSorting 的 CanvasGroup, 最后将列表按照深度从前到后排序(与渲染相反),如果 Blocking Objects 不是 None,那么深度位于ui 前的非 ui 且会相应点击的物体也会加入列表,从而遮挡住 ui 的点击。如果没有,那么列表种第一个检测为命中的 ui 就会响应输入事件。详情还是看官方源码 。

从上述过程可以看出开销的关键在于减少列表的长度, 如果不需要相应点击的 ui,就不要勾选 Raycast Target,RectTranform 不要无谓地拉太大等等,总之就是可以将不需要的 Graphic 从响应目标列表移除的手段的都可以达到优化的目的。

其他优化技术与建议

还有一些会导致难以维护或者有其他副作用的优化手段。

  • 利用 RectTransform 代替 Layout 来实现简单的布局需求。
  • 隐藏 Canvas(Canvas 组件而不是 Canvas 所在节点) 而不是具体 ui 节点,这样需要显示回来的时候不需要 rebuild Canvas。
  • 直接修改ui源码(一定要慎重考虑,仔细分析性能热点是否真的需要改源码而不是其他手段来解决)

总结

总的来说,UGUI 这套东西还是建立在 Unity 的基本渲染系统上,batch build 之类的优化点只要熟悉 Unity 的动态合批机制就不难搞定。不过在基本功能上添加了很多类似于布局、输入处理之类方便 ui 开发的特性。可以看出很多地方对性能不是很友好,比如所有 ui 都当成透明的来处理,很容易导致overdraw。不过不能站着说话不腰疼,至少这是一套有相当完成度的系统,性能方面的问题也可以有很多解决方案的,不至于束手无策,而且 Unity 官方还开放了一部分源码,更有利于性能分析与优化,可惜还是没有把 Canvas 合批的代码开放。

原文地址:https://zhuanlan.zhihu.com/p/266997416