【UnityShader】双面透光布料材质

最近准备尝试写一个可以双面接受阴影,并且有类似透光效果的布料shader,大致效果如下:
总的来说就是在布料逆光的时候可以看到背面物体的阴影

【UnityShader】双面透光布料材质_第1张图片
【UnityShader】双面透光布料材质_第2张图片



实现的原理其实非常简单,unity本身已经提供了一整套阴影渲染的方案,我们所需要做的只是根据法线判断正反面,并在反面正确的渲染出阴影和光照即可,注意还需要自己实现ShadowCaster,以保证光照在背面时也可以投射阴影。

接下来分解一下实现步骤:
一、双面渲染
这涉及到渲染管线中的背面剔除,在渲染封闭的几何体时,其背面通常都是不可见的,因此不渲染背面可以提高渲染性能,即开启背面剔除功能,默认情况下背面剔除是开启的,我们需要在shader中设置cull off来关闭背面剔除。

二、接受阴影:
unity提供了内置的shadowmap采样api:
1.首先需要在顶点输出结构中使用SHADOW_COORDS定义shadowmap uv
2.顶点函数的结尾调用TRANSFER_SHADOW计算灯光空间坐标等
3.片段函数中调用UNITY_LIGHT_ATTENUATION计算阴影
如下代码
SubShader
{
    Tags { "RenderType"="Opaque" }
    LOD 100
 
    Pass
    {
        Tags { "LightMode"="ForwardBase" }
        cull off
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #pragma multi_compile_fog
        #pragma multi_compile_fwdbase 
         
        #include "UnityCG.cginc"
        #include "Lighting.cginc"
        #include "AutoLight.cginc"
     
        struct v2f
        {
            float2 uv : TEXCOORD0;
            float3 worldNormal : TEXCOORD1;
            float4 worldPos : TEXCOORD2;
            UNITY_FOG_COORDS(3)
            SHADOW_COORDS(4)//定义一个字段"_ShadowCoord:TEXCOORD4"
            float4 pos : SV_POSITION;
        };
 
        sampler2D _MainTex;
        float4 _MainTex_ST;
         
        v2f vert (appdata_base v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
            o.worldNormal = UnityObjectToWorldNormal(v.normal);
            o.worldPos = mul(unity_ObjectToWorld, v.vertex);
            UNITY_TRANSFER_FOG(o,o.pos);
            TRANSFER_SHADOW(o);//将顶点转换到灯光空间下
            return o;
        }
             
        fixed4 frag (v2f i) : SV_Target
        {
            fixed4 col = tex2D(_MainTex, i.uv);
            UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos)//投射shadowmap,并比较深度,以此计算出atten值
            float3 litDir = normalize(UnityWorldSpaceLightDir(i.worldPos.xyz));
            float ndl = abs(dot(i.worldNormal, litDir))*0.5+0.5;
            col.rgb = col.rgb * (UNITY_LIGHTMODEL_AMBIENT.rgb + _LightColor0.rgb* ndl*atten);
            UNITY_APPLY_FOG(i.fogCoord, col);
            return col;
        }
        ENDCG
    }
}
fallback "Diffuse"    //fallback为必须,可以不返回Diffuse,但至少要返回一个带有shadowcaster的shader以投射阴影

观察效果,可以发现在灯光投射正面时阴影的接受和投射都是正确的:
【UnityShader】双面透光布料材质_第3张图片

但一旦旋转灯光到背面,可以发现阴影接受出现异常,且无法投射阴影:
【UnityShader】双面透光布料材质_第4张图片


这是因为我们在shader最后使用了fallback “Diffuse”,即使用了Diffuse的默认ShadowCaster pass来投射阴影,而这个pass是不渲染背面的,即当灯光照射背面时,背面不会渲染到shadowmap,我们可以打开framedebugger来验证:
正面(帆和桅杆都渲染到shadowmap):
【UnityShader】双面透光布料材质_第5张图片


背面(只渲染了桅杆和地面,没渲染帆):
【UnityShader】双面透光布料材质_第6张图片
三、背面也正确的接受阴影
在了解了上述原因之后,接下来只需要去掉fallback,手动实现一个双面渲染的shadowcaster即可:

SubShader
{
    Tags { "RenderType"="Opaque" }
    LOD 100
 
    Pass
    {
        Tags { "LightMode"="ForwardBase" }
        cull off
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #pragma multi_compile_fog
        #pragma multi_compile_fwdbase 
             
        #include "UnityCG.cginc"
        #include "Lighting.cginc"
        #include "AutoLight.cginc"
 
        struct v2f
        {
            float2 uv : TEXCOORD0;
            float3 worldNormal : TEXCOORD1;
            float4 worldPos : TEXCOORD2;
            UNITY_FOG_COORDS(3)
            SHADOW_COORDS(4)
            float4 pos : SV_POSITION;
        };
 
        sampler2D _MainTex;
        float4 _MainTex_ST;
             
        v2f vert (appdata_base v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
            o.worldNormal = UnityObjectToWorldNormal(v.normal);
            o.worldPos = mul(unity_ObjectToWorld, v.vertex);
            UNITY_TRANSFER_FOG(o,o.pos);
            TRANSFER_SHADOW(o);
            return o;
        }
         
        fixed4 frag (v2f i) : SV_Target
        {
            fixed4 col = tex2D(_MainTex, i.uv);
            UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos)
            float3 litDir = normalize(UnityWorldSpaceLightDir(i.worldPos.xyz));
            float ndl = abs(dot(i.worldNormal, litDir))*0.5+0.5;
            col.rgb = col.rgb * (UNITY_LIGHTMODEL_AMBIENT.rgb + _LightColor0.rgb* ndl*atten);
            UNITY_APPLY_FOG(i.fogCoord, col);
            return col;
        }
        ENDCG
    }
    Pass{
        Name "ShadowCaster"
        Tags{ "LightMode" = "ShadowCaster" }
 
        cull off//确保正面和背面都正确的渲染到shadowmap
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #pragma multi_compile_shadowcaster
        #include "UnityCG.cginc"
 
        struct v2f {
            V2F_SHADOW_CASTER;
        };
 
        v2f vert(appdata_base v)
        {
            v2f o;
            TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
            return o;
        }
 
        float4 frag(v2f i) : SV_Target
        {
            SHADOW_CASTER_FRAGMENT(i)
        }
        ENDCG
 
    }
}

观察shadowmap,现在背面也渲染到shadowmap了
【UnityShader】双面透光布料材质_第7张图片

四、计算背面光照:
现在阴影的投射和接受都正常了,接下来需要计算光照,考虑到正面和背面的处理需要有所不同,且要保证正面和背面在面对灯光的不同角度时效果相同,即:无论正面还是背面(这里指三角面的正面背面)在背光还是顺光时效果应该一致。
三角面的朝向实际上容易通过法线和视线的夹角求得:
float ndvSign = sign(dot(i.worldNormal, viewDir));//通过法线和视线的点积判断当前的面的朝向

但在处理光照时,注意法线的处理,因为通常法线是和三角面垂直且指向三角面的正面,因此处理光照时需要取法线的绝对值来无差别计算正面和背面的光照:
float ndl = abs(dot(i.worldNormal, litDir))*0.5+0.5;
float ndl = abs(dot(worldNormal, litDir))*0.5 + 0.5;
float ndh = abs(dot(worldNormal, halfVec));

这样即可保证两面的光照是一致的。

五、透光:
这一步我只是简单的考虑当灯光穿透布料时,穿透布料后的颜色可能是灯光本身的颜色和布料颜色的简单混合,并使用一个透光率来简单的模拟透光和的灯光颜色:

float ndlS = 1-saturate(max(0,dot(i.worldNormal, litDir)*ndvSign));//计算一个衰减值,其只影响背光面
 
float3 lightCol = lerp(_LightColor0.rgb, _LightColor0.rgb * _BodyColor.rgb * col.rgb * _Transmittance, ndlS);

虽然无法准确模拟,但是肉眼看效果尚可。
【UnityShader】双面透光布料材质_第8张图片

六、细节:
主要是增加了Rim,法线贴图等,使布料的效果更好:
【UnityShader】双面透光布料材质_第9张图片

Demo下载地址: http://www.lsngo.net/2017/10/23/unityshader_clothshader/
更多内容: http://www.lsngo.net


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