前些天烘焙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的第二项的计算结果就会有更多的有效项,这实际就产生了间接光照了。
关于辐照度算法,何咏大神有一篇很好的译文,很值得一看。
这篇文章思路有点乱,有些问题我自己也没彻底搞明白,算是抛砖引玉,由于资料不多,水平有限,希望没有误导大家,如果发现文章中有任何错误,望及时指正。