最近在研究水面渲染,眼看马上要毕业交论文了,还什么都没有弄,大意了~~~。Tessendorf 2001发表的那篇论文真是影响深远,无论FFT还是顶点纹理的实现方法,本质上都是波纹叠加。简单对比了两种实现的demo,顶点纹理的实现CPU占用率确实很少,用任务管理器看大概就30%左右,而FFT的demo只要一开就是100%。不过顶点纹理只有GeForce 6以上的显卡能跑,而且ATI的卡似乎不支持顶点纹理,应该算是它最大的局限性。另外,对LOD的实现来说,也是方法多多,看了几篇论文,实现都不一样,还在研究中-_-#。
这段时间慢慢把看的文章都翻译整理出来,下面这篇是《GPU Gems 2》中的第18章,讨论了如何使用顶点纹理置换来渲染水面。个人觉得这篇文章讲的不太细致,而且很多问题,比如如何表现水面的深浅、纹理映射,都没有讲,对于法线生成的方法也只是大概提了一下。当然,总的来说,还是值得一看的,特别是文中使用了一种特别的LOD处理,比较有趣。
Using Vertex Texture Displacement for Realistic Water Rendering(上)
水面在计算机图形学,特别是游戏中,是一种常见的效果。它是增加场景真实性的重要元素之一。但要模拟真实的水面,却是一个难题,因为水面的运动和光学效果都相当复杂。这篇文章描述了游戏Pacific Fighters中,用来渲染水面的技术。
支持DirectX Shader Model 3.0的硬件提供了许多相当有用的特性,可以用来帮助渲染水面。下面,我们就将讨论如何使用顶点纹理(Vertex Texture)来渲染真实的水面。此外,我们还使用了braching技术来提高渲染效率。
最终的结果,左图使用了displacement mapping,右图则没有
1.1水体模型
对水面动画和渲染来说,已经发展出多种方法。其中,最出名也是效果最真实的,就是基于流体动力学和快速傅立叶的方法(FFTs)(比如Tessendorf 2001中描述的)。这些方法可以提供非常真实的渲染效果,但不幸的是他们需要相当大的计算量,因此,不适合于交互式的实时渲染。
此外,现在大多数游戏使用的都是相当简单的模型,大部分方法仅仅是通过改变水面法线创建水面效果。使用这些方法渲染水面,虽然相当高效,但真实度很低,而且并没有真正在水面产生任何波纹。
因此,我需要一种综合了两种方法优点的技术来渲染水面。
1.2 实现
我们的实现中,水面渲染将依赖于法线图(normal map)来进行光照计算。因为法线图可以忠实的重现高频率波形下的所有细节。此外,使用低频率、高振幅的波对水面网格进行扰动。
1.2.1 水面模型
我们的水面模型基于多张,在空间和时间上都进行了分割(tiled)的高度图(height map)的重叠。每张纹理都代表了频谱(spectrum)中的一个“谐波(harmonic)”或者“倍频程(octave)”,和那些使用FFT的方法一样,将把这些纹理叠加到一起。这些纹理就是高度图,每个像素元代表了对应位置的水平高度。
对于艺术家来说,创建高度图是很简单的:创建它们就和绘制一张简单的灰度图一样。使用高度图,艺术家只需分别绘制单个波纹的形状,就可以可以很容易的控制水面动画。把高度图当作顶点纹理来使用也是很方便的:使用它来置换(displace)顶点的垂直位置是很有效的。
一张用来置换水面位置的高度图
使用不同的空间和时间缩放比例,混合多张高度图,我们可以获得相当复杂的水面动画效果:
为了实现真实可信的水面,应该合理的选择参数A、B和i的值,以最小化波纹的重复效果。对于Pacific Fighters来说,我们混合了4张高度图来进行光照计算,其中比例尺较大的两张还将用作置换贴图。这样,我们可以模拟比例尺从10cm到40km大小的水面。
1.2.2 实现细节
可以把所有需要进行的运算分为两类:几何体置换计算和光照计算。因为水面镶嵌(tessellated)良好,因此,可以把光照计算放到片断(fragment)程序中,而把置换映射放到顶点程序里。当然,把光照计算放到顶点处理阶段也是可以的,特别是对于远处的顶点来说。
在写这篇文章时,唯一支持顶点纹理的硬件就是GeForce 6系列以及最新的Quadro FX GPU。在这些硬件上,使用顶点纹理有一些特别的限制,只能使用32bit的浮点纹理,只能使用nearest filtering。
1.2.3 采样高度图
我们的实现中,对高度图中的每个顶点进行采样,在顶点程序中计算置换值。对于采样来说,使用了一个放射状的网格,它的中心位于摄像机所在位置。按照这个样子镶嵌的网格,可以让靠近观察者的地方提供更多细节。
下面的公式说明了如何计算放射网格中的顶点位置:
这里i = [ 0….N-1], j = [ 0…..M-1]。我们选择
r0 = a0 = 10cm
rN-1 = a0 + a1( N – 1)4 = 40km
这种基于距离的镶嵌,提供了一种相当简洁的LOD方法。当然,这里也可以使用其他的地形渲染算法来实现LOD,比如ROAM或者SOAR,但这就需要占用一定的CPU资源进行计算,这完全违背了使用顶点纹理的初衷。
下面的代码显示了如何在vertex shader中,使用放射形网格对一张高度图进行采样:
float4 main(float4 position : POSITION,
uniform sampler2D tex0,
uniform float4x4 ModelViewProj,
uniform float4 DMParameters, // displacement map parameters
uniform float4 VOFs) : POSITION
{
//read vertex packed as (cos(),sin(),j)
float4 INP = position;
//transform to radial grid vertex
INP.xy = INP.xy * (pow(INP.z,4) * VOFs.z);
//find displacement map texture coordinates
//VOFs.xy ,DMParameters.x - height texture offset and scale
float2 t = (INP.sy + VOFs.xy) * DMParameters.x;
//fetch displacement value form texture (lod 0)
float vDisp = tex2d(tex0, t).x;
//scale fetched value form 0...1
//DMParameters.y - water level
//DMParameters.z - wavy amplitude
INP.z = DMParameters.y + (vDisp - 0.5) * DMParameters.z;
//displace current position with water height and project it
return mul(ModelViewProj,INP);
}
1.2.4 质量改进和优化
Packing Heights for Bilinear Filtering
顶点纹理拾取(fetch)是相当耗费资源的操作。在GeForce 6系列的硬件上,可以在顶点程序中引入一个顶点纹理拾取器。我们必须最小化在顶点程序中进行纹理拾取的次数。此外,对纹理中的值进行过滤(filtering)也是必须的,否则视觉效果将会大打折扣。
一般来说,最常见的过滤方法就是双线性过滤和三线性过滤。双线性过滤将对最靠近纹理选取坐标位置处的四个像素元(texels)进行均值计算。三线性过滤则需要对邻近mip层次中的双线性过滤结果进行均值计算,同时根据不同的LOD层次选择每个层次的混合权重。
由于当前的图形卡无法对顶点纹理中的值进行任何形式的过滤,我们不得不在shader中直接使用数学运算指令来模拟过滤。对于不好的实现来说,即使是最简单的双线性过滤也需要通过通过四次纹理查找来计算一个过滤值,而三线过滤的查找次数更是翻了两倍。
为了减少过滤时的纹理拾取次数,我们用一种特殊的方法来创建纹理,让一个像素元就包含了一次双线性纹理查找所需要的所有数据。这是一个相当可行的方法,因为高度图本质上就是一张单通道(one-component)的纹理,我们可以把4张高度图打包为一张每个像素元四通道的纹理:
这里i = [ 0….N-1], j = [ 0…..M-1]。H表示高度图的值,F是过滤函数,A则是打包好的输出纹理。
下面的代码实现了对顶点纹理进行双线性查找
float tex2D_bilinear4x(uniform sampler2D tex,
float4 t,
float2 Scales)
{
float size = Scales.x;
float scale = Scales.y;
float4 tAB0 = tex2Dbisa(tex, t);
float2 f = frac(t.xy * size) ;
float3 tAB = lerp(tAB0.xy, tAB0.yw, f.x);
return lerp(tAB.x, tAB.y, f.y);
}
我们可以把这个方法扩展为三线性过滤。因为三线性过滤需要局部LOD的值,因此,我们把顶点到摄像机的距离作为LOD的近似值。下面的代码实现了对打包之后的顶点纹理进行三线性查找:
float tex2D_trilinear(uniform sampler2D tex
float4 t,
float4 Scales)
{
float fr = frac(t.z);
t.z -= fr; // floor(r.zw)
float Res;
if(fr < 0.30)
Res = tex2D_bilinear4x ( tex, t.xyzz, Scales);
else if (fr > 0.70)
Res = tex2Dbilinear4x(tex, t.xyzz + float4(0,0,1,1), Scales * float2(0.5,2);
else
{
Res = tex2D_bilinear4x(tex, t.xyzz, Scales);
float Res1 = tex2D_bilinear4x(tex, t.xyzz + float4(0,0,1,1), Scales * float2(0.5,2));
fr = saturate ( ( fr – 0.30) * ( 1 / ( 0.70 – 0.30)));
Res = Res1 * fr + Res * (-fr) ;
}
return Res ;
}
注意,这里我们进一步对三线性纹理选取进行了优化,只对影响较大的两个mip层次区域进行纹理查找。对其他区域来说,直接把他们的LOD值设置为最相邻mip层次的值,从而节约纹理带宽。(未完待续ing~~~)
Using Vertex Texture Displacement for Realistic Water Rendering(下)
作者: Yuri Kryachko
使用Branching避免不需要的计算
即使优化了纹理拾取方式,渲染水面时的纹理拾取次数还是很高,将严重影响性能。虽然我们能减少需要渲染的顶点数量,但这会影响视觉效果,并且带来锯齿。
由于我们渲染了大片的水面,因此有些三角形有可能一直延伸到了屏幕之外。但即使是这些三角形,顶点程序仍然对他们进行了渲染,浪费了宝贵的计算资源。如果我们渲染时跳过那些摄像机视见体(frustum)之外的三角形,则可以节省许多计算量。
由于顶点程序同一时刻只能处理一个顶点,无法访问拓扑信息,因此,我们只能在顶点层次进行优化,而无法在三角形层次优化。虽然,当三角形中的某些顶点被挑过了,而其他顶点没有时,可能出会一些问题。但是在实践中,我们发现,如果三角形以及顶点纹理置换的位置足够小时,这种效果就不太明显。
下面的伪代码描述了这个思想:
float4 ClipPos = mul ( ModelViewProj, INP)
float3 b0 = abs(ClipPos.xyz) < (ClipPos.www * C0 + C 1) ;
if ( all (b0))
{
// vertex belongs to visible triangle,
//perform texture sampling and displace vertex accordingly
}
这段伪代码里,我们使用裁减空间的顶点纹理来判断当前顶点是否在视见体之内,之后,对于所需要的顶点,再进行余下复杂的计算操作。
这里C0和C1是特殊的裁减常量,控制了三角形延伸出摄像机视见体多少距离将会触发裁减。这样,就能避免由于裁减了那些顶点在视见体外,但三角形还在视见体内的顶点,而导致的问题。出于效率的考虑,我们的“裁剪”视见体只需比摄像机视见体稍微宽一点就可以了,以保证延屏幕边缘有一定的“安全区域(guard-band)”。因为我们的水体表面镶嵌良好,同时顶点纹理置换也很合理,所以这个简单的方法在实践中效果很好。
使用渲染到纹理(Using Render-to-Texture)
可以先使用一个单独的pass,把多张高度图混合为一张浮点纹理,从而进一步提高程序的速度。这样就不必在vertex shader中多次执行昂贵的拾取操作。此外,使用高度压缩的16-bit浮点纹理格式储存原来的高度图。另外,使用3D纹理,我们还可以储存一系列的高度图动画序列帧(a sequence of animated height maps),让水面动画更加平滑。
使用这种优化,需要把渲染循环分为两个pass:
1. 用一个单独的pixel shader把多张高度图混合为一张32-bit的浮点纹理。这张纹理中的象素元对应了放射状mesh中的顶点。
2. 使用前面描述的方法,置换顶点位置。
Back Sides of Waves
由于在pixar shader中进行的光照计算假设水面是平坦(flat),因此在某些特定的情况下,这种近似会导致一些问题。
如下图所示,即使背对着观察者的波纹,也会被看到,而实际上,这些波应该是不可见的。这种效果将会对波峰较亮部分产生影响。
为了减轻这种效果,需要对用于光照计算的法线进行调整,让它朝观察者的方向“倾斜(tileing)”一点,这样,他们就会靠近波纹的前面。 可以在源码中找到具体代码。以下是使用本章所介绍的技术,渲染的一张水面效果图。
1.2.5 渲染局部波纹扰动(Rendering Local Perturbations)
有时候,需要渲染由于物体漂浮或者落入水中,所引起的局部波纹扰动。特别是对游戏来说,这是很有用的,我们通常需要产生爆炸效果,船航行时的轨迹,等等。对基于高度图的水面模型来说,很难引入物理上正确的扰动实现方法,我们将讨论一种简单的方法。
形变模型分析(Analytical Deformation Model)
实现局部波纹的扰动,最简单的方法就是根据分析结果,再次改变置换之后的顶点位置,在vertex shader中把重新计算的顶点位置和置换之后的位置进行加和。例如,对于爆炸效果来说,可以使用以下公式:
这里,r表示水平面上的点到爆炸中心的距离,b是抽样常量(decimation constant)。I0、w和k的值则根据给定的爆炸参数来确定。
至于渲染,同样可以使用前面渲染水面时,所用的放射状网格,但网格的中心必须是爆炸点。
动态置换贴图(Dynamic Displacement Mapping)
另一种选择是,把局部产生的偏移值直接渲染到顶点纹理中,实际上就是在GPU上实现通用编程(general-purose programming on the GPU)(GPGPU)的方法。使用这种方法,先在第一个pass中产生一张顶点纹理,随后的pass中用它来进行实际渲染。对高度图进行过滤,在pixel shader中,再把所有波形加和到一起,可以大大减少vertex shader的工作量。
为了计算偏移值,可以使用上面提到的分析模型,也可以使用一钟细胞自动机(cellular-automata)的方法,逐帧展开(evolving)局部偏移。沿着一定的方向对纹理进行模糊处理,还可以实现风吹过水面的效果。
但是,对于大小为1km,分辨率为50cm的水面来说,意味着纹理尺寸为2048x2048,这将给纹理内存和shader的执行速度都带来极大压力。此外,想要快速改变观察点也很成问题。
然而,我们仍然鼓励读者去尝试一下这些方法。
泡沫生成(Foam Generation)
在波涛汹涌的水面上,我们可以生成一些泡沫来进一步增加真实感。最简单的方法就是对于高度超过值H0的顶点,使用一张预处理过的泡沫纹理进行混合。根据以下公式来计算泡沫的透明度:
Hmax表示可能出现泡沫的最高位置,H0为基准高度,H表示当前高度。
泡沫纹理可以是动态的,以表现泡沫的生成和消失。这些动画纹理序列可以由艺术家手动创建,也可以用纹理生成。
1.3 结论
略
~~~~~~~~~~~~~~~~~~~~~~~~·全文完~~~~~~~~~~~~~~~·~~~~~~
译注:
另外还可以参考NVIDIA SDK中的示例程序Vertex Texture Fetch Water
Using Vertex Texture Displacement for Realistic Water Rendering,布布扣,bubuko.com
Using Vertex Texture Displacement for Realistic Water Rendering
原文:http://www.cnblogs.com/kylegui/p/3843816.html