2010年7月9日
纵观游戏服务器技术框架, 以运行在Linux操作系统上的居多, 为什么要运行在Linux上呢? 相信很多人会给出肯定的答案, Linux操作系统是开源的,而且是免费的,那叫做专业!是的,不错,是专业。但有一个问题让我们值得思考的是,虽然是免费的,但Linux工具比较贫乏,在某种程度上大大加在了开发成本及后期调试时间,所以我们不得不研究出一种代码可以运行在Linux,但又要节省开发成本,提高生产效率的可行性方案。
在Linux上,古老的Linux C程序员一般会采用一个文本编辑工具,比如:VI等,由于我以前是在windows下生活的,对VS.net这一类的工具颇为喜爱,它的开发方便快捷,又有开发辅助插件的支持,使你可以尽情使用智能提示,快捷键,代码模板等高效生产代码。可以说编码更多的是依靠可视化工具查错,快速跳转代码等。反正我的意思就是开发相当愉悦。但Linux下的C程序员则不然,我也曾经问过他们为什么要采用VI等编辑工具来编代码,首先,他们给到我的第一答案是"专业", 其实我也不太懂他们说的专业是什么,是传统意义上的,还是习惯性必须的;然后他们说:“使用makefile文件来编译代码很爽,速度很快”,这一点我是赞同的,但有个问题是程序员的入门门槛比较高,要求大家都要会这东西,还是麻烦,从HR招聘处可以看到,本来投C++程序员的人就比较少了,因为C++应用的领域基础是应用软件,游戏啊,电信啊,还是一些MFC界面性的东西,而且要求功底比较好,确实有时候招人还是挺难的,更不用说还要有Linux开发经验的人了。最后,我问了linux开发不员说那你们的调试用什么工具,他们说:“gdb”, 又是一个"手打牛肉丸",自我感觉很痛苦,心里想“为什么有那么先进的工具,比方说:Eclipse CDT, 你们不用呢?”,其实他们心中也是有答案的,“编码要养成一种好习惯,而不是依靠于某种工具”,显然Linux程序员在编码上大体要比windows上的程序员来得严格,也感觉到他们的代码比较有质量。但很显然的,由于开发环境的布署比较麻烦,对于大规模生产是否能够每个人都有那么高素质的编程能力,那就是个"谜"!------ “猜不准!”
针对上面的情况,我也自己分析了一下,软件开发的三大要素是什么,成本,质量,进度, 只有这三项东西控制得好,那才能控制好项目。
那软件编程的基础是什么? 当然是调用操作系统的API了,很显然的, 不同操作系统有不同的API,除非你有一个跨平台的开发框架,或者叫类库也行。
接下来软件架构在不同领域是否通用?比方说:通信框架,很显然的,还是有区别的,比方说电信系统与游戏系统,那显然还是不一样的。
最后,软件编码与接口(API)是不是应该更多人常用的,而且容易上手的(友好第一!),这样才可以减少开发成本及协调工作。
总结一下,我心中已有答案了,必须采用一种大多数程序员可以接受的,而且是他们熟悉的(不要DIY的),而且开发速度快速的开发方式那才是真道理。
以下是我的基本方案:
一、 跨平台框架的基础设施 (组件图)
1. MySQL数据库操作组件
2. 线程池 及 读写锁
3. 基础数据类型,容器,内存池,环形缓冲区
4. IOCP及Epoll跨平台的面向对象通信框架
5. 集成LuaTinker脚本交互模块等
二、 开发方式与调试环境
本框架的初步设想是前期在Windows下使用VS.net 2008进行开发调试,争取在Windows下解决80%左右的逻辑错误。之后由主程序员把代码移植到Linux,并使用Eclipse CDT可视化开发环境进行后期的调试工作。这样也大大降低招聘人员的知识要求(不用懂Linux),在某一程度降低了人员成本和加快了开发效率。
1. LoginApp
即登录服务器, 它主要完成玩家帐号的验证, 同时它通过BaseAppMgr并向玩家发送一个SessionKey作为基础服务器(BaseApp)的登录密钥;同时LoginServer还向玩家发送服务器列表信息。
2. BaseApp
即基础服务器, 也称连接服务器,它还维持着一个客户端连接列表(用户列表), 这样它可以实现区域消息广播及通过BaseAppMgr实现世界聊天, 玩家信息查看等功能;它还负责消息的分发到CellApp进行处理,并把结果返回到客户端。
3. CellApp
即游戏服务器,它负责世界数据的加载,游戏逻辑的处理及世界对象的管理。在本架构中当为支线服务器。
4. DBMgr
用户服务器,它负责用户相关数据的存取。一般是用户登录选择角色后就获得角色所有相关数据给到MapServer, 并由MapServer定时保存角色的相关数据。
5. BaseAppMgr
基础服务器管理器,主要负责分配基础服务器给到客户端连接,同时它采用某种策略可以实现用户的均衡负载等。
6. CellAppMgr
支线服务器管理器, 它主要根据支线ID为基础代理对象分配支线服务器实体, 这样就可以实现与客户端的通信了。
7. DB
数据库服务器主要分为三个库来存取,AccountDB为玩家账户信息,CharacterDB为玩家角色相关信息,WorldDB为所胡的世界数据。
2009年12月15日
1. 调试快捷键
2. 编辑快捷键
3. 代码快捷键
4. 窗口快捷键
Ctrl+Tab 切换编辑主窗口中的代码选项页
Esc键关闭当前的非模式窗口 (比方说 查找窗口, 切误用 Alt + F4, 这样会连VS.net2005都关掉)
F5: 启动调试
Ctrl+F5: 开始执行(不调试)
Shift+F5: 停止调试
Ctrl+Shift+F5: 重启调试
F7: 生成解决方案
Ctrl+U: 生成当前项目
F11: 独句调试
F10: 过程调试
F9: 打断点 或 取消断点
Alt+F9: 显示断点窗口
F12: 定位到函数的实现体
???: 定位到函数的声明, 只能用右键--->选择A
Shift+Alt+Enter: 切换全屏编辑
Ctrl+F: 查找
Ctrl+Shift+F: 在文件中查找
F3: 查找下一个
Shift+F3: 查找上一个
Ctrl+H: 替换
Ctrl+Shift+H: 在文件中替换
Ctrl+左右箭头键: 一次可以移动一个单词
Ctrl+上下箭头键: 滚动代码屏幕,但不移动光标位置。
Ctrl+L: 删除当前行
Ctrl+M,M: 隐藏或展开当前嵌套的折叠状态
Ctrl+M,L: 将所有过程设置为相同的隐藏或展开状态 或者 右键 + L + O
Ctrl+M,P: 停止大纲显示 或者再按一次Ctrl+M,L 或者 右键 + L + O
Ctrl+G: 转到指定行
Shift+Alt+箭头键: 选择矩形文本
Alt+鼠标左按钮: 选择矩形文本
Ctrl+Shift+U: 全部变为大写
Ctrl+U: 全部变为小写
//有点难度的:
Ctrl+K, Ctrl+C: 注释
Ctrl+K, Ctrl+U: 解除注释
Ctrl+Shift+A: 添加新项
//----------------------------------------
VAssistX: 操作
1. 定义标准的代码段, 并设置快捷键
2. Alt+M, 再打上函数名的前几个字母, 就会过滤掉很多函数, 最后按Enter定位到当前文件所查找的函数体
3. Shift+Alt+O, 查找文件, 输入文件名的前几个字母, 就会过滤掉很多文件, 最后按Enter定位到当前文件所查找的文件
4. Alt+O: 切换到.h和.cpp文件
5. 右键+查看所有引用 或 VassistX菜单中的Find References, Find References in Files : 查看函数引用的所有地方
6. Shift+Alt+S: 查找全局变量
//----------------------------------------
F7: 查看代码
Shift+F7: 查看窗体设计器
Windows键+E 打开资源管理器。
Windows键+D 显示桌面。
Windows键+M 最小化所有被打开的窗口。
Alt+Tab 切换任务栏中的激活窗口
2009年12月11日
SPXG // 标识
// 用到5张图片:
{
blendTexture = 0
layer0Texture = 1
layer1Texture = 2
layer2Texture = 3
layer3Texture = 4
}
!!ARBfp1.0
// 定义常量:
PARAM c[1] = { { 1, 0.30000001, 0.69999999 } };
// 声明3个寄存器:
TEMP R0;
TEMP R1;
TEMP R2;
// 开始混合:
// 一.第0层和第1层使用第4张图的x通道作为alpha进行混合:
TEX R1, fragment.texcoord[0], texture[0], 2D;
TEX R0, fragment.texcoord[1], texture[1], 2D;
ADD R2, R0, -R1;
// r2=Tex1-Tex0
TEX R0, fragment.texcoord[4], texture[4], 2D;
MAD R2, R0.x, R2, R1;
// r2 = Tex4.x*r2+Tex0
// 说明:
// 其中的Tex4.x是对应第1层alpha值, 下面把Tex4.x当a1看
// 即 r2 = a1 * (Tex1-Tex0) + Tex0
// 转换一下即是: Tex0*(1-a1)+a1*Tex1, 呵呵,看到了吧,这就是混合公式!
// 二.第2层和前面结果使用第4张图的y通道作为alpha进行混合:
TEX R1, fragment.texcoord[2], texture[2], 2D;
ADD R1, R1, -R2;
// r1=Tex2-r2
MAD R2, R0.y, R1, R2;
// r2 = Tex4.y*r1+r2, 即 a2*r1+r2 = a2*(Tex2-r2)+r2, 即r2*(1-a2)+Tex2*a2, 其中r2即是上次0和1层混合后的结果
// 三.第3层和前面结果使用第4张图的z通道作为alpha进行混合:
TEX R1, fragment.texcoord[3], texture[3], 2D;
ADD R1, R1, -R2;
// r1=Tex3-r2
MAD R1, R0.z, R1, R2;
// r1=a3*r1+r2 , 即 a3*r1+r2 = a3*(Tex3-r2)+r2, 即r2*(1-a3)+Tex3*a3
// 这样r1就保存了最终的混合结果
// 四.让阴影地表光泽系数为0(Tex4.w即a通道代表地形的阴影,0为阴影,1为正常.而每层贴图中的a通道是光泽通道,所以R1.w保存的是最终的光泽通道值):
MUL R0.x, R1.w, R0.w;
// r0.x = r1.w(光泽)*Tex4.w(阴影值, 是阴影则=0,否则=1)
// 计算削减系数?
MAD R0.w, R0, c[0].y, c[0].z; // r0乘上0.3(削减了30%)再加上一个常量0.69999999
// 反射高光 = secondary_color(反射光)*光泽:
MUL R0.xyz, R0.x, fragment.color.secondary;
// 贴图最终color = 最终贴图混出的color*削减系数:
MUL R1.xyz, R1, R0.w;
// 贴图最终color * primary_color(光照色或是顶点色) + 反射高光:
MAD result.color.xyz, R1, fragment.color.primary, R0;
// alpha:
MOV result.color.w, c[0].x;
END
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/flipcode/archive/2008/03/03/2143452.aspx
2009年12月5日
一直都没勇气去写一个游戏引擎,也许是太懒了,也许是太大了。虽然写了些零零碎碎的Test Case! 但你知道的,这始终不成气候!
最近我被安排到一个小组中,做的项目是一个体感游戏,之前考虑过用ogre开源引擎来做,可惜我对这东西又不熟的,听人家说还要用第三方类库CEGUI, 还要配置一些东西,一听头都大了,光学习理想都用半个月了,我们的工程只给两个月的时间,唉呀!算了吧,自己不是还有些乱七八糟的代码可用吗?狠下决心,决定干它一把!哪怕辛苦一点,做下来一个游戏引擎以后就容易干事了。
经过和另外一个同事两个月的时间奋斗,一路中虽说遇到些困难,也常加班的,项目终于也做完了。
一个游戏引擎原型基本蛋生了,我们命名它为 RedLight,它基本实现了
(0) Win32程序渲染框架
(1) UI的基本消息交互流程,XML窗体配置管理,UI皮肤配置管理,基本的UI控件库,多分辩率无缝UI拼图
(2) 室外场景管理
(3) 摄相机路径摄像
(4) 3D Max8模型及骨骼动画导出插件
(5) 模型渲染, 关键帧动画及骨骼动画控制
(6) 基本的水面反射效果
(7) 简单的面粒子系统
(8) 声音控制接口
这是一个单机游戏引擎的原型,功能有限,但它总算不辱使命完成了一个项目了,以后再扩展使它日益强大吧!
2009年12月1日
我喜欢玩WOW的UI风格,尤其是它的高清字体-------任务窗口的内容总感觉十分舒畅。这是从魔兽世界中任务窗口的游戏截图
经研究, 其实,上面的字体是用方正楷体(不是windows楷体)字体来做的, 用Freetype类库得到每个字符的对应图,并组装排版到一张预先创建好的空纹理中(256*256), 组成一张灰度图,这样可以防止重复字符贴图两遍,提高效率,就像活字印刷术一样灵活组合。
渲染的时候,采用字体颜色RGB + 灰度图合成最终效果。
也许你会发现,左图为什么这么清晰,右图好像稍差一点。对了,其实人的眼睛很奇怪,对颜色的识别是通过对比的,wow正是利用这一点,背景色采用以黄色为主色调,夹杂一些噪声,然后再渲上黑色的文字,这样可以给人感觉到很清晰。
其实wow中还是其他一些文字效果,比方说:阴影文字,原理是渲染两次同一字符,并在第二次遍渲染作偏移即可。当然,还有那种字体外边包起来的效果,不过这种我还没研究它的算法。
怎么说呢?一个游戏世界中,字体我觉得相当重要,丰富的字体表现会给人相当美满的效果,带给人美好的印象,所以还是相当重要的。我也在做我的文字配置系统,祝福我早日完成吧! 谢谢!
2009年11月14日
作为一个3D程序员, 我用了OpenGL两年多, 最近在搞一个项目, 从OpenGL转到D3D, 虽然工程外在的框架都封装得不错, 但想完全地从OpenGL转换到D3D, 看起来还是有难度的, 花了我两个星期的时间, 我终于转换过来了。
D3D与OpenGL的几点比较明显不同的地方:
(一)、正交投影时:OpenGL以屏幕左上角为(0,0), 而D3D却以屏幕中心为(0,0)
(二)、OpenGL使用右手坐标系, 而D3D使用左手坐标系
(三)、OpenGL使用旋转操作等转入的角度参数是 角度, 而D3D是 弧度,所以注意要PI * Angle / 180
下面我把具体地API对照关系列出来(不是很全,以后添加中.......)
1. 坐标变换
pos = D3DXVECTOR3(0,2,-1.5);
at = D3DXVECTOR3(0,0,0);
up = D3DXVECTOR3(0,1,0);
D3DXMatrixLookAtLH(&view,&pos,&at,&up);
pd3dDevice->SetTransform(D3DTS_VIEW,&view);
2. 绘制
pd3dDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_WIREFRAME);
DrawPrimitive()
DrawIndexedPrimitive()
DrawPrimitiveUP()
DrawIndexedPrimitiveUP()
3. 颜色
4. 片段测试
(1) 深度测试
g_pDevice->SetRenderState(D3DRS_ZENABLE, TRUE); //glEnable(GL_DEPTH_TEST);
g_pDevice->SetRenderState(D3DRS_ZFUNC, D3DCMP_LESSEQUAL); //glDepthFunc(GL_LEQUAL);
//--------------------------------------------------------------------------------------------------------
g_pDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW); //glEnable(GL_CULL_FACE);
(2) Alpha测试
//--------------------------------------------------------------------------------------------------------
g_pDevice->SetRenderState(D3DRS_ALPHATESTENABLE, TRUE); //glEnable(GL_ALPHA_TEST);
g_pDevice->SetRenderState(D3DRS_ALPHAFUNC, D3DCMP_GREATER); //glAlphaFunc(GL_GREATER, 0.1f);
g_pDevice->SetRenderState(D3DRS_ALPHAREF, 0.1 * 255); //取值范围 0 ~ 255
(3) 剪裁测试 (平面剪切)
//--------------------------------------------------------------------------------------------------------
// Enable clip plane for reflection map
CMatrix44f pWorldViewProjIT=m_pWorldViewProj;
//pWorldViewProjIT.Transpose();
pWorldViewProjIT.Invert();
// Transform plane to clip-space
float pClipSpacePlane[4];
float pClipPlane[]= { 0, 0, 1, 0};
// Check if camera is below water surface, if so invert clip plane
CVector3f pEye=(CVector3f)m_pCamera.GetPosition();
if(-pEye.m_fZ<0.0)
{
pClipPlane[2]=-pClipPlane[2];
}
MatrixTransformPlane(pClipSpacePlane, pClipPlane, pWorldViewProjIT);
// enable clip plane now
g_pDevice->SetClipPlane(0, pClipSpacePlane);
g_pDevice->SetRenderState(D3DRS_CLIPPLANEENABLE, 1);
(4) 模板测试
//--------------------------------------------------------------------------------------------------------
g_pDevice->SetRenderState(D3DRS_STENCILENABLE, TRUE);
g_pDevice->SetRenderState(D3DRS_STENCILFUNC, 3DCMP_ALWAYS);
g_pDevice->SetRenderState(D3DRS_STENCILREF, 0x1); //取值范围 0 ~ 255
Device->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_KEEP);
5. 纹理操作
g_pDevice->SetSamplerState( 0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
g_pDevice->SetSamplerState( 0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
g_pDevice->SetSamplerState( 0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);
g_pDevice->SetSamplerState( 0, D3DSAMP_ADDRESSU, D3DTADDRESS_CLAMP);
g_pDevice->SetSamplerState( 0, D3DSAMP_ADDRESSV, D3DTADDRESS_CLAMP);
6. 缓冲区操作
(1) 颜色缓冲
//--------------------------------------------------------------------------------------------------------
g_pDevice->SetRenderState(D3DRS_COLORWRITEENABLE, D3DCOLORWRITEENABLE_ALPHA);
g_pDevice->SetRenderState(D3DRS_COLORWRITEENABLE, 0x000000F);
(2) 深度缓冲
//--------------------------------------------------------------------------------------------------------
g_pDevice->SetRenderState(D3DRS_ZENABLE, TRUE); //glEnable(GL_DEPTH_TEST);
g_pDevice->SetRenderState(D3DRS_ZWRITEENABLE, TRUE); //glDepthMask(GL_TRUE);
(3) 模板缓冲
//--------------------------------------------------------------------------------------------------------
(4) 渲染到纹理
//--------------------------------------------------------------------------------------------------------
// Render targets
IDirect3DSurface9 *m_plD3DBackbufferSurf,
*m_plD3DDepthStencilSurfAA,
*m_plD3DDepthStencilSurf;
CRenderTarget *m_pRTRefraction, *m_pRTReflection; //(自定义纹理类)
//-----------------------------------------------------------------------------------
// Get backbuffer
g_pDevice->GetRenderTarget(0, &m_plD3DBackbufferSurf);
// Get depthstencil
g_pDevice->GetDepthStencilSurface(&m_plD3DDepthStencilSurfAA);
// Restore previous states
g_pDevice->SetRenderTarget(0, m_plD3DBackbufferSurf);
g_pDevice->SetDepthStencilSurface(m_plD3DDepthStencilSurfAA);
// (1)折射图--------------------------------------------------------------------------
//下面的语句调用了 g_pDevice->CreateRenderTarget(iWidth, iHeight, (D3DFORMAT) iFormat, (D3DMULTISAMPLE_TYPE)iAASamples, 0, 0, &m_plD3Surf, 0));
if(FAILED(m_pRTRefraction->Create(m_fWidth>>1, m_fHeight>>1, D3DFMT_A8R8G8B8)))
{
return APP_ERR_INITFAIL;
}
// Create depthstencil withouth multisampling
g_pDevice->CreateDepthStencilSurface(m_fWidth, m_fHeight, D3DFMT_D24X8, (D3DMULTISAMPLE_TYPE)0, 0, 0, &m_plD3DDepthStencilSurf, 0);
g_pDevice->SetRenderTarget(0, m_pRTReflection->GetSurface());
g_pDevice->StretchRect(m_plD3DBackbufferSurf, 0, m_pRTRefraction->GetSurface(), 0, D3DTEXF_NONE);
// (2)反射图-----------------------------------------------------------------------------------
m_pRTReflection=new CRenderTarget;
if(FAILED(m_pRTReflection->Create(m_fWidth>>2, m_fHeight>>2, D3DFMT_A8R8G8B8)))
{
return APP_ERR_INITFAIL;
}
g_pDevice->SetRenderTarget(0, m_pRTReflection->GetSurface());
//-----------------------------------------------------------------------------------
g_pDevice->SetRenderTarget(0, m_pRTReflection->GetSurface());
g_pDevice->SetDepthStencilSurface(m_plD3DDepthStencilSurf);
g_pDevice->Clear(0, 0, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_ARGB(255, 0, 0, 128), 1.0f, 0);
SetViewport(m_pRTReflection->GetWidth(), m_pRTReflection->GetHeight());
//-----------------------------------------------------------------------------------
D3DXSaveTextureToFile("imageTex.jpg",D3DXIFF_JPG,(IDirect3DTexture9*)m_pWavesBump->GetTexture(),NULL);
7. 混合操作
g_pDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE); //glDisable(GL_BLEND);
g_pDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); //glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
g_pDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
8. 灯光与材质
g_pDevice->SetRenderState(D3DRS_LIGHTING, FALSE); //glDisable(GL_LIGHTING);
D3DMATERIAL9 mtrl;
mtrl.Ambient = a;
mtrl.Diffuse = d;
mtrl.Specular = s;
mtrl.Emissive = e;
mtrl.Power = p;
Device->SetMaterial(&mtrl); //在设置纹理前设定
//设置当前使用的纹理
2009年9月25日
wmo(wow map object) research
The wmo是一个非常有趣的设计,wow中比较小的物体使用doodad,而building使用wmo,
这里的building可以是桥梁、了望台、简单的小房子、复杂点的旅馆这样的房屋、非常
复杂的建筑群(例如地下城场景),本文对wmo场景文件进行简单的介绍,关于wmo文件的具
体信息请参考wowmapview的source code,这里非常感谢ufoz所做的贡献。
1、命名规则
wmo保存在以.wmo结尾的文件中,这个文件使用数据块来保存数据。一个wmo通常由一个或多个
group组成,而group数据也保存在以.wmo结尾的文件中,不过文件名称存在不同,例如一个wmo
保存在name.wmo文件中,那么group文件名就为name_001.wmo name_002.wmo......
2、结构
wmo就组织结构来说包含两个层次,group和batch。一个group通常包含多个batch,其中group包含
一个AABB。batch是wmo最小的渲染单元,它保存了顶点索引列表,可以直接调用DP进行渲染。group
内保存一个标志位,可以将group分为indoor/outdoor两类,这一个信息非常重要,通过它wmo就将
building分成了内外两部分,outdoor group就是building的外壳,他通过portal与室内场景连接
在一起。
3、portal
wmo文件中保存了portal信息,在wmo中规定group必须通过portal进行连接,portal由PVS和PRS
两部分组成,PVS记录portal顶点信息,PRS记录portal和group的连接信息,PRS结构如下:
struct WMOPR {
int portal;
int group;
int dir;
};
需要注意的是dir,这个成员只有两个值-1或1,由于portal的顶点信息按照顺时针记录,因此group
位于portal的正面时dir为1,否则为-1,通过dir可以快速确定group到底位于portal的哪一侧。
通过wmo中记录的portal信息可以使用portal culling来检查group的可见性,但是这里还是有一些
难度,主要是指portal的记录方式。由于一个group可能有多个portal,而查找连接的portal只能
通过PRS,这样在大的场景中非常不方便。而且在wmo中竟然没有记录portal的plane信息,如何确定
camera到底是位于portal的正面还是反面呢?(现在wmo文件由于没有完全破解,存在一些wowmapview
未读入的数据块,例如MVER、MOPT、MOVV、MOVB等,其中MVER应该是wmo文件的版本号,MOPT怀疑是保
存所有plane信息,而MOVV可能是保存包围体顶点信息,而MOVB保存包围体信息,MOVV、MOVB应当用于
碰撞检测,这些暂时没有验证)我的做法是在载入时计算portal的plane信息,并将PRS信息转换为类似
Q3 BSP中portal的结构。
struct portal_t {
int othergroup;
int pvs;//pvs index
int dir;
};
struct group_t {
int firstportal;
int numportals;
};
4、碰撞检测
在wmo中并没有使用BSP、OC TREE这样的结构来进行场景管理,可能所有人都感觉非常困惑。
场景管理的功能主要是为了加速渲染和方便碰撞检测,由于存在portal,这样第一个功能已经完成。
而对于碰撞检测,我的想法应当是AABB TREE。仔细观察WOW的场景可以发现在indoor场景中曲面、斜面
这样的几何物体非常少,大多数是规则物体,因此可以判断在wmo中所有的物体都是严格按照轴对齐
方式进行建模,也就是对规则性物体AABB=OBB。由于MOVV和MOVB信息并没有完全研究透彻,因此关于
这一部分只能是我的猜测。
5、渲染
对wmo的渲染由于batch的存在从而变的简单化,但还有可以优化的地方。由于wmo中使用portal将其分割
成group,因此有大量的材质相同的model被分割成不同的batch,在渲染时将材质相同的batch合并到一起
渲染可以避免一些无谓的DP调用。wmo一个令人诟病的地方是使用vertex light,为了减少图元数量从而
使顶点数量降低,造成渲染的时候出现色带效果,应当加入lightmap,由于wmo的场景通常不大,预处理
时做radiosity的时间也不会太长。
6、动态载入
对于只包含几个group的小场景的wmo,由于载入时间不是太长,在动态载入时一次性载入对程序影响
并不会太大。但是对于超大场景的wmo就需要考虑载入策略,这样场景典型的就是wow中的地下城场景,
它一个wmo中包含了几百个group,一次性载入时间非常长,需要分段进行载入。此时就显示出wmo分
文件保存group的优势了,为了实现动态载入wmo场景,一种可能的做法是在载入wmo后需要根据camera
所在的group快速的建立group连接层次图,这个图通过PRS数据建立,建立流程如下:
一、将camera所在group作为当前group,获得所有相连的protal;
二、将protal连接的group保存到第一层列表中,遍历第一层列表中所有的group;
三、获得第一层列表中group的portal,检查portal所连接的group是否保存在第一层列表中,如果没有
将其保存到第二层列表中;
四、重复上述过程,直到整个层次图建立。
这个层次图可以预先建立然后保存到文件中运行时载入,这样wmo就是分层载入而不需要一次性载入。
(这里我考虑是否在wmo中也可以建立类似bsp的pvs数据呢?虽然pvs现在已经开始淘汰,但是如果
存在pvs就可以方便确定哪些group需要立即载入,只是不知被portal分割后的group到底是不是convex
hull,如果是的话可以建立pvs,但对建模时限制更加明显,两难的选择!!!)
7、建模
由于wmo是按照group对场景进行保存,因此为了建立wmo需要设计一个强力的模型构建工具,这个工具
主要功能就是对从建模工具(3DS MAX)中建立的场景模型进行分组和处理。美工在制作模型时需要非常
小心,所有的模型要严格的轴对齐(轴对齐的原因是需要模型的AABB=OBB),然后将模型导入工具中。
模型构建工具有以下功能:分组(group)功能、group选择、group显示/隐藏、指定portal,portal对
齐(考虑门、窗户这样天然的portal,手动指定portal时肯定无法与外表墙壁对齐,需要程序自动对齐)
、batch操作(分割、选择、显示/隐藏等)、图元级操作(triangle拣选,用于batch分割)、光照运算
(产生vertex light数据)、放置光源、放置doodad(场景中的道具,如桌子、椅子等)。可能还需要其
他一些功能,但是对比其他引擎的场景建模工具(hammer、sandbox)明显简单化许多。
8、优势及不足
当前处理室内场景的主流技术依然是bsp,但是随着硬件的发展bsp的优势在慢慢地丧失,bsp赖以生存的
预处理PVS现在已经完全被实时的portal culling所取代,bsp优势只剩下对图元排序(用于透明物体的
渲染)和基于brush的快速碰撞检测上,但是对比建模工具的复杂化和场景的限制,采用bsp的开销确实
显得太大。而基于纯portal引擎的结构开始流行,例如cryengine中处理室内场景时就完全抛弃bsp,
场景完全由一块块固定大小的墙壁组成,一块墙壁基本和bsp中brush类似,这样做的好处是建模工具变
的简单(不需要进行CSG运算),而且非常容易的产生portal,同时由于场景使用brush构成也兼具了
bsp方便进行碰撞检测的优势。wmo有些类似cryengine,但是在某些方面更具优势。
首先在建模方面,wmo的场景完全可以通过成熟的建模工具来构建,这样对于美工不需要重新学习新的
建模工具,可以节约大量的时间。其次模型构建工具需要的功能非常少,减少了程序的复杂性,缩短了
编写相关工具的时间。再次,场景管理简单化,相应代码量大幅度减少,同时由于portal的存在,可以方便
的与其他引擎相对接。可以说wmo是一种可以进行快速开发的场景结构。