室外地形生成与渲染总结

来源:互联网 发布:手机淘宝详情顶部活动 编辑:程序博客网 时间:2024/04/28 15:29

GameRes游戏开发资源网 http://www.gameres.com

 

 

室外地形生成与渲染总结

 

北京汇众益智科技有限公司

产品研发部

莫原野

 

2006 4 3

简介

室外地形生成与渲染是各类虚拟场景的基础,其剔除和渲染结果直接关系到应用程序的效率和图像显示的结果。我根据网上流传的GeoMipmaps地形生成的原理,再加上对使用此技术的游戏进行初步的研究,从中得出一些比较实际和快速的地形实现方法。

本文使用的技术参考和大部分资源来自电子艺界(EA GAMES)的以下游戏或工具:

《战地风云1942》(Battlefield 1942

《战地风云 越南》(Battlefield Vietnam

《战地风云2》(Battlefield 2

《战地风云2 编辑器》(Battlefield 2 Editor

原理

       地形生成方面:GeoMipmaps技术,一种预先生成不同细度地形方块模型并在渲染时根据需要选用不同细度的模型进行绘制的技术。它可以在保持图像质量的基础上减少多边形数目,同时不像ROAM技术那样动态消耗CPU时间。

       地形渲染方面:使用可编程管线,可以轻松的解决多层纹理混合以及地形层次切换时的突变问题,同时可以使用更加复杂的运算来增强图像效果。

 

 


地形生成方面:

高度图

       本文介绍的地形模型是根据高度图(HeightMap)生成的。

高度图是一张记录高度信息的图片,它的长宽是我们所需要地形大小的长度加以1所得出的值(2^n+1),它的单元格式可以是任意格式,但是它必须符合应用对地形细度的要求,一般情况下使用单字节或双字节作为一个单元的大小,这已经足够一般虚拟场景使用了,如果需要也可以使用四字节或更高。

高度图除此之外还应该附加以下信息:

长宽,表示高度图的长宽所占的单元数目减1(2^n)

缩放标尺,表示在虚拟场景中所使用的高度图上两个单元之间相隔的距离;

低细度缩放标尺,表示低LOD级别与高LOD之间细度的差别,本文所介绍的地形系统使用的是3层次LOD过渡,《战地2》使用的是2层次过渡。

下图展示了,《战地2编辑器》创建新地形的对话框。

 

       高度图生成可以直接在这个编辑器中生成,也可以通过一些图像编辑工具生成,其格式为RAW。此外,还可以使用3DMax等建模工具制作高度图。


 

地形方块

       细节分级地形的最基本单元就是地形方块(Patch),它是我们实现地形的最基本骨架。

       地形方块将拥有不同细度层次的子模型,子模型根据与之相对应的LOD层次的低细度缩放标尺来生成。最高层次的子模型的低细度缩放标尺为1,也就是不简化。我们的地形模块定义的低细度缩放标尺的规范是:0~2LOD层次分别对应低细度缩放标尺为2^n。《战地2》的规范是:0~1LOD对应低细度缩放标尺为124(这个由编辑器生成地形时用户决定)。

       地形方块拥有长宽属性,也就是0LOD下该子模型边缘分段数目,我们的地形模块此参数为16,可以在配置文中设定。其占用高度图单元数目为长宽数值加1

       地形方块继承高度图属性,它也拥有缩放标尺属性。

       地形模型生成是一个非常重要的过程,它直接关系到输出结果的质量。在这个过程中,比较关键的是,地形模型的网格编制方式 顶点法线的生成。

       比较普遍的地形网格编织方式是三角形扇,其优点是方便生成,原理简单,但是其覆盖面积小,容易造成绘制指令调用过多的情况。因此三角形条带的编织方式被越来越多的开发者所采用。其优点是可以编织出覆盖范围大的网格模型,而且比三角形序列要节省资源(它使用到的顶点数目少)。我们的地形模块所采用的编织顺序为:左右循环。在每一行的末尾需要重复一个顶点作为换行之用。在此又分为两种编织方式,前一种用于固定管线渲染,更节约索引缓冲,但是存在层次切换突变问题;后一种用于可编程管线,可以解决层次切换突变问题。

如下图:

 

固定管线模式

 

可编程管线模式

       生成顶点法线,比较普遍的方法是,对该顶点周围顶点高度采样,计算高差,通过反三角函数获得法线矢量的方向。以下是具体过程,其中m_verTemp保存顶点法线的矢量信息

       float h[4];

     h[0] = GetHeightFromHMapFast(X-1,Y,0);

     h[1] = GetHeightFromHMapFast(X+1,Y,0);

     h[2] = GetHeightFromHMapFast(X,Y-1,0);

     h[3] = GetHeightFromHMapFast(X,Y+1,0);

     float rot = atan2f((h[0]-h[1]),m_fLayerScale);

     m_verTemp.nx = sin(rot);

     m_verTemp.nz = cos(rot);

     rot = atan2f((h[2]-h[3]),m_fLayerScale);

     m_verTemp.ny = sin(rot);

     m_verTemp.nz += cos(rot);

 

       在地形模型的保存方面,我们的地形模块使用顶点缓冲保存0LOD下所有相应顶点的位置,同时使用索引缓冲保存不同LOD层次下的子模型索引信息。

       顶点的信息格式与我们使用的绘制方式息息相关,下面我分别就固定管线和可编程管线介绍两种不同的顶点信息格式。虽然固定管线对图形加速卡要求没有可编程管线那么严格,但是它使用的定点信息格式比较臃肿,而且不灵活。下面您将体会到这一点。

       固定管线下,需要包含位置、法线、不同层次贴图的坐标。其顶点信息格式声明为:

D3DFVF_D3DVERTEX2_FIX (D3DFVF_XYZ|D3DFVF_NORMAL|D3DFVF_DIFFUSE|D3DFVF_TEX2)

       可编程管线下,不需要包含贴图坐标,因为这可以通过着色器程序即时运算出来。我们仍然保留一层贴图坐标的数据,但它并不是用来保存贴图坐标,而是用来保存不同LOD级别下顶点的高度信息,我定义LOD层次为3层,所以除去0层高度信息保存在位置数据里,其余两个高度信息将被保存在这一层贴图数据中。顶点信息格式声明为:

D3DFVF_D3DVERTEX2 (D3DFVF_XYZ|D3DFVF_TEX1)

       地形方块除了保存地形模型的数据以外,还包含一些附加的参数属性,见一下数据结构:

struct BLOCK

{

BOOL m_bCreated;//地形模型是否被创建

BOOL m_bDraw;//是否绘制

BOOL m_bDrawLight;//是否被光源照射

BOOL m_bLodChanged;//LOD层次是否改变

int m_iCenterXp,m_iCenterYp;//地形方块的中心在虚拟世界中的位置

_int16 m_iUsageCount;//地形模型资源维持时间计数器

……

};

       这些参数有些是针对固定管线的特性所加入的,在使用可编程管线渲染的情况下没有使用的意义。

       在地形方块资源管理方面,我们的地形模块使用的动态地形模型资源处理的方式,即只有当渲染剔除代码判定某地形方块可见时,他的资源才被生成,如果不可见,则维持一个资源维持周期计数器,当计数器数值小于0的时候,此时不可见地形方块的地形模型将被删除。在挂载我们的地形模块的试验程序中,资源维持计数器的起始值为1,所以资源最多只被保存一个渲染周期,这是一种极端的情况,在场景变换很快的情况下会降低程序运行效率,但是内存占用会被限制到最小。一个1024*1024的高度图生成的地形方块数组所包含的地形模型需要占用近40兆的内存空间,使用动态资源管理后仅需要十几兆大小的地形模型常驻内存。

 

 

细节层次选择与视觉剔除

       一个好的细节层次选择与视觉剔除方法可以极大地增进虚拟仿真程序的运行效率,并最大可能的保证渲染出优质的画面。反之,一个糟糕的方法或者不进行这些处理,则将使因用程序运行十分低效甚至在广大群众和批评家面前成为失败的笑料。

       目前,网络上可以查找到众多或简单或复杂的算法,我们应该如何选择?我的想法是,简单、够用,以这个商业运作的原则来指导我们解决问题。所以,我采用的算法均比较简单,但是这并不代表它们不能产生比较好的结果。这些算法分别如下:

细节层次选择方面:

距离选择,即根据每个地形方块的中心位置与摄像机之间的距离差别来决定地形方块当前的细节层次;

边缘整理,这是针对固定管线设计的,作用是通过动态检测每个地形方块边缘与相接的方块的细节层次差异,对地形模型的顶点缓冲中处于边缘的顶点的高度信息进行重写,从而达到保证相邻地形方块边缘处顶点不错位,消除撕裂现象;代码如下:

/*   //BK的边缘进行整理

     for(DWORD i=0;i<m_dwBKNumX*m_dwBKNumY;i++)

     {

         if(!m_bkTerrainData[i].m_bDraw)continue;

         if(!m_bkTerrainData[i].m_bLodChanged)continue;

         if(m_bkTerrainData[i].m_iLodLevel>=4)continue;

         DWORD X=(i%m_dwBKNumX);

         DWORD Y=(i/m_dwBKNumX);

         if(X<m_dwBKNumX-1 && Y<m_dwBKNumY-1&& X>0 && Y>0)

         {

              if(m_bkTerrainData[i].m_iLodLevel<m_bkTerrainData[i-1].m_iLodLevel)//如果是左边缘

                   FormatBKEdge(i,0,m_bkTerrainData[i-1].m_iLodLevel);

              else FormatBKEdge(i,0,m_bkTerrainData[i].m_iLodLevel);

              if(m_bkTerrainData[i].m_iLodLevel<m_bkTerrainData[i+1].m_iLodLevel)//如果是右边缘

                   FormatBKEdge(i,1,m_bkTerrainData[i+1].m_iLodLevel);

              else FormatBKEdge(i,1,m_bkTerrainData[i].m_iLodLevel);

              if(m_bkTerrainData[i].m_iLodLevel<m_bkTerrainData[i-m_dwBKNumX].m_iLodLevel)//如果是上边缘

                   FormatBKEdge(i,2,m_bkTerrainData[i-m_dwBKNumX].m_iLodLevel);

              else FormatBKEdge(i,2,m_bkTerrainData[i].m_iLodLevel);

              if(m_bkTerrainData[i].m_iLodLevel<m_bkTerrainData[i+m_dwBKNumX].m_iLodLevel)//如果是下边缘

                   FormatBKEdge(i,3,m_bkTerrainData[i+m_dwBKNumX].m_iLodLevel);

              else FormatBKEdge(i,3,m_bkTerrainData[i].m_iLodLevel);

         }

         //

    }*/

渐进式细节层次过渡,这是针对可编程管线设计,它可以有效防止地形撕裂和层次切换突变的出现。《战地风云2》使用的就是这种方式。根据此原理,我尝试编写了一些着色器代码,基本实现了这几项功能。其做法是在顶点信息中保存不同细节层次下该顶点的高度信息,在顶点着色器中根据顶点与视点距离来插值计算该顶点当前应该处于的位置,使得在细节层次发生改变之前,顶点的位置就统一到一个细节层次上。

 

视觉剔除方面:

距离检测,这种方式再简单不过,就是检测摄像机与方块中心的距离,以确定此地形方块是否处于有效视觉范围之内。一般这个距离与天空盒的大小相关,在《战地2》中,这个距离跟地图参数中的视觉距离绑定;

视锥检测:将每个地形方块的包围盒投影到屏幕,判断其是否出现在屏幕范围内;

下图展示了使用不同视觉剔除方法所获得的图像与结果:

 

渲染加速方面:

深度排序,这是根据渲染的一般常识来做的,目的是保证由近向远渲染不透明的物体,有利于GPU进行早期Z深度剔除,从一定程度上提高程序运行效率。我使用的是中点排序法对待渲染地形方块队列排序。

 

 


地形渲染方面

《战地风云2》的标准地形包含众多渲染阶段,相对应着众多的显示特性。在我们的地形系统中仅仅选取了其中比较简单、实用的阶段来组建我们的地形渲染机制,它们是:多层贴图混合(包含水深着色)和动态点光照。此外,除动态点光照阶段以外,其他阶段均包含顶点深度雾的操作。

多层贴图混合:在这个阶段,我们的地形系统使用一层描述地形基本颜色和轮廓的贴图一层光照图(LightMap&TerrainNormalMap)以及多达8层的灰度细节贴图来表现地表。不同细节贴图的选择和过渡,是通过使用一张带阿尔法通道的索引贴图来实现的。

目前有两种索引方式:

一、  三通道二元插值法:RGB通道确定在四种细节贴图族中过渡,而A通道决定在单个细节贴图族中,两层细节贴图之间的过渡。每种细节贴图族分别对应红、绿、蓝、黑四种颜色。下图表现了,我们是如何通过索引贴图来确定细节贴图的混合程度的:

 

二、三通道二元色环法:RGB通道决定在八种细节贴图中的选择,每种细节对应一种颜色,根据索引色与其差异来确定此细节在当前点上的权重(不支持黑色作为索引色)。此种索引方式的代码效率比前一种略低,单是它支持任意颜色作为索引色,这样生成索引图的约束就比较小。前一种,三通道二元插值法可以看作是本方法的特例。下图表现了,我们是如何通过索引贴图来确定细节贴图的混合程度的:

 

 

       当然有人会说还有别的过渡方式可以支持更多的细节贴图混合,不可否认,但是这种方式有以下两个好处:首先,它兼容《战地风云2》的6层细节索引贴图,应用人员可以直接使用《战地风云2编辑器》的地图工具来生成这张索引贴图;其次,这种方式也方便美工使用绘图工具来绘制索引贴图。我们的地形系统中定义的四个细节贴图族是沙地(15)、草地(26)、岩石(37)和路面(48)。

 

动态点光照:光照模型比较简单,仅包含漫射光计算。优化方面,在视觉剔除的末期,检查地形方块是否有可能受点灯光列表的影响,仅有那些可能受影响的地形方块才参与光照绘制;其次,加入了阿尔法通道测试,根据光照与像素的距离计算阿尔法值,仅有阿尔法值大于一定限度时,该像素才混合到当前绘制目标上,这样可以减少阿尔法混合带来的高像素填充率的需求。

 

水深着色:这是为了表现水下景观随水深加大而逐渐被水的反射光所遮盖而加入的。此阶段也使用阿尔法通道测试进行优化。

 

       下图展示了最终渲染得出的图像:

 


天空的表现

天空的实现包含天空体和天空贴图两部分。

天空体是在3DMAX中事先制作好的模型,在程序中使用时,将根据视野范围大小调节天空体的缩放,以符合渲染的需要。天空体包括天空外壳和太阳两个子集。以下为天空体模型的图片:

 

天空的渲染:天空的渲染包含天空基本贴图和两层云层贴图。云层贴图为即时计算,天空基本贴图的坐标使用一次曲线优化,以解决圆球顶端的贴图不够精细问题。最后根据高度计算深度雾的比例。

太阳的渲染:渲染太阳模型的时候,根据太阳模型顶点到相机矢量和相机观察矢量之间的计算太阳光的强度,以此模拟观察太阳时的光晕。

以下是天空和太阳的渲染效果:

 

 

 


水面的表现

水面模型的生成

水面模型是作为一个成员包含在地形方块当中,但仅当此方块包含有低于水面高度的顶点时,才生成水面的模型。水面模型比较简单,仅包含两个三角形。

水面的渲染

水面由一层环境贴图、一层光照贴图和两层法线贴图组成。根据以上参数,可以模拟水面的高光反射、环境映射和水波。

以下为水面渲染的结果:

 

在我们的地形系统的水面表现中,水面反射没有使用实时的反射贴图,因为这将导致重复渲染全场景,使程序运行缓慢。

如果使用ShadeMode3.0,通过在顶点着色器中读取表达水面起伏的高度贴图,将可以很方便快速地模拟真实起伏的水面。

 

最终渲染结果

 

 


 

下一步需要完善的地方

一、使用相邻传播的可视判断检测顺序;

二、更真实的水面:

       [1]:为场景渲染水面反射贴图;

       [2]:将水面渲染到场景中;

       [3]:待全场景渲染完毕,取得深度缓冲,在此深度信息上将矢量贴图渲染到全画面凹凸贴图;

       [4]:进行视频后处理时进行全画面矢量扭曲。

[5]:或者使用ShadeMode3.0开发真实波动的水面。

三、场景中生成细小物品,比如,草,水草等。

四、当摄像机在水下时如何更好的表现水流的扰动和水深度,以及水下的光影效果。

 

下载附带例子:http://dev.gameres.com/Program/Visual/3D/BattleTank.rar