前言:
任何一种LOD (Level Of Detail) 方式最终想解决的问题,是在需要更多细节的地方尽量多的显示细节,也就是那些地方会有更多的面。解释一下:在没有LOD的时候,我们显示一个地形或者一个其他模型(LOD不仅是对地形而言),无论地形或模型跟镜头(视点)多远,在计算机内部运算的时候,都按照地形或模型拥有的所有面的数量来进行一系列基于数学模型下的运算,最终得到二维屏幕上的画面。在输出结果上,更远的模型占据到的屏幕的面积很小,极端情况可能就是几个像素。因此,计算机研究人员就一直致力于方法来优化这个状况。因为如果把很多不必要的运算(很多三维面经过几轮裁剪,最后没有在二维屏幕上显示)消除,就能让CPU或者GPU干点其他更有用的事情,毕竟资源是有限的。
LOD在这个需求下诞生,总体思想是,离视点远的物体或几何度相近的面,能合并的就合并到一起,尽量在不需要细节的地方,用很少的面代替到地形或物体的某个部分。这样,CPU和GPU在处理这个地形或模型的时候,相对处理的面就少很多。也就可以省下宝贵的CPU和GPU资源到更多的处理其他事务中去。现在很多被运用到的地形LOD技术,大多都是从最粗糙的网格开始,给处理器输入一些参数,让处理器在运行状态下对网格细化,得到最后的结果。
ROAM:
ROAM跟我上一篇博文提到的geomipmapping都属于LOD技术。但是更精确的说,ROAM属于CLOD (Continuous LOD) 技术,而geomipmapping属于DLOD (Discrete LOD) 技术。开源不久的法国商业游戏Ryzom,用到的3D引擎是Nel3D,根据官方文档,内部对LOD的处理就是ROAM
ROAM算法 (http://www.cognigraph.com/ROAM_homepage/),主体是把一个四方形的高程图,从最初分割为左右两个三角形做起始三角形,然后根据一定的参数将这两个三角形不停的循环细分成更小的三角形,直到最后的精度要求。按照我们上面知道的知识,也就是离视点最近的地方,应该最后拥有更多的三角形,而离视点很远的地方,相对三角形就少的多。
除了官方的文档,一篇站在应用角度更容易理解的文章的链接在[1],该篇文档如果参照关联的源代码会很容易理解,而且我在开始不容易理解的地方做了注释(中文注释)。所以,ROAM算法从这篇文章入手,会相对简单一些。要提到的是,关联的源代码是非常易读的,甚至具有原子性(ATOM),逻辑上几乎没有办法再细化和优化(逻辑上,不是算法上)。因此我把它直接拿过来做为示例代码,我唯一改变的是,原来的工程是在VC6下的,我把这个工程改成VC9(visual studio 2008)的。提前要说的是,本博文基本不会对原文提到的做重复,只是因为原文缺少不少流程和细节,我会对这些地方着重记录,所以,这篇博文是对原文的补充。
代码框架上,整个地形用一个Landscape类来做基础,每个Landscape管理多个Patch类,而这个Patch就是个正方形的高程度,ROAM算法作用的对象。这样安排的意图是,跟四叉树不同,ROAM算法以三角形为单位,相关的裁剪(Culling)就不像四叉树那样天生具备。因此,用Landscape管理Patch,每个Patch就相当于四叉树的每个节点,这样裁剪就能以Patch为单位进行 (注:示例代码用的不是四叉树的裁剪方式。用的方式出处在作者的orientation()的注释中)。 另外一个意图,在原文中有提到,用Landscape的方式,在实际运用中,一个很大的世界可以通过多个Landscape的方式管理。如果阅读Ryzom的源代码,能看到这个做法的影子。下图是一张虚拟世界可以用到的地图架构方式:

在代码中,用到的概念是图中的Landscape和Patch。ROAM算法对每个Patch进行细分,而在裁剪的阶段,每个裁剪的单位是一个Patch。如果从上往下看(bird-view),整个世界会这样被切分,下图:

到这,我们就可以集中到类Patch中,来看具体的ROAM算法。入手处(这个是个人喜欢的术语,很多学院派的论文,主要是开始找入手处难,如果找到,很多难点就迎刃而解)是Tessellate()方法。在这个方法中,一个Patch被分成左、右两个三角形,作为最初的两个三角形,进行细分。我们需要把上面的单独一个Patch进行放大了看,如下图:

上面的图是每个Patch的放大版,Left和Right分别是两个作为起始点的三角形。由Tessellate()作为启动点(pump),一个Patch就被慢慢的细分到合适的精度。参照下图观察一下细分的顺序:

这幅图从左向右看的话,显示了程序细分三角形的一定步骤。细分的原则是取底边的中点,从相对顶点划线对本来的三角形一分为二。这个过程用程序来实现是一个递归,用我们后面会提到的误差值和其他一些参数来控制需要到达的深度。细分的数据会被一个TriTreeNode的结构链表记录下来,关于这个结构,原文写的很清楚了,这里不再赘述。
我的这些示意图,加上原文的图,对理解示例代码应该足够了。正像我在上面就提到的,原文的示例代码是非常非常易读的,因此不管喜不喜欢,不要错过,我已经用VC9重新组织过工程,如果是VS2008或者VS2010,应该很容易编译运行。运行起来后建议按下Q键切换到网格(wireframe)模式,我们就能看到在视点的周围,网格是最精细的(可以通过按下F键来暂停动画后观察)。
但是即使原文和代码都很漂亮,我也需要把另外一些东西再重复一遍,因为这些概念是跟所有的LOD技法相关的,重复N多遍也是有意义的。这些是一个LOD需要通常面对的3大问题,如下:
(1) 误差指标(error metric)。
(2) 图像跳跃(Popping)。
(3) 断层 (Cracking / T-junction)。
第(1)个是一个/一系列指标,凭这个/些指标我们知道一个面可不可以进行再细分动作。在示例代码中,ResursTessellate()中依靠这些指标来知道一个输入的三角形能不能再细分。在一个基于四叉树的LOD中,可能根据绑定的指标知道一个四方形结构的中心需不需要被激活,从而更进一步的深入划分。
第(2)个是一个LOD技术会遇到的问题,产生原因跟下面的(3)的原因一样。但这个问题在游戏应用中可能不算是个问题,根据我看到的,像World Of Warcraft这样世界顶级的网游,对这个也是顺其自然,玩家在游戏中很少会觉得这样算是一个bug,甚至有时候我能把它看做一种效果。
第(3)个也是一个LOD技术会遇到的问题,产生的原因很简单,就拿上面的单独的Patch示意图(仅标示Left/Right的一个)来讲。如果Left不细分,我们对这个Patch渲染,不会产生cracking。但是如果对Left进行一次细分,也就是把Left分为两个三角形进行渲染,这个时候,cracking就产生了。原理是:这个时候对此Patch,我们需要渲染3个三角形,Left分成的两个,原来的Right是第三个。Left产生的两个三角形的共享顶点的高度值,我们是在高程图中读出来的。但是在Right这个三角形中,这个位置(平面位置)的高度值是通过Right的底边的两个顶点的高度值插值计算出来的。所以,除非插值正好等于高程图中的值,否则这里就一定会有一个高度上的差异。这个差异导致渲染出来的时候出现断层。
上面三点,在理解任何LOD技术的时候,应该都是线索。因此这里特意把它们拎出来。阅读原文的时候,如果按这三个线索去看,也能帮助自己分一下段,更容易理清这个算法。
参考:
[1],
附录:
1,我加了一些注释的原文,特别是红色解释示意图的部分文字,会对你有帮助。pdf格式,注释是中文:
http://www.163disk.com/fileview_323491.html
2,代码地址:
http://www.163disk.com/fileview_323457.html
声明:代码归原作者所有,使用请阅读相关说明。

浙公网安备 33010602011771号