(八)unity Shader之——————MetaPass(为光照映射和动态全局光照提取表面信息的Pass块)

在做项目的时候遇到一个问题,之前把项目中的surface shader进行了一些优化,换成了新的vertex&fragment shader,效果都一致了,但是场景烘焙后问题就出来了。自己写的shader的间接光照模型在烘焙的时候效果不对,他不会对周围的物体产生间接光照,而且烘焙出来的lightmap很亮,跟老的shader烘焙出来的效果完全不同。

老的shader(正确的lightmap):

(八)unity Shader之——————MetaPass(为光照映射和动态全局光照提取表面信息的Pass块)_第1张图片

新的shader:

(八)unity Shader之——————MetaPass(为光照映射和动态全局光照提取表面信息的Pass块)_第2张图片

最后发现是没有加metaPass的原因。

unity在背后会根据surface shader(不只是自己写的,还包含unity自带的shader,很多也都是surface shader)生成一个包含多个Pass的顶点片元着色器,其中也包含了meta Pass,这个Pass块是为了给光照映射和动态全局光照提取表面信息的。你可以找到一个表面着色器,然后在Inspector面板中点击show generated code查看转换好的V&F代码。代码很长,metaPass一般在最后。

unity5把烘焙系统由Beast换成了Enlighten,Enlighten需要Unity提供材质的Albdeo(反射率)和自发光(emissive)的纹理,从而用来计算间接光照。而这两个贴图都是Unity自己在GPU上渲染得到的。既然需要GPU渲染,那就需要用提供一个相应的Pass来专门让Unity用来进行这种渲染,就是meta Pass。一句话总结,metaPass主要是Unity用来计算Albedo和emissive,然后提供给enlighten用的。

一、原理

为什么要这两个值也很好理解,设想一下一个物体要相对周围物体产生光照影响,一般有两种情况:

1.作为发光体,直接将光线投射到其他物体上,对应着上面的emissive。

2.光线照射到该物体上然后反射(可能经过多次)到周围物体上,最后被后者反射到人眼中,而当要计算前者能反射多少,以及哪些成分的光线到后者身上就要用到Albedo。

可以看一下Enlighten官方Blog给出的Radiosity Equation公式:

(八)unity Shader之——————MetaPass(为光照映射和动态全局光照提取表面信息的Pass块)_第3张图片

对于全局光照处理,目前有两种主流算法,光线追踪和辐射度算法,Enlighten所使用的即是这种。上图中的公式实际上是对RenderEquation的一种简化变形, RenderEquation是一种理想模型,也是目前所有光照处理的理论基础。

上面的模型中实际上把一个像素点的受光情况(这里只考虑间接光照)分成了自发光Le和 来自其它光源的间接光照。其中Pi是材质属性,这里我们可以简单的理解成Albedo反射率,这反应了该点对应的材质对不同波段光的反射能力。那上面公式中后面的一团就不难理解了,实际上就是对从各个方向收集到的反射光的和最后乘上一个材质反射能力,从而得到最后的实际光照结果。这也是为什么在在metapass里enlighten需要Unity给它提供Albedo和emission纹理了。

二、代码

Pass  
{  
    Name "Meta"
    Tags {"LightMode" = "Meta"}
    Cull Off

    CGPROGRAM
    #pragma vertex vert_meta
    #pragma fragment frag_meta

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

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

    uniform fixed4 _Color;
    uniform sampler2D _MainTex;
    v2f vert_meta(appdata_full v)
    {
        v2f o;
        UNITY_INITIALIZE_OUTPUT(v2f,o);
        o.pos = UnityMetaVertexPosition(v.vertex,v.texcoord1.xy,v.texcoord2.xy,unity_LightmapST,unity_DynamicLightmapST);
        o.uv = v.texcoord.xy;
        return o;
    }

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

    ENDCG
}

上面的代码是最简化的一版,最开始的LightMode的Tag,unity要通过它来找到MetaPass,Unity文档中有比较完整的代码。_Color和_MainTex是在Properties里声明的贴图和调和颜色。后面会把他俩相乘作为Albedo的结果,这也正是我们在正常的光照处理里所做的。

其中:

1.Unity_INITIALIZE_OUTPUT(v2f,o);

是一个清零操作,当顶点着色器函数返回结果的时候,如果要返回的结构体没有全部赋值过,那么unity会报错,必须全部赋值,这个宏就是用来清零的,省着手动赋值为0,并不是所有的着色器语言都支持,有些情况下必须手动赋值。

2.UnityMetaVertexPosition(v.vertex,v.texcoord1.xy,v.texcoord2.xy,unity_LightmapST,unity_DynamicLightmapST)

参数中的uv1,uv2分别是模型在静态光照贴图(static lightmap)和动态光照贴图(实时GI)中的uv坐标。lightmapST和dynlightmapST的值应该是经过特殊构造以用来确定其在烘焙空间中的位置。

3.UnityObjectToClipPos(vertex)

只是做了模型空间到齐次空间的转换。

4.

struct UnityMetaInput
 {
     half3 Albedo;
     half3 Emission;
 };
metaIN.Albedo = tex2D(_MainTex,IN.uv).rgb * _Color.rgb;
metaIN.Emission = 0;

根据项目实际需求计算这两个值,然后赋值给UnityMetaInput中。

5.UnityMetaFragment(metaIN);

三、注意事项

上面的代码可以直接用,但是效果可能会有问题,直接用我烘焙出来的效果如下图:

(八)unity Shader之——————MetaPass(为光照映射和动态全局光照提取表面信息的Pass块)_第4张图片 

大家会发现比起没有加metaPass烘焙出来的lightmap,要好上太多了,曝光严重的问题解决了,但是会发现新烘焙的有点深,不通透,黑的地方有点重,有颜色的地方饱和度有点高。

这是因为每个项目,每种shader的计算都是有自己的需求的,我们计算Albedo和emission都是有差异的,就如上面的代码,反射率Albedo的计算一般大多项目都是用的

Albedo = tex2D(_MainTex,IN.uv).rgb * _Color.rgb。

_Color和_MainTex是在Properties里声明的贴图和调和颜色。后面会把他俩相乘作为Albedo的结果,这也正是我们在正常的光照处理里所做的。

emission在上面的代码中直接给赋值为0了,因为有很多建筑shader不需要这个属性,也没有计算它。但是有些环境建筑的shader确实计算了自发光,那么就要把算出来的emission值赋值给metaIN.Emission了,要跟项目中的属性值一致,才能得到正确的效果,比如我的:

fixed4 diffTex;

 diffTex = tex2D(_MainTex, i.uv);

 float2 uvScreen = i.screenPos.xy / max(i.screenPos.w, 1);

fixed4 em = tex2D(_EmissionTex, i.uv);

 fixed3 Emission = _EmissionColor * em * _EmissionPower + _EmissionNight * em
 + diffTex.rgb * _EmissionMain + GetCpLightColor(uvScreen, diffTex.rgb);

metaIN.Emission = Emission;

还有的shader计算了SpecularColor,那么有时候也有必要进行赋值,一切根据项目来定,比如我的:

_Shininess("Shininess", Range(0.03, 1)) = 0.08

fixed Specular = _Shininess;

metaIN.SpecularColor = Specular;

我也是查看了项目中原来的老的surface shader生成的V&F shader的meta Pass的块才看到需要这些属性的值参与计算,所以我们要进行适当的修改来变成我们想要的效果。

四、补充

上面讨论的都是间接光照,并没有说明烘焙时直接光照Unity是如何处理的,查看别人的资料的一些理解:

Enlighten实际上会把光源也作为一个物体,但对于光源来说实际上只有Le而后面的项是没有意义的。而Enlighten在渲染场景光照的时候实际上是一个迭代的过程,在不考虑任何模型物体自发光的情况下,第一次遍历时候实际上对由于某一点的Bi来说,它的第二项只有那些光源物体会对它造成影响,也就是只有光源的Lj是一个非0值。当第二次遍历的时候由于上一次遍历,很多表面的Lj都已经是非0值,那么Bi的第二项的计算结果就会有更多的有效项,这实际就产生了间接光照了。

下面是原文地址,我参考的这篇文章加上项目中的实践来写的:

http://www.cnblogs.com/Esfog/p/MetaPass_In_Unity5.html 

你可能感兴趣的:(unity,Shader,游戏开发,metaPass)