《最長的一幀》理解01_場景渲染

osgViewer:: ViewerBase::renderingTraversals()

OSG 的場景渲染過程可以簡單地分爲三個階段:用戶(APP)階段,更新用戶數據,負責場景對象的運動和管理等等;篩選(CULL)階段,負責對場景中的對象進行篩選裁減,略過那些不會被用戶所見(因而不必渲染)的物體,並根據渲染狀態的相似性對即將進入渲染管線的對象排序(從而避免OpenGL 狀態量的頻繁切換);繪製(DRAW)階段,執行各種OpenGL 操作,將數據送入OpenGL 渲染管線及顯示系統處理。
如果有多個圖形設備(渲染窗口)時,需要分別爲每個窗口的每個攝像機執行相應的篩選和繪製工作,因爲各個攝像機的投影矩陣和觀察矩陣均可能不同;不過用戶(APP)階段不需要被執行多次,因爲用戶數據應當是被各個圖形設備所共享的。
對於單線程運行的系統來說,用戶/篩選/繪製這三個階段在每一幀當中都應當是順序執行的;而對於多線程運行,以至多CPU 的系統來說,則可以將前後兩幀的工作稍微有所交疊。用戶更新(APP)和場景篩選(CULL),以及場景篩選和繪製(DRAW)的工作互相不能重疊;但是我們可以允許在上一幀的繪製沒有結束之前,就開始下一幀的用戶數據更新工作;我們還可以允許由不同的CPU 來執行不同圖形設備的篩選和繪製工作,從而提高整體渲染的效率,實現實時渲染的目的。

單線程模式下,renderingTraversals 函數的基本執行步驟如下:
1、首先使用ViewerBase::checkWindowStatus 檢查是否存在有效的圖形設備,不存在的話,需要使用ViewerBase::stopThreading 停止線程運行。
2、記錄渲染遍歷開始的時間。
3、遍歷視景器對應的所有Scene 場景,記錄分頁數據庫的更新啓動幀,(使用DatabasePager::signalBeginFrame,這將決定DatabasePager 中的數據請求是否過期),並計算場景節點的邊界球。
4、獲取當前所有的圖形設備(GraphicsContext)和攝像機。
5、遍歷所有攝像機的渲染器(Renderer),執行Renderer::cull 場景篩選的操作!
6、遍歷所有的圖形設備,設置渲染上下文(使用ViewerBase::makeCurrent)並執行GraphicsContext::runOperations,實現場景繪製的操作!
7、再次遍歷所有的圖形設備,執行雙緩存交換操作(GraphicsContext::swapBuffers),這是避免動態繪圖時產生閃爍的重要步驟。
8、遍歷視景器中的場景,告知分頁數據庫更新已經結束。
9、釋放當前的渲染上下文(ViewerBase::releaseContext)。
10、記錄渲染遍歷結束的時間,並保存到記錄器當中。

當我們向視景器(Viewer)添加一個新的攝像機(Camera)時,一個與攝像機相關聯的渲染器(Renderer)也會被自動創建。而當我們準備渲染場景時,與特定圖形設備(GraphicsContext)相關聯的攝像機也會自動調用其渲染器的相應函數,執行場景篩選與繪製等工作。

OSG 內部經常使用的類osg::State。簡單來說,這個類是OpenGL狀態機在OSG 中的具體實現。它封裝了幾乎所有的OpenGL 狀態量,屬性參數,以及頂點數組的設置值。我們編程時常見的對StateSet,Geometry 等類的操作,實質上最終都交由State 類來保存和執行。它提供了對OpenGL 狀態堆棧的處理機制(因此我們不必像OpenGL開發者那樣反覆考慮堆棧處理的問題),對即將進入渲染管線的數據進行優化(執行渲染狀數據的排序,減少OpenGL 狀態的變化頻率),同時還允許用戶直接查詢各種OpenGL 狀態的當前值(直接執行State::captureCurrentState,而不必再使用glGet*系列函數)。

明確了單線程模型(SingleThreaded)下OSG 渲染遍歷的工作流程。事實上無論是場景的篩選還是繪製工作,最後都要歸結到場景視圖(SceneView)的相應實現函數中去完成,渲染器類Renderer 只是一個更爲方便和直觀的公用接口而已。下圖中演示了單線程運行時,OSG 系統的場景圖形,攝像機,圖形設備,渲染器和場景視圖的關係:
單線程運行時OSG系統的場景圖形、攝像機圖形設備渲染器和場景圖關係
OSG 視景器的攝像機(包括主攝像機_camera 和從攝像機組 _slaves)均包括了與其對應的渲染器(Renderer)和圖形設備(GraphicsContext);同時,當我們使用setSceneData 將場景圖形的根節點關聯到視景器時,這個根節點實質上被添加爲此Viewer 對象中每個主/從攝像機的子節點(使用View::assignSceneDataToCameras 函數),因而我們可以通過改變攝像機的觀察矩陣來改變我們觀察整個場景的視角。場景的篩選(CULL)和繪製(DRAW)工作實質上都是由內部類osgUtil::SceneView來完成的,但是OSG 也爲場景渲染的工作提供了良好的公用接口,就是“渲染器”。渲染器Renderer 負責將場景繪製所需的各種數據(OpenGL 狀態值,顯示設置,篩選設置等)傳遞給SceneView 對象,並調用SceneView::cull 和SceneView::draw 函數,以完成場景的篩選/繪製工作。攝像機所對應的圖形設備(GraphicsContext)同樣也可能負責調用SceneView::draw 函數,這與我們選擇的線程模型有關。

我們在進行用戶程序的開發時,最常用到的場景管理方式是“場景節點樹”的結構,場景樹頂端的葉節點(osg::Geode)包含了各種需要渲染的幾何體的頂點和渲染狀態信息;而組節點(osg::Group)及其派生出的各種特殊功能節點則作爲場景樹的各個枝節節點,它們也可以擁有不同的渲染狀態;有且只有一個節點可以直接作爲整個場景的根節點,使用setSceneData 將其設置給場景的視景器系統,即等同於將整個場景樹傳遞給OSG 的渲染和顯示系統。而保存節點和幾何體的各種渲染屬性(osg::StateAttribute,例如紋理,霧效,材質,Alpha校驗等)和模式開關,則使用節點所附帶的渲染狀態集(osg::StateSet)。一個狀態集中可以包含多種不同的渲染屬性和開關,處於場景樹頂端的節點將繼承並綜合各級父節點的渲染狀態,實現幾何形狀的正確渲染。

OSG 渲染後臺的主體是場景視圖(SceneView),它同樣實現了“樹狀結構”的管理方式,並據此實現了多個專用於渲染工作的內部類。那麼在深入介紹場景視圖之前,我們先來認識一下OSG 渲染後臺的幾個“幕後英雄”:
osgUtil::CullVisitor:“篩選訪問器”。雖然同樣是繼承自osg::NodeVisitor,不過這個訪問器在整個OSG 系統中可是起了舉足輕重的作用。當我們使用它遍歷場景圖形的各個節點時,CullVisitor 將會對每一個遇到的節點執行場景篩選的工作,判斷它是否會超出視截錐體範圍,過於渺小,或者被遮擋節點(OccluderNode)擋住,從而將無助益於場景瀏覽的物體篩選並剔除,降低場景繪製的資源消耗。我們甚至可以使用SceneView::setCullVisitor 來構建和指定使用自己設計的篩選訪問器,不過在系統渲染後臺之外的環境使用CullVisitor 通常並無用處。
osg::RenderInfo:“渲染信息”管理器。這個類負責保存和管理與場景繪製息息相關的幾個重要數據:當前場景的視景器,當前場景對應的所有攝像機,以及當前所有OpenGL 渲染狀態和頂點數據。這些數據將在場景篩選和渲染時爲OSG 系統後臺的工作提供重要依據。
osgUtil:: StateGraph:“狀態節點”。我們可以對比場景樹的組節點(Group),將StateGraph理解爲OSG 渲染後臺的組節點。它的組織結構與場景圖形的節點結構類似,但是狀態樹的構建主要以節點的渲染狀態集(StateSet)爲依據:設置了StateSet 的場景節點,其渲染狀態會被記錄到“狀態節點”中,並保持它在原場景樹中的相對位置;狀態節點採用映射表std::map 來組織它的子節點,同一層次的子節點如果渲染狀態相同,則合併到同一個“狀態節點”中。
osgUtil::RenderLeaf:“渲染葉”。我們可以把RenderLeaf 理解爲OSG 渲染後臺狀態樹的葉節點。但是,狀態樹的葉節點絕非等同於場景樹的Geode 節點;事實上,“渲染葉”的工作主要是記錄場景樹中存在的各種Drawable 對象(以及與之相關的投影矩陣,模型視點矩陣等信息)。每個“狀態節點”中都包含了一個渲染葉的列表(StateGraph::_leaves),不過只有最末端的“狀態節點”會負責記錄場景中的“渲染葉”。
osgUtil::RenderStage:“渲染臺”。OSG 的渲染後臺除了使用“狀態樹”來組織和優化節點的渲染狀態之外,還有另外一種用於場景實際渲染的組織結構,我們稱之爲“渲染樹”,“渲染樹”的根節點就是“渲染臺”
osgUtil::RenderBin:“渲染元”。它是OSG 渲染樹的分支節點,不過對於沒有特殊要求的場景渲染來說,更多的渲染樹分支也許並不需要:場景中需要渲染的元素及其渲染屬性被保存到各個“狀態節點”和“渲染葉”當中;渲染樹只要按照遍歷的順序,把這些數據記錄到作爲根節點的“渲染臺”當中(即分別保存到std::vector 成員量RenderBin::_ stateGraphList和RenderBin::_renderLeafList 當中,注意RenderStage 派生自RenderBin),就可以執行場景的繪製工作了。

但是,很多時候我們需要某些幾何體在其它對象之前被繪製,比如天空總是要被任何飛過的物體所遮擋;很多時候我們也需要在大部分對象繪製完成之後才繪製某個幾何體的數據(例如HUD 文字總是顯示在所有對象之上)。這種情況下,就有必要對“渲染臺”中的數據進行排序,甚至爲其創建新的分支“渲染元”,以實現這種複雜的渲染順序處理。

首先我們來看一個場景構建的實例,並希望藉此機會瞭解一下“狀態節點”StateGraph和“渲染葉”RenderLeaf 所構成的狀態樹,“渲染臺”RenderStage 及“渲染元”RenderBin所構成的渲染樹,這兩棵樹之間錯綜複雜的關係,以及它們與場景節點樹之間更爲錯綜複雜的關係。
這裏寫圖片描述
上面的場景結構圖中,葉節點_geode3,以及所有六個幾何對象均設置了關聯的渲染狀態集(StateSet),且幾何體1 和幾何體2 共享了同一個StateSet。圖中用“ss”加上數字代號來標識這些StateSet 對象,後面括號中的兩個參數分別表示setRenderBinDetails 的兩個設置項(“-”表示空字串,“R”表示“RenderBin”,“D”表示“DepthSortedBin”)。

進入渲染後臺之後,OSG 將爲這個場景生成“狀態樹”,它是由“狀態節點”StateGraph和“渲染葉”RenderLeaf 所組成的:
這裏寫圖片描述
圖中的“狀態根節點”和“局部狀態節點”都是由狀態樹自動生成的,其中後者的主要工作是保存和維護一些渲染後臺自動創建的渲染屬性;而“全局狀態節點”則保存了一個名爲_ globalStateSet 的渲染狀態集對象。這就是“全局渲染狀態”,它的取值是場景主攝像機的StateSet,換句話說,任何對狀態樹的遍歷都將首先及至場景主攝像機的渲染狀態,然後纔是各個節點的渲染狀態,這就是_globalStateSet 的功用所在了。

而整個狀態樹的構建過程則可以參考上面的場景樹結構圖,其規則爲:
1、狀態樹是根據渲染狀態(StateSet)來生成的,那些沒有設置StateSet 的場景節點將不會影響狀態樹的構架;
2、場景中的Drawable 對象在狀態樹中被置入分別的渲染葉(RenderLeaf)中,而一個或多個渲染葉必然被一個狀態樹末端的節點(StateGraph)所擁有;
3、共享同一個渲染狀態的Drawable 對象(圖中的_drawable1 和_drawable2)在狀態樹中將置入同一個末端節點。

生成狀態樹的同時,OSG 渲染後臺還將生成對應的“渲染樹”,其組成爲一個RenderStage對象和多個RenderBind 對象。如果我們不使用setRenderBinDetails 設置StateSet 的渲染細節的話,那麼所有狀態樹中的末端節點(其中必然包含了一個或多個“渲染葉”)都會按遍歷順序保存到渲染樹根節點(渲染臺)中,渲染樹的構建也就到此結束。

但是,如果我們對於場景中部件的渲染順序有特殊要求的話,那麼渲染樹也會因而變得複雜,上面的場景示例最後可能得到如下的一株“渲染樹”:

這裏寫圖片描述
根據渲染順序的不同,渲染樹生出了三個分支。相應的狀態節點置入各個渲染元(RenderBin)分枝中,其中渲染細節設置爲“RenderBin”的狀態節點(StateGraph)所處的渲染元也可稱爲“不透明體渲染元”;而設置爲“DepthSortedBin”的狀態節點則將其附帶的渲染葉(RenderLeaf)送入“透明體渲染元”,於其中採用按深度值降序的方式排序繪製,以獲得正確的透明體渲染結果;未設置渲染細節的狀態節點則直接由根節點(渲染臺,RenderStage)負責維護。

一個渲染元中可以保存一個或多個狀態節點(或渲染葉);一個狀態節點(或渲染葉)只能置入一個渲染元中。

最後,我們分別用一句話來總結“狀態樹”與“渲染樹”的這幾個組成類。之所以選擇在經歷瞭如此冗長的篇幅之後再作定義,也是爲了便於讀者進行歸納和總結,或者在閱讀和實踐的過程中提出自己的見解。

osgUtil::StateGraph:狀態樹的分枝節點(狀態節點),負責管理場景樹中的一個渲染狀態(StateSet)對象,末端的StateGraph 節點還負責維護一個“渲染葉”(RenderLeaf)的列表。
osgUtil::RenderLeaf:狀態樹的葉節點(渲染葉),負責管理和繪製場景樹末端的一個幾何體(Drawable)對象。
osgUtil::RenderStage:渲染樹的根節點(渲染臺),負責管理默認渲染順序的所有末端StateGraph 節點(附帶“渲染葉”),並保存了“前序渲染”(pre-render)和“後序渲染”(post-render)的渲染臺指針的列表。
osgUtil::RenderBin:渲染樹的分枝節點(渲染元),負責管理自定義渲染順序的末端StateGraph 節點(附帶“渲染葉”);渲染樹的根節點和分枝節點最多只能有“RenderBin”和“DepthSortedBin”兩類子節點,但可以根據不同的渲染順序號衍生出多個子節點,它們在渲染時將按照順序號升序的次序執行繪製。

這裏寫圖片描述
OSG 渲染後臺與用戶層的接口是攝像機類(Camera)。場景中至少有一個主攝像機,它關聯了一個圖形設備(GraphicsContext,通常是窗口),以及一個渲染器(Renderer);我們可以在場景樹中(或者別的視圖View 中,對於複合視景器而言)添加更多的攝像機,它們可以關聯相同的或者其它的圖形設備,但都會配有單獨的渲染器,用以保存該攝像機的篩選設置、顯示器設置等信息。

場景篩選和繪製的工作由渲染器來完成,而圖形設備 GraphicsContext 則負責根據不同時機的選擇,調用渲染器的相關函數。例如在單線程模式中,ViewerBase::renderingTraversals函數依次執行Renderer::cull 和Renderer::draw 函數(後者通過GraphicsContext::runOperations調用),而在多線程模型中調用者的關係將更加錯綜複雜。

OSG 渲染後臺的調度中心是場景視圖(SceneView),它負責保存和執行篩選訪問器(CullVisitor)。CullVisitor 負責遍歷並裁減場景,同時在遍歷過程中構建對於場景繪製至關重要的渲染樹和狀態樹;生成的狀態樹以StateGraph 爲根節點和各級子節點(其中保存場景樹的渲染狀態StateSet 數據),以RenderLeaf 爲末端葉節點的內容(其中保存場景樹中的幾何體Drawable 對象);渲染樹則以RenderStage 爲根節點,RenderBin 爲各級子節點,根據渲染順序和方法的設定,狀態樹中的節點和渲染葉(RenderLeaf)被記錄到RenderStage 和各級RenderBin 中;SceneView 負責保存和維護狀態樹和渲染樹。

繪製場景時,渲染樹中的各級節點將取出保存的渲染葉數據,傳遞給OSG 狀態機(State)。後者是OpenGL 狀態機制的封裝和實現,也是場景繪製的核心元件。狀態機取得渲染葉中的幾何數據之後,再向根部遍歷狀態樹,取得該幾何體繪製相關的所有渲染狀態設置,並親自或者交由StateAttribute 派生類完成渲染狀態的實際設定,以及場景元素的實際繪製工作。

渲染樹(RenderStage/RenderBin),場景樹(StateGraph/RenderLeaf),狀態機(State),渲染屬性(StateAttribute 的諸多派生類)和幾何體(Drawable)之間的關係圖:
這裏寫圖片描述
圖中淺藍色的箭頭表示狀態機對象中保存的各種OpenGL 狀態,即渲染屬性的數據(例如Alpha 檢測,紋理,霧效等),模式數據(種種使用glEnable/glDisable 開啓或關閉的模式),以及頂點座標、法線座標、顏色座標、紋理座標,以及數據索引的數據。這些OpenGL 編程中經常用到的概念在OSG 中被良好地封裝起來,而osg::State 類就是它們的具體實現者。

OSG 的渲染流程大體的認識,即:
1、渲染樹的作用是遍歷各個渲染元(RenderBin),並按照指定的順序執行其中各個渲染葉的渲染函數(RenderLeaf::render)。
2、狀態樹保存了從根節點到當前渲染葉的路徑,遍歷這條路徑並收集所有的渲染屬性數據(StateGraph/moveStateGraph),即可獲得當前渲染葉渲染所需的所有OpenGL 狀態數據。
3、渲染葉的渲染函數負責向狀態機(osg::State)傳遞渲染狀態數據,進而由渲染屬性類本身完成參數在OpenGL 中的註冊和加載工作;渲染葉還負責調用幾何體(Drawable)的繪製函數,傳遞頂點和索引數據並完成場景的繪製工作。

發佈了82 篇原創文章 · 獲贊 58 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章