雾的浓度越大,它吸收目标物体的颜色越多,散射出自己的颜色也越多。从直观上看, 我们一般使用下面的式子来表示: 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 值做雾的浓度,可以制作许多有趣 1 的雾效果,例如即将要阐述的 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 的值,留下的就是屏幕上所有象素点的雾的浓度值。 2 如何进行 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 位于物体的后面,则被抛弃,物体该点则成为 3 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 来解决这个问题,但是这里想讨论另外一种方法。