Terrain 读书笔记:Chapter 4

<Real Time 3D Terrain Engines Using C++ And DX9>

Chapter 4. Gaia Engine Overview

读书的进度明显的慢了,可能是这两天事太多了吧。不过这章也的确太长了点儿,和前三章的长度有一拼。

Snook说,最好的学习引擎的方法就是去读源代码。FT,我是看电子版哎,哪里去找配套的CD-ROM啊?恳请有这本书配套光盘的朋友能打包email给我,拜托拜托!

看不到代码的感觉是痛苦的,非常痛苦……


Meet Gaia, the 3D Terrain Engine

人家这名字起的多好——该亚(Gaia),大地之母。一听就是干地形的。Snook说给引擎命名还是很重要的,这样可以用于区分自己定义的东东和DX原有东东,我倒是觉得没有名字的引擎就不Cool了。

而且,如果一个引擎采用了第三方库来实现例如3D音效或者物理模拟的时候,保不齐就会有代码上的冲突,这样的话,最好就是用一个Namespace把引擎包起来,一了百了。

如果不使用Namespace的话,就要像openGL那样在每个函数前面加个gl。每个阿…… 好多好多。所以使用Namespace还是个非常不错的选择。


the Application Host

cGameHost类,是一个建立在CD3DAppliation基础上的类,是一个单体(singleton),更是引擎的核心。

cGameHost里面还有控制Ticks的部分,就是说把引擎的更新速度固定。Snook给了一段cGameHost::FrameMove的代码,除了实现了Ticks外,还利用调整当前线程的优先级的方法来避免引擎一直是霸占着CPU,挺体贴的。


Creating Pools of Data

自己来写Data Pool?居然在一个Terrain Engine里面还有一个自己的内存管理部件,这个Gaia挺正规嘛。

为什么要做这个Pool呢?因为当alloc过多的时候,每次都直接向内存申请是很没有效率的。alloc越多,效率越低。那么,最好的办法就是预先申请一个很大的内存空间来做一个pool,可以把这个pool看成是一个container,以后的alloc都在这个pool中划出去,不再向内存申请。这样效率就高多了。

这个Pool在设计上有个矛盾:既要保证pool的类型安全,即不同的类型要用不同的template class来表示。但是又需要一个统一的接口来进行Request和Release的操作。解决的方法很简单,建一个Common Interface,所有的template class的都从这个接口继承和扩展,就OK了。

做Pool最简单的方法就是用array,一次做一个足够大的objects类型的array。这样效率高,但是怎样才算足够大得预先知道,就是说得知道objects的最大数量。如果可以预先知道最大数量的话,就可以用这个。否则的话,array就显得不够灵活。

Gaia采用的方法是这样的:有个叫cDataPool的模版类,里面有个链表,叫cPoolGroup。这个链表的每个元素是一个array。这样就在效率和灵活性上取得了平衡。

这样设计还有一个好处,就是一个cDataPool可以唯一对应一个array,索引都不用变。其实cDataPool完全可以看成一个2维数组了,当然和普通的array一一对应了。

如何快速的找到一个available的member(内存区域)呢?Snook在每个cDataGroup里面加了一个链表,用来储存available的member的索引。经常索引的地方就用array,经常增删的地方就用linked list。固定的地方就用array,动态的地方就用linked list。这个道理大家学DS的时候都学过,但是真正能够融会贯通的人太少了。看看这个Data Pool的设计,就知道自己还嫩的很……


Managing Shared Data Resources

在引擎中,有些东东是可以被许多game objects共享的,比如testure, vertex buffers, index buffers, render method等等。

在Gaia中,用cResourcePoolItem这个Base Class来描述可共享的资源。这样做当然也是为了让所有的资源可以有一个common interface。这些接口包括生成、销毁、允许、禁用以及磁盘文件的读写等等。这些接口当然都是virtual的,等着别的类来继承。

cResourcePool是一个继承cDataPool的class,用来管理cResourcePoolItem。

每个可共享的资源都有一个Text String来记录这个资源的绝对路径。而且Gaia还在cResourcePool中使用了STL map来做映射,把Text String和资源一一对应。STL的map可是用RB Tree写的,O(logN)的效率哦。

最后,做一个cResoucePoolManager的单体来管理Resouce Pools。cResourcePool可以在cResoucePoolManager中注册,并且还要标注资源的类型,比如声音、动画等等。

资源文件先是被分为不同的家族(families),然后每个家族里面有不同的类型(types)。资源的类型用32bits来表示,family和type各占16bits,叫做cResouceCode。注册的时候要提供该资源的cResouceCode,以用来帮助Manager找到正确的接口。


The Resouce Base Class

cResourcePoolItem是一个抽象类,它只提供接口(纯虚函数),需要其他类继承并实现具体的方法。

其中disableResouce和restoreResouce是当Device Lost的时候,销毁所有设备相关的objects,然后等到Devie Restore的时候再恢复。

D3DX也提供resource manager,当你建立资源的时候使用D3DPOOL_MANAGED这个标志的时候,DX会提供自动的管理。

但是这对于动态资源并不适用,比如texture或者geometry,如果需要经常的计算的话,最好进行手动的管理。所以,在Gaia中写Resource Manager并不是重复劳动。相反,对于静态数据来说,在重载基类的时候,还可以选择DX来管理或者手动管理。嗯,这个的确很灵活。


Texture Resources & Surface Materials

最简单的资源莫过于cTexture,因为太常用了。而且这个类就是对IDirect3DTexture9做了一个简单的封装。

而对于cSurfaceMaterial,则是对D3DXMATERIAL的一个改进。因为material一次只能指定一个texture。这对于多年以前还是适用的,但是现在明显不行了。所以cSurfaceMaterial支持16张纹理。16张?现在的显卡不支持吧…… 没错,Snook用了一个方法,就是通过多纹理+多次渲染(multi-textures + multi-passes),这样就能够实现了。16张Snook还嫌少呢……


Render Method Resources

Gaia对ID3DXEffect做了封装,这就是cEffectFile类。除了基本的ID3DXEffect接口外,cEffectFile类还提供了变量的操作,当然也是用ID3DXEffect和ID3DXBaseEffect来进行封装的(Snook写成了ID3DXEffectBase,害我查了半天MSDN)。

因为对于操作Effect Files的变量来说,最好的方法是使用Handle。那么,可以预先定义一些语义,然后把他们的Handle做成List,这样效率就很高了。

另外,一个cEffectFile链表构成一个cRenderMethod,用来描述一个场景的渲染。而且在cRenderMethod中,还有一个cSurfaceMaterial来描绘整个渲染过程。


Index & Vertex Buffers

IB和VB都太常用了,作为Gaia最主要的数据,Gaia的完整模型都保留了IB和VB。IB和VB对动态数据来说非常有用,但是要小心处理才是。使用IB和VB,既可以构建几何体,也能够动画已经存在的几何体。Gaia中这两种情况都存在的。

所谓的动态,其实就是替换。用Lock锁住IB和VB来更改其中的值在大多数显卡中效率很差的。一旦更新,就全部替换。这给动态数据一个单行线:只能到显卡去,不能再回到内存来了(读回来更新部分数据)。

基础的类就是cVertexBuffer和cIndexBuffer了。其中这些buffer既可以是普通的,也可以是动态的。

怎么样能够实现动态呢?Nv和MS提出了一种方法。比如有N个顶点,想动M个frame,那么就声明一个M*N的IB,这样第一帧用0到N-1,第二帧用N到2N-1,以此类推。


Model Resources

用来表示模型的cModelResource应该是最重要的资源了。在它的内部,有一个D3DXFRAME的层次树。因为里面可以储存很多顶点,所以在场景中,一个cModelResource甚至可以表示多个物体。比如一个竞技手的模型,除了自己的骨骼,还有肌肉和衣服,还有武器和铠甲,每个mesh都要cRenderMethod和cSurfaceMaterial来描述。然后把他们做成一个实体,就是cModelResource了(一个cModelResource表示了多个物体)。

第2章中说过,D3DXFRAME和D3DXMESHCONTAINER是可以扩展了。这里就扩展了他们。通过用cEffectFile和cSurfaceMaterial的扩展,Snook把一个单纹理的mesh变成了一个可以通过HLSL渲染,并且支持多纹理的mesh。

通过对X文件的扩展,cModelResource也实现了File I/O。D3DX提供了ID3DXAllocateHierarchy,ID3DXLoadUserData和ID3DXSaveUserData这三个接口来实现user的扩展。你需要的就是把基类继承下来然后把重写纯虚函数就OK了。


Scene Nodes & Objects

cSceneNode在3D环境中定义了一个特定的坐标系统(?)。这些节点通过“双亲-孩子”的继承关系连接到一起。所有在场景中的object都是从cSceneNode继承而来的,包括cSceneObject。

cSceneObject是基于cSceneNode的,但是在Node周围用一个有边界的盒子来定义一个体积。cSceneObject可以用来表示一个游戏中的模型,这样的话,这个盒子就可以租略的来表示这个物体了。(这应该就是用来做碰撞检测的吧)


the Render Queue

做这个Render Pipeline的主要意义在于减少Render State的改变,因为可能在一个frame中要渲染很多的effect file,那么每个里面对同一个render state都要做设置,而且是设置同一个值,那么它就被反复的写入同一个值。

通过Render Queue,我们可以去掉多余的state改变,还可以管理models,VB,IB,texture这些东东。

需要渲染的object先进入Render Queue,然后Queue会被排序,最后才被渲染出来。

显示一个object需要三个东东:geometry,material和rendering method,另外还有些参数,比如转换矩阵之类的。因为每个资源都有一个固定的索引,那么用三个索引就可以表示一个Queue中的member。

把这三个index排序的话,最重要的是effect file,然后是geometry,最后是material。

用这三个index组成一个48bits的值,就是利用这个值对Queue中的member来排序的。这样,使用同一个effect file的几何体就被排在了一起,然后在这些当中,所有用同一个materials的又被分在了一起。(就是一个多关键字排序)这样渲染的话,就非常有效率了。

cRenderEntry code把这种思路扩展了,它使用了20bytes。这样不但可以储存以上的那些值(12bytes),还能提供更多的信息。这些信息包括一个回调函数(callback function)和用户定义的参数。这个回调函数是用来做触发用的。Render Queue收集所有的entry,然后按照priority排序,最后逐个渲染,并且触发回调函数。

那12bytes用于储存geometry,material和rendering method的信息,除了index外,每个还多了一个附加参数,对于这个参数,不同的资源就有不同的用途了。


浩浩荡荡的读完了这章,就算是对Gaia有了一个初步的了解。说实话,从这些类的设计上,我完全看得出来Snook无论是对软件开发还是对即时渲染都是有着很深的造诣的。我以前也始终理解不了那些商业游戏究竟复杂在什么地方,现在看来的确是太复杂了,难怪有如此高的效率。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值