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提供了一太基本的模板容器类,这些容器在整个库内使用。
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实现了小型对象的池,使得程序能通过一个池来分配小型对象,并能重复使用,而不是单独的去分配和释放一个小型对象
一个程序会在需要的时候改变一个节点的转换,计算该节点的时间转换,以及该节点子节点的其他转换将被延迟,直到应用程序调用例行的update。
update是高效使用深度优先来遍历子图计算世界转换和世界包围球,从而最大限度的减少节点的访问,当向下递归时,转换被更新,包括所有的自节点,当矩阵更新后,世界包围球通过递归调用返回
总之,转换在递归中被更新,而包围球在递归返回时得到
通常大多数的对象都不会移动的,所有只更新只限于小部分可以移动的对象。在场景图的数据处理初始化中,当应用程序使用到场景图前,至少要对场景图的根节点进行一次例行update,这样保证所有节点的世界信息及本地信息都是最新的。
在应用程序帧到帧的运行中,当符合下面任何一条时,应用程序必须调用对象“O”的update。
·O被绑定到父亲节点或者从父亲节点解除绑定
·O绑定了一个新子节点或者解除了一个子节点的绑定
·O任何一个转换被改变
注意一下,调用当前发父亲节点或者任何祖先节点的update可以代替当前节点update的调用。例如,如果对象A绑定了子节点B和C,只需要调用A的update就够了。没有必要调用三个对象的update,应用程序会以批处理的方式更新。
例如,当应承需要改变了一个活动角色的所有的关节矩阵,他应该推迟update直到所有的改变都完成,并且只需要调用一次角色根节点的update。
但是,注意update尽量在场景图的更下层调用,如果一个场景图每帧只有一个叶子被改变,那么调用根节点的update就太过分了,这将降低性能。
为了世界中对象只需要少量的顶点,一棵树可以用来代表场景图,每个对象被单独表现为树中的节点。但是,在绝大多数场景图中,会多次出现一个需要大量内存来存储的复杂对象,绝大多数的内存是消耗在纹理和顶点数据上,比如纹理坐标和法线。
如果一个应用程序需要多份这样一个对象,是有可能通过共享的NiDataStream来分享模型空间的几何信息,颜色,纹理和其他颜色。换句话说,若干网格对象可能分享NiDataStream对象,在这种情况下,场景图是有向非循环图,而不是一棵树。
在几何数据共享的情况下,叶网格对象共享NiDataStream对象的模型空间网格。但是,两个模型数据的实例是在世界的不同位置,因为他们代表的多个网格对象,并且每个副本自己单独的转换。
这些管理是在应用程序的内部透明处理的——应用程序只需要建立两个网格对象使用同一个NiDataStream对象。网格对象甚至要比一套最小的网格数据小的多,因为网格对象不像数据流,不存在每个顶点的数据
下面的图象是一个典型的情况:
(两个网格对象使用同一个NiDataStream)
两个叶网格对象分享一个轮胎NiDataStream对象的顶点位置和法线。一个网格对象对应到自行车的前轮,另一个对应到自行车的后轮。NiDataStream对象自身存储的网格顶点的位置和法线被共享,两个网格对象都保存表现自己网格的转换。
注意以下,Gamebryo不为任何类型的NiAVObject提供多父亲的功能。绑定一个已经拥有父节点的对象C,会导致C自动脱离原来的父节点
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要比每帧积累属性快的多,但是对于程序员。将多出一个额外的小负担。
软粒子主要是为了解决粒子广告牌和场景几何相交时,产生的生硬边缘,如下图烟雾与地面相交时的边
为了解决上面的情况,我们需要用到场景的深度信息,如下图:
在一般的渲染管线中,点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的一个输入
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