原作者 : Kevin Lynx
BLOG:
http://blog.csdn.net/kevinlynx第一部分:
HGE helper类中的GUI:
引擎版本:1.60release。
文件:hgegui.h , hgegui.cpp, hgeguictrls.h , hgeguictrls.cpp
大致类图:
其中,hgeGUIObject是抽象基类,具体的控件类如按钮,文本标签,都是从它派生而来。HgeGUI属于整个GUI系统的manager,它会保存所有的控件。
关于hgeGUIObject的8个成员变量,文档里已经有所描述:
int id; //控件ID
bool bStatic; //是否可以接受键盘焦点
bool bVisible; //是否可见
bool bEnabled; //是否有效
hgeRect rect; //控件大小
hgeGUI *gui; //其属于的 manager ,相当于父对象
hgeGUIObject *next; //用于双向链表,把所有控件连接在一起
hgeGUIObject *prev;
static HGE *hge; //方便使用HGE接口
关于其部分接口的描述:
Render: 用于渲染控件到屏幕上
Update: 用于更新其动画
Enter: 控件刚显示时的动画
Leave: 控件要消失时的动画
IsDone: 控件刚显示和消失时的状态查询函数
该类也就是提供了一个抽象而已,利用C++语言的多态机制来方便hgeGUI管理所有的控件。其他具体的控件继承了hgeGUIObject后,必须实现构造函数和Render函数。
关于hgeGUI:
这个类应该属于manager。它负责管理所有的控件。
其数据成员:
hgeGUIObject *ctrls; //保存所有控件
hgeGUIObject *ctrlLock; //可能是用来保存当前被鼠标操作的控件
hgeGUIObject *ctrlFocus; //保存焦点控件
hgeGUIObject *ctrlOver; //用来保存鼠标指针所指的控件
int navmode;
int nEnterLeave;
hgeSprite *sprCursor; //渲染鼠标指针用的
float mx,my; //鼠标坐标
int nWheel; //滚轮偏移量
bool bLPressed, bLLastPressed;//本帧左键状态,上一帧的左键状态
bool bRPressed, bRLastPressed;
其Update方法会负责控件的进入和离开动画,还会负责整体的状态设置---哪些控件拥有焦点,哪些控件被鼠标正在操作,哪些控件正被鼠标指针指着,这些控件它都会保存起来。(也就是说,我们还是可以通过检查控件的状态来设置当鼠标指针在其上时的新动画。)
总体而言,该引擎的GUI还是很简单的。一个manager,负责管理所有的控件,然后一个抽象基类,用来协助manager管理---其他具体的控件都必须从那个抽象基类派生。
下面具体看一个Button控件:
Button类的定义如下:
class hgeGUIButton : public hgeGUIObject
{
public:
hgeGUIButton(int id, float x, float y, float w, float h, HTEXTURE tex, float tx, float ty);
virtual ~hgeGUIButton();
void SetMode(bool _bTrigger) { bTrigger=_bTrigger; }
void SetState(bool _bPressed) { bPressed=_bPressed; }
bool GetState() const { return bPressed; }
virtual void Render();
virtual bool MouseLButton(bool bDown);
private:
bool bTrigger;
bool bPressed;
bool bOldState;
hgeSprite *sprUp, *sprDown;
};
其中bTrigger表示该按钮的行为是否象一个RadioButton,bPressed表示当前按钮是否被按下,bOldState表示上一次按钮状态,特别用来实现bTrigger的,sprUp,sprDown分别用来绘制弹起和按下时的按钮外观。这两个精灵的创建都是从构造函数的tex上创建而来的,它要求两个状态必须保存在一幅纹理上,且顺序为从左至右。
按钮的实现代码也很简单:
void hgeGUIButton::Render()
{
if(bPressed) sprDown->Render(rect.x1, rect.y1);
else sprUp->Render(rect.x1, rect.y1);
}
bool hgeGUIButton::MouseLButton(bool bDown)
{
if(bDown)
{
bOldState=bPressed; bPressed=true;
return false;
}
else
{
if(bTrigger) bPressed=!bOldState;
else bPressed=false;
return true;
}
}
联系起来,当hgeGUI::Update里处理ProcessCtrl时,如果鼠标左键按下且其指针在按钮范围内,那么就调用hgeGUIButton::MouseLButton( true ),这个时候button的bPressed=true,那么在渲染的时候,自然就表现出被按下时的状态。
事实上对于这种类型的按钮---如同windows下的窗体按钮,我们一般不检查其是否被按下,而是检查其是否发生了clicked 这个事件,而这个事件是在先按下在弹起的情况下发生的。因此,判断该事件发生的条件就为:上一帧状态被按下,这一帧没被按下。
虽然HGE引擎的GUI很简单,但是其扩展性很好。因为hgeGUI::Update基本上派发了所有控件需要的消息---键盘操作,以及鼠标操作;而hgeGUIObject基类的很多成员函数都会处理这些消息,我们只需要派生hgeGUIObject,然后重载我们需要的消息处理即可。
第二部分:
HGE扩展GUI库,从HGE官方论坛下载(作者不明):
工程结构:
其中,guitest.cpp为测试文件。
类结构:
整个系统的工作原理:
用户继承抽象基类GUIApp,实现具体的OnEvent函数,然后该类会管理所有的GUIAppWindow对象,GUIAppWindow窗口对象会管理其上的所有子控件。
相应地,GUIApp派生类会直接得到鼠标和键盘消息,然后派发给所有窗口对象,然后窗口对象再把消息派发到具体的控件对象上。
这种Parent-Child关系大致为:
GUIAppWindow类保存有其所属的GUIApp对象指针,每个具体的控件又保存有其所属的GUIAppWindow 的对象指针。
当一个控件处理了某个事件后,例如按钮处理了鼠标单击事件,它就需要告诉外界用户单击了这个按钮。这里采用的方法是:在基类GUIAppObject里定义了一个虚函数OnEvent( int id)
,然后在其派生类GUIAppWindow里把这个函数重载为纯虚函数,函数有一个参数,那就是控件ID。当一个控件处理了某个事件后,就通过其内部保存的父窗口指针来调用OnEvent函数,然后GUIAppWindow的派生类---如果该类能产生对象,那么其必然实现了OnEvent的具体代码(这就是为什么在GUIAppWindow里要把OnEvent又重载为纯虚函数的原因),然后在此代码里,窗口根据传进来的控件ID来得知哪个控件发生了事件!
GUIAppWindow里有一个容器,它保存了所有该窗口上的控件。
所有控件再创建时,都是以其父窗口为参考坐标系的,也就是相对坐标,但是其实际保存的坐标却是绝对坐标—既相对于整个屏幕的坐标(如果是窗口程序,就相对于整个窗口)。大致过程为:在GUIAppWindow派生类中创建子控件时,给子控件指定的坐标为相对坐标,然后当 AddCtrl 时就会重新把子控件的坐标改变为绝对坐标。
GUIApp里直接有了BeginScene和EndScene的渲染代码。
要使用该扩展库,大致步骤为:
1. 继承GUIAppWindow类,在这个派生类里重载具体的处理OnEvent的函数,并创建所有该窗口上的子控件。
2. 继承GUIApp类,在这个派生类中创建窗口对象,并把窗口对象AddCtrl,在这里可以进行其他的初始化工作
3. 在FrameFunc里调用GUIApp::FrameFunc函数。
总体而言,这个扩展GUI主要是扩展了GUI Manager以及GUI Object,并且加入了Parent-Child机制。比较经典的部分在于提供了一个 OnEvent 函数,这样就可以让客户程序员能够得知窗体上的控件发生的事件。 ----其实这种方法的目的就跟Windows中的消息机制,Qt中的signal/slot机制一样。
近日很多朋友咨询Overlay中文显示问题,回答的多了想索性再写个文档算了,放在网上共享,于是就有了本篇。
在Ogre1.2.5版本中,通过与Ogre官方论坛的开发者讨论实现了Overlay的中文显示,当初的实现非常的怪异,具体的实现可以参见Ogre官方论坛。
随着Ogre的更新,现在Ogre已经发布了1.4.7,1.4系列版本有一个重要的改进,就是加入了UTFString,这为Ogre中文显示予以很大的帮助。为了便于演示,我直接使用Ogre自带的Overlay,也就是大家熟悉的DebugOverlay,测试工程我选择Demo_ParticleFX,选择其他的也没有关系。现在编译它,运行后得到下图:
图的最左下角显示的就是英文DebugOverlay,接下来我们的任务就是把它编程中文的,^_^。
Overlay中文化操作步骤如下
1. 打开OgreSDK\media\packs\ OgreCore.zip。
2. 打开C:\WINDOWS\Fonts,把simhei.ttf添加到OgreCore.zip,(什么,没有simhei.ttf这个文件,那就还其他的中文ttf字体吧)。
3. 打开OgreCore.zip中的Ogre.fontdef,里面有BlueHighway这个字体定义块,在他的下面添加我们的SimHei,code_points里面的一大堆数字看不明白没关系,随后文章会解释。
SimHei
{
type truetype
source simhei.ttf
size 16
resolution 96
code_points 33-166 24403-24403 21069-21069 24103-24103 36895-36895 29575-29575 24179-24179 22343-22343 26368-26368 39640-39640 20302-20302 19977-19977 35282-35282 24418-24418 25968-25968 37327-37327 25209-25209 27425-27425
}
4. 打开OgreCore.zip中的OgreDebugPanel.overlay,把BlueHighway全部替换成SimHei,我们要使用中文字体了,嘿嘿。
5. 修改完成后,确保所做的修改已经保存到OgreCore.zip。
6. 进入Ogre解决方案,打开文件ExampleFrameListener.h,把54-59行的代码替换如下:
static String currFps = "Current FPS: ";
static String avgFps = "Average FPS: ";
static String bestFps = "Best FPS: ";
static String worstFps = "Worst FPS: ";
static String tris = "Triangle Count: ";
static String batches = "Batch Count: ";
static DisplayString currFps = L"当前帧速率: ";
static DisplayString avgFps = L"平均帧速率: ";
static DisplayString bestFps = L"最高帧速率: ";
static DisplayString worstFps = L"最低帧速率: ";
static DisplayString tris = L"三角形数量: ";
static DisplayString batches = L"批次: ";
7. 最后重新编译工程,下面是我运行的截图,是不是已经显示中文了,^_^。
现在再来看看SimHei中的code_points是如何生成的,这个可以参考我上次写的这篇文章http://www.cnblogs.com/gogoplayer/archive/2008/05/09/1189795.html,至此,实现Overlay中文显示。
转载请注明出处:
作者:gogoplayer
E-mail : gogoplayer@163.com
QQ : 78939328
http://www.gogoplayer.com.cn
OGRE主要渲染流程简介
谢伟亮 feiyurainy@163.com
转载请注明出处
很早以前就想写一些关于OGRE的文章了,一直没机会。
理解一个渲染引擎,我觉得最重要的是先抓住了它的主架构,它的主线,渲染流程,不然的话,一个引擎几万行,甚至几十万行的代码,光是打开solution就能吓你一跳了,OGRE也有十几万行的代码量,我一开始看它的时候也是无从下手,感觉代码太多了,不知道从哪开始看好,这个class看看,那个class看看,由于对整个引擎没有一个清晰的认识,看过了也印象不深,所以,最后,还是决定先找出它的主线,了解它的渲染流程,这样才能有机地把各个部分联系起来。
这篇短文也是对OGRE的主要渲染流程的一个介绍,可能对一些class不会太多地去介绍具体的实现细节。我所用的代码都是取自于OGRE的最新的CVS版本。
读者最好对OGRE有一定的了解,至少得看懂它的example,不然可能一些东西理解起来比较困难。对D3D,OPENGL有一定了解更好。
如果你看过D3D SDK中带的例子,你一定知道一个比较简单的3D程序要运行起来,至少都会涉及以下的几部分:
首先是数据的来源,包括顶点数据,纹理数据等,这些数据可以从文件中读取,也可以在程序运行时生成。
接下来,我们会建立顶点缓冲区把顶点保存起来,建立texture对象来表示texture,对顶点组成的物体设置它在世界坐标系下的坐标,设置摄像机的位置,视点,设置viewport的位置和大小,然后就可以在渲染循环中开始调用渲染操作了,经过了front buffer和back buffer的交换,我们就能在屏幕上看到3D图形了,伪代码如下:
setupVertexBuffer
setWorldTransform
setCamera
setProjectionTransform
setViewport
beginFrame
setTexture
drawObject
endFrame
以下就是渲染一个物体的主要步骤,在我看来,这就是3D程序的主线,同样道理,无论你多复杂的渲染引擎,都得实现上述的这些步骤,其他的一些效果如阴影,光照等,都是附着在这条主线上的,所以,如果你能在你所研究的渲染引擎上也清晰地看到这条主线,可能对你深入地研究它会大有帮助,下面,我们就一起来找到OGRE中的这条主线。
OGRE的渲染循环都是起源于Root::renderOneFrame,这个函数在OGRE自带的example中是不会显式调用的,因为example都调用了Root::startRendering,由startRendering来调用renderOneFrame,如果你用OGRE来写真正的游戏,或者编辑器,你可能就需要在的消息主循环中调用renderOneFrame了,顾名思义,这个函数就是对整个OGRE进行一帧的更新,包括动画,渲染状态的改变,渲染api的调用等,在这个函数中,会包括了我们上述伪代码的几乎全部内容,所以是本文的重点所在。
进入renderOneFrame,可以看到头尾两个fire函数,这种函数在OGRE中经常出现,一般都是fire…start和fire…end一起出现的,在这些函数中,可能会处理一些用户自定义的操作,如_fireFrameStarted就会对所以的frameListener进行处理,这些fire函数可以暂时不用理会,继续看_updateAllRenderTargets,在这个函数中,会委派当前所用的renderer对所有创建出来的render target进行update,render target也就是渲染的目的地,一般会有两种,一种是render texture,一种是render buffer,接着进入RenderSystem::_updateAllRenderTargets,可以看到在render system中,对创建出来的render target是用RenderTargetPriorityMap来保存的,以便按照一定的顺序来对render target进行update,因为在渲染物体到render buffer时,一般会用到之前渲染好的render texture,所以render texture形式的render target需要在render buffer之前进行更新。
进入render target的update,可以看到,它仍然把update操作继续传递下去,调用所有挂在这个render target上的viewport的update。
Viewport其实就是定义了render target上的一块要进行更新的区域,所以一个render target是可以挂多个viewport的,以实现多人对战时分屏,或者是画中画等效果,可以把OGRE中的viewport看成是保存camera和rendertarget这两者的组合,把viewport中所定义的camera所看到的场景内容渲染到viewport所定义的render target的区域里。
Viewport还有一个重要信息是ZOrder,可以看到RenderTarget中的ViewportList带有一个比较函数,所以在RenderTarget::update中,ZOrder越小的,越先被渲染,所以,如果两个viewport所定义的区域互相重叠了,而且ZOrder又不一样,最终的效果就是ZOrder小的viewport的内容会被ZOrder大的viewport的内容所覆盖。
继续进入Viewport::update,就像前面所说,它调用它所引用的camera来渲染整个场景,而在Camera::_renderScene中,是调用SceneManager::_renderScene(Camera* camera, Viewport* vp, bool includeOverlays)。SceneManager::_renderScene里就是具体的渲染流程了。从函数名称还有参数也可以看出来,这个函数的作用就是利用所指定的camera和viewport,来把场景中的内容渲染到viewport所指定的render target的某块区域中。根据camera,我们可以定出view matrix,projection matrix,还可以进行视锥剔除,只渲染看得见的物体。注意,我们这里只看标准的SceneManager的方法,不看BspSceneManager派生类的方法,而且,我们会抛开跟主线无关的内容,如对shadow的setup,骨骼动画的播放,shader参数的传递等,因为我们只注重渲染的主流程。
在SceneManager::_renderScene中所应看的第一个重要函数是_updateSceneGraph,OGRE对场景的组织是通过节点树来组织的,一个节点,你可以看成是空间中的某些变换的组合,如位置,缩放,旋转等,这些变换,会作用到挂接在这些节点上的具体的物体的信息,也就是说,节点保存了world transform,对具体的物体,如一个人,在空间中的定位,都是通过操作节点来完成的。同时节点还保存了一个世界坐标的AABB,这个AABB能容纳所有它所挂接的物体的大小,主要是用于视锥裁减的,如果当前摄像机看不见某个节点的AABB,那么说明摄像机看不见节点所挂接的所有物体,所以在渲染时可以对这个节点视而不见。
_updateSceneGraph的内部处理比较繁琐,我们只需知道,经过了_updateSceneGraph,场景节点树中的每个节点都经过了更新,包括位置,缩放,和方位,还有节点的包围盒。
继续回到SceneManager::_renderScene,接下来要看的是setViewport,它会调用具体的renderer的setviewport的操作,设置viewport中所挂接的render target为当前所要渲染的目标,viewport中的区域为当前所要渲染的目标中的区域。
接下来要碰到OGRE渲染流程中的一个重要的概念,Render Queue。这个东西实在内容比较多,还是以后有机会单独提出来说吧,你可以简单把它想成是一个容器,里面的元素就是renderable,每个renderable可以看成是每次调用drawprimitive函数所渲染的物体,可以是一个模型,也可以是模型的一部分。在RenderQueue中,它会按材质来分组这些renderable,还会对renderable进行排序。
在每一次调用SceneManager::_renderScene时,都会调用SceneManager::prepareRenderQueue来清理RenderQueue,然后再调用SceneManager::__findVisibleObjects来把当前摄像机所能看见的物体都加入到RenderQueue中。
SceneManager::__findVisibleObjects是一个递归的处理过程,它从场景的根节点开始,先检查摄像机是否能看见这个节点的包围盒(包围盒在_updateSceneGraph时已经计算好了),如果看不见,那么这个节点,还有它的子节点都不用管了。如果能看见,再检测挂在这个节点上的所有MovableObject,如果当前所检测的MovableObject是可见的,就会调用它的_updateRenderQueue方法,一般在这个方法里就可以把和这个MovableObject相关的renderable送入RenderQueue了。
这里要说说MovableObject,MovableObject主要是用于表示场景中离散的物体,如Entity,顾名思义,能移动的物体,不过它的“能移动”这个能力是要通过SceneNode来实现的,所以MovableObject来能显示出来,首先得先挂接在某个场景节点上,通过场景节点来定位。你可以控制MovableObject的一些属性,如某个MovableObject是否要显示,是否要隐藏,都可以通过MovableObject::setVisible方法来实现。
检测完该节点上的MovableObject之后,就继续调用所有子节点的_findVisibleObjects方法,一直递归下去。这样,就能把场景中所有要渲染的renderable所加入到RenderQueue中了。
至此,我们就拥有了要渲染的物体的信息了,接下来就是对这些物体进行渲染了,你会发现跟D3D或OpenGL的代码很类似的调用:
mDestRenderSystem->clearFrameBuffer
mDestRenderSystem->_beginFrame
mDestRenderSystem->_setProjectionMatrix
mDestRenderSystem->_setViewMatrix
_renderVisibleObjects();
mDestRenderSystem->_endFrame();
这些api的作用和D3D中的类似调用的作用都差不多,这里再说一下_renderVisibleObjects(),在这个函数中,会对RenderQueue中的每个renderable进行渲染,用的是visitor模式来遍历操作每个renderable,最终在SceneManager::renderSingleObject中取出每个renderable所保存的顶点,索引,世界矩阵等信息,来进行渲染。这其中还包括了查找离该renderable最近的光源等操作,比较复杂。
到这里,SceneManager::_renderScene的流程基本走完了,也就是说,OGRE一帧中的渲染流程差不多也结束了,你应该也发现,这个流程跟你用D3D写一个简单程序的流程基本是一样的,在这个流程的基础上,再去看具体的实现,如怎么样设置纹理,怎么样调用你熟悉的D3D或OpenGL的API来渲染物体,应该会简单得多。
对OGRE的渲染流程的大概介绍到这里也结束了,很多细节都没涉及,以后有机会再写吧。