2006年以来, KlayGE一直都是用Variance Shadow Map(VSM)来表达阴影。VSM只比标准shadow map(SSM)增加了几行代码,但却能通过插值,极大减少边缘的锯齿,甚至模拟软阴影的效果。VSM的缺点是,需要抓用两个32F的通道。这么一来,带宽消耗大得多了,并且没办法通过编码到RGBA8的技巧在不支持浮点纹理的设备上使用。另外,VSM的light leak也是很讨厌的毛病,需要仔细调参数才能减轻。
Exponential Shadow Map
实际上在VSM出来不久之后的2008年,就有了Exponential Shadow Map(ESM)的方法。和VSM类似,ESM也是通过巧妙的方法使线性插值成为可能,从而完成各种blur。比较一下SSM、VSM和ESM的生成和使用,就能看出来ESM在代码上比VSM简单,速度也更快。同时,ESM只需要占用和一个32F,所以不但带宽小了,还能编码到RGBA8。ESM的原理可以在很多地方找到,这里就不提了。简单来说,它就是用exp(k*(z-d)) = exp(k*z) * exp(-k*d)来近似(z-d>0)。生成了ESM之后,照样可以和VSM一样用gaussian blur或者box blur。
SSM | VSM | ESM | |
---|---|---|---|
生成 | return d; | float2 dxdy = float2(ddx(d), ddy(d)); return float2(d, d* d + 0.25f * dot(dxdy, dxdy)); |
return exp(c * d); |
使用 | return z < d; | float p = (z < moments.x);float variance = moments.y – moments.x * moments.x; variance = max(variance, min_variance); float m_d = moments.x – z; float p_max = variance / (variance + m_d * m_d); p_max = linstep(bleeding_reduce, 1, p_max);return max(p, p_max); |
return saturate(occluder * exp(-c * z)); |
参数 | 无 | min_variance和bleeding_reduce,场景相关 | c,场景无关,越大越好 |
优点 |
|
|
|
缺点 |
|
|
|
既然ESM有这些好处,为何不早点切换到ESM的框架呢?原始的ESM有个致命的问题,就是对精度要求太高。对32F来说,c到88就已经到极限了。但为了要让那个近似更接近原始值,c应该越大越好,否则在z-d越接近0的时候,误差会越来越大。另一个缺点在于,原始ESM要求depth在非线性的projection space,这就给点光源的阴影造成了麻烦。如果用CSM的话,projection space也会因为在不同的层级而需要分别计算,分界线可能出现跳变。
改进ESM
如果上面提到的那些缺点不被改进,ESM就很难实用化。在KlayGE里面,我用到了来自两方面的改进。
view space depth
首先先解决通用问题。如果depth是在线性的view space,那么点光源和CSM都能用上ESM,也就是各种光源的shadow都可以切换到ESM。这个公式来自于EGSR2013上浙大的文章“Exponential Soft Shadow Mapping”。
[latex]e^{-c\left(\frac{d_l-z_n}{z_f-z_n}-\frac{z_l-z_n}{z_f-z_n}\right)} = e^{-\frac{c}{z_f-z_n}(d_l-z_n)}[/latex]
这么一来,depth就都可以用view space的,只需要在c上除个far plane – near plane即可。
精度
前面提到了,c越大越好。但如果c太大,exp(c*d)就有可能超过float的范围。但其实c*(d-z)本身远远小于c*d,不容易越界。所以如果不需要blur,那么只要在生成阶段保存d,就像SSM那样;在使用阶段,计算exp(c*(d-z))即可。不过这样的话,已经失去了所有ESM的优点,还要ESM做什么。所以这里还需要改进blur的部分,争取在里面解决问题。实际上早在SIGGRAPH 2009的Advances in Real-Time Rendering in 3D Graphics and Games里,Lighting Research at Bungie就提到了logarithmic space filtering的方法。这里正是利用d-z远小于d或z的原理,把取值范围缩小了,精度也因此提高。filtering本身就是完成这个:
[latex]\sum_{i=0}^N w_i e^{cd_i}[/latex]
其中w来自于gaussian filter的kernel。如果进一步推这个公式,就能得到:
[latex]\sum_{i=0}^N w_ie^{cd_i} = e^{cd_0}e^{\ln\left(w_0+\sum_{i=1}^N w_ie^{c(d_i-d_0)}\right)}[/latex]
这个被称为log space filtering。最终filter的结果是个不会溢出的量(感谢空明流转指出我漏提了这点)
[latex]cd_0 + \ln\left(w_0+\sum_{i=1}^N w_ie^{c(d_i-d_0)}\right)[/latex]
用这个修改过的公式去做gaussian blur,那么即便是16F也可以承受高达300的c。
总结
总结一下这里的改进版ESM:
SSM | ESM | 改进的ESM | |
---|---|---|---|
生成 | return d; | return exp(c * d); | return d; |
使用 | return z < d; | return saturate(occluder * exp(-c * z)); |
return saturate(exp(occluder – c * z)); |
参数 | 无 | c,场景无关,越大越好 | c,场景无关,越大越好 |
优点 |
|
|
|
缺点 |
|
|
|
有了这两个改进,ESM就可以适用于多种应用场合了。KlayGE也因此从VSM切换到了ESM的表达,性能也有所提高。
Comments