文章出處

 

【Unity Shaders】Shader中的光照,shadersshader


 

寫在前面

 

自己寫過Vertex & Fragment Shader的童鞋,大概都會對Unity的光照痛恨不已。當然,我相信這是因為我們寫得少。。。不過這也是由于官方文檔對這方面介紹很少的緣故,導致我們無法自如地處理很多常見的光照變量。這篇我們就來討論下Unity內置的一些光照變量和函數到底怎么用。

 

以下內容均建立在Forward Rendering Path的基礎上。

 

自己總結的,如果有硬傷一定要告訴我啊!感激不盡~

 

主要參考:

 

  • http://en.wikibooks.org/wiki/Cg_Programming/Unity/Multiple_Lights
  • http://docs.unity3d.com/Manual/RenderTech-ForwardRendering.html
  • http://docs.unity3d.com/Manual/SL-BuiltinIncludes.html
  • http://www.cnblogs.com/wonderKK/p/4031754.html

 

 

Forward Rendering Path的渲染細節

 

在開始后面的討論之前,先要弄懂一個問題就是Unity可以在Forward Rendering Path中可以處理哪些以及處理多少光照。這里只提取官方文檔中的一些內容加以說明。

 

在Forward Rendering中,有三種處理光照(即照亮物體)的方式:逐頂點處理,逐像素處理,球諧函數(Spherical Harmonics,SH)處理。而決定一個燈光是哪種處理模式取決于它的類型和模式:

 

  • 場景中最亮的平行光總是逐像素處理的。這意味著,如果場景里只有一個平行光,是否設置它的模式都無關緊要。
  • Render Mode被設置成Not Important的光源,會按逐頂點或者球諧函數處理。經試驗,第一點中的平行光不受這點的約束。
  • Render Mode被設置成Important的光源,會按逐像素處理。
  • 如根據以上規則得到的像素光源數量小于設置中的像素光源數量(Pixel Light Count),為了減少亮度,會有更多的光源以逐像素的方式進行渲染。
    • 這一點我沒有讀懂,按我的實驗結果是,如果所有的光源設置成Auto,那么逐像素光源的數目不會超過Pixel Light Count。但如果設置了Render Mode為明確的Not Important或者Important,那么設置Pixel Light Count似乎沒有任何影響。


 

那在哪里進行光照處理呢?當然是在Pass里。Forward Rendering有兩種Pass:Base Pass,Additional Passes。這兩種Pass的圖例說明如下:

 

注意其中的Per-Vertex Lights/SH Lights前面我標注了可選的,這是說,我們可以選擇是否處理這些光源。如果我們沒有在Base Pass中寫明相關的處理函數,那么這些光源實際上不會對物體產生影響。另一點就是其中橘黃色字表明的代碼,其中Tags我就不贅述了,這是基本要求。“#pragma multi_compile_fwdbase”這種在長久的實驗中表明最好是寫上它們,這會讓一些函數和宏可以正確工作,很可惜,現在官方沒有給出明確的文檔說明,因此我們還是乖乖地每次都加上它們比較好。最后,注意對于Forward Rendering來說,只有Bass Pass中處理的第一個平行光可以有陰影效果

 

從上面的圖中,我們已經知道,由于逐像素的光源是最重要的一種光源,因此Unity會花費一整個Pass來處理它。而對于逐頂點/SH光源來說,它們都將會在Bass Pass中處理(和最重要的平行光一起)。沒分量就是這種結果。那么,Base Pass會說,“我這么小就讓我做這么多東西,平行光就一個數量少就算了,SH光工作量少也算了,但頂點光也來搗亂我就不干了,不行!我得有條件!”于是Unity規定說,最多只有4個光源會按照逐頂點光源來處理,其他只能按SH光源處理。

 

這里很容易就弄混弄蒙了。我們先來看官方給的情況,即第一種情況:所有光源都被設置成Auto。這種情況下,Unity會自動為光源選擇合適的類型。這時,有一個項目設置很重要就是Pixel Light Count,它決定了逐像素光的最大數目。當Pixel Light Count為4時,就是那張著名的圖例情況(來自官方文檔):

 

 

上面的類型選擇過程大概是這樣的:首先,前Pixel Light Count(這里是4)個光源會按照逐像素進行處理,然后最多4個逐頂點光源,剩下的就是SH光了。其中,注意每種光源之間會有重疊的情況,這主要是為了防止物體移動時光照產生突變。

 

但是,如果光源沒有被設置為Auto,而是被指明是Important和Not Important,又會怎樣呢?(不要問我有的被設置成Auto,有的設置成Important會怎樣,你這人真討厭自己分析吧。。。)那么,第二種情況:自定義光源類型。首先,記住一點,這時不再受Pixel Light Count的限制,那么被設置成Important全部會被當成逐像素光源,一個不剩;如果被設置成Not Important,那么最多有4個光源會被當成逐頂點光源,其他就會被當做SH光源進行處理。

 

上面聽起來很復雜,其實就是個“物競天擇”的過程。我們可以想象,所有的光源都在爭搶更多的計算資源,都想讓自己成為最重要的逐像素光,再不濟點就逐頂點光,要是實在混的不好就只能當成SH光了。那么掙到了資源又怎么處理呢?對于逐像素光,它有一整個Pass的資源可以揮霍,而這里會涉及到各種光照變量和函數的使用,后面會講;對于逐頂點光和SH光來說,很可惜,Unity并沒有明確的文檔來告訴我們如何訪問它們,我們只能通過UnityShaderVariables.cginc中的變量聲明和Surface Shader的編譯結果來“揣測”用法。這也是后面講的內容。

 

吐槽時間:雖然文檔上這么寫,但實際過程中還是有很多莫名其妙的問題:

  • 奇葩情況一:我在4.6.1版本中,創建一個場景包含了1個平行光+4個點光源,如果使用的Shader沒有Additional Passes的定義話,那么4個點光源即便設置成Important,還是會被Unity當成逐頂點光源。
  • 奇葩情況二:如果只定義了Additional Passes,而沒有Base Pass的話,就更奇葩了,整個Pass感覺都沒有在工作,而得到的結果像是上次緩存之類的東西。總之,請一定要先定義Base Pass再定義Additional Passes。不要任性!
  • 其他更多奇葩等待你發現

 

 

光照變量和函數

 

在UnityShaderVariables.cginc文件中,我們可以找到Unity提供的和處理光照有關的變量:

 

CBUFFER_START(UnityLighting)

	#ifdef USING_DIRECTIONAL_LIGHT
	uniform fixed4 _WorldSpaceLightPos0;
	#else
	uniform float4 _WorldSpaceLightPos0;
	#endif

	uniform float4 _LightPositionRange; // xyz = pos, w = 1/range

	// Built-in uniforms for "vertex lights"
	float4 unity_4LightPosX0;	// x coordinates of the 4 light sources in world space
	float4 unity_4LightPosY0;	// y coordinates of the 4 light sources in world space
	float4 unity_4LightPosZ0;	// z coordinates of the 4 light sources in world space
	float4 unity_4LightAtten0;	// scale factors for attenuation with squared distance

	float4 unity_LightColor[8];	// array of the colors of the 4 light sources
	float4 unity_LightPosition[8];	// apparently is not always correctly set
	// x = -1
	// y = 1
	// z = quadratic attenuation
	// w = range^2
	float4 unity_LightAtten[8];	// apparently is not always correctly set
	float4 unity_SpotDirection[8];

	// SH lighting environment
	float4 unity_SHAr;
	float4 unity_SHAg;
	float4 unity_SHAb;
	float4 unity_SHBr;
	float4 unity_SHBg;
	float4 unity_SHBb;
	float4 unity_SHC;
CBUFFER_END

 


 

在UnityCG.cginc可以找到光照處理輔助函數:

 

// Computes world space light direction
inline float3 WorldSpaceLightDir( in float4 v );

// Computes object space light direction
inline float3 ObjSpaceLightDir( in float4 v );

// Computes world space view direction
inline float3 WorldSpaceViewDir( in float4 v );

// Computes object space view direction
inline float3 ObjSpaceViewDir( in float4 v );

float3 Shade4PointLights (
float4 lightPosX, float4 lightPosY, float4 lightPosZ,
float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,
float4 lightAttenSq,
float3 pos, float3 normal);

float3 ShadeVertexLights (float4 vertex, float3 normal);

// normal should be normalized, w=1.0
half3 ShadeSH9 (half4 normal);

 

下面我們來看下如何在兩種Pass中使用上面的變量和函數處理不同類型的光照。

 

 

一個基本的Shader

 

下面的討論主要建立在下面的代碼下,可以先掃一遍,這里不用細看。它主要計算了漫反射光照和高光反射光照,還示例了逐頂點光源和SH光源的計算等。

 

Shader "Light Test" {
    Properties {
        _Color ("Color", color) = (1.0,1.0,1.0,1.0)
    }
    SubShader {
    	Tags { "RenderType"="Opaque"}
    	
        Pass {
            Tags { "LightMode"="ForwardBase"}	// pass for 4 vertex lights, ambient light & first pixel light (directional light)
            
            CGPROGRAM
            // Apparently need to add this declaration 
            #pragma multi_compile_fwdbase	
            
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
			#include "AutoLight.cginc"
             
            uniform float4 _Color;
             
            struct vertexInput {
            	float4 vertex : POSITION;
            	float3 normal : NORMAL;
         	};
         	struct vertexOutput {
            	float4 pos : SV_POSITION;
            	float4 posWorld : TEXCOORD0;
            	float3 normalDir : TEXCOORD1;
            	float3 lightDir : TEXCOORD2;
            	float3 viewDir : TEXCOORD3;
            	float3 vertexLighting : TEXCOORD4;
            	LIGHTING_COORDS(5, 6)
         	};
			
            vertexOutput vert(vertexInput input) {
                vertexOutput output;
                
                output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
               	output.posWorld = mul(_Object2World, input.vertex);
                output.normalDir =  normalize(mul(float4(input.normal, 0.0), _World2Object).xyz);
				output.lightDir = WorldSpaceLightDir(input.vertex);
				output.viewDir = WorldSpaceViewDir(input.vertex);
				output.vertexLighting = float3(0.0);
				
				 // SH/ambient and vertex lights
  				#ifdef LIGHTMAP_OFF
				float3 shLight = ShadeSH9 (float4(output.normalDir, 1.0));
				output.vertexLighting = shLight;
				#ifdef VERTEXLIGHT_ON
				float3 vertexLight = Shade4PointLights (
					unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
				    unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
				    unity_4LightAtten0, output.posWorld, output.normalDir);
				output.vertexLighting += vertexLight;
				#endif // VERTEXLIGHT_ON
  				#endif // LIGHTMAP_OFF
				
				// pass lighting information to pixel shader
  				TRANSFER_VERTEX_TO_FRAGMENT(output);
  
                return output;
            }
             
            float4 frag(vertexOutput input):COLOR{
                float3 normalDirection = normalize(input.normalDir); 
            	float3 viewDirection = normalize(_WorldSpaceCameraPos - input.posWorld.xyz);
            	float3 lightDirection;
           		float attenuation;
 
            	if (0.0 == _WorldSpaceLightPos0.w) // directional light?
       			{
               		attenuation = 1.0; // no attenuation
               		lightDirection = normalize(_WorldSpaceLightPos0.xyz);
            	} 
            	else // point or spot light
            	{
               		float3 vertexToLightSource =  _WorldSpaceLightPos0.xyz - input.posWorld.xyz;
               		float distance = length(vertexToLightSource);
               		attenuation = 1.0 / distance; // linear attenuation 
               		lightDirection = normalize(vertexToLightSource);
            	}
                      
                // LIGHT_ATTENUATION not only compute attenuation, but also shadow infos
//                attenuation = LIGHT_ATTENUATION(input);
                // Compare to directions computed from vertex
//				viewDirection = normalize(input.viewDir);
//				lightDirection = normalize(input.lightDir);
                
                // Because SH lights contain ambient, we don't need to add it to the final result
                float3 ambientLighting = UNITY_LIGHTMODEL_AMBIENT.xyz;
                 
                float3 diffuseReflection = attenuation * _LightColor0.rgb * _Color.rgb * max(0.0, dot(normalDirection, lightDirection)) * 2;
                
                float3 specularReflection;
                if (dot(normalDirection, lightDirection) < 0.0)  // light source on the wrong side?
	            {
	               	specularReflection = float3(0.0, 0.0, 0.0);  // no specular reflection
	            }
	            else // light source on the right side
	            {
	               	specularReflection = attenuation * _LightColor0.rgb * _Color.rgb * pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)), 255);
	            }
                
                return float4(input.vertexLighting +  diffuseReflection + specularReflection, 1.0);  
            }               
            ENDCG
        }
        
        Pass{
            Tags { "LightMode"="ForwardAdd"}		// pass for additional light sources
            ZWrite Off Blend One One Fog { Color (0,0,0,0) }	// additive blending
            
            CGPROGRAM
            // Apparently need to add this declaration
            #pragma multi_compile_fwdadd
            
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"
			#include "AutoLight.cginc"
             
            uniform float4 _Color;
             
            struct vertexInput {
            	float4 vertex : POSITION;
            	float3 normal : NORMAL;
         	};
         	struct vertexOutput {
            	float4 pos : SV_POSITION;
            	float4 posWorld : TEXCOORD0;
            	float3 normalDir : TEXCOORD1;
            	float3 lightDir : TEXCOORD2;
            	float3 viewDir : TEXCOORD3;
            	LIGHTING_COORDS(4, 5)
         	};
             
            vertexOutput vert(vertexInput input) {
                vertexOutput output;
                
                output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
               	output.posWorld = mul(_Object2World, input.vertex);
                output.normalDir =  normalize(mul(float4(input.normal, 0.0), _World2Object).xyz);
				output.lightDir = WorldSpaceLightDir(input.vertex);
				output.viewDir = WorldSpaceViewDir(input.vertex);
				
				// pass lighting information to pixel shader
				vertexInput v = input;
  				TRANSFER_VERTEX_TO_FRAGMENT(output);
  
                return output;
            }
             
            float4 frag(vertexOutput input):COLOR{
                float3 normalDirection = normalize(input.normalDir); 
            	float3 viewDirection = normalize(_WorldSpaceCameraPos - input.posWorld.xyz);
            	float3 lightDirection;
           		float attenuation;
 
            	if (0.0 == _WorldSpaceLightPos0.w) // directional light?
       			{
               		attenuation = 1.0; // no attenuation
               		lightDirection = normalize(_WorldSpaceLightPos0.xyz);
            	} 
            	else // point or spot light
            	{
               		float3 vertexToLightSource =  _WorldSpaceLightPos0.xyz - input.posWorld.xyz;
               		float distance = length(vertexToLightSource);
               		attenuation = 1.0 / distance; // linear attenuation 
               		lightDirection = normalize(vertexToLightSource);
            	}
                      
                // LIGHT_ATTENUATION not only compute attenuation, but also shadow infos
//                attenuation = LIGHT_ATTENUATION(input);
                // Compare to directions computed from vertex
//				viewDirection = normalize(input.viewDir);
//				lightDirection = normalize(input.lightDir);
               	
                float3 diffuseReflection = attenuation * _LightColor0.rgb * _Color.rgb * max(0.0, dot(normalDirection, lightDirection)) * 2;
                
                float3 specularReflection;
                if (dot(normalDirection, lightDirection) < 0.0)  // light source on the wrong side?
	            {
	               	specularReflection = float3(0.0, 0.0, 0.0);  // no specular reflection
	            }
	            else // light source on the right side
	            {
	               	specularReflection = attenuation * _LightColor0.rgb * _Color.rgb * pow(max(0.0, dot(reflect(-lightDirection, normalDirection), viewDirection)), 255);
	            }
                
                return float4(diffuseReflection + specularReflection, 1.0);  
            }              
            ENDCG
        }
    } 
    FallBack "Diffuse"
}

 

 

Base Pass

 

回想一下,上面我們說過在Bass Pass中,我們可以處理全部三種光照:處理第一個平行光作為逐像素光處理,處理所有的逐頂點光,處理其他所有SH光。還有很重要的一點就是,我們還要處理環境光、陰影等。一句話,由于Additional Passes只能處理逐像素光,如果你想要其他光照效果,都需要在Bass Pass中處理。

 

 

環境光

 

這里的環境光指的是我們在Edit->Render Setting里面的Ambient Light的值。在Shader中獲取它很容易,只需要訪問全局變量UNITY_LIGHTMODEL_AMBIENT即可。它是全局變量,因此在在哪個Pass里訪問都可以,但環境光只需要加一次即可,因此我們只需要在Bass Pass中疊加到其他顏色上即可。

 

 

陰影和光照衰減

 

Base Pass還有一個非常重要的作用就是添加陰影。上面提到過,對于Forward Rendering來說,只有Bass Pass中處理的第一個平行光可以有陰影效果。也就是說,錯過了這里就不會得到陰影信息了。程序中模擬陰影主要是依靠一張Shadow Map,里面記錄了從光源出發距離它最近的深度信息。Unity很貼心地提供了這樣的一張紋理(_ShadowMapTexture),不用我們自己再編程實現了。

 

與陰影的實現類似,Unity還提供了一張紋理(_LightTexture0),這張紋理包含了光照衰減(attenuation)。

 

由于陰影和光照衰減都是對紋理進行采樣,然后將結果乘以顏色值,因此Unity把這兩步合并到一個宏中,讓我們通過一個宏調用就可以解決這兩個問題。既然是對紋理采樣,那么首先就要知道頂點對應的紋理坐標,Unity同樣是通過宏來輔助我們完成的,我們只需要在v2f(vertexOutput)中添加關于宏LIGHTING_COORDS即可。然后,為了計算頂點對應的兩張紋理上的坐標,需要在vert函數里面調用一個新的宏:TRANSFER_VERTEX_TO_FRAGMENT。

 

這個過程中使用的宏定義都在AutoLight.cginc文件中。

 

一個完整的過程如下:

 

Unity就是使用了這三個宏來完成陰影和衰減的計算的。我們來看一下這三個宏到底是個什么東東。這里僅以不開啟cookie的平行光和點光源為例:
#ifdef POINT
#define LIGHTING_COORDS(idx1,idx2) float3 _LightCoord : TEXCOORD##idx1; SHADOW_COORDS(idx2)
uniform sampler2D _LightTexture0;
uniform float4x4 _LightMatrix0;
#define TRANSFER_VERTEX_TO_FRAGMENT(a) a._LightCoord = mul(_LightMatrix0, mul(_Object2World, v.vertex)).xyz; TRANSFER_SHADOW(a)
#define LIGHT_ATTENUATION(a)	(tex2D(_LightTexture0, dot(a._LightCoord,a._LightCoord).rr).UNITY_ATTEN_CHANNEL * SHADOW_ATTENUATION(a))
#endif

#ifdef DIRECTIONAL
	#define LIGHTING_COORDS(idx1,idx2) SHADOW_COORDS(idx1)
	#define TRANSFER_VERTEX_TO_FRAGMENT(a) TRANSFER_SHADOW(a)
	#define LIGHT_ATTENUATION(a)	SHADOW_ATTENUATION(a)
#endif

#define SHADOW_COORDS(idx1) float4 _ShadowCoord : TEXCOORD##idx1;
#define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_World2Shadow[0], mul(_Object2World,v.vertex));
#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)

可以發現,對于點光源來說,會計算兩種紋理,即光照衰減紋理和陰影紋理,并在最后計算attenuation的時候,就是將兩種紋理的采樣結果相乘。而對于平行光來說更加簡單,由于平行光沒有衰減,因此只需要計算陰影紋理就可以了。
再次強調以下,Forward Rendering來說,只有Bass Pass中處理的第一個平行光可以有陰影效果。例如,下面左圖中的平行光可以投射出陰影,而右圖中即便小球在光源和小蘋果的中間也不會產生任何陰影: 



逐頂點光照


其實逐頂點光照就是一個名字,Unity把這些所謂的“逐頂點光照”的數據存儲在一些變量中,我們完全可以按逐像素的方式來處理它們。當然,處于性能的考慮,我們通常還是會在頂點函數階段處理它們,因此把它們稱為逐頂點光照。
逐頂點光照涉及的變量和函數有兩組。這里的組別主要是依靠Unity提供的頂點光照計算函數使用的變量來歸類的。
第一組如下:
   uniform float4 unity_4LightPosX0; // x coordinates of the 4 light sources in world space
   uniform float4 unity_4LightPosY0; // y coordinates of the 4 light sources in world space
   uniform float4 unity_4LightPosZ0; // z coordinates of the 4 light sources in world space
   uniform float4 unity_4LightAtten0; // scale factors for attenuation with squared distance

對應的函數如下:
float3 Shade4PointLights (
	float4 lightPosX, float4 lightPosY, float4 lightPosZ,
	float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,
	float4 lightAttenSq,
	float3 pos, float3 normal)
{
	// to light vectors
	float4 toLightX = lightPosX - pos.x;
	float4 toLightY = lightPosY - pos.y;
	float4 toLightZ = lightPosZ - pos.z;
	// squared lengths
	float4 lengthSq = 0;
	lengthSq += toLightX * toLightX;
	lengthSq += toLightY * toLightY;
	lengthSq += toLightZ * toLightZ;
	// NdotL
	float4 ndotl = 0;
	ndotl += toLightX * normal.x;
	ndotl += toLightY * normal.y;
	ndotl += toLightZ * normal.z;
	// correct NdotL
	float4 corr = rsqrt(lengthSq);
	ndotl = max (float4(0,0,0,0), ndotl * corr);
	// attenuation
	float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);
	float4 diff = ndotl * atten;
	// final color
	float3 col = 0;
	col += lightColor0 * diff.x;
	col += lightColor1 * diff.y;
	col += lightColor2 * diff.z;
	col += lightColor3 * diff.w;
	return col;
}

調用的話代碼如下:
				float3 vertexLight = Shade4PointLights (
					unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
				    unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
				    unity_4LightAtten0, output.posWorld, output.normalDir);

注意其中頂點位置和法線方向都是指在世界坐標系下的。

第二組變量:
	float4 unity_LightPosition[8];	// apparently is not always correctly set
	// x = -1
	// y = 1
	// z = quadratic attenuation
	// w = range^2
	float4 unity_LightAtten[8];	// apparently is not always correctly set
	float4 unity_SpotDirection[8];

函數:
float3 ShadeVertexLights (float4 vertex, float3 normal)
{
	float3 viewpos = mul (UNITY_MATRIX_MV, vertex).xyz;
	float3 viewN = mul ((float3x3)UNITY_MATRIX_IT_MV, normal);
	float3 lightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;
	for (int i = 0; i < 4; i++) {
		float3 toLight = unity_LightPosition[i].xyz - viewpos.xyz * unity_LightPosition[i].w;
		float lengthSq = dot(toLight, toLight);
		float atten = 1.0 / (1.0 + lengthSq * unity_LightAtten[i].z);
		float diff = max (0, dot (viewN, normalize(toLight)));
		lightColor += unity_LightColor[i].rgb * (diff * atten);
	}
	return lightColor;
}

用法:
vertexLight = ShadeVertexLights(input.vertex, input.normal)

注意其中的頂點坐標和法線方向是在對象坐標系下的。而且,其計算結果包含了環境光。。。
這兩組函數看起來做了一樣的工作,但其實Forward Rendering我們只可以選擇第一組。下面是官方文檔中的解釋:

Forward rendering helper functions in UnityCG.cginc

These functions are only useful when using forward rendering (ForwardBase or ForwardAdd pass types).

  • float3 Shade4PointLights (...) - computes illumination from four point lights, with light data tightly packed into vectors. Forward rendering uses this to compute per-vertex lighting.

Vertex-lit helper functions in UnityCG.cginc

These functions are only useful when using per-vertex lit shaders (“Vertex” pass type).

  • float3 ShadeVertexLights (float4 vertex, float3 normal) - computes illumination from four per-vertex lights and ambient, given object space position & normal.
文檔里說的很清楚,對于Forward Rendering來說,我們應該使用Shade4PointLights來計算最多四個逐頂點光照,而且只能計算Point Lights和Spot Lights,如果一個平行光被設置成逐頂點光源,那么是不會被計算的。換句話說,我們應該使用unity_4LightPosX0、unity_4LightPosY0、unity_4LightPosZ0、unity_4LightAtten0這些數據來訪問逐頂點的光源數據。而另一組是在Vertex Pass(e.g. Tags { "LightMode"="Vertex"})中使用的。
還有有一些需要我們了解的地方
  • Unity給出的函數只是為了方便我們提供的一種計算方法,可以看出來Shade4PointLights中,只是按逐頂點的方法(即只需在vert函數中提供頂點位置和法線)計算了漫反射方向的光照,但我們也完全可以自己根據這些光照變量處理逐頂點光源,例如添加高光反射等等。
  • 我們甚至還可以按照逐像素的方式來處理它們,即在frag函數里訪問并計算它們。只要你愿意,沒有什么可以阻止你這么做。(就是這么任性。)


好啦,說完了理論我們來看下視覺效果是怎樣的。我們在場景里放了一個小蘋果+一個球,并且放了四個不同顏色的點光源,只輸出Shade4PointLights的結果如下(左圖為逐頂點光照,右圖為逐像素光照): 


可以看出來,逐頂點光源從視覺效果上不如逐像素光源,但性能更好。
那么,還有一個問題,即支持計算的逐頂點光源數目最多為4個,定義的存儲逐頂點光源信息的變量數組也只有4維。也就是說,如果場景里被設置(或者排序后得到的數目)成逐頂點光源的數目大于4個,那么Unity會對它們進行排序,把其中最重要的4個光源存儲到那些變量中。但這種排序方法Unity沒有文檔進行說明,而從實驗結果來看,這個排序結果和光的顏色、密度、距離都有關。例如,如果我們再加一個藍色光源,可以發現不會對結果有任何變化:


而如果我們調整它的顏色、密度、或者位置時,由于排序結果發生變化,就會生成光照突變(左圖為改變顏色,右圖為改變密度):  


 

 

 

SH光照

 

那些既不是逐像素光又不是逐頂點光的光源,如果想對物體產生影響,就只能按SH光照進行處理。宮斗失敗就是這個結果。Unity里和計算SH光有關的變量和函數如下:

 

	// SH lighting environment
	float4 unity_SHAr;
	float4 unity_SHAg;
	float4 unity_SHAb;
	float4 unity_SHBr;
	float4 unity_SHBg;
	float4 unity_SHBb;
	float4 unity_SHC;

 

 

// normal should be normalized, w=1.0
half3 ShadeSH9 (half4 normal)
{
	half3 x1, x2, x3;
	
	// Linear + constant polynomial terms
	x1.r = dot(unity_SHAr,normal);
	x1.g = dot(unity_SHAg,normal);
	x1.b = dot(unity_SHAb,normal);
	
	// 4 of the quadratic polynomials
	half4 vB = normal.xyzz * normal.yzzx;
	x2.r = dot(unity_SHBr,vB);
	x2.g = dot(unity_SHBg,vB);
	x2.b = dot(unity_SHBb,vB);
	
	// Final quadratic polynomial
	float vC = normal.x*normal.x - normal.y*normal.y;
	x3 = unity_SHC.rgb * vC;
    return x1 + x2 + x3;
} 

調用代碼如下:

 

float3 shLight = ShadeSH9 (float4(output.normalDir, 1.0));

關于SH光照的實現細節我沒有研究,有興趣的可以查資料理解下上面函數的含義。之前有網友留言告訴我一篇文章。但太長了我沒看。。。還有論壇中的一個帖子,可以看看里面的代碼初步了解一下。

 

 

我們以之前的例子為例,看一下只輸出SH光照的結果。下面左圖中,是只有四個光源的情況,可以看出此時并沒有任何SH光,這是因為這四個光源此時被當做是逐頂點光照。這里物體顏色非黑是因為unity_SHAr、unity_SHAg、unity_SHAb包含了環境光數據,而非真正的光照造成的,因此理論上只要包含了計算SH光照的代碼就不需要在最后結果上添加上面提到的環境光了。右圖則是增加了4個新的Not Important光源后的SH光照結果。

 

 

我們將逐頂點光照和SH光照結合在一起,代碼如下:

 

				 // SH/ambient and vertex lights
  				#ifdef LIGHTMAP_OFF
				float3 shLight = ShadeSH9 (float4(output.normalDir, 1.0));
				output.vertexLighting = shLight;
				#ifdef VERTEXLIGHT_ON
				float3 vertexLight = Shade4PointLights (
					unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
				    unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
				    unity_4LightAtten0, output.posWorld, output.normalDir);
				output.vertexLighting += vertexLight;
				#endif // VERTEXLIGHT_ON
  				#endif // LIGHTMAP_OFF

其中,需要添加#ifdef這些聲明是為了保證,在Unity不提供這些數據時可以不用計算這些光照。

 

 

我們把兩者相加的結果輸出,可以得到以下的結果:

 

 

 

Additional Passes

 

最后,我們來談談Additional Passes中的逐像素光。我們需要知道的是,其實在Base Pass中我們也需要處理逐像素光,但我們可以明確的知道這個逐像素光只能是第一個平行光。而在Additional Passes中,逐像素光可能是平行光、點光源、聚光燈光源(Spot Light)。這里不討論使用了LightMap或者開啟了Cookie的情況。

 

同樣,這里的逐像素光其實也只是一個名字,Unity只是負責把所謂的逐像素光的數據放到一些變量中,但是,沒有什么可以阻止我們是在vert中計算還是在frag中計算。

 

注意:想要Additional Passes是疊加在Bass Pass上的話(一般人的目的都是這個),請確保你給Pass添加了合適的混合模式。例如:

 

        Pass{
            Tags { "LightMode"="ForwardAdd"}		// pass for additional light sources
            ZWrite Off Blend One One Fog { Color (0,0,0,0) }	// additive blending


 

對于逐像素光照,我們最長使用的變量和函數如下:

 

來自UnityShaderVariables.cginc:

 

uniform float4 _WorldSpaceLightPos0;
uniform float3 _WorldSpaceCameraPos;

 

 

來自Lighting.cginc:

fixed4 _LightColor0;

 

來自UnityCG.cginc(文檔說明):
// Computes world space light direction
inline float3 WorldSpaceLightDir( in float4 v );
// Computes object space light direction
inline float3 ObjSpaceLightDir( in float4 v );
// Computes world space view direction
inline float3 WorldSpaceViewDir( in float4 v );
// Computes object space view direction
inline float3 ObjSpaceViewDir( in float4 v );

可以發現,只有函數給出了明確的文檔說明,其他都只能靠Unity內部Shader的結構來揣測了。

 

我們先不管這些變量和函數,先來想想我們到底想利用逐像素光照來計算什么,在哪里計算。最常見的需求就是計算光源方向和視角方向,然后再進行漫反射和高光反射的計算。在Unity里在哪里計算這些方向似乎從視覺上沒有太大的區別,理論上在vert中計算比在frag中計算更快一點。但計算位置的選擇決定了我們可以如何使用上面的變量和函數。

 

可以注意到,Unity提供的函數都是在vert函數中的輔助函數,即都是只需要提供頂點位置就可以得到光照方向和視角方向的。也就是說,如果我們想要在vert函數中就計算各個方向的值,可以這么做:

 

				output.lightDir = WorldSpaceLightDir(input.vertex);
				output.viewDir = WorldSpaceViewDir(input.vertex);

當然,上面是得到世界坐標系下的用法,我們也可以得到對象坐標系下的,看需求即可。這些函數其實也是利用了_WorldSpaceLightPos0和_WorldSpaceCameraPos而已。例如WorldSpaceLightDir的定義如下:

 

 

// Computes world space light direction
inline float3 WorldSpaceLightDir( in float4 v )
{
	float3 worldPos = mul(_Object2World, v).xyz;
	#ifndef USING_LIGHT_MULTI_COMPILE
		return _WorldSpaceLightPos0.xyz - worldPos * _WorldSpaceLightPos0.w;
	#else
		#ifndef USING_DIRECTIONAL_LIGHT
		return _WorldSpaceLightPos0.xyz - worldPos;
		#else
		return _WorldSpaceLightPos0.xyz;
		#endif
	#endif
}

其中,由于平行光的方向不隨頂點位置發生變化,因此直接使用_WorldSpaceLightPos0.xyz即可,此時里面存儲的其實就是平行光的方向,而非位置。同時,_WorldSpaceLightPos0.w可以表明該光源的類型,如果為0表示是平行光,為1表示是點光源或者聚光燈光源。因此,我們常常可以看到類似下面的代碼:

 

 

            	if (0.0 == _WorldSpaceLightPos0.w) // directional light?
       			{
               		attenuation = 1.0; // no attenuation
               		lightDirection = normalize(_WorldSpaceLightPos0.xyz);
            	} 
            	else // point or spot light
            	{
               		float3 vertexToLightSource =  _WorldSpaceLightPos0.xyz - input.posWorld.xyz;
               		lightDirection = normalize(vertexToLightSource);
            	}

其實是和WorldSpaceLightDir函數的意義是一樣的。

 

 

_LightColor0就沒什么可說的了,就是存儲了該逐像素光的顏色。

 

 

寫在最后

 

今天就到這里。

 

 

 

 

 

 

 
 
 

 

 

文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()