前些天烘焙lightmap的時候發現用自己寫的Shader的模型在烘焙時候不會烘焙效果不對,它不會產生對周圍物體的間接光照,但是我放到了unity4.x中就是沒問題的。查了一番,發現Unity5中加了一個MetaPass的東西。大家可以自己去看下。
要想搞清楚為啥需要MetaPass,只看Unity是不行的,所以本文中還會結合着去分析分析Enlighten的工作原理。
什么是MetaPass
加入MetaPass的原因就是因為Unity5把烘焙系統從Beast換成了Enlighten。看一下metapass的流程:
(圖1:metapass flow圖片來源:Unity官方文檔)
從圖中可以看到,enlighten需要Unity提供材質的Albdeo(反射率)和自發光(emissive)的紋理,從而用來計算間接光照。而這兩個貼圖都是Unity自己在GPU上渲染得到的。既然需要GPU渲染,那就需要用提供一個相應的Pass來專門讓Unity用來進行這種渲染。
這與3.x和4.x版本的Unity是不同的,在這些版本中烘焙器(姑且叫這個名字吧),從材質Shader上獲取一些烘焙時使用的信息是要通過檢測材質屬性的名字(也就是你在Shader的Properties塊里的內容)來完成的。具體可以參考響應版本文檔中的Lightmapping In-Depth一節。
那么為什么Unity自帶的Shader在烘焙的時候沒問題呢,是因為內置的Shader都是SurfaceShader.而SurfaceShader實際上最后都會被Unity轉化成V&F Shader。在轉化的這個過程中,Unity給他們添加上了metaPass(可以在#pragma 中加上nometa讓響應SurfaceShader不產生metapass)。如果對Surface Shader如何轉換成V&F Shader感興趣,可以看一下《UnityShader入門精要》的第17章。
如果你手頭有5.x版本的內置Shader源代碼,不妨放到Unity中一個,然后在Inspector面板中點擊show generated code查看轉換好的V&F代碼。代碼很長,metaPass一般在最后。
前面說了,metaPass主要是Unity用來計算Albedo和emissive,然后提供給enlighten用的。
原理解釋
至於為什么需要這兩個值,其實很好理解,設想一下一個物體要相對周圍物體產生光照影響,無非兩種情況:
1. 作為發光體,直接將光線投射到其它物體上,對應着上面的emissive。
2. 光線照射到該物體上然后反射(可能經過多次)到周圍物體上,最后被后者反射到人眼中。而當要計算前者能反射多少,以及哪些成分的光線到后者身上就要用到Albedo。
可以看一下Enlighten官方Blog給出的Radiosity Equation公式:
(圖2:Radiosity Equation 圖片來源:Enlighten官方Blog)
對於全局光照的處理,目前有兩種主流算法,光線追蹤和輻射度算法。Enlighten所使用的即是這種。上圖中的公式實際上是對RenderEquation的一種簡化變形,RenderEquation是一種理想模型,也是目前所有光照處理的理論基礎,詳情可以自行wiki。
上面的模型中實際上把一個像素點的受光情況(這里只考慮間接光照)分成了自發光Le和 來自其它光源的間接光照。其中Pi是材質屬性,這里我們可以簡單的理解成Albedo反射率,這反應了改點對應的材質對不同波段光的反射能力。那上面公式中后面的一團就不難理解了,實際上就是對從各個方向收集到的反射光的和最后乘上一個材質反射能力,從而得到最后的實際光照結果。此處只是個人理解,可以去Enlighten的Blog自己看,(網上能搜到一篇中文翻譯,翻譯的一般)
好了這下子我們就應該能理解為什么在圖1所示的metapass flow里enlighten需要Unity給它提供Albedo和emission紋理了。
代碼分析
我們來直接看一下代碼:
1 Pass 2 { 3 Name "Meta" 4 Tags {"LightMode" = "Meta"} 5 Cull Off 6 7 CGPROGRAM 8 #pragma vertex vert_meta 9 #pragma fragment frag_meta 10 11 #include "Lighting.cginc" 12 #include "UnityMetaPass.cginc" 13 14 struct v2f 15 { 16 float4 pos:SV_POSITION; 17 float2 uv:TEXCOORD1; 18 float3 worldPos:TEXCOORD0; 19 }; 20 21 uniform fixed4 _Color; 22 uniform sampler2D _MainTex; 23 v2f vert_meta(appdata_full v) 24 { 25 v2f o; 26 UNITY_INITIALIZE_OUTPUT(v2f,o); 27 o.pos = UnityMetaVertexPosition(v.vertex,v.texcoord1.xy,v.texcoord2.xy,unity_LightmapST,unity_DynamicLightmapST); 28 o.uv = v.texcoord.xy; 29 return o; 30 } 31 32 fixed4 frag_meta(v2f IN):SV_Target 33 { 34 UnityMetaInput metaIN; 35 UNITY_INITIALIZE_OUTPUT(UnityMetaInput,metaIN); 36 metaIN.Albedo = tex2D(_MainTex,IN.uv).rgb * _Color.rgb; 37 metaIN.Emission = 0; 38 return UnityMetaFragment(metaIN); 39 } 40 41 ENDCG 42 }
上面代碼中是我寫的最簡化后的代碼。最開始的LightMode的Tag是必須寫的,Unity要通過它來找到MetaPass。Unity文檔中有比較完整的代碼。_Color和_MainTex是在Properties里聲明的貼圖和調和顏色。后面會把他倆相乘作為Albedo的結果,這也正是我們在正常的光照處理里所做的。
上面的代碼比較簡單,有幾個地方需要說明一下:
1. 首先是Unity_INITIALIZE_OUTPUT 【HLSL.cginc中定義】:
1 // Initialize arbitrary structure with zero values. 2 // Not supported on some backends (e.g. Cg-based like PS3 and particularly with nested structs). 3 // hlsl2glsl would almost support it, except with structs that have arrays -- so treat as not supported there either :( 4 #if defined(UNITY_COMPILER_HLSL) || defined(SHADER_API_PSSL) || defined(SHADER_API_GLES3) || defined(SHADER_API_GLCORE) 5 #define UNITY_INITIALIZE_OUTPUT(type,name) name = (type)0; 6 #else 7 #define UNITY_INITIALIZE_OUTPUT(type,name) 8 #endif
很簡單,就是一個清零。當頂點着色器函數返回結果時候,如果要返回的結構體沒有全部賦值過,那么Unity會報錯,必須全部賦值。而這個宏就是用來清零的,省着手動賦0.但並不是所有的着色語言都支持。有些情況下必須手動賦值。
2.UnityMetaVertexPosition(v.vertex,v.texcoord1.xy,v.texcoord2.xy,unity_LightmapST,unity_DynamicLightmapST)[UnityMetaPass.cginc中定義]:
1 float4 UnityMetaVertexPosition (float4 vertex, float2 uv1, float2 uv2, float4 lightmapST, float4 dynlightmapST) 2 { 3 if (unity_MetaVertexControl.x) 4 { 5 vertex.xy = uv1 * lightmapST.xy + lightmapST.zw; 6 // OpenGL right now needs to actually use incoming vertex position, 7 // so use it in a very dummy way 8 vertex.z = vertex.z > 0 ? 1.0e-4f : 0.0f; 9 } 10 if (unity_MetaVertexControl.y) 11 { 12 vertex.xy = uv2 * dynlightmapST.xy + dynlightmapST.zw; 13 // OpenGL right now needs to actually use incoming vertex position, 14 // so use it in a very dummy way 15 vertex.z = vertex.z > 0 ? 1.0e-4f : 0.0f; 16 } 17 return UnityObjectToClipPos(vertex); 18 }
參數中的uv1,uv2分別是模型在靜態光照貼圖(static lightmap)和動態光照貼圖(實時GI)中的uv坐標。lightmapST和dynlightmapST的值應該是經過特殊構造以用來確定其在烘焙空間中的位置。
上面代碼中有兩個if判斷,這個unity_MetaVertexControl是個什么東西呢?
在UnityMetaPass.cginc中找到了定義如下:
1 CBUFFER_START(UnityMetaPass) 2 // x = use uv1 as raster position 3 // y = use uv2 as raster position 4 bool4 unity_MetaVertexControl; 5 6 // x = return albedo 7 // y = return normal 8 bool4 unity_MetaFragmentControl; 9 CBUFFER_END
上面的CBUFFER_START 宏是和DX11的constant Buffer有關的,注意由於Unity對DX11的支持,導致了UnityShaderLab中有很多為了處理DX11的新添加的宏。這不是我們要討論的核心問題,有興趣可以自己研究下。
從上面注釋中看到unity_MetaVertexControl的xy變量指定了我們要處理的是靜態光照貼圖還是動態光照貼圖。而這兩個具體的值應該是Unity引擎自己在烘焙時候根據烘焙的配置來設置的。比如你只選擇了BakedGI,那么x=true,y=false.if里的具體語句就是計算出模型頂點在lightmap空間的位置。
至於最后的UnityObjectToClipPos(vertex)[定義在UnityCG.cginc]:
1 // Tranforms position from object to homogenous space 2 inline float4 UnityObjectToClipPos( in float3 pos ) 3 { 4 #ifdef UNITY_USE_PREMULTIPLIED_MATRICES 5 return mul(UNITY_MATRIX_MVP, float4(pos, 1.0)); 6 #else 7 // More efficient than computing M*VP matrix product 8 return mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(pos, 1.0))); 9 #endif 10 } 11 inline float4 UnityObjectToClipPos(float4 pos) // overload for float4; avoids "implicit truncation" warning for existing shaders 12 { 13 return UnityObjectToClipPos(pos.xyz); 14 }
實際上只是做了模型空間到齊次空間的轉換。
3.UnityMetaInput[UnityMetaPass.cginc定義]:
1 struct UnityMetaInput 2 { 3 half3 Albedo; 4 half3 Emission; 5 };
很簡單就是我們之前提到的兩個結果值。在之前的代碼里我們直接像下面這樣計算了。
1 metaIN.Albedo = tex2D(_MainTex,IN.uv).rgb * _Color.rgb; 2 metaIN.Emission = 0;
4:UnityMetaFragment(metaIN)[UnityMetaPass.cginc中定義]:
1 half4 UnityMetaFragment (UnityMetaInput IN) 2 { 3 half4 res = 0; 4 if (unity_MetaFragmentControl.x) 5 { 6 res = half4(IN.Albedo,1); 7 8 // d3d9 shader compiler doesn't like NaNs and infinity. 9 unity_OneOverOutputBoost = saturate(unity_OneOverOutputBoost); 10 11 // Apply Albedo Boost from LightmapSettings. 12 res.rgb = clamp(pow(res.rgb, unity_OneOverOutputBoost), 0, unity_MaxOutputValue); 13 } 14 if (unity_MetaFragmentControl.y) 15 { 16 half3 emission; 17 if (unity_UseLinearSpace) 18 emission = IN.Emission; 19 else 20 emission = GammaToLinearSpace (IN.Emission); 21 22 res = UnityEncodeRGBM(emission, EMISSIVE_RGBM_SCALE); 23 } 24 return re
有點長,一點一點分析,首先unity_MetaFragmentControl和前面的MetaVertexControl一樣,他的xy值用來表示fragment要返回的值是albedo,還是normal。它們是由unity來設置的。在第一個if判斷里應該是對albedo的值根據用戶的烘焙設置進行最后的調整,具體是對應的什么值,我也查不到。第二個if判斷里先區分當前計算是不是在線性空間,如果不是,就轉換到線性空間。並把結果值轉換到RGBM空間(RGBM格式一般是用來存儲HDR空間的lightmap的)。
對於UnityMetaFragment這個函數我也有很多不太理解的地方,大家姑且理解一下流程吧,google也查不到什么資料,也沒有源碼。待日后研究吧。
補充
另外我這篇文章還留個尾巴,上面我們討論的都是間接光照,並沒有說明烘焙時直接光照Unity是如何處理的,這個我只能個人參考了一些資料的理解:
Enlighten實際上會把光源也作為一個物體,它也適用圖2的公式,但對於光源來說實際上只有Le而后面的項是沒有意義的。而Enlighten在渲染場景光照的時候實際上是一個迭代的過程,在不考慮任何模型物體自發光的情況下,第一次遍歷時候實際上對由於某一點的Bi來說,它的第二項只有那些光源物體會對它造成影響,也就是只有光源的Lj是一個非0值。當第二次遍歷的時候由於上一次遍歷,很多表面的Lj都已經是非0值,那么Bi的第二項的計算結果就會有更多的有效項,這實際就產生了間接光照了。
關於輻照度算法,何詠大神有一篇很好的譯文,很值得一看。
這篇文章思路有點亂,有些問題我自己也沒徹底搞明白,算是拋磚引玉,由於資料不多,水平有限,希望沒有誤導大家,如果發現文章中有任何錯誤,望及時指正。