版权声明:本文为博主原创文章,未经博主允许不得转载 https://blog.csdn.net/mobilebbki399/article/details/78359534
最近准备尝试写一个可以双面接受阴影,并且有类似透光效果的布料shader,大致效果如下:
总的来说就是在布料逆光的时候可以看到背面物体的阴影
实现的原理其实非常简单,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以投射阴影
但一旦旋转灯光到背面,可以发现阴影接受出现异常,且无法投射阴影:
这是因为我们在shader最后使用了fallback “Diffuse”,即使用了Diffuse的默认ShadowCaster pass来投射阴影,而这个pass是不渲染背面的,即当灯光照射背面时,背面不会渲染到shadowmap,我们可以打开framedebugger来验证:
正面(帆和桅杆都渲染到shadowmap):
背面(只渲染了桅杆和地面,没渲染帆):
三、背面也正确的接受阴影
在了解了上述原因之后,接下来只需要去掉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
}
}
四、计算背面光照:
现在阴影的投射和接受都正常了,接下来需要计算光照,考虑到正面和背面的处理需要有所不同,且要保证正面和背面在面对灯光的不同角度时效果相同,即:无论正面还是背面(这里指三角面的正面背面)在背光还是顺光时效果应该一致。
三角面的朝向实际上容易通过法线和视线的夹角求得:
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);