(这个功能本来应该是KlayGE 4.10就有的,但因为时间原因,拖过了发布时间。所以变成4.11里第一个实现的功能。)
粒子系统在游戏引擎里用得非常普遍。而粒子系统的渲染本身,却是一个不怎么快的过程。因为大量粒子会叠在屏幕上,给填充很大的压力。
加速的方向
既然是填充率的瓶颈,那显而易见的加速方法就是缩小分辨率。常见的做法是把粒子渲染到一个半分辨率的纹理上,在根据depth的分布混合到全分辨率。在KlayGE里,shadowing是就是用这个方法加速的。如果插值后的depth更接近于point采样的depth,就填充point采样的颜色,否则填充linear采样的颜色。这么做的话,大约能达到原先4倍的速度。GPU Gems 3的High-Speed, Off-Screen Particles一章,就是在讨论这个议题。
Simple & Naive缩小分辨率的尝试
首先先来一些感性认识,看看直接naive地缩小分辨率,会得到什么样的结果。
机器后面的火焰是用全分辨率渲染的粒子。注意观察粒子和机器交界的边缘,放大后是这个样子的。这是我们的ground truth,之后的结果都跟这个比。
如果用半分辨率渲染粒子,则会变成:
比较一下这几个分辨率下渲染粒子的速度,就会发现和分辨率非常相关。
分辨率 | 时间 | 加速比 |
---|---|---|
1 | 0.337 ms | 1 x |
1/2 | 0.079 ms | 4.28 x |
1/4 | 0.016 ms | 20.91 x |
但缺点也是非常明显的,边缘的锯齿随着分辨率下降变得越来越严重。Simple & naive地缩小,虽然跑得快,但这么做质量是不行的。
VDM
在SIGGRAPH 2013的著名讲座Advances in Real-Time Rendering in Games上,Bungie的Destiny: From Mythic Science Fiction to Rendering in Real-Time提到了一个做法,称为Variance Depth Map(VDM)。可以在进一步加速粒子的渲染。但那篇ppt里的描述非常简短。网上也找不到什么资料,只有一篇来自安柏霖的博客文章[siggraph13]《命运》的实时渲染技术提到了原理,并做了一些解释。不过仍然没有公式和细节。
这里我们就来看看如何实现这个东西。这个算法的思路和前面一样,通过渲染到小的纹理,再用某种方式混合到全分辨率。不过,VDM可以在1/4分辨率上使用,混合的时候需要比较复杂的算法,以保证边缘。
步骤1,生成1/4分辨率的保守depth map
首先是把场景的depth map拿来,渲染到1/4分辨率的另一张depth map上。按照最大值的规则缩小。也就是说,在新生成的depth texture上,每个pixel表示之前4×4的区域里最大的depth值。这样的道德保守深度图有两个好处。第一是,可以做early-Z。如果一个粒子大于那个区域的最大深度,就表示它一定已经被不透明的物体挡住,一定不可见。所以可以直接忽略那个粒子。第二个好处是在混合的时候可以更精确。后面会提到。
步骤2,以1/4的分辨率渲染粒子
下一步就是在1/4分辨率的纹理上渲染粒子。这里不能单纯地渲染颜色,还需要记录depth和depth平方的平均值。在Bungie的ppt里,假设全分辨率是1280×720的话,这里用的是这样的2个RT:
w | h | 格式 | 初始值 | |
---|---|---|---|---|
color (pre-multiplied) | 320 | 240 | a8:r8:g8:b8 | rgb:0.0 a:1.0 |
depth transition | 320 | 240 | r16:g16 | rg:0.0 |
depth transition的r通道记录了该像素上可见粒子depth的平均值,g通道记录了该像素上可见粒子depth平方的平均值。但我不是很明白如何通过alpha blending直接得到平均值,所以我目前的实现方法是用3个RT:
w | h | 格式 | 初始值 | |
---|---|---|---|---|
color | 320 | 240 | a8:r8:g8:b8 | rgba:0.0 |
depth transition | 320 | 240 | r16f:g16f | rg:0.0 |
count | 320 | 240 | r16f:g16f | rg:0.0 |
因为KlayGE别的地方都没用pre-multiplied的颜色,这里也不打算破例。所以初始值的alpha也是0。第二个RT记录的是depth和depth平方的总和,第三个RT用来记录每个pixel上积累的粒子数量。这样之后就能除一下得到平均值。
步骤3,混合
最关键的一步就是混合了。在这一步里,我们要用之前得到的信息,混合出一个边缘几乎没有锯齿的粒子渲染效果。这个步骤比较数学,会有多个公式。基本思想是,全分辨率depth map上的每一个pixel,都能和1/4分辨率depth transition里的信息带入一个函数,算出一个粒子可见的概率。这个概率就等于透明度。比如,如果一个场景pixel就在摄像机的位置(depth = 0),那么这个概率是0,粒子完全不可见。如果一个场景pixel远远超过了粒子的可能深度,那概率是1,粒子完全可见。
I3D 2006的著名文章Variance Shadow Maps(VSM)也是这个思路,记录depth和depth的平方,之后通过插值后的方差和切比雪夫不等式计算阴影的概率。对理解VDM的方法会有所帮助。当然,VDM在这里更复杂一些,选的函数是正态分布的累积分布函数(CDF),公式是
[latex]CDF = \frac12\left[1 + \mathrm{erf}\left( \frac{x-\mu}{\sigma\sqrt{2}}\right)\right] [/latex]
这个函数的图象是这样的,图来自wikipedia。
下面就来看看这个公式里的每个项如何得出。其中x是全分辨率的depth,[latex]\mu[/latex]和[latex]\sigma[/latex]在VSM的paper里就有。
[latex]\mu = M_1[/latex]
[latex]\sigma^2 = M_2 – M_1^2[/latex]
其中[latex]M_1[/latex]是粒子depth的平均值,[latex]M_2[/latex]是粒子depth平方的平均值。这两个都可以通过前面的depth transition和count相除得到。
erf的定义比较麻烦
[latex]erf(x) = \frac{2}{\sqrt{\pi}}\int_0^x e^{-t^2}\, \mathrm{d}t[/latex]
直接算是没可能了,但有很多近似。我所知道的最简单的一个是PG 2007里Fogshop: Real-Time Design and Rendering of Inhomogeneous, Single-Scattering Media附录中的公式
[latex]erf(x) = \begin{cases}
sign(x), & |x| > 2.629639 \\
(0.0145688 z^6 – 0.0348595 z^5 + 0.0503913 z^4 – 0.0897001 z^3 + 0.156097 z^2 – 0.249431 z + 0.533201)x, & |x| \le 2.629639
\end{cases}[/latex]
其中[latex]z = 0.289226 x^2 – 1[/latex]。
好了,现在我们已经一步步把每个项搞出来了,转成代码就能计算每个pixel的CDF,从而得到每个pixel上粒子可见的概率。用一个post process根据可见概率把粒子混合到场景中,得到的结果是这样的:
边缘仍然清晰,就好像用全分辨率渲染粒子的结果一样。那么速度呢?这里和前面几个放在一起比一把。
方法 | 时间 | 加速比 |
---|---|---|
全分辨率 | 0.337 ms | 1 x |
Naive 1/2分辨率 | 0.079 ms | 4.28 x |
Naive 1/4分辨率 | 0.016 ms | 20.91 x |
VDM 1/4分辨率 | 0.034 ms | 9.77 x |
从这张表可以看出,VDM的方法大约能有渲染全分辨率10倍的速度,同时仍然保持了几乎无锯齿的边缘。可以说,VDM在很大程度上解决了粒子渲染的速度问题。
细节
后处理的alpha blending
如何把一个透明物体渲染到纹理,在叠回不透明的渲染结果?这一点已经在前面提到的High-Speed, Off-Screen Particles中解决了。在积累粒子的时候,不但像平常一样混合颜色的部分,还把alpha的部分设置成SrcBlendAlpha = Zero,DestBlendAlpha = InvSrcAlpha,这样积累出来的alpha就能用于一次性混合到不透明的场景。
VDM的局限
原文里也有提到这个方法的一些局限,和解决方法。但KlayGE里还没实现这些。
- 如果粒子方差差别很大,VDM的近似会不精确。解决方法是限制方差的范围。实际上前面提到的保守depth,也对这个有好处。
- 按照方差排序粒子,而不是深度本身。这样保证可见概率大的更靠前,减少artifact。
VDM还能用在哪
这个方法不光能用在粒子的渲染。凡是半透明、需要巨大填充率、颜色变化较平滑的内容,都可以用VDM来渲染,最后一次混合到全分辨率的场景上。比如大气效果、体积光,都符合这个条件。
总结
VDM是个有效加速粒子渲染的方法。值得在引擎中尝试。但需要注意的是,VDM并不完美,有的情况下还是有可见的artifact。如何取舍需要视实际情况而定。
Comments