eXtreme3D

常用链接

统计

最新评论

2009年1月13日 #

新版本WOWMAPVIEWER发布

添加了occlusion culling功能,当前的OC还是一个演示版本,由于WOW的世界中没有很好的遮挡物,因此我只好使用距离camera最近的wmo物体的AABB作为遮挡物,对地形块进行遮挡剔除。

打开occlusion culling需要使用功能键F8,同时为了方便观察遮挡效果又增加两个键:

NUM_3 显示occluder和occludee的box;

NUM_4 显示遮挡效率。

为了更好的观察效果,建议打开OC功能后直接选择数字键3。

下载地址:wowmapviewer

posted @ 2009-01-13 10:15 Dreams 阅读(292) | 评论 (3)编辑

2008年12月24日 #

新版本的wowmapviewer发布!!

新版本主要改进有以下几个:

1、修改了上一版本在进行portal culling时在一些山洞和地下城场景出错的bug,现在打开F7功能时不会出现渲染错误;

2、添加了碰撞检测功能,功能键为F10,简单的使用半径为1.0的球体和场景进行碰撞检测;

3、增加了功能键NUM 1来显示wmo的group AABB;

4、增加了800*600的显示选项(原来默认的1024*768在我的CRT上显得有些大,呵呵)。

下载地址:wowmapview

posted @ 2008-12-24 13:55 Dreams 阅读(112) | 评论 (0)编辑

2008年11月14日 #

wow map viewer的portal culling版本

在通过ufoz的wow map viewer对wmo文件研究过程中,尝试在其中加入portal culling功能,经过几天的努力,完成了一个初始的版本,这个版本存在以下的bug:1、一些只包含一个group的wmo在某些情况下会被错误的culling;2、在地下城场景中只要是在indoor就会出现culling错误;3、某些山洞场景会出现culling错误。这些bug打算在以后版本解决。

使用方法:进入程序后使用功能键F7打开portal culling功能。

wowmapview版本号:0.5。

注意一点:使用wowmapview需要下载WOW的客户端。

下载地址:/Files/dreams/wowmapview.rar

问题交流可留言或发送邮件至:dreams_wu@sina.com

posted @ 2008-11-14 19:39 Dreams 阅读(203) | 评论 (0)编辑

wmo(wow map object) research

wmo(wow map object) research
The wmo是一个非常有趣的设计,wow中比较小的物体使用doodad,而building使用wmo,
这里的building可以是桥梁、了望台、简单的小房子、复杂点的旅馆这样的房屋、非常
复杂的建筑群(例如地下城场景),本文对wmo场景文件进行简单的介绍,关于wmo文件的具
体信息请参考wowmapview的source code,这里非常感谢ufoz所做的贡献。
1、命名规则
wmo保存在以.wmo结尾的文件中,这个文件使用数据块来保存数据。一个wmo通常由一个或多个
group组成,而group数据也保存在以.wmo结尾的文件中,不过文件名称存在不同,例如一个wmo
保存在name.wmo文件中,那么group文件名就为name_001.wmo name_002.wmo......
2、结构
wmo就组织结构来说包含两个层次,group和batch。一个group通常包含多个batch,其中group包含
一个AABB。batch是wmo最小的渲染单元,它保存了顶点索引列表,可以直接调用DP进行渲染。group
内保存一个标志位,可以将group分为indoor/outdoor两类,这一个信息非常重要,通过它wmo就将
building分成了内外两部分,outdoor group就是building的外壳,他通过portal与室内场景连接
在一起。
3、portal
wmo文件中保存了portal信息,在wmo中规定group必须通过portal进行连接,portal由PVS和PRS
两部分组成,PVS记录portal顶点信息,PRS记录portal和group的连接信息,PRS结构如下:
struct WMOPR {
int portal;
int group;
int dir;
};
需要注意的是dir,这个成员只有两个值-1或1,由于portal的顶点信息按照顺时针记录,因此group
位于portal的正面时dir为1,否则为-1,通过dir可以快速确定group到底位于portal的哪一侧。
通过wmo中记录的portal信息可以使用portal culling来检查group的可见性,但是这里还是有一些
难度,主要是指portal的记录方式。由于一个group可能有多个portal,而查找连接的portal只能
通过PRS,这样在大的场景中非常不方便。而且在wmo中竟然没有记录portal的plane信息,如何确定
camera到底是位于portal的正面还是反面呢?(现在wmo文件由于没有完全破解,存在一些wowmapview
未读入的数据块,例如MVER、MOPT、MOVV、MOVB等,其中MVER应该是wmo文件的版本号,MOPT怀疑是保
存所有plane信息,而MOVV可能是保存包围体顶点信息,而MOVB保存包围体信息,MOVV、MOVB应当用于
碰撞检测,这些暂时没有验证)我的做法是在载入时计算portal的plane信息,并将PRS信息转换为类似
Q3 BSP中portal的结构。
struct portal_t {
int othergroup;
int pvs;//pvs index
int dir;
};
struct group_t {
int firstportal;
int numportals;
};
4、碰撞检测
在wmo中并没有使用BSP、OC TREE这样的结构来进行场景管理,可能所有人都感觉非常困惑。
场景管理的功能主要是为了加速渲染和方便碰撞检测,由于存在portal,这样第一个功能已经完成。
而对于碰撞检测,我的想法应当是AABB TREE。仔细观察WOW的场景可以发现在indoor场景中曲面、斜面
这样的几何物体非常少,大多数是规则物体,因此可以判断在wmo中所有的物体都是严格按照轴对齐
方式进行建模,也就是对规则性物体AABB=OBB。由于MOVV和MOVB信息并没有完全研究透彻,因此关于
这一部分只能是我的猜测。
5、渲染
对wmo的渲染由于batch的存在从而变的简单化,但还有可以优化的地方。由于wmo中使用portal将其分割
成group,因此有大量的材质相同的model被分割成不同的batch,在渲染时将材质相同的batch合并到一起
渲染可以避免一些无谓的DP调用。wmo一个令人诟病的地方是使用vertex light,为了减少图元数量从而
使顶点数量降低,造成渲染的时候出现色带效果,应当加入lightmap,由于wmo的场景通常不大,预处理
时做radiosity的时间也不会太长。
6、动态载入
对于只包含几个group的小场景的wmo,由于载入时间不是太长,在动态载入时一次性载入对程序影响
并不会太大。但是对于超大场景的wmo就需要考虑载入策略,这样场景典型的就是wow中的地下城场景,
它一个wmo中包含了几百个group,一次性载入时间非常长,需要分段进行载入。此时就显示出wmo分
文件保存group的优势了,为了实现动态载入wmo场景,一种可能的做法是在载入wmo后需要根据camera
所在的group快速的建立group连接层次图,这个图通过PRS数据建立,建立流程如下:
一、将camera所在group作为当前group,获得所有相连的protal;
二、将protal连接的group保存到第一层列表中,遍历第一层列表中所有的group;
三、获得第一层列表中group的portal,检查portal所连接的group是否保存在第一层列表中,如果没有
    将其保存到第二层列表中;
四、重复上述过程,直到整个层次图建立。
这个层次图可以预先建立然后保存到文件中运行时载入,这样wmo就是分层载入而不需要一次性载入。
(这里我考虑是否在wmo中也可以建立类似bsp的pvs数据呢?虽然pvs现在已经开始淘汰,但是如果
存在pvs就可以方便确定哪些group需要立即载入,只是不知被portal分割后的group到底是不是convex
hull,如果是的话可以建立pvs,但对建模时限制更加明显,两难的选择!!!)
7、建模
由于wmo是按照group对场景进行保存,因此为了建立wmo需要设计一个强力的模型构建工具,这个工具
主要功能就是对从建模工具(3DS MAX)中建立的场景模型进行分组和处理。美工在制作模型时需要非常
小心,所有的模型要严格的轴对齐(轴对齐的原因是需要模型的AABB=OBB),然后将模型导入工具中。
模型构建工具有以下功能:分组(group)功能、group选择、group显示/隐藏、指定portal,portal对
齐(考虑门、窗户这样天然的portal,手动指定portal时肯定无法与外表墙壁对齐,需要程序自动对齐)
、batch操作(分割、选择、显示/隐藏等)、图元级操作(triangle拣选,用于batch分割)、光照运算
(产生vertex light数据)、放置光源、放置doodad(场景中的道具,如桌子、椅子等)。可能还需要其
他一些功能,但是对比其他引擎的场景建模工具(hammer、sandbox)明显简单化许多。
8、优势及不足
当前处理室内场景的主流技术依然是bsp,但是随着硬件的发展bsp的优势在慢慢地丧失,bsp赖以生存的
预处理PVS现在已经完全被实时的portal culling所取代,bsp优势只剩下对图元排序(用于透明物体的
渲染)和基于brush的快速碰撞检测上,但是对比建模工具的复杂化和场景的限制,采用bsp的开销确实
显得太大。而基于纯portal引擎的结构开始流行,例如cryengine中处理室内场景时就完全抛弃bsp,
场景完全由一块块固定大小的墙壁组成,一块墙壁基本和bsp中brush类似,这样做的好处是建模工具变
的简单(不需要进行CSG运算),而且非常容易的产生portal,同时由于场景使用brush构成也兼具了
bsp方便进行碰撞检测的优势。wmo有些类似cryengine,但是在某些方面更具优势。
首先在建模方面,wmo的场景完全可以通过成熟的建模工具来构建,这样对于美工不需要重新学习新的
建模工具,可以节约大量的时间。其次模型构建工具需要的功能非常少,减少了程序的复杂性,缩短了
编写相关工具的时间。再次,场景管理简单化,相应代码量大幅度减少,同时由于portal的存在,可以方便
的与其他引擎相对接。可以说wmo是一种可以进行快速开发的场景结构。

posted @ 2008-11-14 09:05 Dreams 阅读(1093) | 评论 (0)编辑

2007年3月26日 #

Hidden Surface Removel Algorithms----Occlusion Culling<2>一种高效的基于大规模地形场景的OCCLUSION CULLING算法

对于通用场景来说我以前介绍的区间扫描线Z缓冲器算法可以剔除大部分的不可见物

体,但是在基于heightmap的大规模地形场景下会发现作用不是太大,区间扫描线Z

缓冲器算法需要在场景中手工指定occluder,occluder必须为规则物体,而在地形

场景中这种occluder非常少,也就是场景中的建筑物之类的物体。实际上地形场景

中最适合作为occluder的是连绵起伏的丘陵、山脉,它遮挡住了场景中的大部分物

体,但是区间扫描线Z缓冲器算法对这种情况下的OC就无能为力了,需要使用其它算

法进行OC计算,现在网上完全公开的适用于地形场景的OC技术主要有以下几种:
Voxel column culling、Hierarchical visibility、incremental horizon。这些

算法都需要进行一些预处理,其中最流行是incremental horizon(增量地平线)技

术,这种技术要求在渲染前对每一个地形块计算一个潜在轮廓线,在渲染时将这些

潜在轮廓线合并为地平线进行OC处理。由于这种算法需要进行预处理因此也不太适

用游戏开发,此后我自己又开发了一个realtime计算的incremental horizon算法,

但是发现开销太大,根本无法用于realtime rendering(和Pascal Junod在论文
《Implementation of a O(na(n)log(n)) Point
Visibility Algorithm on Digital Terrain Models》中使用的算法相同,这篇论

文我也是后来才发现的,有兴趣的可以翻看一下我以前在gameres发表的文章)。
下面列出的是网上相关的论文,有兴趣可以自己看一下。
Lloyd B, Egbert P. Horizon occlusion culling for real-time rendering of

hierarchical terrains. In: Gross M, Joy KI, Moorhead RJ, eds. Proc. of

the IEEE Visualization. Boston: IEEE Computer Society Press, 2002. 403-

410.
Stewart J. Hierarchical visibility in terrains. In: Dorsey J, Slusallek

P, eds. Eurographics Workshop on Rendering. Vienna: Springer-Verlag,

1997. 217-228.
Zaugg B, Egbert P. Voxel column culling: Occlusion culling for large

terrain models. In: Ebert D, Favre JM, Peikert R, eds. Proc. of the Joint

Eurographics-IEEE TCVG Symp. on Visualization. Vienna: Springer-Verlag,

2001. 85-93.
Stewart J. Fast horizon computation at all points of a terrain with

visibility and shading application. IEEE Trans. on Visualization and

Computer Graphics, 1998,4(1):82-93.
Daniel Archambault. All the Distant Horizon Edges of a Terrain. B.Sc.

(Hons.) in Computing Science, Queen’s University (Kingston), 2001
Pascal Junod. Implementation of a O(na(n)log(n)) Point
Visibility Algorithm on Digital Terrain Models. October 1999

后来我仔细观察farcry的editor sandbox,经过差不多两个多月的试验终于开发出

一个可以realtime运行的用于地形环境的OC算法,这个算法的开销非常小,经过我

在OGRE平台上的试验,此算法可以做到非常精确的剔除,FPS提升明显。由于这个算

法的核心是线段求交,因此我暂时称其为线段求交OC算法。
在介绍这个算法前先明确一下坐标系,一张heightmap的行为X,列为Y,高度方向为

Z。
先考虑单点OC的情况,如下图所示:
         P
         |
         |C
A-------------------B
         |
         |
         O
假设plane AB为一个occluder,O为camera位置,如果要判断点P是否被AB遮挡,只

需要简单的判断线段OP是否和occluder AB相交即可,如果存在相交点C则点P即被遮

挡,问题被简化为线段和平面的求交。下面继续考虑在地形环境下如何简化这个问

题,在地形环境下heightmap的每一行和列都可以看作一个occluder,这里假设AB为

行或列相临两个顶点组成的线段,如果要判断点P是否被AB遮挡,可以通过比较交点

C在线段OP和线段AB上的z值来确定:
Zc_op > Zc_ab   点P没有被线段AB遮挡
Zc_op <= Zc_ab  点P被线段AB遮挡
现在检查点P是否被遮挡在地形环境下简化为一个简单的2D线段求交问题,这也是本

算法之所以高效的原因。
下面继续来看一下如何将算法由一个点推广到整个地形。在基于heightmap的地形系

统中通常将地形分成一块块小的tile,根据LOD算法的不同tile大小可以为17*17或

者33*33不等,这里假设使用17*17的tile。
首先来看如何剔除场景中被遮挡的tile。对于tile来说如果所有的顶点都位于其前

方行和列的下方,那么它一定被遮挡。换句更精确的定义,对于tile的每一个顶点

,如果与camera所在位置所形成的线段,全部与位于tile和camera之间的行或列中

任意一条线段相交,那么可以确认tile被完全遮挡。
按照上面的定义,一个tile如果被检查到完全遮挡需要检查17*17=289次,虽然2D线

段求交运算开销非常小,但是一个tile就需要进行289次运算仍然是不可接受的,需

要更简化的算法。考虑一下区间扫描线Z缓冲器算法,occludee使用都是物体的AABB

,是否可以使用tile的AABB进行运算呢?由于地形环境的性质可以不用考虑AABB最

下面的四个顶点,但是直接使用AABB最上面的四个顶点进行运算绝对不行,如图所

示:

四个顶点虽然被完全遮挡,但是occludee并没有被完全遮挡,如果解决这个问题需

要将AABB的up表面分割成16*16的格子,这样的话运算次数并没有发生变化。
这里可以使用一个取巧的方法,如下图所示:


将AABB投影到camera空间,直接获得线段AB,将AB线段16等分,获得17个新的顶点

,注意这些顶点z值全部相等,现在ocludee是否可见只需要检查17次就可以了。
下面看一下算法复杂度,对于一个完全遮挡的tile只需要进行17个顶点的计算,完全未遮挡的tile只需要计算一个顶点,部分遮挡的tile大约是2-16个顶点左右,由于tile在进行OC运算之前首先要做frustum culling,剔除被frustum culling的tile,然后剔除那些没有被遮挡的tile,实际上的运算量非常少。
注意这里的每个顶点的计算不是指简单的17次线段求教运算,根据tile距离camera位置的远近每一个顶点求交的数量是不一样的,例如下图所示:


图中线段OA需要检查三条行和列,一共是六个交点,需要进行六次线段求交运算.
对于场景中的模型进行OC运算时,也需要按照上面的方法将模型的AABB变换到camera空间,获取一条occludee线段,然后根据地形相临顶点之间的距离确定顶点数。
使用这个算法和区间扫描线Z缓冲器算法相配合可以获得在室外场景中最大限度的剔除被遮挡物体,先用线段相交OC算法剔除被地表遮挡的tile和模型,然后用区间扫描线Z缓冲器算法剔除被建筑物遮挡的tile和模型,完美的室外场景OC解决方案,very nice!!!!

原创文章,转载请注明出处!!!!!!!

线段相交OC算法演示程序下载(使用OGRE平台,运行前先看readme文件):
/Files/dreams/ogrenew.part01.rar
/Files/dreams/ogrenew.part02.rar
/Files/dreams/ogrenew.part03.rar

/Files/dreams/ogrenew.part04.rar
/Files/dreams/ogrenew.part05.rar

/Files/dreams/ogrenew.part06.rar
由于cnblog上传文件大小限制,因此文件被分割成1M大小上传,抱歉!!!!

posted @ 2007-03-26 16:22 Dreams 阅读(1358) | 评论 (2)编辑

2007年3月25日 #

Hidden Surface Removel Algorithms —— Occlusion Culling<1>

隐藏面剔除算法的出现是基于以下的现实,在游戏场景中你不必每帧都将所有的物体渲染到屏幕上,它的作用主要有两个:一个是减少GPU的屏幕填充率,另一个是减少CPU和GPU的传输带宽。根据当前的硬件现状GPU的填充率已经不存在任何问题,而现在唯一的限制位于AGP上,它那可怜的一点带宽已经成为图形渲染的瓶颈,因此当前HSR算法大部分都放在CPU端来做以尽量减少每帧需要传输的数据量,这里你不需要对CPU的负担感到担忧,因为现在的CPU完全可以适应这样的要求。当你将场景采用适当的场景分割算法如BSP、OC tree算法组织起来后,就需要在每帧渲染时使用适当的HSR算法对其中无用的信息进行剔除,现在的HSR算法基本上可以分为四种:backface culling、frustum culling、portal culling、occlusion culling。它们的作用分别如下:backface culling用于剔除mesh背面的triangle,frustum culling用于剔除屏幕之外的物体,portal culling用于剔除不能通过portal看到的物体,而occlusion culling用于剔除场景中被自己前面的物体所遮挡的物体。其中前两种算是最常用的算法了,对于backface culling来说它已经在GPU的硬件上实现,不过为了减少传输带宽,在适当的地方你也可以将其使用到CPU部分,主要是指地形这样的大规模MESH上,如何使用这需要看你是如何组织地形数据的,当你使用triangle strip渲染地形时,尽量按块来对地形进行culling,而当你使用triangle list来渲染地形时,你就可以尝试使用逐triangle来对其进行culling,关于实际使用应该使用哪种方法,需要你自己试验一下。frustum culling由于太普遍这里不再解释,而portal culling如果详细介绍的话那就是另外一篇文章了,这里只是简单的介绍一下,通常的portal culling实际分为两种,一个是用于bsp的PVS算法,这种方法的优点是通过预计算获得可见性信息,速度非常快,而且场景中的portal可以通过程序获得,不需要手工进行指定,但是缺点也非常明显,基本上它只能用于室内场景,而且由于PVS是一个可能的可见集合因此不能对不可见场景作到完全的剔除。另一个可以被称为实时的portal culling,这种方法的优点很明显,如果场景安排合理的话它可以作到完全的剔除,而且它可以用于任意的场景中,indoor、city、terrain它都可以使用,缺点也非常明显,portal必须手工进行指定,而且由于portal culling每帧都需要计算因此每帧出现在屏幕上的portal必须进行限制,还有最重要一点通常由于场景的限制尤其是在outdoor场景中它也不能完全作到完全剔除,下面我举一个简单的例子来看看它为什么不能作到完全的剔除:

(感谢Siney提供插图)


  上面可以看作是一个房子,B为房间的门,也就是一个portal,A是场景中的一个物体如果我们做portal culling计算时,如果一个npc位于D点那么可见的,但是当位于C点时,由于前面存在物体A,应当是不可见的,但是portal culling并没有将其剔除因此npc还是会被传送到GPU进行渲染。

  这个时候就需要可以使用occlusion culling技术来弥补这个缺憾了,因此在一个portal引擎中必然需要OC技术来配合使用,这里再举一个outdoor的例子,在室外存在一个building,我们可以使用building的门和窗户作为portal来对building内部进行culling,将building作为一个occluder对整个场景进行OC操作。根据相关文献提供的资料,当场景使用OC技术后会比不使用OC的场景提高30%到70%不等的FPS,因此如果有可能的话应尽量将OC技术加入到你的引擎中,而且OC技术并不是只能使用到portal引擎中,它可以被使用到任意技术构建的引擎中。

  现在的OC算法非常多,比较优秀的如HZB、HOM算法虽然比较强悍,但是放在CPU进行计算的话,效率还是非常低的,达不到实时的要求,而当前的GPU还没有对其实现硬件的支持,因此不是本文介绍的重点,这里介绍的OC算法称为区间扫描线Z缓冲器算法,这是一种可以进行完全剔除并且效率非常高的一种OC算法。在进行介绍前我们必须明确几个概念,在OC中遮挡物被称为occluder,被遮挡物称为occludee,OC算法就是检查occludee是否被occluder所遮挡。场景中那些比较大的物体可以作为occluder,如房屋,城堡等,occluder上位于同一plane上的polygen称为surface,surface上由edge首尾相连构成一个闭合的空间。

  进行OC计算之前我们首先需要获得场景中的所有occluder,这是非常重要的一个步骤,这一步通常在场景的预处理时进行,根据场景来产生一个occluder列表并保存到文件中。

  下面我们讨论一下如何获得occluder,场景中的occluder通常需要手工进行指定,这是因为开发一个自动获得occluder的算法非常复杂,同时由于OC运算效率问题同一帧中出现的occluder数量必须进行限制。下面我们分情况来考虑一下:对于一个标准的BSP引擎来说,整个场景都由brush构成,因此获得occluder相对简单一些,在对场景建模时手工指定哪些brush作为occluder,然后在进行BSP分割时对这部分brush单独进行处理,按照和场景获得可见面一样方法获得这些brush的可见面来作为occluder。而对于其他技术构建的引擎获得occluder也相对简单,例如现在MMO比较流行的基于terrain的引擎,场景中可以作为occluder的物体其实都是一个个单独的model,因此求occluder实际上就变成获得model的convex hull,这类算法也比较多如qhull算法,但是如果使用程序获得hull的话对model的外形存在一定的限制,一个替代的方法是在美工对物体进行建模时同时为其指定一个hull并保存到文件中,这样的话就不会限制model的形状了,因此对于第二种情况概括起来说就是在编辑场景时指定适当的物体作为occluder,然后将物体的convex hull保存到occluder list中。

  当我们获得场景中的occluder就可以进行OC运算了,先来看看区间扫描线Z缓冲器算法是依照什么样的原理来实现OC的。
|
(感谢Siney提供插图)

  在上图中物体N为occluder,A、B、C为occludee,我们假定它们投影到屏幕上都为一个矩形,我们这里称N的上下两条edge a、b为扫描线,从上图中可以看出如果物体A位于N的后面那么A一定被N所遮挡,这可以通过比较N和A所在surface的z值来进行比较,这也就是本算法中Z缓冲器的来历,现在我们假设物体A、B、C都位于N的后面,可以看出A被完全遮挡,B是部分遮挡,C没有被遮挡,如何判断呢,先看A的情况,N和A存在四条扫描线按照y从小到大顺序分别是b,d,c,a,在扫描线b和d构成的区间中由于不存在A的edge因此跳过,检查扫描线d和c构成的区间,我们称位于两条扫描线之间的edge为活化边,它们保存在活化边列表active edge list中,每一条活化边保存了两个surface指针,分别是左surface和右surface,例如对于N的左edge而言,它的左surface不存在因此指向屏幕的background,也就是z值为最大的surface,而右surface为N所在的surface,其他类似。现在我们检查扫描线d和c之间的活化边列表,首先获得A的x值最小的火花边为A的左edge,接着获得N的活化边列表中x大于A的左edge的活化边,为N的右edge,比较两条edge的左surface,A的左edge的左surface的z大于N的右edge的左surface的z,因此A的左edge被N所遮挡,接着同样的方法检查A的右edge还是被遮挡,这样我们就可以判断A完全被N所遮挡。使用同样的方法检查B,在扫描线f和b构成的区间中,由于N不存在活化边列表可以判断B没有被N完全遮挡。再检查C,在扫描线h和g之间N的活化边列表中所有的edge的x都小于C的左edge,可以判断C没有被N遮挡。对于其它复杂的情况例如存在多个occluder的情况你自己可以验证,这种算法在任何情况下都可以对物体是否被完全遮挡进行精确的判断。

  通过上面的介绍相信你已经完全理解区间扫描线Z缓冲器算法的基本原理,下面看一下具体如何来实现这个算法,步骤如下:

1、获得场景中所有的occluder,剔除occluder上所有背对camera的surface。

2、使用near clip plane对所有的surface进行clip操作,剔除位于near clip plane之外的surface,如果surface和near clip plane相交计算交点,这样也就剔除了那些位于nearclip plane之外的edge。

3、将所有的顶点变换到投影空间,这里需要注意投影空间的性质,投影变换实际上就是将frustum变换为一个BOX,对于openGL来说就是一个立方体,最小拐点为(-1,-1,-1),最大拐点为(1,1,1),对于DX来说是一个长方体,最小拐点为(-1,-1,0),最大拐点为(1,1,1)。在投影空间所有物体投影到屏幕上形状不会发生改变,这也是为什么要变换到投影空间的原因。计算每一个surface在屏幕上的投影面积,剔除那些面积过小的surface,剔除位于frustum之外的surface,但是并不对surface求交,这是为了防止因为计算精度的原因出现错误。将surface所在的plane也变换到投影空间,这样做是为了方便计算指定顶点的z值。

4、将变换到投影空间中的所有顶点保存到edge list中,注意edge保存方式,edge起点的y值应当小于终点的y值,并且我们称起点处的y值为edge的y值。在edge list中按照edge的y对所有的edge按照从小到大的顺序进行排序,如果y相同比较x值,如果x也相同比较edge的斜率。因为edge list完全按照y进行排序,因此我们也可以称其为Y桶。需要注意的是如果edge和X轴平行的话,并不需要将其加入到edge list中。

5、查找扫描线并计算扫描线之间的活化边列表。从上面的原理介绍中我们也能大致了解如何获得扫描线,扫描线必然位于edge的端点上,而且如果edge存在相交的话,那么交点也需要做为扫描线,那么我们需要对Y桶进行遍历来查找位于扫描线Y=-1和Y=1之间所有的扫描线,当获得所有的扫描线后接着需要计算扫描线区间的活化边列表,注意活化边列表中的edge需要使用edge与扫描线之间的交点作为端点,并且活化边列表是按照x进行排序,因此也可以称为X桶。

6、对场景需要检查是否被遮挡的物体传入它的AABB作为occludee,首先检查occludee在屏幕上的投影面积是否足够大,如果太小不需要对其进行OC,接着将AABB转换为一个BOX,计算BOX的surface和edge,将其变换到投影空间并按照上面的方法查找扫描线并获得活化边列表。从occluder和occludee中y最小的扫描线出发,逐区间对活化边列表进行比较检查occludee是否被完全遮挡。

  在这种算法中,几乎每帧都需要对活化边列表进行更新,因此计算量最大的部分也在这里,不过一旦活化边列表建立以后对occludee进行遮挡计算非常方便快捷,适合对大量的动态物体进行OC运算,尤其是那些渲染代价非常大的角色模型。

posted @ 2007-03-25 17:26 Dreams 阅读(348) | 评论 (0)编辑

Half Life2 Displacement Terrain System

   HL2的地形系统是一个另类的地形系统,它没有使用当前流行的heightmap技术,而是完全使用displacement map技术来进行处理,使用displacement map技术的初衷是为了在框架中和BSP技术保持一致,这是因为在BSP中最小的渲染图元是surface,surface的限制是所有的顶点必须位于同一个平面上,实际上从理论上来说在HL2这样的引擎中也可以使用heightmap来构造地形,那就是将heightmap数据也分块保存,但是当前流行的地形技术使用的都是一个整张heightmap,而且大量的LOD算法都是基于这个基础上的。使用displacement map的另外一个好处是可以构造更加复杂的地形,这是因为它存在法线的缘故。同时使用displacement map还有一个好处是在将来,因为当前MS已经对displacement map技术进行了支持,现在的问题是GPU在硬件上还不支持,如果一旦硬件支持的话,就有可能将整个地形由GPU来处理,那么对地形渲染的速度和精度会有非常大的提高。
1、what’s Displacement Map
      displacemtn map技术是最近几年才出现的一个技术,它出现的初衷是为了解决内存和GPU之间的带宽问题,它开始主要用于高poly角色模型的渲染,由于模型的高数据量会使内存和GPU之间的传输成为瓶颈,因此将其转换为低poly模型和一张displacement传送到GPU中,在GPU中转换为高poly模型进行渲染,可以大大减轻传送的数据量。
但是当前这项技术已经获得非常大的发展,它使用的范围非常大,从室外地形、水面、海洋到室内场景的渲染,它为提高场景的真实度提供了一个有力的工具。一个displacement map通常包含两部分:一张normal map用来保存法线,另一张height map用来保存顶点的偏移。当需要从低poly模型转换为高poly模型时很简单,首先从模型的一个face上获得一个插值点P1,然后从displacement map中获得相应位置的normal和distance,那么新的顶点P2为:
P2 = P1 + normal*distance
关于displacement map的参考文章请看shaderX2中的Displacement Mapping。
2、HL2 Displacement Map Rule
   在HL2中displacement map信息被保存在BSP文件中,它有以下几个数据块:
LUMP_DISP_VERTS  保存displacement真实顶点信息
LUMP_DISP_TRIS   保存displacement三角形标识符信息(walkable or buildable)
LUMP_DISPINF     保存displacement 的连接信息
它们保持的信息结构如下:
LUMP_DISP_VERTS:
class CDispVert
{
public:
Vector m_vVector;// Vector field defining displacement volume.
float m_flDist; // Displacement distances.
float m_flAlpha;// "per vertex" alpha values.
};
LUMP_DISP_TRIS:
class CDispTri
{
public:
unsigned short m_uiTags; // Displacement triangle tags.
};
LUMP_DISPINF:
class ddispinfo_t
{
public:
int NumVerts() const{ return NUM_DISP_POWER_VERTS(power); }
intNumTris() const{ return NUM_DISP_POWER_TRIS(power); }

 

public:
Vector startPosition;// start position used for //orientation -- (added BSPVERSION 6)
int m_iDispVertStart;// Index into LUMP_DISP_VERTS.
int m_iDispTriStart;// Index into LUMP_DISP_TRIS.

int power; // power - indicates size of map //(2^power + 1)
int minTess; // minimum tesselation allowed
float smoothingAngle;   // lighting smoothing angle
int contents;        // surface contents
unsigned short m_iMapFace; // Which map face this
                                        //displacement comes from.
int m_iLightmapAlphaStart;// Index into ddisplightmapalpha.

int m_iLightmapSamplePositionStart;// Index into.
                            // LUMP_DISP_LIGHTMAP_SAMPLE_POSITIONS   
CDispNeighbor m_EdgeNeighbors[4];// Indexed by NEIGHBOREDGE_ defines.
CDispCornerNeighbors m_CornerNeighbors[4]; // Indexed by CORNER_ defines.

enum { ALLOWEDVERTS_SIZE = PAD_NUMBER( MAX_DISPVERTS, 32 ) / 32 };
unsigned long m_AllowedVerts[ALLOWEDVERTS_SIZE]; //
// This is built based on the layout and 
//sizes of our neighbors
// and tells us which
 //vertices are allowed to be active.
};

在HL2中规定一个displacement最大由17*17个顶点组成,每一个displacement都将左下角看作是自身坐标系的原点,并按照顺时针方向进行记数。如下:


在ddispinfo_t类中我们可以看到为了记录displacement的邻接关系,它使用了两个数组,m_EdgeNeighbors记录四条边的邻接关系,而m_CornerNeighbors记录了四个角的邻接关系。必须注意的是在HL2中邻接的displacement并不一定都是一样大的,也就是说邻接的displacement不一定是一个,也可以是两个,如果邻接的为两个的话那么邻接的displacement的大小必须小一倍。如下:


在displacement和surface中都有一个变量来指定它们相互之间的对应关系,你可以这样认为,如果一个surface是一个displacement surface的话,那么在surface所保存的顶点信息就是displacement所在的基准面信息,实际上就是记录基准面的四角坐标,当产生实际顶点时过程如下:通过这四角的坐标和displacement的power信息可以插值获得每一个真实顶点的基准点坐标,然后通过CdispVert所保存的信息获得真实的顶点坐标。
3、如何处理地形系统
      在HL2中为了高效的渲染地形定义了大量的类,其中最重要的为CdispInfo,它几乎包含了地形的大部分处理,还有一个类CpowerInfo,这是一个予计算的模版类,它预先计算了每一种大小displacement的信息,这样做是为了加速地形的渲染,而类CcoreDispInfo只用来载入数据时使用,它的功能就是将BSP文件中的信息转换为程序可以处理数据。其实在HL2中比较难懂的是它的LOD算法,下面是它具体的算法:
      首先是它的更新策略,在正常情况下(LOD的控制台变量为默认值)对displacement的更新有两种情况,它规定了一个更新半径(由控制台变量r_DispFullRadius指定)凡是位于此半径内的displacement的LOD等级都为最大。另外在每次更新时每一个displacement内部都记录了更新时camera的位置,如果当前camera的位置减去上一次更新时camera位置的大小大于一个规定值(由控制台变量r_DispRadius指定),对其进行更新,必须注意的是此时更新的是位于更新半径外的displacement。
      下面是它的LOD算法,还是要提醒此时是对更新半径外的displacement进行处理。通常现在LOD算法都是基于距离进行计算的,但是HL2中没有采用这种方法,而是使用一种称为屏幕误差的方法进行。具体方法如下:
      首先假设将要更新的displacement位于屏幕的中心,然后求出此时的camera变换矩阵,遍历displacement中的所有顶点(不包含当前displacement的中心点和四个角上的点),对于每一个顶点,用它的坐标和它所在error edge的算术中点坐标相减,然后用这个值和当前displacement的AABB中心点相加,结果通过camera矩阵进行变换然后将X,Y部分分别除以Z值得到的就是这个顶点在屏幕上的视觉误差了。这是一个非常不容易理解的部分,尤其是除以Z值的部分,其实这样做是为了将其投影到Z为1.0的平面上,省略了进行投影变换的过程,提高了效率。当这个误差大于预定义值就将这个顶点渲染出来。
      通过上面的介绍可以看出在HL2中并没有象其它LOD算法一样对每一个displacement硬性的规定一个LOD级别,而是通过每一个顶点的屏幕误差来确定它是否可以被渲染,这样做在渲染非常大的地形时效率不会太好,但是在处理HL2这样的小场景时效率确实非常不错。同时也要注意这种地形方法的优点,它非常适合处理落差非常大的室外场景,如地牢围攻这样比较夸张的地形,不过此时就需要对它的LOD算法进行一下改进,另外在HL2中地形一般都比较小,如果地图设计良好的话基本上很少会渲染hidden surface,因此上不需要做地形tile的HSR,而将这种地形系统扩展到无限地形系统时,必然需要使用一定OC算法来做HSR的工作,这可能是将其扩展到无限地形系统的一个非常具有挑战性的工作了。

posted @ 2007-03-25 17:08 Dreams 阅读(321) | 评论 (0)编辑

游戏引擎中的通用编程技术

你是否正在考虑构建一个游戏引擎呢?你对如何构建一个游戏引擎是否已经有了一个明确的
计划呢?你是否已经对如何组织游戏引擎各个模块之间的关系有了一个通盘的考虑?如果没有,
那么本文将对你建立一个良好的游戏架构提出一些有益的方案,如果你已经对上面的问题有了一
个明确的答案,那么本文不是你需要阅读的内容。本文的目的是给那些没有任何建立完整游戏引
擎经验的人提供一些入门性的知识,使他们初步了解一下如何来构建一个游戏引擎,构建游戏引
擎应该注意哪些方面的问题,并提供了一些成熟的设计模版并指出这些设计模版使用的范围,我
希望这些内容对那些中级编程人员也有一个良好的参考作用。本文的内容来源于一些流行的编程
书籍,具体书目请见本文最后的部分,由于本文是介绍性质的文章,因此如果你对哪方面的内容
非常感兴趣请参考相应的书籍,本文或许有很多错误的地方,如果你有什么看法的话可以通
过Email和我进行讨论,我的地址为dreams_wu@sina.com。
    这里必须再次提醒你,本文介绍的是一些通用的游戏编程技巧,虽然是通用但是可能并不是
非常全面,可能存在这样或那样的缺陷,因此如果你希望它发挥最大的效用必须恰当的使用它,
而不是不分场合的滥用。切记切记,一个初学者最容易犯的错误就是任意使用一些设计模版而不
顾它的使用范围。
    在开始构建一个游戏引擎时你需要先考虑哪些方面的问题呢?这是你必须认真考虑的问题,
我的答案是首先必须考虑代码的可读性,尤其是在多人进行开发时更必须高度重视,如果你写的
代码其他人需要花费非常大的精力进行阅读,那么根本谈不上提高工作效率,下面是提高代码可
读性的一些良好建议:
1、建立一份简单明了的命名规则。一份良好的命名规则可以大幅提高代码的可读性,规则必须
简单明了,通常只需要两三分钟的阅读应该可以让其他人掌握,例如在代码中直接使用匈牙利
命名法这种大家熟知的规则,使用字母I作为接口类的首字母,使用C开头作为实现类的首字母,
使用g_开头的变量名作为全局变量,s_开头作为静态变量名,m_开头作为内部变量名,使用_开
头作为类内部使用的函数名等等,通过名字就可以使你大概了解对象的使用范围和基本功能。
2、不要讨厌写注释。一个编程者易犯的错误就是不写注释,认为它会增加自己的工作量,但是
他没有考虑到相应的工作量已经转移到代码阅读者的身上,可能看代码的人会花费比写注释时间
两倍或者三倍的时间来阅读代码,这是一种非常不负责任的行为,通过一段简短的注释可以使阅
读者迅速的了解代码的功能,从而把时间更多的用到功能的扩展上。下面是一些良好的建议:尽
量对每一个变量标明它的功能。对每一个函数声明的地方标明它的功能,对于复杂的函数还应当
写清参数和返回值的作用,注意是在声明函数的头文件中。在关键的代码处写清它的作用,尤其
是在进行复杂的运算时更应如此。在每一个类声明的地方简要的介绍它的功能。
3、减少类的继承层次。通常对于游戏编程来说每一个类的继承层次最好不要超过4层,因为过多
的继承不仅会减少代码的可读性,同时使类表指针变长,代码体积增大,减低类的执行效率。还
要注意要减少多重继承,因为不小心它会形成编程者非常讨厌的“钻石”形状。同时还要注意如
果能使用类的组合的话那么就尽量减少使用类的继承,当然这是设计技巧的问题。
4、减少每行代码的长度。尽量不要在一行代码中完成一个复杂的运算,这样做会增加阅读难度,
同时不符合现代CPU的执行,由于CPU现在都使用了超长流水线的设计,它非常适合执行那些每行
代码非常短而行数非常多的代码,例如对一个复杂的数学运算,写成一行不如每一步骤写一行。
    以上建议是我的一些粗略看法,如果你还有什么好的看法可以给我指出来,同时上面的建议
并不是绝对的,例如类的继承并不是绝对不能超过4层,如果你需要的话可以使用更多的继承,前
提是这样带来的好处大于代码执行效率的损失。
    接着看看要考虑什么,在Game Programming Gems3的《一个基于对象组合的游戏架构》一文
指出了几个值得考虑的问题,首先是平台相关性与独立性和游戏相关性与独立性的问题,也就是
说应当作到引擎的架构与平台和游戏都无关。为什么要做到与平台无关性呢?这是因为你必须在
开始架构引擎考虑它的可移植性,如果在开始你没有注意到这个问题,那么一旦在游戏完成后需
要移植到其他的游戏平台上,你会发现麻烦大了,你需要修改的地方实在是太多了,所有与平台
相关的API调用都需要修改,所有使用了平台特定功能的模块也需要修改,这是一个非常耗费精力
的事情,可能需要花费和开发一个游戏一样的时间,而如果你在开始的时候就考虑到这个问题,
那么非常简单,只需要写一个相应平台的模块替换掉原来的模块即可,这样精力就可以放在如何
充分的利用特定平台的能力来提高游戏的表现力上,而不是代码修改上。下面简单的谈一下如何
使引擎作到与平台无关。
1、注意操作系统的差异。现在主流的操作系统主要是Windows和Linux两种,当然还有Unix和Mac
,在编程时你必须注意这一点,当你需要包含Windows的头文件时,你必须将它包含在宏_WIN32
中,下面是一个简单的例子:
#ifdef _WIN32
#include "windows.h"
#endif
而你使用Windows平台特定的API时也应当如此,这样在其他平台上编译时可以保证Windows平台
相应的代码不会被编译进去。对于其他平台也应当如此。
2、注意编译器的差异。现在通用的编译器主要有VC,BC和gcc几种,在进行Windows平台编程时,
你通常会使用VC或BC,而对Linux平台编程时通常使用gcc,使用VC编译器你不可能编译出用于Linux
平台的代码,因此在编程时也需要注意,你可以使用上面的方法通过特定的宏来将不同的编译器
分离开。举一个简单的例子:
#ifdef _WIN32
#ifdef _MSC_VER
typedef signed __int64  int64;
#endif
#elif defined _LINUX
typedef long long  int64;
#endif
在不同的编译器中对64位变量的命名是不同的,因为它并不是C++标准的一部分,而是编译器的扩
展部分。另外一个例子是编译器使用的内联汇编代码,在VC中你可以使用_asm来指明,而对于
Linux平台的编译器你需要使用它的专用关键字了。
3、注意CPU的差异。对于不同平台来说它通常会使用不同的CPU,不过幸好Windows和Linux都支持
X86的CPU,这也是PC游戏的主流CPU平台,而XBOX使用的也是X86的CPU,除非你需要移植到PS2平台
,否则这将大大减轻你的编程负担,在X86平台上提供了一个cpuid的指令可以非常方便的检查CPU
的特性,如是否支持MMX,SSE,SSE2,3DNow!技术等,通过它你可以使用特定的CPU特性来加速你
的代码执行速度。
4、注意图形API的差异。现在图形API主要存在两种主流的平台DirectX和OpenGL,DirectX只能用于
Windows平台,而OpenGL几乎被所有的平台所支持。因此你需要为不同的图形API进行封装,将它做
成不同的模块,在需要的时候进行切换。完成这个工作最好的方法是使用后面介绍的类厂模式。
5、注意显卡的差异。现在显卡有两大主流ATI和NV,虽然显卡可以被主流的操作系统所支持,但是
必须注意在不同的游戏平台上还是使用不同的GPU,而在GPU之间也相应有自己的功能扩展,因此在
使用特定的扩展功能时必须检查一下是否被显卡所支持。
6、注意shader语言的差异。可编程图形语言的出现是最重要的一项发明,现在几乎每一个游戏都
在使用这项技术,而正由于它的重要性现在出现了多个标准,HLSL只能用于DX中,而OpenGL由于
标准的开放性更加混乱,每一个显卡厂商都根据自己的产品推出相应的扩展指令来实现shader,
而NV更推出了GC可以同时适用于DirectX和OpenGL,这是一个非常好的想法,不过由于这不是一个
开放的标准因此没有得到其他厂商的支持,在ATI显卡上运行GC代码你会发现比在NV显卡慢了几个
数量级,由于上面的情况你需要根据不同的平台相应进行封装,方法和第4条一样。下面的建议值得
你去考虑,当你使用DirectX平台时应当使用HLSL,而对于OpenGL可以封装为两个模块,根据显卡
的不同进行切换,也可以使用GC特别为NV的显卡封装一个模块来对它进行优化。
    这里需要补充一点,如果可以的话尽量和OGRE一样为不同的操作系统进行封装,这样方便在
不同的系统之间进行切换。
    接着看看如何实现游戏无关性,通常游戏引擎如果要实现游戏的无关性是非常困难的,这也就
是说要求你的引擎适合所有的游戏类型,这太难了,考虑一下一个RPG游戏引擎如果用来做一个RTS
游戏那简直是不可能,类似的你不可能拿Q3引擎来做RTS游戏,但是如果引擎设计的非常良好的话
还是可以实现部分的游戏无关性。也就是说你可以将引擎的一部分模块设计成通用的模块,这样在
开发其他类型的游戏时可以重用这部分的代码,这部分代码包括底层显示,声音,网络,输入等
部分,在设计它们时你必须保证它们具有良好的通用性。
    在这些问题之后你应当考虑程序的国际化问题。这也是非常重要的方面,因为你的游戏可能在
其它国家发行,这主要是注意语言方面的问题,尤其是字符串的处理,在C++的标准库中提供了一个
String容器,它提供了对国际化的良好支持,因此在引擎中你需要从头到尾的使用它。
    接下来我们看看本文最重要的内容,如何组织一个引擎的架构。这是引擎最重要的部分,为什么
重要呢?如果我们把引擎看作一间房子的话,那么架构可以看作是房子的框架,当你完成这个框架
后就可以向框架内添砖加瓦盖房子了。下面让我们来看看如何构建这个框架,通常一个大型的软件
工程是按照模块化的方式来构建的,编程之前要进行必要的需求分析,将软件工程根据不同的功能
划分为几个较大的功能模块,对比较复杂的模块你可能还需要将它分为几个子模块,并需要给出各
个模块之间的逻辑关系。当你编写一个引擎时也需要进行相应的功能分析,让我们看看如何来划分
引擎的功能模块,如果按照上面的游戏无关性和相关性进行分析的话我们可以发现它可以分为游戏
相关层和无关层两层,游戏相关层由于包含了游戏的逻辑性代码也被称为逻辑层。逻辑层应该位于
引擎的最顶层,如果你在开发一个局域网或在线游戏的话,按照网络程序的C/S开发模式,这一层
应该分为两个模块,服务器和客户端模块,它包含了和特定游戏相关的所有功能,如AI,游戏角色,
游戏事件管理,网络管理等等。在它下面就是游戏无关层了,包括了引擎核心模块,GUI模块,文件
系统管理模块等等,其中引擎的核心模块是最重要的部分,逻辑层主要通过它来和底层的模块打交
道,它应该包含场景管理,特效管理,控制台管理,图形处理等等内容。在向下就是一些底层模块
了,如图形渲染模块,输入设备模块,声音模块,网络模块,物理模块,角色模型模块等等,所有
的这些底层模块必须通过核心模块来和逻辑层进行交互,因此核心模块是整个引擎的枢纽,所有的
模块都通过它来进行交互。
    下面看看应该如何来进行模块的设计,这里有一些通用的规则是你应当遵守的:
1、减少模块之间的关系复杂度。我们知道通常每一个模块内部都存在大量的对象需要在各个模块
之间进行相互的调用,如果我们假设每一个模块内部对象的数量为N的话,那么每两个模块之间的
关系复杂度为N*N,这样的复杂度是不可接受的,为什么呢?首先是它非常不利于管理,由于各个
模块都存在大量的全局对象,并存在相互依存的关系,并且各自建立的时间各不相同,这就存在
初始化顺序的矛盾,考虑这种情况,一个模块中存在一个对象需要另外一个模块中的对象才能进行
初始化,当这个对象进行初始化时而另外的对象在之前并没有初始化就会引发程序的崩溃。其次,
不利于多人进行同时的开发,由于各个模块存在相互依存的关系,当复杂度非常高时就会出现模块
与模块的高度依存,也就是说一个模块没有完成下一个模块就无法完成,因此就需要一个模块一个
模块按照它的依存关系进行编程,而无法同步进行。因此在设计模块时的第一件事情
是减少模块之间的复杂度,为此你在设计模块时必须为模块设计一个交互接口,并约定所有模块之
间的交互必须通过这个接口来进行,这样模块之间的关系复杂度就降低为1*1了,非常方便管理,同
时这非常利于多人之间进行开发,假如每个人负责一个模块的开发的话,那么你只需要先完成这个
接口类,其他人就可以利用这个接口进行其他模块的开发,而不必等到你完成所有的类再进行,这
样所有的模块都是同步进行,可以节省大量宝贵的开发时间。
2、对类的抽象接口而不是类的实现编程。这是《Design Patten》一书作者对所有软件编程者的建议
,它也对游戏编程有很大的指导意义。对模块中所有被其它模块使用的类都要建立一个抽象接口,
其它模块要使用这个抽象接口进行编程,这样其它模块就可以在不需要知道类是如何实现的情况下
进行编程。这样做的好处是在接口不改变的情况下任意对类的实现进行改变而不必通知其它人,这
对多人开发非常有用。
3、根据调用对象的不同对类进行分层。实际上本条还是对第2条的补充,分层还是为了更好隐藏底
层的实现。通常一个类不仅被其它模块使用还要被自身模块所调用,而且它们需要的功能也不同,
因此我们可以让一个类对外部显现一个接口而对内部也显现一个接口,这样做的好处和上面一样,
因为一个复杂的模块也是多人在进行编程的。
4、通过让一个类对外显现多个接口来减少类的数量。减少关系复杂度的一个方法是减少类的数量,
因此我们可以把完成不同功能的类合并成一个类,并让它对外表现为多个接口,也就是一个类的
实现可以继承多个接口。
上面的建议只是起到参考作用,具体实现时你应该根据情况灵活使用,而不是任意乱用。
    下面的内容涉及到具体的编程技巧,
    对于引擎中的全局对象你可以使用Singleton,如果你不了解它是什么可以阅读《Design Patten
》,里面有对它的详细介绍,具体的使用可以通过OGRE引擎获得。
    调用模块内的对象可以通过类厂来实现。COM可以看作是一种典型的类厂,DX就是使用它来进行
设计的,而著名的开源引擎Crystle Space也是通过建立一个类似的COM物体来实现的,但是我并不
对它很认可,首先构建一个类似COM的类厂非常复杂,开销有点大,其次COM的一个优点是可以对程
序实现向下兼容,这也是DX使用它的重要原因,而一个游戏引擎并不需要。OGRE中也实现了一个类厂
结构,这是一个比较通用的类厂,但是使用起来还是需要写一段代码。我比较欣赏VALVE的做法,它
通过使用一个宏就解决了这个问题,非常高效,使用起来也非常方便。这个做法很简单,它把每个
模块中需要对外暴露的接口都连接到一个内部维护的链表上,每一个接口都和一个接口名相连,这
样外部模块可以通过传入一个接口名给CreateInterface函数就可以获得这个接口的指针了,非常简
单。下面看看它的具体实现。它内部保存的链表结构如下:
class InterfaceReg
{
public:
InterfaceReg( InstantiateInterfaceFn fn , const char *pName );

public:
InstantiateInterfaceFn m_CreateFn;
const char *m_pName;

InterfaceReg *m_pNext;
static InterfaceReg *s_pInterfaceRegs;
};
并定义了两个函数指针和一个函数
#define CREATEINTERFACE_PROCNAME "CreateInterface"
typedef void *(CreateInterfaceFn)( const char *pName , int *pReturnCode );

 

typedef void *(InstantiateInterfaceFn)( void );
DLL_EXPORT void *CreateInterface( const char *pName , int *pReturnCode );
下面看看它如何通过宏来建立链表
#define EXPOSE_INTERFACE( className , interfaceName , versionName ) \
    static void *__Create##className##_Interface() { return (interfaceName*) new className; } \
    static InterfaceReg  __g_Create##interfaceName##_Reg( __Create_##className##_Interface , versionName );
如果你有一个类CPlayer它想对外暴露接口IPlayer,那么很简单,可以这么做
#define PLAYER_VERSION_NAME   "IPlayer001"
EXPOSE_INTERFACE( CPlayer , IPlayer , PALYER_VERSION_NAME );
如果在其他模块内你需要获得这个接口,可以这么做
CreateInterfaceFn factory = reinterpret_cast<CreateInterfaceFn> (GetProcAddress( hDLL , CREATEINTERFACE_PROCNAME ));
IPlayer player = factory( PLAYER_VERSION_NAME , 0 );
其中hDLL为模块的句柄。这里函数指针factory实际指向模块内部的CreateInterface函数,这个
函数通过比较传入的接口名从链表找到指定类指针。

    解决了类厂问题,下面让我们看看如何建立模块对外的接口,在Game Programming Gems3的
《一个基于对象组合的游戏架构》一文提出了一种架构,Half Life2引擎中对这种架构进行了有效
的扩展,你可以让所有的对外暴露的接口都使用这个架构,前提是模块只有一个接口对外暴露。
class IAppSystem
{
public:
// Here's where the app systems get to learn about each other 
virtual bool Connect( CreateInterfaceFn factory ) = 0;
virtual void Disconnect() = 0;

// Here's where systems can access other interfaces implemented by this object
// Returns NULL if it doesn't implement the requested interface
virtual void *QueryInterface( const char *pInterfaceName ) = 0;

// Init, shutdown
virtual InitReturnVal_t Init() = 0;
virtual void Shutdown() = 0;
};
通过Connect方法你可以将两个模块建立一个连接关系,通过QueryInterface方法你可以检索到其他
需要暴露接口,这种方法很好的为所有的模块建立一个标准的对外接口,极大的减轻了编程的复杂
性,遗憾的是在HL2引擎中只有部分模块使用了这个方法,可能是这个接口引入时间太晚的缘故。
(待续)

posted @ 2007-03-25 17:04 Dreams 阅读(594) | 评论 (0)编辑

gimbal lock

posted @ 2007-03-25 17:02 Dreams 阅读(195) | 评论 (0)编辑

一个快速的内存分配池

对于现代的游戏引擎来说,为了提高性能和有效的管理内存,需要使用各种各样
的内存分配模型,内存池作为一种有效的分配模型被大量的使用,它通过一次分配
足够的内存来减少对new delelte使用以提高引擎的性能,并且由于每一个内存块
都有相同的大小因此非常易于管理,并可以防止内存的泄露。它通常被用于需要
分配大量相同对象的场合,如粒子系统这样的地方。

 

对于在运行时可以明确知道分配数量的物体,可以通过一个静态数组来实现它,
但对于不知道分配数量的地方,设计就变的有些复杂,通常需要使用一个链表来
进行实现,如STL的list容器,不过使用它有一个很大的缺点,链表对表中的对象
进行查询操作时速度不是很理想,会极大影响它的性能。因此需要寻找一个比较
好的方法对其进行改进。

通常设计内存池有两个问题必须考虑,一个是内存分配的策略,由于你不是明确
知道待分配物体的数量,因此每次分配多大数量的内存是一个值得注意的问题。
另一个是如何对内存池进行管理,使用什么样的数据结构才能在常数时间内来获得
指定的内存。对于第一个问题解决的方案很多,你可以每次都分配一个指定数量
的内存块,也可以在每次分配时都分配比上一次多一倍的内存,哪种方案更好,需要
你自己在实际使用中体会。第二个问题是本文的核心,一般的做法是将已经分配的
内存块分成两个部分,已使用和未使用两个链表,但是这样做的性能并不理想,下面
看看如何对其进行改进:

我们先建立一个结构用于保存每次分配的一整块内存:
sturct MemChunk
{
  MemChunk* m_pPre;
  MemChunk* m_pNext;
  unsigned int m_nSize;
  char m_Data[1];
}
在这个结构中m_pPre,m_pNext用于建立一个双向链表将每一次分配的内存连接起来,
m_nSize表示当前内存块的大小,m_Data是所分配的内存指针,必须注意这是一个BYTE
指针。我们现在假设当前的内存池用于对CObject物体分配内存,每次分配都一次分配
64个CObject物体的内存,因此m_nSize的大小为64*sizeof(CObject)。下面看看如何
保存未使用的内存块,我们需要一个指针来指向当前未使用的内存块。
void* s_pCurrent;
然后令它指向当前还未使用的内存块。
s_pCurrent = pMemChunk->mData;
下面是本文最关键的部分,为了提高性能我们令每一个未使用的内存块的头部都保存
一个指针,让它指向下一个未使用的内存块,这样就为未使用的内存块形成了一个单
向链表。当你需要一个物体的内存时可以这么做:
void* returnPtr = s_pCurrent;
s_pCurrent = *((void **)s_pCurrent);
return returnPtr;
这样returnPtr就是你要获得的内存指针,而s_pCurrent通过一个简单的指针转换巧妙
的又指向了下一个内存块,如果上一句看不懂,请你重新复习一下C++教材中关于指针
的解释。
当你需要释放一个物体的内存时,方法和此类似。
*((void**)pMem = s_pCurrent;
s_pCurrent = pMem;
这样就可以将内存块重新连接到未使用的内存块链表中。通常对指针进行转换的时间
非常短,比一般的链表的插入、删除操作速度快的多,因此这个技巧是非常值得借鉴
的做法。

posted @ 2007-03-25 17:02 Dreams 阅读(913) | 评论 (0)编辑