
·本专栏文章记录笔者阅读学习《Unity Shader入门精要》的感想与笔记,方便日后复习与查找
一.移动端的特点
移动端的GPU架构和PC端有很大的不同,它更专注于使用更小的带宽和功能。
1.传统架构
- 使用的GPU:Tegra(英伟达的芯片)
- 缺陷:不会对过多的overdraw【指一个像素被重复绘制多次】进行处理
2.基于瓦片的延迟渲染架构
使用的GPU:
- PowerVR(常用于IOS设备和某些安卓设备):把所有的渲染图象装入一个个瓦片中,再有硬件找到可见的片元,其中只有可见的片元才会被执行片元着色器
- Adreno(高通的芯片):使用Early-Z或相似技术进行低精度的深度检测以剔除不必要的片元
- Mali(arm的芯片):和Adreno差不多
因此,对不同的芯片我们的游戏需要发布不同的版本才能移植和适配。尤其是安卓的平台,而IOS的平台的硬件条件则相对统一
二.影响性能的因素
要治病,我们就需要先了解病因之所在。而要对性能进行优化,我们也就需要先了解哪些因素会对性能造成影响
1.CPU(负责保证帧率)
- 过多的Draw Call【通过批处理优化】
- 复杂的脚本或物理模拟:布料模拟,蒙皮,粒子模拟等
2.GPU(负责分辨率相关的处理)
顶点处理:
- 过多的顶点【通过优化几何体,LOD技术,遮挡剔除优化】
- 过多的逐顶点计算
片元处理:
- 过多的片元(分辨率过大或OverDraw导致)【控制绘制顺序,小心透明物体,减少实时光照 优化】
- 过多的逐片元计算
带宽
- 使用了尺寸很大而且没有压缩过的纹理【减少纹理大小,利用分辨率缩放优化】
- 过多的逐片元计算
三.渲染分析工具
1.统计窗口(statistics)
可以帮助我们快速查看一些数据

上图中黄色框住的是相对比较重要的一些属性值
- FPS:在Graphic的右侧显示,给出了渲染一帧需要的时间
- Batches:一帧中需要进行的批处理数量
- Saved by Batching:合并批处理数量,这里显示告诉我们节约了多少draw call
- Tris和Verts:需要绘制的三角面片和顶点数目
- Screen:屏幕的大小,以及它所占用的带宽大小(比如这里是5.8MB)
- SetPass:渲染使用的Pass数目,每个pass都需要Unity的runtime来绑定一个新的Shader,这可能造成CPU的瓶颈
- Visible Skinned Meshs:渲染的蒙皮网格的数目
- Animations:播放的动画数目
drawCall的具体数量需要在性能分析器中才能查看
2.性能分析器(Profiler)
可以帮助我们查看更多和渲染有关的统计信息
性能分析器在哪里打开
性能分析器界面
绿线显示批处理数目
蓝线显示Pass的数目

点击Rendering部分可以在下面显示出来当前使用的Draw Call数目和其他在stats窗口上也显示了的数据,以及一些如动态/静态批处理数目等数据
这里的Draw Call数目往往会比我们预期的要多一些(和批处理数目,Pass数目不相等),这是因为有一些工作Unity在背后帮助我们完成了(比如初始化缓存,为阴影更新深度纹理和阴影映射纹理等)
3.帧调试器(Frame Debbuger)
可以帮助我们更加清楚地看到每一个Draw Call的工作与结果
打开帧调试器
点击启动帧调试器
查看帧调试器
可以看出来,这里一共用了30个事件
- 24个是DrawCall 事件
- 4个是更新深度法线纹理
- 2个是屏幕后处理事件
以上这些都是Unity自带的性能分析工具,不过值得注意的是这些数据有些是基于当前开发平台上得到的,而不是真的到对应设备上(比如手机端)的数据(FPS等)。
4.其他工具
有时候我们希望得到对应设备上的性能数据,这个时候我们就需要使用一些外部的测试工具了。
Android平台:
- 高通的Adreno分析工具(不同测试机的详细数据分析)
- 英伟达的NVPerfHUD(几乎所有需要的性能分析数据)
IOS平台:
- Unity内置的分析器(得到整个场景花费的GPU时间)
- PowerVRAM的PVRuniSco Shader分析器(给出大致的性能评估)
- Xcode的OpenGL ES Driver Instruments(宏观上的性能信息)
不过IOS平台上没有办法得到每个Draw Call花费的时间,因为PowerVR芯片采用的是基于瓦片的延迟渲染,只能通过宏观的信息进行参考。
其他的工具可以去Unity官方手册中再去具体看
四.针对CPU的优化-减少Draw Call
造成CPU性能瓶颈的因素之一是Draw Call的数量,因为每次Draw Call都需要CPU去进行一次数据打包与传递。这部分消耗的性能在数量很大的情况下是很可观的,因此我们一般希望在一次Draw Call中就传递尽可能多的数据以节约CPU的性能。这个就是对要渲染的模型进行批处理(可以理解成模型的打图集)
1.动态批处理
动态批处理是Unity会自动帮助我们实现的
1.1.原理:
每一帧把可以进行批处理的模型网格进行合并,再把合并后的模型数据传递给GPU,然后再使用同一个材质对其进行渲染。
1.2.好处:
①实现方便
②经过批处理后物体依然可以移动(因为每帧Unity都会重新合并一次网格)
1.3.条件:
虽然确实很好,但是可以进行动态批处理的物体需要满足以下条件:
①能够进行动态批处理的网格的顶点属性规模小于900。如果Shader中需要使用到顶点位置,法线和纹理坐标3个顶点属性,则顶点数目不能超过300
②所有对象的缩放尺度相同
③使用光照纹理的时候保证它们在批处理之后依然能够指向光照纹理的同一位置
④多Pass的Shader会中断批处理(比如前向渲染的完整实现至少会用到两个Pass,此时用到了两个Pass进行渲染的模型就不会被动态批处理了)
比如这里在添加了一个点光源后,这三个立方体和一个球体都会再使用第二个Pass来计算点光源的光照,此时它们就没有进行批处理了,Saved by batching的值也变回了0,Batches则变成了8(因为加上了点光源后每个物体需要进行2次Pass,4个物体一共就是8次了)
还有就是,如果这个点光源的范围内不包含这个模型,那这个模型也是不会调用第二个Pass的,依然可以被动态批处理)
不过其实一般来说我们用到的模型的顶点数量大多是会大于900的,此时动态批处理往往不能满足我们的需求
2.静态批处理
静态批处理需要我们手动设置几个模型的为静态模型

2.1.原理:
运行开始的时候,把需要进行静态批处理的模型合并到一个网格当中
2.2.内部实现:
①先把静态物体变换到世界空间下,然后给他们构建一个更大的顶点和索引缓存。
②对同一材质的物体调用一次Draw Call
③对不同材质的物体调用多个Draw Call,但是可以避免这些Draw Call之间的状态切换
2.3.优点:
①只用进行一次批处理,比动态批处理更加高效
②适用的模型范围广,没有顶点数量限制
2.4.缺点:
①静态批处理的物体不能移动
②会占用更多的内存
2.5.示例:

比如:我们先把场景中这些墙壁模型设置为静态批处理对象。
场景中物体静态批处理前
场景中物体静态批处理后
之后点击运行,可以看到批处理数量用39瞬间减少到了22,为我们节约了17个批处理的数量。

然后我们还可以看到的是这些被静态批处理的Mesh变成了一个集合体网格。并且看到它有6个子网格(Sub Meshes),模型图看起来也是把这六个模型合并成一个模型了(就很像打图集),对于合并后的网格,Unity会判断其中使用同一个材质的子网格,然后对它们进行批处理。
使用静态批处理前
使用静态批处理后
可以看出来使用的Buffer变得多了一点点(193变到199)。这是因为静态批处理会占用更多的内存。
注意:
①如果一些物体共享了相同的网格,那在内存中每一个物体都会对应一个该网格的复制品
②如果有额外的光源让物体需要调用额外的Pass去渲染处理,那么这个额外的Pass部分是不会被批处理的(不过平行光的部分【Base Pass】依然会被静态批处理)
五.针对GPU的优化
1.减少顶点
1.1.优化模型
Unity中顶点数量与建模软件中数目的差异:
①建模软件中是按人类习惯来的,看起来有几个就是几个
②Unity中是按GPU的计算习惯来的,每个顶点在不同的面的需要被拆分为不同的顶点,这么做的目的有以下两个:
- 分离纹理坐标(uv splits):此时一个顶点在不同面上对应不同的纹理坐标,顶点和它当前计算时所在的面的顶点属性(纹理坐标等)要一一对应
- 产生平滑的边界(smoothing splits):此时一个顶点在不同面上对应不同的法线或切线信息(用于决定这是一个硬边还是平滑边)
基于以上知识,我们得到如下的
优化建议:
①尽可能减少顶点的数目
②移除不必要的硬边以及纹理衔接以避免分离纹理坐标和产生平滑的边界
1.2.LOD技术(Level of detail)
描述:即细节等级技术。就是当越远的时候,这个模型的面片数量就会被减少地越多。(可以理解成3D版的midmap)

Unity中可以给他物体加上一个LOD Group组件来实现与调整(LOD0,LOD1,LOD2分别代表三种细节等级,Culled则表示完全剔除该物体)
1.3.遮挡剔除技术
描述:就是剔除那些在当前视角下不可能被看到,即被遮挡的物体(和视锥体裁剪还是有所区别的,视锥体裁剪只会判断当前物体是否在视锥体内,但是不会判断当前物体是否被其他物体遮挡)
大致实现:①通过一个虚拟摄像机遍历场景构建潜在可见的对象层级结构。运行的时候每个摄像机都会根据这个数据来识别哪些物体是可见的。
效果:①减少Overdraw ②减少要处理的顶点数目 ③提高游戏性能
2.减少片元
优化重点在于减少Overdraw(指一个像素被绘制了多次)

Unity中可以在场景视图中切换查看Overdraw(不过这个Overdraw只是提供查看物体的相互遮挡层数,并非最终屏幕绘制的Overdraw,即没有任何深度测试和其他优化策略时候的Overdraw)
2.1.控制绘制顺序
描述:我们可以利用深度测试,让最靠近屏幕的物体先被渲染出来,这样如果层级在它之后的物体在深度测试的时候就会被直接剔除啦。即让物体从前往后绘制
比如这里先绘制红色的物体,再去尝试绘制蓝色的物体
优化建议:
①更优先设置为不透明物体的渲染队列,尽量避免半透明队列(不透明物体会优先被渲染)
②主要角色(会挡住很大一片区域的角色),我们可以使用更小的渲染队列先绘制它们;杂兵角色(常常在掩体后面那种)则使用常规不透明物体之后再渲染它们
③对于天空盒子,它一定是在所有物体后面,所以队列给他设置为Geometry+1,让他一直都是在这些物体渲染完成之后再渲染的
2.2.小心透明物体
描述:
①透明物体关闭了深度写入,这导致我们没办法用控制从前到后的渲染顺序的方式来对他进行优化处理。它必须从后往前渲染,导致无可避免的OverDraw。
②GUI大多是半透明的,如果GUI太多且主摄像机依然投射整个屏幕,那就会导致大量OverDraw
③移动平台上,透明度测试也会影响游戏性能。因为它的discard或clip操作会导致一些优化策略失效。这个时候往往用透明度混合会更好
2.3.减少实时光照和阴影
描述:
①每增加一个逐像素点光源,都会让场景中有处理额外Pass的物体多进行一次处理。且会终端批处理
优化建议:
①光照纹理的应用
②God Ray,通过透明纹理模拟得到
③查找纹理/查找表(LUT): 把复杂的光照计算结果存储到一张表中,然后根据光源方向,视角方向,法线方向等参数对LUT采样得到光照结果
④尽可能减少逐像素光源的使用(每个物体不要超过一个),需要更多就用逐顶点光照计算
七.针对带宽的优化
1.减少纹理大小
优化建议:
①尽量使用正方形,且边长为2的整数幂
②使用mipmapping和纹理压缩
③纹理压缩(根据不同平台选择不同的压缩格式)
2.利用分辨率缩放
描述:过大的分辨率和不太好的GPU导致性能表现不佳
优化建议:
①针对特定机器进行分辨率的缩放
八.针对计算复杂度的优化
1.Shader的LOD技术
描述:可以让Shader只有在LOD小于某个值的时候才会被启用。(不设置就默认无限大)
使用:
cs
SubShader{
Tags { "RenderType" = "Opaque" }
LOD 200 //LOD值小于200的时候才启用
设置全局最大允许LOD:Shader.maximumLOD 或 Shader.globeMaximumLOD来设置最大LOD值。这样可以把特定的Shader剔除出去。
2.代码相关优化
优化建议:
①优先进行特定的运算:对象数< 顶点数 < 像素数 。即尽可能把计算放在对象和逐顶点上
②尽可能使用精度更低的浮点值进行运算。不过要注意避免频繁的精度切换
- float/highp使用存储顶点坐标等变量。但是计算速度最慢
- half/mediump适用于标量,纹理坐标等变量。速度是float的两倍
- fixed/lowp适用于绝大多数颜色变量和归一化的方向矢量。速度是float的四倍,精度要求不高用这个
③使用尽可能少的插值变量。比如float4类型的uv,xy和zw分量可以存两个不同的纹理坐标。(不过PowerVR平台插值很廉价,直接用两个变量反而性能更好)
④尽可能不要用全屏的屏幕后处理效果。如果一定要用则进坑用fixed/lowp进行低精度运算(纹理坐标还是用half/mediump),高精度运算就用查找表或者顶点着色器中处理。
⑤尽可能把多个特效合并在一个Shader中。
3.根据硬件条件进行缩放
描述:先做到保证在所有平台都能流畅运行的程度,然后再根据具体运行平台的性能去逐步开启更多的特效
九.总结
①对于Unity的优化主要在三个方向上:CPU,GPU,带宽
②针对CPU的优化主要是减少Draw Call 和 减少代码的复杂程度
- 减少Draw Call主要用到的是动态和静态批处理技术
- 动态批处理技术Unity自动帮我们处理,它处理的物体可以移动,但是能顺利被动态批处理的条件比较苛刻
- 静态批处理需要我们自己指定哪几个物体需要被作为一个处理批次。它处理的物体不能移动,且内存占用会提升。但是可以减少大量Draw Call
- 处于一个处理批次中的物体,如果用的是同一个材质,那就用一个DrawCall去处理全部;如果用的是不同材质就用不同的Draw Call去处理,不过这里不同的Draw Call之间不必进行状态切换
- 减少代码复杂度的方面主要是优先对象处理和逐顶点处理,使用尽可能低的精度同时避免频繁精度转换,尽可能使用更少的插值变量以及尽可能少用全屏的屏幕后处理效果
③针对GPU的优化主要是针对顶点和片元两方面
- 针对顶点要尽可能减少模型的顶点数目,要尽可能地去除不必要的硬边和顶点衔接。还可以LOD技术,以及遮挡剔除技术来实现
- 针对片元目标是减少Overdraw,可通过控制渲染顺序(从前到后渲染),减少光照和阴影的计算,还有避免使用透明物体来实现
④针对带宽的优化主要是针对图片纹理大小和分辨率两方面
- 图片纹理大小通过渐进纹理技术以及图片压缩来实现控制,同时尽可能保证它是正方形且为2的整数幂
- 分辨率则根据平台的性能去升高或者降低
⑤还有一种缩放的思想是先做到把质量拉到最低保证所有平台都能流程运行,然后再根据当前所处的平台去上调对应的画面质量等。
⑥对于性能的分析,Unity中提供了stats窗口,性能分析器,和帧分析器来获取需要的信息。不过要想测试在移动端的性能表现需要使用其他外部的性能分析工具
⑦针对不同架构的平台,需要根据其特点去指定不同的优化方案