Ogre2.0 全新功能打造新3D引擎


 不知當初是在那看到,說是Ogre2.0浪費了一個版本號,當時也沒多想,以為沒多大更新,一直到現在想做一個編輯器時,忽然想到要看下最新版本的更新,不看不知道,一看嚇一跳,所以說,網絡上的話少信,你不認識別人,別人張嘴就來,對別人也沒損失,還可以裝B下,靠.

  從現在Ogre2.1的代碼來看,大約總結下,更新包含去掉過多的設計模式,SoA的數據結構(用於SIMD,DOD),新的線程模式,新的渲染流程與場景更新,新的材質管理系統,新的模型格式,新的合成器方案,更新是全方面的,可以說,Ogre2.x與Ogre1.x完全不是同一個引擎,不管是效率,還是從渲染新思路的使用上.

  大體上參照二份主要文檔,一份是OGRE.2.0.Proposal.Slides.odp,現在Ogre的維護者之一dark_sylinc比對其他的引擎以及相關測試寫的Ogre2.0要修改的方向,一是Ogre 2.0 Porting Manual DRAFT.odt,移植手冊,簡單來說,Ogr2.0具體的修改位置與說明.非常有價值的二份文檔,可以說,這是全新Ogre改動的精華,我們從這二份文檔里,能學到如何針對C++游戲引擎級別的包含效率,可用性的重構.這是一個幸運的學習經歷.

  從https://bitbucket.org/sinbad/ogre下載最新版本,里面的DOC文件夾,有多份文檔,我整理了下,每部分包含改動原因,改動位置,相關代碼來說,因為全是英文文檔,所以如果理解有錯誤,歡迎大家指出,不希望誤導了大家.本文只針對新模型新功能,也就是加了v1命名空間的(Ogre1.x中的功能,有對應Ogre2.x版本),本文不會特別說明.

Ogre1.x中問題與建議 

Cache末命中

  看看作者的幻燈片,哈哈,圖片特別形象生動.

  這個是函數是判斷模型是否在當前攝像機可見,如果可見,加入渲染通道.不過你去看現在的Ogre2.1的代碼,這個方法沒有變,不是因為沒有改,是因為渲染流程變了,這個函數的功能被MovableObject::cullFrustum包含了,其中判斷攝像機與模型AABB相交的算法也換了.

  這個函數因為每楨都對每個模型來計算,如果Cache misses,損失有點大.

  同樣這個一般用來得到模型的世界坐標位置,也是每楨每個模型要計算的,如果Cache miss,同上.

  那么如何改進,像上面,你不要那些判斷,要么多計算,要么結果不對,作者給出的答案就是改進渲染流程,減少判斷條件的出現.后面會細說.

低效的場景遍歷和操作.

  可以看到場景每次更新都在重復,檢查是否需要更新,然后更新.很多不必要的變量和是否更新狀態的跟蹤,以及太多的判斷,分別造成cache misses緩存不友好.(我去,if判斷有這么大的破壞力?還是只是引擎級別的代碼才會造成這樣的影響,后面渲染流程中,原來很多if都去掉了).

  然后指出Ogre的渲染流程中,其中SceneManager::_renderScene()調用太多次,如Shadow Map一次,合成器中的render_scene一次,然后他們還沒有重復使用剔除的數據,每次renderScene,都重新剔除了一次.特別是合成器中多次調用render_scene,每次都會把渲染隊列里的模型全部檢查剔除一次,這是無效的操作.

  綜合這二點,渲染隊隊肯定要大改,如下是作者綜合別的商業渲染引擎,給出的在Ogre2.0中新的實現建議,根據現在Ogre2.1我所看到的代碼,已經實現如下圖的功能.

  這個圖后面會簡單說下其中的線程相關部分,這就是Ogre2.x的渲染流程了,從圖中,我們可以看到新的合成器是Ogre核心中的一部分,已經不是可選組件了,當然新的合成器也有相當大的更新,功能更強大,更好用.其中更詳細的部分,后面會專門寫一篇介紹Ogre2.x新的渲染流程與合成器.

SIMD,DOD,SoA

  在看如下內容時,先介紹一下什么是基於DOD的設計.DOD(面向數據設計),以及我們面向對象OOP常用的OOD(面向對象設計)

  DOD與OOD之爭: Data oriented design vs Object oriented design

  Data-Oriented Design Data-Oriented Design 二 什么是DOD,為什么要使用DOD,什么情況下用DOD

  [譯]基於數據的設計(Data-oriented design) 這是CSDN上針對第一篇的翻譯

  有興趣大家仔細讀下,這里總結下DOD相對OOP的優勢.簡潔高效的並行化,緩存友好.

  先看如下 http://stackoverflow.com/questions/12141626/data-oriented-design-in-oop 中提出的一個問題,二代碼如下:  

  DOD VS OOP

  下面有人解釋為什么第一段代碼要高效,在第二段代碼中,每次得到一個結構域,浪費更多帶寬,以及更新無用數據到緩存中,緩存Miss高.第一種一次取一個float塊,提高緩存有效利用.

  如在游戲中最常見的操作,取得每個模型的MVP矩陣,而OOP告訴我們,要取的位置,先要取得模型.模型還包含許多其它的內容,但是是無用的,占用緩存空間,緩存命中變低.而DOD是把所有的字段存放在一起,一下取的所有位置,請看下面SoA.

  下面再次提出二個概念,一個是SoA(Structure of Arrays,非你百度搜出來的SOA),一個是AoS(Arrays of Structure),暫時先說下,SoA是一種DOD里常用的數據組織方式,對應OOP里常用的AoS組織方法.

  簡單說下,SoA的組織方式,是把一組元素的每個字段連續保存,如下是我針對Ogre2.x里的代碼改寫的.

  SIMD

  下面的ArrayVector3和ArrayTransformSoA就是SoA相關組織方式與操作,這里上面注釋寫的是SIMD,因為這個組織方式確實是用於SIMD的,SIMD在這不多說,如果有機會,專門研究這個后再來詳細說明,在這,我們只需要知道SSM2可以每下處理128位數據,在32位下,每下處理4個float數據,如上面的ArrayTransformSoA中的move方法中,第二個循環中對於使用SSM2指令來說,就是一個指令,簡單來說速度提高4倍,可以說是一種最簡單安全的並行處理,不要你來設線程,關心同步啥的.更具體點的說,在游戲中一下可以處理四個頂點進行操作,如移動,縮放,以及矩陣運算(當然這四個頂點也有限制,並不是所有都放一起,請看后面).同時如上圖所示,這也是緩存友好的.

  上面演示了常見SoA結構方法,可以看到,對於對象來說,他的存放不再是連續的了,如點的位置y和x相關了4*sizeof(float)個距離,在面向對象結構中,他們應該是相鄰的.但是對於SoA來說,對象列表中的每個字段是相鄰的,如上圖所示應該是XXXXYYYYZZZZ這種內存布局方式,而不是XYZXYZXYZXYZ這種.前面說過,DOD也常用SoA結構,那這是不是就是Ogre中的DOD核心設計,不算,因為這種四個一組的只是專門為了SIMD的SoA結構,真正的DOD核心應該Ogre中的ArrayMemoryManager類,直接拖出這個方法可能看不明白,如下是我針對Ogre2.x中的ArrayMemoryManager改寫的,只保留核心幫助大家理解.

  ArrayTransformManager//對應Ogre中的ArrayMemoryManager

  這個是結合了SMID的DOD設計,不看SMID部分,就看初始化部分,maxMemory表示最多存入多少個Transform,而elements表示Transform對應每個字段占多少位,memoryPool表示每個字段(連續的)在一起占多少個字段,其中調用createArrayTransform生成一個ArrayTransformSoA數據,每四個連續的ArrayTransformSoA的里的如Pos,Scale等地址一樣,如上面那種圖,不同的是對應mIndex,用於指明是當前在SoA中的索引.其實對比上面的ArrayVector3來看,組織數據應該算是一樣的,不同之處是一下並排放maxMemory個數據,而ArrayVector3一下放四個vector3數據.總的來說,這就是SoA數據結構,分別用於SIMD與DOD.

  幻燈片文檔第一點與第二點主要包含二點,一是渲染流程(后文細說),二是SMID,DOD等基本數據格式與操作的改變.后面關於頂點格式與着色器先暫時不說了,大家有興趣可以看下,對應代碼還沒查到,不知是否已經完成.

  最后結束,還不忘指出Ogre中的設計模式被過度使用,說明OOD的多態太浪費,並且用宏來控制一些virtual_l0 ,virtual_l1 ,virtual_l2 來控制多態級別,默認已經不啟用虛函數,如SceneNode::getPosition(),SceneNode::setPosition默認不能重載,如果定義了SceneNode的子類,並重載了如上函數,你需要自己設定多態級別,並自己編譯.嗯,Ogre1.x最出名的多設計模式使用也隨着渲染流程的改變而去掉很多.相信大家看到相關更新后,都會說這改的太大了吧.

  "We don't care really how long it takes" Ogre采用OOD帶來的好處,在客戶級別上的.

Ogre2.x移植手冊:

模型,場景和節點:

  1.Ogre2.0后,Ogre很多對象去掉name,改用IdObejct,這個更多意義就是很多原來的聚合關系都是用的map來表示,name當key,現在為IdObejct,且為自動生成,所以相關id意思不大,相關聚合關系用的是Ogre開發人員自己寫的一個輕量級仿vector的類FastArray.

  The Sorted Vector pattern Part I 

  The Sorted Vector pattern Part II

  因為我們場景中,更多如更新所有模型的位置,AABB等,能快速迭代是我們最大的要求,並且vector更節省空間,內存塊連續(AoS,SMID,DOD).相反,map的優勢如快速查找,隨機刪除與添加並不常用.所以像Ogre1.x中很多map的用法並不明智.

  其中,Ogre內部更多聚合關系使用的是FastArray,FastArray是針對std::vector的輕量級實現,去掉了其中的大量邊界檢查與迭代器驗證.沿用大部分std::vector的功能,如std::for_each正常工作,也有同標准不一樣的,如FastArray<int> myArray(5),不會自動初始化這里面的5個數據是0.注釋中默認不建議我們用,因為如前面所說,和標准std::vector不同,這個類主打效率,針對邊界檢查與相關驗證全部去掉,除非我們知道我們應該怎么用.

  2.如何查看如MovableObject與Node里的數據.

  認真看過前面SoA部分的,這部分都不用細說了,Node里的位置信息用Transform保存,MovableObject的信息用ObjectData保存,對應的信息是四個一組用於SIMD指令加速,所以一個Node對應的Transform其實有4個Transform信息,這4個Transform位置在內存中數據如下XXXXYYYYZZZZ,根據Transform的mIndex(0,4]找出對應數據.

  避免SIMD不能正常計算,以及大量的非空判斷,Transform中如果只有三個node,最后一個node不設為null,為虛擬指針代替,這也是DOD常用的一個方法.

  3.在Ogre1.x中,MovableObject只有附加到SceneNode后,才算是在場景中,而Ogre2.x就沒有這個概念了,Node只算是MovableObject用來操作與保存相關位置信息.其實說起來,應該是和渲染流程的改變有變,原來的渲染流程中,通過Node一級一級查找下去所有的MoveableObject是否在視截體范圍內,而現在生成一個MovableObject后,在對應的ObjectMemoryManager(同前面所講,分配SoA結構) 保留指針,而在場景進行剔除時,根據ObjectMemoryManager來的,所以說MovableObject一直在場景中.但是Node保留位置信息,沒有位置信號,一樣不能在場景中渲染.

  在Attaching/Detaching操作后,會自動調用對應setVisible,Attaching后,自動設visible為true,也可以設為flase,Detaching后,自動設visible為false,如果手動設true,會出現錯誤.

  如果你Attaching一個SceneNode后,你再次Attaching另一個SceneNode時,需要先Detaching.否則斷言錯誤. 

  4.所有的MovableObject都需要SceneNode,包含燈光與攝像機,所有的都需要附加到SceneNode中才行,很簡單,原來如Light與Camera與一般的MovableObject有些區別,一是不渲染自己,二是有自己的位置信息,但是現在SceneNode不用於渲染通道,只是保存位置信息,自然和一般的MovableObject一樣用SceneNode來保存位置信息了,燈光與攝像機也都必須附加到SceneNode上才有位置信息.

  5. 改變Node的局部坐標位置,並不能馬上得到對應的全局位置.不同於Ogre1.x版本,如setPosition會設置一個flag表示父節點要更新,而調用getDerivedPosition后,檢查到flag就去更新父節點了.這是一個不友好的Cache設計.在Ogre2.x中,去掉了上面的一些flag,更新不會更新父節點,所有節點的更新都在每楨中的updateAllTransforms,就是說如果你setPosition后,你需要當前楨運行之后(調用updateAllTransforms)后才能得到getDerivedPosition的正確值.當然如果你一定現在要,可以用getDerivedPositionUpdated,當然這就是走老路了,如果可能,請更新你的設計.同時原Ogre1.x中的getDerivedPosition在Ogre2.x分成了二個方法,也去掉了if判斷.

  6.Node和MovableObject區分成動態與靜態,靜態就是告訴Ogre,我不會每楨去更新位置.這樣Ogre能做的優化一是節省CPU,不用每楨去更新相應位置和相應AABB,二是告訴GPU,某些模型可以合並批次渲染.其中動態模型只能附加到動態節點上,靜態模型只能附加到靜態模型上.動態節點可以包含靜態子節點,而靜態節點不可以包含動態子節點(根節點除外),原因很簡單,靜態節點不常更新,你放個動態子節點在我里面,是讓我更新了還是不更新了.

  7.在Ogre2.0以前,我們知道最后用於渲染的只是Renderable與Pass,其中在場景可見性檢查模型時, MovableObject把當下的所有Renderable加入RenderQueue,用戶可以不通過MovableObject也能把Renderable加入RenderQueue,在Ogre2.1后,必需把Renderable與對應的MovableObject一起加入RenderQueue,因為新的渲染系統中的渲染模型,渲染中要求的Lod等級,骨骼動畫,MVP矩陣等都直接保存在MovableObject.就如Ogre2.1以后MVP矩陣是直接保存在對應MovableObject中,不再是通過Renderable的getWorldTransforms獲取.詳細請查看相關QueuedRenderable引用相關信息.

  此外去掉原Ogre2.0在場景可見性檢查時要得到是否可以接收陰影用到的訪問者模式,讓修改的人來說,這個模式花費太大,不值得.當然Ogre2.0以后的陰影相關全部改變,只支持shadow map,以及shadow map很多變種技術.模版陰影去掉支持,可以看到MovableObject不再是ShadowCaster的子類,這個類包裝模版陰影相關.

  再一個就是新模型和VAO的引進,原來的VBO的類也改成相應VaoManager.具體后文詳細分析.

SIMD,DOD,Thread:

  SIMD與DOD設計前面有說,在這里,只是簡單說幾個類.所在文件都在OgreMain/Math/Array/SSE2下.

  如下這段代碼是前面我說的針對Ogre2.x中抽取的,主要是根據相關Ogre中SIMD與DOD設計中改寫的,幫助理解. 

  幫助理解Ogre中的SIMD,DOD

  1.ArrayVector3 對應ArrayVector3.是SIMD要求的SoA結構,用於使用SSE2指令.

  2.Transform 對應ArrayTransformSoA.用於Node的位置信息.

  3.ArrayMemoryManager對應ArrayTransformManager.用於生成DOD數據結構內存排列.

  當然ArrayMemoryManager本身的功能要復雜的多,如刪除插槽,追蹤已被刪除插槽,添加時自動選擇刪除插槽,隊列中空白插槽太多后的自動清理.模擬的ArrayTransformManager都是沒有的.

  線程在Ogre2.x中再也不是一個可有可無的功能,也不是一個玩具,也不是簡單的邏輯一個線程,渲染一個線程這種簡單用法.因為Ogre2.x中使用位置太多,如下只列出SceneManager::updateSceneGraph()中關於新的線程的使用,我們來看下基本情況.

  先看如下代碼. 

  SceneManager::updateSceneGraph

   我們先假設有n個工作線程.如下是針對這段代碼的分析:

  Barrier類型mWorkerThreadsBarrier對象:同步主線程與工作線程,用二個信號量來模擬.( 我們用來互換信號量。否則,如果多個工作線程中如果一個,造成這一困在當前線程的同步點,最終會死鎖)。

  mNumThreads:工作線程與主線程之和,n+1

  mLockCount:當前鎖定線程數.

  工作線程執行方法updateWorkerThread -> _updateWorkerThread(死循環)

  主線程里fireWorkerThreadsAndWait與工作線程中的_updateWorkerThread都會調用二次mWorkerThreadsBarrier->sync()方法.如下我們簡化mWorkerThreadsBarrier->sync()為sync方法,如沒做特殊說明,sync都是指mWorkerThreadsBarrier->sync().

  創建Barrier對象mWorkerThreadsBarrier,信號量為0.

  創建n個工程線程,因為當前信號量的值為0,所以在工作線程_updateWorkerThread循環中第一次的sync方法會引起當前信號量WaitForSingleObject堵塞.最終鎖定線程數mLockCount為n.

  主線程更新場景時,假設到了如上面的updateSceneGraph->updateAllTransforms后,更新標識為CULL_FRUSTUM,調用fireWorkerThreadsAndWait中的第一次sync方法后,達到mWorkerThreadsBarrier中的條件mLockCount== mNumThreads,此時重置當前信號量為工作線程數n(然后切換成下一個信號量,下一個信號量值為0).這樣當前信號量下的工作線程就可以執行工作(_updateWorkerThread->updateAllAnimationsThread).

  當主線程和n個工作線程紛紛通過第一個sync方法,執行任務,各達到線程中第二次sync方法前,前n個線程(主線程可能也在里面了)來到下一個信號量前面,這個信號量的值為0,所以大家都等着,等到最后一個線程也執行完了,到二次sync方法,此時和第三步差不多,因為mLockCount== mNumThreads, 此時重置當前信號量為工作線程數n(然后切換成下一個信號量,下一個信號量值為0).這樣當前信號量下所有線程都紛紛跨過第二次sync方法.工作線程就是執行完當前循環,進到下一個循環里的第一次sync這里.這樣又到當前信號量這里來了,因信號量為0,所以WaitForSingleObject堵塞.和第2步狀態.

  然后重復這個過程,一些平等關系的更新就可以用工作線程更新,等主線程調用fireWorkerThreadsAndWait的第一個sync方法.然后重復下去.不過要指出的是,線程的順序是不確定的,不僅僅是說第四步最后到達的是不確定的,也有可能在第三步中,因為主線程執行fireWorkerThreadsAndWait后,接着執行fireWorkerThreadsAndWait的第一個sync可能還在工作線程第二個sync到時第下一個循環的第一個sync之前完成.但是這個其實是沒有關系,因為我們要求的同步也不麻煩,只有一齊開始,然后等到一齊結束,這個是滿足的.

  這只是Ogre中新線程方案中的一例,這個過程我們可以看到Ogre中的渲染過程中,所有節點更新,所有動畫,所有模型AABB全部是多線程開動的,這些方法內部數據的組合也多是DOD對應的SOA結構,前面DOD鏈接中說明DOD優勢就有更容易的並行化以及更好的緩存命中.前面的幻燈片文檔里有專門針對DOD與OOD緩存命中的比較.

HLMS:只是簡介

  Ogre2.0已經放棄FFP了,不過本來就是一個應該早放棄的東東,在Ogre1.9就能用RTSS組件替換FFP了,不過在Ogre2.0是真真完全沒有,相關API都沒有了,那是不是說要簡單渲染一個模型都要寫着色器代碼了,或是一定要用到RTSS,這都不是,我們需要用到最新的高級材質系統HLMS.HLMS可以說是組合原來的material和RTSS的新的核心功能,使用更方便與靈活,高效. 

  在說明新的HLMS時,我認為有必要先講解一下渲染流水線,這是博友亮亮的園子OpenGL管線(用經典管線代說着色器內部),本文FFP與可編程管線都有說明,對比如上,HLMS采用分塊方案,這樣有很多好處,第一每塊狀態可以重復使用,減少內存和帶寬,提高cache命中.第二D3D和OpenGL都是狀態機模式,使用塊模式,可以組合相同塊一起渲染,減少狀態切換,提高渲染效率.這也是為什么作者說原來的Material是低效的,不建議使用,還有作者特意說明,這種分塊模式看起來像是D3D11中的,但是作者本身是一個OpenGL fan,他開發這個HLMS一直都是在OpenGL下,只能說,作者說D3D11開發者想到一起了.

  Macroblocks是光柵化狀態,它們包含深度的讀/寫設置,剔除模式。Blendblocks就像D3D11混合狀態,含混合模式及其影響因素。Samplerblocks就像D3D11在GL3 +或采樣狀態采樣對象,包含過濾信息,紋理尋址模式(包,卡,等),紋理的設置,等等。

  Macroblocks塊: 包含逐片斷處理中的深度檢查,還有剔除模型,顯示模式,類似如D3D11中的ID3D11RasterizerState.

  Blendblocks塊: 逐片斷處理中的Alpha混合操作.類似ID3D11BlendState.

  Samplerblocks塊:紋理塊的屬性集合.類似D3D11_SAMPLER_DESC.

  Datablocks塊:這個OgreMain里沒怎么體現出來,應該去看OgreHlmsPbs,對應文件夾Media\Hlms\Pbs中,可以看下是怎么回事. Datablocks與Renderable結合一起填充着色器代碼,如RTSS一樣.

  包含上面所有塊,承載着相當於原Material項.舉例如原來Ogre1.x老模型Renderable原來是setMaterial,現在新模型Renderable使用setDatablock.

  原來Material中,如表面顏色等影響頂點着色器與片斷着色器之間屬性分被分配到Datablocks,剛開始看到alaph_test等相關設置在里面還疑惑了下,后面直接在Media\Hlms\Pbs里查看,如glsl中的PixelShader_ps.glsl可以直接根據相應alaph_test設置已經可以丟棄片斷,和逐片斷處理中的AlphaTest一樣,這里有點還是沒搞清楚,是逐片斷處理放棄AlphaTest了還是提前到片斷着色器中處理了,畢竟逐片斷處理是在片斷着色器之后. 逐片斷處理別的處理如上也是單獨分塊的.

總結:  

  這些都只是對應二份文檔的一小部分翻譯,只是簡單介紹Ogre2.x中一部分新功能,從這一小部分,我們已經可以看到,這是一個完全不同的Ogre引擎,如下幾點后面會具體分析.

  1.新的渲染流程.

  2.新模型格式以及VAO的引進

  3.HLMS詳解.

  4.新合成器詳解.

  5.新線程詳解.

  當然Ogre2.1事實還是半完成狀態,相關文章會盡量使用最新版本進行分析.最后,不得不說句,TMD果然只能用C++或C來設計游戲引擎,如上優化如果用C#來做,肯定比用C++都來的麻煩.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM