Ogre渲染主循环内部流程简介
Ogre在开源图形引擎里面属于比较重的,涉及内容比较复杂。Ogre诞生至今,其版本已发生了很大的变化。Ogre早先诞生于windows平台(D3D)。后来在移动终端设备上,由于OpenGL ES版本的引入,Ogre不断支持了ES的RenderSystem。但在起初Ogre在移动终端上的性能不高, 近两年Ogre对于ES 2.0的优化使得性能大幅提升。
1.Ogre渲染流程
Ogre的教程可以搜到很多,一般是介绍Ogre的组件和SDK用法。但对于一个图形引擎,拿到源码最急迫的是想知道其渲染流程是什么样的,说白了就是主循环都干了啥。
一个图形引擎它要渲染一幅画不外乎干这么几件事:
- 设置物体顶点——Vertex更新
- 设置空间变换——Transform更新
- 设置Camera和投影视口变换——Viewport更新
- 设置纹理数据——Texture更新
- 使用需要的GPU Shader,开始渲染,管线执行
上述步骤反复调用,绘制所有物体
- 最后,绘制完一帧提交渲染结果。
当然,在这之前需要将顶点、纹理、Shader等资源数据首先加载到显存中。以上就是渲染流程的主线,无论多复杂的渲染引擎,都得实现上述的这些步骤,其他的一些特效如光照、骨骼等,都是附着在这条主线上的。因此,能在Ogre上也清晰地看到这条主线,对Ogre的深入研究将有很大帮助。
Ogre到底是怎么做的?
首先是Root::renderOneFrame
SDK的例子一般是利用Root::startRendering来触发该函数。renderOneFrame这个函数就是对整个OGRE进行一帧的更新,包括动画,渲染状态的改变,渲染api的调用等。本文就是剖析这个函数里面都做了什么。renderOneFrame有两个fire函数,给用户以渲染前后的一些回调,如_fireFrameStarted就会对所以的frameListener进行处理,这些fire函数可以暂时不理会。
下一步,进入_updateAllRenderTargets
在这个函数中,用当前的RenderSystem对所有创建出来的RenderTarget进行update,RenderTarget也就是渲染的目的地,它分为两种,一种是RenderTexture,表示渲染到纹理,另一种是RenderWindow,即渲染到窗口。
在RenderSystem::_updateAllRenderTargets中,
可以看到在RenderSystem中,对创建出来的RenderTarget是用RenderTargetPriorityMap来保存的,用于按照一定的顺序来对RenderTarget进行update,因为在渲染物体到RenderBuffer时,一般会用到之前渲染好的RenderTexture,所以RenderTexture形式的RenderTarget需要在RenderBuffer之前进行更新。
RenderTarget的update,调用所有挂在这个RenderTarget上的Viewport的update。
Viewport其实就是定义了某RenderTarget上的一块要进行更新的区域,所以一个RenderTarget是可以包含多个Viewport。多个viewport就可以在画面中开多个窗口。 在Ogre中,Viewport看成是保存Camera和RenderTarget这两者的组合;渲染时把Viewport中Camera拍摄到的东西渲染到RenderTarget上。Viewport有一个ZOrder的属性,ZOrder越小的,越先被渲染。如果两个Viewport区域重合,Zorder大的最后会覆盖掉Zorder小的内容。
继续进入Viewport::update;
就像前面所说,它所引用的camera来渲染整个场景,而在Camera::_renderScene中,是调用
SceneManager::_renderScene(Camera* camera, Viewport* vp, bool includeOverlays)
真正的场景渲染流程开始了, _renderScene是机器复杂的。下面慢慢道来。
从函数名称还有参数也可以看出来,这个函数的作用就是利用所指定的Camera和Viewport,来把场景中的内容渲染到Viewport所指定的RenderTarget的某块区域中。根据Camera,可以获取计算出View Matrix与Projection Matrix,还可以进行视锥体的剔除与裁剪,另外,可以只渲染Camera可见的物体。
由于_renderScene的任务比较多,一些特效的计算更新也放入其中,如:骨骼等。这些本文先忽略,主要关注渲染流程。
在SceneManager::_renderScene中所应看的第一个重要函数是_updateSceneGraph,
Scene的更新从根Node开始。在Ogre中,Scene图是用Node节点来进行组织的,Node之间有父子关系,有一个根节点。所有的物体都需要挂接在某个Node上,任何一个Node可以进行位置,缩放,旋转的空间变换;并且会作用到挂接在这些节点上的具体的物体上,也就是说,Node保存了全局的World Transform。对于任何一个物体来说,操作Node的空间变换,物体也进行响应的空间变换。
此外,Node节点还保存了AABB(包装盒),这个包装盒是一个包含Node上的所有物体的一个立体空间,它的主要是用于视锥体裁减的,如果Camera看不见某个节点的AABB,那么说明Camera就看不见Node上所挂接的所有物体,在渲染时可以忽略掉这个Node。我个人觉得这块儿的计算量较大,是主循环主要耗时的地方。经过了_updateSceneGraph,场景中的每个节点都经过了更新,包括位置,缩放,旋转,还有节点的包围盒AABB。
继续回到SceneManager::_renderScene,接下来重要的是setViewport:
它会调用具体的RenderSystem的setViewport的操作,设置Viewport中所包含的RenderTarget为当前所要渲染的目标,而Viewport中的区域为当前所要渲染的目标区域。
接下来一个重要的概念RenderQueue。可以简单把它想成是一个容器,里面的元素就是Renderable,每个Renderable可以看成是每次绘制时需要渲染的物体,可以是一个模型,也可以是模型的一部分。在RenderQueue中,它会按材质Material来分组这些Renderable,还会对Renderable进行一定规则的排序。
在每一次调用SceneManager::_renderScene时,都会调用SceneManager::prepareRenderQueue来清理RenderQueue,然后再调用SceneManager::__findVisibleObjects来把当前摄像机所能看见的物体都加入到RenderQueue中。
SceneManager::__findVisibleObjects是一个递归过程,它从场景的根节点开始,先检查Camera是否能看见节点的包围盒,如果看不见,忽略它。如果能看见,再检测挂在这个节点上的所有MovableObject,如果MovableObject是可见的,就会调用它的_updateRenderQueue方法,把这个MovableObject相关的Renderable送入RenderQueue中去。
MovableObject用于表示场景中真实的离散的物体,他有很多子类,如Entity。一个MovableObject要先显示,必须先挂接在某个场景节点上,通过场景节点来设置它的位置。
一般来说,检测完所有Node节点之后,场景中当前Camera可以看得到无物体MovableObject,他们的Renderable将被添加到RenderQueue中去。
这块儿的计算量是除特效计算外,渲染流程中CPU计算量最大的地方,个人觉得是Ogre需要进一步优化的地方。
到此, 我们就有了场景中要渲染的所有数据信息,下面_renderScene就开始进行实际的渲染。
这里解释一下_renderVisibleObjects(),他会对RenderQueue中的每个Renderable进行渲染,最终在SceneManager::renderSingleObject中取出每个Renderable所保存的顶点、世界矩阵等信息来进行渲染。
看看整个渲染过程,是不是很像自己写一个OpenGL的渲染过程。而且我个人发现,Ogre还保留着以前PC上固定管线控制上的痕迹,比较喜欢用CPU来计算哪些可以看到哪些看不到。这方面随着硬件的发展,GPU可以介入进来,提高CPU这边的性能。