|
最近看了一些关于雾的文章,把几篇文章按照我自己的理解融合了一下,文章来源都标明了,大家不要说我抄袭哦。欢迎大家一起探讨。
雾
雾是什么?
它形状千变万化,行踪飘忽不定,时而给人带来缥缈如仙境般的感受,时而给人一种神秘莫测的感觉。其实,雾只是漂浮在空气中,由成千上万的小水滴组成的可见聚合体罢了。
雾是这么常见的一种自然现象,相信大家都体会过。所以为了增加虚拟世界的真实感,在现在的3D游戏中大量运用雾化效果来营造气氛,增强场景的纵深感和距离感。其实,在游戏中运用雾效果的深层原因是为了掩盖硬件的不足。在绘制每一帧场景的时候,由于受到目前硬件的限制,只能绘制一定距离内的场景,在这个距离之外即后裁剪面(back clip plane)之后的部分是空白的。为了弥补这个缺陷,很多游戏采用雾来掩盖这些没有画出的场景。
那么,在游戏中是怎么产生这些雾的呢?
最简单的是深度雾。它假设空间中雾无处不在,并且均匀分布。所以对于观察者而言,他所看到的雾的浓度与他和目标物之间的距离是一个简单的线性关系。距离越远雾越浓。但是距离的计算本身不是一件简单的事情,它一般要进行平方根的计算,这样的计算量对于游戏这种需要实时反馈的应用显然是太大了,所以需要进一步的简化。我们可以假设观测者处于一个无穷远的位置,这样,距离就可以用目标物体的Z值来表示。
得到了雾的浓度,接下来我们需要确定观察到的雾的颜色。
观察者为什么能看见雾呢?这是因为雾吸收一部分光线,同时又散射出自己本身的颜色。雾的浓度越大,它吸收目标物体的颜色越多,散射出自己的颜色也越多。从直观上看,我们一般使用下面的式子来表示:
C= Cm*(1-Af)+Cf*Af (1)
其中,C代表观察到的颜色;Cm代表目标物原来的颜色;Cf代表雾的颜色;Af代表雾的浓度。
我们已经知道如何计算雾的浓度和颜色,那么我们如何在计算机里真正产生雾呢?
第一种可用的方法,也是目前运用的比较多的方法,就是Vertex Fog。把3D物体进行投影变换之后,用每个物体的顶点(Vertex)的Z值代表雾的浓度,用类似式一的式子计算并修改该顶点的颜色。这样我们就得到了一个充满雾的场景。但是如果3D模型的顶点不是很多,就会出现不精确的情况。
另外一种方法是fog table。所谓的fog table,就是建立一张浓度和距离对应的关系表。在绘制场景时,对每一个象素(pixel),用其Z值在fog table 中查找对应的浓度,再进行fog的计算。这个方法的速度很快,因为在使用Z buffer时,每一个点的Z值已经算出,而且查表的速度也是很快的。这种方法的优点在于,因为是对每一个象素进行计算,所以无论模型的顶点多少都不会影响绘制效果。但是并不是所有的硬件都支持fog table,所以这种方法的使用受到了限制。Direct3D是支持fog table的。
无论硬件是否支持fog table,它总会支持texture的。我们可以用1D的纹理来模拟fog table的效果,这就是纹理雾(texture fog)。
用纹理来模拟fog table有很多好处。首先,几乎所有的硬件都支持perspective corrected texture(在透视空间中正确做插值,需要双线性插值,如果不支持,纹理将会有明显的错误),但是很少有硬件支持perspective corrected color,所以vertex fog的效果常常不正确。其次,texture coordinate generation可以有很多种变形,不用Z值做雾的浓度,可以制作许多有趣的雾效果,例如即将要阐述的volume fog效果。
用纹理来做雾也是有很多缺点的。最主要的缺点就是成本太高。在一个本身就需要多重纹理贴图(如使用了light map效果等)的场景中,再增加一层纹理,对硬件而言,往往需要一个额外的pass,这就需要更大的带宽。
用1D的纹理做雾,还是有缺陷的。假设这样一种场景,观察者离雾很近,那么近处的雾应该比较淡一点,这样才符合现实的体验,但是用1D的纹理贴图不可能产生这种效果,因为这不是一种简单的线性关系,所以必须用2D甚至3D纹理才能制作出真正的雾效果。
从上面的阐述可以看出,深度雾可以快捷的产生雾效果,复杂度比较低。但是这种整个场景都充满均匀分布的雾的模型显然无法体现真实生活中的各种现象,例如在一个熬药的大锅上面经常有大团的水蒸气,但是场景中别的地方可以没有。为了表现这些复杂的模型,我下面将仔细阐述体积雾的原理。
雾中包含着成千上万的小粒子,这些小粒子不仅吸收来自场景中的光线,他们还要反射一部分的光线回到场景中去,粒子之间还存在反射、散射、吸收等问题,要建立这样一个模型简直太复杂了。为了能在计算机中模拟出这样的雾模型,必要的假设是必须的。
第一个假设:雾中的每一个粒子,无论它处于什么位置,它所吸收到的光线是相等的。
第二个假设:雾中的每一个粒子,它所反射的光线也是等量的,并且向所有的方向进行反射。
在这两个假设的前提下,给定一个球体雾,那么在所有的方向上将有等量的光线。
我们用一个多面体来代替雾,用一束光线进出该多面体的点的w值的差值乘以某些常数来近似表示雾的浓度(从数学上说,这是stroke定理的简化形式)。我们对每一个象素点都进行相同的计算。图示如下:
图一:用B-A近似表示雾的浓度
最简化的一个例子:
让我们现在来看看一个简单的例子。我们要渲染一个不包含任何物体(相机也不包含在内)的凸多面体。为什么要强调是凸多面体呢?因为只有在这种情况下,一束光线进出多面体的次数才不会大于两次。那么如何计算每一个进出的象素点呢?我们先渲染fog volume的正面,读取该点的w值作为A值。然后改变三角形的缠绕方向,再次渲染fog volume,读取该点的w值作为B值,B-A即为屏幕上该象素点的雾的浓度。对屏幕上每一个象素点都进行相同的计算,用A buffer存储前面(A点)的值,用B buffer存储背面的值,从B buffer中减去A buffer的值,留下的就是屏幕上所有象素点的雾的浓度值。
如何进行w值的操作呢?vertex shader把w值编码进alpha管道,所以我们可以把每一个pixel的值编码进alpha管道,相减后剩下的值就代表该pixel的雾的浓度值。图示如下:
图二:前面、后面以及差值
在本例中,算法可以描述如下:
1、 渲染fog volume的背面到一个后台缓冲区,用每一个pixel的w值代替alpha值。
2、 用相同的编码方式渲染该fog volume的正面,并且用新的alpha值去减后台缓冲区中的alpha值。
3、 用后台缓冲区中留下的alpha值作为雾的浓度值进行后续处理。
fog volume中包含物体的例子:
在实际的例子中fog volume一般包含有物体。在这种情况下,如果继续按照上面的算法进行渲染,则会出现比较明显的错误,如下图所示:
图三:左边是错误的情况,右边是正确的情况
出现错误的原因是很明显的。因为此时fog volume的背面已不再是我们所定义的fog volume的背面,而是物体的前表面,我们还按照先前定义的后表面进行渲染,计算出的雾的浓度自然就不正确了。
解决这个问题可以利用一个小技巧。先渲染包含物体的场景,打开深度测试,然后渲染fog volume的背面,如果某个pixel位于物体的后面,则被抛弃,物体该点则成为fog volume的后表面。
算法描述如下:
1、 清空缓冲区。
2、 激活深度测试,渲染整个场景(包含所有的物体)到后台缓冲区A,用每个pixel的值代替该pixel的alpha值。
3、 在激活深度测试的条件下,渲染fog volume的背面到同一个缓冲区A。因为打开了深度测试,所以深度测试失败的pixel被抛弃,物体前表面成为fog volume的实际后表面。
4、 渲染fog volume的前表面,用得到的w值去减缓冲区A中对应的值。
5、 用缓冲区A中的alpha值作为雾的浓度值进行后续处理。
fog volume包含一部分物体的情况:
前述的算法还存在一个缺点。当fog volume没有全部包含整个物体的时候,不在fog volume中的部分物体也被渲染到了后台缓冲区,这导致原本没有雾的地方也被雾笼罩了。我们可以用stencil buffer来解决这个问题,但是这里想讨论另外一种方法。在刚才叙述的算法中,我们用渲染场景的方式来正确渲染fog volume的后表面,这里我们可以利用同样的技巧来正确渲染fog volume的前表面,算法描述如下:
1、 清空缓冲区。
2、 渲染场景到后台缓冲区A,用每个pixel的w值代替该pixel的alpha值。激活深度测试。
3、 用相同的编码方式渲染fog volume的背面到缓冲区A。
4、 渲染场景到后台缓冲区B(或者在第三步之前拷贝A中的值)。
5、 渲染fog volume的前表面。因为打开了深度测试,所以物体位于fog volume之后的点被fog volume上的点替代。
6、 用B减去A得到的值作为雾的浓度值进行后续处理。
相机在fog volume中的情况:
我们前面讨论的都是相机在fog volume外的情况,如果相机移动到fog volume内部,这时的情形是什么样的呢?由于相机在fog volume内部,这导致fog volume的前表面被剔除,上述算法的第五步失效(因为fog volume的前表面已经被剔除)。最后渲染的结果是不正确的,场景中有的地方有雾,有的地方本该有点雾的现在一点雾也没有。
解决这个问题的方法也很简单。前面算法的第四步是因为物体部分处于雾中而添加的。当相机处于雾体中的时候,所有的物体都处于雾中,所以第四步可以完全跳过。
下面是渲染凸多面体雾的一个通常的算法:
1、 清空缓冲区。
2、 渲染场景到后台缓冲区A,用每个pixel的w值代替alpha值,激活深度测试。
3、 渲染fog volume的背面到缓冲区A。
4、 如果相机在fog volume体外,则渲染场景到后台缓冲区B中(或者在第三步之前从A中拷贝),否则,跳过这一步。
5、 渲染fog volume的前面到缓冲区B中,如果第四步执行了,则fog volume的前表面将代替物体处于fog volume后面的点,如果第四步没有执行,则这一步的执行效果被忽略,因为fog volume的前表面已经被剔除。
6、 用B减去A得到的值作为雾的浓度值进行后续处理。
进一步的优化:
前面所述的是一些基本的雾模型。有很多种方法可以进一步优化。讨论最多的应该是如何提高精度。现阶段的硬件的alpha管道只有8位,这限制了某些应用,如铺满整个地面的一大片雾。解决方法之一就是本文开始提到的纹理雾。用1D的纹理模拟fog table,每个纹元可以包含更高的精度。这种方法受到纹理大小的限制。当精度的问题解决后,我们就可以在现有8bit的机器上渲染凹多面体雾。第一种方法是把凹多面体分割成一系列的凸多面体进行渲染,第二种方法是累加进入的点去减出去的点。后一种方法由于受限于pixel shader而很难实现。
下面进一步阐述如何在8bit的alpha管道上获得更高的精度进行凹多面体的渲染。
凹多面体的处理:
在凸多面体的情况下计算进出fog volume的距离是非常简单的,但是对于凹多面体就不行了,因为一束光线有可能进出fog volume两次以上。解决方法如下图所示:
图四:(B1-A1) + (B2-A2) = (B2+B1)-(A2+A1)
从上图可以直观的看出,这相当于把一个凹多面体分割成了一系列凸多面体,然后进行相加。8bit的精度是不够的,每增加一位的精度可以增加一次加减法运算,可以实现更复杂的雾体。
在这里,凹多面体有一个前提限制。它必须是连续的、方向性的体,即不允许这个体中有孔(洞)。换句话说,一束光线进入体n次,出来也是n次。
获得更高的精度:
现在的32位机,每个管道实际上只有8位。并且只有在alpha blending unit中,雾的深度才可以累加。Alpha blender除了可以对alpha管道进行融合,它也可以对color管道进行融合,但是不幸的是,当每个管道的值超过255后,该值都将被截取位255。
为了在alpha blending unit上获得额外的精度,我们可以如下图所示的方式组织数据:
图五:利用数据组织方式提高精度
上图的数据组织方式可以让我们在8位的管道上获得12位的精度。Red channel包含高8位的信息,blue channel包含低4位的信息,加上3位的进位标志,首位没有被使用。
但是上述的过程pixel shader无法实现,主要原因有两个,第一:颜色差值只能处理8位;第二:深度计算是基于vertex shader的,vertex shader不会让高位的信息进入独立的颜色管道,即使颜色管道有更高的精度,pixel shader也没有指令来捕获更高精度中的低位信息。解决方法之一就是利用纹理。利用纹理有两个好处,第一:纹理差值可以获得比颜色差值更高的精度;第二:累加fog volume的前面和背面的时候不需要任何pixel shader。这种方法主要受限于纹理的大小。现在的硬件一般允许的纹理最大尺度是4096,这意味着我们通过纹理可以获得最高12位的精度。虽然这也是一个不足之处,但相比8位已经能够让我们作出非常好的效果了。
完整的过程:
有三点需要仔细考虑:fog volume面的累加,补偿处于雾中的物体,和最后的相减。
累加可以分三步完成:首先,渲染整个场景到Z buffer中。然后,在一个缓冲区中绘制所有的正面,对结果进行累加;在另一个缓冲区中绘制所有的背面,对结果进行累加。
在上面的描述中没有考虑volume中包含物体的情况。Fog volume中包含物体的情况在凸多面体的fog volume中已经很好的解决了,但是同样的方法在凹多面体的情况下会产生错误。如下图所示:
图六:fog volume中包含物体的情形
在上图中,如果不考虑物体本身的深度,那么该点计算出的深度值为B1-A1-A2,因为B2没有通过深度测试,永远不会被画出。该点的雾的浓度值为负值,显示的结果是该点没有雾,实际上应该有雾。在这种情况下,物体本身的深度值C必须加到方程中。
该解决方案需要确定某一点是否需要加上该点的场景中的深度值。确定这些点也比较简单,哪些位于fog volume的前表面和后表面之间的pixel就是需要加上该点的场景深度值的点。这个问题可以转化成这样一种形式:从某pixel进入的光线是否穿出了volume。通过一个简单的inside outside 测试可以解决这个问题。
Inside/outside测试:每一次渲染,该pixel的alpha值就增加1,如果出点和入点的alpha值相减不为零,则通过该点的光线没有出volume,相减为零证明通过该点的光线穿透了volume。
接下来的处理过程就比较浅显了:fog volume的前表面和后表面在不同的buffer中分别被累加,场景在另外的buffer中进行渲染。后表面的buffer与前表面的buffer相减,在alpha值不为零的点加上场景buffer中对应的深度值。
相应的pixel shader实现如下:
ps.1.1
def c1, 1.0f,0.0f,0.0f,0.0f
def c4, 0.0f,0.0f,1.0f,0.0f
tex t0 // near buffer B
tex t1 // far buffer A
tex t2 // scene buffer C
// input:
// b = low bits (a) (4 bits)
// r = high bits (b) (8 bits)
// intermediate output:
// r1.b = (a1 - a2) (can't be greater than 7 bits set )
// r1.r = (b1 - b2)
sub r1.rgb,t1,t0
+sub_4x r1.a,t0,t1 //If this value is non zero, then
mov_4x r0.a,r1.a //the were not as many backs as
mad r1.rgb,r0.a,t2,r1 //front and must add in the scene
dp3 t0.rgba,r1,c4 // move red component into alpha
// Need to shift r1.rgb 6 bits. This could saturate
// to 255 if any other bits are set, but that is fine
// because in this case, the end result of the subtract
// would have to be saturated since we can't be
// subtracting more than 127
mov_x4 r1.rgb,r1
dp3_x4 t1.rgba,r1,c1 // move into the alpha
add_x2 r0.a,t0.a,t1.a // the subtract was in 0-127
mov_d2 r0.a,r0.a // chop off last bit else banding
+mov r0.rgb,c3 // load the fog color
该pixel shader给出了alpha值代表雾的浓度值,并且加载了雾的颜色到color管道中。
还有一种导致错误的情况我们前目没有考虑。前面的例子都是假设相机完全处于fog volume外,或者完全处于fog volume内部,如果相机部分位于fog volume内部,则部分fog volume被裁剪,错误同时发生了。
一个权宜的方法是禁止多边形裁剪。Vertex shader可以检测出哪些顶点将要被裁剪掉,然后使该点对齐到近裁剪面。下面的vertex shader使w值对齐到近裁剪面,z值变为0。
// transform position into projection space
m4x4 r0,v0,c8
max r0.z,c40.z,r0.z //clamp to 0
max r0.w,c12.x,r0.w //clamp to near clip plane
mov oPos,r0
// Subtract the Near clipping plane
add r0.w,r0.w,-c12.x
// Scale to give us the far clipping plane
mul r0.w,r0.w,c12.y
// load depth into texture, don't care about y
mov oT0.xy,r0.w
参考资料:
1、 Volumetric Rendering in Realtime By Charles Boyd and Dan Baker ; Gamasutra October 3,2001
2、 Texture-based fog By Ping-Che Chen; Game Resource
3、 Fog ; By 朝三暮四郎; Game Resource
[em2]
E:\fog |
|