喔嚯!你现在开始学习所有地形编程的核心部分,他们又难以置信的复杂算法组成。实际上,这是一个谎话。在这一章我将讲述3个算法,选择他们是因为他们简单而且高效。并且,再一次在这本书,我将让你省略漫长的介绍而简要的告诉你这章的事项:
n 连续层次细节(CLOD)含义的解释
n Geomipmapping(Geometrica Mipmap)的理论知识
n 实现GeoMipMapping的方法
为了简单,我打破了这3个事项。不管这些分类,这章就太庞大了。不过,别让这章的大小吓到你;这些具体内容将一如既往的用一种有趣且简单的方法描述。然而,注意我将改变一些学习的风格。第5、6、7章比前面的章节更多关注算法的讲解和伪代码。在随后几章,我仍然为你提供我自己的示例和实现,但这些实现是简单的,而且你应该只有在它们同文字关联起来时使用。照之前说的,我们开始吧。
CLOD Terrain 101
你已经在这本书里听过Continuous Level of Detail(CLOD)几次了,但现在是我告诉你它到底是什么的时候了。连续层次细节(CLOD)算法用一句话来说,是一个动态多边形网格“给”需要更多细节的区域额外的三角形。那是个简单的描述,但是你将在这节结束前明白更多关于连续层次细节(CLOD)的东西,你将更多知道这章结束了。如果你现在还没理解连续层次细节(CLOD)是什么,别烦恼。
CLOD 地形为什么麻烦
连续层次细节(CLOD)算法需要更多探究,比代码更难,平均比蛮力(brute force)方法占用更多CPU周期。同那些一起在你的心里,为什么你希望麻烦的连续层次细节(CLOD)算法?答案真的简单:为了创建更真实的、更细致的、最重要的、更快速的地形片。
更多细节添加到需要更多细节的地方
连续层次细节(CLOD)的一个基本想法是我们添加更多的细节(更多三角形)到需要它们的地方。举例来说,如果我们有一个相当平滑的地形片(见图5.1),比起我们想要一个复杂的多的地形片(见图5.2),我们想要平均较少的三角形。
图5.1 需要较少的三角形渲染这个平滑地形。
图5.2 需要许多三角形渲染这个复杂地形
然而,不是所有算法都担心需要细化的区域中三角形的分布。Geomipmapping不对需要细化的区域放置更多三角形,但是Rottger的四叉树(Quadtree)算法(在第6章,“攀登四叉树”)是这样做的。因此,做为一个整体来看对连续层次细节(CLOD)而言,对需要细化的区域增加更多细节,并不总是真的,但是绝大多数情况是。我已经完全把你搞混乱了吗?
剔出你以前从未剔除的!
另外可以确定的是连续层次细节基础(CLOD-based)算法它们允许比蛮力(brute force)算法拣选更多的多边形剔除。这意味着那些多边形不可见、不会发送给API。举例来说,我们从一系列地形片开始工作以实现Geomipmapping。如果一个图块是不可见的,我们清除潜在的289个渲染顶点(17×17的顶点块)象掉下一般。这极大减轻显卡加载的负担,并且剔除也没让CPU繁忙(剔除载CPU中进行)。用简单的方法,象一个整体样的我们让GPU和CPU两者都满意,也让主板高兴。
不是在CLOD地形大陆的每件事都高兴
尽管,使用连续层次细节(CLOD)算法有一些缺点。很神奇的,全世界的都帮助我写下这一节,在今天早些时候我的邮箱收到了8月发行的Game Developer Magazine。在大多数连续层次细节(CLOD)算法中主要的缺点是在每帧多边形网格更新时复杂的“薄记”。
这个“薄记”的缺点在大多数这些算法(Geomipmapping、Rottger的四叉树算法、ROAM)被设计时几乎不流行。那是因为在这些算法希望大部分工作量放在CPU,并且只有少量需要的信息传给GPU。从那时起,尽管事情完全改变了一小部分。现在我们希望比放在CPU更多的焦点放在GPU上。
包装你对CLOD地形的入门
很显然,如果Geomipmapping、Quadtree、ROAM算法都过时了,现在你就不会读到他们了,意思就是某人,某时对每个算法优化后仍旧让它们成为当今重要的地形渲染。在这个观点下,我们将走出连续层次细节(CLOD)的入门期,并且开始进入Geomipmapping实现的入门。
半弱的CLOD的Geomipmapping理论
Geomipmapping由Willem H. de Boer开发,是一个友好的GPU连续层次细节(CLOD)算法。它也是完美转变到CLOD大陆的简单算法。我们继续前进,你也许会想查阅实际Geomipmapping的白皮书,为了你方便浏览放在CD-ROM里。(Algorithm Whitepapers/geomipmapping.pdf。)
简单的基础
如果你对Mipmap纹理映射的概念很熟悉,那么Geomipmapping似乎对你来说也应该很浅显。它们的概念是一样的,除此之外我们不是对纹理而言,而是对地图片的顶点而言。继续Geomipmapping的概念,需要规则的地形片。意思就是,一个片的大小是5个顶点(5×5的格子)。5×5的块将有许多级别(level)的细节,级别0是最细致的,在这里面级别2是最粗糙的。如果你需要每个块不同级别的直观展示请看图5.3。在图中黑色顶点不会被送到渲染API,但是白色顶点会。
如果你想查阅Willem de Boer的Geomipmapping白皮书,你也许会注意到三角形排列方式在图5.3与白皮书中的方式有些稍微不同。这么做的原因在晚一点说,但是现在就只需要知道我这么做是有原因的。
现在是时候我们讨论Geomipmapping更多一点的内容。之前你已经知道了基础,但是现在是时候让你了解所有……
图5.3 地图块的三角形排列方式,最高细节排列在左边,最底细节排列在右边。
好的,几乎所有事。我也许会保留些信息。
如同我之前所说的,Geomipmapping与MapMip纹理映射相似,除了用陆地块代替纹理块以外。我们需要做什么呢,从我们在3D空间中的点(摄像机观察点的位置)开始,这让在观看者周围的所有块都是最高细节,因为这些块占据了用户视野的大部分。在一个特定的距离之外,我们将选择低一些级别细节的块。同时,在又一个距离外,我们选择更低级别的细节。图5.4形象的展示了这些内容。
如同你在图中所看的,这些块在观看者所在的当前区域是0细节级别(Level of Detail简称LOD),这个意思就是这些块是最高细节级别。随着块变的更远,他们变成级别1,这是第2高细节级别。在距离观看者更远的距离,图块是级别2,这是图像呈现的最低细节级别。
让三角形排列简单
早些时候,你也许注意到我在图5.3中使用的三角形排列方式与我让你查阅的Geomipmapping文章(在CD中或者互联网上,网址在这章的末尾)中有些不同。实际上你不必现在就打开那篇文章,图5.5中展示了文章建议的三角形排列看起来象什么样。
图5.4 在Geomipmapping算法中,随着图块远离观看者,选择更低的细节级别。
图5.5 Geomipmapping白皮书中呈现的作为图块渲染的三角形排列方式。
这种排列可能看起来像是一个比较好的主意,并且它在很多地方使用。(警告:我会在这里稍微转变下话题。)如果你计划用顶点缓冲渲染块,三角形带将有明确的传送路线,这是我给你的建议。然而,因为实现顶点缓冲渲染系统对API有非常高的依赖,我选择用直接方式渲染,因为如果有需要的话,这样更容易变换到其他API的语句。用顶点缓冲给渲染提供快捷的速度以增强任何地形实现,因为当你单独的发送每个顶点、纹理坐标、颜色等等给API时它减少当前的函数头。另外,大部分显卡喜欢顶点信息作为顶点缓冲的类型传递。在最后,我劝告你们使用顶点缓冲做为地形渲染。你将得到极大地速度的提升,并且它总有额外的成果值得完成它。如果你想看Geomipmapping用Direct3D顶点缓冲的技术实例,请看“Simplified Terrain Using Interlocking Tiles,”在游戏编程精髓,第2册。
不管怎样,是时候回到主题了。图5.3展示的排列方式是我们渲染图块要用的方式。这种排列提供给我们一个很大的好处:当我们需要的时候它让我们容易的跳过渲染一个顶点,这是非常经常的。它带我进入我们下一个讨论的话题。
砍的痕迹和裂缝,
但通常只是裂缝
经常的当你应对CLOD地形算法时,你必须应付断裂的对象。断裂的发生,在Geomipmapping中,是当高细节的块邻接着低细节块时(看图5.6)。
如同你从图片中所见一样,在左边的块比右边的块的细节级别高。我们的问题位于A、B两个点。这个问题是在A点的左边比在B点的细节级别高。意思是左边的块在A点会渲染精确的高度,但是右边的块只是取高于它和低于它的平均高度。(应该是说左右两边细节级别不同所以高度不同,高级别的那边高度更精确,低级别的这边高度是上下2个邻接顶点的平均值,左右高度不一样就会裂开了。)
图5.6 2个图块,不同的细节级别,并列一起。
这整个“断裂”东西可能似乎没这样大的数量,但是看看图5.7,展示了我没用抗裂痕度量的Geomipmapping实现的效果屏幕截图。
图5.7 没使用反裂痕度量的Geomipmapping实现的屏幕截图。
这不是精确的平滑地面,不是吗?只能在景象中看到裂开的洞。让我们修缮它吧!
断裂试验你的Geomipmapping引擎
断裂试验你的Geomipmapping引擎比听起来容易许多。某人(有可能是我)给你解释这个概念,你就会得到更多的好处,让整个进程如同……一样简单,好吧,某些事情简单。
我有2个可能的选择方法来修缮断裂问题。一个方法是给低数量细节的块增加顶点,这样有问题的顶点将与高细节块的顶点的高度一致。这个解决方法可能很丑陋,尽管如此,这意味着我们需要重新排列块(添加其他的三角形扇面)。
另外个解决问题的方法是删除更多细节块的顶点。这个方法无缝地、轻松地解决断裂问题。看图5.8看这是多么容易的事,简单的删除一个顶点修缮了裂缝。
图5.8 省略渲染A和B2个点的顶点,根除了裂缝。
尔裂缝的艺术在哪里?
你知道裂缝的起因并且知道怎么修缮它们。真正的问题是:你怎么知道什么时候修改它们?在根本上,当你渲染目前的块时,你需要测试这个块周围(看图5.9)以确定它们是否是低细节级别。如果它们是,你就会知道你需要删除一些顶点。
测试每个块不是很困难。你只需要实现一系列简单的if-else声明。(后面显示了伪代码。)
If LeftPatch.LOD is less than CurrentPatch.LOD
RenderLeftVertex= true;
Else
RenderLeftVertex= false;
If RightPatch.LOD is less than CurrentPatch.LOD
RenderRightVertex= true;
Else
RenderRightVertex= false;
If UpperPatch.LOD is less than CurrentPatch.LOD
RenderUpperVertex= true;
Else
RenderUpperVertex= false;
If LowerPatch.LOD is less than CurrentPatch.LOD
RenderLowerPatch.LOD= true;
Else
RenderLowerPatch.LOD= false;
图5.9 邻接的块需要被测试,看它们是否更低LOD。
看到它是多么简单了吧?在测试之后,渲染你的三角形扇面,你跳过较为粗糙的块的顶点。例如,如果右边的块是较粗糙级别的细节,而当前块是高细节级别(多部分的列/行三角形扇面被渲染),这时你只想跳过图块的右边远处的顶点(看图5.10)。
『小心
请小心你移除的只能是有必要的顶点。否则,可能由一个满顶点的图块结束这意味着没移除。例如,在图5.10中,图块由多部分的三角形扇面行和列构成。你不想删除每个扇面的右边顶点;你只想删除最右边列的扇面的右边顶点。』
这就是给你的简单的Geomipmapping理论!现在是时候了,我们实现所有东西,我们刚才学的。
图5.10为了防止与右边图块断裂,需要移除最右边列的扇面的右边顶点的图块。
实现Geomipmapping仅是稍微弱地CLOD
你知道Geomipmapping基础背后的理论,但是现在我们需要实现它。这应该不会使你脑袋很头痛。难的部分已经完结了,象往常一样,我们将每一次谈论一步。拿一些咖啡,锁上你的门,放一些好的音乐,开始吧!
拼凑它
因为Geomipmapping是一系列块组成的,实现从构造图块的数据结构开始,大概是一个好主意。结构真的不需要包含很多信息,我们需要包含的越少就越好。事实上,这始终是你在这本书看到的构造的最小的结构。别不太习惯它漂亮的大小!
所有的块结构真正需要的是2个变量。一个变量将记录当前块的细节级别(LOD),而另一个变量将存储从块中心到摄像机位置的距离。这就是给它的所有了!这是完整的块的数据结构。它们的代码在这里:
Struct SGEOMM_PATCH
{
float m_fDistance;
int m_iLOD;
};
它看起来象一个极小的结构,但是记住:大东西得到了小包装。如同这个包小一样,我们将时常地使用它,所以确定你花了时间记熟它的成员。
创造Geomipmapping实现的基础
是的,不再说那2个成员数据结构的无用的东西。现在我们开始从Geomipmapping引擎最劳累的地方工作――Geomipmapping类。在开始,我们需要一个指针保持我们的块信息,它会在我们的演示中在一些地方被动态的分配。接下来,我们需要算出块的尺寸(用顶点),还有在地形中每一边有多少块。块的大小会完整地给使用者,那么我们能让用户在他初始化这个类时具体指定他喜欢的块的大小。(我倾向于固定块的大小为17×17顶点,因为它提供漂亮的细节和速度的平衡。在这章的描述中假定这就是块的大小。)
『小心
Geomipmapping的实现是基于一个 像素的正方形高度图。这意味着你不能用中心点有位移的,不规则的高度图生成器生成的高度图。确保你所有高度图是由这个有缺陷组成的生成器生成。』(应该是指需要使用中心点正中的高度图,至少是矩形的吧,不能是不规则的。)
Geomipmapping的初始化
Geomipmapping初始化系统中,我们需要的所有东西是用户希望的块的大小。(我将重声我的建议是17×17的顶点。)在我们得到它之后,我们就能初始化这个系统了。
首先,我们需要计算出地形的每条边有多少块。我们通过把高度图大小按照单个块大小划分计算结果,如图5.11所示。
P代表每条边块的数量,h代表高度图的大小,s代表单独一块的大小。借助那个公式,看一下前面的图5.12,看一会之后我们将把什么变量带进这个公式。
在我们计算出每条边有多少块之后,我们需要分配每条边块数量(我们刚完成了这个值的计算。)的平方大小的地形块的缓冲区。
m_pPatches= new SGEOMM_PATCH [SQUARE( m_iNumPatchesPerSide )];
下一步,虽然这不是必要的初始化部分,我希望计算图块能完成的最大数的细节级别。注意这个最大数的细节级别是指最少细节的级别,最多细节级别是0。随着级别数的增大,细节减少。
图5.11 计算地形网格每条边块的数量的公式。
这是计算方法:
iDivisor= m_iPatchSize-1;
while( iDivisor>2 )
{
iDivisor= iDivisor>>1;
iLOD++;
}
我们在这里所要做的就是看有多少个循环让iDivisor下降成2。当iDivisor到达2,我们不能再下降任何更深一层了,由我们所控制我们已计算出细节级别。以17×17块的大小为例,我们最大数细节级别是3,这意味着对于任何单一的块我们有4个不同细节级别(0、1、2和3)选择。那些是为了初始化!现在我们继续前进到极其大的停工部分。
Geomipmapping的停工
这很简单并且照惯例的停止Geomipmapping系统。我们所需要做的是释放分配给图块缓冲区的内存和重置所有类的成员变量。
Geomipmapping的维护
这个地形的工作与我们过去的3章不同,连续层次细节(CLOD)地形算法需要每帧都更新(这也是为什么被叫做连续层次细节的原因)。大部分基本的连续层次细节(CLOD)算法需要在更新时期间做大量的维护工作,但是Geomipmapping不是其中之一。我们在更新函数中所要做的工作是相当微小的;它简单的由计算出我们的图块应该是什么细节级别(LOD)构成。
在我们的Geomipmapping实现的update函数中,我们需要更新每个块;因此,我们需要用双层for循环:
for( z=0; z<m_iNumPatchesPerSide; z++)
{
for( x=0; x<m_iNumPatchesPerSide; x++)
{
在循环里面我们要做的第一件事是计算从观看者的位置(摄像机视点位置)到当前块中心的距离。这个计算在你的高中数学课上看起来应该很熟悉,在那里他们播种距离公式到你的脑袋里。正好假使你象我一样你也在那个所有课程上从头到尾的睡过去了,它在这里(看图5.12):
图5.12 3D的距离公式。
伴随这个公式,看下图5.13看我们将把什么变量带进公式。
图5.13 与距离计算(从观看者到当前图块中心)有关的变量。
一样的距离计算的代码:
m_pPatches[iPatch].m_fDistance= sqrtf(
SQUARE( ( fX-camera.m_vecEyePos[0] ) )+
SQUARE( ( fY-camera.m_vecEyePos[1] ) )+
SQUARE( ( fZ-camera.m_vecEyePos[2] ) ) );
在我们计算出从观看者的距离,我们就能计算给块多少细节级别。在代码中,我计算这个级别用的硬编码距离。(我在代码片段的一些段落中是这么做的;你也许想掠过一点前面的直接看我说的是什么。)在你的引擎里,虽然,你会想更精确的方法计算图块应该的细节级别。例如,在Geomipmapping白皮书中,Willem de Boer 展示了一种屏幕像素测定算法所以当块改变它的细节级别时,太多的突变不会被呈现。
突变是当一个多边形物体改变到不同细节级别时出现的。这个变化也许明显或者不明显。例如,从级别1变到级别0不会导致许多突变因为一个级别1的块仍然很细致(对17×17块而言,至少是这样)。然而,从级别3变到级别2会引起相当小部分的突变因为三角形从8个变成了32个。虽然是同样的比例的三角形被添加到前者的块中,级别3到级别2的变化更明显。任何连续层次细节(CLOD)算法的一个重要目标是减少甚至是完全的消除突变。我们将在随后谈论更多。
无论如何,这本书里我的Geomipmapping实现,我简单的把细节级别(LOD)距离的变换硬编码。(我想留下这个习题公开给你,读者。是的,我知道我是个漂亮的家伙。)这里是细节级别变换的代码片段:(所谓把细节度变换的距离固定下来及是硬编,这样做好像是容易出现突变)
if( m_pPatches[iPatch].m_fDistance<500 )
m_pPatches[iPatch].m_iLOD= 0;
else if( m_pPatches[iPatch].m_fDistance<1000 )
m_pPatches[iPatch].m_iLOD= 1;
else if( m_pPatches[iPatch].m_fDistance<2500 )
m_pPatches[iPatch].m_iLOD= 2;
else if( m_pPatches[iPatch].m_fDistance>=2500 )
m_pPatches[iPatch].m_iLOD= 3;
这些距离使得速度和细节有效的结合起来。如果演示在你的图像卡上显示有点缓慢,你也许想改变这些距离,但是别这样做。我们将在这章后面做一些速度的最优化,所以别绝望!这些就是所有Geomipmapping图块更新内容……对现在来说,至少是这样。现在是时候到了任何地形实现有趣的部分:渲染它!
Geomipmapping的渲染
这也许是你将在这章碰到的最难的一节,这并不是很坏。它一时有点难懂,但是我会带你走过它。
一点一点的把东西划分开
最简单的渲染每个东西的方法是,我们需要的渲染就是把单独的渲染程序一点点的划分开,所以代码不会变得很臃肿。这可能会增加一些函数头,但是只要我们把那些东西弄的整齐漂亮,就不会太糟糕。
我计算它的方法,Geomipmapping类应该有一个高层次渲染函数,能把低层的函数连续添加进去。例如,最高层的渲染函数是Render。跟着Render的是RenderPatch、RenderFan。RendeVertex做为最低层。使用这些函数,函数头数量会增长一点,但值得注意的是我们减少了代码丑陋的地方。这个交易是值得的。如果你花了时间很难理解我计划用的这个设计,看图5.14。随着实现我们的渲染系统,让我们从低起点向高的路线工作。
RenderVertex函数
系统的顶点渲染函数不是什么特别的东西,但坦诚的说,它很小。我们会经常调用它,让它成为一个理想的内联(inline)函数。RenderVertex基于阴影值设置顶点的颜色,阴影值是从光照图取出和光线颜色的RGB各分量值复合而来。这时RenderVertex传送纹理坐标给渲染API(为细节映射,如果需要也为纹理颜色)。在那之后,简单的你需要传送的顶点坐标缩放比例给渲染API。这就完成了!
图5.14 Geomipmapping类的渲染构造。
RenderFan函数
每个Geomipmapping的块都能分散成许多小扇形,不管是1个块还是256个块都是如此。使用这个函数就清理了在RenderPatch里的很多代码,下一节会讨论RenderPatch。
所有块渲染函数做的就是渲染单个三角形扇面。因此,函数需要接收的参数有扇形的中心点和扇形一边的长度,这样函数就能渲染它。RenderFan也需要获取块的邻接信息。好吧,是一些信息。邻接信息是单独的扇形的。如果因为一个比较粗糙的块在它右边需要删除一个顶点,但是当前扇形是在块的中间,这时邻接结构会显示所有邻接是true。(只有在块的右面这条边的扇形需要担心顶点的移除。)如果这个扇形正在被渲染,然而,顶点的移除是需要的。例如,如果这个扇形在块的右面的边上,并且在当前块的右边的块是更粗糙的细节级别(LOD),这时当前扇形就需要移除渲染右顶点。
RenderPatch函数
块渲染函数是整个渲染系统中最为至关紧要的,因为如果不渲染块你就不会看到地形。大部分的反裂缝步骤就被放在这个函数里,其余反裂缝部分(顶点的移除)被放在RenderFan函数中。记住我给你看的伪代码在前面的“尔裂缝的艺术在哪里?”好了,我们需要实现它。我们需要填充一个“邻接结构,”它是个简单的数据结构包含了4个布尔值(boolean)来标识周围的块(左、右、上和下的块)是否比当前块细节级别高。如果邻接值被标记ture,我们就能标准的渲染这边的块,因为我们不需要特殊计量来防止裂缝。如果它被标记为false,我们需要做特别的测算。
【注释
当我们在这章谈到“较高级别细节”,意思是“更低的地形细节。”这是因为我们的级别系统是从高(低数量的地形细节)开始的,并且级别越低地形细节越多,由级别0结束是最多地形细节。只要别在我们谈论高/低细节级别时搞混就行了。】(意思是细节级别有0~3,从0开始到3地形的细致程度越来越少。)
在反裂缝步骤后,我们需要计算出从哪里开始渲染三角形扇面。这个比听起来稍微复杂点,但不是很难。这里面最难的部分是试图计算出三角形扇面的中心点距离。在这完成后,我们会感觉好到极点!
我们怎样计算出每个扇面的中心点相互的距离?好吧,我着手的方法,虽然它看起来有点奇怪,首先以单个块的大小开始尝试计算块被划分成了多少,由此得到每个扇相互中心点长度。首先从除数、块的大小开始,用while循环计算块的总大小被划分成多少。例如,如果块是0级,我们用单个块大小划分它本身的大小,这样我们渲染的每个扇都产生1个单位的长度。(我们不想缩放顶点直到我们运行RenderVertex函数。)一个级别1的块,每个扇的距离就会是2个单位,2级就有8个单位,等等。前面计算的代码如下:
fDivisor= ( float )m_iPatchSize;
fSize = ( float )m_iPatchSize;
iLOD = m_pPatches[iPatch].m_iLOD;
//find out how many fan divisions we are going to have
While( iLOD-->=0)
iDivisor= fDivisor/2.0f;
//the size between the center of each triangle fan
fSize/= fDivisor;
(意思是以级别0为基准的话,计算出大边上有多少个小边,因为是2的倍数,所以用循环计算被划分了多少,用总长度除这个分块数就是扇形距离的长度了,间隔的基准单位是以0级别的。原文循环中那条语句可能有点问题。)
这个计算还不是完全正确的;当我们用时,所产生的地形看起来象图5.15。
图5.15 哎呀!它看上去象是一些错误计算产生的!
这个计算哪里出错了呢?好,有个简单东西被我们做错了。我们想这个除数变量,fDivisor,是2的幂。记住当除数等于块的大小时,扇的距离是0级的1个单位长度对吧?好,从一个中心点到另一个中心点,我们需要至少2个单位间隔(看图5.16)。
你看到图5.16中顶部的一对扇形相互怎样重叠(产生出一个相当糟糕的结果)的了么?它们是1个单位的间隔。看到在底部的2个扇形完美的恰当的在一起了么?它们是2个单位间隔。好了,我们需要改变先前扇的中心点计算,这样使块的最小间隔是2个单位。你问我们怎么做?这很简单。我们只要设置除数变量的初始值是块大小的减1,这使除数变量一直是2的幂,由此修正我们之前的所有问题。看新代码。(fDivisor变成iDivisor这样我们能提升一点计算速度。)
图5.16 1个扇中心间隔为1个单位与1个块是2个单位间隔。
fSize = ( float )m_iPatchSize;
iDivisor= m_iPatchSize-1;
iLOD = m_pPatches[iPatch].m_iLOD;
//find out how many fan divisions we are going to have
While( iLOD-->=0 )
iDivisor= iDivisor>>1;
//the size between the center of each triangle fan
fSize/= iDivisor;
在这完成之后,渲染块的三角形扇变得轻松。只需要确定渲染的起点是fSize的一半因为第一个扇的中心点在那里。我们需要检查是否每个扇都需要移除顶点。意味着我们需要用到
//if this fan is in the left row, we might need to
//adjust its rendering to prevent cracks
if( x==fHanlfSize )
fanNeighbor.m_bLeft= patchNeighbor.m_bLeft;
else
fanNeighbor.m_bLeft= true;
//if this fan is in the bootom row, we might need to
//adjust its rendering to prevent cracks
if( z==fHalfSize )
fanNeighbor.m_bDown= patchNeighbor.m_bDown;
else
fanNeighbor.m_bDown= true;
//if ths fan is in the right row, we might need to
//adjust its rendering to prevent cracks
if( x>=( m_iPatchSize-fHalfSize ) )
fanNeighbor.m_bRight= patchNeighbor.m_bRight;
else
fanNeighbor.m_bRight= true;
//if ths fan is in the top row, we might need to
//adjust its rendering to prevent cracks
if( z>=( m_iPatchSize-fHalfSize ) )
fanNeighbor.m_bUp= patchNeighbor.m_bUp;
else
fanNeighbor.m_bUp= true;
//render the triangle fan
RenderFan( ( PX*m_iPatchSize )+x, ( PZ*m_iPatchSize )+z, fSize, fanNeighbor, bMultiTe x, bDeltail );
由填充一个单独的扇邻接结构,我们不需要改造块的邻接结构。扇的邻接结构是送到扇渲染函数的,用来找出是否任何顶点需要从渲染中移除。这些就是所有低级别渲染函数。现在我们简单的讨论类的高级Render函数,它是用户会用到的。我们由demo5_1走进这个世界!
Render函数
好了,我们刚才完成了我们简单的Geomipmapping的实现。你感觉兴奋激动了吗?我知道我是的!
在这个高级渲染函数中,我们需要循环遍历所有块调用RenderPatch函数,但是我们会有3个不同的块渲染循环。还记得我们的蛮力(brute force)实现吗?如果用户支持重纹理我们只需要一个渲染循环;不管怎样,如果用户不支持多重纹理,我们可能需要创建一个纹理图过程和细节图过程。2个不同的渲染过程对一个地形实现来说决不是好事情,然而,所以一个选项是避开细节映射过程在用户不支持多重纹理时。如果你从一开始挨着读这本书的每章那么你应该已经熟悉这个概念。你需要做的事只是循环遍历所有块并调用RenderPatch函数渲染它们。
这就完成了!我们完成了基础的Geomipmapping实现。看demo5_1(在CD里目录Code/Chapter5/demo5_1下)也看下demo的简单屏幕截图在图5.17中。在左边它展示了纹理/细节映射图像,在右边是相同图像的网格展示。注意在网格这边中,离你近的块有更多细节,离你远的细节较少。这就是漂亮的CLOD算法!
修改存在的问题
是,是的,我知道。我们刚完成的Geomipmapping实现中有些问题。例如,除非你有真的最高端的显卡,你可能有经验在前面的demo是个非常差的帧速率。(我在GeForce 4 TI4600上测试这个demo,这是在写作时市面上最好的显卡,我得到稳定的45-50帧每秒。)在地形块改变他们的细节级别时这个demo也受到突变的影响。我们讲修改所有这些而更多的在后续章节中,所以别担心!
给引擎增加一点动劲
首先,我想我们应该提升我们的实现。提升东西的速度很简单。它简单地关注于做一些视截体裁剪。
图5.17 一个纹理和细节映射地形截图(左)和相同的网格图像(右),从demo5_1取得。
因为我们的地形被划分成很多块,我们最好打赌只用视截体为基准剔除那些块,也就是不需要特别多的测试。
再次的剔出你以前从未剔除的
在这我们将做些基础的视截体裁剪。给CCAMERA类增加些视截体计算功能,其中基于的一些信息在Mark Morley’s的文章“Frustum Culling in OpenGL”中,这是我在互联网上看到的最有效的视截体裁剪教程。(那篇文章你可以在下面网址找到http://www.markmorley.com/opengl/frustumculling.html.)是的我的承认,我的数学知识不是很好(注意这本书没有到处都是复杂的数学!),但是这也许是个好事――除非你是超级的数学迷恋者,在这情况下我可能应该期待一些反感的邮件。
无论如何,这里是些我们将要做的基础内容。我们会裁剪超出视截体的地形块(图5.18这里你需要一个直观的感受)这样我们排出任何额外的GPU/CPU工作避免浪费资源。(如果观看者看不到它,就没有渲染/更新它的必要。)
我们需要对视截体测试地形块。为了这么做,我们用轴对称包围盒(AABB)包围块。(实际上,我们让立方体边缘比块大些。)这时我们对视截体测试它。以计算块的尺寸,我们取得中心和缩放比例变量(看图5.19)。
图5.18 视截体。
图5.19 用立方体包围Geomipmapping的块,对视截体测试。
因为我们仅处理块的中心,需要把大小的一半用函数参数传递(用立方体的平截体做交集)以计算基于块的立方体的角。我们也能得到一些更精确的盒子,但是在我的裁剪实验中,需要额外的“空间缓冲区”那么观看者不会在地形中看到不和谐(比如一个块是看的见一点但是无论如何也会被裁剪掉)。
现在你知道更多的关于裁剪和它对地形的实际使用,看demo5_2(在CD目录Code/Chapter5/demo5_2)并给你自己证明得到了多快的东西。例如,在图5.20中,超过910个左右快,我们只看到369个。在demo中,得到稳定的帧速(80-120fps),并且这用的高度图是demo5_1中的2倍大小。
不是很低劣!
图5.20 demo5_2的屏幕截图
今天突变,明天解决
下一个我们着手对付的问题比前一个更复杂。我们的目标是减少——甚至更好——清楚突变。有许多方法做这个,我会展示他们之中2个背后的理论。这些解决问题的实际实现方法还是会展示给你。
平滑它!平滑的更好!
首先解决突变问题的叫做Geomorphing。虽然这个名字听起来象一些在大型网格动画中预计听到的,这绝对不是对大型机器人故事做什么,而是有大量消除的能力。Geomorphing实际上是逐步平滑变换(morphing)一套多边形(象我们的Geomipmapping的块)顶点的过程,会改变它们的细节级别。就我个人而言认为mech thing听起来更有趣,但这实际上Geomprphing的意思更为有用些。
你问为什么这是有用?实际上这很简单。你看,当一个低细节级别的块,它近似许多高度区