很早以前就想写一些关于OGRE的文章了,一直没机会。
理解一个渲染引擎,我觉得最重要的是先抓住了它的主架构,它的主线,渲染流程,不然的话,一个引擎几万行,甚至几十万行的代码,光是打开solution就能吓你一跳了,OGRE也有十几万行的代码量,我一开始看它的时候也是无从下手,感觉代码太多了,不知道从哪开始看好,这个class看看,那个class看看,由于对整个引擎没有一个清晰的认识,看过了也印象不深,所以,最后,还是决定先找出它的主线,了解它的渲染流程,这样才能有机地把各个部分联系起来。
这篇短文也是对OGRE的主要渲染流程的一个介绍,可能对一些class不会太多地去介绍具体的实现细节。我所用的代码都是取自于OGRE的最新的CVS版本。
读者最好对OGRE有一定的了解,至少得看懂它的example,不然可能一些东西理解起来比较困难。对D3D,OPENGL有一定了解更好。
D3D Render Process
如果你看过D3D SDK中带的例子,你一定知道一个比较简单的3D程序要运行起来,至少都会涉及以下的几部分:
首先是数据的来源,包括顶点数据,纹理数据等,这些数据可以从文件中读取,也可以在程序运行时生成。
接下来,我们会建立顶点缓冲区把顶点保存起来,
建立texture对象来表示texture,对顶点组成的物体设置它在世界坐标系下的坐标,
设置摄像机的位置,视点,设置viewport的位置和大小,
然后就可以在渲染循环中开始调用渲染操作了,
经过了front buffer和back buffer的交换,我们就能在屏幕上看到3D图形了,
伪代码如下:
setupVertexBuffer
setWorldTransform
setCamera
setProjectionTransform
setViewport
beginFrame
setTexture
drawObject
endFrame
OGRE RenderProcess
Introduction
1. _fireFrameStarted()
2. 按某种优先级更新所有渲染目标
2.1 firePreUpdate()
2.2 更新所有视口
2.2.1 fireViewportPreUpdate()
2.2.2 场景管理的渲染函数
2.2.2.1 更新阴影
2.2.2.2 更新动画
2.2.2.3 更新节点
2.2.2.4 更新一些参数
2.2.2.5 清空渲染队列
2.2.2.6 填充渲染队列 进行可见性判别
2.2.2.6.1 firePreFindVisibleObjects()
2.2.2.6.2 查找可见节点并加入渲染队列
2.2.2.6.3 firePostFindVisibleObjects()
2.2.2.7 依次渲染每个队列组
2.2.2.7.1 fireRenderQueueStarted()
2.2.2.7.2 渲染该队列组
2.2.2.7.3 fireRenderQueueEnded()
2.2.3 fireViewportPostUpdate()
2.3 firePostUpdate()
3. _fireFrameRenderingQueued()
4. swapBuffer()
5. _fireFrameEnded()
More For Detail
SceneManager::_renderScene
|
|-------_updateSceneGraph(camera);
|
|-------prepareRenderQueue();
|
|-------_findVisibleObjects
|
|
|
|
|
|
|------_renderVisibleObjects
| |
| |-----renderVisibleObjectsDefaultSequence
| |
| |-------_renderQueueGroupObjects
| |
| |---------renderBasicQueueGroupObjects
| |
| |---------sort
| |---------renderObjects
| | |
| | |--------acceptVisitorGrouped
| | |
| | |---------visit
| | |
| |-------------renderSingleObject
以下就是渲染一个物体的主要步骤,在我看来,这就是3D程序的主线,同样道理,无论你多复杂的渲染引擎,都得实现上述的这些步骤,其他的一些效果如阴影,光照等,都是附着在这条主线上的,所以,如果你能在你所研究的渲染引擎上也清晰地看到这条主线,可能对你深入地研究它会大有帮助
下面,我们就一起来找到OGRE中的这条主线。
Root::renderOneFrame
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之前进行更新。
RenderTarget
进入render target的update,可以看到,它仍然把update操作继续传递下去,调用所有挂在这个render target上的viewport的update。
Viewport
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
在SceneManager::_renderScene中所应看的第一个重要函数是_updateSceneGraph,OGRE对场景的组织是通过节点树来组织的,一个节点,你可以看成是空间中的某些变换的组合,如位置,缩放,旋转等,这些变换,会作用到挂接在这些节点上的具体的物体的信息,也就是说,节点保存了world transform,对具体的物体,如一个人,在空间中的定位,都是通过操作节点来完成的。同时节点还保存了一个世界坐标的AABB,这个AABB能容纳所有它所挂接的物体的大小,主要是用于视锥裁减的,如果当前摄像机看不见某个节点的AABB,那么说明摄像机看不见节点所挂接的所有物体,所以在渲染时可以对这个节点视而不见。
_updateSceneGraph的内部处理比较繁琐,我们只需知道,经过了_updateSceneGraph,场景节点树中的每个节点都经过了更新,包括位置,缩放,和方位,还有节点的包围盒。
继续回到SceneManager::_renderScene,接下来要看的是setViewport,它会调用具体的renderer的setviewport的操作,设置viewport中所挂接的render target为当前所要渲染的目标,viewport中的区域为当前所要渲染的目标中的区域。
Render Queue
接下来要碰到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,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的渲染流程的大概介绍到这里也结束了,很多细节都没涉及,以后有机会再写吧。