天天看點

執行個體化的基本原理

引言:

     最近剛好有個項目要用到這個技術,就學習了下DXSDK的Sample裡面的Instancing10。

     執行個體化的技術是利用多個頂點緩沖區有效的合并,減少Draw Call的次數,進而有效的提高程式的性能。執行個體化比較适合于相同物體的重複渲染,例如粒子系統,草被模拟等等。

     本文實作了一個非常簡單,甚至簡陋的執行個體化的Demo。雖然Demo非常簡單,但是執行個體化的每個必要的元素都已經被展現出來了。相對于Dx的Sample而言簡單的多,不過同樣可以把這個技術簡單解釋清楚。

正文:

     執行個體化适合于相同物體的反複渲染,例如草,粒子系統等。傳統的渲染方法中,相同物體由于一些細節不同,例如位置,顔色等資訊,使得Draw Call被迫調用多次。對于很小的元素,例如草,可能視野裡面會有上萬根草,那麼如果僅僅因為紋理,方向甚至位置不同,就必須調用上萬次DrawCall,進而嚴重降低了程式的性能。利用執行個體化技術,雖然這些Mesh在一些簡單的細節上有不同,但是大體上還是一緻的,是以可以僅僅用一個Draw Call就可以把所有的Mesh渲染出來,這樣節省了CPU與GPU之間的帶寬,非常高效。

     下面是這次Demo的截圖:

執行個體化的基本原理

      這個Demo的畫面非常簡單,就是一個靜态的粒子集合的渲染。但是所有的球體都是利用一個Draw Call渲染出來的,就是說每一幀隻Draw一次。那麼下面我簡單介紹下這個技術的基本原理:

      首先需要了解的一個概念是IA(input assembler)。在VertexShader之前,我們的輸入是一個或者幾個頂點緩沖區,而這些頂點緩沖區并不是VS的輸入,事實上VS的輸入是經過IA處理後的資訊。IA實際上是不可以程式設計的,它就像一個狀态機一樣,隻能設定簡單的幾個狀态,但是由于不同的應用程式對于IA的要求基本都一緻,是以完全沒有可程式設計的必要。IA的目的就是根據頂點緩沖區,圖元拓撲結構以及輸入的資料格式(Layout)生成VS所需要的資訊。我們在渲染任何物體之前,都需要為IA設定好這幾個狀态,分别是通過IASetInputLayout,IASetVertexBuffers,IASetPrimitiveTopology來搞定的。

      在簡單的了解了IA之後,我們來看一下Instancing的基本思想。其實,執行個體化就是通過把每個執行個體的不同的資訊存儲在緩沖(可能是頂點緩沖,常量Buffer等)裡面,然後利用過個頂點緩沖區來設定,進而使生成的每個頂點都包含有自己Custom的資料定義。舉個簡單的粒子,我們看到這個程式中的每個球體的位置是不同的。按照傳統的做法,僞代碼如下:

for( i : 0 to sphereNum )

{

     set world matrix

     draw sphere i

}

      上面的方法,也是最笨拙的方法。僅僅因為每次的Draw Call調用中world matrix不同,就必須調用多次。而其實每次的調用都是很相似的。其實我們在set world matrix的時候,我們更新的是某一個constant value。從宏觀的角度上來說,就是通過constant value來設定球體的位置資訊。但是既然我們可以描述每個頂點的法線,紋理坐标等資訊,我們完全可以為每個頂點描述world matrix資訊,其實就是在VS的輸入裡面多加一個float4而已(這裡我隻用了float4,因為球體不需要旋轉。如果需要旋轉的話,完全可以加四個float4組成一個matrix)。下面的問題是,我們怎麼樣利用IA生成我們想要的資訊:

      假設我們需要渲染100個球體,每個球體256個三角形。那麼我們不可能在CPU端寫入256*100個三角形的頂點資料,因為這樣以來,從CPU到GPU端的傳輸代價會非常大,而這些代價完全可以避免(當然,這裡面如果預處理的話,這些代價也可以無視,但是這種方法實際上相當于用CPU去做GPU擅長的事情,代碼看着很不舒服)。那麼實際上我們應該怎麼做呢?其實也很簡單,Dx10幫我們提供了非常友好的接口。

      這個Demo裡面需要兩個頂點緩沖區,一個來描述球體的頂點資訊,256個三角形而已(768個頂點)。另一個用來描述位置資訊,100個頂點而已。兩個加起來還不到1k個頂點,相對于上面的768000個頂點少了上千倍。這些資料是要走PCIE總線的!那麼似乎上面的資訊無法描述這麼多個球體,但是通過IA處理之後,我們完全可以達到同樣的效果。唯一需要設定的就是Input Layout。

//the vertex layout

D3D10_INPUT_ELEMENT_DESC layout[] =

{

    { "POSITION" , 0 , DXGI_FORMAT_R32G32B32_FLOAT , 0 , 0 , D3D10_INPUT_PER_VERTEX_DATA , 0 } ,

    { "mTransform" , 0 , DXGI_FORMAT_R32G32B32_FLOAT , 1 , 0 , D3D10_INPUT_PER_INSTANCE_DATA , 1 }

};

      利用上面連個頂點緩沖,我們要設定如上的layout。第一個element描述的是球體的頂點資訊。第二個element描述的是位置資訊。我們注意到第五個參數,D3D10_INPUT_PER_VERTEX_DATA/D3D10_INPUT_PER_INSTANCE_DATA,這裡面如果是前者,IA會把頂點緩沖中的每個頂點當做頂點處理。而如果是後者,IA實際上是把頂點緩沖區中的每個元素當做執行個體來處理的。那麼最後IA可以為我們生成 numberOfVertex * numberOfInstance 個頂點資料,這也正是我們想要的資料。

      有了這些資料,我們就可以進行頂點處理了。看看VS中有什麼變化:

//the input struct of the vertex shader

struct    VS_INPUT

{

    //the instance id

    uint    uInstanceID : SV_InstanceID;

    //the position of the particle

    float3    vPosition : POSITION;

    //the instanced position

    float3    vTransform : mTransform;

};

//the default vertex shader

VS_OUTPUT    DefaultVertexShader( VS_INPUT input )

{

    //the output of the vertex shader

    VS_OUTPUT vs_out;

    //transform the vertex

   vs_out.vPosProj = mul( float4( ( input.vPosition + input.vTransform ) , 1.0f ) , ViewProjMatrix );

    //pass the normal

    vs_out.vNormal = input.vPosition.xyz;

    //copy the color

    vs_out.vColor = ColorBuffer[input.uInstanceID % 8];

    //return the output struct

    return vs_out;

}

     很簡單的一個VS。但是這裡我們注意加粗體的一行,這裡我們為每個頂點變換的矩陣是ViewProjectionMatrix。我們沒有做WorldMatrix變換,原因很簡單,是因為我們根本就沒有WorldMatrix這樣一個常量。我們每個頂點的World Matrix都已經存儲到了頂點結構中的vTransform中了。實際上input.vPosition + input.vTransform就相當于做了World Matrix的變換了。

     通過上面的步驟,就可以渲染出來不同位置的球體了。但是我們注意到,VS的Input裡面并沒有Color這一屬性,為什麼球體會有不同的顔色呢?其實球體的顔色資訊是存儲到了Constant Array中的,因為簡單的幾種顔色就可以滿足人的視覺需求了,完全沒有必要每個球體都生成不同的顔色。是以這裡面我隻生成了8中基本的顔色。每個頂點中是有uInstanceID的屬性的,通過對于這個屬性求模運算,我們可以為每個球體選擇出相應的顔色。這裡的uInstanceID是IA為我們生成的,而不是我們自己寫入的資料。

     有了這些處理,我們就可以實作一個簡單的執行個體化的Demo了,下面是源代碼: 

     http://filer.blogbus.com/4730079/resource_473007912613770374.rar

繼續閱讀