翻译《real-time 3d terrain engines using C++ and DX9 》
心血来潮啊,决定要翻译《real-time 3D terrain engines using C++ and DX9》,希望多广大的游戏爱好者有所帮助。首先声明,没有商业用途的意思(当然不排除要是一不小心弄出本书,小赚一笔的想法,咔咔。。。YY一下)。
本文章原版版权归作者所有,中文版版权归本人所有,其他人未经允许不得转载,或用于商业用途。(模仿人家来个版权声明什么的。。。。汗)
还有还有,水平有限,四六级惨不忍睹。。。。自己也还在学习,所以多多指正(虽然我觉得说得很假)
从第四章开始,主要是觉得前三张介绍的东西不是很具体,如果不会的话看这本书不可能学会的,所以。。。。
第四章 GAIA engine overview
在最初的三章里,我们介绍了一些我们开发我们的引擎将要依赖的DX组件:the Directx3D sample framework, the Direcct3D extension library 和 HLSL,在这一部分的最后一章里,我们通过解释一些引擎中的一些关键的资源(resource)来结束这一部分。当你通读整个引擎源代码的时候,你将经常碰到这些类,所以我们花一点时间来给出这些类和他们的功能的概述。
不要担心这些早期的开发任务看起来有点烦人---确实是这样。然而花准备工作中的少许时间对我们将来进入那些有趣的世界将是非常有帮助。这些简单的类和接口将会为我们接下来的道路上节省很多时间,并且将确保我们我们接下来的写的引擎共享一个一直的框架。我们将通过介绍引擎和实际的编写代码来开始,然后介绍我们将要创建一些特别的资源类型。
这些资源的细节可以通过阅读提供的源代码获得他们最详尽的细节。在这章里,我们提供每个资源类型的简要介绍以及他们设计中的重点。具体的实现则参看提供的源文件。
初遇GAIA,3D地形引擎
我们将把我们的3D屋外地形游戏引擎叫做Gaia,一个希腊神——大地之母的名字(毕竟这是一个地形引擎)。命名引擎对于维护我们的代码和D3DX支持库的逻辑分隔是非常有用的并且也提供了我们自己定义的类型和外来库中定义的类型的物理分隔。
许多游戏引擎利用一些特别的第三方库来处理一些特别的任务,比如管理3D音效或者物理仿真。要做到这点,就有必要采取一些步骤来确保我们的引擎代码不会和外部第三方库代码冲突。最简单的办法就是用名字空间(namespace)包装我们的整个引擎,这个名字空间对外界隐藏所有的类,类型和函数声明。这可能会是一些不习惯与名字空间的程序员产生挫败感的地方,但是使用名字空间要比在每个函数和类型中加上词缀来区别他们要高级。
然而看起来可以试着跳过名字空间转而使用词缀gaia来命名我们的引擎函数,例如gaiaDrawBox(),gaiaPrintText(),这种多余的命名方式将很快陷入麻烦。为了避免我们引擎中类型声明与作者的冲突,gaia的词缀必须被加到每个累,株距类型和函数申明中。
与使用词缀相比,一个名字空间可以用来隔绝整个引擎。所有全局函数和累定义都在这个空间中声明。这高效的模拟了gaia词缀的作用同时提供了一个捷径来避免它必须被显式的标注。这是因为所有在名字空间gaia中声明的类和函数都自然默认的使用这个名字空间,将程序员从必须显式的在类中使用函数和类型时标明中解放了出来。并且,关键字 using namespace gaia 可以在所有引擎源文件的头部声明来通知编译器:名字空间gaia将被作为默认的名字空间,这有一次模拟了显式的表明词缀。
对老练的C++程序员来说,使用名字空间来隔绝库代码并不是什么新鲜的东西。标准模板库(STL)做了同样的事情——将所有库模板分装在名字空间std中。如果这对STL——一个别数百万程序员使用的实用类集合中 很好,那么我们这么做也很好。在本书剩余的部分中,我们总是假定已经使用了名字空间gaia,为了让代码读起来更容易一些,名字空间被省略了。
主程序(the application host)
为了添加我们自己的程序控制层,我们将以Direct3d sample framewrok提供类为基础来简历我们自己的类。框架类CD3Dapplication将作为我们的基类,我们使用它来初始化和枚举视频设备,建立显示模式和处理基本窗口消息。在这个类之上,我们将建立我们自己的资源和设备管理类,控制我们程序的执行和渲染我们的世界。我们通过见了我们的主程序类cGameHost来做到以上几点。
CgameHost类有许多目的。 最重要的是,它是我们整个引擎的核心。我们在设计类的时候采用可管理的方式,创建特殊的单实例(single-instance)类来管理我们引擎的关键方面。例如管理类将处理这些事情:渲染管线,用户输入等。这些自包含的管理类需要一个线程,一个程序访问他们及与他们交互的方法。cGameHost就作为一个所有管理类的容器并且提供重新得到接口访问方法。
建立这个类我们使用singleton声明方式来确保只建立唯一一个cGameHost类实例,并且程序有全局方法来访问这个接口。关于singleton方式的类在附录A中有详尽的细节“Gaia Utility Classes”。对于程序可扩展性的考虑,使用singleton的类同样允许类继承。如果需要自定义的主程序功能,那么可以虫cGameHost派生一个特别的游戏主程序对象来执行附加的动作。使用singleton声明方式的最大好处就是可以在程序的任何地方,包括在管理类自身,cGameHost对象都提供对引擎中使用的的管理对象的访问方法。
除了在管理类之间提供一个交互的总线接口,cGameHost类也作为整个程序的核心类。这个类作为整个程序的壳(shell),包含了所有与主机操作系统的交互。在我们的例子中,这就以为这cGameHost对象接管了包含程序的桌面窗口,所有发送到窗口的程序都由它来处理。我们可以通过从由Direct3D simple framework中提供的CD3Dapplication类派生我们自己的类来的到这个便利。然而,从该类中派生自己的类有利于我们控制程序的某些特定的方面。
作为开始,我们将以一种与DirectX样例稍不同的方式更新我们的的游戏世界。DirectX样例在更新他们的世界状态和渲染每一帧之间维护一个一对一的关系(one-to-one relationship)。在每个过程中,跟新整个程序状态同时渲染一个新的帧。这些更新过程在没有任何类型的管理的情况下发生,所以实时值必须用来决定在每次更新中有多少个物体要移动或者是动画。
我们采用一种更加以符合游戏的方式来控制时间,把时间分成很小的步子,我们称之为”滴答数”(ticks),每个tick代表一个固定的时间间隔。因此,我们每次执行一个tick,我们就知道经过的确切时间。这个采用固定时间步长的方法可以使我们不必去见识一个实时的时钟以在我们的时间里执行更加复杂的可变时间更新(variable-time)。
一个直觉的关于使用这种固定时间方式的批评就是他不能让动画像可变时间方式那样平滑的运行。这是一个有价值的担心。如果我们让我们的游戏世界在某个特定的时间间隔更新,那么我们的动画绝不可能像时间间隔允许的那样平滑的运行。但是,固定时间方式没有要求只能使用一个时间步长,
我们在我们的程序中使用多个更新
tick.
例如,我们可以使用一个频率更形我们的游戏状态,然后在一个更高的频率更新我们的动画。这让我们能简化我们更新程序世界使用的任何状态机(state-machine)和逻辑操作,同时提供平滑的,富有细节的动画。对我们的简单引擎来说,我们规定我们的ticks为一秒的1/30,也就大约33毫秒。我们同样使用这个频率跟新动画,但是如果我们愿意,可以让门以一个更高的频率打开。
我们的时间步长(time-step)方法是在我们重载(overload)的CD3Dapplication的成员函数FrameMove()中实现的,程序清单4-1。我们同时增加了了一些线程管理函数来是我们的程序通其他windows程序和平的同步。我们的程序将会空闲知道一个完整的tick过去。当这段时间过去,我们更新我们的游戏状态,然后继续回到空间状态。如果我们估计我们的更新不需要耗费一个tick的时间来处理更新,我们的程序仍然花费等量的时间空间着。通过在执行完每个tick后降低我们线程的优先权和放弃剩余的时间片段来让其他的线程有机会更新。
如果没有这些线程核查,我们的程序将会耗费100%的可用CPU。这将导致其他等待运行的线程挂起,给操作系统一个不必要的负担来让所有的线程平滑的运行。增加这些措施帮助操作系统高效的实行线程管理并且确保我们会在我们最需要的时候得到运行优先权。
cGameHost包含的一个关键管理对象就是资源管理器(resource manager).我们的程序使用他自己的管理系统来管理我们将要创建的设备。这个资源管理系统的根,cResourcePoolManager类,是建立在一个我们引擎中提供的支持类的集合的基础上的。在我们讨论资源对象本身,我们花一点时间来了解这个我们将要使用的资源池和将会使用的资源管理计划。
创建数据池
一个高效内存管理的关键就是限制分配的总数。完成这个最简单的方法就是将相似的物体组合起来成为一个大的内存分配。我们把这个叫做内存池(date pools),使用他们作为复合物体的容器。其中包含的物体被认为是池的成员,并且通过使用句柄被提供给请求者。成员可以被客户请求和释放,同时以固定的增量来扩张数据池以满足更多成员空间的需要。
这是一个代码会变得复杂的地方。我们想要这些池是类型安全的,所以他们被建成模板类(template calsses)——实用一个特殊的数据类型来代表池的成员。然后,依然需要使用一个普通的接口所有这些池间的通信。当我们随后建立维互纹理,顶点,动画的数据池的资源管理器的时候,在不同的池间通过一个普通的接口实现通信对于创建和销毁这些资源就非常重要了。因此,即使数据池他们自己是独立的模板类的实例,他们也必须继承自一个可以用来创建和释放成员的外部管理类的普通接口。
虽然有充足的理由来简单的分配一个大的数组并且通过他们的数组索引引用他们,但是这个方法非常的不灵活。数组的大小必须事先知道,并且数组不能超过预先分配的大小,因此限制了能够在游戏中使用的对象的数量。固定数组提供了优化的内存使用,同时却牺牲了能够容纳的对象的数量。当我们明确的知道我们游戏中要使用的物体的数量并且确实可以被预先分配内存的时候,这是一个好主意,但对于我们开发循环,我们需要更多一些的灵活性。
程序中管理数据池的解决方案,cDatePool模板类,不是真的包含一个成员对象的大小。事实上,它包含了一个成员对象的链表(linked list),叫做cPoolGroups。每个组包含一个固定数量的成员对象,能够根据需要从数据池中增加和删除。这是一个在高效内存使用和固定对象数目的折中方案。当数据池根据需要增长,它只能按照预订的块(chunk)增长,每次分配一块新的成员对象。
这个设计最有价值的地方就是他可以被转化成常规数组而不会影响游戏的其他方面。在数据池创建对象树的同时嵌套的为每个成员创建了一个独立的缩影。如果数据池维护的对象数退化为常规数组,这个索引值依然不会变。示意图4.1并列的展示了这两个数据结构,并且比较的展示了他们索引方法的相似性。数据池的这个属性使用数据池的灵活性来工作,尽管是少一些效率的方式,我们随后可以用一个高效的,常规的数组一次分配所有的需要的内存来代替。对游戏来说,数据池中的对象作为句柄使用的索引值不会改变。
使用cDatePool类的树状存储方式或者是常规数组方式意味着一块内存块将被重复使用。当数据池中的成员被释放,这些存储空间变为可用的以被接下来的请求使用。因此,我们需要追踪哪些成员是可用的,这样当请求空间的时候可以被快速的在池中找到可用的缝隙。在cPoolGroup中,我们维护一个开放成员的列表。它只是一个对应于组中成员数目的简单的WORD值数组。数组中的每个值表示它对应的池成员是否可用。
每个正在被使用的成员在数组中使用一个常量(INVALID_INDEX)表示它是不可用的。那些可用的成员形成一个索引值的链表。每个成员包含表中下一个可用成员的索引。我们的cDatePool类存储第一个可用实例的索引值,最后一个可用实例的包含它自己的索引值来表示这条链的结束。我们可以通过简单的返回第一个可用的索引并且将我们的内部句柄移动到下一个可用索引(如果有的话)来返回一个可用的成员给调用者。当调用者把成员释放回数据池,我们简单的将我们的句柄直接指向返回的成员,这个句柄先前是指向先前可用成员链表的头部。
查看源代码将更容易理解这个过程。完整的数据池对象的源代码可以在配套CD的source_code/core/date_pool.h中找到。这些对象的函数要全部在书中列出实在是很棘手的事情,但是负责从数据池中添加和移除成员的函数可以在清单4.2中找到。这些例子展示了定位可用cPoolGroup,从中释放一个可用索引,创建一个包含索引值的cPoolHandle对象给调用者的基本操作。释放扯远,成员将在池中背定位并且放在可用成员链表的头节点中。
![](https://p-blog.csdn.net/images/p_blog_csdn_net/gamedevers/list4.2.jpg)
![](https://p-blog.csdn.net/images/p_blog_csdn_net/gamedevers/list4.2-2.jpg)
![](https://p-blog.csdn.net/images/p_blog_csdn_net/gamedevers/list4.2-3.jpg)
![](https://p-blog.csdn.net/images/p_blog_csdn_net/gamedevers/list4.2-4.jpg)
管理共享数据资源
引擎包含集中不同的共享数据。这些资源表示的数据在多人游戏中访问和共享。例子包括纹理,顶点缓存,索引缓存和渲染方法。事实上,许多共享资源都是依赖于显卡,因为他们使用显存作为他们存储的一部分。这些设备的管理器需要访问这些依赖资源。例如,显示管理器需要访问所有纹理,顶点和索引缓存这样它能够处理那些显示设备失去和重新得到的接口的情况。
这需要一些关键的类。首先,我们为所有共享资源定义一个基类,cResourcePoolItem.他提供了一个外部访问方法可以使用的通用接口。这些包括基本的创建和销毁这些资源,启用和禁用共享资源,把它作为流输入和输出磁盘的接口函数。这些基类中的成员函数被定义为纯虚函数,强制所有派生类提供基于这些函数的实现。这个虚接口允许外部访问者,例如一个设备管理器去影响一个基于cResourcePoolItem类的对象的集合而不需要知道他们代表的具体类型。
带着同样的想法,我们增加一个类叫做cResourcePool。这个类是cDatePool的一个扩展,设计来处理基于cResourcePoolItem类的兑现,提供额外的方法来操作这些资源。就像cResourcePoolItem类,cResourcePool类提供一个范围者可以使用的基本接口。假定所有的cResourcePoolItem对象包含一个标准的接口方法集合,cResourcePool提供增加成员函数在全部池成员中实现迭代并且传递数据给这些函数。对外部访问者来说,这提供了重要的对共享数据类型的处理。例如,显卡管理器可以使用这个类型的接口去快速的启用和禁用一个cDatePool对象中的所有纹理。
共享资源通常通过一个文字字符串定义。例如,对纹理来说,这个字符串可能是磁盘上源图像的实际路径名。除了提供重要的操作方法,cResourcePool也存储这些资源名字字符串的查找表。这个给访问者提供一个通过名字查找共享资源对象的方法。使用标准模板库(STL)的map容器类来建立这个资源名字的“电话本”,并且提供访问和查找方法来定位数据池中的每个成员。
最后,cResourcePoolManager单实例类(singleton class)提供了所有贡献资源池的存储仓库。每个cResourcePool可以在cResourcePoolManager对象中创建和注册,同时索引值定义了数据池中保存的资源类型和它所属于的家族(family)。这些索引可以在cResourcePoolManger头文件的全局枚举中找到。一个枚举定义了所有资源可能属于的家族(family)(音频的,视频的,等等)。附加的枚举定义了每个家族包含的资源类型。
联合的家族和类型索引被存储为一个32位值的16位中。这个值,叫做cResourceCode,被用于唯一的定义管理者中的所有资源对象类型。当新的资源数据池背简历,他们在管理器中注册他们自己,提供一个cResourceCode对象来唯一的标识他们自己。外部的访问者可以通过使用合适的cResourceCode从管理器中请求cResourcePool接口来得到对他们的访问。
当我们建立实际的引擎资源和管理器的时候,这些资源类的功能将变得更加明显。现在,参考配套光盘上资源相关的源代码文件来进一步探索他们的功能。资源文件在被张的开头列出。清单4.3是这些资源对象怎样被创建和使用的例子。
资源基类
所有的引擎资源对象都是从cResourcePoolItem类中继承得到的。这个类提供了一个cResourcePoolManager类能够使用的函数的集合,用以创建,销毁,从磁盘保存和载入资源。cResourcePoolItem类对象也为每个资源对象维护一个核心的数据集合,包括定义资源类型的代码,一个资源管理器的接口和一个资源的cPoolHandle对象的索引。一个位标志同样背维护,记录着资源当前的状态。
cResourcePoolItem类提供一个通用的函数集合来重新找回资源的名字,查询资源的状态和重新得到资源数据池的句柄。然而,最重要的成员函数是一个用来维护资源本身的纯虚函数的集合。从cResourcePoolItem资源基类派生得到的引擎资源提供这些纯虚接口函数的实现。这些成员函数创造了可以被管理器对象用来操作资源的接口。清单4.4展示了这些成员函数。
![](https://p-blog.csdn.net/images/p_blog_csdn_net/gamedevers/list4.4.jpg)
使用这个简单接口函数集合,一个管理器能够完全的维护一个资源对象的集合。最值得注意的是,接口函数disableResource和restoreResource允许管理器从可变内存中清除或者重建资源。一个例子就是位于显存中的纹理。如果显示设备接口丢失,资源管理器将被通知去禁用所有的依赖资源。当重新获得显示设备,这些子资源能够很容易的通过调用合适的成员函数来重建。
使用D3DX,我们可以奢侈的使用微软提供的资源建立管理器。对我们的大多数视频依赖资源来说,我们将在设备暂时丢失后对视频依赖资源利用这一自动重获管理。以这种方式建立资源只需要简单的在创建Direct3d资源的时候指定D3DPOOL_MANAGED标志来支持自动内存管理。使用自动内存管理我们也得到一个额外的好处,能够根据需要在内存空间有限的情况下确保纹理被从显存中上载和删除。
这个不会应用与动态资源,例如那些我们经常需要重新计算的纹理和几何体。这些资源最好使用手动管理,因为我们明白什么时候和他们应该怎么背新的数据替代。我们的接口使我们能够有机会使用这两种方法,这取决我我们怎样为我们定义的每种资源类型重载虚函数。
纹理资源和表面材质
我们最简单的资源是cTexture对象。这个类实质上是一一种和我们的资源管理计划兼容的方式容纳一个IDirect3dTexture9对象的包装物体。我们在这里简单的提到这个类是因为我们将在渲染方法中经常使用到这个类。在我们管理包装之外,cTexture同样包含使用D3DX库提供的从磁盘中加载纹理文件的函数。
然而,表面材质是我们自己的创造,需要一点解释。D3DX作为一个向后兼容的偶像,怡然坚持标准的一个材质搭配一个纹理。这一点能够明显的从存储这些对象的一级结构D3DXMATERIAL中看出,这个结构只允许指定唯一一个材质。几年前,这是合适的,当对于当前的顶级显卡,这过时得可怕。
作为我们渲染的需要,我们提供一个更新的表面材质类,cSurfaceMaterial,它能容纳16个标准的标准的带有散射光,镜面光和自发光光照色调纹理。虽然16个纹理看起来会耗尽现今的可以每个过程(pass)使用4~8个纹理的显卡,但这使得我们有可能使用为多过程渲染同一个表面提供纹理。
例如,我们可能建立一个设计使用两个过程(pass)共同渲染的表面材质,每个过程(pass)使用4个不同的纹理。这8个纹理在我们的限制之内,并且提供独立的纹理索引给我们的HLSL着色器使用。我们也可以使用存储空间来存储不同版本的表面材质,比如每个纹理的冬天和夏天的版本。着色器可以在初始化是便秘啊选择合适的集合使用。考虑这些可能,可能16个纹理的限制都太有限了。
cSurfaceMaterials也包含一个位标志的集合,每个纹理位置一个标志。这16个位标志标明某个材质是否被载入到相应的纹理位置。就像我们过一会儿会看到的,这些让我们能够使用渲染方法来使表面材质生效。
渲染方法资源
cEffectFile类是我们ID3Deffect对象的容器。为了提供我们通常的管理和文件I/O函数集合,cEffectFile类使用引擎中设置的变量和常量解析编译后的效果(effect)。正如我们在第二章“Fundamental 3D Object”讨论的那样,Direct3D效果文件带有可以通过注释或者语义而被程序查找的全局变量。程序可以通过调用ID3Deffect类提供的接口函数设置这些变量的值,该类的基类是ID3DeffectBase类。
这些函数通过接受一个字符串或者一个句柄来确认这些变量正在被设置,这些函数在DirectX文档中有详细描述。通过句柄访问效果(effect)变量更高效,因为它不包括查找变量名的字符串比较开销。cEffectFile类通过在加载这些效果文件时预先解析他们来建立一个已知变量类型的句柄列表来利用这一特性。表4.1列出了一些cEffectFile类能够识别的变量类型。
预解析这些语义得到句柄也使我们能够在内部设置一些位标志来表示那些变量出现在一个效果(effect)中。例如,我们为可以每个效果文件中找到的标号的纹理位置(texture slot)设置一个位标志。通过使用逻辑AND操作将这些纹理标志位痛一个cSurfaceMaterial资源比较,我们可以迅速的验证表面材质是包含一个给定的渲染方法所要求的材质数目。
cEffectFile对象的父资源是是cRenderMethod类。我们最终的引擎将在一个复合的,独特的阶段渲染场景。这些阶段的两个例子是光照和凹凸贴图阶段。为了渲染场景,我们允许物体包含多重效果文件,每个渲染过程一个。cRenderMethod类用于存储这些cEffectFile对象文件。cRenderMethod类只是一个到cEffectFile对象文件链接的集合,渲染过程的每个阶段一个。cRenderMethod类也有为每个阶段存储一个独立的cSurfaceMaterial的标志,是它能够描述一个完整的渲染过程。
现在,我们简单的为每个模型使用唯一一个cEffectFile对象。这能够是我们的引擎迅速的建立起来并且快速的运行而不会被凹凸贴图和其他渲染效果阻碍。我们将在本书的第三部分重新回到这个主题,那时我们开始为每个对象使用多个着色器来达到更好的效果。现在,即使我们的模型会包含一个cRenderMethod,这个集合只在“默认”的位置(slot)包含一个cEffectFile对象。
索引缓存和顶点缓存
索引缓存和顶点缓存资源在例子引擎中使用得非常频繁,并且把他们作为独立资源是很有用的。对我们引擎的大部分数据来说,在那些传统模型不提供索引和顶点缓存的时候保留他们然后在某个时候覆盖整个模型符合我们的基本要求。索引和顶点缓存对于动态数据特别有用,当使用设备依赖资源的时候他们必须被很小心的处理。使用动态顶点和索引缓存使我们能够随时建立几何体或者在CPU上动画已经存在的物体。在我们构建地形引擎的时候,我们使用这两种办法。
澄清下,我们所说的动态(dynamic)意味着完全代替。锁定一个顶点或索引缓存来改变一些随机的值将在大多数显卡上产生交错的糟糕的表现。因此我们在我们的接口中对这些资源不支持这种行为。在我们的引擎中,我们在每次需要更新的时候将整个动态缓存全部替代。这让驱动能够维护一个单方向的动态数据。一旦数据背传送到显卡,将不再试着将数据读入系统内存去更新某些特殊值。
类对象cVertexBuffer和cIndexBuffer提供我们的缓存需要的基本操作。这些对象同时包含我们的常规缓存和动态缓存。为了支持动态(可以认为是代替)缓存数据,我们使用一个NVIDIA和Microsoft共同认可的最好的方法来更新动态数据。
这个函数使用一个特大的缓存来存放动态数据。例如,如果你的动态数据由10个顶点组成,你将会创建一个能够容纳100个数据的动态缓存。利用这个大空间,你可以每次只使用10个顶点数据。在第一帧,你使用顶点0~9,索引10~19在第二帧被使用,然后继续下去。当你使用完所有的空间,整个缓存的内容都会被销毁,然后从0号顶点开始处理。这个滑动窗口计划(sliding window scheme)被认为使用起来是很友好的,因为它能导致最少数目的直接内存访问(Direct Memory Access ,DMA)操作被打断。清单4.5展示了基于在Microsoft DirectX9 Developer FQA中列出的算法伪码。
![](https://p-blog.csdn.net/images/p_blog_csdn_net/gamedevers/list4.5-2.jpg)
模型资源
cModelResource类可能是我们将会使用的最重要的类了,它当然是我们最经常使用的资源。这个资源类是一个完整的基于D3DXFRAME层级树的容器。因为复合节点可以背这些对象包含,cModelResource在游戏中可以代表不只一个的物理物体。例如,一个格斗者人物角色可能包含一个完整的制作了动画的骨骼,蒙好了皮的皮肤和衣服网格,和每个武器或者盾牌的静态模型。每个网格一次维护到cRenderMethods和cSurfaceMaterial对象的连接,使cModelResource对象在我们的引擎中表现得是一个完全的实体(entity),而不仅仅是一个几何体。
正如先前提到的,我们漂亮的使用D3DXFRAME层级来完成这种存储。这些对象在第二章“Fundamental 3D Objects”中有描述,可以作为理想的骨骼动画和蒙皮网格的存储方法。一个D3DX对这些结构的实现的很大特点就是他们是用户可扩展的。通过从D3DXFRAME和D3DXMESHCONTAINER中派生得到我们自己的结构,我们可以在这些积累提供数据的顶部添加我们自己的针对平台的数据。
这很重要,因为他允许我们我们挂接我们自定义资源,cEffectFile和cSurfaceMaterial。这将D3DX提供的简单的单纹理包装的的网格转化为HLSL着色的,符合纹理数据库。使用这些扩展的类需要创建一个D3DX接口来管理帧层级中的结点。然而,第一步是申明我们的新数据类型。清单4.6展示了我们对D3DXFRAME和D3DXMESHCONTAINER的扩展。
我们对D3DXFRAME基类的增加是最小化的。用来构建层级数D3DXFRAME_DERIVED对象被分配到一个固定数组中。因此,即使我们使用一个树结构去使用这个数据,我们怡然可以使用它在线性数组中唯一的索引标识每个节点。我们可以这么做因为可以假定我们的D3DXFRAME_DERIVED树不可以动态改变大小和顺序。一旦一个层级被加载,它在它余下的生命中都保持同样的结构。
为了比D3DXFRAME提供的更多的维护家族(family)信息,我们可以在存储他们的数组中使用索引值去标识任何可能的父对象和根对象。frameIndex是整棵树的根节点。prentIndex指向该节点的上一个父节点。使用-1(word值为0xffff)标识未使用的索引值。
为什么不使用指针?这些数据打算作为引用对象。这个课模型的符合实例可以被放在我们的世界中,每个都有它自己的D3DXFRAME_DERIVED结构集合。这些唯一的结构将包含帧节点每个实例的变换矩阵。通过存储我们的家族(family)信息而不是指针作为索引值,就更容易为该模型创建一个新的实例,仅仅通过为框架对象分配新的数组然后拷贝数据就可以了。不能协商,因为索引值和存储数据的数组的根相关。
D3DXMESHCONTAINER_DERIVED结构更复杂一些。在D3DXMESHCONTAINER基类顶部,我们增加了所有我们引擎特有的数据。这包括一个网格使用的cRenderMethods和cSurfaceMaterials的链表,网格的蒙皮信息和网格本身。我们将我们自己的D3DMEHSDATE结构同基类包含的那个分开存储,这样可以在系统内存中的模型和我们的渲染方法将要使用的经过优化并且载入显存中的模型的维护他们分离。
除了允许我们扩展积累,为框架树提供文件I/O的D3DX函数也可以被扩展以提供我们自定义数据。这将创建一个扩展自Direct3D X文件格式的符合我们需求的接口。这要求我们创建3个关键接口类:一个负责分配和销毁我们的数据结构,一个负责在X文件中保存这些结构,一个负责从X文件中加载这些数据。
这3个接口都在D3DX类ID3DXAllocateHierarchy,ID3DXLoadUserDate和D3DXSaveUserDate中提供。为了增加我们自己的数据,我们简单的从这些接口中派生我们的类,并且为每个基类中定义的纯虚函数提供实现。位于随书光盘的d3dx_frame_manager源文件展示了这些函数。这些文件比我们希望的用文字更好的从细节上展示了分配,清除和文件I/O。
渲染队列(the render queue)
维护一个像我们这样的渲染管线最重要的一个方面就是控制那些程序所要求的昂贵的状态改变(state changes)开销。一个“状态改变”就是指示显卡去切换它正在处理的模型和纹理所要作的所有事情。这些很耗时的状态改变包括激活顶点和像素shaders,还有改变D3DX的渲染状态和纹理采样器。使用D3DX的效果文件(effect file)使得这个问题更加困难,因为效果文件可以包含许多的状态改变。这些渲染状态可能在多个效果文件中被设置很多次,导致同样的渲染状态值被一遍又一遍的设置。
我们可以对我们显卡驱动处理这些冗余的状态改变有一点信心,因为这些直接的管理已经超出了我们的控制范围。然而,我们可以直接控制效果文件中“技术”(techniques)来确保整个渲染状态的集合,包括顶点和像素shaders,在一个给定的场景中使用不超过一次。而且,我们也可以扩展我们的管理到模型,顶点缓冲,索引缓冲和模型的激活。如果我们将这些状态改变按耗费给予优先值,我们就可以控制他们被送到显示硬件中去渲染的顺序。
这个可以使用渲染队列实现,可以在我们代码库的cRendderQueue找到。这个队列有点像一个我们要渲染的物体的执行表(execution list)。我们把这些对象提交到渲染队列而不是直接渲染到屏幕。一旦所有的物体都被放到这个队列,我们就可以按照他们的耗费来排序,然后渲染整个场景。这里的技巧是设计一个表示队列一项的方法让它们能够容易被排序。
渲染的基本元素是几何体,材质和渲染方法。这三个东西,带有一些基本的参数比如转换矩阵,就是现实一个对象需要的全部。因为很幸运的有了这个,我们已经有了一个代表我们资源对象cModelResource,cSurfaceMaterial 和cEffectFile的很渐渐的表示。事实上,我们的资源管理器每个对象赋予了一个16位的索引代表他们在一个已经飞配的对象池中的位置。使用这些索引值,我们可以用一个3字(three-word)的值来代表渲染队列中的元素。
假定我们强制给这些字一个顺序。了解到激活一个效果文件(包含了顶点和像素shader)是一个非常昂贵的操作,我们把cEffectFile的索引放在我们三字集合的最高位部分。集合体改变是接下来昂贵的,因为他们包含索引缓冲和多个可能的顶点流。记住这个,我们将cModelResource的索引作为第二个字的值,然后近似的把cSurfaceMaterial索引放在最低的字。
这个构造优先值的过程为我们的渲染队列创建了一个48位有序的索引。如果我们以这些对象的48位值来排序他们,我们可以确保所有在相同的cEffectFile中的对象在表中都是成组的。在每个组中,所有使用相同cModelResource的几何体也是成组的,最后,在一个几何组中所有使用相同cSurfaceMaterial的对象也是成组的。这个有序的列表现在代表了一个渲染场景所需要的状态改变资源的更高效的数量。
cRenderEntry 的代码在这个思想的基础上用一个20字节的值来代表渲染队列的每一个任务。这笔我们48位的例子大得多,然而提供了我们可以用来排序我们渲染操作的更多信息。cRenderEntry提供了12字节的值用来排序,另外的8字节包含一个回调指针(callback pointer)和用户定义参数。这使得一个调用可以提交一个对象渲染并且提供一个当实际渲染将要发生时将被触发的回调函数。cRenderEntry 集中了所有的对象,按优先级对他们排序,然后通过触发提供的回调函数一个接一个的渲染他们。
用来排序cRenderEntry对象的12字节的值包含了我们例子中相同的基本信息,几何体,材质和渲染方法。cRenderEntry扩展这个来包含用来标识每个资源特殊用法的额外参数。例如,渲染方法在队列中不只是用cEffectFile资源的所以字值,还包括用来描述所使用的效果通道(pass)的额外信息。使用这个数据,我们既是按效果文件来排序,也是按效果文件中每个独立的通道。
查看cRenderQueue和cRenderEntry的类定义是熟悉我们将要用来处理我们渲染管线的优先队列的最好办法。一个值得注意的是那些最后用来执行最终渲染的那些回调函数的标志的集合。当渲染队列处理这个列表,它一直跟踪那个当前正在使用的资源。当它遇见了表中的一个新资源,回调函数就被传递,同时回调函数带有一个标志来通知用户去激活新资源。下面的4.7是cRenderQueue和cRenderEntry类的一些重要的代码,接下来的4.8展示了我们的一个对象,cModel,使用渲染队列来提交他自己以渲染,然后调用回调函数在适合的时间执行实际的模型光栅化。
ps:好吧,到这里为止,一章终于结束了,断了很久,居然写了一年。。。我还是强调下,翻译是个很幸苦的事情,即时我还是在不保证翻译质量的情况下,所以有什么错误大家多包涵,这次的代码先不贴了,换了电脑,找起来麻烦,看球先,下次补上。