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

(这个功能本来应该是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地缩小分辨率,会得到什么样的结果。

Full Resolution

机器后面的火焰是用全分辨率渲染的粒子。注意观察粒子和机器交界的边缘,放大后是这个样子的。这是我们的ground truth,之后的结果都跟这个比。

Full Resolution Zoom

如果用半分辨率渲染粒子,则会变成:

Naive Half Resolution放大看已经能看到一点锯齿。

Naive Half Resolution Zoom
再进一步,用1/4分辨率渲染粒子。

Naive Quarter Resolution放大看一下,锯齿的程度已经无法接受了。

Naive Quarter Resolution Zoom

比较一下这几个分辨率下渲染粒子的速度,就会发现和分辨率非常相关。

分辨率 时间 加速比
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),公式是

这个函数的图象是这样的,图来自wikipedia。

下面就来看看这个公式里的每个项如何得出。其中x是全分辨率的depth,在VSM的paper里就有。

其中是粒子depth的平均值,是粒子depth平方的平均值。这两个都可以通过前面的depth transition和count相除得到。

erf的定义比较麻烦

直接算是没可能了,但有很多近似。我所知道的最简单的一个是PG 2007里Fogshop: Real-Time Design and Rendering of Inhomogeneous, Single-Scattering Media附录中的公式

其中

好了,现在我们已经一步步把每个项搞出来了,转成代码就能计算每个pixel的CDF,从而得到每个pixel上粒子可见的概率。用一个post process根据可见概率把粒子混合到场景中,得到的结果是这样的:

VDM Quarter Resolution
还是放大那块区域,看看边界。

VDM Quarter Resolution Zoom
边缘仍然清晰,就好像用全分辨率渲染粒子的结果一样。那么速度呢?这里和前面几个放在一起比一把。

方法 时间 加速比
全分辨率 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里还没实现这些。

  1. 如果粒子方差差别很大,VDM的近似会不精确。解决方法是限制方差的范围。实际上前面提到的保守depth,也对这个有好处。
  2. 按照方差排序粒子,而不是深度本身。这样保证可见概率大的更靠前,减少artifact。

VDM还能用在哪

这个方法不光能用在粒子的渲染。凡是半透明、需要巨大填充率、颜色变化较平滑的内容,都可以用VDM来渲染,最后一次混合到全分辨率的场景上。比如大气效果、体积光,都符合这个条件。

总结

VDM是个有效加速粒子渲染的方法。值得在引擎中尝试。但需要注意的是,VDM并不完美,有的情况下还是有可见的artifact。如何取舍需要视实际情况而定。

Save

Save