[转]简要分析Ogre的渲染队列实现原理(二)
http://www.cnblogs.com/cppguru/archive/2009/11/25/1610626.html
简要说Ogre渲染主流程分三步使用渲染队列:清空、构造和访问,这个过程在每一帧的绘制过程中重复执行。
渲染队列的清空
Ogre在每一帧渲染前都会先清空渲染队列。熟悉Ogre渲染流程的很容易看到在SceneManager::_renderScene() 这个方法中调用prepareRenderQueue()方法。这个方法的实现很简单,就是清空现在的渲染队列并且初始化渲染队列的一些配置参数。渲染队列的清空函数是RenderQueue::clear(),该函数继续调用内部其它对象的清理函数。
渲染队列的构造
渲染队列构造的基本原理是从场景管理器中计算出当前帧可见的所有对象,并依次将这些可见的对象加到渲染队列中。根据前面介绍的内容,渲染队列根据传入的对象和自身的配置按照一定规则将对象保存起来。
在调用SceneManager::prepareRenderQueue()方法之后,Ogre会继续调用场景管理器的_findVisibleObjects() 方法。从函数名称我们可以看出这个函数是用来寻找可见对象的。下面是它实现的核心代码:
getRootSceneNode()->_findVisibleObjects(cam, getRenderQueue(), visibleBounds, true, mDisplayNodes, onlyShadowCasters);
从代码中很容易看出它取出场景管理器中的根节点,并调用根节点的_findVisibleObjects() 方法完成寻找可见对象和渲染队列填充功能。这个函数是个递归函数,从根节点出发递归调用所有子节点的这个函数。我们最关心的是这个函数的第二个参数,它代表当前场景管理器使用的渲染队列。将场景管理器传进这个函数意味着由每个node对象负责渲染队列的填充。当这个函数返回时我们就可以拿到填充好的渲染队列了。为了能深入了解整个过程我们继续向下分析,在节点的_findVisibleObjects方法中我们可以看到它获取了绑定到node上的所有的MovableObject对象,并调用这些MovableObject对象的_updateRenderQueue方法。Ogre中有很多继承自MovableObject的对象,这里我们从最熟悉的Entity对象来继续我们的分析。Entity是一种ovableObject对象,所以如果该节点上绑定的对象是Entity对象,那么节点上调用_updateRenderQueue方法其实就是调用Entity对象的_updateRenderQueue方法。
我们知道Entity对象中包含SubEntity对象,而只有SubEntity对象才是可以显示的对象,所以在Entity的_updateRenderQueue方法中获得所有可以显示的SubEntity对象,并将其放入作为参数传入的渲染队列中,代码如下:
SubEntityList::iterator i, iend;
iend = displayEntity->mSubEntityList.end();
for (i = displayEntity->mSubEntityList.begin(); i != iend; ++i)
{
if((*i)->isVisible())
{
if(mRenderQueueIDSet)
queue->addRenderable(*i, mRenderQueueID);
else
queue->addRenderable(*i);
}
}
在这里我们终于看到了对渲染队列的添加操作,调用RenderQueue的addRenderable函数。addRenderable函数有几个重载的版本,根据需要可以选用合适的函数。
至此我们了解了一个渲染对象是如何被更新至渲染队列的大体过程。当然这里提到的函数本身功能是很复杂的,这里我只将重要的部分做了介绍,其余的内容不在讨论的范围。
渲染队列的访问
在构造了当前这帧所有可见对象的渲染队列后,剩下一步就是如何从渲染队列中取出我们要绘制的渲染对象,我们回到SceneManager::_renderScene() 方法。之前我们讲到调用SceneManager::_findVisibleObjects()方法来填充渲染队列,在这个方法之后我们看到了_renderVisibleObjects()方法,它调用了renderVisibleObjectsDefaultSequence方法。源代码中这里还有一个按照自定义的顺序进行渲染的方法,这个部分我们后面再进行详细分析。Ogre总是尽可能追求扩展性,所以这里也不例外为用户提供了自定义的渲染顺序。我们先从简单的默认渲染顺序入手吧。
由于开启阴影的处理比较复杂,所以这里我们只讨论默认不带阴影的渲染过程。SceneManager::renderVisibleObjectsDefaultSequence函数实现很简单,按照优先级由小到大从RenderQueue中取出RenderQueueGroup进行处理。前面讲过RenderQueueGroup内部又进行了一次优先级排序,所以从RenderQueueGroup中按照优先级取出RenderPriorityGroup对象依次进行处理。我们这里讨论不包含阴影的处理,所以只需要处理RenderPriorityGroup中的三个渲染队列就可以了(前面有介绍,RenderPriorityGroup对象包含六个不同功能的渲染队列)。包括一般对象、不排序透明对象和排序透明对象三个序列。
在渲染对象插入队列的时候并没有进行排序,现在到了进行排序的时机了。每个渲染队列分别调用sort方法完成排序工作,排序之后获得了正确有效的绘制顺序,并使用访问者模式进行最后的渲染调用,最终所有绘制都会调用SceneManager::renderSingleObject函数完成一个对象的渲染工作。这里使用访问者模式增大了理解代码的难度,我想暂时先不管这部分,以后有空专门写个文章来讲一下吧。
着一块背后的思想挺简单的,但实现的时候相互交织太深,很难一下看清原貌。所以上一段很难写清楚,将就看看吧。
下面一部分讲讲使用Ogre的用户能够做些什么。
总体上来说Ogre在这个功能上开放了三个层次的控制权,
1. RenderQueue对象配置。
2. RenderQueue对象监听者。
3. 自定义渲染顺序。
RenderQueue对象配置
一个场景管理器只拥有一个RenderQueue对象,而且该对象在渲染过程中不会发生变化,所以这个对象拥有若干可以配置的参数。配置的时机当然是在渲染之前。从RenderQueue对象的方法上可以看出一连串set开头的函数负责这些参数的设置。利用这种方式只能总体上对渲染队列进行配置,在渲染队列运作过程中无法干预内部的功能,所以称之为第一个层次的控制。
想要配置这个对象非常简单,调用SceneManager::getRenderQueue方法可以获得RenderQueue对象的指针,通过对象指针访问相关的方法。
RenderQueue对象监听者
监听者这个概念在Ogre的体系中非常常见,同样在渲染队列这里也提供了设置监听者的接口。接监者接口方法定义如下:
virtual bool renderableQueued(Renderable* rend, uint8 groupID, ushort priority, Technique** ppTech, RenderQueue* pQueue) = 0;
用户可以自己实现上述监听者接口的方法。这个方法在渲染对象被放入渲染队列前被调用。通过这个方法可以修改绘制这个对象的Technique对象,而且可以通过函数返回false阻止Ogre将这个对象放入渲染队列。使用监听者可以通过调用RenderQueue::setRenderableListener方法完成。
自定义渲染顺序
自定义渲染顺序当然是最灵活的控制方式,同时也是比较复杂的一种方式。这个功能主要包括两个主要的类RenderQueueInvocationSequence和RenderQueueInvocation。场景管理器在渲染时会判定用户是否设置了RenderQueueInvocationSequence对象,如果有就按照这个对象定义的方式启动渲染,反之就按照之前介绍的默认方式渲染。RenderQueueInvocationSequence对象是与viewport相关的,用户可以在每一个viewport上关联一个自定义渲染对象,用来控制每个viewport的渲染过程。
RenderQueueInvocationSequence对象是很单纯的容器类,用于保存RenderQueueInvocation对象的实例,而RenderQueueInvocation对象负责调用RenderQueueGroup的渲染过程。
这个结构的控制原理其实也挺简单的。利用RenderQueueInvocation对象做了一次渲染顺序的映射,这个对象在RenderQueueInvocationSequence中按顺序存放,按顺序访问,同时这个对象与RenderQueueGroup做关联,完成渲染顺序的变更。
比如有两个RenderQueueInvocation对象,第一个关联第五十号RenderQueueGroup对象,第二个关联第一号RenderQueueGroup对象对象。而RenderQueueInvocation是按顺序存储和访问的,所以五十号先于一号RenderQueueGroup对象绘制,这就改变了绘制的顺序。 需要明确的是这套机制只是重新定义了RenderQueueGroup的渲染顺序,如果想真正完全控制整个渲染顺序必须自己从RenderQueueInvocation类继承,根据需要重载这个类的虚函数。
结语
从总体上来看渲染队列还是比较复杂的,这里的介绍也只能是走个流水过程,很多细节还需要仔细推敲,真要想讲清楚每个部分都需要单独拿出来写成一篇文章。对我来说写这篇内容的过程又是一次学习,很多原先没有仔细研究的部分又重新看了一番,我始终觉得阅读高质量源代码确实是提升能力的一种有效方法。