ToB企服应用市场:ToB评测及商务社交产业平台

标题: 【UnityShader入门精要学习笔记】第十六章 Unity中的渲染优化技术 (上) [打印本页]

作者: tsx81429    时间: 2024-6-26 07:19
标题: 【UnityShader入门精要学习笔记】第十六章 Unity中的渲染优化技术 (上)

本系列为作者学习UnityShader入门精要而作的笔记,内容将包罗:

我的GitHub仓库
总之适用于同样开始学习Shader的同学们举行有弃取的参考。


  

移动平台上的优化

对游戏的优化,一开始就应当视为游戏计划的一部分,特别是当游戏大概在一些低配装备上运行的时间,例如移动装备,在移动装备上的GPU与PC的GPU计划完全不同,它能利用的带宽,功能和其他资源特别有限。这要求我们时候把优化服膺在心。才可以制止等到项目完成时才发现游戏根本无法在移动装备上运行。
在本章中,我们将会学习到一些关于渲染的优化技术:
影响性能的因素

一个游戏重要利用两种盘算资源:CPU和GPU。其中CPU重要负责保证帧率,GPU重要负责分辨率渲染等相干的一些处置惩罚
我们可以把造成性能瓶颈的重要缘故原由分成以下几个方面:
(1) CPU

(2) GPU



(3) 带宽


对于CPU来说,限制他的重要是每一帧中DrawCall的数目。我们曾介绍过DrawCall的相干概念和原理,简单来说,就是CPU在每次关照GPU举行渲染之前,都需要提前准备好顶点数据(如位置、法线、颜色、坐标纹理等),然后调用一系列API把他们放到GPU可以访问到的指定位置。
末了调用一个DrawCall绘制指令关照GPU将准备好的数据取走并举行盘算。
但是过多的DrawCall会导致CPU性能瓶颈,因为每次调用DrawCall时CPU往往都需要改变很多的渲染状态的设置,这些利用都是很耗时的。假如一帧中需要的DrawCall数目过多的话,就会导致CPU大部分时间都花在提交DrawCall上了。
其他的一些因素,例如物理、布料模仿、蒙皮、粒子模仿等,这些都是盘算量很大的利用,也会导致CPU效率低下。

而对于GPU来说,它负责整个渲染流水线,从处置惩罚CPU传递过来的模型数据开始,举行顶点着色器、片元着色器等一系列的工作,末了输出到屏幕上的每个像素。因此,GPU的性能瓶颈和需要处置惩罚的顶点数目、屏幕分辨率、显存等因素有关。而相干的优化策略可以从减少处置惩罚的数据规模(包罗绘制的顶点数量和片元数量,制止overDraw)、减少运算复杂度等方面入手
后续本章会涉及的优化技术有:
CPU优化:

GPU优化

(3) 节省内存带宽


渲染统计窗口


在游戏画面的右上角,我们可以通过states来查看渲染统计窗口,该窗口表现了3个方面的信息:音频、图像和网络(似乎后面的版本不表现网络了)。
渲染统计窗口表现了很多紧张的渲染数据,例如FPS、批处置惩罚数目、顶点和三角形网格的数目等。


这些较为基础的数据指示了我们该从哪些方面举行优化,但是需要更多分析的话则需要性能分析器:
性能分析器


性能分析器指示了程序运行时的大部分信息。例如在Rendering一栏中,绿线代表批处置惩罚数量,蓝线代表了PassCall的数量,还有一些其他的信息,例如顶点和三角形面的信息等。
然而性能分析器给出的DrawCall数量和批处置惩罚数量、Pass数量等等不肯定准确,往往会大于我们估算的数量。这是由于Unity需要举行很多其他的工作,例如初始化各个缓存,为阴影更新深度纹理和阴影映射纹理等,因此需要花费比预期更多的DrawCall。
帧调试器


利用帧调试器,我们可以清楚的看到每一个DrawCall的工作结果,看到渲染该帧时发生的所有的DrawCall渲染变乱以及当前渲染变乱利用的Pass,每一步实现了什么样的效果。
在移动平台上举行优化时,由于上述的内置分析器往往是基于PC的分析结果,有时我们还需要利用移动平台专用的性能分析工具来举行分析。

减少DrawCall的数量

为了将一个物体渲染到屏幕上,CPU需要检查哪些光源影响了该物体,绑定Shader并设置它的参数(包罗材质,网格,各类贴图等等,这些都由drawcall传递),再把渲染命令发送到GPU,当场景中包含了大量对象时,这些利用就会非常耗时。
例如我们想要渲染一千个三角形,假如按照一千个单独的网格举行渲染,所花费的时间要远宏大于渲染一个带有一千个三角形的网格。因为为一千个物体准备DrawCall和为一个物体准备DrawCall,显然前者的耗时更多,而在GPU上二者的盘算却根本没有区别。
因此CPU的DrawCall会成为优化瓶颈,一个优化思想就是尽大概的减少DrawCall的数量。
关于渲染相干数据结构的说明

顶点缓冲区对象(VBO):重要用于存储顶点以及顶点附带的各种属性信息,比如顶点位置、法线、颜色、UV等
顶点数组对象(VAO):规定VBO中数据的格式。比如多少空间存储顶点坐标,多少空间存储顶点法线等等
索引缓冲区对象(EBO):负责缓存VBO中顶点的索引,用来解决顶点数据重复利用的题目,制止顶点数据被重复存储。举个例子,绘制一个长方形需要四个顶点、两个三角形,在没有EBO的情况下需要在VBO中存储6个顶点的数据(其中两个是重复的)。存在EBO时,VBO中存储四个顶点的数据,通过EBO中的索引顺序重复调用VBO中相应顶点数据绘制三角形
批处置惩罚

什么样的物体可以一起处置惩罚呢?答案是利用了同一个材质的物体,对于利用了同一材质的物体,他们的区别仅仅在于利用的顶点数据的差别,我们可以将他们的顶点数据在一次DrawCall中合并,再一起发给GPU,从而完成一次批处置惩罚
Unity中支持两种批处置惩罚方式,一种是动态批处置惩罚,另一种是静态批处置惩罚,对于动态批处置惩罚来说,优点是一切处置惩罚都是Unity自动完成的,不需要我们本身完成任何利用,且物体是可以移动的。但缺点是限制很多,大概一不小心就劈坏了这种机制,导致Unity无法动态批处置惩罚利用了雷同材质的物体
静态批处置惩罚的优点是自由度很高,限制很少;但缺点是大概会占用更多的内存,并且经过静态批处置惩罚后的所有物体都不可以再移动了。(即使在脚本中尝试改变物体的位置也是无效的)
动态批处置惩罚

假如一些模型共享了同一个材质并满足一些条件,则Unity会自动为其举行动态批处置惩罚,将这些网格合并一个DrawCall。
动态批处置惩罚的原理是对可批处置惩罚的模型网格举行一次合并,在把合并后的模型数据传递给GPU,并用同一个材质举行渲染。且举行了批处置惩罚之后的模型仍然可以移动,这是由于处置惩罚每帧时Unity都会重新合并一次网格。
固然动态批处置惩罚不需要我们举行任何利用,但是注意只有满足条件的模型和材质才会被动态批处置惩罚:
转自Unity性能优化之动态合批
动态合批条件:

动态合批的适用范围:


勾选动态批处置惩罚前,三个方块需要4个DrawCall

在Project Setting中勾选动态批处置惩罚后同样材质的物体将节省两个DrawCall。假如勾选静态批处置惩罚也是节省两个DrawCall,但是静态批处置惩罚后物体位置不可移动。

动态合批的缺点:

动态合批失败的情况:

随后我们试试不同模型的动态合批:

可以看到即使是不同的模型,也成功举行了动态合批,这是由于这些模型的顶点数小于300

在增加了一个顶点数大于300的球体后,drawcall也随之增加了,说明该网格并没有被动态合批。

动态合批中断的情况:


在中间穿插了一个不同材质的物体,此时是有合批的

将左边物体移出合批范围,尽管左边网格仍然在屏幕内,但此时两个雷同材质的网格不合批了
在隐藏中间的物体之后,两个雷同材质的网格又合批了


将其中一个物体的一个轴举行翻转,则不合批
若两个轴举行翻转,则还是合批
若三个轴举行翻转,则不合批


在场景中新增了一个点光源后,动态合批失效了,这是由于渲染了多个Pass的Shader在应用多个光照下破坏了动态合批的机制。需要处置惩罚的pass由2个变为了2*2=4个。当然不在点光源内的物体们依旧会被合批。

静态合批

静态批处置惩罚是另一种合批方式,它可以适用于任何巨细的多少模型。其原理是:在程序开始运行的阶段,把需要举行静态批处置惩罚的模型合并到一个新的网格结构中,这意味着这些模型不可以在运行时候被移动。但由于它只需要举行一次合并利用,因此比动态批处置惩罚更高效。
静态批处置惩罚算是一个典型的用内存换时间的策略。要做优化无非就是从两个资源下手:内存优化和盘算时间优化。而除了减少内存斲丧和盘算时间斲丧外,我们也可以利用内存换时间,或是用时间换内存。
静态批处置惩罚的缺点在于:往往需要占用更多的内存来存储合并后的多少结构。假如合并前的物体共享了雷同的网格,那么合批时内存中的每一个该物体都会复制一个该共享网格。例如有1000棵树模型共享了一个树的网格模型,当我们对这1000棵树举行静态批处置惩罚,那么每棵树都会在内存中复制一个树的网格模型,那么所斲丧的内存就是原来的1000倍!
在上述情况下固然合批后性能是进步了,但是内存斲丧太大了,反而得不偿失。那么假如这些树的网格恰恰超过了动态合批限制的顶点数量,那么动态合批也用不了了。这种情况下,要么自行编写合批代码,要么我们可以利用GPU实例化来解决。

在静态合批下,可以看到节省了2个DrawCall
在面板中查看此时的模型网格,会发现所有静态合批的模型都合并为了同一种VBO网格:

可以看到包含了4个submeshes,对于合并后的网格,Unity会判断其中利用同一个材质的子网格,然后对它们举行批处置惩罚。
在内部实现上,unity会将这些静态物体变换到天下空间下,然后为它们构建一个更大的顶点和索引缓存。而对于利用了同一材质的物体,Unity只需要调用一个DrawCall就可以绘制全部的物体。而对于利用不同材质的物体,静态批处置惩罚则同样可以提升渲染性能——尽管利用材质不同,但是静态批处置惩罚减少了这些DrawCall之间的状态切换(上下文切换)。
同时我们发现,尽管有三个茶壶,但是子物体依旧是四个,因此尽管每个茶壶利用的网格是雷同的,但是在内存中会缓存一个该网格的复制,因此是三个网格,而非三个茶壶直接共用一个网格。

现在我们在场景中增加一个点光源,会发现DrawCall数量显然增加了。但是由于处置惩罚平行光的BasePass部分仍然会被静态批处置惩罚,因此依然为我们节省了两个DrawCall。

共享材质

无论是动态批处置惩罚还是静态批处置惩罚,都要求模型利用同一种材质(同一材质,而非同一Shader)。但有时我们希望利用同一种材质,但材质中的部分数据有变革,例如颜色,某些属性等,例如我们想要给茶壶换换色,使得场景中同时存在两种颜色的茶壶,那么在编辑器中就需要创建两种材质,即使它们是同一个shader。
为了利用一个材质实现不同模型的微调效果,一种常用的方法就是利用网格的顶点数据(最常见的就是顶点颜色数据)来存储这些参数。
经过合批的物相识集成一个更大的VBO发送给GPU,VBO中的数据作为输入传递给顶点着色器,因此,我们可以巧妙地对VBO中的数据举行控制,例如丛林场景中所有的树利用了同一种材质,我们希望它们可以通过批处置惩罚减少DrawCall,又希望不同树利用不同颜色,此时我们可以利用网格的顶点颜色来调整。
假如我们需要访问合批后的共享材质,应当利用Renderer.shadedMaterial来保证修改的是和其他物体共享的材质,但这意味着材质修改会应用到所有利用该材质的物体上。另一个雷同的API是Renderer.material,假如利用Renderer.material来修改材质,Unity会创建一个该材质的复制品,从而破坏批处置惩罚在该物体上的应用。
批处置惩罚的注意事项

在选择利用动态批处置惩罚还是静态批处置惩罚时,有一些小小的建议:

在利用批处置惩罚时还需要注意,由于批处置惩罚需要把模型变换到天下空间下再合并,因此,假如Shader中存在一些基于模型空间下的坐标的运算,那么往往会得到错误的结果。要么把坐标运算变换到天下空间下举行,要么再Shader中利用DisableBatching标签来强制利用该shader的材质不会被批处置惩罚。
另一个注意事项是,利用半透明材质的物体通常需要利用严格的从后往前的绘制顺序来保证透明混淆的正确性。对于这些物体,unity会先保证绘制顺序,再应用批处置惩罚,若合批的绘制顺序不能满足则无法应用批处置惩罚。

GPU实例化

这里要拓展一些优化DrawCall的小本领,一个是GPU实例化。假设我们要生成很多士兵,这些士兵利用雷同的模型网格和雷同的材质,利用雷同的动画和骨骼。那么我们能不能用一个DrawCall来生成所有的士兵?假如利用之前讲的静态合批的话,未免也太耗内存了,有一个更好的方法——GPU Instancing

通过在材质面板上勾选该选项开启GPU实例化,Unity的表面着色器自带GPU实例化选项,而顶点片元着色器则需要在代码中利用开启GPU实例化的宏。
GPU Instancing(GPU实例化)有很多优点,起首,我们可以通过GPU示例化用一个DrawCall就能完成一批雷同材质雷同模型物体的渲染——因为一次DrawCall能把材质和模型缓存生存在GPU中,接着GPU直接调用缓存即可。
除此之外,我们可以用GPU Instancing对每个重复生成的物体单独举行材质修改,只需要在shader中设置属性块变量,并在C#中利用MaterialPropertyBlock类赋值该变量即可,例如下面代码:
Shader:
  1. UNITY_INSTANCING_BUFFER_START(prop)
  2. UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color)
  3. UNITY_INSTANCING_BUFFER_END(prop)
复制代码
C#:
  1. GameObject chair = Instantiate(Prefab,new Vector3(pos.x,0,pos.y),Quaternion.identity);
  2. MaterialPropertyBlock prop = new MaterialPropertyBlock();
  3. prop.SetColor("_Color",color);
  4. chair.GetComponentInChildren<MeshRenderer>().SetPropertyBlock(prop);
复制代码
开启GPU实例化后,所有雷同模型网格和材质的实例渲染时只调用一个DrawCall,我们可以Instantiate直接生成实例物体(当然生成实例时需要斲丧性能),生成实例方便我们直接去访问它们并对其举行各类利用。
大概有时我们并不需要利用大概访问这些实例,此时我们可以直接将其渲染到屏幕上,如下述代码所示:
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEngine.Rendering;
  5. public class GrassInstance : MonoBehaviour
  6. {
  7.        
  8.         public GameObject prefab;
  9.         public int InstanceCount = 10;
  10.         public float Scale = 4.0f;
  11.    
  12.         private Mesh mesh;
  13.         private Material material;
  14.         private List<Matrix4x4[]> matrixList;
  15.         private Matrix4x4[] matrix;
  16.         private MeshFilter[] meshFilters;
  17.         private int matrixCount;
  18.         private Renderer[] renders;
  19.         private MaterialPropertyBlock materialPropertyBlock;
  20.    
  21.         void Awake() {
  22.                 if(prefab == null)
  23.                         return;
  24.                 matrixList = new List<Matrix4x4[]>();
  25.                 var meshFilter = prefab.GetComponent<MeshFilter>();
  26.                 if(meshFilter) {
  27.                         mesh = prefab.GetComponent<MeshFilter>().sharedMesh;
  28.                         material = prefab.GetComponent<Renderer>().sharedMaterial;
  29.                 }
  30.                 materialPropertyBlock = new MaterialPropertyBlock();
  31.                
  32.                 // 每组GPUInstance指令最多生成1023个物体
  33.                 matrixCount = InstanceCount / 1024 + 1;
  34.                 int RemainCount = InstanceCount;
  35.                 for (int i = 0; i < matrixCount; i++)
  36.                 {
  37.                         int LoopTime = RemainCount - (matrixCount - i - 1) * 1023;
  38.                         matrix = new Matrix4x4[LoopTime];
  39.                         for (int j = 0; j < LoopTime; j++)
  40.                         {
  41.                                 float x = Random.Range(2.0f, 6.0f);
  42.                                 float y = 0;
  43.                                 float z = Random.Range(-10.0f, 10.0f);
  44.                                 matrix[j] = Matrix4x4.identity;  
  45.                                 //设置位置
  46.                                 matrix[j].SetColumn(3, new Vector4(x, y, z, 1));  
  47.                                 //设置缩放,矩阵缩放
  48.                                 matrix[j].m00   = Scale;
  49.                                 matrix[j].m11   = Scale;
  50.                                 matrix[j].m22   = Scale;
  51.                         }
  52.                         matrixList.Add((matrix));
  53.                         RemainCount -= LoopTime;
  54.                 }
  55.         }
  56.    
  57.         void Update() {
  58.                 for (int i = 0; i < matrixList.Count; i++)
  59.                 {
  60.                         Graphics.DrawMeshInstanced(mesh, 0, material, matrixList[i], matrixList[i].Length,(MaterialPropertyBlock) null, ShadowCastingMode.On, true, 5);
  61.                 }
  62.         }
  63. }
复制代码
我们可以利用DrawMeshInstanced直接将物体渲染到屏幕上,如许就不用生成实例了,更节省性能。当然这些直接绘制的物体由于没有实例,就无法访问预制体上的一些组件了,不外一些shader中的交互还是可以实现的。

(利用GPU实例化生成的4096棵草)
利用GPU实例化,我们就可以生成很多雷同模型雷同材质的士兵,它们利用雷同的骨骼动画,并且我们可以分别设置它们的属性块。而且竟然只利用一个DrawCall,对于性能是巨大的提升。

享元模式

参考:Unity计划模式:享元模式,享元模式(附代码)
享元模式严格来说并不属于渲染范畴的内容,而是一种计划模式。还是以上述的士兵为例,每个士兵单元的基础属性应该都是雷同的,而通常士兵的血量、身高这些属性是独立的。假如每个士兵预制体类用一个脚本SoliderManager
来管理士兵的属性的话。那么每个Manager上都带有重复的基础属性,1000个士兵的属性就要在内存中重复生存1000次,且不同预制体上的雷同属性的地址各不雷同,显然浪费了内存和CPU资源。
因此假如对于同个士兵,我们能够让所有士兵都引用同一个属性的话,就不需要再创建一个属性了。即使有一千个,一万个士兵,它们引用的基础属性也始终只有一个。
  1. public class FlyweightAttr
  2. {
  3.     public int maxHp { get; set; }
  4.     public float moveSpeed { get; set; }
  5.     public string name { get; set; }
  6.     public FlyweightAttr(string name, int maxHp, float moveSpeed)
  7.     {
  8.         this.name = name;
  9.         this.maxHp = maxHp;
  10.         this.moveSpeed = moveSpeed;
  11.     }
  12. }
复制代码
  1. public class SoldierAttr
  2. {
  3.     public int hp { get; set; }
  4.     public float height { get; set; }
  5.     public FlyweightAttr flyweightAttr { get; }
  6.     // 构造函数
  7.     public SoldierAttr(FlyweightAttr flyweightAttr, int hp, float height)
  8.     {
  9.         this.flyweightAttr = flyweightAttr;
  10.         this.hp = hp;
  11.         this.height = height;
  12.     }
  13. }
复制代码
可以看到士兵属性类的构造函数中定义了一个共享属性类,只需在实例化士兵属性类的时间为构造函数传入共享属性类的引用就可以使所有的士兵都引用同一个共享属性类。
之以是提到享元模式,是因为在模型情况下,例如我们要利用雷同材质网格模型时,材质,网格实际上也可以看作共享属性。利用享元模式,我们可以将共享的属性只发给GPU一次,不就是相当于将多个DrawCall节省为了一个DrawCall?
利用享元模式利用雷同材质:
  1. using UnityEngine;
  2. using System.Collections;
  3. using System;
  4. public class flyweightTerrain : MonoBehaviour {
  5.     public Material redMat;
  6.     public Material greenMat;
  7.     flyweightTile redTile;
  8.     flyweightTile greenTile;
  9.     flyweightTile[,] tiles;
  10.     int width = 5;
  11.     int height = 5;
  12.     int[,] terrain = {
  13.         { 0,1,0,0,0},
  14.         { 0,0,0,1,0},
  15.         { 1,0,0,1,0},
  16.         { 1,0,0,0,0},
  17.         { 0,0,1,0,0}
  18.     };
  19.     void Start () {
  20.         redTile = new flyweightTile(redMat, true);
  21.         greenTile = new flyweightTile(greenMat, false);
  22.         drawTerrain();
  23.     }
  24.     void drawTerrain() {
  25.         tiles = new flyweightTile[width, height];
  26.         for (int i = 0; i < width; i++)
  27.             for (int j = 0; j < height; j++)
  28.             {
  29.                 if (terrain[i, j] == 0)
  30.                     tiles[i, j] = greenTile;
  31.                 else
  32.                     tiles[i, j] = redTile;
  33.             }
  34.         for (int i = 0; i < width; i++)
  35.             for (int j = 0; j < height; j++)
  36.             {
  37.                 GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
  38.                 obj.transform.position = new Vector3(i - 2, 0, j);
  39.                 obj.GetComponent<MeshRenderer>().material = tiles[i, j].mat;
  40.             }
  41.     }
  42. }
  43. class flyweightTile {
  44.     public flyweightTile(Material mat, bool isHard=false){
  45.         this.mat = mat;
  46.         _ishard = ishard;
  47.     }
  48.     public Material mat;
  49.     bool _ishard = false;
  50.     public bool ishard {
  51.         get { return _ishard; }
  52.     }
  53. }
复制代码

利用享元模式,我们生成了两种不同的立方体。其中雷同材质的立方体共享了性能斲丧。
假设这是个生成地形的代码,两种材质代表了两种地形,我们就可以用享元模式共享性能斲丧,生成两种不同地形。


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4