在自定义Shader中应用Unity全局光照

目录

  • 从一个简单的Diffuse Shader开始
  • 接受间接光照——使用内置函数
  • 贡献间接光照——meta pass
  • 结果

上一篇是关于Unity全局光照系统的介绍和使用方法,但是用于测试效果的场景物体都是使用了Standard材质。大部分的builtin-shader都能够支持Unity的全局光照系统,和Lightmap和Light Probe配合工作。而使用surface shader编写的自定义shader不需要做任何额外处理也同样能够支持,这是因为Unity在通过surface shader生成vertex&fragment shader时替我们做了这些处理。但是今天要讨论的是使用vertex&fragment shader编写自定义shader时,如何让我们的shader支持Unity的全局光照系统。

从一个简单的Diffuse Shader开始

我们从一个简单的Diffuse Shader开始,逐步为它加添加代码让它能够支持全局光照。Shader如下:

Shader "Custom/Diffuse"
{
	Properties
	{
		_MainTex("Main Texture", 2D) = "white" {}
		_Color("Color Tint", Color) = (0.0, 0.0, 0.0, 0.0)
		[HDR]_Emission("Emission", Color) = (0.0, 0.0, 0.0, 0.0)
	}
	SubShader
	{
		Tags { "RenderType" = "Opaque" }

		// forward base pass
		Pass
		{
			Tags{ "LightMode" = "ForwardBase" }
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile_fwdbase

			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			#include "AutoLight.cginc"

			sampler2D _MainTex;
			float4 _MainTex_ST;
			float4 _Color;
			float4 _Emission;

			struct appdata
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float2 uv : TEXCOORD0;// main texture uv
			};

			struct v2f
			{
				float4 pos : SV_POSITION;
				float2 uv : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
				float3 worldNormal : TEXCOORD2;
				SHADOW_COORDS(3)
			};

			v2f vert(appdata v)
			{
				v2f o = (v2f)0;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				o.worldNormal = UnityObjectToWorldNormal(v.normal);

				TRANSFER_SHADOW(o);
				return o;
			}

			float4 frag(v2f i) : SV_Target
			{
				float4 albedo = tex2D(_MainTex, i.uv) * _Color;

				// calculate direct diffuse lighting (Lambert)
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLight = normalize(UnityWorldSpaceLightDir(i.worldPos));
				float3 direct = _LightColor0.rgb * saturate(dot(worldNormal, worldLight));

				// calculate light attenuation and shadow
				UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

				return float4(_Emission + albedo * direct * atten, 1.0);
			}


			ENDCG
		}

		// forward add pass
		Pass
		{
			Tags{ "LightMode" = "ForwardAdd" }
			Blend One One

			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile_fwdadd_fullshadows

			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			#include "AutoLight.cginc"

			sampler2D _MainTex;
			float4 _MainTex_ST;
			float4 _Color;

			struct appdata
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float2 uv : TEXCOORD0;// main texture uv
			};

			struct v2f
			{
				float4 pos : SV_POSITION;
				float2 uv : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
				float3 worldNormal : TEXCOORD2;
				SHADOW_COORDS(3)
			};

			v2f vert(appdata v)
			{
				v2f o = (v2f)0;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				o.worldNormal = UnityObjectToWorldNormal(v.normal);

				TRANSFER_SHADOW(o);
				return o;
			}

			float4 frag(v2f i) : SV_Target
			{
				float4 albedo = tex2D(_MainTex, i.uv) * _Color;

				// calculate direct diffuse lighting (Lambert)
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLight = normalize(UnityWorldSpaceLightDir(i.worldPos));
				float3 direct = _LightColor0.rgb * saturate(dot(worldNormal, worldLight));

				// calculate light attenuation and shadow
				UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

				return float4(albedo * direct * atten, 1.0);
			}

			ENDCG
		}
	}

	FallBack "Diffuse"
}

目前这个Shader只能接收场景中光源的直接光照明,能够处理平行光、点光源、聚光灯照明并产生阴影。Emission项只是加在最终颜色上,并不会对场景产生任何照明效果。

接受间接光照——使用内置函数

获取Lightmap
首先来让这个Shader能够从Lightmap中获取间接光照。要采样Lightmap,我们需要知道模型的Lightmap uv坐标。Unity默认会将模型的Baked Lightmap uv和Realtime Lightmap uv分别存放在appdata的TEXCOORD1和TEXCOORD2中,我们在forward base pass的appdata结构体中加入这两个坐标:

struct appdata
{
	float4 vertex : POSITION;
	float3 normal : NORMAL;
	float2 uv : TEXCOORD0;// main texture uv
	float2 lmUV : TEXCOORD1;// lightmap uv
	float2 dlmUV : TEXCOORD2;// dynamic lightmap uv
};

在v2f结构体中,同样加入这两个坐标(SHADOW_COORDS的offset相应后移):

struct v2f
{
	float4 pos : SV_POSITION;
	float2 uv : TEXCOORD0;
	float3 worldPos : TEXCOORD1;
	float3 worldNormal : TEXCOORD2;
	float2 lmUV : TEXCOORD3;
	float2 dlmUV : TEXCOORD4;
	SHADOW_COORDS(5)
};

在顶点着色器中,和Main Texture uv一样,我们要用uv tiling参数来对uv进行变换。这里使用unity_LightST和unity_DynamicLightmapST这两个变量来访问Baked Lightmap和Realtime Lightmap的uv tiling参数(和main texture的_MainTex_ST同理):

o.lmUV = v.lmUV * unity_LightST.xy + unity_LightST.zw;
o.dlmUV = v.dlmUV * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;

在片元着色器中,我们通过传入的lightmap uv采样并解码lightmap,并将光照颜色加到indirect项上。在最终颜色的计算中,不仅考虑直接光强度,将间接光照强度indirect也纳入计算:

// calculate indirect diffuse lighting
float3 indirect = float3(0.0, 0.0, 0.0);
#ifdef LIGHTMAP_ON
	float3 lmColor = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lmUV));
	indirect += lmColor;
#endif
#ifdef DYNAMICLIGHTMAP_ON
	float3 dlmColor = DecodeRealtimeLightmap(UNITY_SAMPLE_TEX2D(unity_DynamicLightmap, i.dlmUV));
	indirect += dlmColor;
#endif

return float4(_Emission + albedo * (direct * atten + indirect), 1.0);

需要注意的是,代码中使用宏LIGHTMAP_ON和DYNAMICLIGHTMAP_ON是否被定义来判断是否要采样lightmap,引擎会为我们根据应用层的设置自动定义(或不定义)这些宏,但前提是我们的Pass必须写了#pragma multi_compile_fwdbase的snippet语句。另外,lightmap采样的结果还需要经过函数DecodeLightmap和DecodeRealtimeLightmap的解码才能得到正确的颜色,这是因为不同平台对lightmap的储存格式是不同的,这两个函数帮我们封装了平台差异性的细节问题,详情可以参考这篇博客和这篇博客。
注意,对于间接光照的计算在整个着色过程中只需要计算一次即可,所以,以上采样lightmap的代码,以及下文获取Light Probe的代码,均是在forward base pass中做的
获取Light Probe
接下来我们让这个shader能够接受Light Probe的照亮。我们在顶点层面获取逐顶点的光照颜色,经过插值传入片元着色器并加在indirect项上。在v2f结构体中,加入:

float3 SHLighting : COLOR;

之前说过Light Probe使用球谐光照技术来储存光照信息,因此我们使用采样球谐光照的函数ShadeSH9来获取间接光颜色。这个函数接受一个四维向量作为输入,前三位是世界空间的法线方向,第四位为1。在顶点着色器中,加入代码:

o.SHLighting = ShadeSH9(float4(o.worldNormal, 1.0));

最后,在片元着色器中,加入代码:

#ifdef UNITY_SHOULD_SAMPLE_SH
	indirect += i.SHLighting;
#endif

和lightmap一样,我们使用宏UNITY_SHOULD_SAMPLE_SH进行了条件判断。

贡献间接光照——meta pass

经过以上改动,目前我们的shader已经能够接受Lightmap和Light Probe形式的间接光照了。但是还没有结束,因为它还不能 贡献 任何的间接光照。Unity提供了一个专门的LightMode标签来让我们做这件事,那就是"Meta"。我们在shader的最后加入一个LightMode为"Meta"的Pass,直接上代码:

// meta pass
Pass
{
	Name "Meta"
	Tags { "LightMode" = "Meta" }
	Cull Off

	CGPROGRAM
	
	#pragma vertex vert_meta
	#pragma fragment frag_meta

	#include "Lighting.cginc"
	#include "UnityMetaPass.cginc"

	sampler2D _MainTex;
	float4 _MainTex_ST;
	float4 _Color;
	float4 _Emission;

	struct appdata
	{
		float4 vertex : POSITION;
		float2 uv : TEXCOORD0;// main texture uv
		float2 lmUV : TEXCOORD1;// lightmap uv
		float2 dlmUV : TEXCOORD2;// dynamic lightmap uv
	};

	struct v2f
	{
		float4 pos : SV_POSITION;
		float2 uv : TEXCOORD0;
	};

	v2f vert_meta(appdata v)
	{
		v2f o = (v2f)o;
		o.pos = UnityMetaVertexPosition(v.vertex, v.lmUV, v.dlmUV, unity_LightmapST, unity_DynamicLightmapST);
		o.uv = TRANSFORM_TEX(v.uv, _MainTex);
		return o;
	}

	float4 frag_meta(v2f i) : SV_Target
	{
		UnityMetaInput metaIN = (UnityMetaInput)0;
		metaIN.Albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
		metaIN.Emission = _Emission;
		return UnityMetaFragment(metaIN);
	}

	ENDCG
}

这是一个很特殊的Pass,会在Enlighten烘焙过程中用于生成Lightmap。首先我们将pass name和LightMode标签设置为"Meta",并关闭剔除(Cull Off)。另外还需要引用"UnityMetaPass.cginc"文件,其中包含了一些我们需要在meta pass中使用的内置函数。
在顶点着色器中,我们调用了函数UnityMetaVertexPosition来获取裁剪空间坐标。这个函数需要五个参数,第一个是模型空间顶点坐标,第二、三个分别为顶点的lightmap uv和realtime lightmap uv,第四、五个分别为lightmap和realtime lightmap的tiling系数。
Unity的全局光照系统计算Lightmap需要我们提供表面的两个属性,那就是反照率Albedo和自发光Emission。在片元着色器中,我们需要将这两个属性装入结构体UnityMetaInput中,经过函数UnityMetaFragment编码成四维向量输出。
关于meta pass的更多细节,包括内置函数的具体实现,可以参考"UnityMetaPass.cginc"源码,也可以参考这篇博客。关于计算Lightmap的辐射度算法,可以参考这篇翻译文章。

结果

做完以上所有改动,就能够让我们的自定义vertex&fragment shader支持Unity全局光照系统了。完整代码如下:



Shader "Custom/Diffuse WIth GI"
{
	Properties
	{
		_MainTex("Main Texture", 2D) = "white" {}
		_Color("Color Tint", Color) = (0.0, 0.0, 0.0, 0.0)
		[HDR]_Emission("Emission", Color) = (0.0, 0.0, 0.0, 0.0)
	}
	SubShader
	{
		Tags { "RenderType" = "Opaque" }

		// forward base pass
		Pass
		{
			Tags{ "LightMode" = "ForwardBase" }
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile_fwdbase

			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			#include "AutoLight.cginc"

			sampler2D _MainTex;
			float4 _MainTex_ST;
			float4 _Color;
			float4 _Emission;

			struct appdata
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float2 uv : TEXCOORD0;// main texture uv
				float2 lmUV : TEXCOORD1;// lightmap uv
				float2 dlmUV : TEXCOORD2;// dynamic lightmap uv
			};

			struct v2f
			{
				float4 pos : SV_POSITION;
				float2 uv : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
				float3 worldNormal : TEXCOORD2;
				float2 lmUV : TEXCOORD3;
				float2 dlmUV : TEXCOORD4;
				float3 SHLighting : COLOR;
				SHADOW_COORDS(5)
			};

			v2f vert(appdata v)
			{
				v2f o = (v2f)0;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				o.worldNormal = UnityObjectToWorldNormal(v.normal);

				o.lmUV = v.lmUV * unity_LightST.xy + unity_LightST.zw;
				o.dlmUV = v.dlmUV * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;

				o.SHLighting = ShadeSH9(float4(o.worldNormal, 1.0));

				TRANSFER_SHADOW(o);
				return o;
			}

			float4 frag(v2f i) : SV_Target
			{
				float4 albedo = tex2D(_MainTex, i.uv) * _Color;

				// calculate direct diffuse lighting (Lambert)
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLight = normalize(UnityWorldSpaceLightDir(i.worldPos));
				float3 direct = _LightColor0.rgb * saturate(dot(worldNormal, worldLight));

				// calculate light attenuation and shadow
				UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

				// calculate indirect diffuse lighting
				float3 indirect = float3(0.0, 0.0, 0.0);
				#ifdef LIGHTMAP_ON
					float3 lmColor = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lmUV));
					indirect += lmColor;
				#endif
				#ifdef DYNAMICLIGHTMAP_ON
					float3 dlmColor = DecodeRealtimeLightmap(UNITY_SAMPLE_TEX2D(unity_DynamicLightmap, i.dlmUV));
					indirect += dlmColor;
				#endif
				#ifdef UNITY_SHOULD_SAMPLE_SH
					indirect += i.SHLighting;
				#endif

				return float4(_Emission + albedo * (direct * atten + indirect), 1.0);
			}


			ENDCG
		}

		// forward add pass
		Pass
		{
			Tags{ "LightMode" = "ForwardAdd" }
			Blend One One

			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile_fwdadd_fullshadows

			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			#include "AutoLight.cginc"

			sampler2D _MainTex;
			float4 _MainTex_ST;
			float4 _Color;

			struct appdata
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float2 uv : TEXCOORD0;// main texture uv
			};

			struct v2f
			{
				float4 pos : SV_POSITION;
				float2 uv : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
				float3 worldNormal : TEXCOORD2;
				SHADOW_COORDS(3)
			};

			v2f vert(appdata v)
			{
				v2f o = (v2f)0;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				o.worldNormal = UnityObjectToWorldNormal(v.normal);

				TRANSFER_SHADOW(o);
				return o;
			}

			float4 frag(v2f i) : SV_Target
			{
				float4 albedo = tex2D(_MainTex, i.uv) * _Color;

				// calculate direct diffuse lighting (Lambert)
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLight = normalize(UnityWorldSpaceLightDir(i.worldPos));
				float3 direct = _LightColor0.rgb * saturate(dot(worldNormal, worldLight));

				// calculate light attenuation and shadow
				UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

				return float4(albedo * direct * atten, 1.0);
			}

			ENDCG
		}
	}

	// meta pass
	Pass
	{
		Name "Meta"
		Tags { "LightMode" = "Meta" }
		Cull Off

		CGPROGRAM
		
		#pragma vertex vert_meta
		#pragma fragment frag_meta

		#include "Lighting.cginc"
		#include "UnityMetaPass.cginc"

		sampler2D _MainTex;
		float4 _MainTex_ST;
		float4 _Color;
		float4 _Emission;

		struct appdata
		{
			float4 vertex : POSITION;
			float2 uv : TEXCOORD0;// main texture uv
			float2 lmUV : TEXCOORD1;// lightmap uv
			float2 dlmUV : TEXCOORD2;// dynamic lightmap uv
		};

		struct v2f
		{
			float4 pos : SV_POSITION;
			float2 uv : TEXCOORD0;
		};

		v2f vert_meta(appdata v)
		{
			v2f o = (v2f)o;
			o.pos = UnityMetaVertexPosition(v.vertex, v.lmUV, v.dlmUV, unity_LightmapST, unity_DynamicLightmapST);
			o.uv = TRANSFORM_TEX(v.uv, _MainTex);
			return o;
		}

		float4 frag_meta(v2f i) : SV_Target
		{
			UnityMetaInput metaIN = (UnityMetaInput)0;
			metaIN.Albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
			metaIN.Emission = _Emission;
			return UnityMetaFragment(metaIN);
		}

		ENDCG
	}

	FallBack "Diffuse"
}

你可能感兴趣的:(图形学,Unity)