Unity场景优化初探

一、什么是批处理?

  我们知道Unity3D在屏幕上绘制一个图形本质上调用OpneGL或者DirectX这样的API,因此在这个过程中会产生一定程度上的性能消耗。DrawCall是OpenGL中描述绘制次数的一个量,例如一个基本的OpenGL绘制流程是设置颜色->绘图方式->顶点坐标->绘制->结束,在绘制的过程中每帧都会重复这个过程,这就是一次DrawCall,所以当游戏中的绘制过程变得复杂的时候,就会带来DrawCall的急剧增加,进而带来游戏的性能问题,反映到游戏表现上就变成了优化问题。那么在Unity3D中采取了什么样的措施来降低DrawCall呢?这就是我们今天要说的批处理,换句话说Unity3D使用了批处理来达到降低DrawCall的目的,批处理希望通过对物体网格的重组来获得更高的绘制效率,试想以下如果将多个物体合并为一个物体,那么在绘制的时候只需要绘制一次就够了,因此从这个角度上来讲这样做肯定是可以降低DrawCall的,更深刻的一种理解是这里体现了一种资源循环调用的思想,接触过Android开发的朋友们一定知道ListView控件可以对其元素进行“缓存”从而提高效率,因为我们可以发现其实ListView是对列表项进行某种程度上的“复用”从而提高了效率,在Unity3D这里同样遵循了这个原理。在Unity3D中进行批处理的一个前提是相同材质的物体可以被合并,如果这些物体使用不同的材质,那么当我们把这些材质对应的纹理打成“图集”以后可以对其进行合并,并且在合并的时候应该是用Renderer.sharedMaterial 而非 Renderer.material以保证材质是可以共享的。关于DrawCall的相关细节大家从这里来了解。

二、Unity3D中批处理的三种方式

  在Unity3D中有静态批处理和动态批处理以及GPU Instancing。下面我们就来分别说说这三种不同的批处理方式!

静态批处理

  因为我们在使用Unity3D的过程将场景中相对来说“静态”的物体都勾选Static选项,这在Unity3D中称为Static GameObjects,并且因为这一特性和Lightmapping、Navigation、Off-meshLinks、ReflectionProbe、Occluder and Occludee等内容均有着密切的联系。静态批处理允许游戏引擎尽可能多的去降低绘制任意大小的物体所产生的DrawCall,它会占用更多的内存资源和更少的CPU资源,因为它需要额外的内存资源来存储合并后的几何结构,如果在静态批处理之前,如果有几个对象共享相同的几何结构,那么将为每个对象创建一个几何图形。使用静态批处理非常简单啦,只要勾选物体的Static选项即可!

静态批处理会在构建时将多个静态网格物件合并为一个或多个大的网格物件,然后在运行时一次批处理渲染一个大网格中的多个物件。如果不同的物体间共享材质,则可以直接通过静态批处理降低DrawCall 

Unity场景优化初探_第1张图片

动态批处理

  相对静态批处理而言,动态批处理的要求更为严格一些,它要求批处理的动态对象具有一定的顶点,所以动态批处理只适用于包含小于900个顶点属性的网格。如果你的着色器使用顶点位置,法线和单光,然后你可以批处理300个顶点的动态对象;而如果你的着色器使用顶点位置,法线,uv0,UV1和切线,那么只能处理180个顶点的动态对象。接下来最为重要的一点,如果动态对象使用的是不同的材质,那么即使进行了动态批处理从效率上来讲并不会有太大的提升。如果动态对象采用的是多维子材质,那么批处理是无效的。如果动态对象接收实时光影,同样批处理是无效的。动态批处理并不能降低DrawCall、面数和顶点数

动态批处理在每帧中获取多个小型网格物件,在CPU中对其进行顶点变换,将相似的顶点组合到一起,然后一次绘制它们。

GPU Instancing

可以利用少量Draw Call绘制多个具有不同的位置、旋转以及其他着色器属性的相同对象。

 

三、导致批处理失败的原因

有时在编辑器中可以清楚地看到,一些本应被批处理的对象出于某些原因没有被批处理。首先,请检查Player Settings中是否启用批处理功能。这个步骤看似多余,但我们遇到太多的无法处理的原因都是因为忘记开启。

我们专门为此提供了展示项目来演示Unity在什么情况下必须发起新的批处理请求。首先下载项目并复制到Unity项目中。请注意,你需要安装Unity 5.6才能看到Frame Debugger中关于批处理状态的说明。

以下是展示项目(Unity 5.6)中导致无法进行批处理的原因。每个原因对应一次单独的批处理:

AdditionalVertex Streams — 对象使用MeshRenderer.additionalVertexStreams设定了额外的顶点信息流。

DeferredObjects on Different Lighting Layers — 该物件位于另一不同的光照层中。

DeferredObjects Split by Shadow Distance — 两个物体中有一个在阴影距离范围内而另一个不是。

DifferentCombined Meshes — 该对象属于另一个已合并的静态网格。

DifferentCustom Properties — 该对象设定了不同的MaterialProperyBlock。

DifferentLights — 该物件受不同的前向光照(Forward Light)影响。

DifferentMaterials — 该对象使用不同的材质。

DifferentReflection Probes — 该对象受不同的反射探头(Reflection Probe)影响。

DifferentShadow Caster Hash — 该对象使用其他的阴影投射着色器,或是设定了不同的着色器参数/关键词,而这些参数/关键词会影响阴影投射Pass的输出。

DifferentShadow Receiving Settings — 该对象设定了不同的“Receive Shadows”参数,或是一些对象在阴影距离内,而另一些在距离之外。

DifferentStatic Batching Flags — 该对象使用不同的静态批处理设定。

DynamicBatching Disabled to Avoid Z-Fighting — Player Settings中关闭了动态批处理,或在当前环境中为避免深度冲突而被临时关闭。

InstancingDifferent Geometries — 使用GPU Instancing渲染不同的网格或子网格。

LightmappedObjects — 对象使用了不同的光照贴图,或在相同的光照贴图中有不同的光照贴图UV转换关系。

LightprobeAffected Objects — 对象受其他光照探头(Light Probe)影响。

MixedSided Mode Shadow Casters — 对象的“Cast Shadows”设定不同。

Multipass — 对象使用了带多个Pass的着色器。

MultipleForward Lights — 该物件受多个前向光渲染影响。

Non-instanceableProperty Set — 为instanced着色器设定来non-instanced属性。

OddNegative Scaling — 该对象的缩放为很奇怪的负值,例如(1,-1,1)。

ShaderDisables Batching — 着色器使用“DisableBatching”标签显式关闭了批处理。

TooMany Indices in Dynamic Batch — 动态批处理索引过多(超过32k)。

TooMany Indices in Static Batch — 静态批处理中的组合网格索引过多。对于OpenGL ES来说是48k,OSX是32k,其他平台是64k。

TooMany Vertex Attributes for Dynamic Batching — 欲进行动态批处理的子网格拥有超过900个顶点属性。

TooMany Vertices for Dynamic Batching — 欲进行动态批处理的子网格顶点数量超过300个。

四、Frame Debugger

 

从版本5开始,Unity包含了一个全新的可视化帧调试工具,Frame Debugger。该工具能帮你解决很多图形方面的问题,Z-fightingGPU状态不正常,渲染队列错误、混合操作错误,过多的draw call,效率低下等等。相比游戏视图中的状态列表,它提供了更加详尽的信息,通过与渲染事件/步骤的交互和检查,你也能学习到大量GPU管线的相关知识。真地,每个开发人员都应该了解这个工具。

 

1.  使用Frame debugger

 

使用Window-> Frame Debugger菜单打开其主窗口,我建议同时打开frame debugger game view (游戏视图)。为此你可能需要对窗口布局做些调整,本人用来调试图形显示时的界面如下图:

 

Unity场景优化初探_第2张图片 
Recommended layout (except for the Asset Store tab)

建议的窗口布局(除了资源商店标签页)

 

使游戏进入播放状态,在frame debugger窗口中,点击enable就能得到最近渲染帧的细节信息。

 

其中包含了两个主要部分:事件,以及在其右侧的详细信息。事件中基本上都是CPU发送给GPU的命令,相当的简单,毕竟在此无法看到真正被调用的API函数,只是将整个过程按照时间顺序打包显示出来。其中大部分是清除和绘制(不透明/透明)几何体的命令,但是你也可以注意到更多的信息,如:GUI渲染,阴影处理,图形效果等以及相应的回调次数。

 

在选择一个事件后,你可以立即在details面板中得到关于该事件的详细信息。包括:着色器范围和其标志,渲染输出事件和着色器属性信息。同时,游戏视图会显示在该事件发生前已被渲染的对象(包括当前事件哦)。下面是一个例子

 

Unity场景优化初探_第3张图片 
Event example

 

2. Frame Debugger干嘛用?

 

Frame debugger为我们提供了一些有价值的信息。绘制事件的调用顺序(也即是:渲染队列)对渲染的正确性十分重要,如果游戏中的元素未能正确的显示粗来,在此你就可能发现它未被正确的放入渲染队列中,从而导致过早或延迟渲染;特别是在进行自定义着色器开发或Z write(深度缓冲写入)参数调整和测试时。通过这个工具为我解决了不少问题:查找出由于忘掉设置标志位和渲染次序而导致天空盒子覆盖了俺着色器输出的问题。

 

其次,相对的,你也可以在此查看draw call的调用次数,并通过顶点/索引数量来间接衡量场景的渲染代价。当然,着色器pass数也有帮助意义,但其复杂度在此没有显示,这就要求你得有些基本常识了,所有这些信息都能帮助你提高场景的性能,比如:你会发现出于某些原因一些网格绘制未能进行静态/动态批处理。在我的例子中,你会发现通过使用材质图集来将两个图片精灵的2draw call操作合并为1个。

 

第三,通过与Frame Debugger交互,你可以快速学习GPU的体系架构知识并了解Unity渲染处理的流程。可以用键盘在不同的事件中跳转一步步查看场景的渲染过程。在之前的例子中,可以看到场景渲染开始于对三个缓存的清理(颜色、zstencil),接着是不透明几何体(从前面到背面),天空盒子和透明几何体(从背面到前面)的渲染。

 

最后,你可以访问着色器属性以获得关于材质和着色器的更多信息,也可以得到被对象使用的数据的引用,如:材质。大多数人都没用过Unity中的advanced views,你可以在inspector中通过点击右上部的paragraph图标,并选择“Debug”来访问他们。

 

Unity场景优化初探_第4张图片 

 

Shader properties

着色器属性

 

其他好用的工具

4. RenderDoc

 

在进行了一些软件的测试后,我铁定可以为大家推荐几款。你工具箱里的首选必须是RenderDoc,Crytek开发的一款免费工具,用于解决底层调用信息的问题,该工具使用了网络攻击者的经典做法:它建立一个虚拟的图形驱动作为中间人进行(MITM)捕获应用中的DX/GL/Vulkan调用并提供详细的调试信息。好消息是:该工具已经支持在Unity中原生集成,你只需在windows机器上安装RenderDoc并重启unity的编辑器即可。

 

本节俺的目标是为大家概述该工具的用法,让你充满兴趣的开始,使用时更加顺畅,而且少花电费。

 

我们开始吧,如果安装顺利,在游戏中你应该可以右键单击并在菜单中选择Load RenderDoc

 

 

 

几秒钟后,进入播放模式,如果你对刚渲染的那帧感兴趣,点击最大化播放按钮左侧光头佬一样的新图标:

 

 

 

接着切换到新打开的RenderDoc窗口,双击自动捕捉的日志开始分析它:

 

Unity场景优化初探_第5张图片 

 

RenderDoc captured log

 

Rendoc捕捉日志

 

哇哦,这就是信息的大海啊,我会淹死的。冷静下来,一切都会顺利。现在你会在几千个窗口中得到所有帧的数据,表担心,下面我会继续讲解真正有趣的内容。


4.1. Event browser + API calls.

4.1 事件浏览器+API调用

 

该工具的优点就是能够获得每个事件的底层信息,事件浏览器就像UnityFrame Debugger的一个扩展,其中还包括耗时(按毫秒计)和其他提示信息,能为衡量效率提供大量帮助。RenderDoc具有上下文敏感特征,也就是说,不论何时你选择一个事件,大部分其他窗口和区域将会显示仅与该事件相关的信息,其下的API 调用窗口则列出了为处理该事件而调用的函数。

 

Unity场景优化初探_第6张图片 

 

RenderDoc: Event Browser + APIcalls

 

能得到这些耗时数据真的很棒!虽然它们可能不是绝对精确,但是具有相对准确性,也就是说,通过比较耗时你就能推断出哪些操作的draw call需要优化。

 

4.2. Pipeline state.

管线状态

 

我经常怀疑渲染管线的工作模式,精巧的几何体是如何进入它的内部并转换为50寸屏幕上像素的?好吧,不幸的是,在这个工具中你也无法得到一个用户友好的动画以展示其工作过程。

 

但确实有些很好的免费工具可以做到。不过,不管怎样,RenderDoc能让你访问事件中的大量管线状态信息:input assembler,顶点///几何体/片段/计算着色器,栅格化器和输出合并器。相信俺的判断,RenderDoc不虚其名。

 

Unity场景优化初探_第7张图片 

 

RenderDoc: Pipelinestate for a pixel shader

 

Unity场景优化初探_第8张图片 

 

RenderDoc: Output Merger example


 

 

RenderDoc: Rasterizer state


4.3. Mesh output

网格输出

 

RenderDoc的另一项特征是可以通过顶点着色器的:输入输出位置,法线,纹理坐标等信息获取事件中渲染的网格数据。实际上,你可以用它抓取并保存3D程序或游戏中正在渲染的网格,但在这边文章中我就不结合案例讲解了。

 

4.4. Texture viewer

纹理观察器

 

这是我的菜,将事件中的输入和输出纹理进行可视化展示。包括渲染到纹理或其他中间缓冲数据,在你使用中间缓冲进行图像效果处理时会非常有趣,此外,这也也能能节省纹理空间。


Unity场景优化初探_第9张图片 

 

RenderDoc: Texture viewer

 

4.5. Pixel debugging

像素级调试

 

在帧缓冲中,错误渲染的坏像素覆盖正确像素的情况并不少见,大多数开发者都碰到过这种噩梦,坏像素在场景中留下的奇怪感受让我们意识到有问题发生了,但却不知原因何在,就是赶脚不对!如何确认我们的猜想并找出问题所在…

 

是的,以下是该问题的解法:选择一个像素,点击histroy(得到一个在帧缓冲中向那个像素写入信息的事件序列)或者debug(调试它的像素着色器)。调试这些问题需要真正的技术大牛,你需要汇编知识,并对像素着色器的反汇编后的版本有一定了解。

 

Unity场景优化初探_第10张图片 

 

RenderDoc: Pixel selection

RenderDoc:像素选择

 

Unity场景优化初探_第11张图片 

 

RenderDoc: Pixel’s history

RenderDoc: 像素历史


Unity场景优化初探_第12张图片 

RenderDoc: shader debugger

 

如何创建一个新场景地图

https://www.colabug.com/743997.html

你可能感兴趣的:(Unity)