1. 程式人生 > >簡要分析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在每一幀渲染前都會先清空渲染佇列。熟悉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類繼承,根據需要過載這個類的虛擬函式。
 
結語
  從總體上來看渲染佇列還是比較複雜的,這裡的介紹也只能是走個流水過程,很多細節還需要仔細推敲,真要想講清楚每個部分都需要單獨拿出來寫成一篇文章。對我來說寫這篇內容的過程又是一次學習,很多原先沒有仔細研究的部分又重新看了一番,我始終覺得閱讀高質量原始碼確實是提升能力的一種有效方法。