以下内容是根据Unity 2020.1.01f版本进行编写的
前端是指渲染过程中GPU处理顶点数据的部分。它从CPU中接收网格数据(一大堆顶点信息)并发出Draw Call。然后GPU将从网格数据中收集顶点信息,通过顶点着色器进行传输。对数据按1:1的比例进行修改和输出。之后,GPU得到一个需要处理的图元列表(三角形——3D图形中最基本的形状)。接下来光栅化器获取这些图元,确定最终图形的哪些像素需要绘制,并根据顶点的位置和当前的相机视图创建图元。这个过程中生成的像素列表称为片元,将在后端进行处理。
后端描述了管线渲染中处理片元的部分。每个片元都通过片元着色器(也称为像素着色器)来处理。与片元着色器相比,片元着色器往往涉及更复杂的活动,例如深度测试、alpha测试、着色、纹理采样、光照、阴影以及一些可行的后期效果处理。之后这些数据绘制到帧缓冲区,帧缓冲区保存了当前图像,一旦当前帧的渲染任务完成,图像就发送到显示设备(例如显示器)。
正常情况下,图像API默认使用两个帧缓冲区(尽管可以给自定义的渲染方案生成更多的帧缓冲区)。在任何时候,一个帧缓冲区包含渲染到帧中、并显示到屏幕上的数据;另一个帧缓冲区则在GPU完成命令缓冲区中的命令后被激活,进行图像绘制。一旦GPU完成swap buffers命令(GPU请求完成指定帧的最后一条指令),就翻转帧缓冲区,以呈现新的帧。GPU则使用旧的帧缓冲区绘制下一帧。每次渲染新的帧时,都重复此过程,因此,GPU只需要两个帧缓冲区就可以处理这个任务。
在后端,有两个指标往往是瓶颈的根源——填充率和内存带宽
1)填充率
填充率是一个使用广泛的术语,它指的是GPU绘制片元的速度。然而,这仅仅包含在给定的片元着色器中通过各种条件测试的片元。片元只是一个潜在的像素,只有它未通过任一测试,则会被立即丢弃。这可以大大提升性能,因为管线渲染可跳过昂贵的绘制步骤,开始处理下一个片元。
一个可能导致片元被丢弃的测试是Z-Test,它检查较近对象的片元是否已经绘制在同样的片元位置。如果已被绘制,则丢弃当前片元。如果没有绘制,片元将通过片元着色器推送,在目标像素上绘制,并在填充率中消耗一个填充量。现在,假设将该过程用于成千上万的重叠对象,每个对象都可能生成成百上千个片元。这导致每帧都需要处理上百万个片元,因为从主相机的视角来看,片元可能产生重叠。更重要的是,我们每秒都在尝试重复该过程数十次。这是管线渲染中执行许多初始化设置工作非常重要的原因,很明显,尽可能跳过这些绘制过程将节省大量的渲染成本。
填充率也会被其它高级渲染技术所消耗,例如阴影和后期效果处理需要提取同样的片元数据,在帧缓冲区中执行自己的处理。即便如此,由于渲染对象的顺序,我们总是会重绘一些相同的像素,这称为过度绘制,这是衡量填充率是否有效使用的一个重要指标。
过度绘制:
跳过使用叠加alpha混合平面着色来渲染所有对象,过度绘制的多少就可以直观地显示出来。过度绘制多的区域将显示得更加明亮,因为相同的像素被叠加混合绘制了多次。这恰是Scene窗口的Overdraw Shading模式显示场景经过了多少过度绘制的方式。
2)内存带宽
GPU后端的另一个潜在瓶颈来自于内存带宽。只要从GPU VRAM的某个部位将纹理拉入更低级别的内存中,就会消耗内存带宽。这通常发生在对纹理采样时,其中片元着色器尝试选择匹配的纹理像素,以便在给定的位置绘制给定的片元。GPU包含多个内核,每个内核都可访问VRAM的相同区域,还都有一个小得多的本地纹理缓存,来存储GPU最近使用的纹理。
如果在内存带宽方面遇到瓶颈,GPU将继续获取必要的纹理文件,但整个过程将受到限制,因为纹理缓存将等待获取数据后,才会处理给定的一批片元。GPU无法及时将数据推回到帧缓冲区,以渲染到屏幕上,整个过程被堵塞,帧速率也会降低。
如何对内存带宽进行合理使用需要进行估算。例如:每个内核的内存带宽为每秒96GB,目标帧速率为每秒60帧,在到达内存带宽的瓶颈之前,GPU每秒可提取1.6GB(90/60)的纹理数据。当然,这不是一个确切的估算值,因为还存在一些缓存丢失的情况,但它提供了一个粗略的估算值。
请注意,这个值并不是游戏可以在项目、CPU RAM或VRAM中包含的纹理数据量的最大限制。其实,这个指标限制的是在一帧中可以发生的纹理交换量。
在管线渲染的所有过程中,光照和阴影往往会消耗大量的资源。由于片元着色器需要多次传递信息来完成最终的渲染,因此后端在填充率(大量需要绘制、重绘、合并的像素)和内存带宽(为Lightmap和Shadowmap拉入和拉出的额外纹理)方面将处于繁忙状态。这就是为什么和大多数其它渲染特性相比,实时阴影异常昂贵,在启用后会显著增加Draw Call数的原因。
1)前向渲染
前向渲染是场景中渲染灯光的传统方式。在前向渲染中,每个对象都通过同一个着色器进行多次渲染。渲染的次数取决于光源的数量、距离和亮度。Unity优先考虑对对象影响最大的定向光源组件,并在基准通道中渲染对象,作为起点。然后通过片元着色器使用附近几个强大的点光源组件对同一个对象进行多次重复渲染。每一个点光源都在每个顶点的基础上进行处理,所有剩余的光源都通过“球谐函数”技术被压缩成一个平均颜色。
为了简化这些行为,可以将灯光的Render Mode调整为Not Important,并在Edit | Project Settings | Quality | Pixel Light Count中修改参数。这个参数限制了前向渲染采集的灯光数量,但当Render Mode设置为Important时,该值将被任意灯光数覆盖,因此,应该慎用这个设置组合。
可以看出,使用前向渲染处理带有大量点光源的场景,将导致Draw Call计数呈爆炸式的增长,因为需要配置的渲染状态很多,还需要着色器通道。
2)延迟渲染
延迟渲染有时又称为延迟着色,是一项在GPU上已使用十年左右的技术,但一直未能完全取代前向渲染,因为涉及一些手续,移动设备对它的支持也有限。
延迟渲染的工作原理是创建一个几何缓冲区(称为G-buffer),在该缓冲区中,场景在没有任何光照的情况下进行初始渲染。有了这些信息,延迟着色系统可以在一个过程中生成照明配置文件。
从性能角度来看,延迟着色的结果让人印象深刻,因为它可以产生非常好的逐像素照明,而且几乎不需要Draw Call。延迟着色的一个缺点就是无法独立管理抗锯齿、透明度和动画人物的阴影应用;另一个问题是它往往需要高性能、昂贵的硬件来支持,且不能用于所有平台,因此很少有用户能使用它。
3)顶点照明着色(传统)
顶点照明着色是光照的大规模简化处理,因为光照是按顶点处理而不是像素处理。
该技术主要应用在一些不需要使用阴影、法线映射和其它照明功能的简单2D游戏。
4)全局照明
全局照明,是烘培Lightmapping的一种实现。全局照明是在Lightmapping后最新一代的数字技术,它不仅计算光照如何影响给定对象,还计算光照如何从附近的表面发射回来,允许对象影响它周围的照明配置文件,从而提供非常真实的着色效果。这种效果有一个称为Enlighten的内部系统来计算。该系统既可创建静态Lightmap,也可创建预计算的实时全局照明,它是实时和静态阴影的混合,支持在没有昂贵的实时照明效果下模拟一天中的时间效果(太阳光的方向随时间变化)。
大多数系统默认都开启多线程渲染功能,例如台式电脑和终端平台的CPU都有多个内核来支持多线程。
Android系统可以选中Edit | Project Settings | Player | Other Settings | Multithreaded Rendering复选框来开启此功能
IOS系统的多线程渲染可通过程序配置使用Edit | Project Settings | Player | Other Settings | Graphics API下面的Apple’s Metal API开启。
Unity通过CommandBuffer类对外提供渲染API。这允许通过C#代码发出高级渲染命令,来直接控制渲染管线,例如采样特定的材质,使用给定的着色器渲染指定的对象,或者绘制某个程序几何体的N个实例。
性能分析器可将管线渲染中的瓶颈快速定位到所使用的两个设备:CPU或者GPU。必须使用性能分析器窗口中的CPU使用率和GPU使用率来检查问题,这样可以知道哪个设备负荷较重。
为了执行准确的GPU受限的性能分析测试,应在Edit | Project Settings | Quality | Other | V Sync Count中禁用Vertical Sync,否则测试数据将受到干扰。
如果在深入分析性能数据后还无法确定问题的根源,或者在GPU受限的情况下需要确定管线渲染的瓶颈所在,就应尝试使用暴力测试方法,即在场景中去除指定的活动,并检查性能是否有大幅提升。如果一个小的调整导致速度大幅提升,就说明找到了瓶颈所在的重要线索。
对于CPU受限,最明显的暴力测试就是降低Draw Call来检查性能是否有突然的提升。
有两种好的暴力测试方法可用来测试GPU受限的应用程序,以确定是填充率受限还是内存带宽受限,这两种方法分别是降低屏幕分辨率和降低纹理分辨率。
可以通过牺牲GPU Skinning来降低CPU或GPU前端的负载。Skinning是基于动画骨骼的当前位置变换网格顶点的过程。在CPU上工作的动画系统会转换对象的骨骼,用于确定其当前的姿势,但动画过程中的下一个重要步骤是围绕这些骨骼包裹网格顶点,以将网格放在最终的姿势中。为此,需要迭代每个顶点,并对连接到这些顶点的骨骼执行加权平均。
该顶点处理任务可以在CPU上执行,也可以在GPU的前端执行,具体取决于是否启用了GPU Skinning选项。该功能可以在Edit | Project Settings | Player Settings | Other Settings | GPU Skinning下切换。该功能开启后,会将Skinning活动推送到GPU中,但注意,CPU仍必须将数据传输到GPU,并在命令缓冲区上为任务生成指令,因此不会完全消除CPU的负载。
第四章介绍了一些网格优化技术,这有助于减少网格的顶点属性
通过积和着色器进行曲面细分非常有趣,因为曲面细分是一种相对还未充分使用的技术,可以真正使图形效果在使用最常见效果的游戏中脱颖而出。但是,它也极大地增加了前端处理的工作量。
除了改进曲面细分算法或减轻其它前端任务的负载,来使曲面细分任务有更多的空闲空间外,并没有其它简单的技巧可以改进曲面细分。不管那种方式,如果前端遇到瓶颈,却在使用曲面细分技术,就应仔细检查曲面细分是否消耗了前端的大量资源。
GPU实例化利用对象具有相同渲染状态的特点,快速渲染同一网格的多个副本,因此只需要最少的Draw Call。这其实和动态批处理一样,只不过不是自动处理的过程。
有关GPU实例化的更多信息,请参阅Unity文档,网址为:
https://docs.unity3d.com/Manual/GPUInstancing.html
LOD(Level Of Detail,LOD)是一个广义的术语,指的是根据对象与相机的距离和/或对象在相机视图中占用的空间,动态地替换对象。由于远距离很难分辨低质量和高质量对象之间的差异,一般不会采用高质量方式渲染对象,因此可能用更简化的版本动态替换远距离对象。LOD最常见的实现是基于网格的LOD,当相机越来越远时,网格会采用细节更少的版本代替。
为了使用基于网格的LOD,可以在场景中放置多个对象,使其成为具有附加LODGroup组件的GameObject的子对象。LOD组的目的是从这些对象中生成边界框,并根据相机视野内的边界框大小决定应该渲染哪个对象。如果网格太远,可以将其配置为隐藏所有子对象。因此,通过正确的设置,可以让Unity用更简单的替代品代替网格,或者完全剔除网格,减轻渲染的负担。
但是,这个特性需要花费大量的开发时间才能完全实现;美术必须为同一个对象生成多边形较少的版本,而关卡设计师必须生成LOD组,并进行配置和测试,以确保它们不会在相机移近时出现不和谐的转换。
关于基于网格的LOD功能的更详细信息,请参阅Unity文档,网址为:
https://docs.unity3d.com/Manual/LevelOfDetail.html
剔除组(Culling Group)是Unity API的一部分,允许创建自定义的LOD系统,作为动态替换某些游戏或渲染行为的方法。
关于剔除组的更多信息,请参阅Unity文档,网址为:
https://docs.unity3d.com/Manual/CullingGroupAPI.html
减少填充率消耗和过度绘制的最佳方法之一是使用Unity的遮挡剔除系统。该系统的工作原理是将世界分割成一系列的小单元,并在场景中运行一个虚拟摄像机,根据对象的大小和位置,记录哪些单元对其它单元是不可见的(被遮挡)。
注意,这与视锥剔除技术不同,视锥剔除的是当前相机视图之外的对象。视锥剔除总是主动和自动进行的。因此遮挡剔除将自动忽略视锥剔除的对象。
只有在StaticFlags下拉列表下正确标记为Occluder Static和/或Occludee Static的对象才能生成遮挡剔除数据。Occluder Static是静态物体的一般设置,它们既能遮挡其它物体,也能被其它物体遮挡。Occludee Static是一种特殊的情况,例如透明对象总是需要利用它们后面的其它对象才能呈现出来,但如果有大的对象遮挡了它们,则需要隐藏它。
因为必须为遮挡剔除启用Static标志,因此该功能不适用与动态对象。
启用遮挡剔除功能将消耗额外的磁盘空间、RAM和CPU时间。需要额外的磁盘空间来存储遮挡数据,需要额外的RAM来保存数据结构,需要CPU处理资源来确定每个帧中哪些对象需要被遮挡。如果为场景进行了正确的配置,遮挡剔除可以剔除不可见的对象,减少过度绘制和Draw Call数,来节省填充率。
1)使用粒子删除系统
Unity Technologies发布了一篇关于这个主题的优秀博客文章,网址如下:
https://blogs.unity3d.com/2016/12/20/unitytips-particlesystemperformance-culling/
(试了一下已经不可用了)
2)避免粒子系统的递归调用
ParticalSystem组件中的很多方法都是递归调用。这些方法的调用需要遍历粒子系统的每个子节点,并调用子节点的GetComponent()方法获得组件信息。
有几个粒子系统API会受到递归调用的影响,例如Start()、Stop()、Pause()、Clear()、Simulate()、isAlive()。这些方法都有一个默认为true的withChildren参数。给这个参数传递false值(例如:调用Clear(false))可以禁用递归行为和子节点的调用。但这并不总是很理想,因为通常我们希望粒子系统的所有子节点都受方法调用的影响。因此,另一种方式是第2章采用的方式:缓存粒子系统组件,并手动迭代它们(确保每次传递给withChildren参数都是false)。
1)使用更多画布
画布组件的主要任务是管理在层次窗口中绘制UI元素的网格,并发出渲染这些元素所需的Draw Call。画布的另一个重要作用是将网格合并进行批处理(条件是这些网格的材质相同),以降低Draw Call数。然而,当画布或其子对象发生变动时,这称为“画布污染”。当画布污染后,就需要为画布上的所有UI对象重新生成网格,才可发出Draw Call。
值得注意的是,更改UI元素的颜色属性不会污染画布。
将UI拆分为多个画布,可以将工作负载分离开,简化单个画布所需的任务。在这种情况下,即使单个元素仍然发生变化,响应时需要重新生成的其它元素也更少,从而降低了性能成本。这种方法的缺点是,不同画布上的元素不会被批量组合在一起,因此,如果可能的话,应该尽量将具有相同材质的相似元素组合在同一画布中。
2)在静态和动态画布中分离对象
应该努力尝试在生成画布时,采用基于元素更新的时间给元素分组的方式。元素可分为3组:静态,偶尔动态,连续动态。静态UI元素永远不会改变,典型的示例有背景图等。动态元素可以更改,偶尔动态对象只在做出响应时更改,例如UI按钮按下或者暂停动作,而连续对象会定期更新,例如动画元素。
3)为无交互的元素禁用Raycast Target
4)通过禁用父画布组件来隐藏UI元素
如果想禁用UI的一部分,只要禁用其子节点的画布组件,就可以避免布局系统昂贵的重新调用。为此,可以将画布组件的enabled属性设置为false。这种方法的缺点是,如果任何子对象具有Update()、FixedUpdate()、LateUpdate()、Coroutine()方法,就需要手动禁用它们,否则这些方法将继续运行。
5)避免Animator组件
Unity的Animator组件从未打算用于最新版本的UI系统,它们之间的交互是不切实际的。每一帧,Animator都会改变UI元素的属性,导致布局被污染,重新生成许多内部UI信息。应该完全避免使用Animator,而使自己的动画内插方法或使用可实现此类操作的程序。
6)为World Space画布显式定义Event Camera
画布可用于2D和3D中的UI交互,这取决于画布的Render Mode设置是配置为Screen Space(2D)还是World Space(3D)。每次进行UI交互时,画布组件都会检查其eventCamera属性以确定要使用的相机。默认情况下,2D画布会将此属性设置为Main Camera,但3D画布会将其设置为null。每次需要Event Camera时,都是通过调用FindObjectWithTag()方法来使用Main Camera。通过标记查找对象并不像使用Find()方法的其它变体那么糟糕,但是其性能成本与在给定项目中使用的标记数量呈线性关系。更糟的是,在World Space画布的给定帧期间,Event Camera的访问频率相当高,这意味着将此属性设置为null,将导致巨大的性能损失且没有真正的好处。因此,对于所有的World Space画布,应手动将该属性设置为Main Camera。
7)不要使用alpha隐藏UI元素
color属性中alpha值为0的UI元素仍会发出Draw Call。应该更改UI元素的isActive属性,以便在必要时隐藏它。另一种方法时通过CanvasGroup组件使用画布组,该组件可用于控制其下所有子元素的alpha透明度。画布组的alpha值设置为0,将清楚子对象,因此不会发出任何Draw Call。
8)优化ScrollRect
· 确保使用RectMask2D
· 在ScrollRect中禁用Pixel Perfect
· 手动停用ScrollRect活动
即使移动速度是每帧只移动像素的一小部分,画布也需要重新生成整个ScrollRect元素。一旦使用ScrollRect.velocity和ScrollRect.StopMovement()方法检测到帧的移动速度低于某个阈值。就可以手动冻结它的运动。这有助于大大降低重新生成的概率。
9)使用空的UIText元素进行全屏交互
大多数UI的常用实现是激活一个很大、透明的可交互元素来覆盖整个实体屏幕,来强制玩家必须处理弹出窗口才能进入下一步,但仍然允许玩家看到原色背后发生的事情。这通常由UI Image组件完成,但可惜的是这可能会中断批处理操作,透明度在移动设备上可能会是一个问题。
解决这个问题的简单方法是使用一个没有定义字体或文本的UI Text组件。这将创建一个不需要生成任何可渲染信息的元素,只处理边界框的交互检查。
10)查看Unity UI源代码
Unity在bitbucket库中提供了UI系统的源代码,具体网址为:
https://bitbucket.org/Unity-Technologies/ui
经测试,书上网址已不可用,可以使用github上的Unity官方源码:
https://github.com/Unity-Technologies/uGUI
如果UI的性能上有重大问题,可以查看源代码来确定问题的原因。
11)查看文档
通过以下页面,可以了解更多有用的UI优化技巧,网址为:
https://unity3d.com/learn/tutorials/temas/best-practices/guide-optimizing-unity-ui
1)考虑使用针对移动平台的着色器
Unity中内置的移动着色器没有任何特定的约束限制它只能在移动设备中使用。它们只是针对最小的资源使用量进行了优化。
桌面应用完全可以使用这些着色器,但它们的图形质量往往会下降。能否接受图形质量下降只是一个小问题。因此,应考虑对面向移动平台的常见着色器做测试,以检查它们是否适合游戏。
2)使用小的数据类型
GPU使用更小的数据类型来计算比使用更大的数据类型(特别是在移动平台上)往往更快,因此可以尝试的第一个调整是用较小的版本(16位浮点)或甚至固定长度(12位定长)替换浮点数据类型(32位浮点)。前述数据类型的大小将根据平台偏好的浮点格式而有所不同。列出的大小都是最常见的。优化来自格式之间的相对大小,因为要处理的位数更少。
颜色值是降低精度的很好选择,因为通常可以使用低精度的颜色值而不会有明显的着色损失。然而,对于图形计算来说,降低精度的影响是非常不可预测的。因此,需要一些测试来验证降低精度是否会损失图形的保真度。
3)在重排时避免修改精度
重排是一种着色器编程技术,它将组件按照所需的顺序列出并复制到新的结构中,从现有向量中创建一个新的向量。例如:
float4 input = float4(1.0,2.0,3.0,4.0);
float3 value = input.xyz;
可以使用xyzw和rgba表示法依次引用相同的组件。不管是代表颜色还是向量,它们只是为了让着色器代码容易阅读。还可以按照想要的顺序列出组件,以填充新的数据,并在必要时重复使用它们。
在着色器中将一种精度类型转换为另一种精度类型是一项很耗时的操作,在重排时转换精度类型会更加困难。如果有使用重排的数学运算,请确保它们不会转换精度类型。更明智的做法是从一开始就只使用高精度数据类型,或者全面降低精度,以避免需要更改精度。
4)使用GPU优化的辅助函数
着色器编译器通常能很好地将数学计算简化为GPU的优化版本,但自定义代码的编译不太可能像CG库的内置辅助函数和Unity CG包含文件提供的其他辅助函数那样有效。应该尽量使用CG库或Unity库中的辅助函数,它能更好地完成自定义代码的工作。
5)禁用不需要的特性
只要禁用不重要的着色器特性,就可以节省成本。
6)删除不必要的输入数据
应该仔细检查着色器代码,以确保输入的所有几何体,顶点和片元数据都被实际使用。
7)只公开所需的变量
如果在项目结束时发现一些变量一直使用相同的值,就应该在着色器中使用常量替代它们,以去除过量的运行时负载。
8)减少数字计算的复杂度
复杂的数学会成为渲染流程中严重的瓶颈,因此应该尽可能限制其危害。完全可以提前计算复杂的数学函数,并将其输出作为浮点数据存储在纹理文件中,作为复杂数学函数的映射图。毕竟,纹理文件只是一个巨大的浮点值数据块,可以通过x,y和颜色(rgba)这三个维度进行快速索引。可以将这张纹理提供给着色器,并且在运行时在着色器中采样提前生成的表格,而不是在运行时进行复杂的计算。
这项技术需要额外的图形内存,在运行时存储纹理和一些内存带宽,但如果着色器已经接收到纹理但未使用alpha通道,就可以通过纹理的alpha通道偷偷把数据导入,因为数据已经传输过来,所以根本没有性能消耗。
9)减少纹理采样
纹理采样是所有内存带宽开销的核心消耗。使用的纹理越少,制作的纹理越小,则效果越好。
更糟糕的是,不按顺序进行纹理采样可以会给GPU带来一些非常昂贵的缓存丢失。如果这样做,纹理需要重新排序,以便按顺序进行采样。例如,用tex2D(y,x)替代tex2D(x,y),那么纹理查找操作将垂直遍历纹理,然后水平遍历纹理,几乎每次迭代都会造成缓存丢失。简单地旋转纹理文件数据,并按正确的顺序执行纹理采样(tex2D(x,y)),可以节省大量性能消耗。
10)避免条件语句
现代CPU运行条件语句时,会使用许多巧妙的预测技术来利用指令级的并行性,这是CPU的一个特性,它试图在条件语句实际被解析之前预测条件将进入的方向,并使用不用于解析条件的空闲内核推测性地开始处理条件的最可能结果。如果最终发现决策时错误的,则丢弃当前结果并选择正确的路径。只要推测处理和丢弃错误结果的成本小于等待确定正确路径所花费的时间,并且正确的次数多于错误的次数,这就是CPU速度的净收益。
然而,由于GPU的并行性,该特性对于GPUS架构来说并不能带来很大的好处。因此,应该避免在着色器代码中使用分支和条件语句。
11)减少数据依赖
编译器尽力将着色器代码优化未更友好的GPU底层语言,这样,在处理其它任务时,就不需要等待获取数据。例如:
float sum = input.color1.r;
sum = input.color2.g;
sum = input.color3.b;
sum = input.color4.a;
这段代码有一个数据依赖关系,由于对sum变量的依赖关系,每个计算都需要等上一个计算结束才能开始。但是,着色器编译器经常检测到这种情况,并将其优化为使用指令级并行的版本:
float sum1,sum2,sum3,sum4;
sum1 = input.color1.r;
sum2 = input.color2.g;
cum3 = input.color3.b;
sum4 = input.color4.a;
在本例中,编译器将识别并从内存中并行提取4个值,并通过线程级并行性操作独立获取所有4个值之后完成求和。相对于串行地执行4个取值操作,并行操作可以节省很多时间。
然而无法编译的长数据依赖链绝对会破坏着色器的性能。例如:
float4 value1 = tex2D(_tex1, input.texcoord.xy);
float4 value2 = tex2D(_tex2, value1.yz);
float4 value3 = tex2D(_tex3, value2.zw);
任何时候,都应该避免这样的强数据依赖关系。
12)表面着色器
Unity的表面着色器是片元着色器的简化形式,允许Unity开发人员以更简化的方式进行着色器编程。
13)使用基于着色器的LOD
可以强制Unity使用更简单的着色器来渲染远端对象,这是一种节省填充率的有效方法,特别是将游戏部署到多个平台或需要支持多种硬件功能时。LOD关键字可以在着色器中用来设置着色器支持的屏幕尺寸参数。如果当前LOD级别不匹配此参数值,它将转到下一个回退的着色器,以此类推,直到找到支持给定尺寸参数的着色器。
有关基于着色器的LOD的更多信息,请参见Unity文档:
https://docs/unity3d.com/Manual/SL-ShaderLOD.html(好像进不去这个网址)
这种方法简单直接,不失为一个值得考虑的好主意。不管是通过分辨率或者比特率来降低纹理质量,都不能获得理想质量的图形,但有时可以使用16位纹理来获得质量没有明显降低的图形。
查看Unity文档,以了解所有可用的纹理格式以及Unity默认推荐的纹理格式:
https://docs.unity3d.com/Manual/class-TextureImporterOverride.html
如果内存带宽存在问题,就需要减少正在进行的纹理采样量。这里并没有什么特别的技巧而言,因为内存带宽只与吞吐量有关,所以我们考虑的主要指标是所推送的数据量。
减少纹理容量的一种方法是直接降低纹理分辨率,从而降低纹理质量。但这显然不理想,所以另一种方法是采用不同的材质和着色器属性在不同的网格上重复使用纹理。例如,适当变暗的砖纹理可以看起来像石墙。当然,这需要不同的渲染状态,这种方法不会节省Draw Call,但它可以减少内存带宽的消耗。
还有一些方法可以将纹理组合到图集中,以减少纹理交换的次数。如果有一组纹理总是在相同的时间一起使用,那么它们可能会合并在一起,这样可以避免GPU在同一帧中反复拉取不同的纹理文件。
1)用隐藏的GameObject预加载纹理
在异步纹理加载过程中使用的空白纹理可能会影响游戏质量。我们想要一种方法来控制和强制纹理从磁盘加载到内存,然后在实际需要之前加载到VRAM。
一个常见的解决方法是创建一个使用纹理的隐藏GameObject,并将其放在场景中一条路径的某个位置,玩家将沿着这条路到达真正需要它的地方。一旦玩家看到该对象,就将纹理数据从内存复制到VRAM中,进行管线渲染。该方法有点笨拙,但是很容易实现,适用于大多数情况。
还可以通过脚本代码更改材质的texture属性,来控制此类行为:
GetComponent()material.texture = textureToPreload;
2)避免纹理抖动
极少数情况下,如果将过多的纹理数据加载到VRAM中而所需的纹理又不存在,则GPU需要从内存请求纹理数据,并覆盖一个或多个现有纹理,为其留出空间。随着时间推移,内存碎片化的情况会越来越糟,这将带来一种风险,即刚从VRAM中刷新的纹理需要在同一帧中再次取出。这将导致严重的内存冲突,因此应尽全力避免发生这种情况。
1)谨慎地使用实时阴影
阴影很容易成为Draw Call和填充率的最大消耗者之一,因此应该花时间调整这些设置,直到获得所需的性能和/或图形质量。
值得注意的是,因为硬阴影和软阴影唯一的区别是着色器比较复杂,因此相对于硬阴影,软阴影并不会消耗更多的内存或CPU。这意味着有足够填充率的应用程序可以启用软阴影特性,来提高图形的保真度。
2)使用剔除遮罩
灯光组件的Culling Mask属性是基于层的遮罩,可用于限制受给定灯光影响的对象。
剔除遮罩的对象只能是单个图层的一部分,在大多数情况下,减少物理开销可能比减少照明开销更重要。因此,如果两者存在冲突,那么这可能不是理想的方法。
3)使用烘培的光照纹理
优点:计算强度低
缺点:增加了应用程序的磁盘占用、内存消耗和内存带宽滥用的可能性。
1)避免alpha测试
2)最小化Draw Call
3)最小化材质数量
4)最小化纹理大小
5)确保纹理是方形且大小为2的幂次方
6)在着色器中尽可能使用最低的精度格式