Shadow map 是目前实现阴影效果的主流做法,可以做到self-shadow.
假设在3D环境中只有太阳光源,如果太阳光可以直射到物体上某个位置A时,A点就不会落在阴影中。如果太阳光移动到物体表面上的位置B前,先照射到另一个物体,太阳的光线会被切断,B就落在阴影区里。可以把阴影的计算视为碰撞检测,A点不在阴影区内,也就是从A点到太阳之间可以直达,完全没有遮蔽;B点在阴影区内,也就等于是从B点到太阳之间的有某样东西切断了它们的直接连接。
使用shadow map 来实现阴影效果,相当于使用GPU做碰撞检测,测试物体表面与光源之间是否存在任何阻隔。做隐藏面消除用的Z Buffer事件上就是对镜头做类似的工作。镜头里所看到的物体,它们到镜头之间没有任何阻隔,Z Buffer Test其实就是在做碰撞检测。如果物体到镜头之间有阻隔,就算它没有被略过不画,也会在后面的步骤中被其它对象所覆盖。
把镜头移到光源位置,再把物体到光源间的距离值填入到动态贴图中,它就具有足够的信息来对光源做碰撞检测。把距离值画入动态贴图中是一件简单的事,不需要特别的技巧,因为Z Buffer存储的就是这个距离值,只要把 Z Buffer做为贴图直接拿来使用就可以了。没有规定动态贴图只能使用存储颜色的RGBA类型,有些硬件允许把存储距离的Z Buffer用来存储动态贴图:
Uniform Shadow map 的原理很容易理解,有很多资料,这里不再介绍。shadow map 需要一次额外的渲染pass( shadow mapping requirs only one additional rendering pass),
在引擎中一般会Z pre-pass, 那么我们利用MRT(Multi Render Target)技术把这两个pass 合二为一,另外把表面法线等存入额外的表面。以供 Deferred Lighting, Depth of field, Bloom 等 后期处理使用
2:Focus region B
Shadow map遇到的一个最大的问题就是当场景很大时图片分辨率不足。在一个很大的场景中,我们能看到的区域A只是场景的一小部分,能投射阴影到区域A的物体所形成的区域B也只是一小部分,我们只需要把B中物体拍摄到阴影贴图中就可以了。
图:详见shaderX 4 Page313 Robust shadow mapping with light space perspective shadow maps.
3:light space perspective shadow map
基于PSM改进的 Warp the shadow map 技术, 使离摄像机更近的物体在shadow map中所占的区域更大,离的远的减少。
4:Variance shadow map
variance shadow map 是一种柔化阴影技术,即在阴影边缘产生软化的边缘.
效果如下图:
VSM主要利用了 "期望","方差","切比雪夫不等式"的概念。
与常规阴影利用直接把深度存入阴影贴图不同,在此我们使用D3DFMT_G32R32F格式,分别记录深度,及深度平方到深度图中
// 计算矩
float2 ComputeMoments( float depth)
{
float2 moments;
// 第一个moment为深度本身
moments.x = depth;
moments.y = depth * depth;
return moments;
}
float4 PS( VS_OUTPUT In) : COLOR
{
float2 moments = ComputeMoments( In.screenPos.z / In.screenPos.w);
return float4( moments, 0.f, 1.f);
}
方差 σ^2 = E(x^2) - (E(x))^2;
切比雪夫不等式:
p (t) = σ^2 / (σ^2 + ( t-E(x))^2);
//--------------------------------------------------------
// Computes Chebyshev's Inequality
//
float ChebyshevUpperBound( float2 moments, float t, float minVariance)
{
// Standard shadow map comparison
float p = ( t<=moments.x);
// compute probabilistic upper bound
float variance = moments.y - ( moments.x*moments.x);
variance = max( variance, minVariance);
// compute probabilistic upper bound
float d = t - moments.x;
float p_max = variance / ( variance + d*d);
return max( p, p_max);
}
//--------------------------------------------------------
// variance shadow map
//
uniform float g_lbrAmount = 0.18f;
float tex2DSM( sampler2D shadowSampler, float4 posInLightSpace)
{
float4 projPos = posInLightSpace / posInLightSpace.w;
// soft shadow
float2 moments = tex2D( shadowSampler, projPos.xy).xy;
float shadowContrib = ChebyshevUpperBound( moments, projPos.z, 0.00001f);
float lbr = clamp( (shadowContrib - g_lbrAmount)/(1-g_lbrAmount), 0, 1);
//float lbr = smoothstep( g_lbrAmount, 1, shadowContrib);
return saturate( lbr + 0.4f);
}
详见 gpu gems 3第八章节
5:Parallel-Split Shadow Maps
平行分隔阴影基于以上的工作,还是很容易实现的。本人在此未实现,详情可参考 gpu gems3 第十章节!