您的位置:首页 > 产品设计 > 产品经理

使用基于GPU的Geometry Clipmap渲染地形(上)

2010-11-07 20:10 357 查看
使用基于GPU的Geometry Clipmap渲染地形(上)

使用基于GPU的Geometry Clipmap渲染地形(Terrain Rendering Using GPU-Based Geometry Clipmaps)(上)

翻译:clayman
clayman_joe@yahoo.com.cn
仅供个人学习使用,勿用于任何商业用途,转载请注明作者^_^

注:这篇文章翻译的是《GPU Gem2》中第二章的内容,老早就翻译的东西了,但因为翻的不太好,一直放着,找不到原版的PDF文档,截图有些是在网上找的,有些是我自己做的,大家凑活看了,基本和原图还是差不多的。《GPU Gem2》中的内容实在很难,本人水平有限,有错的地方还请大家指正。

Geometry clipmap是用于渲染地形LOD的新方法。它把地形缓存在一组嵌套的规则网格中,同时在观察点移动时,不断更新这些数据。和之前不规则网格(irregular-mesh)技术相比,规则的网格有很多优点:简洁的数据结构、平滑的视觉效果,稳定的渲染速率,合适的分级(graceful degradation),高效的压缩,以及运行时的细节合成(synthesis)。这里,我们将描述一种通过顶点纹理(vertes texture)实现的几何clipmap。把地形几何体作为图片来处理,几乎所有计算都能通过GPU来完成,从而减少CPU的负担。

1.1 Geometry clipmap简述
在大型户外环境中,处理地形数据将会占用大量的储存空间以及渲染带宽。我们已经开发了多种LOD技术来处理地形。但是,大多数技术需要在运行时创建和修改mesh数据(顶点和索引缓冲),而这些操作对当前的图形处理技术(graphics architecture)来说代价是很大的。此外,不规则的网格通常需要使用CPU来处理,而在诸如游戏之类的应用中,CPU本来就不够用了。
Gemetry clipmap框架中(Losasso and Hoppe 2004)把地形作为一张2D的高度图(elevation image)来处理,并预先把它过滤为一张包含L层的mipmap金字塔。



处理复杂地形时,完整的金字塔可能对内存来说太大了。Geometry clipmap结构对每一层进行窗口大小为n x n的采样并缓存,这和Tanner 1998介绍的texture clipmap方法有些类似。对观察者来说,这些窗口相当于一组嵌套规则的网格。注意,高细节层次所占的空间总是比较粗糙的层次少。这样的目的是保证在屏幕空间,所有三角形都统一大小(the aim is to maintain trangles that are uniformly sized in screen space)。如果clipmap大小为 n = 255, 那么在1024 x 768的分辨率下,每个三角形大约是5个像素。



只有最高的层次(L-1级)渲染为完整的方形网格。其他层次都渲染为空心环,因为空心部分已经由较高层次填充了。
当观察者移动时,clipmap窗口也作相应的改变,同时更新数据。为了保证高效的持续更新,以一种环形的方法来访问clipmap每一层的窗口,也就是说使用2D环绕寻址(the clipmap window in each level is accessed toroidally, that is ,with 2D wraparound addressing)。
其中,挑战之一就是如何隐藏相邻层次之间的边界,同时,保证一个完美的网格,避免临时的撕裂效果。Geometry clipmap的嵌套网格结构提供了一种简单的解决方案。其中,关键的思想就是每层在靠近外层边界的地方引入一个交换区域(transition region),这样,几何体和纹理都能平滑的通过插值过渡到下一个粗燥级别。使用顶点和像素着色器,可以分别高效的实现这些交换区域。



Geometry clipmap的嵌套网格结构同样能实现高效的压缩以及合成。它可以通过对较粗糙的层次的数据进行upsample(提高取样率),预测(prediction)每一层的高度数据。因此,我们只需要储存多余的细节信息并合成到这个预测的信号上就可以了。

1.2 Overview of GPU Implementation
Geometry cliipmap最初的实现方法把每一层数据当作普通的顶点缓冲。由于当前的GPU缺乏修改顶点缓冲数据的能力,所以这种实现在更新数据和渲染时都需要CPU参与。而这里,我们将使用顶点纹理来实现geometry clipmap。这样做是有益的,因为clipmap 2D的窗口网格数据更适合保存为一张2D纹理,而不是通过人工线形化处理保存为1D的顶点缓冲。由于clipmap包含L层,每一层又包含了n x n的几何采样,我们将把采样的(c,y,z)信息分为两部分:
* (x,y)坐标储存为常量顶点数据
*Z坐标储存为一个单通道(single-channel)的2D纹理——高度图(elevation map)。为每层都定义一个n x n的高度图。这些纹理都将在clipmap层次根据观察点变换时更新。
由于clipmap的层次是统一的2D网格,(x,y)坐标也是规则的,同时,变换和比例也是常量。因次,我们定义了一组只读的顶点和索引缓冲,用来表示2D“footprints”,同时,在每个层次都重复使用这些footprints。
顶点通过把高度图作为顶点纹理来采样,获得高度值。在顶点着色器中访问纹理是DirectX 9 Shader Modle 3.0的新特性,NVIDIA Geforce6 系列的GPU都支持这个功能。把高度数据保存为图片,可以直接使用GPU的光栅管道(rasterization pipeline)来进行处理。对于需要进行合成操作的地方,所有运行时的计算(高度图的upsample,地形细节合成,法线计算,以及渲染)都完全由GPU来执行,这样就可以留出宝贵的CPU资源)。对压缩地形来说,就需要CPU不停的为图形卡解压和更新数据。

1.2.1 数据结构
总的来说,主要的数据结构如下。我们预先定义一组顶点和索引缓冲常量,为clipmap的网格(x,y)编码。对0…..L-1中的每一层,分配一张高度图(单通道浮点2D纹理 1-channel floating-point 2D texture)和一张法线图(4通道 8-bit 2D纹理)。以上所有数据结构都保存在显存中。

1.2.2 Clipmap的尺寸
由于每层的外边界都必须覆盖在下一个粗糙层次的网格上,网格尺寸n必须是奇数。当纹理尺寸是2的幂时,硬件可以进行优化,所以我们选择n = 2^k-1,保留一行一列作为未使用的纹理。我们的例子中大多数情况下使用n = 255。
选择 n = 2^k-1还有一个优点就是较好的层次永远不会位于下一个粗糙层次的中心(the finer level is never exactly centered with respect to its parent next-coarser level)。换句话说,就是对于下一个层次,它在x或y方向总有一个网格单位的偏移,具体偏向那个位置则根据观察点的位置来决定。事实上,当下一个粗糙层次不变时,应该允许较好的层次可以改变,因此,较好的层次有时就必须进行偏移。当然,我们也可以选择n = 2^k+3作为网格尺寸,来达到中心对齐的效果,但是这样仍然需要处理中心偏移的情况,导致最后的结果过于复杂。

1.3 渲染

1.3.1 有效层(Active Levels )
虽然我们为clipmap分配了L层,但是通常只渲染(和更新)一组有效的层次0……L’-1,L’的值基于观察点的高度来计算。这样做的目的是,当观察点足够高时,clipmap中最好的层次就不必渲染了。特别的,我们把网格范围小于2.5h的层次作为无效层,这里,h表示观察点到地形的垂直距离。由于地形数据都储存在显存中,计算h将导致重新读取最好的有效层高度纹理图采样点数据。这个重新读取的工作将带来一些小小的额外开销,因此,每过几帧检测一次。当然,需要把L’-1层渲染为一个完全的方形而不是空心环。
Losasso和Hoppe2004描述的方法中,对于在观察者移动时没有完全更新的层进行了裁剪(cropping)。为了简化GPU的实现,我们放弃了这个特性。我们假设每个层是完全更新的,或者完全无效。

1.3.2 顶点和索引缓冲

正如之前所说的,我们把网格的(x,y)值作为顶点数据,同时,z方向的高度值保存为单通道的浮点纹理。为每层的环定义一个单独的顶点缓冲来保存(x,y)数据是一种有效的方法。但是,为了减少内存空间和实现可视区域裁剪,我们把环分为一些小的footprint片。



大部分环都由12个m x m的渲染单元块(block)组成,如图中的灰色区域,这里m = (n + 1) / 4。因为2D网格是规则的,同一个层中的(x,y)都进行同样的变换。此外,不同层次之间也可以使用统一的缩放值。因此,使用一个只读的顶点和索引缓冲,以及一些额外的参数,就可以让vertex shader对每个渲染单元块进行缩放和变换,完成所有渲染 。
当clipmap大小为255时,每个规则的渲染单元块包含64x64个顶点。使用顶点缓冲来保存这些三角形带的索引。为了减少储存空间,将使用16位的索引值作为索引缓冲,这样,渲染单元块的m最大值就为255,同时,clipmap中n的最大值为1023。
但是12个渲染单元块的大小并不能完全覆盖整个环。我们使用一些小的2D footprint来填充这些裂缝。注意,这些额外区域的大小和渲染单元块相比应该是很小的,就像上图所示的那样。首先,环每条边的中间都有 (n – 1) – ((m -1) x 4) = 2个方格的裂缝,图中黄色部分,可以使用4个m x 3的固定区域来填充他。我们把这些区域编码为一个顶点和索引缓冲,并且对所有层复用这个缓冲。第二,在内层环的两条边界处会产生一个方格大小的裂缝,图中蓝色部分,以满足较好层次中心偏移的效果。这条L形的裂缝可能出现在四个位置(左上,左下,右上,右下),我们将使用4个顶点以及一个索引缓冲来填充这条内部的裂缝,使用对所有层次复用这几个缓冲。
此外,还需要在环的外边界渲染一系列退化(degenerate)三角形(图中橙色的部分)。为了避免T-junction效果,这些0区域(zero-area)三角形是必须的。最后,使用4个额外的渲染单元块以及一个L形区域填满整个环。
由于每个footprint(x,y)都为局部坐标,所以,不需要32-bit的精度,使用D3DDECLTYPE_SHORT2类型就可以了,这样每个象素只需要4 bit。将来,我们甚至可以在每个m x m大小的小的块内使用顶点索引i来计算这些坐标(x,y),比如(fmod( i , m) , floor (i/m))。

1.3.3 View Frustum Culling
我们以块为最小单元在CPU上进行view frustum culling计算。每个块都扩张到[ Zmin, Z max]的范围内,并且在三维空间内由可视平截体进行分割。只有在分割快不为空时,才对它进行渲染。根据观察点的位置,对于90的观察范围来说,可以把渲染负载减少到原来的1/2 ~ 1/3之间,如下图所示。



1.3.4 关于DrawPrimitive调用
对于每一层来说,我们余姚调用14次DrawPrimitive(DP)方法:12个块每个一次,为footprint调用一次,为剩下的三角形再调用一次。但是,在视平截体裁减之后,平均每帧只需要渲染12个块中的4个。而对于最好的层来说,则需要多调用5次来填充中间的空间。因此,总的来说,平均每帧需要调用6L+5(当L=11时为71)次DP方法。更进一步,使用Geometry Instancing技术把每个层次中的所有大块作为一个统一的整体调用一次DP,则可以把DP的次数减少到 3L + 2 (35)

(未完待续,下一部分将给出具体的代码实现)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: