前言
本文章主要介绍unity的SRPBatcher和GPUInstancing,在Universal Render Pipeline(通用渲染管道,简称URP)中使用注意事项和相关知识。
一、SRP Batcher
1.简介
SRP Batcher是URP的一种新的合批技术,可以加速CPU,对于DX平台提升比较大,但对于移动平台来说,提升不是很明显,大概1.2倍左右,这部分还根据不同机型有一些差距,感兴趣的可以拿机子去验证。
SRP Batcher是为了解决什么问题诞生的?一个Drawcall被一个新的材质使用的时候,需要准备进行渲染设置工作,这部分耗时是一个drawcall的主要耗时点,所以如果场景有越多的materials,就会有越多的CPU必须使用去设置GPU 数据。传统的优化做法是减少drawcall数量去提升CPU渲染性能,而实际上真正的CPU消耗来自那些设置工作,而不是GPU drawcall本身,Drawcall只是一些Unity向GPU的 command buffer发送的字节数据。
下图显示了SRPBatcher工作流程:
更加详细的知识读者可以参考官方参考Blog,笔者主要介绍如何在urp管道中写Shader去让SRP Batcher生效,还有SRP Batcher兼容的问题等。
2.URP的Shader支持SRP Batcher
下图显示了示例目录结构:
ScenePbrInput.shader的伪代码如下:
#ifndef SCENEPBR_INPUT_INCLUDED
#define SCENEPBR_INPUT_INCLUDED
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
half4 _BaseColor;
half _Cutoff;
half _Opacity;
half4 _EmissionColor;
CBUFFER_END
#endif // SCENEPBR_INPUT_INCLUDED
ScenePbr.shader的伪代码如下:
Shader "Custom/Scene/XXX"
{
Properties
{
_BaseMap("Texture", 2D) = "white" {}
_BaseColor("Color", Color) = (1, 1, 1, 1)
_Cutoff("AlphaCutout", Range(0.0, 1.0)) = 0.5
_Opacity("Opacity",Range(0.0, 1.0)) = 0
[Toggle(_EMISS_ON)]_EmissOn("Emiss On", Float) = 0
[HDR]_EmissionColor("Color", Color) = (0,0,0,0)
_EmissionMap("Emission", 2D) = "white" {}
// BlendMode
[HideInInspector] _Surface("__surface", Float) = 0.0
[HideInInspector] _Blend("__blend", Float) = 0.0
[HideInInspector] _AlphaClip("__clip", Float) = 0.0
[HideInInspector] _SrcBlend("Src", Float) = 1.0
[HideInInspector] _DstBlend("Dst", Float) = 0.0
[HideInInspector] _ZWrite("ZWrite", Float) = 1.0
[HideInInspector] _Cull("__cull", Float) = 2.0
[HideInInspector] _QueueOffset("Queue offset", Float) = 0.0
[HideInInspector] _ReceiveShadows("Receive Shadows", Float) = 1.0
}
SubShader
{
Tags{"RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "IgnoreProjector" = "True"}
Pass
{
Name "ForwardBase"
Tags{"LightMode" = "UniversalForward"}
Blend [_SrcBlend][_DstBlend]
ZWrite [_ZWrite]
Cull [_Cull]
HLSLPROGRAM
#pragma prefer_hlslcc gles
#pragma exclude_renderers d3d11_9x
#pragma vertex vert
#pragma fragment frag
//#pragma shader_feature_local _ALPHATEST_ON
#pragma shader_feature_local _RECEIVE_SHADOWS_OFF
#pragma shader_feature_local _EMISS_ON
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ LIGHTMAP_ON
#pragma multi_compile_fog
#pragma multi_compile_instancing
#include "ScenePbrInput.hlsl"
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
TEXTURE2D(_EmissionMap); SAMPLER(sampler_EmissionMap);
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
float2 uv : TEXCOORD0;
float2 lightmapUV : TEXCOORD1;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
half2 uv : TEXCOORD0;
half fogCoord : TEXCOORD1;
#ifdef _MAIN_LIGHT_SHADOWS
half4 shadowCoord : TEXCOORD2;
#endif
half3 normalWS : TEXCOORD3;
half3 viewDirWS : TEXCOORD4;
half2 lightmapUV : TEXCOORD5;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
Varyings vert(Attributes i)
{
Varyings o = (Varyings)0;
UNITY_SETUP_INSTANCE_ID(i);
UNITY_TRANSFER_INSTANCE_ID(i, o);
VertexPositionInputs vertexInput = GetVertexPositionInputs(i.positionOS.xyz);
o.positionCS = vertexInput.positionCS;
o.uv = TRANSFORM_TEX(i.uv, _BaseMap);
o.fogCoord = ComputeFogFactor(vertexInput.positionCS.z);
#if defined(_MAIN_LIGHT_SHADOWS) && !defined(_RECEIVE_SHADOWS_OFF)
o.shadowCoord = GetShadowCoord(vertexInput);
#endif
OUTPUT_LIGHTMAP_UV(i.lightmapUV, unity_LightmapST, o.lightmapUV);
half3 viewDirWS = GetCameraPositionWS() - vertexInput.positionWS;
o.normalWS = normalize(TransformObjectToWorldNormal(i.normalOS));
return o;
}
half4 frag(Varyings i) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(i);
half4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv);
half3 color = texColor.rgb * _BaseColor.rgb;
half alpha = texColor.a * _BaseColor.a;
AlphaDiscard(alpha, _Cutoff);
#if defined(_MAIN_LIGHT_SHADOWS) && !defined(_RECEIVE_SHADOWS_OFF)
half4 shadowCoord = i.shadowCoord;
#else
half4 shadowCoord = half4(0, 0, 0, 0);
#endif
Light mainLight = GetMainLight(shadowCoord);
#ifdef LIGHTMAP_ON
//half3 lightDir = mainLight.direction;
half3 bakedGI = SampleLightmap(i.lightmapUV, i.normalWS);
bakedGI = SubtractDirectMainLightFromLightmap(mainLight, i.normalWS, bakedGI);
color *= bakedGI;
#else
half3 atten = mainLight.distanceAttenuation * mainLight.shadowAttenuation;
color *= atten;
#endif
#ifdef _EMISS_ON
half4 emission = SAMPLE_TEXTURE2D(_EmissionMap, sampler_EmissionMap, i.uv) * _EmissionColor;
color += emission.rgb;
#endif
color.rgb = lerp(MixFog(color.rgb, i.fogCoord), color.rgb, _SaturationScene);
alpha -= _Opacity;
alpha = saturate(alpha);
return half4(color, alpha);
}
ENDHLSL
}
Pass
{
Name "ShadowCaster"
Tags{"LightMode" = "ShadowCaster"}
ZWrite On
ZTest LEqual
Cull[_Cull]
HLSLPROGRAM
#pragma prefer_hlslcc gles
#pragma exclude_renderers d3d11_9x
#pragma target 2.0
#pragma shader_feature_local _ALPHATEST_ON
#pragma vertex ShadowPassVertex
#pragma fragment ShadowPassFragment
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "ScenePbrInput.hlsl"
//实现部分功能
ENDHLSL
}
Pass
{
Tags{"LightMode" = "DepthOnly"}
ZWrite On
ColorMask 0
HLSLPROGRAM
#pragma prefer_hlslcc gles
#pragma exclude_renderers d3d11_9x
#pragma target 2.0
#pragma vertex DepthOnlyVertex
#pragma fragment DepthOnlyFragment
#pragma shader_feature_local _ALPHATEST_ON
#pragma multi_compile_instancing
#include "ScenePbrInput.hlsl"
//实现部分功能
... ...
ENDHLSL
}
}
}
从上面代码可以看出,我们需要每个pass都添加上同样的CBUFFER_START(UnityPerMaterial) CBUFFER_END(Constant Buffer),这样shader编译后才会显示:
需要注意的是,SRP Batcher对于Shader有不同编译的变体,最后生成的shader也是无法支持SRP Batcher,这个可以用Frame Debugger调试去查看。
比如下图的变体收集:
A物体使用的是编译后的上面的那个变体集合,B物体使用的是下面那俩个变体集合,那即使这两个使用的同个Shader也无法支持SRP Batcher。简单来说,其合并条件为相同Shader和相同的激活的keyword。
二、GPU Instancing
1.简介
使用 GPU Instancing可使用少量绘制调用一次绘制(或渲染)同一网格的多个副本。它对于绘制诸如建筑物、树木和草地之类的在场景中重复出现的对象非常有用。合并批次的前提条件是同网格同材质,使用比较多的是植被相关的,比如草和树木。在使用上需要注意当代码调用改变属性时候,需要用MaterialPropertyBlock,从而不会破坏GPU Instancing。更为详细的知识可以阅读官方文档。
总结
在URP管道中使用GPU Instancing和SRP Batcher需要注意,二者只能存在其中一种,而SRP Batcher优先级最高,对于写草和树的Shader,笔者比较建议直接支持GPU Instancing,而不支持SRP Batcher, 在移动端上效率相对SRP Batcher更好。