`

Ogre渲染队列的实现原理

    博客分类:
  • OGRE
 
阅读更多

一篇很好的文章,对渲染队列的分析,来自网络

渲染队列在Ogre中是一个重要的概念,在场景中的所有物体都会在绘制前被Ogre放入到一个特定的渲染队列中。渲染队列主要起两个作用:1.确保正确的绘制顺序。比如先绘制天空盒再绘制一般物体,最后绘制界面。2.提高渲染效率。Ogre将具有相同pass的物体放在一起进行绘制,目的是尽可能减少渲染状态的切换。一般用户常用的是在entity中设置渲染队列序号,其实整个渲染队列的工作流程远远比这个复杂,但基本不需要最终用户干预。这篇文章就是要把这部分不用干预的细节分析一下。

渲染队列是构成Ogre渲染流程和控制渲染效率的关键一环,所以应该深入学习和理解这个部分。我认为通过这个过程不仅可以学习到优秀开源库的设计思想,而且了解实现原理可以帮助我们更好的使用它们。另外在整个Ogre的渲染流程中有很多关键类与之有关联,理解渲染队列对进一步深入理解其它功能是大有好处的。

 

为了更好的讲清楚这个内容我打算按以下的顺序进行展开,希望能够做到深入浅出。

1.渲染队列的实现。

这个部分主要分析Ogre中渲染队列各个类之间的关系与实现细节,

2.Ogre的其它部分如何与渲染队列交互。

这个部分主要分析谁在什么时候操作渲染队列。这部分有助于深入理解渲染队列的各个部分是如何协作完成整个工作的。

3.用户可以做什么。

这个部分主要分析渲染队列的留给我们多大的可操作空间。

 

渲染队列的实现

    下图是渲染队列对应的主要类的简单关系图,作为阅读后续内容的一个参考图.

                                                         

 

 

要了解渲染队列首先接触到的类是RenderQueue,从名字上乍一看很容易让人产生误解,其实RenderQueue不是渲染队列而是一个管理器,负责管理一组名为RenderQueueGroup的对象。RenderQueue本身隶属于场景管理器(SceneManager)对象,一个场景管理器拥有一个RenderQueue对象。

 

RenderQueueGroup表示具有固定编号的渲染队列组。很明显这个类也不是最终的渲染队列,而同样是一个管理器,负责管理一组具有优先级的RenderPriorityGroup对象。RenderQueueGroup的固定编号通过Ogre中的RenderQueueGroupID枚举变量定义,如下:

     enum RenderQueueGroupID

    {

        RENDER_QUEUE_BACKGROUND = 0,

        RENDER_QUEUE_SKIES_EARLY = 5,

        RENDER_QUEUE_MAIN = 50,

        RENDER_QUEUE_SKIES_LATE = 95,

        RENDER_QUEUE_OVERLAY = 100,

 };

每一个渲染队列组对象拥有在上述枚举变量中的一个值,用来标识这个渲染队列组的处理优先级。从枚举值上看编号越小的越先绘制,比如编号为5的天空队列组要先于编号为100的界面叠加队列组绘制。

RenderQueue这个类按照渲染队列组的固定编号作为索引保存每一个RenderQueueGroup对象。RenderQueue没有提供创建RenderQueueGroup对象的方法,只要提供编号就可以获得一个队列组对象。内部会根据指定编号是否存在而自动创建对应对象,最终确保每一个编号的渲染队列组只有一个。RenderQueue类的内部使用固定编号作为key的map来保存渲染队列组对象,这样做的好处是可以快速插入和获取渲染队列组对象。由于map在插入时会自动排序,所以在绘制的时候可以按照顺序从map中依次取出渲染队列组对象,而不用单独再处理排序工作了。

    RenderQueueGroup这个管理器本身的实现比较简单,并提供了一个称之为优先级编号的东西。每一个优先级编号对应一个RenderPriorityGroup对象。其实这一层优先级划分是为了能基于上面的分组后提供更加精细的优先级划分。比如在编号为1的天空盒渲染对象组中又可以划分若干个优先级。这个优先级大部分情况都不需要设置,可以采用默认值。

下面我们来看看RenderQueueGroup这个管理器负责管理的RenderPriorityGroup对象。实际上这还不是最终的渲染队列,而又是一个管理器,这个管理器管理着六个固定类型的渲染队列。通过层层结构终于在这里确定了渲染队列的位置。至于渲染对象最终被放入哪个渲染队列是由RenderPriorityGroup对象负责的。分类标准是基于渲染对象所关联的Technique信息确定的。一般根据是否使用透明材质或是否开启阴影等参数决定最终该渲染对象所属的渲染队列,其中具体的算法就不在这里详细讲解了。

RenderPriorityGroup对象中管理的对象是QueuedRenderableCollection类就是真正的渲染队列,负责渲染对象的存储和排序。目前渲染队列支持三种排序方式:升序、降序和按pass排序。排序方式必须在渲染队列为空的时候确定,因为在向渲染队列插入元素的时候依赖排序方式,所以集合不为空的时候是不能修改排序方式的。渲染队列类内部有两个用来保存渲染对象的集合,如下:

            PassGroupRenderableMap mGrouped;

            RenderablePassList mSortedDescending;

第一个集合负责保存按照pass排序的集合,凡是属于使用同一个pass对象绘制的渲染对象被放在这一个组中。

第二个集合负责按照渲染对象与摄像机的距离进行排序的集合。其实升序和降序排列都使用这个集合,可以通过反向访问达到反序的结果。

从源码中可以看出插入渲染对象时,根据当前渲染队列设置的排序方式,将渲染对象分别放入上面描述的集合中。需要注意的是在元素插入的时候只是分组存储而没有进行真正的排序,排序工作触发的时机将在后面分析。渲染队列有sort方法完成的这个排序工作。

 

我们现在回过头来看看整个渲染队列是如何达到确保正确绘制顺序和提高绘制效率的设计目标的。

首先来看绘制顺序。带固定编号的RenderQueueGroup对象是确保绘制顺序的第一步,将背景、物件和界面划分开,确保渲染顺序不会出问题。另外渲染队列QueuedRenderableCollection对象支持按距离升序或降序排列,可以确保具有透明属性的物件按照由远及近的顺序绘制,完成了确保绘制顺序的第二步。

然后我们看渲染绘制效率。渲染队列QueuedRenderableCollection对象支持按照pass分组,在绘制的时候使用相同pass进行绘制的渲染对象会连续被绘制,大幅减少渲染状态的切换。

    至此大体上分析了渲染队列功能的实现原理。系统共分为四层结构实现整个渲染队列的功能,其中前三层都起到管理器的作用,最后一层存储渲染对象。从组织结构来看是比较复杂的,我想只有通过应用的上下文才能更容易理解这样一个复杂的软件结构,所以下一步我们来分析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类继承,根据需要重载这个类的虚函数。
 
结语
  从总体上来看渲染队列还是比较复杂的,这里的介绍也只能是走个流水过程,很多细节还需要仔细推敲,真要想讲清楚每个部分都需要单独拿出来写成一篇文章。对我来说写这篇内容的过程又是一次学习,很多原先没有仔细研究的部分又重新看了一番,我始终觉得阅读高质量源代码确实是提升能力的一种有效方法。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics