Inside Geometry Instancing(上)



翻譯:clayman
[email protected]
僅供個人學習使用,勿用於任何商業用途,轉載請註明作者^_^

注:呵呵,發現我對翻譯東西上癮了。這次翻譯了《GPU Gem2》中第三章的內容,大家共同學習^_^

在交互式程序中,豐富用戶體驗的重要方法之一就是呈現一個充滿大量各種有趣物體的世界。從數不清的草叢、樹木到普通雜物:所有這些都能提高畫面最終的效果,讓用戶保持幻想狀態(suspension of disbelief。只有用戶相信並且融入了這個世界,纔會對這個世界充滿感情——這就是遊戲開發的聖盃(Holy Grail)。

         從渲染的觀點來看,實現這種效果,無非就是渲染大量小物體,一般情況下,這些物體彼此都很類似,只在顏色、位置以及朝向上有細小的差別。舉個例子,比如森林中所有樹的幾何形狀都是很類似的,而在顏色和高度上有很大差別。對用戶來說,由外形各異的樹組成的森林才真實,纔會相信它,從而豐富自己的遊戲體驗。

         但是,使用當前的GPU和圖形庫渲染大量由少量多邊形組成的小物體會帶來很大的性能損失。諸如Direct3DOpenGL之類的圖形API都不是爲了每幀渲染只有少數多邊形的物體數千次而設計的。本文將討論如何使用Direct3D把同一幾何體渲染爲大量獨特的實體(instances)。下圖是Back & White 2中,使用了這一技術的一個例子:

 

3.1 爲何使用Geometry Instancing Why Geometry Instancing

         Direct3D中,把三角形數據提交給GPU是一個相對很慢的操作。Wloka 2003顯示使用Direct3D1GHzCPU上,每秒只能渲染10000400000批次(batches)。對於現代的CPU,可以預測這個值大概在每秒30000120000批次之間(對FPS30frame/sec系統來說大概每幀10004000批次)。這太少了!這意味着如果我要渲染一片森林,每批次提交一顆樹的數據,那麼無論每棵樹包含多少多邊形,都將無法渲染4000棵樹以上——因爲CPU已經沒有時間來處理其他任務了。這種情況當然是我們不想看到的。在應用程序中,我們希望最小化渲染狀態和紋理的改變,同時,在Direct3D中使用一次方法調用,在同一批次中對同一三角形進行多次渲染。這樣,就能減少CPU提交批次的時間,把CPU資源留給物理、AI等其他系統。

 

3.2 定義(Definitions

         我們先來定義一系列與geometry instancing相關的概念。

3.2.1 幾何包(Geometry Packet

         A geometry packet is a description of a packet of geometry to be instanced, a collection of vertices and indices。一個幾何包可以使用頂點——包括他的位置、紋理座標、法線、切線空間(tangent space)以及用於skinning的骨骼信息——以及頂點流中的索引信息來描述。這樣的描述,可以直接映射爲一個高效的提交幾何體的方法。

         幾何包是對一個幾何體在模型空間進行的抽象描述, 從而可以獨立於當前的渲染環境。

         下面是對幾何包的一種可能的描述,它不但包含了幾何體的信息,同時還包含了物體的邊界球體信息:

struct GeometryPacker

{

         Primitive mPrimType;

         void* mVertice;

         unsigned int mVertexStride;

 

         unsigned short* mIndices;

         unsigned int mVertexCount;

         unsigned int mIndexCount;

 

         D3DXVECTOR3 mSphereCentre;

         float mSphereRadius;

}

 

3.2.2 實體屬性(Instance Attribute

         對每個實體來說,典型的屬性包括模型到世界的座標變換矩陣,實體顏色以及由animation player提供的用於對幾何包進行skin的骨骼。

struct InstanceAttributes

{

         D3DXMATRIX mModelMatrix;

         D3DCOLOR mInstanceColor;

         AnimationPlayer* mAnimationPlayer;

         unsigned int mLOD;

}

 

3.2.3 幾何實體(Geometry Instance

         幾何實體就是一個幾何包與特定屬性的集合。他直接聯繫到一個幾何包以及一個將要用於渲染的實體屬性,包含了將要提交給GPU的關於實體的完整描述。

struct GeometryInstance

{

         GeometryPacket* mGeometryPacket;

         InstanceAttributes mInstanceAttributes;

}

 

3.2.4 渲染及紋理環境(Render and Texture Context

         渲染環境指的是當前的GPU渲染狀態(比如alpha blending, testing states, active render target等等)。紋理環境指的則是當前激活(active 的紋理。通常使用類來對渲染狀態和紋理狀態進行模塊化。

class RenderContext

{

         public:

         //begin the render context and make its render state active

         void Begin(void);

         //End the render context and restore previous render states if necessary

         void End(void);

        

         private:

         //Any description of the current render state and pixel and vertex shaders.

         //D3DX Effect framework is particularly useful

         ID3Deffect* mEffect;

         //Application-specific render states

         //….

};

class TextureContext

{

         public:

         //set current textures to the appropriate texture stages

         void Apply(void) const;

        

         private :

         Texture mDiffuseMap;

         Texture mLightMap;

         //……..

}

 

3.2.5 幾何批次(Geometry Batch

         幾何批次是一系列幾何實體的集合,以及用來渲染這個集合的渲染狀態和紋理環境。爲了簡化類的設計,通常直接映射爲一次DrawIndexedPrimitive()方法調用。以下是幾何批次類的一個抽象接口:

 

class GeometryBatch

{

         public:

         //remove all instances form the geometry batch

         virtual void ClearInstances(void);

         //add an instance to the collection and return its ID. Return -1 if it can’t accept more instance.

         virtual int AddInstance(GeometryInstance* instance);

         //Commit all instances, to be called once before the render loop begins and after every change to the instances collection

         virtual unsigned int Commit(void) = 0;

         //Update the geometry batch, eventually prepare GPU-specific data ready to be submitted to the driver, fill vertex and

         //index buffers as necessary , to be called once per frame

         virtual void Update(void) = 0;

         //submit the batch to the driver, typically impemented eith a call to DrawIndexedPrimitive

         virtual void Render(void) const = 0;

        

         private:

         GeometryInstancesCollection mInstances;

}

3.3 實現(Implementation

         引擎的渲染器只能通過GeometryBatch的抽象接口來使用geometry instancing,這樣能很好隱藏具體的實體化(instancing)實現,同時,提供管理實體、更新數據、以及渲染批次的服務。這樣引擎就能集中於分類(sorting)批次,從而最小化渲染和紋理狀態的改變。同時,GeometryBatch完成具體的實現,並且與Direct3D進行通信。

         下面使用的僞代碼實現了一個簡單的渲染循環:

//Update phase

Foreach GeometryBatch in ActiveBatchesList

         GeometryBatch.Update();

 

//Render phase

Foreach RenderJContext

Begin

         RenderContext.BeginRendering();

         RenderContext.CommitStates();

        

         Foreach TextureContext

         Begin

                   TextureContext.Apply();

                   Foreach GeometryBatch in the texture context

                            GeometryBatch.Render();

         End

End

 

         爲了能一次更新所有批次並且進行多次渲染,更新和渲染階段應該分爲獨立的兩部分:這種方法在渲染陰影貼圖或者水面的反射以及折射時特別有用。這裏我們將討論4GeometryBatch的實現,並且通過比較內存佔用量、可控性來分析各種技術的性能特性。

         這裏是一個大概的摘要:

l          靜態批次(static batching:執行instance geometry最快的方法。每個實體通過一次變換移動到世界座標,附加上屬性值,然後就提交給GPU。靜態批次很簡單,但也是可控性最小的一種。

l          動態批次(Dynamic batching:執行insance geometry最慢的方法。每一幀裏,每個經過變換,附加了屬性的實體都以流的形式傳入GPU。動態批次可以完美的支持skinning,也是可控性最強的。

l          Vertex constants instancing:一種混合的實現方法。每個實體的幾何信息都被複制多次,並且一次性把他們複製到GPU的緩存中。通過頂點常量,每一幀都重新設置實體屬性,使用一個vertex shader完成gemetry instancing

l          Batching with Geometry Instancing API。使用DirectX 9提供的Geometry Instancing API,可以獲得GeForce 6系列顯卡完全的硬件支持,這是一種高效而又具有高度可控性的gemetry instancing方法。與其他幾種方法不同的是它不需要把幾何包複製到Direct3D的頂點流中。

 

3.3.1 靜態批次(Static Batching

         對靜態批次來說,我們希望對所有實體進行一次變換之後,複製到一塊靜態頂點緩衝中。靜態批次最大的優點就是高效,同時幾乎市場上所有的GPU都能支持這個特性。

         爲了實現靜態批次,先創建一個用來填充經過變化後的幾何體的頂點緩衝對象(當然也包括索引緩衝)。需要保證這個這個緩衝足夠大,足以儲存我們希望處理的所有實體。由於我們只對緩衝進行一次填充,並且不再做修改,因此,可以使用Direct3D中的D3DUSAGE_WRITEONLY標誌,提示驅動程序把緩衝放到速度最快的可用顯存中:

HRESULT res;

res = lpDevice -> CreateVertexBuffer( MAX_STATIC_BUFFER_SIZE, D3DUSAGE_WRITE, 0, D3DPOOL_MANAGED, &mStaticVertexStream, 0 );

ENGINE_ASSERT(SUCCEEDED(res));

         根據應用程序的類型或者引擎的內存管理方式,可以選擇使用D3DPOOL_MANAGEDD3DPOOL_DEFAULT標誌來創建緩衝。

         接下來實現Commit()方法。它將把需要渲染的經過座標變換的幾何體數據填充到頂點和索引緩衝中。以下是Commit方法的僞代碼實現:

Foreach GeometryInstance in Instances

Begin

         transform geometry in mGeometryPack to world space with instance mModelMatrix

         Apply other instnce attributes(like instace color)

         Copy transformed geometry to the Vertex Buffer

         Copy indices ( with the right offset) to the Index Buffer

         Advance current pointer to the Vertex Buffer

         Advance currect pointer to the Index Buffer

End

         好了,接下來就只剩使用DrawIndexedPrimitive()方法,提交這些準備好的數據了。Update()方法和Render()方法的實現都很簡單,這裏不具體討論。

         靜態批次是渲染大量實體最快的方法,它可以在一個批次中包含不同類型的幾何包,但也有一些嚴重的限制:

l          大內存佔用(Large memory footprint:根據幾何包大小和希望渲染的實體數量,內存佔用量可能會變的很大。對於大場景來說,應該預留出幾何體所需的空間。Falling back to AGP memory is possible(注:這裏應該指的是當顯存不夠用時,需要把數據分頁存放到AGP memory中),但這會降低效率,因此,應該儘量避免。

l          不支持多種LODNo support for different level of detal:由於在提交數據時,所有實體都被一次性複製到頂點緩衝中,因此很難對每種環境都選擇一個有效的LOD層次,同時,還會導致對多邊形數量的預算不正確。可以使用一種半靜態的方法來解決這個問題,把特定實體的所有LOD層次都放在頂點緩衝中,每一幀選擇不同的索引值,來選擇實體的正確LOD。但這樣會讓實現看起來很笨拙,違反了我們使用這種方法最初的目的:簡單並且高效。

l          No support for skinning

l          不直接支持實體移動(No direct support for moving instances:由於效率的原因,實體的移動應該使用vertex shader邏輯和動態批次來實現。最終的解決方案其實就是vertex constants instancing

接下來的一種方法將解除這些限制,以犧牲渲染速度換取可控性。

 

3.3.2 動態批次(DynamicBatching

         動態批次以降低渲染效率爲代價,克服了靜態批次方法的限制。動態批次最大的優點和靜態批次一樣,也能在不支持高級編程管道的GPU上使用。

         首先使用D3DUSAGE_DYNAMICD3DPOOL_DEFAULT標誌創建一塊頂點緩衝(同樣也包括相應的索引緩衝)。這些標誌將保證緩衝處於最容易進行內存定位的地方,以滿足我們動態更新的要求

HRESULT res;

res = lpDevice->CreateVertexBuffer(MAX_DYNAMIC_BUFFER_SIZE, D3DUSAGE_DYNAMIC | D3DUSAGE_WRITEONLY, 0 , D3DPOOL_DEFAULT, &mDynamicVertexStream, 0)

這裏,選擇正確的MAX_DYNAMIC_BUFFER_SIZE值是很重要的。有兩種策略來選擇這個值:

l          選擇一個可以容納每一幀裏所有可能實體的足夠大值。

l          選擇一個足夠大的值,以保證可以容納一定量的實體。

第一種策略在一定程度上保證了更新和渲染批次的獨立。更新批次意味着對動態緩衝中的所有數據進行數據流化(streaming);而渲染則只是使用DrawIndexedPrimitive()方法提交幾何數據。當這種方法將會佔用大量的圖形內存(顯存或者AGP memory),同時,在最差的情況下,這種方法將變的不可靠,因爲我們無法保證緩衝在整個應用程序生命期中都足夠大。

第二種策略則需要在幾何體信息數據流化和渲染之間進行交錯:當動態緩衝被填滿時,提交幾何體進行渲染,同時丟棄緩衝中的數據,準備好填充更多將被數據流化的實體。爲了優化性能,使用正確的標誌是很重要的,換句話說就是,在每一批實體開始時都使用D3DLOCK_DISCARD標誌鎖定(locking)動態緩衝,此外,對每個將要數據流化的新實體都使用D3DLOCK_WRITEONLY標誌。這個方法的缺點是每次當批次需要進行渲染時,都需要重新鎖定緩衝,以數據流化幾何體信息,比如實現陰影影射時。

應該根據應用程序的類型和具體要求來選擇不同方法。這裏,由於簡單和清楚的原因,我們選擇了第一種方法,但是也添加了一點點複雜度:動態批次天生支持skinning,我們順便對他進行了實現。

Update方法與之前在3.3.1討論的Commit()方法很類似,但它需要在每一幀都執行。這裏是僞代碼的實現;

Foreach GeometryInstance in Instances

Begin

         Transform geometry in mGeometryPacket to world space with instance mModelMatrix

         if instance nedds skinning, request a set of bones from mAnimationPlayer and skin geometry

         Apply other instance attributes(like instance color)

         Copy transformd geometry to the Vertex Buffer

         Copy indices (with the right offset) to the Index Buffer

         Advance current pointer to the Vertex Buffer

         Advance current pointer to the Index Buffer

End

         這種情況下,Render()方法只是簡單的調用DrawIndexedPrimitive()方法而已。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章