同时启用时,生效优先级:

SRPBatcher > GPUInstancing > 动态合批

SRPBatcher:

适用前提:

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

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

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

效果:

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

优化原理:

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

1.在过去的渲染架构中,Unity采取对一个材质分配一个CBuffer(or 一个Pass,这不是重点),这个CBuffer 包括shader里的显性的参数(你自己定义的uniform参数)和隐性的参数(unity固定的uniform modelMatrix,modelviewMatrix之类。),所以每一次drawcall,要更新这个CBuffer

2.在SRP渲染架构中,Unity采取的策略是对一个材质分配一个半CBuffer,为什么是一个半呢?首先shader的显性参数分配到一个CBuffer里,shader的隐性参数则是N个物体共享一个CBuffer。比如一个shader 对应 10个物体,在SRP渲染架构中,一共分配了11个CBuffer,其中10个分别存这10个物体材质中定义的显性参数。然后分配一个大的共享CBuffer,把这10个物体的modelMatrix这类隐性参数都放在一起。

乍一看这不是负优化吗?老架构更新10个CBuffer,你现在更新11个。

这个策略叫做动静分离,材质的显性参数大部分都是低频更新的(你总不能一个游戏所有的材质参数每桢都改变吧),所以在理想情况下,这10个放显性参数的CBuffer就基本不修改。而modelMatrix之类的隐性参数是高频更新的,很多模型会动来动去。他们被批量放在一个CBuffer里,一次更新可以更新一片。

如下图:大型的共享CBuffer和每个材质自己的CBuffer都有各自专门的代码进行更新,大部分情况只需要更新大型共享CBuffer,从而降低了一帧内SetPassCall的数目,和GUIInstancing对比,由于ConstantBuffer里不包含位置等信息(GUIInstancing是包括的),仅包括了显性属性,所以一次drawCall无法渲染所有物体,所以SRPbatcher的drawCall次数是没有降低的。而GUIInstancing需要相同的Mesh和材质球,条件更苛刻,但可以降低drawCall。

标准流程和SRPBatcher流程的区别:

以上,据Unity官方宣传 SRP Batcher 可以取得 1.2~4 倍的 CPU渲染时间提升(仅提升CPU部分,不是渲染耗时提升这么多,还得看cpu瓶颈占多大比重)

流程图:

如何让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)

GPUInstancing:

适用前提:

  • 兼容的平台及API

  • 相同的Mesh与Material

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

  • 不支持SkinnedMeshRenderer

  • Shader支持GPU Instancing

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

效果:

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

原理:

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

Unity会在运行时对于正在视野中的符合要求的所有对象,将其位置、缩放、uv偏移、lightmapindex等相关信息放到CBuffer(Constant Buffer)中,然后统一保存在显存中的“统一/常量缓冲器”中,当一个对象作为实例送入渲染流程时,在执行DrawCall操作后,根据传入的InstanceID从显存中取出当前实例对应的部分共享信息与从GPU常量缓冲器中取出对应对象的相关信息一并传递到下一渲染阶段,与此同时,不同的着色器阶段都可以从缓存区中直接获取到需要的常量,不用设置两次常量。总的来说就是一次性存入所有对象的公共信息到CBuffer,后续根据id来取,不用每次都发数据到GPU。

流程图:

如何使用GUPInstancing:

Shader "Custom RP/Unlit"
{Properties{_BaseColor("Color", Color) = (1.0, 1.0, 1.0, 1.0)}SubShader {Pass {HLSLPROGRAM// 让shader支持GUIInstancing // 一次对具有相同网格物体的多个对象发出一次绘图调用。// CPU收集所有每个对象的变换和材质属性,并将它们放入数组中,然后发送给GPU(SetPassCall)。// 最后,GPU遍历所有条目,并按提供顺序对其进行渲染。#pragma multi_compile_instancing#pragma vertex UnlitPassVertex#pragma fragment UnlitPassFragment#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_INPUT_INSTANCE_ID                                               // 启用GUIInstancing的时候,用此宏,可以让顶点传入实例化id
};// 顶点着色器输出
struct Varyings {float4 positionCS : SV_POSITION;UNITY_VERTEX_INPUT_INSTANCE_ID                                             // 启用GUIInstancing的时候,用此宏,让顶点着色器输出实例化id
};Varyings UnlitPassVertex(Attributes input){Varyings output;UNITY_SETUP_INSTANCE_ID(input);                                                // 从input中提取对象索引,并将其存储在其他GUIInstancing相关宏所依赖的全局静态变量中UNITY_TRANSFER_INSTANCE_ID(input, output);                                   // 把input中的实例化id转换到片元着色器中用的实例化idfloat3 positionWS = TransformObjectToWorld(input.positionOS);output.positionCS = TransformWorldToHClip(positionWS);return output;
}float4 UnlitPassFragment(Varyings input) : SV_TARGET{UNITY_SETUP_INSTANCE_ID(input);                                               // 从input中提取对象索引,并将其存储在其他GUIInstancing相关宏所依赖的全局静态变量中return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);            // 根据实例id从_BaseColor数组中取出对应的_BaseColor
}

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

例子:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;/// <summary>
/// 动态生成大量球体mesh,用来测试GPUInstancing
/// </summary>
public class MeshBall : MonoBehaviour
{static int baseColorId = Shader.PropertyToID("_BaseColor");[SerializeField]Mesh mesh = default;         // 手动拖入mesh[SerializeField]Material material = default;  // 手动拖入支持GPUInstancing的材质球Matrix4x4[] matrices = new Matrix4x4[1023];Vector4[] baseColors = new Vector4[1023];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, 1023, block);}
}

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

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

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

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

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

静态批处理

静态合批最关重要的一件事就是合并网格。

游戏引擎会把渲染器中的网格信息取出来,之后对网格上的顶点进行空间变换,变换到世界空间下,合并成一个新的大网格。即会形成一个新的大Vertex Buffer和Index Buffer(这两个缓冲区一旦确定了就不会再被修改了),最终会在一个批次中提交这些顶点信息。(如果之后需要对合并的网格进行空间变换,由于已经在同一坐标系了,所以直接对合并的节点进行矩阵变换即可。)

在Unity中,直接勾选物体的static(自动静态合批)或者通过代码调用合并函数(手动静态合批),均可以实现网格合并。

Unity会把合并网格的相关信息存在场景文件(.scene)下,所以对于那些存在静态物体的场景文件一般来说会大很多。

因此静态合批实际上是属于空间换时间的一种策略。并且不仅仅是文件增大,在运行时,静态合批的网格会常驻内存,如果存在大量的静态合批网格,那么内存压力会十分可怕。

还有一点需要注意,不同引擎、平台对于最终合并网格生成的大网格顶点数和索引数是有限制的。一旦超过了限制,就会合并成另一个新的大网格。

静态合批虽然通过合并网格生成了新的大Vertex Buffer和Index Buffer,从而可以在一个批次中提交顶点信息,但是这并不意味着这一个批次中只有一个DC。也就是说,虽然静态合批可以有效地减少批次,但是可能无法减少DC。

举个例子,当一个合并过的网格中某一部分发生剔除或是被隐藏时,其对应Vertex Buffer和Index Buffer并不会被修改。引擎会选择将整个大网格拆分成若干个小部分来进行分次渲染,每次小渲染都是一个DC,通过调整每个DC来跳过不显示的内容。

即使是一个批次中存在多个DC,但由于这些DC之间没有渲染状态的切换,所以渲染效率还是很高的。

(上图出自「Don里个冬」的原创文章https://blog.csdn.net/weixin_42186870/article/details/117675164)

Static batching也会带来一些性能的负面影响。Static batching会导致应用打包之后体积增大,应用运行时所占用的内存体积也会增大。

另外,在很多不同的GameObject引用同一模型的情况下,如果不开启Static batching,GameObject共享的模型会在应用程序包内或者内存中只存在一份,绘制的时候提交模型顶点信息,然后设置每一个GameObjec的材质信息,分别调用渲染API绘制。开启Static batching,在Unity执行Build的时候或手动调用代码合批的时候,场景中所有引用相同模型的GameObject都必须将模型顶点信息复制,并经过计算变化到最终在世界空间中,存储在最终生成的Vertex buffer中。这就导致了打包的体积或运行时内存的占用增大。例如,在茂密的森林级别将树标记为静态会严重影响内存[3]。可以在unity的Profiler窗口中的Memory->Detailed->SceneMemory->Mesh查看静态合批占的内存

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


动态批处理

  • 定义

在使用相同材质球的情况下,Unity会在运行时对于正在视野中的符合条件的动态对象在一个Draw call内绘制,所以会降低Draw Calls的数量。

Dynamic batching的原理也很简单,在进行场景绘制之前将所有的共享同一材质的模型的顶点信息变换到世界空间中,然后通过一次Draw call绘制多个模型,达到合批的目的。模型顶点变换的操作是由CPU完成的,所以这会带来一些CPU的性能消耗。并且计算的模型顶点数量不宜太多,否则CPU串行计算耗费的时间太长会造成场景渲染卡顿,所以Dynamic batching只能处理一些小模型。

Dynamic batching在降低Draw call的同时会导致额外的CPU性能消耗,所以仅仅在合批操作的性能消耗小于不合批,Dynamic batching才会有意义。而新一代图形API( Metal、Vulkan)在批次间的消耗降低了很多,所以在这种情况下使用Dynamic batching很可能不能获得性能提升。Dynamic batching相对于Static batching不需要预先复制模型顶点,所以在内存占用和发布的程序体积方面要优于Static batching。但是Dynamic batching会带来一些运行时CPU性能消耗,Static batching在这一点要比Dynamic batching更加高效。

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

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

  • 流程原理

参考资料:关于静态批处理/动态批处理/GPU Instancing /SRP Batcher的详细剖析 - 知乎

URP下SRPBatcher,GPUInstancing,动态合批,静态合批相关推荐

  1. 批量 材质 调整_游戏图形批量渲染及优化:Unity静态合批技术

    作者:枸杞忧天 (本文首发于公众号"偶尔学学Unity",文章仅为作者观点,不代表GWB立场) 最近在准备公司的技术分享,主题是入门批量渲染,想着反正也总结了,不如充几篇博客吧,也 ...

  2. Unity中的静态合批、动态合批、GPU Instance 以及SRP Batching

    文章目录 Unity中的静态合批.动态合批.GPU Instance 以及SRP Batching 四种合批简介 GPU instancing static Batching Dynamic batc ...

  3. 动态合批和静态合批的区别

    参考文章: 图形渲染及优化-Unity合批技术实践 图形渲染及优化-Batch 批是啥? 对某对象进行批量处理叫批处理 Batch是啥? 引擎每帧提交的Batch数量就是衡量渲染压力的指标 Batch ...

  4. unity3d 动态合批设置_Unity动态合批(Dynamic Batching)与静态合批(Static Batching)

    动态合批与静态合批其本质是对将多次绘制请求,在允许的条件下进行合并处理,减少cpu对gpu绘制请求的次数,达到提高性能的目的. 1.静态合批是将静态(不移动)GameObjects组合成大网格,然后进 ...

  5. Unity的URP下使用SRPBatcher

    回到目录 大家好,我是阿赵.这里继续来讲一下URP相关的东西. 这次主要说的是SRP Batcher的使用 一.在URP下实现SRP Batcher 1.设置 在我们创建的URPAsset文件的高级选 ...

  6. g++ 编译mysql动态库_Linux下g++编译以及使用静态库和动态库的方法详解

    下面小编就为大家带来一篇Linux下g++编译与使用静态库和动态库的方法.小编觉得挺不错的,现在就分享给大家,也给大家做个参考.一起跟随小编过来看看吧 在windows环境下,我们通常在IDE如VS的 ...

  7. testmeshpro合批_Unity合批原理及失败的原因

    1.1什么是BatchRendering 问题: 渲染时性能的瓶颈主要在提交渲染数据(Mesh),每次提交Mesh数据时还需要对描画State的状态进行一次设置,设置每次都会消耗大量的CPU时间. 对 ...

  8. 为什么php动态语言,动态语言静态化

    [TOC] ## 什么是动态语言静态化 将现有PHP等动态语言的逻辑代码生成为静态HTML文件,用户访问动态脚本重定向到静态HTML文件的过程. 对实时性要求不高的页面 ## 为什么要静态化 动态脚本 ...

  9. Qt动态库静态库的创建、使用、多级库依赖、动态库改成静态库等详细说明

    本文描述的是windows系统下,通过qtcreator在pro文件中添加动态库与静态库的方法: 1.添加动态库(直接添加动态库文件.dll,非子项目) 通过qtcreator创建动态库的方法就不在此 ...

最新文章

  1. ActivityRouter 框架简单实用
  2. 对比激光SLAM与视觉SLAM:谁会成为未来主流趋势?
  3. 高效JQuery编码
  4. runtime无法执行grep_让你的 Shell 命令执行可视化和告警
  5. 如何做网络营销推广之网站SEO中title标签优化技巧!
  6. [转]Android输入法框的梳理
  7. 1周第1课 Linux 认知、安装 Centos7
  8. jdbc连接oracle的几种格式
  9. 关于质量标准化的思考和实践
  10. Entity Framework 4 in Action读书笔记——第四章:使用LINQ to Entities查询:预先加载和延迟加载...
  11. 计算机网络第四章课后答案(第七版谢希仁著)
  12. [基础知识点]马尔可夫随机场MRF与 条件随机场CRF
  13. matlab里mod函数什么意思,mod函数_excel中mod函数的使用方法
  14. 用excel和window系统自带功能给文件批量改名(超详细小白教程!)
  15. 监控电脑屏幕python
  16. 38、EST序列拼接流程
  17. mysql(.msi)下载、安装及配置教程
  18. 前端面试题必考(四)- HTTP短连接,长连接(keep-alive),websocket,postmessage
  19. 最小二乘优化整理(信赖域方法)
  20. 浪潮英信NF5280M4服务器蓝屏

热门文章

  1. Jquery日期选择组件
  2. python神经网络:女生颜值打分器(一)
  3. oppo 上架游戏流程
  4. mtcnn人脸检测python_基于mtcnn和facenet的实时人脸检测与识别系统开发
  5. scrapy_splash简单爬取淘宝页面信息
  6. 丰润达S400来了!5.8G千兆无线网桥“横空出世”
  7. 计算机体系结构.微结构概述
  8. 【编辑中】python 常用模块
  9. 学习任何东西的 10 个心智模型
  10. 专网内网页爬取征信报告