Shader入门(二十七)平面阴影

发表于2018-02-02
评论0 3k浏览

以下摘录自《Unity 3D ShaderLab开发实战详解》,第10章。 
转注:这是一个非常简单且廉价的实时假阴影,比较常用到,据我所知,王者荣耀中就使用了类似的手法。缺点就是比较假,只能投射到平面上,不能应对地面的凹凸,最明显的问题就是会插到墙里去。

转注:需要传给shader接受阴影的地面(平面)的worldToLocalMatrix和localToWorldMatrix。对应的变量分别为_World2Ground和_Ground2World。

Shader "Tut/Shadow/PlanarShadow_3" {
//转注:这里对原作里的平行光计算部分和阴影淡出部分做了整合,略去了点光源计算部分。
    Properties{
        _Intensity("atten",range(1,16))=1
    }
    SubShader {
        Pass {
            Tags {"LightMode" = "ForwardBase"}
            Material {Diffuce(1,1,1,1)}
            Lighting On
        }
        Pass {
            Tags {"LightMode" = "ForwardBase"}
            Cull Front
            Blend DstColor SrcColor
            Offset -1,-1 //使阴影在平面之上
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #inlcude "UnityCG.cginc"
            float4x4 _World2Ground;
            float4x4 _Ground2World;
            float _Intensity;
            struct v2f{
                float4 pos:SV_POSITION;
                float atten:TEXCOORD0;
            };
            v2f vert(float4 vertex:POSITION)
            {
                v2f o;
                float3 litDir;
                litDir = WorldSpaceLightDir(vertex);
                litDir = mul(float3x3(_World2Ground), litDir);//把光源方向转换到接受平面空间
                litDir = normalize(litDir);
                float4 vt;
                vt = mul(_Object2World, vertex);
                vt = mul(_World2Ground, vt);//将物体顶点转换到接受平面空间
                vt.xz=vt.xz-(vt.y/litDir.y)*litDir.xz;//用三角形相似计算沿光源方法投射后的xz
                //上面这行代码可拆解为如下的两行代码,这样可能在进行三角形相似计算时更好理解
                //vt.x=vt.x-(vt.y/litDir.y)*litDir.x;
                //vt.z=vt.z-(vt.y/litDir.y)*litDir.z;
                vt.y=0;//使阴影保持在接受平面上
                vt=mul(_Ground2World,vt);//返回到世界坐标空间
                vt=mul(_World2Object,vt);//将结果重新表达为Object Space坐标
                o.pos = mul(UNITY_MATRIX_MVP, vt);//经典的MVP变换,输出到Clip Space
                o.atten=distance(vertex,vt)/_Intensity;//根据前后定点的距离计算衰减
                return o;
            }
            float4 frag(void) : COLOR
            {
                return smoothstep(0,1,i.atten/2);
            }
            ENDCG
        }
    }
}

在此Shader中,我们首先使用固定管线对物体做了一个简单的照明。在计算阴影的ForwardBase中,首先使用一个可以叠加阴影的混合模式,然后使用z偏移保证出来的阴影在接受平面之上。_World2Ground和_Ground2World分别是我们自定义的两个进出阴影接受平面矩阵(转注:这里原作解释的不好,按字面意思理解即可)。在具体计算中,首先将光源方向和投影物体的顶点都转换到接受平面的空间(接受平面的物体空间),在它们都处于同一空间后,通过简单的三角形近似算法,来计算投影物体顶点沿光线投影后在接受平面上的新位置。因为这是一个Build In的(Built-in)Unity Plane,所以只计算其xz分量即可,并将y分量设为0,这样投影出来的阴影就出现在接受平面上了。投影计算完成后,返回世界空间,然后到投影物体本身的Object Space,之后的事情就如通常那样,一个经典的MVP变换即可。 
这个方法还有一个显而易见的问题,那就是物体本身是立体的,不在一个平面,因此,这个计算前后的点的距离是包括物体本身厚度的,这个厚度就会表现在阴影上。 
转注:使用Stencil可以解决这个问题,但是原作中并没有涉及,所以暂时略过。
如上图所示,Cube和Cylinder貌似没有什么问题,但是右边那个绳结形状的物体的阴影就出现了问题。解决方法,在第二个pass里CGPROGRAM之前,加入以下代码:

        Stencil{
            Ref 1           //参考值为1,stencilBuffer值默认为0  
            Comp Greater    //stencil比较方式是大于
            Pass replace    //通过的处理是替换,就是拿1替换buffer 的值  
            Fail Keep       //深度检测和模板检测双失败的处理是保持
            ZFail keep      //深度检测失败的处理是保持
        }

结果:  
效果略好,但是也并不太理想,那是因为原作并不是把绳结物体当成一整个物体对待,而是当做一个一个的顶点处理,用原顶点和对应的影子顶点计算距离明显有问题。另外有一种做法是传入对象的世界坐标,然后跟世界空间的影子顶点计算距离,但其实也略复杂了,最简单的就是把o.atten=distance(vertex,vt)/_Intensity;这一段替换成:

o.atten=length(vt)/_Intensity;

其实就是计算影子顶点和物体的原点的距离。 
效果如图: 
 
这就需要在制作模型的时候把物体的原点设在y=0的平面上,这样这种影子才能达到较好的效果。 
最后……需要提一句,原作中将物体和影子在一个shader里渲染,我认为这是有问题的,因为没法分别设置Queue,物体的Queue应该是Geometry,而影子的Queue应该是Transparent。所以正确的做法应该是,物体该怎么渲染就怎么渲染,影子做一个GameObject挂在物体上,添加MeshFilter和MeshRenderer使用相同的Mesh,然后MeshRenderer里使用阴影的材质。即上文中的Shader去掉第一个Pass,并修改Tags。Blend方法建议修改为Blend SrcAlpha OneMinusSrcAlpha,最后frag方法建议修改为:

        float4 frag(v2f i) : COLOR 
        {
            return float4(0,0,0, smoothstep(1, 0, i.atten / 2));
        }

综上所述,整合一下代码:

Shader "Tut/Shadow/PlanarShadow_Chaos" {
    Properties{
        _Intensity("atten",range(1,16))=1
    }
    SubShader {
        Tags{ "LightMode" = "ForwardBase"
        "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" 
    }
    pass {
        Cull Front
        Blend SrcAlpha OneMinusSrcAlpha
        Offset -1,-1
        Stencil{
            Ref 1           //参考值为1,stencilBuffer值默认为0  
            Comp Greater    //stencil比较方式是大于
            Pass replace    //通过的处理是替换,就是拿1替换buffer 的值  
            Fail Keep       //深度检测和模板检测双失败的处理是保持
            ZFail keep      //深度检测失败的处理是保持
        }
        CGPROGRAM
        #pragma vertex vert 
        #pragma fragment frag
        #include "UnityCG.cginc"
        float4x4 _World2Ground;
        float4x4 _Ground2World;
        float _Intensity;
        struct v2f{
            float4 pos:SV_POSITION;
            float atten:TEXCOORD0;
        };
        v2f vert(float4 vertex: POSITION)
        {
            v2f o;
            float3 litDir;
            litDir=normalize(WorldSpaceLightDir(vertex));  
            litDir=mul(_World2Ground,float4(litDir,0)).xyz;
            float4 vt;
            vt= mul(unity_ObjectToWorld, vertex);
            vt=mul(_World2Ground,vt);
            vt.xz=vt.xz-(vt.y/litDir.y)*litDir.xz;
            vt.y=0;
            vt=mul(_Ground2World,vt);//back to world
            vt=mul(unity_WorldToObject,vt);
            o.pos=UnityObjectToClipPos(vt);
            o.atten=length(vt)/_Intensity;
            return o;
        }
        float4 frag(v2f i) : COLOR 
        {
            return float4(0,0,0, smoothstep(1, 0, i.atten / 2));
        }
        ENDCG 
        }//
   }
}

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引