转载请注明出处为KlayGE游戏引擎,本文的永久链接为http://www.klayge.org/?p=2672

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,场景无关,越大越好
优点
  • 简单
  • 可以blur
  • 无bias问题
  • 简单
  • 可以blur
  • 无bias问题
  • blur kernel较小
  • 1个32F通道
  • 效果和深度复杂度无关
缺点
  • 无法blur
  • bias问题
  • 需要较大的blur kernel
  • 2个32F通道
  • 效果和深度复杂度有关
  • 精度
  • 非线性depth

既然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,场景无关,越大越好
优点
  • 简单
  • 简单
  • 可以blur
  • 无bias问题
  • blur kernel较小
  • 1个32F通道
  • 效果和深度复杂度无关
  • 简单
  • 可以blur
  • 无bias问题
  • blur kernel较小
  • 1个16F通道
  • 效果和深度复杂度无关
  • 很大的c
  • 线性depth
缺点
  • 无法blur
  • bias问题
  • 精度
  • 非线性depth
  • 必须使用修改过的blur

有了这两个改进,ESM就可以适用于多种应用场合了。KlayGE也因此从VSM切换到了ESM的表达,性能也有所提高。