找回密码
 立即注册
查看: 184|回复: 0

【Unity】URP衬着效率优化: SRP合批技巧

[复制链接]
发表于 2024-7-15 17:48 | 显示全部楼层 |阅读模式
URP(Universal Render Pipeline)是目前Unity游戏开发中常用的衬着管线,是官方基于SRP(Scriptable Render Pipeline)架构提供的一套模板实现。本篇主要记录一些在实际项目开发落地过程中的实战操作注意点,以便团队和所有想做游戏的小伙伴在实际开发过程中可以快速了解合批操作,而想要了解URP和SRP具体技术细节道理的的小伙伴可以移步到官网链接
【URP使用手册】通用衬着管线概述 | Universal RP | 12.1.1 (unity3d.com)
本篇所采用开发环境如下

  • Unity3D 2020.x ~ 2022.x
  • Visual Studio Community 2022 v17.5.0 or later
  • XCode 14.2
Universal RP与Built In RP最大的衬着性能提升点之一 —— SRP Batch(合批):对于同一个Shader,撑持拥有不异关键字(keyword)和不异衬着挨次的分歧变体进行合批操作,以达到降低draw call的目的,进而减少cpu消耗。本篇主要就以下几个方面进行分享:

  • 着色器Shader
  • 关键字Keyword
  • 衬着队列Render Queue
  • 暗影合批Shadow Cast
  • 动态与静态物体
<hr/>1、着色器Shader

Built In管线中,unity仅撑持不异的材质球(Material)的物体进行合批(不异材质球代表着色器必然不异)。而URP管线则撑持不异着色器的分歧材质球进行合批操作后再调用draw call,这个操作就是SRP合批操作。在合批操作后,每个批次的衬着都需要通过pass通道来绑定一个着色器,分歧批次之间的调用则需要更改Pass通道,这个操作的次数就是SetPass call。按照这个道理,在URP衬着的优化过程中存眷SetPass calls调用数要比其他任何数据都更重要得多,SetPass call数可以类比于draw call。
在Unity中新建一个空场景,可以看到SetPass calls值为5



图1 空场景中pass为5

向场景中插手不异着色器Shader下的两个分歧材质球的小方块



图2

此时pass call值为8,打开Frame Debugger仔细查看,此时多出了3个SRP Batch,分袂是:

  • 绘制暗影的MainLightShadow
  • 绘制法线的DepthNormalPrepass
  • 绘制不透明物体的DrawOpaqueObjects
每项绘制中都只有一个SRP Batch,也就是说两个方块在绘制时被合成了1个批次



图3

为了证明这一点,我们再向场景中插手第三个小方块,使用的是分歧着色器的第3种材质球,可以看到此时呈现了两组共6个SRP Batch:草箱子和铁箱子合批成一个SRP Batch,木箱子是一个SRP Batch,相应的SetPass calls数量增加到了11。所以提高衬着效率的第一步就是要在着色器的使用种类上要克制。



图4 分歧的着色器之间无法合批

2、关键字Keywords

在Unity引擎中允许通过关键字来使用着色器的变体【注1】:同一个着色器不异关键字组合可以SRP Batch合批;相对的,同一着色器分歧关键字组合之间则无法合批。
【注1】如何使用变体和关键字可以在官方文档种查询,不在本篇讨论范围:Unity - Manual: Declaring and using shader keywords in HLSL (unity3d.com)
在Frame Debug点选SRP Batch可以查看batch所带的具体的Keywords组合,以及该Batch具体绘制的内容



图5 SRP Batch查看关键字组合

在衬着效率优化的过程中,若前后衬着批次调用了同一个着色器,则该当尽量保证关键字不异的物体同时衬着。 需要注意的是,在目前的Unity版本Editor Inspector的BaseShaderGUI中没有keywords标签,但分歧的功能可能会开启关键字,比如:Workflow Mode选Specular、激活Emission发光、使用外描边Outline等等。



图6 配置影响关键词

为了具体知道材质球到底使用了哪些关键字,可以用文本编纂器以代码形式打开材质球,找到键m_ShaderKeywords查看其对应的值即为该材质球在衬着时使用到的关键字组合。在优化衬着的过程中,通过排查和修正mat文件中的关键字来测验考试合批【附1】
  1. Material:
  2.   serializedVersion: 6
  3.   m_ObjectHideFlags: 0
  4.   m_CorrespondingSourceObject: {fileID: 0}
  5.   m_PrefabInstance: {fileID: 0}
  6.   m_PrefabAsset: {fileID: 0}
  7.   m_Name: road_b_01.002 % 材质球资源名
  8.   m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} % 材质球文件id
  9.   m_ShaderKeywords:  % 材质球在调用着色器时使用的关键词组合
复制代码
【附1】在老版本的unity中,在操作Inspector激活/封锁关键词功能时,会在mat文件中造成增加了但不删除的问题,在目前的2020.x之后的版本已经修复了,开/关关键词配置在mat文件中已经不再会造成mat文件中残留不用的关键词问题。在网上搜索相关问题的时候还会查阅到老版本的该问题。
<hr/>3、衬着队列Render Queue

通过衬着批次可以控制材质球的衬着挨次,不异衬着队列值的物体会优先在同一时机进行衬着并测验考试SRP合批。
【注2】最新版的unity中对衬着队列进行了智能合批:相邻的衬着队列值若着色器和关键字一样,则进行合批,例如衬着队列2029与2030两个批次的shader和keyword一样,会自动进行SRP Batch。
分歧的衬着批次配置无法合批。这里最新版本的Unity URP管线中不再直接提供衬着队列Render Queue属性,通过查看URP/Lit.shader着色器代码可以看到,取而代之的是_QueueOffset属性,而且在Inspector中是无法直接配置该值的(HideInInspector)
  1.         // Editmode props
  2.         [HideInInspector] _QueueOffset(”Queue offset”, Float) = 0.0 //在inspector中不会直接配置
复制代码
可以通过查看dll反编译代码得知该属性在Inspector中对应的属性为queueOffsetProp,并通过类BaseShaderGUI的方式DrawQueueOffsetField绘制了一个滑动条在界面上,对应的属性名为Priority。
  1.         protected MaterialProperty queueOffsetProp { get; set; }
  2.         public virtual void DrawBaseProperties(Material material)
  3.         {
  4.             if (baseMapProp != null && baseColorProp != null)
  5.             {
  6.                 materialEditor.TexturePropertySingleLine(Styles.baseMap, baseMapProp, baseColorProp);
  7.                 if (material.HasProperty(”_MainTex”))
  8.                 {
  9.                     material.SetTexture(”_MainTex”, baseMapProp.textureValue);
  10.                     Vector4 textureScaleAndOffset = baseMapProp.textureScaleAndOffset;
  11.                     material.SetTextureScale(”_MainTex”, new Vector2(textureScaleAndOffset.x, textureScaleAndOffset.y));
  12.                     material.SetTextureOffset(”_MainTex”, new Vector2(textureScaleAndOffset.z, textureScaleAndOffset.w));
  13.                 }
  14.             }
  15.         }
复制代码
而这个改动最主要的原因是因为在unity的衬着队列中按照空间的从远到近预设了以下常量空间:
  1.     public enum RenderQueue
  2.     {
  3.         //
  4.         // Summary:
  5.         //     This render queue is rendered before any others.
  6.         Background = 1000,
  7.         //
  8.         // Summary:
  9.         //     Opaque geometry uses this queue.
  10.         Geometry = 2000,
  11.         //
  12.         // Summary:
  13.         //     Alpha tested geometry uses this queue.
  14.         AlphaTest = 2450,
  15.         //
  16.         // Summary:
  17.         //     Last render queue that is considered ”opaque”.
  18.         GeometryLast = 2500,
  19.         //
  20.         // Summary:
  21.         //     This render queue is rendered after Geometry and AlphaTest, in back-to-front
  22.         //     order.
  23.         Transparent = 3000,
  24.         //
  25.         // Summary:
  26.         //     This render queue is meant for overlay effects.
  27.         Overlay = 4000
  28.     }
复制代码
在老版本中直接配置RenderQueue值(不管是通过代码还是通过inspector界面配置)非常容易造成层级的混淆,例如一个不透明的物体(Geometry)值被配成了2600变成了透明衬着批次,造成不必要的问题。新版的Priority则预设了[-50,50]共101个层级,并在最终转换成衬着队列上的偏移量,避免了层级混淆。
  1.             if (surfaceType == SurfaceType.Opaque) {
  2.                 if (alphaClip) {
  3.                     material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.AlphaTest;
  4.                     material.SetOverrideTag(”RenderType”, ”TransparentCutout”);
  5.                 } else {
  6.                     material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Geometry;
  7.                     material.SetOverrideTag(”RenderType”, ”Opaque”);
  8.                 }
  9.                 // 先检测透明度层级,再累加在区间空间
  10.                 material.renderQueue +=
  11.                     material.HasProperty(”_QueueOffset”) ? (int)material.GetFloat(”_QueueOffset”) : 0;
复制代码
最终保留到材质球mat文件中对应的键值:
  1. m_CustomRenderQueue: 2000 % _QueueOffset偏移与其他设置计算后保留的最终值
  2.     - _QueueOffset: 0 % Inspector中slider滑动的配置值
复制代码
我们首先把场景中的暗影都封锁(Shadow Cast: off)来进行尝试:
尝试零:默认情况,所有Priorty都是0,按照从近到远、畴前到后原则,先衬着木箱子;之后衬着合批的铁箱子和草箱子;
尝试一:先把木箱子从场景中移除,并把铁箱子的材质球Priority设置为-10,我们可以看到此时的SRP Batch数量还是2,SetPass calls也保持7,进行了相邻合批;



图7 分歧queue智能合批

尝试二:在尝试一的基础上,我们再次把木箱子插手场景中,SRP Batch变为了6个(每口箱子有两个batch),SetPass calls变成了11,点击SRP Batch列表查看可知此时的衬着挨次为:铁箱子(Priority -10) > 木箱子 > 草箱子。



图8 木箱子打断了合批

尝试三: 在尝试二的基础上,我们再把草箱子的材质球priority也设置为-10,我们会发现铁箱子和草箱子又合批了,而且会优于木箱子衬着



图9 同样设置为-10的后排两个箱子优先衬着

以上就是衬着队列的感化。值得出格注意的点是,Unity Material Inspector中对Priority的说明原文为:“Determines the chronological rendering order for a Material. High values are rendered first.”但实际情况是值越小越先衬着。不知道是不是Unity官方的一个小bug。
<hr/>4、暗影合批Shadow Cast

URP管线一个很大的改良就是对于实时光照下的暗影效率的优化(具体优化另开坑再说),这让我们在有限的性能空间中可以使用更多的实时光影效果。而对于分歧的材质球或是分歧的关键字的物体来说,其暗影的绘制与其材质球绘制时的SRP是对应的:分歧材质球受到光照所发生的投影也应该是分歧的,所以不应该进行合批。我们在场景中把shadow cast打开,然后我们会发现绘制暗影和物体一样用了两个SRP Batch。



图10 MainLightShadow调用了两个SRP Batch

在实际项目开发过程中,在一个物体上常常需要用到多个分歧关键字或者是多种着色器的材质球,遇到这种情况这个问题的实时暗影就会占用多个SRP Batch,这就对场景的衬着造成了很大的开销。对于这种情况我们可以为该物体复制并添加一个专门负责生成暗影的子节点模型绑定在该物体上,并把该子节点的材质球统一为场景中的暗影材质球,同时设置shadow cast为shadow only,并把本体的暗影封锁(off),这也算是一个讨巧的法子。



图11 多种分歧的材质球绘制实时暗影会占用多个SRP

<hr/>5、动态与静态物体

很基础但是也经常掉进的坑:动态物体与静态物体无法合批,只能分袂合批。在制作场景中需要查抄动态、静态物体的配置。

总之,但愿这篇小记能成为大师开发游戏过程中的一些小助力,文中如有不合错误的处所,还请多多指出,谢谢

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-23 16:22 , Processed in 1.339748 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表