芳草春晖

偶尔记录自己思绪的地方...

 

Gamebryo使用经验谈(1)

Gamebryo的材质系统

一个模型(有一大堆顶点跟索引数据组成)描画的方式,跟材质有很大的关系。
Gamebryo提供了一个很强大的材质系统。

首先gamebryo使用了一个自定义的Pipeline,这个也是在之前的文章中介绍过的。其实这个Pipeline就是大家最常用的一些Shader。GB帮我们总结出来了,并做成了一个标准的材质。这在GB里叫StandardMaterial。

标准材质跟Pipeline是相对应的。但是标准材质的实现是非常困难的,可以查阅NiStandartMaterial,大约有5000多行代码。GB会首先查找一下Shader文件夹下的那些Shader。
这些Shader的文件名是由一对数字+字母组成的。这些文件都是不重复的,因为GB内部通过Hash码得出这些值。如果在Shader文件夹下没有,那么GB会把当前的渲染方式记录到这个Shader中去,作为缓存。

当然你也可以构建自己的Material系统,比如GB的NiCommonMaterial里也给出了一些构建自己材质系统的例子,不过这是非常复杂,基本思想都是需要维护一个Shader树。

不过自己写Shader是非常方便的,你可以用RenderMonkey或者ShaderFX,把做好的*.fx文件放到Shader文件夹中,MAX再次打开的时候就会找到这些Shader。让美工使用起来非常的方便。

关于优化的问题总结

引擎本身做的很好,不会对速度产生太大影响。但是使用者往往会由于开发经验过低等等因素,导致游戏运行速度太慢。这里有些是因为对Gamebryo系统的不熟悉造成。

美术上:美术如果不熟悉Art文档,不熟悉图形技术的话,用任何引擎都是白搭。其实大部分的浪费都是美术造成的。下面先说说美术的优化
1.注意Mesh的颗粒度,什么意思呢。就是主要不要有太多小物件。三角面很少的Mesh,如果材质相同的话,完全可以合并起来。


1.关于Gamebryo中的update与update select。这是avobject里重要的两个函数,也是Gamebryo中更新场景图最重要的函数之一。update select是做了优化的,也就是说,如果变换矩阵没有做出变换的话,gamebryo不会更新world信息,这样会节省一定的时间,所以如果是静态物体,请优先使用UpdateSelect.
相反动态物体,比如人物。如果确定他一定不是静态的话,请不要使用UpdateSelect,因为这会多做一次对变换矩阵是否改变的判断,这是没有意义的。

gamebryo中的管线

跟开源图形引擎一样,商业引擎的价值在于帮助你做了很多前人的积累。
gamebryo就是这样的引擎,当然unreal做的更加牛。这是后话了,关于unreal的做法我将以后分析。

先来说说gamebryo,跟Ogre不一样的是,gamebryo有一个自己所谓的pipeline。
Ogre很灵活,当然灵活的代价是很多东西需要你自己做,gamebryo也很灵活,不过作为商业引擎,他同时帮你做了很多事情。




Gamebryo中的对象系统之一:智能指针和引用计数

本文是一系列Gamebryo底层系统的介绍。
Gamebryo除了是一个强大的游戏引擎之外,在研究他的源代码同时,我惊喜的发现他提供了很多值得我们在写代码过程中学习和借鉴的东西。
本文就是来介绍这些底层系统到底是在穿插在引擎中使用的。
Gamebryo的底层系统运用于他所有的模块和工具中,他提供了对象管理,引用计数,对象持久化(有点像Java的东西)以及快速的运行时型别转换等功能。



首先我们来介绍一下引用计数与智能指针。

在Gamebryo中所有基础NiRefObject的Object都会被计数的。这就意味着Object存储着所有引用他的数目。如果需要长期的引用一个对象的话,需要声明一个引用,并在结束时释放掉改引用。当引用计数等于零的时候,对象会自己释放掉所申请的内存区。不过这样看起来用起来会比较烦,好在GB提供了“smart pointers”来减轻繁杂的工作。

要注意的是所有继承NiRefObject的子类都必须通过堆来分配,不用使用于静态类型对象或者栈上对象。因为当引用计数为零的时候,该对象会被删除,而删除静态对象或者栈上对象都会导致内存错误或者崩溃。

谈谈批量渲染 

     随着显卡寄存器数量越来越多,批量渲染已经不是什么稀奇的事情了.

      其实,根据我最近研究发现,暴雪早在DX8时代就做了批量渲染这件事,所以一举占据了RTS老大的地位.很凑巧的时候我现在的项目也是个RTS类游戏.所以批量渲染就成了一个亟待解决的问题.

      Gamebryo是支持MeshInstancing的,就是模型的批量渲染。
    哦,先介绍下DX9支持的几种Instancing的方式吧,一种叫HardwareInstancing,中文叫硬件批量渲染吧,其实是DX9提供了SetStreamSourceFreq这个接口,让你可以把一个数据源多次使用,比如我们要批量需然一组模型,每个模型有自己的Translation信息,但是他们的顶点和索引数据是共用的。例如,你要批量10个这样的模型的话,只要抽取出他们不同的数据上传到一个数据源中,然后顶点和索引数据重复使用十次就可以了。
    另外一种是ShaderInstancing,所谓Shader Instancing,意思就是,把Instancing数据传到vertex shader里去,在渲染的时候通过一些方法索引到这些Instancing数据,用来对顶点数据做不同的描画。

Gamebryo帧渲染系统详解

如果没记错的话,我曾经写过一篇关于Gamebryo帧渲染系统的内容。估计当时剖析的不是太详细。那么现在我在这里重新讲一下帧渲染系统,希望能把他将清楚。由于我手头没有代码,而且又是商业引擎,所以很多函数我并不是完整的使用gamebryo中的,能使用伪代码的地方我尽量使用伪代码。

好,首先再次强调下帧渲染系统是逻辑系统,严格上跟渲染没有任何关系,就是说如果你可以绕开帧渲染系统,一样可以画出想描画的东西,只不过GB这样做的目的是使得渲染层次更加清晰,灵活了。
OGRE也有类似的概念,在Ogre中也可以定义自己的层,但是由于没有帧渲染系统,所以层次上不如Gamebryo灵活,方便易用。

RenderFrame 和RenderStep我就不再赘述了,因为这两个概念很简单,里面也没有实质性的内容。你想怎么理解都可以,前者是后者的超集,Step又是Click的超集。

详细说一下RenderClick和RenderView。RenderView可以理解成我们所要描画的物件。RenderView里有AppendScene这样的接口,就可以把所有想View的东西都挂接上去。RenderView里还有个重要的工具叫做Culler,Culler是做什么用的呢?是负责裁剪的,这里的裁剪是逻辑上的裁剪,就是精确到几何体级别的裁剪。(注意不是三角面级别的)。Culler是作为Processor被加进去的,就是一个裁剪的过程。Culler提供了一些抽象接口,来满足用户的自定义裁剪。就是说你可以根据你的需要来在渲染前进行裁剪。

下面说到RenderClick,RenderClick精确的字面意思就是一次描画,这个类的功能也基本是这样,知道了要画的东西,但是要画到什么上去就需要RenderClick了,每个RenderClick对应了一个RenderTarget。就是要描画的地方。一个描画的过程是这样的,首先找到RenderTarget就是要描画的地方,因为如果是后期特效,有时候会有多个RenderTarget。
知道了要描画的地方后就开始描画了,首先从RenderView里找出要描画的几何体(这里的几何体已经是被裁剪过后的几何体了),然后Click里再做一个处理,这里的处理也是个Processor,就是意味着用户可以定义。这里处理的目的基本上是对物体做一个排序。这里不会再对几何体进行裁剪了,而是进行类似Alpha排序等这样的工作,保证物体被正确的按层次画出来。最终得到的RenderObjList就是用来描画的了。

整个过程其实还是比较简单的,给用户很多自己选择的机会。是一个不错的设计。

GameBryo ---- 模板类

GameBryo提供了一太基本的模板容器类,这些容器在整个库内使用。

Lists

NiTPointerList对象可以包含和管理指针,智能指针,以及其他任何大小小于等于指针的元素,该链表可以有效的插入和删除所有元素,以及正向遍历和反向遍历所有元素,同样可以通过给定值查找元素的实体和所在位置,NiTPointerList的元素的内存是从一个共享内存中分配的,从而提高类的执行速度和内存效率,如果链表元素大于指针,程序可以使用NiTObjectList.

Array

NiTArray对象实现勒几乎可以包含所有对象的动态数组,该数组可以缩放,并且可以压缩(通过转移元素来移除空空间)。内置类型(char*, float, int等)使用NiTPrimitiveArray。NiMemObject派生出的类型使用NiTobjectArray。注意NiTArray的元素上限为65535;如果大于该限制,使用NiTLargeArray派生出的类,比如NiTLargePrimitiveArray或NiTLargeObjectArray。

Map

NiTPointMap对象实现勒哈希表的功能,允许任何类型的键值来影射到指针,智能指针,以及其他任何大小小于等于指针的元素,并能快速的储存和查找键值对,不过不能使用字符串键哈希表,而NiTStringPointerMap对象是专为此设计的,NiTPointMap和NiTStringPointerMap的元素内存也是从一个共享内存中分配的,从而提高执行速度和内存效率,如果map元素大于指针,程序可以使用NiTMap和NiTStringMap

StringMap

NiTStringMap和NiTStringPointerMap对象的函数和NiTMap和NiTPointerMap风格类似,但是允许字符串作为键,并且通过字符串比较来进行键散列

FixedStringMap

NiTFixedStringMap对象函数和NiTMap对象风格相似,但是允许NiFixedString对象作为键

Queue

NiTQueue实现勒基本所有类型对象的先进先出队列,但不提供智能指针,需要注意链表可以当做队列来使用

Set

NiTSet实现了基本所有类型的无序集合,也没提供智能指针,内置类型(char*, float, int等)使用NiTPrimitiveSet。NiMemObject派生出的类型使用NiTobjectSet,智能指针则使用NiTObject或者NiTPrimitivePtrSet,这将正确的处理引用计数。

Pool

NiTPool实现了小型对象的池,使得程序能通过一个池来分配小型对象,并能重复使用,而不是单独的去分配和释放一个小型对象



GameBryo ---- 场景图的几何更新

一个程序会在需要的时候改变一个节点的转换,计算该节点的时间转换,以及该节点子节点的其他转换将被延迟,直到应用程序调用例行的update。

update是高效使用深度优先来遍历子图计算世界转换和世界包围球,从而最大限度的减少节点的访问,当向下递归时,转换被更新,包括所有的自节点,当矩阵更新后,世界包围球通过递归调用返回

总之,转换在递归中被更新,而包围球在递归返回时得到

通常大多数的对象都不会移动的,所有只更新只限于小部分可以移动的对象。在场景图的数据处理初始化中,当应用程序使用到场景图前,至少要对场景图的根节点进行一次例行update,这样保证所有节点的世界信息及本地信息都是最新的。

在应用程序帧到帧的运行中,当符合下面任何一条时,应用程序必须调用对象“O”的update。

·O被绑定到父亲节点或者从父亲节点解除绑定

·O绑定了一个新子节点或者解除了一个子节点的绑定

·O任何一个转换被改变

注意一下,调用当前发父亲节点或者任何祖先节点的update可以代替当前节点update的调用。例如,如果对象A绑定了子节点B和C,只需要调用A的update就够了。没有必要调用三个对象的update,应用程序会以批处理的方式更新。

例如,当应承需要改变了一个活动角色的所有的关节矩阵,他应该推迟update直到所有的改变都完成,并且只需要调用一次角色根节点的update。

但是,注意update尽量在场景图的更下层调用,如果一个场景图每帧只有一个叶子被改变,那么调用根节点的update就太过分了,这将降低性能。


GameBryo ---- 网格数据共享

为了世界中对象只需要少量的顶点,一棵树可以用来代表场景图,每个对象被单独表现为树中的节点。但是,在绝大多数场景图中,会多次出现一个需要大量内存来存储的复杂对象,绝大多数的内存是消耗在纹理和顶点数据上,比如纹理坐标和法线。

如果一个应用程序需要多份这样一个对象,是有可能通过共享的NiDataStream来分享模型空间的几何信息,颜色,纹理和其他颜色。换句话说,若干网格对象可能分享NiDataStream对象,在这种情况下,场景图是有向非循环图,而不是一棵树。

在几何数据共享的情况下,叶网格对象共享NiDataStream对象的模型空间网格。但是,两个模型数据的实例是在世界的不同位置,因为他们代表的多个网格对象,并且每个副本自己单独的转换。

这些管理是在应用程序的内部透明处理的——应用程序只需要建立两个网格对象使用同一个NiDataStream对象。网格对象甚至要比一套最小的网格数据小的多,因为网格对象不像数据流,不存在每个顶点的数据

下面的图象是一个典型的情况:

 

 

 

                                            (两个网格对象使用同一个NiDataStream)

两个叶网格对象分享一个轮胎NiDataStream对象的顶点位置和法线。一个网格对象对应到自行车的前轮,另一个对应到自行车的后轮。NiDataStream对象自身存储的网格顶点的位置和法线被共享,两个网格对象都保存表现自己网格的转换。

注意以下,Gamebryo不为任何类型的NiAVObject提供多父亲的功能。绑定一个已经拥有父节点的对象C,会导致C自动脱离原来的父节点


GameBryo ---- Working with Properties

Gamebryo通过一套相互独立的渲染性质为每个能渲染的叶子对象定义了渲染属性,每一个渲染属性都为能渲染的对象定义了某一方面的渲染状态,并且都是NiProperty的子类。对个可渲染的对象可以共享渲染属性。针对一个对象的完整渲染状态是所有属性的完整组合。当属性状态对象存在与NiRenderObject叶子节点时,这个个体渲染属性就被绑定在场景图的任何NiAVObject上。这正式用于每个可渲染叶子对象产生属性状态的每个NiAVObject的属性。一个属性被绑定到一个NiAVObject将影响所有子数上的子对象(包括它自己),除非在子树中同样的属性类型被其他属性所替代。如果没有任何属性被设置到场景图的对象上,该对象会通过合适的默认属性来绘制。每个属性类型的默认相当于为该类型设置一个默认构造,每个NiAVObject都包含了一个绑定与它的所有属性的链表,一个NiAVObject可能没有任何属性绑定,也有可能绑定一个或多个属性。所有的方式达到一个NiAVObject能绑定的每样属性的最大值。注意确保应用程序任何时候都不能给单一的NiAVObject绑定一个以上已经类型的属性。一个单一的NiAVObject绑定已经类型的一个以上的属性会导致奇怪的视觉效果和未知的问题。

绘制属性类型

NiProperty对象在Gamebryo中的数据层次如下:

NiObject

              NiProperty

                              NiAlphaProperty

                              NiDitherProperty

                              NiFogProperty

                              NiMaterialProperty

                              NiShadeProperty

                              NiSpecularProperty

                              NiStencilProperty

                              NiTexturingProperty

                              NiVertwxColorProperty就如上面讨论的,属性设置从根到叶层次。一个被绑定到NiAVObject对象的属性会影响该对象及它的子对象。除非在更低的子树中绑定该类型的其他属性。因此,一个可渲染叶节点的当前状态是由场景图中它祖先的链所决定的

                              NiWireFramProperty

                              NiZBufferProperty

更新属性到集合物体上

 

Gamebryo会让整个场景图保持绑定属性,绑定在每个可渲染叶对象的属性状态是包含所有提供的类型的属性的数组,这样,每个可渲染的对象包含一个直接指向用来绘制的渲染属性的指针。这很重要,因为渲染只涉及可渲染的叶对象,而不是整个场景图。于是每个可渲染对象的属性状态都是继承其他可渲染对象的所有属性的副本。

Gamebryo使用一个系统类似使用NiAVObject::Update函数来更新这些属性状态对象。这个类似的渲染属性函数是NiAVObject::UpdateProperties。当出现下面的情况UpdateProperties必须在obejct"O"或任何一个他的祖先调用下一次渲染

·一个以O个根节点的树刚被创建

·一个属性被绑定到O或从O移除

·O被绑定到节点P或从P上被移除

注意,当只改变了一个已有属性,应用程序不需要调用UpdateProperties。

要实现最佳性能,这些UpdateProperties的调用可以以相同的方式进行批处理来执行批处理更新,如果应用程序将在子树上绑定或解除绑定许多属性,它必须调用所有的绑定或解除绑定函数,然后在子树的根部调用一次UpdateProperties,通常的,因为属性和子节点的绑定和解绑没有每一帧这样频繁,所以UpdateProperties要比每帧积累属性快的多,但是对于程序员。将多出一个额外的小负担。


GameBryo ---- SoftParticles

软粒子主要是为了解决粒子广告牌和场景几何相交时,产生的生硬边缘,如下图烟雾与地面相交时的边

为了解决上面的情况,我们需要用到场景的深度信息,如下图:

在一般的渲染管线中,点P3就是产生生硬边的点,为了改善这种情况,SoftParticles通过改变粒子的alpha值来处理粒子后面的场景,这里使用了自定义的shader常量来决定距离d以便我们调整alpha值(d为world place中),任何距离原场景深度大于d的粒子相素我们将不处理他的alpha值(对应上图P1的公式),

上图中点P1正好达到该距离,点P1到P3的alpha混合程度会递增,距离d设置的越小,那效果就越接近于硬粒子的效果,因为P1的条件很容易满足,对alpha值的修改会减少,但是如果距离设置的过大,那P2就很容易满足,这样导致Len/d产生的值很小,让粒子变的很透明,造成的粒子很稀疏,具体的效果要自己手动调节。

这种边缘软化的方式只是近似的,当场景的法线于摄像机方向锤子时会失效,当出现这种情况时,随便粒子与相交面很接近了,但因为摄像机与相交面近乎垂直,而粒子相素的深度检测是沿与摄像机方向的,从而产生一个很大len值,导致了本来应该成为P3效果的点,成为P1。

DEMO5个类,SoftParticles,MRT_ColorDepthMaterial,SoftParticlesMaterial,SoftParticlesManager,MRT_ColorBlackMaterial

SoftParticles::CreateScene()

负责创建场景,从Nif文件中获取场景,摄像机及粒子系统,设置alpha排序,剔除,及默认材质MRT_ColorDepthMaterial, MRT_ColorDepthMaterial继承于NiStandardMaterial,重载了函数bool HandleFinalVertexOutputs()和函数bool HandleFinalPixelOutputs(),这两个函数分别在vertex shader和pixel shader的最后执行,通过HandleFinalVertexOutputs函数,为vertex shader的output结构增加成员NiMaterialResource* pkVertOutViewTexCoord = Context.m_spOutputs->AddInputResource("float4", "TexCoord", "World",
"PosViewPassThrough");作为纹理坐标的格式输出

NiMaterialNode* pkSplitterNode = GetAttachableNodeFromLibrary(
        "PositionToDepthNormal");
    kContext.m_spConfigurator->AddNode(pkSplitterNode);
    kContext.m_spConfigurator->AddBinding(pkViewPos,
        pkSplitterNode->GetInputResourceByVariableName("Input"));
    kContext.m_spConfigurator->AddBinding(
        pkSplitterNode->GetOutputResourceByVariableName("Output"),
        pkVertOutViewTexCoord);

获得自定义函数PositionToDepthNormal,将pkViewPos作为传入参数,把新增的pkVertOutViewTexCoord作为输出参数

而HandleFinalPixelOutputs则在输入结构中增加float4 WorldPosProjected : TEXCOORD6;代码如下

NiMaterialNode * pkInputResource;
    pkInputResource = kContext.m_spConfigurator->GetNodeByName("PixelIn");

    NiMaterialResource* pkDepthFromVP = pkInputResource->AddOutputResource(
        "float4", "TexCoord", "World", "WorldPosProjected");

然后在输出结构中增加深度颜色,通过WorldPosProjected来赋值,CreateScene()还将场景中所有的粒子系统添加进SoftParticlesManager,添加完所有的ParticlesSystem后,SoftParticlesManager通过Initialize()函数,创建默认的粒子材质,以及SoftParticlesManager自己的RnderView及click,然后通过InitializeScene()为每个粒子系统设置材质,并添加进RnderView,这里要注意执行顺序

SoftParticles::CreateFrame()

该函数中,通过获取后备缓冲的属性,来创建相同一张texture,用来渲染深度,不过格式要设置为NiTexture::FormatPrefs::SINGLE_COLOR_32,表示32位的单通道颜色,用R通道表示,然后新建一个RenderGroup,要包含原来的后备缓冲以及先建的位图缓冲,让场景同时渲染到这两个缓冲上,并将纹理缓冲作为SoftParticlesManager中创建的click的一个输入


GameBryo ---- MSAA

MSAA(MultiSampling Anti-Aliasing)

GB中MSAA实现的关键代码基本是直接用的D3D,除了自己的渲染批次系统,DX9不能直接对渲染到纹理启用MSAA,但是提供了渲染表面surface,可以对surface启用MSAA。

实现的关键步骤大致如下

1。NiRenderedTexture::Create创建一个和背景缓冲一样大的RenderTexture,NiRenderTargetGroup::Create利用RenderTexture创建RenderTarget,屏幕最终渲染的目标就是该RenderTarget,我们需要的也是将MSAA后的数据给RenderTexture

2。通过D3D的CreateRenderTarget按照自定的MSAA级别来创建surface,利用surface的buffer创建另外一个MSAARenderTarget

3。新建一个RenderView,GB中用的NiScreenFillingRenderView,就是D3D中四个顶点组成的矩形,该RenderView绑定一个baseTexture为步骤1所创建的RenderTexture的NiTexturingProperty,创建RenderClick挂接该RenderView,并设置一个CallBackFunc,然后将该click插到mainClick后面,这样用两pass来完成MSAA

4。主click将画面渲染到开启MSAA的MSAARenderTarget,然后进入新click的CallBackFunc,获取MSAARenderTarget的buffer,用D3D的StretchRect复制数据到RenderTexture,这样新click就会渲染出进行MSAA后的texture

posted on 2010-04-20 00:04 CrazyDev 阅读(4042) 评论(2)  编辑 收藏 引用 所属分类: 游戏引擎

评论

# re: Gamebryo使用经验谈(1) 2011-04-12 18:17 yzhg

楼主经验会误导别人
你要是有程序Demo 和详细的图纸在出来百话,我就觉的不可笑
什么叫暴雪用批渲染决定他游戏的地位,这个是你主观猜测的吧,别瞎说。  回复  更多评论   

# re: Gamebryo使用经验谈(1) 2011-04-12 18:20 yzhg

研究一个从根上就不是自己的系统,真是怪,为什么不塌心的一点一点用c++写
呢,我看到国外有人能用c++ opengl 写个rts 不从根上做一辈子都不会
更何况rts game,真是说笑  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


导航

统计

常用链接

留言簿(1)

随笔档案

文章分类

文章档案

C/C++

CEGUI

Friend Bog

Game Industry

Lua

OGRE

Other

搜索

最新评论

阅读排行榜

评论排行榜