dx9实现Geometry Instancing

| January 31st, 2011

为解决引擎批次比较高的问题,通常一个有效的方法是将相同性质的物体顶点组织到一个批次中,一次性渲染;这个方法被称作Geometry Instancing(几何实例化)。

在非固定管线支持2.0以上的显卡上,dx可以硬件支持该技术,具体的实现流程如下:
1. 为物体实例定义属性结构InstanceAttribute(大概包括:位置、法线、颜色等),假设一个批次绘制N个物体实例,则需要申请N*sizeof(InstanceAttribute)个byte的顶点缓冲。
2. 为单个物体实例分配顶点缓冲,这个跟普通的绘制没有区别,即根据顶点格式定义,申请相应大小的顶点缓冲就行了。
3. 在每帧渲染的时候,需要向显卡送入两个顶点流,API调用顺序如下:
SetStreamSourceFreq(0, …, INDEXEDATA|InstNum)
SetStreamSourceFreq(1, …, INSTANCEDATA|1)

SetStreamSource(0, …..)
SetIndices(…)
SetStreamSource(1, ….)

pEffect->SetTechnique(…)
pEffect->SetMatrix(…)

pEffect->Begin()
pEffect->BeginPass()
pEffect->DrawPrimitive(…)
pEffect->EndPass()
pEffect->End()

在实际应用中,对于场景中大面积的静态物体(比如:灌木、草体)等,如果将分散的很开的物体实例加入到一个批次中,则在裁剪的时候会出现一大块草皮突然出现,又突然消失的问题;解决这个问题的方法可以将大块的草皮根据物理分布分割成多个小块,并根据小块中的每个实例计算出小块的质心。

json格式以简单著称,用大括号、中括号、逗号及任意嵌套就完备表示了任意数据结构,在人类阅读和机器阅读之间权衡的比较好。

json官网给出了各种语言(基本我所知道的语言都有,我不知道的语言也有)的实现方案,推荐c++平台上的两款实现:JSONCPP和libjson,前者简单易用,后者性能极高。

libjson导出的数据有三种表现方式:binary、unformated-text、formated-text;一般用unformated-text查看即可,还可以放到一些json在线格式化的webtool中高亮显示。

————-分割线———————

接上回说Max导出插件,一般实现max导出插件有三类做法:
1。传统max sdk:很多引擎都是这么搞的
2。max script:可以实时调试,不用重启max,但是要去抱着max reference成天看语法,没几人有这个耐心
3。IGame接口:接口相对于max sdk更加清晰,我推荐的就是这个

3dsmax导出插件的使命

| December 14th, 2010

一个max导出插件要做的事情(无论是maxscript还是maxsdk实现):
1。导出基本的网格信息(顶点位置、法线、纹理坐标)、三角形信息
2。导出基本的材质信息,包括标准的材质(diffuse,specular,emissive,ambient),纹理贴图信息(diffuse map\opacity map\bump map等),uv变换矩阵、渲染状态(如 two-side等)等
3。导出骨骼信息(cs bone和skin bone)
4。导出 cs蒙皮和skin蒙皮并导出cs和skin的骨骼动画
5。导出基于max 节点的关键帧transform动画(translate,rotation,scale)
6。导出基本的材质颜色动画、alpha动画和uv变换动画
7。导出mesh的morph动画
8。支持自定义优化器对导出数据进行优化、冗余数据检测剔除
9。支持自定义的目标文件格式输出器,支持输出成不同的文件格式

场景中的树

| December 11th, 2010

层次结构树
场景中比较典型的层次结构:
1。一个巨大的无限延伸的地面向远方延伸(层层叠叠的山岚、一望无际的大海、汗如穹庐的天空、不时出现的飞鸟走兽等等)
2。一个很帅的英雄背着一把大刀站在缓缓向前的船头很装逼的凝视着远方,可能身上还加了个buff
3。这个英雄觉得可能有点冷,就在甲板上来来回回的跑或者干脆跳过来跳过去
4。英雄突然觉得这船好像有点慢,然后就从后舱唤出一只大鸟,骑到背上腾空而起

这里涉及到的是场景管理中所谓的层次结构关系树,把场景中所有的对象都抽象成节点,然后按父子关系组成如下的结构:

节点挂接到父节点上之后,可指定其跟随父节点矩阵变化的方式:
1。跟随旋转和平移
2。只跟随旋转
3。只跟随平移
每次节点坐标系发生变化时要遍历以其自身作为根节点的子树上的所有节点,按照跟随方式更新其坐标系。

如果在是可以运动的节点(比如主角、载具),指定其运动坐标系是否相对于父节点,则可实现:英雄在移动的船上跑来跑去 这类具有多重相对运动的关系。

将摄像机也作为场景节点添加到不同的节点上,可以很方便的继承节点坐标系,获取不同的观察视角。

————分割线—————–
裁剪树
就是裁剪树了

————分割线—————–
渲染状态树
这个要不要组织成树结构,现在还不确定。按我的想法,将最耗时的渲染状态项放在最顶端,按照其取值分出n个分支,再接上次耗时的渲染状态项,由此组织成一个树,树的根节点上放上要渲染的节点,渲染时按照顺序遍历的方法,即可实现最少切换渲染状态,但是跟大牛聊了聊,觉得这种组织方式本身会比较耗时(整个渲染状态树最坏情况下回比较庞大),不过试了才能知道。

树的每条边表示一个状态值,沿着根节点从顶到底一次遍历就构成了一个完整的渲染状态,但从图上可以发现这种构造方法实际上状态冗余相当厉害。

实际实现上:我想直接将每个完整的渲染状态看做一个多维向量,对多维向量进行排序时可以从耗时最多到最少的依次指定排序key,并对那些相对不那么重要的key以后的状态不做排序,既可以达到比较理想的材质排序效果。

d3d多渲染窗口结构

| November 28th, 2010

使用d3d的交换链可以方便的实现多渲染窗口结构,类的层次结构如下图所示:

D3DRes用来定义dx设备丢失的处理,所有需要关心设备丢失的资源都从此类继承,然后重写设备丢失回调函数和设备重置回调函数即可方便的处理设备丢失。

RenderTarget用来定义基本的渲染目标,成员包括长宽及大小的定义,及渲染目标的虚接口定义(比如:缓冲区切换等)。

Win32Window封装win32接口,传入配置参数创建并注册windows窗口,并定义消息处理回调,利用监听器或者事件机制向外部应用层传递鼠标键盘操作。

RenderWindow从D3DRes及RenderTarget继承,并聚合Win32Window对象,表示一个可渲染的窗口。在RenderWindow创建的时候,根据外部传入的D3D对象(IDirect3D9)创建D3D Device;有了D3D Device,对于主窗口可通过接口函数GetSwapChain获取Device的SwapChain对象,代码如下:

IDirect3DSwapChain9* pSwapChain = 0;
if( d3d_device)
{
d3d_device->GetSwapChain(0, &pSwapChain);
}

对于非主窗口,则需要利用Device的接口CreateAdditionalSwapChain来创建更多的交换链对象,此接口需要硬件支持,因此创建之前需要检查硬件设备能力。

准备好RenderWindow的交换链对象后,在渲染帧即可调用Present接口驱动硬件渲染:

void RenderWindow::SwapBuffers()
{
LPDIRECT3DDEVICE9 d3d_device = Graphics::Inst()->GetD3DDevice();
if(d3d_device)
{
if (D3DERR_DEVICELOST == mSwapChain->Present(NULL, NULL, mhWnd, NULL, 0))
{
this->ResetDevice();
}
}
}

———-分割线—————

应用层通过创建并管理一个RenderWindow的数组,并在渲染主循环中依次切换渲染,即可实现多渲染窗口结构。

脚本接口层

| November 24th, 2010

1. 脚本接口层的执行流程

@.打开lua虚拟机

lua_State* pL = lua_open();
luaL_openlibs( pL );

@.注册基本的C函数,用于加载或者执行lua脚本文件(从包里读取)
luajit 1.1.5版本提供接口函数: lua_setfilefun(openfun, seekfun, readfun, closefun, tellfun, sizefun), 参数为函数指针,用户即可自定义函数操作函数(为毛要自定义呢?因为一般情况下,发布出去的版本会将脚本也打包到包文件中,因此读取脚本文件时就不是标准的文件流操作了)。

lua_setfilefun( (openfun)Open, (seekfun)Seek, (readfun)Read, (closefun)Close, (tellfun)Tell, (sizefun)Size );

@.初始化luajit
@.设置错误处理

@.注册脚本接口

@.执行启动脚本,开始游戏
@.结束游戏,关闭虚拟机

2. lua脚本层的环境设置
设置require搜索路径:可以通过修改package.path表实现,package.path的值形式如下:
“./script/?.lua;./main/?.lua;./common/?.lua;./script/?;./main/?;./common/?;?.lua;”

搜索顺序从前之后,并用require的参数替换?,参数中的”.”会被替换为”/”

lua的require机制详细内容参见这里。

3. LuaJit

4. LuaBridge

5. 引擎回调lua脚本的实现
引擎某些接口逻辑改动频繁,一般会将回调函数定义在脚本中,即定义脚本回调函数,形如:

void CModel::OnPosChange(x, y)
{
      CALLTOLUA_V_2(OnPosChange, x, y);

      //....(其他引擎逻辑)
}

这里的CALLTOLUA_V_2是带参数的宏定义:

#define CALLTOLUA_V_1(FUNCNAME,a1)\
        if( calltolua_v_1(this,#FUNCNAME,a1) ) return;

其中calltolua_v_1函数将当前调用环境的this指针,以及回调函数名及附带参数,作为参数;根据this指针取出lua虚拟机中该对象对应的lightuserdata,并以回调函数名为key取出脚本回调函数,然后利用luabridge::tdstack::push将附带参数压入脚本堆栈,实现脚本回调,calltolua_v_1函数形如:

template<typename T1>
bool calltolua_v_1(CBase* obj, const char* func, T1 t1)
{
        //宏定义:根据obj和函数名func取出脚本对象lightuserdata和脚本回调函数压入堆栈
        GETOBJANDFUNC(obj,func);//obj,func

        //倒一下堆栈顺序
        lua_insert(L,-2); //func, obj
        //压入模板参数(压入的同时检查参数类型)
        luabridge::tdstack<T1>::push(L,t1);
        //调用回调函数,errfunc为错误处理函数
        lua_pcall(L,2,0,errfunc);
        //回复栈内容到回调之前的状态
        lua_settop(L,top);

        return true;
};

根据不同的T1实例化形如:

template bool calltolua_v_1<int>(CBase* obj, const char* func, int t1);

其中luabridge::tdstack::push压入附带参数到脚本时,形如:

template <typename T>
struct luabridge::tdstack
{
        static void push (lua_State *L, T data)
        {
                lua_pushinteger(L, data);
        }

        static T get (lua_State *L, int index)
        {
                return (T)luaL_checkint(L,index);
        }
};

6. lua脚本调试器的实现

d3d渲染状态

| October 17th, 2010

在渲染循环中,在渲染物体之前需要设置好dx的渲染环境,其中调用的最频繁的api莫过于用于切换渲染状态的函数:IDirect3DDevice::SetRenderState().

SetRenderState函数用于设置各种不同类型的渲染状态,其功能用法汇总于下:

0、填充模式
SetRenderState(D3DRS_FILLMODE, …);
可设置的模式有:D3DFILL_POINT,D3DFILL_WIREFRAME,D3DFILL_SOLID,分别为点模式、线框模式及实填充模式。

1、着色模式
SetRenderState(D3DRS_SHADEMODE, …);
可设置的着色模式为:D3DSHADE_GOURAND(默认),D3DSHADE_FLAT,D3DSHADE_PHONG,分别为:高洛德着色、平面着色和PONG模型着色。其中平面着色最简单,使用三角形的第一个顶点颜色进行着色;高洛德着色则利用三角形面片的三个顶点的颜色进行线性插值来对面上像素进行着色,又成为双线性亮度插值着色;PHONG着色则对利用三个顶点的法向量进行插值,因此又被称作双线性法向量插值着色,相比于高洛德着色,其更加逼真,但计算更复杂。

2、CullFace相关
SetRenderState(D3DRS_CULLMODE, …);
可设置的Cull模式为:D3DCULL_NONE, D3DCULL_CW, D3DCULL_CCW, 分别为:不剔除背面,按顺时针方向确定背面,按逆时针方向确定背面;如果绘制时按顺时针组织网格,则应使用逆时针剔除;否则,则使用顺时针剔除。

3、ZBuffer相关
SetRenderState(D3DRS_ZENABLE, …);
可设置的值有:D3DZB_FALSE, D3DZB_TRUE, D3DZB_USEW, 分别表示禁用深度缓冲、启用Z-深度缓冲,启用W-深度缓冲。
所谓深度缓冲,是一个DirectDraw表面,保存着D3D使用的深度信息;当一个场景进入光栅,并且深度缓冲启用时,渲染表面上的每个点都会被检查;深度缓冲中的值作为点的z坐标值或者等同的w坐标值;使用z值的缓冲叫ZBuffer,使用w值的就交WBuffer;ZBuffer几乎被所有的硬件支持,是最常用的深度缓冲;然而z-buffers也有它本身的缺陷。由于它所使用的数学方法,使得一个z-buffer中产生的z值在它允许的范围内(通常为0.0到1.0之间,包括它们)并不是均匀分布的。特别是靠近剪切面与远离剪切面处的比例,更是影响了z值的均匀分布。相对于ZBuffer, WBuffer能在远近裁剪面之间进行更精确的分配,但是wbuffer却不被硬件厂商广泛支持。

SetRenderState(D3DRS_ZFUNC, …);
设置深度缓冲比较函数,可设置的值有:D3DCMP_LESSEQUAL(默认),D3DCMP_GREATER 等等,在DX文档中的枚举定义为:

typedef enum D3DCMPFUNC
        {
                D3DCMP_NEVER = 1,
                D3DCMP_LESS = 2,
                D3DCMP_EQUAL = 3,
                D3DCMP_LESSEQUAL = 4,
                D3DCMP_GREATER = 5,
                D3DCMP_NOTEQUAL = 6,
                D3DCMP_GREATEREQUAL = 7,
                D3DCMP_ALWAYS = 8,
                D3DCMP_FORCE_DWORD = 0x7fffffff,
        } D3DCMPFUNC, *LPD3DCMPFUNC;

默认值D3DCMP_LESSEQUAL表示如果被测试点的z值小于等于z缓冲中的深度值,则通过;否则拒绝。而D3DCMP_NEVER则表示永远拒绝被测试的像素点;D3DCMP_ALWAYS则表示永远通过被测试点的测试。

SetRenderState(D3DRS_ZWRITEENABLE, …);
设置深度缓冲是否可以写,可设置的值为:TRUE 和 FALSE。

4、StencilBuffer相关
5、Blend相关
6、雾相关
7、光照相关

渲染的基本流程

| October 17th, 2010

在渲染函数RenderFrame中做了那些事情呢?其基本流程莫过于下面伪代码所示:

//调用SetRenderState接口设置好渲染状态,诸如:光照、雾、贴图等等。。
SetRenderState(...);
SetRenderState(...);
...
SetRenderState(...);

Clear();                //清除(可以通过参数指定清除内容,包括:颜色缓冲、ZBuffer、StencilBuffer)

SetMatrix();            //世界变换矩阵(WORLD),镜头变换矩阵(VIEW),投影矩阵(PROJECTION)

BeginScene();           //告诉DX开始绘制
SetFVF();               //设置数据格式
DrawPrimitive..();      //调用DP命令
EndScene();             //告诉Dx结束绘制

Present();              //提交绘制

渲染流程中设置渲染状态属于比较耗时的操作,实际引擎应该尽量减少切换状态的次数;DrawPrimitive和DrawPrimitiveUp函数被称作DP调用,每次DP调用形成一个批次(Batch),现行的图形设备都不能承受太高的批次,因此实际引擎也应该尽量减少渲染批次

游戏主循环

| October 16th, 2010

最基本的游戏流程如下:

CreateWindow;  //创建windows窗口,用于显示及接受鼠标键盘消息
InitGraphicDevice;  //初始化图形设备
while(!bQuit)
{
        ProcessLogicFrame;      //处理逻辑心跳
        RenderFrame;            //处理渲染
};
ReleaseGraphicDevice;   //释放图形设备

中间的while循环就是基本的游戏循环了,在实际引擎的循环中当然可以加入诸如:性能统计、Log等之类的其他代码

在VS2005中不能使用VS2003的远程调试程序。VS2005远程调试的方法:
1. 远程端:安装VS2005光盘”X:\vs\Remote Debugger\x86\ rdbgsetup.exe”。或者直接运行或copy本地端的: “Program Files\Microsoft Visual Studio 8\Common7\IDE\Remote Debugger\x86″
2. 远程端:“本地安全策略 - 安全选项 - 网络访问:本地帐户的共享和安全模式”改为:经典-本地用户以自己的身份验证。
3. 远程端:启动Remote Debuger,从“工具 - 选项”中将身份验证模式改为“无身份验证,允许任何用户进行调试”。
或者直接用命令行:”X:\ msvsmon.exe” /noauth /anyuser /nosecuritywarn,可以建个快捷方式以方便运行。
4. 本地:在VS2005中,“工具”--“附加到进程”,传输选“远程”,限定符输入远程端的主机名或IP地址,回车,终于出来了可爱的进程列表。