catlikecoding:Custom SRP(Draw Calls)

catlikecoding Custom SRP第二章,主要是有关着色器的内容。

1、Shaders

主要是一些基本的shader书写,直接看原文教程吧。1、Shaders

2、Batching

假设场景中有5个Cube,每个Cube都有自己独立的材质。一般情况下,场景中的Draw Call数量是5 + 1 + 1。后两项是天空盒和Clear。

2.1 SRP Batcher

批处理是组合Drawcall的过程,可减少CPU和GPU之间的通信时间。

SRP批次不会减少Draw Call的数量,而是使其更精简。它在GPU上缓存了材质属性,因此不必在每次绘制调用时都将其发送出去。这样既减少了需要传达的数据量,又减少了每个绘图调用CPU需要完成的工作。但这仅在着色器遵守用于uniform 数据的严格结构时才有效。

为了使SRP Batcher兼容着色器,就需要将材质的属性放在特定的常量GPU缓冲区,但是并非所有平台(例如OpenGL ES 2.0)都支持常量缓冲区,所以需要用核心RP库中的宏。在这种情况下,我们得到的结果与之前完全相同,只是不支持cbuffer的平台不存在cbuffer代码。

// cbuffer UnityPerMaterial {
	// float _BaseColor;
// };

CBUFFER_START(UnityPerMaterial)
	float4 _BaseColor;
CBUFFER_END

还需要对unity_ObjectToWorld,unity_WorldToObject和unity_WorldTransformParams执行此操作,它们必须分组在UnityPerDraw缓冲区中。如果我们使用特定的一组值,则需要全部定义它们。对于转换组,即使我们不使用它,我们也需要包括float4 unity_LODFade。

CBUFFER_START(UnityPerDraw)
	float4x4 unity_ObjectToWorld;
	float4x4 unity_WorldToObject;
	float4 unity_LODFade;
	real4 unity_WorldTransformParams;
CBUFFER_END

此时,我们的着色器就支持Batcher了,下一步是启用SRP批处理程序,在CustomRenderPipeline脚本中写构造函数。

	public CustomRenderPipeline()
	{
		GraphicsSettings.useScriptableRenderPipelineBatching = true;
	}

 6,就是节省的批次。

2.2 Many Colors

即使我们使用四种材质,也可以得到一个批次。之所以可行,是因为它们的所有数据都缓存在GPU上,并且每个绘制调用仅需包含一个指向正确内存位置的偏移量。唯一的限制是每种材质的内存布局需要相同,这是因为我们对所有材质都使用相同的着色器,每个着色器仅包含一个颜色属性。

如果要为每个球体赋予自己的颜色,那么就需要创建更多的材质。接下来的代码是在不创建多个材质球的情况下,为每个球赋予自己的颜色。但是这种情况下就无法支持合批处理。原文的解释:SRP批处理程序无法处理每个对象的材质属性。因此,这24个球体每个都有一次DrawCall,由于排序,也可能将其他球体分成多个批次。

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

[DisallowMultipleComponent]
public class PerObjectMaterialProperties : MonoBehaviour
{

	static int baseColorId = Shader.PropertyToID("_BaseColor");
	static MaterialPropertyBlock block;

	[SerializeField]
	Color baseColor = Color.white;


	void Awake()
	{
		OnValidate();
	}

	void OnValidate()
	{
		if (block == null)
		{
			block = new MaterialPropertyBlock();
		}
		block.SetColor(baseColorId, baseColor);
		GetComponent<Renderer>().SetPropertyBlock(block);
	}
}

2.3 GPU Instancing

一种可以合并draw call,并对逐对象材质属性有效的方法,它被称为GPU实例化,作用是对使用相同mesh的多个对象处理成一个draw call来提交。CPU会收集每个对象的材质属性,并把它们放进实例数据的数组发送到GPU。GPU则遍历数组按照提供的顺序来渲染它们。

GPU实例化需要通过数组来提供数据,我们的shader目前不能支持。让它工作的第一步就是添加#pragma multi_compile_instancing指令。

这会让Unity为我们的shader生成两个变体,一个支持,一个不支持CPU实例化。材质面板中也出现了一个开关,让我们可以依据不同的材质选择不同的版本。

使用GPU实例化需要在shader中提供当下渲染对象的索引,索引是顶点数据提供的。hlsl定义了宏来简化这个过程,由于我们还要传递POSITION参数,所以我们需要为vertex函数定义了结构体参数,作为函数的输入。

原文解释:当使用GPU实例化的时候对象索引也可以作为顶点属性,我们可以在恰当的时候添加它,只需要把UNITY_VERTEX_INPUT_INSTANCE_ID放入Attributes结构体中。

接下来,在UnlitPassVertex的开头添加。这将从输入中提取索引,并将其储存在其他Instancing宏所依赖的全局变量中。

struct Attributes {
	float3 positionOS : POSITION;
	UNITY_VERTEX_INPUT_INSTANCE_ID
};

现在还不支持不同的实例材质参数,要添加这个功能需要用一个引用数组来替换原来的_BaseColor。

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
	//float4 _BaseColor;
	UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

批次的大小是有限制的,这取决于目标平台和每个实例的数据,如果你超过这个限制,那么最终得到的不止一个批次,此外,排序可以打断批次,如果有多种材质在使用的话。

VertexShader的输出输出也可以再简化一下代码,最后的UnlitPass.hlsl代码如下:

// NOT GPU Instance
/* #ifndef CUSTOM_UNLIT_PASS_INCLUDED
#define CUSTOM_UNLIT_PASS_INCLUDED

#include "../ShaderLibrary/Common.hlsl"

CBUFFER_START(UnityPerMaterial)
	float4 _BaseColor;
CBUFFER_END

float4 UnlitPassVertex (float3 positionOS : POSITION) : SV_POSITION {
	float3 positionWS = TransformObjectToWorld(positionOS.xyz);
	return TransformWorldToHClip(positionWS);
}

float4 UnlitPassFragment () : SV_TARGET {
	return _BaseColor;
}

#endif
*/


// GPU Instance
#ifndef CUSTOM_UNLIT_PASS_INCLUDED
#define CUSTOM_UNLIT_PASS_INCLUDED

#include "../ShaderLibrary/Common.hlsl"

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
	//float4 _BaseColor;
	UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

struct Attributes {
	float3 positionOS : POSITION;
	UNITY_VERTEX_INPUT_INSTANCE_ID
};

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;
}

float4 UnlitPassFragment (Varyings input) : SV_TARGET {
	UNITY_SETUP_INSTANCE_ID(input);
	return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
}

#endif

2.4 Drawing Many Instanced Meshes

通过代码,使用GPU实例化绘制多个相同mesh的实例化网格。

我们也可以手动生成许多游戏对象,但是这里我们不手动这么做。我们通过填充一个变换矩阵和颜色的数组来渲染一个网格,这就是GPU实例化最有用的地方。

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

public class MeshBall : MonoBehaviour
{

	static int baseColorId = Shader.PropertyToID("_BaseColor");

	[SerializeField]
	Mesh mesh = default;

	[SerializeField]
	Material material = default;

	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);
		}
	}

	void Update()
	{
		if (block == null)
		{
			block = new MaterialPropertyBlock();
			block.SetVectorArray(baseColorId, baseColors);
		}
		Graphics.DrawMeshInstanced(mesh, 0, material, matrices, 1023, block);
	}
}

2.5 Dynamic Batching

将共享相同材质的多个小网格合并为一个较大的网格,然后绘制该网格。较大的网格一般按需生成,所以动态合批仅适用于较小的网格。球体还是太大了,但立方体可以使用。要跟踪查看它的过程,需要禁用GPU实例化。代码设置如下:

		var drawingSettings = new DrawingSettings(
			unlitShaderTagId, sortingSettings
		) {
			enableDynamicBatching = true,
			enableInstancing = false
		};
		GraphicsSettings.useScriptableRenderPipelineBatching = false;

一般来说,GPU实例化优于动态批处理。动态批处理也有一些注意事项,例如,当涉及不同的比例时,不能保证较大网格的法线向量为单位长度。此外,绘制顺序也将更改,因为它现在是单个网格而不是多个。

2.6 Configuring Batching

设置我们的渲染管线到底最终采用哪种合批操作。这部分代码较多,直接看官网流程吧 —— 配置批处理

2.7 总结

总结一下:这里对应下图的Custom RP配置,对应下图。

Use Dynamic Bathching:将共享相同材质的多个小网格合并为一个较大的网格,然后绘制该网格。但是这个需要禁用SRP批处理,因为SRP批处理的优先级高。

Use GPU Instance:对于使用相同mesh的多个对象(如两个Cube,一个Cube一个Sphere不行)处理成一个draw call来提交。需要使用同一个材质球,每个实例可以具有不同的参数(可以给不同的物体通过上文的PerObjectMaterialProperties设置)并且这些相同mesh的对象不需要scale一样。这个也需要禁用SRP批处理,因为SRP批处理的优先级高。其实这里说SRP批处理和GPU Instance不能同时用也不准确,只是两者同时用的时候,不一定能够发挥最优的性能。

Use SRP Batcher:可以使用不同的材质球,但是必须需要使用相同的着色器,合批的原理:SRP合批并不会减少drawcall的调用而是让它们更加精简,它将材质属性缓存到GPU上。

3、Transparency

可以将渲染队列设置为透明,但是这只会在绘制对象时改变绘制顺序,而不是如何绘制。

3.1 Blend Modes

两个shader属性:_SrcBlend and _DstBlend。他们是混合模式的枚举。

为了简化编辑,我们可以将Enum属性添加到属性中,并使用完全限定的UnityEngine.Rendering.BlendMode Enum类型作为参数,这样在unity中就可以选择采用哪种混合方式了。

 此时的设置:这个默认值代表我们已经使用的不透明配置,源被设置为1,意味着它被完全添加,目标被设置为0,以为着它完全被忽略了。

标准透明度的源混合模式是SrcAlpha,这意味着渲染颜色的RGB分量乘以其alpha分量。因此,alpha值越低越弱。然后将目标混合模式设置为相反:OneMinusSrcAlpha,以达到总权重1。

3.2 ZWrite

这部分是shader相关的内容,说白了就是当采用半透明的材质时,需要关闭ZWrite。

[Enum(Off, 0, On, 1)] _ZWrite ("Z Write", Float) = 1


Blend [_SrcBlend] [_DstBlend]
ZWrite [_ZWrite]

3.3  Use Texture

需要提供纹理直接上代码:

// Unlit.shader
_BaseColor ("Color", Color) = (1.0, 1.0, 1.0, 1.0)
_BaseMap("Texture", 2D) = "white" {}

// UnlitPass.hlsl
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
	//float4 _BaseColor;
	UNITY_DEFINE_INSTANCED_PROP(float4, _BaseMap_ST)
	UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

写到这里就会发现,Attributes和Varyings结构就是我们默认渲染管线的Unlit Shader的appdata,v2f结构体,一个是CPU传递的数据,一个是Vertex Shader传递给Fragment Shader的数据,代码如下:

// appdata
struct Attributes {
	float3 positionOS : POSITION;
	float2 baseUV : TEXCOORD0;
	UNITY_VERTEX_INPUT_INSTANCE_ID
};

// v2f
struct Varyings {
	float4 positionCS : SV_POSITION;
	float2 baseUV : VAR_BASE_UV;
	UNITY_VERTEX_INPUT_INSTANCE_ID
};

然后就是在FragmentShader中采用纹理了,使用SAMPLE_TEXTURE2D宏,将纹理、采样器状态和坐标作为参数,在此处对纹理进行采样。

// Vertex Shader
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);

	float4 baseST = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseMap_ST);
	output.baseUV = input.baseUV * baseST.xy + baseST.zw;
	return output;
}

// Fragment Shader
float4 UnlitPassFragment (Varyings input) : SV_TARGET {
	UNITY_SETUP_INSTANCE_ID(input);
	float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.baseUV);
	float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
	return baseMap * baseColor;
}

现在我们的材质就能够支持纹理了。

 3.4 Alpha Clipping

就是透明度剔除,clip函数,clip(x),当x小于0时,就不渲染此片元。

_BaseColor("Color", Color) = (1.0, 1.0, 1.0, 1.0)
_Cutoff ("Alpha Cutoff", Range(0.0, 1.0)) = 0.5

UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_DEFINE_INSTANCED_PROP(float, _Cutoff)

// Fragment Shader
float4 UnlitPassFragment (Varyings input) : SV_TARGET {
	UNITY_SETUP_INSTANCE_ID(input);
	float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.baseUV);
	float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
	float4 base =  baseMap * baseColor;
	clip(base.a - UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Cutoff));
	return base;
}

3.5 Render Queue:Alpha Test

一个材质通常使用半透明混合或者裁剪,而不是同时都使用。一个典型的裁剪类型的材质除了丢弃的那些片段以外是完全不透明的,并会写入深度缓冲。它使用的是AlphaTest渲染队列,这意味着它会在完全不透明的对象后面进行渲染。这样做是因为丢弃片段使得一些GPU优化变得不可能,因为三角形不再被认为完全覆盖了它们背后的东西。通过首先绘制完全不透明的对象,它们可能最终覆盖了alpha剪裁对象的一部分(没discard),这样就不需要处理它们隐藏的片段。

因此,将我们的材质的Render Queue设置为AlphaTest。

 3.6 Cutoff Per Object

在PerObjectMaterialProperties.cs中将Cutoff像Color一样添加。

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

[DisallowMultipleComponent]
public class PerObjectMaterialProperties : MonoBehaviour
{

	static int baseColorId = Shader.PropertyToID("_BaseColor");
	static int cutoffId = Shader.PropertyToID("_Cutoff");
	static MaterialPropertyBlock block;

	[SerializeField]
	Color baseColor = Color.white;

	[SerializeField, Range(0f, 1f)]
	float cutoff = 0.5f;



	void Awake()
	{
		OnValidate();
	}

	void OnValidate()
	{
		if (block == null)
		{
			block = new MaterialPropertyBlock();
		}
		block.SetColor(baseColorId, baseColor);
		block.SetFloat(cutoffId, cutoff);
		GetComponent<Renderer>().SetPropertyBlock(block);
	}
}

3.7 Ball of Alpha-Clipped Spheres

还记得如果用相同的mesh就可以用GPU Instance来优化我们的合批吧,这里让我们随机产生相关的mesh,给予随机的颜色和Culloff,就是前文的MeshBall.cs,代码如下:

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

public class MeshBall : MonoBehaviour
{

	static int baseColorId = Shader.PropertyToID("_BaseColor");

	[SerializeField]
	Mesh mesh = default;

	[SerializeField]
	Material material = default;

	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.Euler(Random.value * 360f, Random.value * 360f, Random.value * 360f), 
										Vector3.one * Random.Range(0.5f, 1.5f));
			
			
			baseColors[i] = new Vector4(Random.value, Random.value, Random.value, Random.Range(0.5f, 1f));
		}
	}

	void Update()
	{
		if (block == null)
		{
			block = new MaterialPropertyBlock();
			block.SetVectorArray(baseColorId, baseColors);
		}
		Graphics.DrawMeshInstanced(mesh, 0, material, matrices, 1023, block);
	}
}

跟2.4中的颜色数组一样,这里Unity最终还是会向GPU发送一个裁剪值的数组,每个实例一个,即使它们都是相同的。该值是材质上的copy,因此可以通过修改它来一次性让所有球体的孔发生变化

接下来,要想看到场景中的效果,记得将我们的材质Enable GPU Instancing和我们的渲染管线的Use GPU Instance开启。

 最后,上一张第二章的最后效果图,此章完结撒花 ~~~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值