参考:https://catlikecoding.com/unity/tutorials/custom-srp/draw-calls/ 之所以不算翻译了是因为 觉得原文文章介绍的太啰嗦了不打算一字一语翻译下来,只提炼中间比较重要的内容。
Draw Calls 着色器批处理
编写HLSL 着色器
支持SRP 合批,GPU实例化和动态批处理
配置每个物体的材质属性,支持随机性绘制
创建透明和cutout 材质
这是自定义管线的第二课,这节课主要编写着色器和高效的绘制物体。
unity版本是nity 2019.2.9f1
绘制了很多球但是只有很少的drawcall
Shader - 着色器
cpu 告诉gpu绘制东西。通常绘制的是网格,怎么绘制是由 shader决定的,传递一些指令给gpu即可。除了网格之外, shader还需要其他信息来完成其工作,包括对象的变换矩阵和材质属性。
unity的 LW/Universal 和 HDRPs (低级管线,自定义管线,和高级管线)可以让你通过 Shader Graph package来设计shader,然后自动帮你生成shader 代码。但是为了完全掌握和理解shader,将会自己编写shader实现它。
-
Unlit shader - 不含光照的着色器
第一个shader 是简单的绘制一个带有固定颜色的面片,它不会包含任何光照效果。为了能够重新开始,删除所有已经创建好的shader,并在CustomRP 文件夹下 创建一个叫做shaders 文件夹,然后创建一个叫做Unlit的shader。
shader 的代码大部分看起来像c#代码(我觉得应该更像是C语言才对),但是会包含一些很古老陈旧的风格的代码。shader 的定义看起来像是一个类,里面会包含一个字符串类型的关键字,可以用来作为在材质中的下拉列表框中去选择的入口。后面一系列包含了关键字的代码块。 Properties
代码块定义材质属性, SubShader
块包含有定义渲染方式的Pass
块。
Shader "Custom RP/Unlit" {
Properties {}
SubShader {
Pass {}
}
}
在材质中这样来使用它。
默认的材质颜色是渲染成白色,材质会自动默认设置一个渲染队列为2000,这个也是不透明物体的默认渲染队列值。还有一个toogle是否开启double-sided global illumination(双面全局光照)先这么翻译,现在先不用管它。
-
HLSL 着色器
hlsl着色器是一种高级的着色器语言,放到pass块当中,并且需要放到在HLSLPROGRAM
and ENDHLSL
中,之所以这样的原因是因为pass块也可以写其他类型的着色器代码。
注:unity中也支持编写CG语言,但是在unity自定义管线中,只会使用hlsl。(注:GLSL 不知道支不支持,以后再验证吧)
GPU会通过光栅化所有的三角形,然后进行像素着色。着色器的两大阶段:顶点着色器和像素着色器,就不多说了。定义一个顶点着色器和像素着色器需要遵循这样的规则:以#pragma 开头,后面跟着 vetex 或者 fragment 然后是声明的顶点着色器
和像素着色器的名称。这里声明为 UnlitPassVertex
and UnlitPassFragment
HLSLPROGRAM #pragma vertex UnlitPassVertex #pragma fragment UnlitPassFragment ENDHLSL
这个时候编译器会提示我们找不到对应的着色器的名称。那是因为现在只声明了还没有定义UnlitPassVertex
and UnlitPassFragment。
(这里我借用了c与c++的规则)。这里把他们的定义放到另外一个文件当中。并使用
#include
来把它包含进来(跟使用c与c++包含文件一个样子)。
HLSLPROGRAM
#pragma vertex UnlitPassVertex
#pragma fragment UnlitPassFragment
#include "UnlitPass.hlsl"
ENDHLSL
由于unity 没有提供快速创建hlsl文件的快捷方式,可以通过复制已经的shader,然后命名为 UnitPass,并修改文件的扩展名为.hlsl,然后清空文件所有内容。
-
防止着色器代码包含多次
hlsl 着色器代码么有类的概念,除了再代码块中包含以外,它也是可以全局访问的。她会在编译的时候把文件所有内容都插入#include包含的地方,因此为了防止被包含多次,我们会阻止这样的情况发生(注:这跟c语言一毛一样)。也就是通过 在文件的头部使用#define CUSTOM_UNLIT_PASS_INCLUDED。通过下面的宏定义:
#ifndef CUSTOM_UNLIT_PASS_INCLUDED #define CUSTOM_UNLIT_PASS_INCLUDED #endif
这样就能确保文件代码永远不会包含多次了,即使#include 多次。
-
着色器函数
- 下面就该添加还未定义的两个着色器函数了,这里先简单定义一下.
#ifndef CUSTOM_UNLIT_PASS_INCLUDED #define CUSTOM_UNLIT_PASS_INCLUDED void UnlitPassVertex () {} void UnlitPassFragment () {} #endif
现在可以编译通过了,会渲染出一个青的球。
可以通过修改像素着色器函数的返回值来修改显示的颜色,颜色值是一个思维的向量对应这红 绿 蓝 alpha 四个组件,float4(0.0, 0.0, 0.0, 0.0) 是黑色,也可以直接写一个 0或者0.0(都一样,0 最后也会变成0.0),它会自动的设置其他三个向量的值,现在使用不透明渲染,alpha值设置为0 也是可以的。
float4 UnlitPassFragment () { return 0.0; }
UnlitpassVetex 负责进行顶点坐标的转换,最后会返回一个位置值. 其实着色器也有一个float4 的向量,只不过它定义在齐次裁剪空间,后面在介绍它。现在我将会仍然让它返回一个0向量值,并且声明它是一个SV_POSITION类型。
float4 UnlitPassVertex () : SV_POSITION { return 0.0; }
-
空间变换
我们都知道顶点着色器中主要进行的是从本地坐标位置到齐次裁剪空间(NOTI: 物体顶点着色器器最终输入的结果齐次裁剪空间,裁剪后的范围是:物体的x y坐标 都介于 -w到w 之间,在opengl 中 z 的坐标 是-w到w ,dx是 0到w),而上面的顶点坐标变换最后输出的都是0,显然是不对的,
正确的写法应该是 需要输入一个三维的向量,代表物体的自身坐标位置,还有矩阵, 然后这个位置经过矩阵变换,最终输出一个变换后的结果。物体的空间位置unity已经帮我们弄好了,我们只是需要设置一个三维的或者四维的形参即可,并且为了防止参数中可能包含多个表示位置的值,需要加上POSITION 这个语义。而变换用的矩阵是可以被共同使用的,为了保证代码结构化,将会把它放到一个单独的文件中,添加一个UnityInput.hlsl的文件,然后放到 ShaderLibrary 文件夹下,如下:
设置CUSTOM_UNITY_INPUT_INCLUDED 进行条件编译判防止被包含多次,定义一个全局的 float4x4 矩阵 unity_ObjectToWorld
GPU每次绘制的时候只设置一次,它可以在顶点和像素着手器中传递使用并且值始终是固定的。
#ifndef CUSTOM_UNITY_INPUT_INCLUDED #define CUSTOM_UNITY_INPUT_INCLUDED float4x4 unity_ObjectToWorld; #endif
接下来,在 Common.hlsl 文件中把它包含进来,然后定义 TransformObjectToWorld
来具体的使用它。
#ifndef CUSTOM_COMMON_INCLUDED #define CUSTOM_COMMON_INCLUDED #include "UnityInput.hlsl" float3 TransformObjectToWorld (float3 positionOS) { return mul(unity_ObjectToWorld, float4(positionOS, 1.0)).xyz; } #endif
现在可以在自定义的顶点着色器: UnlitPassVertex,通过include
../ShaderLibrary/Common.hlsl 调用TransformObjectToWorld,把物顶点从物体空间转换到世界空间。
#include "../ShaderLibrary/Common.hlsl" float4 UnlitPassVertex (float3 positionOS : POSITION) : SV_POSITION { float3 positionWS = TransformObjectToWorld(positionOS.xyz); return float4(positionWS, 1.0); }
但是我们最终输出的应该是齐次裁剪空间,因此还需要引入一个矩阵unity_MatrixVP。同添加 unity_ObjectToWorld
一样需要在Common.hlsl 中添加,然后再添加一个从世界到齐次裁剪空间转换的方法。
float3 TransformObjectToWorld (float3 positionOS) { return mul(unity_ObjectToWorld, float4(positionOS, 1.0)).xyz; } float4 TransformWorldToHClip (float3 positionWS) { return mul(unity_MatrixVP, float4(positionWS, 1.0)); }
最后UnlitPassVertex
中返回齐次裁剪空间的结果:
float4 UnlitPassVertex (float3 positionOS : POSITION) : SV_POSITION { float3 positionWS = TransformObjectToWorld(positionOS.xyz); return TransformWorldToHClip(positionWS); }
-
Core Library
- 上面定义的两个方法其实在Core RP Pipeline package.里面已经有了,并且这个核心库里面还包含了很多实用且有用的东西。安装完这个package后 移除我们自定义的文件,并用 这个路径下的文件代替:Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl.
-
//float3 TransformObjectToWorld (float3 positionOS) { // return mul(unity_ObjectToWorld, float4(positionOS, 1.0)).xyz; //} //float4 TransformWorldToHClip (float3 positionWS) { // return mul(unity_MatrixVP, float4(positionWS, 1.0)); //} #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"
这个时候编译会报错因为 SpaceTransforms.hlsl 文件里面没有定义 unity_ObjectToWorld,而是把它看做成了
UNITY_MATRIX_M
,因此需要添加一个宏定义:#define UNITY_MATRIX_M unity_ObjectToWorld 来识别它。
#define UNITY_MATRIX_M unity_ObjectToWorld
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"
同样的 SpaceTransforms.hlsl 也需要定义几个其他的矩阵:比如 unity_WorldToObject(从世界到物体坐标变换矩阵)被定义为 UNITY_MATRIX_I_M,unity_MatrixV 被定义为 UNITY_MATRIX_V,unity_MatrixV 被定义为UNITY_MATRIX_VP,投影矩阵glstate_matrix_projection被定义为UNITY_MATRIX_P。
#define UNITY_MATRIX_M unity_ObjectToWorld
#define UNITY_MATRIX_I_M unity_WorldToObject
#define UNITY_MATRIX_V unity_MatrixV
#define UNITY_MATRIX_VP unity_MatrixVP
#define UNITY_MATRIX_P glstate_matrix_projection
UnityInput.hlsl 文件里面也得把这几个额外的矩阵包含进来,还有一个特殊的东西- unity_WorldTransformParams,目前我们还用不到。 它其实是一个向量real4,它本身并不是一个有效的类型它只是一个别名,它会根据不同的类型去转换为 float4 或者 half4 类型。
float4x4 unity_ObjectToWorld;
float4x4 unity_WorldToObject;
float4x4 unity_MatrixVP;
float4x4 unity_MatrixV;
float4x4 glstate_matrix_projection;
real4 unity_WorldTransformParams;
每个图形AP都定义了该别名和许多其他基本宏,们可以通过在Packages / com.unity.render-pipelines.core / ShaderLibrary / Common.hlsl来获得所有信息。在包含UnityInput.hlsl之前,包含Common.hlsl(NOTI:文件包含有先后顺序的)。如果您对文件的内容感到好奇,可以到这些文件中去查看里面内容。
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "UnityInput.hlsl"
-
颜色
-
为了可以在相同的材质中可以设置不同的颜色,因此需要定义一个uniform 颜色属性 - _BaseColor,它前面的下划线表示它是一个颜色属性,因此需要在properties中定义它,然后让像素着色器返回这个颜色只即可。
在 UnlitPass.hlsl 文件中修改:
#include "../ShaderLibrary/Common.hlsl"
float4 _BaseColor;
float4 UnlitPassVertex (float3 positionOS : POSITION) : SV_POSITION {
float3 positionWS = TransformObjectToWorld(positionOS);
return TransformWorldToHClip(positionWS);
}
float4 UnlitPassFragment () : SV_TARGET {
return _BaseColor;
}
在Unlit.hlsl 中修改属性定义:
Properties
{
_BaseColor("Color", Color)
}
现在可以在材质中修改设置不同的颜色的值了。
批处理
每一个drawcall 都是需要CPU 与GPU 交流。影响帧率的原因有两个:
1 一个 是很多数据需要发送到GPU,GPU不得不等待。
2 CPU 如果忙于发送数据的化,它就不能做其他的事情。
目前来说:每一个物体都有自己的drawcall,这其实是很糟糕的。但是由于目前我们发送的数据很少,所以还看不出来。比如:如果场景中有76个球 ,76个球中每一个球使用红,绿,黄和蓝色中的一种材质,那么就会产生 78个drawcall,76个是球的,一个是天空盒,一个是清除rendertarget(应该是清除屏幕) 用的。
打开 unity 的state 面板可以看到实际显示了77个,另外一个清除屏幕的unity默认隐藏 了,0个 batchiing。
-
SPR Batcher
Batching 就是合并drawc calls 的过程,减少 GPU 与 CPU 交流的时间。最容易出来的方法是启用 SPR batcher,然而这仅适用于兼容的着色器,不适用于 Unlit shader,这可以通过在Inspector 面板中查看不能使用的原因。(开启 SPR batcher 方法在下面:)
SPR batch 采用的方法不是减少 draw calls 的数量,而是采用了更精简的办法。它把材质的属性先缓存到GPU中,因此这样每个Drawcall就不用发送同样的数据了。这大大减少了GPU 与 CPU 的 交流中使用的数据。但是 使用这种方式的话会有严格的要求条件:数据是Uniform 类型的结构体。
所有材料属性都必须在具体的存储缓冲区内定义,而不是在全局级别上定义。比如这个材质属性是 _BaseColor, 可以把它包裹进一个用cbuffer 块修饰的 UnityPerMaterial的结构体当中。
它通过将_BaseColor放入特定的常量内存缓冲区中来隔离它,尽管它仍可在全局级别访问。
cbuffer UnityPerMaterial { float _BaseColor; };
但是 由于 常量缓冲区 并不会被 所有平台支持(注:opengl 中没有常量缓存区,与它最接近的是 Uniform buffer),不过么有关系,Unity Core RP Library 提供了 一些宏可以代替使用它,也就是 CBUFFER_START
and CBUFFER_END,这个宏就相当于一个方法,还需要传递一个命名的缓冲区的名称作为参数
。
CBUFFER_START(UnityPerMaterial) float4 _BaseColor; CBUFFER_END
另外 还得把 unity_ObjectToWorld
, unity_WorldToObject
, and unity_WorldTransformParams 放到 buffer 中,还需要包含一个变量名
float4 unity_LODFade 目前还不使用它,这些变量的顺序是无关紧要的,但是这次需要定义一个另外的缓冲区名称参数。
CBUFFER_START(UnityPerDraw) float4x4 unity_ObjectToWorld; float4x4 unity_WorldToObject; float4 unity_LODFade real4 unity_WorldTransformParams; CBUFFER_END
下一步就是开启SRP batcher了,因为仅需要设置一次就可以因此可以 在 CustomRenderPipeline 中添加构造函数并把 GraphicsSettings.usescriptableRenderPipelineBatching 设置为 true即可。
public CustomRenderPipeline () { GraphicsSettings.useScriptableRenderPipelineBatching = true; }
现在运行shader可以在static 面板中看到76个 batches 被省下了,unity中会显示为-76,现在打开frame debugger ,会看到 仅有一个SPR 项并且显示在 RenderLoopNewBatcher.Draw 下面,但是请记住 这不代表仅有一个drawcall ,仅是代表是很多drawcalls 被优化后的结果。
-
设置多种颜色:
目前实现了即使使用四种材质但是只有一个draw。这是因为所有的数据都缓存在GPU 上,每一个draw 仅仅是包含一个正确的偏移位置。唯一的限制是是每一个i材质的内存的的结构必须相同,这也就是为什么我们可以给他们使用同一个shader的原因。Untiy不会正正的比较材质的内存结构,它只是仅批处理使用完全相同的着色器变体(什么是着色器变体?回头解释)。
如果想使用几个不同的颜色这个条件满足了,但是如果想给每一个球它自己单独的颜色,我们不得不创建多个材质。如果我们可以设置每个对象的颜色,那会更方便。可以通过创建一个自定义的组件类型来实现它。这个组件就叫 PerObjectMaterialProperties。
这背后的思想就是每个一游戏物体都可以有一个
PerObjectMaterialProperties的组件,可以用它来设置 _BaseColor 材质属性。它需要知道shader 属性的标识符(
identifier of the shader property),这可以通过Shader.PropertyToID检索,然后存储为静态的属性。
就像之前在CameraRenderer中为着色器传递一个int类型的标识符一样。
using UnityEngine; [DisallowMultipleComponent] public class PerObjectMaterialProperties : MonoBehaviour { static int baseColorId = Shader.PropertyToID("_BaseColor"); [SerializeField] Color baseColor = Color.white; }
设置每一个物体的次啊之属性是通过 MaterialPropertyBlock来实现。我们仅仅需要重复使用
l PerObjectMaterialProperties实例就即可。因此需要定义一个静态的 S
tatic MaterialPropertyBlock block;字段就可以,然后调用它的setcolor方法把标志符传递进去,然后通过
SetPropertyBlock来把它应用到渲染组件中,为了能够在编辑器中立即看到效果,把这些实现
放到OnValidate
中(onValid 会在编辑器中inspector面板中修改数据或者脚本加载的时候调用,它可以用来确保当你在编辑器中修改数据的时候,这些数据会被限制在一定范围内)。
因此修改数据可以在编辑器中立即看到编辑的效果。
void OnValidate () { if (block == null) { block = new MaterialPropertyBlock(); } block.SetColor(baseColorId, baseColor); GetComponent<Renderer>().SetPropertyBlock(block); }
现在在场景中添加24个不同颜色的材质球
不幸的是,目前SPR 的批处理还不能应对每一个材质的属性,因此目前每一个球还会有自己的drawcall。由于排序,也有可能将其他区域的球分成多个批次。
24 non-batched draw calls.
并且由于OnValidate在build的过程中不会被调用,因此为了让不同的颜色都出现我们需要在awake中来调用它。
void Awake () { OnValidate(); }
GPU Instancing
GPU Instancing 也 可以处理每一个物体材质属性并且减少drawcall的方法,它可以让有相同mesh的很多个物体仅产生一个drawcall 。CPU 收集每一个物体的变换和材质属性,以数组的形式传递给GPU,GPU 然后按照发送的顺序依次渲染出来。
因为GP实例化需要提供数组的形式的数据,但是目前 shader还不能支持它。因此首先第一步是需要在shader的pass中顶点和像素程序上方添加#pragma multi_compile_instancing指令
#pragma multi_compile_instancing #pragma vertex UnlitPassVertex #pragma fragment UnlitPassFragment
这会让shader产生两个变体(variants),一个是没有 GPU instancing一个是有。而且在材质面板里面也会出现一个toggle 让选择使用他们中的哪一种。
支持GPU Instancing 需要包含shader库中的UnityInstancing.hlsl ,需要在定义完UNITY_MATRIX_M和其他的宏之后,并且在SpaceTransforms.hlsl.之前
#define UNITY_MATRIX_P glstate_matrix_projection #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl" #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"
UnityInstancing.hlsl UnityInstancing.hlsl所做的就是重新定义这些宏来访问实例化的数据数组,但是需要指定结构体来支持。
可以定义一个结构体类似于cbuffer,然后把它当作函数的参数。为了表达的比较清晰定义一个结构体,使用POSITION 语义作为变量,然后传递到顶点着色器中。
struct Attributes { float3 positionOS : POSITION; }; float4 UnlitPassVertex (Attributes input) : SV_POSITION { float3 positionWS = TransformObjectToWorld(input.positionOS); return TransformWorldToHClip(positionWS); }
另外 我们还需要知道当前渲染物体的索引值,来实现渲染功能,这可以通过UNITY_VERTEX_INPUT_INSTANCE_ID来实现,可以放到Attribute结构体中。
struct Attributes { float3 positionOS : POSITION; UNITY_VERTEX_INPUT_INSTANCE_ID };
然后在UnlitPassVertex方法里面添加 UNITY_SETUP_INSTANCE_ID(input)来提取出实例化的索引,这会把它保存为全局静态的变量,然后可以供其他的程序使用。另外还得需要做如下替换
//CBUFFER_START(UnityPerMaterial)
//float4 _BaseColor;
//CBUFFER_ENDUNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor) //这个宏
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
为了能让实例化的索引值能够传递到像素着色器当中,还得需要定义一个结构体,用 UNITY_TRANSFER_INSTANCE_ID(input, output);来复制实例化的索引值,这个结构体叫Varyings,因为它包含的数据在相同三角形的片段之间可能会有所不同(?)
struct Varyings {
float4 positionCS : SV_POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
Varyings UnlitPassVertex (Attributes input) { //: SV_POSITION {
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_TRANSFER_INSTANCE_ID(input, output);
float3 positionWS = TransformObjectToWorld(input.positionOS);
output.positionCS = TransformWorldToHClip(positionWS);
return output;
}
同时需要传递一个同样的结构体参数给 UnlitPassFragment
. ,同时需要使用 UNITY_SETUP_INSTANCE_ID
来获取索引值。同时必须使用UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor)
.来获取材质属性。
float4 UnlitPassFragment (Varyings input) : SV_TARGET {
UNITY_SETUP_INSTANCE_ID(input);
return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
}
现在可以看到已经能把24个使用不同颜色的材质球drawcall 合并成了4个,这是因为目前使用了四个不同材质的原因,为了能减少到一个drawcall,你只需要使用一个材质,然后设置不同的颜色就可以了。当只使用一个材质时候,最终结果会是这样。
总结:使用实例化渲染对批处理的大小数据是有限制的,这个会根据不同的平台会限制具体的大小。如果操作超出这个限制,最终得到到批次会不止一个。还有如果使用多种不同的材质的化,可以拆分出多个批次来处理。
实例化多个网格
既然GPU 实例化可以让上百个物体合并成一个drawcall 。那么下面就搞大点看看结果怎样。
创建一个类叫MeshBall 通过它来生成很多物体,保存下材质属性_BaseColor,添加两个可配置的支持实例化的属性Mesh和material。
using UnityEngine;
public class MeshBall : MonoBehaviour {
static int baseColorId = Shader.PropertyToID("_BaseColor");
[SerializeField]
Mesh mesh = default;
[SerializeField]
Material material = default;
}
给两个属性赋上一个球和实例化的材质。
下面用GPU 实例化产生 1023个位置随机分布在半径为10单位的范围内的颜色随机的(大小也可以随机)的球。定义1023个矩阵和颜色数组,然后定义一个MaterialPropertyBlock 来传递颜色数据。具体的执行放在Awake方法中,保证了脚本被初始化的时候就开始执行。
Matrix4x4[] matrices = new Matrix4x4[1023];
Vector4[] baseColors = new Vector4[1023];
MaterialPropertyBlock block;
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);
}
}
在Update方法中动态的实例化MaterialPropertyBlock,然后通过调用 Graphics.Graphics.DrawMeshInstanced来传递 mesh ,sub-mesh(因为这里没有使用多个材质所以把index设置为0),材质和实例化的物体的数量 ,属性块参数。这里设置了颜色的属性块参数以便使颜色生效。
void Update () {
if (block == null) {
block = new MaterialPropertyBlock();
block.SetVectorArray(baseColorId, baseColors);
}
Graphics.DrawMeshInstanced(mesh, 0, material, matrices, 1023, block);
}
现在运行游戏可以看到上面的效果,但是生成的drawcall数量取决于运行的平台,这是因为么一个drawcall 在不同的平台上占用的缓冲区0大小不一样,这里生成了3个drawcall。
值得注意的是 GPU 实例化 渲染出的网格的顺序跟提供的渲染的数据的顺序是一致的,这里不会进行排序(应该就是不透明的渲染的顺序从前到后),并且不会任何裁剪。
动态批处理
动态批处理是第三种减少Drawcall的方法。这是一种很老的技术了,它会把使用相同材质的多个小的面片合并成一个大的面片来渲染。动态批处理也不能应用在per-ojbect(每一个物体一个材质)上。由于生成的最大的面片是有要求限制的,因此适合于需要处理很多小的面片渲染的问题。为了看到它工作的效果,在 CameraRenderer.DrawVisibleGeometry
.里面 设置enableDynamicBatching
to true
,并且禁用GPU 实例化。
var drawingSettings = new DrawingSettings(
unlitShaderTagId, sortingSettings
) {
enableDynamicBatching = true,
enableInstancing = false
};
同时把 SPR batcher 也禁用掉
GraphicsSettings.useScriptableRenderPipelineBatching = false
通常来说 GPU实例化比动态批处理要好,但是有几点也需要注意:绘制不同的大小的物体时候,注意法线不能保证是单位化的,不同的缩放值会对法线有影响。还要注意绘制的顺序。
也有一种减少drawcall的方法 静态批处理,工作原理于动态批处理相似,但是属于只能提前处理静态物体的。同时还需要使用大量的内存来存储信息。但是 这个跟自定义管线无关,因此现在不需要理会它。
可配置批处理
首先添加bool变量来控制是否动态合批还是使用GPU 实例化。并当作 DrawVisibleGeometry的参数来控制。在
CustomRenderPipeline里面跟踪设置,render 里面来处理。
void DrawVisibleGeometry (bool useDynamicBatching, bool useGPUInstancing) {
var sortingSettings = new SortingSettings(camera) {
criteria = SortingCriteria.CommonOpaque
};
var drawingSettings = new DrawingSettings(
unlitShaderTagId, sortingSettings
) {
enableuseDynamicBatching = useDynamicBatching,
enableInstancing = useGPUInstancing
};
…
}
bool useDynamicBatching, useGPUInstancing;
public CustomRenderPipeline (
bool useDynamicBatching, bool useGPUInstancing, bool useSRPBatcher
) {
this.useDynamicBatching = useDynamicBatching;
this.useGPUInstancing = useGPUInstancing;
GraphicsSettings.useScriptableRenderPipelineBatching = useSRPBatcher;
}
protected override void Render (
ScriptableRenderContext context, Camera[] cameras
) {
foreach (Camera camera in cameras) {
renderer.Render(
context, camera, useDynamicBatching, useGPUInstancing
);
}
}
最后把三个批处理操作都传递到CustomRenderPipelineAsset构造函数
,中。
[SerializeField]
bool useDynamicBatching = true, useGPUInstancing = true, useSRPBatcher = true;
protected override RenderPipeline CreatePipeline () {
return new CustomRenderPipeline(
useDynamicBatching, useGPUInstancing, useSRPBatcher
);
}
现在可以在选择使用哪种批处理方法了。 一旦发现 这个RP资源发生了改变 unity编辑器将会创建出一个新的RP实例出来。