Shader 前向渲染和延迟渲染

渲染路径

渲染路径(Rendering Path)决定了光照是如何应用到Unity Shader中的,如果我们没有指定正确的渲染路径,那么一些光照变量很可能不会被正确赋值,我们计算出的效果也就很有可能是错误的
Shader 前向渲染和延迟渲染_第1张图片
在Graphics中可以为整个项目设置渲染路径,默认为前向渲染
Shader 前向渲染和延迟渲染_第2张图片
可以使用多个渲染路径,例如相机A使用前向渲染路径,而相机B使用延迟渲染路径。这时可以设置每个相机的渲染路径,以覆盖Project Settings中的设置

Pass {
    Tags { "LightMode" ="ForwardBase" }

可以在每个Pass中使用标签来指定该Pass使用的渲染路径,LightMode支持的标签如下

标签名 描述
Always 不管使用哪种渲染路径,该Pass总是会被渲染,但不会计算任何光照
ForwardBase 用于前向渲染。该Pass会计算环境光、最重要的平行光、逐顶点/SH光源和Lightmaps
ForwardAdd 用于前向渲染。该Pass会计算额外的逐像素光源,每个Pass对应一个光源
Deferred 用于延迟渲染。该Pass会渲染G缓冲(G-buffer)
ShadowCaster 把物体的深度信息渲染到阴影映射纹理(shadowmap)或一张深度纹理中
Vertex、VertexLMRGBM和VertexLM 用于遗留的顶点照明渲染

前向渲染

每进行一次完整的前向渲染,需要渲染该对象的渲染图元,并计算两个缓冲区的信息:一个是颜色缓冲区,一个是深度缓冲区。利用深度缓冲来决定一个片元是否可见,如果可见就在片元着色器中计算光照并更新颜色缓冲区中的颜色值,大致流程如下
Shader 前向渲染和延迟渲染_第3张图片

下面的伪代码来描述大致过程:

Pass 
{    
    for (each primitive in this model)
    {        
        for (each fragment covered by this primitive)
        {            
            if (failed in depth test) 
            {                
                // 如果没有通过深度测试,说明该片元是不可见的
                discard;            
            } 
            else 
            {                
                // 如果该片元可见
                // 就进行光照计算
                float4 color =Shading(materialInfo, pos, normal, lightDir, viewDir);
                // 更新帧缓冲(颜色缓冲)
                writeFrameBuffer(fragment, color);            
            }
        }
    }
}

如果一个物体在多个光源的影响区域内,那么该物体就需要执行多个Pass,每个Pass计算一个光源的光照结果,然后在帧缓冲中把这些光照结果混合起来得到最终的颜色值。假设场景中有N个物体,每个物体受M个光源的影响,那么要渲染整个场景一共需要N * M个Pass,光源数量对前向渲染影响很大

前向渲染路径有3种处理光照的方式:逐顶点处理、逐像素处理,球谐函数(Spherical Harmonics,SH)处理,Unity对这些光源进行一个重要度排序

Shader 前向渲染和延迟渲染_第4张图片
Shader 前向渲染和延迟渲染_第5张图片

  • 场景中最亮的平行光总是按逐像素处理
  • 渲染模式被设置成Important的光源,会按逐像素处理
  • 渲染模式被设置成Not Important的光源,会按逐顶点或者SH处理
  • Pixel Light Count = 4,表示最多4个逐像素光照

Shader 前向渲染和延迟渲染_第6张图片

前向渲染的两种Pass,必须添加 #pragma multi_compile_fwdbase 和 #pragmamulti_compile_fwdadd 这两个指令才可以在相关的Pass中得到一些正确的光照变量,例如光照衰减值等

在Additional Pass中还设置了混合模式。这是因为我们希望每个Additional Pass可以与上一次的光照结果在帧缓存中进行叠加,从而得到最终有多个光照的渲染效果

前向渲染可以使用的内置光照变量

名称 类型 描述
_LightColor0 float4 逐像素光源的颜色
_WorldSpaceLightPos0 float4 该Pass逐像素光源的位置。如果该光源是平行光,那么w是0,其他光源类型w值为1
unity_WorldToLight float4×4 从世界空间到光源空间的变换矩阵。可以用于采样cookie和光强衰减(attenuation)纹理
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0 float4 仅用于Base Pass。前4个非重要的点光源在世界空间中的位置
unity_4LightAtten0 float4 仅用于Base Pass。存储了前4个非重要的点光源的衰减因子
unity_LightColor half4[4] 仅用于Base Pass。存储了前4个非重要的点光源的颜色

前向渲染可以使用的内置光照函数

函数名 描述
float3 WorldSpaceLightDir(float4 v) 仅可用于前向渲染。输入一个模型空间中的顶点位置,返回世界空间中从该点到光源的光照方向。内部实现使用了UnityWorldSpaceLightDir函数。没有归一化
float3 UnityWorldSpaceLightDir (float4 v) 仅可用于前向渲染。输入一个世界空间中的顶点位置,返回世界空间中从该点到光源的光照方向。没有归一化
float3 ObjSpaceLightDir (float4 v) 仅可用于前向渲染。输入一个模型空间中的顶点位置,返回模型空间中从该点到光源的光照方向。没有归一化
float3 Shade4PointLights (…) 仅可用于前向渲染。计算四个点光源的光照,它的参数是已经打包进矢量的光照数据,如unity_4LightPosX0,unity_4LightPosY0, unity_4LightPosZ0、unity_LightColor和unity_4LightAtten0等。前向渲染通常会使用这个函数来计算逐顶点光照

Shader 前向渲染和延迟渲染_第7张图片
使用前向渲染,在场景中添加两个胶囊体,1个平行光,4个点光源,前2个是Base Pass渲染平行光,后8个是Additional Pass渲染点光源,总共10个Pass

对点光源,聚光灯的衰减计算更复杂,涉及开根号、除法等计算量较大操作,因此Unity选择了使用一张纹理(_LightTexture0)作为查找表(Lookup Table,LUT),以在片元着色器中得到光源的衰减。在_LightTexture0上(0, 0)点表明了与光源位置重合的点的衰减值,而(1, 1)点表明了在光源空间中距离最远的点的衰减。为了得到某个顶点的光照衰减值,需要用unity_WorldToLight把该顶点从世界空间变换到光源空间,然后用这个顶点坐标的模的平方对_LightTexture0进行采样,得到衰减值

Shader实现

Shader "Unity Shaders Book/Chapter 9/Forward Rendering" 
{
	Properties
	{
		_MainTex	("Texture",	 2D)			  = "white" {}
		_Diffuse	("Diffuse",  Color)			  = (1, 1, 1, 1)
		_Specular	("Specular", Color)			  = (1, 1, 1, 1)
		_Gloss		("Gloss",	 Range(8.0, 256)) = 20
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		
		Pass 
		{
			// Base Pass只处理平行光,如果场景中有多个平行光,Unity会选择最亮的平行光传递给
			// Base Pass进行逐像素处理,其他平行光按照逐顶点或在Additional Pass中按逐像素处理
			Tags { "LightMode"="ForwardBase" }
		
			CGPROGRAM
			// 这个指令保证我们在Shader中使用光照衰减等光照变量可以被正确赋值
			#pragma multi_compile_fwdbase	
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"
			
			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float2 uv	  : TEXCOORD0;
			};
			
			struct v2f
			{
				float4 pos		   : SV_POSITION;
				float2 uv		   : TEXCOORD0;
				float3 worldNormal : TEXCOORD1;
				float3 worldPos    : TEXCOORD2;
			};

			sampler2D _MainTex;
            float4 _MainTex_ST;
			fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;
			
			v2f vert(a2v v)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target
			{
				// 使用Blinn-Phong光照模型
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
				
				// 环境光只在Base Pass中计算一次
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

				// 漫反射,使用_LightColor0来得到平行光的颜色和强度
			 	fixed3 diffuse = tex2D(_MainTex, i.uv).rgb * _LightColor0.rgb * _Diffuse.rgb
								 * max(0, dot(worldNormal, worldLightDir));

				// 高光
			 	fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
			 	fixed3 halfDir = normalize(worldLightDir + viewDir);
			 	fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

				// 衰减,平行光认为是没有衰减的,这里不写也行
                float atten = 1.0;
				return fixed4(ambient + (diffuse + specular) * atten, 1.0);
			}
			ENDCG
		}
	
		Pass 
		{
			// 去掉Base Pass中环境光、自发光、逐顶点光照、SH光照的部分,并添加一些对不同光源类型的支持
			Tags { "LightMode"="ForwardAdd" }
			// 设置混合模式,将光照结果在帧缓存中与之前的光照结果进行叠加。
			Blend One One
		
			CGPROGRAM
			// 这个指令可以保证我们在Additional Pass中访问到正确的光照变量。
			#pragma multi_compile_fwdadd
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"
			
			fixed4 _Diffuse;
			fixed4 _Specular;
			float _Gloss;
			
			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			};
			
			struct v2f
			{
				float4 pos : SV_POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
			};
			
			v2f vert(a2v v)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target
			{
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
				fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
				
				fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
				fixed3 halfDir = normalize(worldLightDir + viewDir);
				fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

				// 对于位置、方向和衰减属性,我们需要根据光源类型分别计算
				#ifdef USING_DIRECTIONAL_LIGHT
					fixed atten = 1.0;
				#else
					#if defined (POINT)
						// 通过unity_WorldToLight变换把顶点从世界空间转到灯光空间
				        float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
						// .rr操作相当于构建了一个二维采样坐标,UNITY_ATTEN_CHANNEL来得到衰减纹理中衰减值所在的分量
				        fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
				    #elif defined (SPOT)
				        float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1));
				        fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
				    #else
				        fixed atten = 1.0;
				    #endif
				#endif

				return fixed4((diffuse + specular) * atten, 1.0);
			}
			ENDCG
		}
	}
	FallBack "Specular"
}

延迟渲染

除了前向渲染中使用的颜色缓冲和深度缓冲外,延迟渲染还会利用额外的缓冲区,这些缓冲区被统称为G缓冲(G-buffer),G是Geometry的缩写

延迟渲染包含了两个Pass。在第一个Pass中,不进行任何光照计算,仅仅计算哪些片元是可见的,这是通过深度缓冲技术来实现,当发现一个片元是可见的,会把物体的漫反射颜色、高光反射颜色、平滑度、法线、自发光和深度等信息渲染到屏幕空间的G缓冲区中。在第二个Pass中,利用G缓冲区的数据进行真正的光照计算,再存储到帧缓冲中

下面列出了 G 缓冲区中渲染目标 (RT0 - RT4) 的默认布局(RT,Render Texture)
RT0:格式是ARGB32,RGB通道用于存储漫反射颜色,A通道遮罩
RT1:格式是ARGB32,RGB通道用于存储高光反射颜色,A通道用于存储高光反射的指数部分
RT2:格式是ARGB2101010,RGB通道用于存储法线,A通道没有被使用
RT3:格式是ARGB32(非HDR)或ARGBHalf(HDR),用于存储自发光+lightmap+反射探针(reflection probes)
深度缓冲
模板缓冲

大致流程如下
Shader 前向渲染和延迟渲染_第8张图片

下面的伪代码描述大致过程

Pass 1
{
	// 第一个Pass不进行真正的光照计算
	// 仅仅把光照计算需要的信息存储到G缓冲中
	for (each primitive in this model)
	{        
		for (each fragment covered by this primitive)
		{            
			if (failed in depth test)
			{                
				// 如果没有通过深度测试,说明该片元是不可见的
				discard;
			}
			else
			{                
				// 如果该片元可见
				// 就把需要的信息存储到G缓冲中
				writeGBuffer(materialInfo, pos, normal);
			}
		}   
	}
}

Pass 2 
{    
	// 利用G缓冲中的信息进行真正的光照计算
	for (each pixel in the screen)
	{        
		if (the pixel is valid)
		{            
			// 如果该像素是有效的
			// 读取它对应的G缓冲中的信息
			readGBuffer(pixel, materialInfo, pos, normal);
			// 根据读取到的信息进行光照计算
			float4 color = Shading(materialInfo, pos, normal, lightDir, viewDir); 
			// 更新帧缓冲
			writeFrameBuffer(pixel, color);
		}
	}
}

可以看出延迟渲染使用的Pass数目通常就两个,跟光源数目没有关系。即延迟渲染的效率不依赖于场景的复杂度,而是和屏幕空间的大小有关。因为我们需要的信息都存储在缓冲区中,而这些缓冲区可以理解成是一张张2D图像,我们的计算实际上就是在这些图像空间中进行的,光照计算相当于一个2D后处理

延迟渲染的一些缺点:

  • 不支持真正的抗锯齿(anti-aliasing)功能
  • 不能处理半透明物体
  • 对显卡有一定要求。如果要使用延迟渲染的话,显卡必须支持MRT(Multiple RenderTargets)、Shader Mode 3.0及以上、深度渲染纹理以及双面的模板缓冲

Shader 前向渲染和延迟渲染_第9张图片
Shader 前向渲染和延迟渲染_第10张图片
使用延迟渲染,场景内容和上面一样,在GBuffer部分2个Pass,相关信息存储到RT上,Light部分4个点光源和1个平行光

fixed4 frag(v2f i) : SV_Target

前向渲染中 SV_Target 就是颜色缓冲(ColorRT),延迟渲染需要多个RT,所以要定义一个新的输出数据结构 deferredOutput
下面Shader是延迟渲染的第一个Pass,存储GBuffer,第二个Pass计算光照,默认会使用Unity内置的Standard光照模型

Shader "MyCustom/DeferredGBuffer" 
{
	Properties
	{
		_MainTex	("Texture",		2D)				= "white" {}
		_Diffuse	("Diffuse",		Color)			= (1, 1, 1, 1)
		_Specular	("Specular",	Color)			= (1, 1, 1, 1)
		_Gloss		("Gloss",		Range(1, 256))	= 20
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		
		Pass 
		{
			Tags { "LightMode"="Deferred" }
		
			CGPROGRAM
			#pragma target 3.0
			#pragma vertex vert
			#pragma fragment frag
			#pragma exclude_renderers nomrt
			#pragma multi_compile __ UNITY_HDR_ON

			#include "UnityCG.cginc"
			
			struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv	  : TEXCOORD0;
            };
			
			struct v2f
			{
				float4 pos			: SV_POSITION;
				float2 uv			: TEXCOORD0;
				float3 worldNormal  : TEXCOORD1;
				float3 worldPos		: TEXCOORD2;
			};

			// 延迟渲染输出的数据结构,需要4个Target
			struct deferredOutput
			{
				// rgb存储漫反射颜色,a存储遮罩
				float4 gBuffer0 : SV_Target0;
				// rgb存储高光反射颜色,a存储gloss
				float4 gBuffer1 : SV_Target1;
				// rgb存储时世界空间法线,a没有使用
				float4 gBuffer2 : SV_Target2;
				// 自发光 + lightmap + reflection probes
				float4 gBuffer3 : SV_Target3;
			};

			sampler2D _MainTex;
            float4 _MainTex_ST;
			float4 _Diffuse;
			float4 _Specular;
			float _Gloss;
			
			v2f vert(appdata v)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				return o;
			}
			
			deferredOutput frag(v2f i)
			{
				deferredOutput o;

				float3 color = tex2D(_MainTex, i.uv).rgb * _Diffuse.rgb;
				o.gBuffer0.rgb = color;
				o.gBuffer0.a = 1;

				o.gBuffer1.rgb = _Specular.rgb;
				o.gBuffer1.a = _Gloss / 256.0;

				// 法线的取值范围 [-1, 1],转换到纹理的范围 [0, 1] 
				o.gBuffer2.rgb = i.worldNormal * 0.5 + 0.5;
				o.gBuffer2.a = 1;
				
				o.gBuffer3.rgb = 0;
				o.gBuffer3.a = 1;
				
				return o;
			}
			ENDCG
		}
	}
}

也可以使用自己实现的光照模型,在Graphics中引用
Shader 前向渲染和延迟渲染_第11张图片

光照部分Shader

Shader "MyCustom/DeferredLighting" 
{
	Properties
	{
		_MainTex	("Texture",		2D)				= "white" {}
		_Diffuse	("Diffuse",		Color)			= (1, 1, 1, 1)
		_Specular	("Specular",	Color)			= (1, 1, 1, 1)
		_Gloss		("Gloss",		Range(1, 256))	= 20
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		
		Pass 
		{
			Tags { "LightMode"="Deferred" }
			// 第一个Pass经过了深度测试,光照部分就不需要了
			ZWrite Off
			// 把光照渲染到已经存在的GBuffer上
			Blend One One
		
			CGPROGRAM
			#pragma target 3.0
			// vert_deferred 是 UnityDeferredLibrary 中定义的顶点着色器
			#pragma vertex vert_deferred
			#pragma fragment frag
			// 需要所有的灯光变体
			#pragma multi_compile_lightpass
			// 区分是否开启HDR
			#pragma multi_compile __ UNITY_HDR_ON
			
			#include "Lighting.cginc"
			#include "UnityDeferredLibrary.cginc"
			
			struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

			sampler2D _CameraGBufferTexture0;   // 漫反射颜色和遮罩
            sampler2D _CameraGBufferTexture1;   // 高光和平滑度
            sampler2D _CameraGBufferTexture2;   // 法线
            sampler2D _CameraGBufferTexture3;   // 自发光,lightmap,反射探针
			
			fixed4 frag(unity_v2f_deferred i) : SV_Target
			{
				// 定义光照属性
				float3 worldPos;
				float2 uv;
				half3 lightDir;
				float atten;
				// 衰减距离
				float fadeDist;
                // UnityDeferredLibrary中定义的方法,计算并返回上面定义的属性
				UnityDeferredCalculateLightParams(i, worldPos, uv, lightDir, atten, fadeDist);

				float3 lightColor = _LightColor.rgb * atten;
				//采样GBuffer
				float4 diffuse = tex2D(_CameraGBufferTexture0, uv);
				float4 specular = tex2D(_CameraGBufferTexture1, uv);
				// 转回 [-1, 1]
				float4 worldNormal = normalize(tex2D(_CameraGBufferTexture2, uv) * 2 - 1);

				// 漫反射
				float3 diff = lightColor * diffuse.rgb * max(0, dot(worldNormal, lightDir));

				// 高光
				lightDir = normalize(lightDir);
				float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
				float3 halfDir = normalize(lightDir + viewDir);
				float hDotN = max(0, dot(halfDir, worldNormal));
				float3 spec = lightColor * specular.rgb * pow(hDotN, specular.a * 50);

				float4 color = 1;
				color.rgb = diff + spec;

				#ifdef UNITY_HDR_ON
					return color;
				#else
					return exp2(-color);
				#endif
			}
			ENDCG
		}
		
		//转码pass,主要是对于LDR转码
		Pass
		{
            //使用深度测试,关闭剔除
			ZTest Always
			Cull Off
			ZWrite Off
			
            //修改模板缓冲区,避免破坏天空盒颜色
            Stencil
			{
				//_StencilNonBackground是unity提供的天空盒遮蔽模板
				ref[_StencilNonBackground]
				readMask[_StencilNonBackground]

				compback equal
				compfront equal
			}

			CGPROGRAM
			#pragma target 3.0
			#pragma vertex vert
            #pragma fragment frag

			#include "UnityCG.cginc"

			sampler2D _LightBuffer;
            struct appdata
			{
				float4 pos	: POSITION;
				float2 uv   : TEXCOORD0;
			};
			struct v2f
			{
				float4 pos : SV_POSITION;
				float2 uv  : TEXCOORD0;
			};

			v2f vert(appdata v)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.pos);
				o.uv = v.uv;
				return o;
			}
			fixed4 frag(v2f i): SV_Target
			{
				return -log2(tex2D(_LightBuffer, i.uv));
			}

			ENDCG
		}
	}
}

对比

Shader 前向渲染和延迟渲染_第12张图片

参考

《Unity Shader入门精要》
《Unity ShaderLab新手宝典》
延迟着色渲染路径
Unity里的延迟渲染
【技术美术百人计划】图形 3.4 延迟渲染管线介绍

你可能感兴趣的:(技术美术,unity)