Unity 批处理详讲(含URP)

          咱们在项目中,优化性能最重要的一个环节就是合批处理,,在早期Unity中,对于合批的处理手段主要有三种:   

  • Static Batching 
  • Dynamic Batching  
  • GPU Instancing  

如今Unity 为了提升合批范围与效率,提供了新的合批方式(SRP Batcher)。 

   

      一:初识   Draw Call、Batcher、 Sat pass Call  

                                               衡量CPU处理渲染速率的参考值

    Draw Call

            衡量CPU在渲染时的资源消耗大多都是是通过Draw Call的数量 因为CPU在渲染流水线中的处理阶段是应用程序阶段,主要是做一些数据的准备与提交工作,而Draw Call的数量代表了CPU向GPU提交的数据的次数,Draw Call本身只是一些数据流的字节,主要的性能消耗在于CPU的数据准备阶段。

   Batcher

          由于合批的出现,并不会每一个渲染对象都会产生一个Draw Call,所以这个时候就提出了一个新的衡量标准:Batcher

   Sat pass Call  

         CPU在渲染阶段,性能消耗的峰值一般不在于Draw Call,而往往存在于对其数据准备的阶段,因此单纯以数据的提交数量为衡量标准并不准确,同时在数据准备的过程中,假如前后两个材质发生了变化,会更大幅度的消耗性能,这也是整个CPU在渲染阶段最消耗性能的步骤,因此Unity通过Set Pass Call来作为性能消耗的标准。   (注意:实际上并没有减少Draw Call

   二:主流合批技术详讲

    1、Static Batching

            原理:

                     

  • 将静态游戏对象转换到世界空间并为它们构建一个共享的顶点和索引缓冲区。
  • 如果已启用 Optimized Mesh Data,则 Unity 会在构建顶点缓冲区时删除任何着色器变体未使用的任何顶点元素。为了执行此操作,系统会进行一些特殊的关键字检查;例如,如果Unity 未检测到 LIGHTMAP_ON关键字,则会从批处理中删除光照贴图 UV
  • 针对同一批次中的可见游戏对象,Unity 会执行一系列简单的绘制调用,每次调用之间几乎没有状态变化。在技术上,Unity不会减少 API绘制调用,而是减少它们之间的状态变化(这正是消耗大量资源的部分)。在大多数平台上,批处理限制为 64k 个顶点和 64k 个索引(OpenGLES 上为 48k 个索引,在 macOS 上为 32k 个索引)

      简单的来说,Static Batching通过对一些小的网格进行合并备份到内存中,当执行渲染操作时,CPU一次性将合并的内存的发送给GPU来减少Draw Call的数量,不过这样做有一定的限制:

  • 对象必须是静态的,不可移动
  • 合并的对象使用相同的材质

同时在使用Static Batching时需要额外的内存来存储组合的几何体,导致内存在一定程度上的浪费。简单来说,作为通过内存的上的置换可以获得时间上的高效运行,需要根据实际情况来谨慎添加渲染对象,避免获取CPU性能优势时产生不必要的内存问题

而关于Static Batching的使用,首先需要在Project Setting中的Player选项中勾选Static Batching

      接下来就可以在Inspector面板中对需要Static Batching的对象勾选上Batching Static,具体位置如下图所示: 

      

  • 无法参与批处理情况
  1. 改变Renderer.material将会造成一份材质的拷贝,因此会打断批处理,你应该使用Renderer.sharedMaterial来保证材质的共享状态。
  2. 相同材质批处理断开情况
    位置不相邻且中间夹杂着不同材质的其他物体,不会进行同批处理,这种情况比较特殊,涉及到批处理的顺序。
    拥有lightmap的物体含有额外(隐藏)的材质属性,比如:lightmap的偏移和缩放系数等。所以,拥有lightmap的物体将不会进行同批处理(除非他们指向lightmap的同一部分)。

总结: 虽然静态合批可以有效地减少批次,但是可能无法减少DC。

 2、Dynamic Batching   

        Dynamic Batching同样是可以对于有共同材质的对象进行相关的合并,但是其对象可以为动态的,而且这一过程是动态进行的,只需要在Project Setting中的Player中勾选上Dynamic Batching即可,但是注意,在URP模板中,这一选项移到了URP的配置文件中,具体位置如图: 

  

虽然Dynamic Batching的设置简单,但是其使用条件却很苛刻,Unity官方在文档中详细罗列限制条件:

  • 批处理动态游戏对象在每个顶点都有一定开销,因此批处理仅会应用于总共包含不超过 900 个顶点属性且不超过 300 个顶点的网格。如果着色器使用顶点位置、法线和单个 UV,最多可以批处理 300 个顶点,而如果着色器使用顶点位置、法线、UV0、UV1 和切线,则只能批处理 180 个顶点。
  • 如果游戏对象在变换中包含镜像,则不会对这些对象进行批处理(例如,具有 +1 缩放的游戏对象 A 和具有 –1 缩放的游戏对象 B 无法一起接受批处理)。即使游戏对象基本相同,使用不同的材质实例也会导致游戏对象不能一起接受批处理。例外情况是阴影投射物渲染。
  • 带有光照贴图的游戏对象具有其他渲染器参数:光照贴图索引和光照贴图偏移/缩放。通常,动态光照贴图的游戏对象应指向要批处理的完全相同的光照贴图位置。 多 pass 着色器会中断批处理。
  • 几乎所有的 Unity 着色器都支持前向渲染中的多个光照,有效地为它们执行额外 pass。“其他每像素光照”的绘制调用不进行批处理。 旧版延迟(光照 pre-pass)渲染路径会禁用动态批处理,因为它必须绘制两次游戏对象

    

   批处理中断情况:
       位置不相邻且中间夹杂着不同材质的其他物体,不会进行同批处理,这种情况比较特殊,涉及到批处理的顺序,我的另一篇文章有详解。
物体如果都符合条件会优先参与静态批处理,再是GPU Instancing,然后才到动态批处理,假如物体符合前两者,此次批处理都会被打断。
GameObject之间如果有镜像变换不能进行合批,例如,"GameObject A with +1 scale and GameObject B with –1 scale cannot be batched together"。
拥有lightmap的物体含有额外(隐藏)的材质属性,比如:lightmap的偏移和缩放系数等。所以,拥有lightmap的物体将不会进行批处理(除非他们指向lightmap的同一部分)。
使用Multi-pass Shader的物体会禁用Dynamic batching,因为Multi-pass Shader通常会导致一个物体要连续绘制多次,并切换渲染状态。这会打破其跟其他物体进行Dynamic batching的机会。
我们知道能够进行合批的前提是多个GameObject共享同一材质,但是对于Shadow casters的渲染是个例外。仅管Shadow casters使用不同的材质,但是只要它们的材质中给Shadow Caster Pass使用的参数是相同的,他们也能够进行Dynamic batching。
Unity的Forward Rendering Path中如果一个GameObject接受多个光照会为每一个per-pixel light产生多余的模型提交和绘制,从而附加了多个Pass导致无法合批,如下图:

可以接收多个光源的shader,在受到多个光源是无法合批

       总结:同时使用的Shader一定要是单Pass的。同时因为单Pass的限定,对于延迟渲染来说,由于将光照分离到单独的Pass去处理而导致受光的对象完全没有办法进行动态合批的操作,所以会直接屏蔽掉Dynamic Batching。

3、GPU Instanceing

     

适用前提:
兼容的平台及API

相同的Mesh与Material

支持不同的材质球属性块(MaterialPropertyBlock),用于解决动态修改材质的某些属性后无法合批的问题(因为动态改了相当于不同材质了)

不支持SkinnedMeshRenderer

Shader支持GPU Instancing

缩放为负值的情况下,会不参与加速。
受限于常量缓冲区在不同设备上的大小的上限,移动端支持的个数可能较低。
只支持一盏实时光,要在多个光源的情况下使用实例化,我们别无选择,只能切换到延迟渲染路径。为了能够让这套机制运作起来,请将所需的编译器指令添加到我们着色器的延迟渲染通道中。

效果:
  • 批渲染Mesh相同的那些物体,以降低DrawCall数
  • 这些物体可以有不同的参数,比如颜色与缩放,不同颜色要用(MaterialPropertyBlock)实现。

       使用 GPU Instanceing可使用少量绘制调用一次绘制(或渲染)同一网格的多个副本。它对于绘制诸如建筑物、树木和草地之类的在场景中重复出现的对象非常有用

  • GPU Instanceing在每次绘制调用时仅渲染相同的网格,但每个实例可以具有不同的参数(例如,颜色或比例)以增加变化并减少外观上的重复。
  • GPU Instanceing可以降低每个场景使用的绘制调用数量。可以显著提高项目的渲染性能。

         GPU Instanceing同样有一些使用限制条件:

  • Unity 自动选取要实例化的网格渲染器组件和 Graphics.DrawMesh调用。请注意,不支持 SkinnedMeshRenderer
  • Unity 仅在单个GPU实例化绘制调用中批量处理那些共享相同网格和相同材质的游戏对象。使用少量网格和材质可以提高实例化效率。要创建变体,请修改着色器脚本为每个实例添加数据

       参考文献:GPU实例化GPU 实例化 - Unity 手册GPU实例化

上面是官方文档对于GPU Instanceing的一些描述,可以看出与其他两种合批手段不同的是,除了材质相同之外,其主要是对于使用同一网格的物体有效,所以正如名字的Instanceing那样,是通过GPU直接对于某一物体进行实例化来降低CPU对场景物体的数据命令准备所产生的性能消耗的技术手段。 

原理:

简单地说就是一次对具有相同网格物体的多个对象发出一次绘图调用。CPU收集所有每个对象的变换和材质属性,并将它们放入数组中,然后发送给GPU。然后,GPU遍历所有条目,并按提供顺序对其进行渲染。

如何使用GUPInstancing:

   

//熊猫悟道
Shader "XiongMaoWuDao/Gpu_Instancing"
{
 
	Properties{
		_BaseColor("Color", Color) = (1.0, 1.0, 1.0, 1.0)
	}
	SubShader {
		Pass {
			HLSLPROGRAM
 
			// GUIInstancing  调用
			// 一次对具有相同网格物体的多个对象发出一次绘图调用。
			// CPU收集所有每个对象的变换和材质属性,并将它们放入数组中,然后发送给GPU(SetPassCall)。
			// 最后,GPU遍历所有条目,并按提供顺序对其进行渲染。
			#pragma multi_compile_instancing
 
			#pragma vertex vert
			#pragma fragment frag
			#include "UnlitPass.hlsl"					// 里面定义了顶点着色器以及片元着色器
			ENDHLSL
		}
	}
}
 
 
// --------------以下是UnlitPass.hlsl里的代码-----------------
 
// 为了支持GUIInstancing,这里CBUFFER_START改成用UNITY_INSTANCING_BUFFER_START宏
    UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
	UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)								// 把所有实例的_BaseColor放入内存缓冲区
    UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
 
// 顶点着色器输入
struct Attributes{
	float3 positionOS : POSITION;
	UNITY_VERTEX_i_INSTANCE_ID												// 启用GUIInstancing的时候,用此宏,可以让顶点传入实例化id
};
 
// 顶点着色器输出
struct Varyings {
	float4 positionCS : SV_POSITION;
	UNITY_VERTEX_i_INSTANCE_ID												// 启用GUIInstancing的时候,用此宏,让顶点着色器输出实例化id
};
 
Varyings vert(Attributes i){
	Varyings o;
	UNITY_SETUP_INSTANCE_ID(i);												// 从i中提取对象索引,并将其存储在其他GUIInstancing相关宏所依赖的全局静态变量中
	UNITY_TRANSFER_INSTANCE_ID(i, o);									// 把i中的实例化id转换到片元着色器中用的实例化id
	float3 positionWS = TransformObjectToWorld(i.positionOS);
	o.positionCS = TransformWorldToHClip(positionWS);
	return o;
}
 
float4 frag(Varyings i) : SV_TARGET{
	UNITY_SETUP_INSTANCE_ID(i);												// 从i中提取对象索引,并将其存储在其他GUIInstancing相关宏所依赖的全局静态变量中
	return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);			// 根据实例id从_BaseColor数组中取出对应的_BaseColor
}

当需要创建海量mesh的时候,一般不要用实例化游戏物体的方式,这样会比较消耗性能,推荐使用Graphics.DrawMeshInstanced来创建。

   例如  :

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 

 // 动态生成大量球体mesh,用来测试GPUInstancing
public class MeshBall : MonoBehaviour
{
    static int baseColorId = Shader.PropertyToID("_BaseColor");
 
    [SerializeField]
    Mesh mesh = default;         // 手动拖入mesh
 
    [SerializeField]
    Material material = default;  // 手动拖入支持GPUInstancing的材质球
 
    Matrix4x4[] matrices = new Matrix4x4[500];
    Vector4[] baseColors = new Vector4[500];
 
    MaterialPropertyBlock block;
 
    private void Awake()
    {
        for (int i = 0; i < matrices.Length; i++)
        {
            matrices[i] = Matrix4x4.TRS(Random.insideUnitSphere * 10f, Quaternion.identity, Vector3.one);
            baseColors[i] = new Vector4(Random.value, Random.value, Random.value, 1f);
        }
    }
 
    private void Update()
    {
        if(block == null)
        {
            block = new MaterialPropertyBlock();
            block.SetVectorArray(baseColorId, baseColors);
        }
        Graphics.DrawMeshInstanced(mesh, 0, material, matrices, 500, block);
    }
}

注意: Graphics.DrawMeshInstanced()这方法还有两问题

1.一次最多画1023个元素,如果超出就会报错,所以需要将草进行分类管理。

2.它不提供裁切的功能,也就是说摄像机看不到的地方,这些草是不会被剔除掉的,依然会被渲染。

解决这个问题,为了避免运行时暴力的for循环来判断是否在视野内,我采取的方法是预先将场景分成20X20若干个格子(可根据游戏的可视范围而定)根据玩家的位置,始终只渲染周围9个格子内的草元素,这样将大幅度减少运行时for循环的次数。

如果每个草的顶点色是不一样的怎么办呢?可以用MaterialPropertyBlock来让同一个材质求有不同的属性。

4、SRP Batcher

适用前提:
        需要是同一个shader变体,可以是不同的材质球,项目需要使用自定义渲染管线,Shader代码必须兼容SRP Batcher。

        但是不支持用材质球属性块(MaterialPropertyBlock)

        渲染的物体必须是一个mesh或者skinned mesh。不能是粒子。

效果:

        可以有效降低SetPassCall(设置渲染状态)的数目,用于CPU性能优化。

开启SRP Batch: 要使用 SRP Batcher,项目必须使用可编程渲染管线。可编程渲染管线可以是:

  • 通用渲染管线 (URP)
  • 高清渲染管线 (HDRP)
  • 自定义 SRP

由于后两种方式不常用,所以本文章会基于URP模板来介绍,而关于URP的具体细节,可以查看该文章:Unity 升级项目到Urp(通用渲染管线)以及画面后处理

当我们在项目中使用URP模板后,就可以在资源目录中找到当前项目的URP配置文件,在其中可以看到SRP Batcher的控制选项:

同时当项目在URP模板下时Dynamic Batching的开关控制选项也被迁移到了配置文件,但是相比于默认渲染管线该技术默认是被关闭的,因为其相对于SRP Batcher来说并没有优势

   参考文献:可编程渲染管线 SRP Batcher - Unity 手册

SRP Batcher原理:

Unity中,可以在一帧内的任何时间修改任何材质的属性。但是,这种做法有一些缺点。例如,DrawCall 使用新材质时,要执行许多作业。因此,场景中的材质越多,Unity 必须用于设置GPU 数据的 CPU也越多。解决此问题的传统方法是减少 DrawCall的数量以优化CPU 渲染成本,因为 Unity 在发出 DrawCall之前必须进行很多设置。实际的 CPU 成本便来自该设置,而不是来自 GPU DrawCall本身(DrawCall 只是 Unity需要推送到 GPU 命令缓冲区的少量字节)

正如Set Pass Call的描述那样,游戏在渲染阶段CPU的性能消耗主要在与材质切换阶段的一些作业,而SPR Batcher通过在GPU的数据缓冲区的持久化存储来换取CPU的新材质的准备时间,从而降低CPU的数据准备压力

SRP Batcher 通过批处理一系列 BindDraw GPU 命令来减少 DrawCall之间的 GPU 设置,具体过程如图所示:

     

            为了获得最大渲染性能,这些批次必须尽可能大。为了实现这一点,可以使用尽可能多具有相同着色器的不同材质,但是必须使用尽可能少的着色器变体

在内渲染循环中,当 Unity 检测到新材质时,CPU 会收集所有属性并在 GPU 内存中设置不同的常量缓冲区。GPU缓冲区的数量取决于着色器如何声明其 CBUFFER 。

为了在场景使用很多不同材质但很少使用着色器变体的一般情况下加快速度,SRP 在原生集成了范例(例如GPU 数据持久性)。

  SRP Batcher是一个低级渲染循环,使材质数据持久保留在 GPU 内存中。如果材质内容不变,SRP Batcher 不需要设置缓冲区并将缓冲区上传到 GPU。实际上,SRP Batcher 会使用专用的代码路径来快速更新大型 GPU 缓冲区中的 Unity 引擎属性,如下所示:

    

这是 SRP Batcher 渲染工作流程。SRP Batcher 使用专用的代码路径来快速更新大型 GPU 缓冲区中的 Unity 引擎属性。在此处,CPU仅处理上图中标记为 Per Object large buffer的 Unity 引擎属性。所有材质在 GPU 内存中都有持久的 CBUFFER,可供随时使用。这样会加快渲染速度,原因是: 现在,所有材质内容都持久保留在 GPU 内存中。 专用代码针对所有每对象属性,管理着一个大型的每对象GPU CBUFFER 。

优化原理:

        简单的说,就是把同一种shader对应的材质球的材质、颜色通通放到一个缓冲区中,不用每帧设置给GPU,每帧仅仅设置坐标、缩放、转换矩阵等变量给GPU。

SRP Batcher 限制条件:

为了使 SRP Batcher代码路径能够渲染对象:

  • 渲染的对象必须是网格或蒙皮网格。该对象不能是粒子。
  • 着色器必须与 SRP Batcher 兼容。HDRP 和 URP 中的所有光照和无光照着色器均符合此要求(这些着色器的“粒子”版本除外)。 为了使着色器与 SRP Batcher 兼容:
  • 必须在一个名为UnityPerDraw的 CBUFFER 中声明所有内置引擎属性。例如unity_ObjectToWorld 或 unity_SHAr
  • 必须在一个名为 UnityPerMaterial的 CBUFFER 中声明所有材质属性
如何让Shader支持SRPBatcher:
   1、必须声明所有内建引擎properties 在一个名为"UnityPerDraw"的CBUFFER里。

     

// 如果需要支持SRP合批,内置引擎属性必须在“UnityPerDraw”的 CBUFFER 中声明
CBUFFER_START(UnityPerDraw)
float4x4 unity_ObjectToWorld;			// 模型空间->世界空间,转换矩阵(uniform 值。它由GPU每次绘制时设置,对于该绘制期间所有顶点和片段函数的调用都将保持不变)
float4x4 unity_WorldToObject;			// 世界空间->模型空间
float4 unity_LODFade;
real4 unity_WorldTransformParams;		// 包含一些我们不再需要的转换信息,real4向量,它本身不是有效的类型,而是取决于目标平台的float4或half4的别名。(需要引入unityURP库里的"Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"才能使用real4)
CBUFFER_END

        注意:URP内置的UnityInput.hlsl里自带更全面的代码。也就是说,如果你的代码有引用或间接引用UnityInput.hlsl,那就不用做这一步了。

 2、必须声明所有材质properties在一个名为"UnityPerMaterial"的CBUFFER里。
// 使用核心RP库中的CBUFFER_START宏定义,因为有些平台是不支持常量缓冲区的。这里不能直接用cbuffer UnityPerMaterial{ float4 _BaseColor };
// Properties大括号里声明的所有变量如果需要支持合批,都需要在UnityPerMaterial的CBUFFER中声明所有材质属性
// 在GPU给变量设置了缓冲区,则不需要每一帧从CPU传递数据到GPU,仅仅在变动时候才需要传递,能够有效降低set pass call
CBUFFER_START(UnityPerMaterial)
float4 _BaseColor;															// 将_BaseColor放入特定的常量内存缓冲区
CBUFFER_END

若需要配合GPUInstancing则需要改写为

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
	UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)								// 把所有实例的_BaseColor以数组的形式声明并放入内存缓冲区
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

 

  • 15
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值