Shader入门(二十七)平面阴影
以下摘录自《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 }// } }