Unity3D 阴影和深度纹理总结
首先介绍一下,阴影的生成,阴影一般是在屏幕空间生成的,利用Unity3D引擎实现阴影的绘制,首先要明白其实现原理,Unity5的Demo已经为我们提供了代码实现,先介绍一下阴影的实现原理:
屏幕空间的阴影实现主要有这么几个步骤:
1、首先得到从当前摄像机处观察到的深度纹理。在延迟渲染里这张深度图本来就有,如果是前向渲染的话就需要把场景整个渲染一遍,把深度渲染
到深度图中。
2、然后再从光源出发得到从该光源处观察到的深度纹理,也被称为这个光源的ShadowMap。
3、然后在屏幕空间做一次阴影收集计算(Shadows Collector),这次计算会得到一张屏幕空间阴影纹理,也就是说这张图里面需要有阴影的部分
已经显示在图上了。这个过程概括来说就是把每一个像素根据它在摄像机深度纹理中的深度值得到世界空间坐标,再把它的坐标从世界空间转换到
光源空间中,和光源的ShadowMap里面的深度值对比,如果大于ShadowMap中的深度距离,那么就说明光源无法照到,在阴影内。
4、最后,在正常渲染物体为它计算阴影的时候,只需要按照当前处理的fragment在屏幕空间中的位置对步骤3得到的屏幕空间阴影图采样就可以了。
Hidden/Internal-ScreenSpaceShadows(在DefaultResourcesExtra/Internal-ScreenSpaceShadows.shader)。这个Pass在不同的Unity版本
里是不一样的,比如在Unity 5.3里面就是Internal-PrePassCollectShadows(在DefaultResources/Internal-PrePassCollectShadows.shader),看的时候还是要全局搜索确定下。
主要思路就是从摄像机的深度纹理里采样得到该fragment的深度值,然后利用矩阵变换计算得到该点
对应的世界空间的世界坐标(利用CameraToWorld矩阵),然后再变换到光源空间下的坐标(利用WorldToShadow矩阵),最后拿
这个坐标对光源的ShadowMap采样计算阴影。
通常的做法其实都可以满足需求,最重要的是遇到一些特殊情况比如材质是透明或者是半透明的,这个就需要特殊处理了。
// Shadow rendering pass
Pass {
Name "ShadowCaster"
Tags { "LightMode" = "ShadowCaster" }
ZWrite On ZTest LEqual
CGPROGRAM
#pragma target 3.0
// TEMPORARY: GLES2.0 temporarily disabled to prevent errors spam on devices without textureCubeLodEXT
#pragma exclude_renderers gles
#pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON
#pragma multi_compile_shadowcaster
#pragma vertex vertShadowCaster
#pragma fragment fragShadowCaster
#include "UnityStandardShadow.cginc"
ENDCG
}
可以发现也是和我们一样定义了LightMode为ShadowCaster的Pass。_ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON这些keyword就是为半透明阴影效果定义的。再看fragShadowCaster的实现(在UnityStandardShadow.cginc中定义):
half4 fragShadowCaster (
#ifdef UNITY_STANDARD_USE_SHADOW_OUTPUT_STRUCT
VertexOutputShadowCaster i
#endif
#ifdef UNITY_STANDARD_USE_DITHER_MASK
, UNITY_VPOS_TYPE vpos : VPOS
#endif
) : SV_Target
{
#if defined(UNITY_STANDARD_USE_SHADOW_UVS)
half alpha = tex2D(_MainTex, i.tex).a * _Color.a;
#if defined(_ALPHATEST_ON)
clip (alpha - _Cutoff);
#endif
#if defined(_ALPHABLEND_ON) || defined(_ALPHAPREMULTIPLY_ON)
#if defined(UNITY_STANDARD_USE_DITHER_MASK)
// Use dither mask for alpha blended shadows, based on pixel position xy
// and alpha level. Our dither texture is 4x4x16.
half alphaRef = tex3D(_DitherMaskLOD, float3(vpos.xy*0.25,alpha*0.9375)).a;
clip (alphaRef - 0.01);
#else
clip (alpha - _Cutoff);
#endif
#endif
#endif // #if defined(UNITY_STANDARD_USE_SHADOW_UVS)
SHADOW_CASTER_FRAGMENT(i)
}
代码也很简单,我们需要关注的就是tex3D(_DitherMaskLOD, float3(vpos.xy_0.25,alpha_0.9375)).a这一句。可以看出来,它是对一张
3D的dither纹理采样,这张纹理的z方向对应了透明度值。再使用clip对dither后的结果进行cutout,造成一种半透明阴影的假象。
说得简单点,这种Dither技术其实就是利用人眼的特性,我们看到的其实是有很多细小孔洞的阴影,由于这些空洞的大小和密度变化,
在人眼看来就像是半透明了一样,但实际上真正的阴影颜色是没有变的。总结其实就是,dither texture cutout。
如果我们要自己在shader中实现这种效果,也可以仿照Standard Shader的做法,对_DitherMaskLOD这张纹理进行采样。例如,
在Shader中添加自定义的ShadowCaster Pass:
Pass{
Name "ShadowCaster"
Tags{ "LightMode" = "ShadowCaster" }
ZWrite On ZTest LEqual
CGPROGRAM
#pragma target 3.0
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#define UNITY_STANDARD_USE_SHADOW_OUTPUT_STRUCT
#define UNITY_STANDARD_USE_DITHER_MASK
#define UNITY_STANDARD_USE_SHADOW_UVS
#include "UnityStandardShadow.cginc"
fixed _AlphaScale;
struct VertexOutput
{
V2F_SHADOW_CASTER_NOPOS
float2 tex : TEXCOORD1;
};
void vert(VertexInput v, out VertexOutput o, out float4 opos : SV_POSITION)
{
TRANSFER_SHADOW_CASTER_NOPOS(o,opos)
o.tex = v.uv0;
}
half4 frag(VertexOutput i, UNITY_VPOS_TYPE vpos : VPOS) : SV_Target
{
half alpha = tex2D(_MainTex, i.tex).a * _AlphaScale;
half alphaRef = tex3D(_DitherMaskLOD, float3(vpos.xy*0.25,alpha*0.9375)).a;
clip(alphaRef - 0.01);
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG
}
同时禁用其他Fallback,就可以得到“半透明”阴影的效果了。