managed DirectX (第三章)

 

使用簡單的渲染技術

翻譯:clayman


    至今爲止,我們的渲染工作效率都很低。每次渲染場景時,都要分配新的頂點列表,並且所有東西存儲在系統內存裏。現代顯卡集成了足夠顯存,把頂點數據存放在顯存可以獲得大幅的新能提升:存放在系統內存裏的數據,渲染每一幀時都要拷貝到顯卡,這會帶來極大的損失。只有移除每幀時的這種分配才能幫助我們提高性能。

 

 

使用頂點緩衝(Using Vertex Buffers

Direct3D已經包含了這種機制:頂點緩衝(vertex buffer)。頂點緩衝,就像他名字的意思一樣:一塊儲存頂點的內存。頂點緩衝的機動性能完美實現共享場景裏變經過變換的幾何體。如何讓我們在第一章編寫的三角形程序使用頂點緩衝呢?

創建頂點緩衝同樣簡單,有三個構造函數能完成這個任務,我們依次來看看:

public VertexBuffer( Device device, int sizeOfBufferInBytes, Usage usage, VertexFormats vertexFormat, Pool pool);

public VertexBuffer( Type typeVertexType, int numVerts, Device device, Usage usage,VertexFormats vertexFormat, Pool pool);

 

以下是各參數的意義:

n          device——用來創建頂點緩衝的device,創建的頂點緩衝只能被這個device使用;

n          sizeOfBufferInBytes——所創建的頂點緩衝大小,以字節爲單位。使用帶有這個參數的構造函數創建的頂點緩衝可以存放任何類型的頂點;

n          typeVertexType——如果去要創建的頂點緩衝只儲存一種類型的頂點,則使用這個參數。它的值可以是CustomVertex類中的頂點結構類型,也可以是自定義的頂點類型。且這個值不能爲null

n          numVert——指定了頂點緩衝的儲存類型之後,也必須指定緩衝儲存的頂點數量最大值。這個值必須大於0

n          usage——定義如何使用頂點緩衝。並不會是所有Usage類型的成員都能使用,只有一下幾個是正確的參數:

DoNotClip,Dynamic, Npatches, Points, PTPatches, SoftwareProcessing, WriteOnly;

n          vertexFormat—— 定義儲存在頂點緩衝中的頂點格式。,如果創建的爲通用緩衝的話,則使用VertexFormat.None

n          pool——定位頂點緩衝使用的內存池位置,可以指定一下幾個內存池位置:

Default Managed SystemMemory Scratch

 

觀察第一章中的程序,把三角形的數據移動到頂點緩衝裏應該很容易。首先,申明頂點緩衝變量:

private Device device = null;

private VertexBuffer vb = null;

 

接着添加創建三角形的代碼:

device = new (0,DeviceType.Hardware, this.CreatFlags.softwreVertexProccessing, presentParams);

CustomVertex.positionColored[] verts = new CustomVertex. positionColored[3];

Verts[0].SetPosition(new Vector3(0.0f,1.0f,1.0f));

    Verts[0].Color = System.Drawing.Color.Aqua.ToArgb();

Verts[1]`````````

    Verts[2]`````````

vb = new VertexBuffer(typeof(VustomVertex.PositionColored),2,device,Usage.Dynamic| Usage.WriteOnly, CustomVertex.PositionColored.Format, Pllo.Default);

    vb.SetData(vets,0,LockFlags.None);

 

    唯一的改變就是定義了三角形之後的兩行代碼。首先,創建用來保存三個頂點的頂點緩衝。出於性能上的考慮,創建的緩衝是動態、只讀的並且位於默認的內存池。接下來,我們把三角形的頂點放到緩衝內,使用簡單的SetData方法。這個方法接收任何類型的對象作爲第一個參數,第二個參數是頂點緩衝中所要放置數據地址的便宜量。我們打算填充所有的頂點緩衝,所以設置爲0。最後一個參數描述了當寫入數據時,如何鎖定緩衝。我們將稍後討論鎖存機制;現在,不用關心他是怎樣鎖定的。

     現在編譯程序,很自然,得到了一個編譯錯誤:因爲OnPaint方法裏的DrawUserPrimitives需要獲得verts變量。需要有一個方法告訴Direct3D,我們要繪製頂點緩衝裏的內容,而不是先前所申明的數組。調用deviceSetStreamSourceDirect3D繪圖的時候讀取頂點緩衝。這個方法有以下兩種重載:

     public void SetStreamSource(int streamNumber, VertexBuffer streamData, int offsetInBytes, int stride);

public void SetStreamSource( int streamNumber, VertexBuffer streamData, int offsetInBytes);

 

兩個函數的不同之處在於其中一個多了表示(數據)流步幅大小(stride size of the stream)的參數。第一個參數是這段數據所使用流的數量。現在,把它設置爲0即可;我們會在下一章討論使用多個流。第二個參數是作爲數據源的頂點緩衝,第三個則是頂點緩衝裏需要DirectX繪製的數據的偏移量(以字節爲單位)。stride則是緩衝裏每一個頂點的大小。如果是用特定類型創建的頂點緩衝,則不需要這個參數。

現在修改繪圖的方法:

device.SetStreamSource(0, vb, 0);

device.DrawPrimitives(PrimitiveType.TriangleLise, 0, 1);

 

正如剛纔所描述的,我們把頂點緩衝作爲數據流0,同時把偏移量設置爲0,使用所有數據。值得注意的是,我們同時也改變了真正繪圖的函數。既然所有數據都在頂點緩衝裏了,就不需要調用DrawUserPrimitives方法。因爲DrawUserPrimitives只是用於繪製直接傳遞給它的用戶定義數據。更加通用的DrawPrimitives將會繪製來自數據流源裏的幾何體。DrawPrimitives有三個參數,第一個我們已經討論過了。第二個表示流裏的起始頂點,最後一個表示所要繪製的幾何體個數。

就連這個僅繪製一個三角形的小樣在使用了頂點緩衝之後都帶來了10%的性能提升(基於畫面更新率,即幀頻frame rate)。我們會在稍後幾張來討論有關性能及幀頻。不幸的是,當你嘗試改變窗口大小的時候,三角形會立即消失。(注:偶在實際測試時三角形並米有消失,只是當窗口縮放爲一定比例時,三角形會消失)

有幾種情況會導致這種行爲,其中的兩種我們先前已經討論過了。回想一下上一章,我們知道在改變窗口大小的時候,設備會自動重置。但當所創建的資源位於默認的內存池時(比如頂點緩衝),重置設備會釋緩衝。所以當改變窗口大小的時候,重置了device,釋放了頂點緩衝。Managed DirectX有一個極好的特新就是在重置device之後會自動的重建頂點緩衝。但是,這是頂點緩衝裏已經沒有了數據,所以沒有任何東西被繪製出來。

我們可以捕獲頂點緩衝一個叫做“created”的事件,它會在重建頂點緩衝,準備好填充數據的時候發生。現在是使用這個事件更新我們程序的時候了,修改代碼如下:

private void OnVertexBufferCreate(object sender, EventArgs e)

    {

        VertexBuffer buffer = (VertexBuffer)sender;

CustomVertex.positionColored[] verts = new CustomVertex. positionColored[3];

Verts[0].SetPosition(new Vector3(0.0f,1.0f,1.0f));

        Verts[0].Color = System.Drawing.Color.Aqua.ToArgb();

Verts[1]`````````

        Verts[2]`````````

        buffer.SetData(verts,0,LockFlags.None);

    }

    訂閱事件處理程序:

     vb.Created += new EventHandleer(this.OnVertexBufferCreate);

     OnVertexBufferCreate(vb,null);

     這段代碼爲頂點緩衝訂閱了事件處理程序,並且保證無論在什麼情況下創建頂點緩衝,都會調用OnVertexBufferCreate方法。因爲第一次創建頂點緩衝的時候,還沒有訂閱過處理程序,所以需要手動調用一次。

     好了,通過使用video memory和頂點緩衝,我們已經把原來緩慢的小樣改變爲了一個高效的程序。當然,它還是相當的枯燥。那麼,接下來讓我們創造一個盒子吧。

    

三維場景裏的所有幾何體都是由三角形組成,那麼如何來渲染一個盒子或一個立方體呢?Well,每個立方體由六個正方形構成,而兩個三角形可以構成一個正方形(呵呵,這個都要講,看來老外的數學真的不行)實際上,我們只需要獲得立方體8個頂點的座標就可以了。添加代碼:

CustomVertex.PositionColored[] verts = new CustomVertex.PositionColored[36];

     // Front face

     verts[0] = new CustomVertex.PositionColored(-1.0f, 1.0f, 1.0f, Color.Red.ToArgb());

     verts[2] =````, verts[3] , verts[4], verts[5] =`````````

     // Back face (remember this is facing *away* from the camera, so vertices should be clockwise order)

     verts[6] = new CustomVertex.PositionColored(-1.0f, 1.0f, -1.0f, Color.Blue.ToArgb());

 verts[7] , verts[8], verts[9], verts[10], verts[11]=````````

(注:詳見附件中的源碼,注意頂點申明的順序)

 

正如前面提到的,盒子由12個三角形組成,每個三角形有三個頂點,構成一個頂點集合。還有幾個需要修改的地方

vb = new VertexBuffer(typeof(CustomVertex.PositionColored),36,device,Usage.Dynamic | Usage.WriteOnly,CustomVertex.PositionColored.Format,Pool.Default);

evice.Transform.World = Matrix.RotationYawPitchRoll(angle/(float)Math.PI, angle/(float)Math.PI*2.0f, angle/(float)Math.PI);

device.DrawPrimitives(PrimitiveType.TriangleList,0,12);

 

這裏最大的改變就是重新定義了頂點緩衝的大小。同時,我們也改變了盒子的旋轉角度,讓他轉的更瘋狂一點。最後改變所要渲染的圖元數量。實際上,既然盒子完全是三維的,就沒有必要看到他的背面。使用Direct3D裏的默認剔除模式(逆時針):刪除前面申明剔除模式的行。好了現在運行程序。

非常了不起,我們現在有了一個在屏幕中瘋狂旋轉的彩色盒子。但是如果需要渲染一系列盒子的話,沒有人希望申明一系列頂點緩衝吧。有一個簡單的方法可以做到這一點。

現在我們要肩並肩的繪製三個盒子。由於現在的攝像機設置讓第一個盒子佔慢了整個屏幕,我們需要把他攝像機稍稍往後移一點:

device.Transform.View = Matrix.LookAtLH(new Vector3(0,0,18.0f),new Vector3(),new Vector3(0,1,0));

 

如你所見,我們只是把他往後移了一點點就可以看到更多場景。爲了繪製更多的盒子,我們可以再次利用現有的頂點緩衝,只需要告訴Direct3D再次繪製同樣的頂點就可以了。在device.DrawPrimitives之後添加一下代碼:

device.Transform.World = Matrix.RotationYawPitchRoll(angle/(float)Math.PI, angle/(float)Math.PI/2.0f, angle/(float)Math.PI*4.0f) *Matrix.Translation(5.0f,0.0f,0.0f);

device.DrawPrimitives(PrimitiveType.TriangleList,0,12);

device.Transform.World = Matrix.RotationYawPitchRoll(angle/(float)Math.PI, angle/(float)Math.PI*4.0f, angle/(float)Math.PI/2.0f)*Matrix.Translation(-5.0f,0.0f,0.0f);

device.DrawPrimitives(PrimitiveType.TriangleList,0,12);

    

     好了,這次我們又作了些什麼呢?因爲繪製第一個盒子時已經設置過VertexFormat屬性,所以Direct3D知道將要繪製的頂點類型。同樣,它也知道在哪裏獲得數據。那麼繪製第二個盒子Direct3D還需要知道什麼呢?只需要繪製的位置和繪製什麼就可以了。

     設置world transform可以把數據從局部座標(object space)“移動”到世界座標(world space),那麼把什麼用作變換矩陣呢?首先,使用類似SetupCamera函數裏的方法;做一點點改變,讓盒子以不同的角度旋轉。然而 world transform裏的另一半則是新內容:把一個Matrix.Translation與現有的旋轉矩陣相乘。變換矩陣可以把空間中的一個點移動到另一個位置。我們的變換矩陣把第二個盒子向坐移動了5個單位,第三個盒子則向右移動了5個單位。

     需要注意的是兩個變換矩陣相乘得到的累積效果,是由相乘時矩陣的順序來決定的。在這裏,我們的先旋轉盒子,然後再移動。如果先移動再選旋轉,那麼結果將有很大區別。記住變換時的順序是很重要的。

 

 

爲對象添加紋理

   雖然使用顏色和燈光來渲染很有趣,但僅使用這樣的技術,對象看起來並不真實。在非三維的程序裏“紋理(texture)”通常用來描述對象的粗糙程度(roughness of an object)。三維場景裏的紋理就是一張用來模擬幾何圖元紋理的2D位圖。Direct3D可以同時爲每一個圖元渲染8層紋理,但現在,我們只解決每個圖原一張紋理的情況。因爲Direct3D使用普通的位圖作爲它的紋理格式,任何加載的位圖都能當作紋理對象。 如何把2D的紋理映射到3D的對象上呢?繪製到場景中的每個對象都有一個可以在光柵化時把每個texel映射到屏幕特定位置的紋理座標。textltexture element的縮寫,或者表示紋理中每個address的特定顏色值。Address可以想象爲一個表示行和列的數字,分別稱爲UV座標。一般來說,這些值都是標量,取值範圍從0.01.0 。(00)表示紋理的左上角,(1,1)表示右下角,中央的座標爲(0.5,0.5)。

     爲了使用紋理來渲染盒子,必須改變盒子的頂點格式,以及傳遞給圖形卡的數據。使用紋理座標來代替頂點數據中的“color”元素。雖然同時使用顏色和紋理都是有效的,當作爲練習,我們只用紋理來定義圖元的顏色。修改代碼:

     CustomVertex.PositionTextured[] verts = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured[36];

    verts[0] = new CustomVertex.PositionTextured(-1.0f,1.0f,1.0f,0.0f,0.0f);

    vert[1]```````````()

    

     顯然,最大的改變就是儲存頂點集合的數據類型。每個頂點中的最後兩個float值儲存了渲染圖元所用的紋理UV值。應爲盒子每個面和紋理都爲正方形,所以直接把紋理映射到每個面就可以了。注意,圖元的左上角映射紋理的(00textl,右下角映射到(11textl。同時,我們還必須修改創建頂點緩衝的地方:

vb = new VertexBuffer(typeof(CustomVertex.PositionTextured),36,device,Usage.Dynamic|Usage.WriteOnly, CustomVertex.PositionTextured.Format,Pool.Default);

    

有如此多的重複代碼,現在讓我們用一個簡單的方法來繪製盒子,添加一個函數完成這個任務。

private void DrawBox(float yaw, float potch, float roll, float x, float y,float z,texture t)

    {

        ngle += 0.01f;

        device.Transform.World = Matrix.RotationYawPitchRoll(yaw,pitch,roll)*Matrix.Translation(x,y,z);

        device.SetTexture(0,t);

        device.DrawPrimitives(PrimitiveType.TriangleList,0,12);

}

 

前六個參數和我們之前使用的一樣,最後一個新的參數表示渲染時所使用的紋理。我們還調用了SetTexture方告訴Direct3D渲染時使用哪個紋理。它的第一個參數是這張紋理的“層(stage)”。還記得先前我提過可以爲一個圖元渲染8層紋理嗎,這個參數就是這些紋理的索引。因爲只有一張紋理,我們使用第一個索引,0。同時應該注意到,我們修改了angle變量以及world transform,可以把SetupCamera裏同樣的幾行刪了。

在調用新方法渲染之前,先要申明一些將要使用的紋理,源碼裏附帶了一個包含三張紋理的資源文件。分別爲puck.bmp,ground.bmp,banana.bmp。添加如下代碼:

private Texture tex = null;

     private Texture tex1 = null;

     private Texture tex2 = null;

 

     這是我們即將使用的三張紋理。但是,還需要真正“裝配”起作爲資源嵌入的三張位圖。在創建頂點緩衝之後添加如下代碼:

tex = new Texture(device,new Bitmap(this.GetType(),"puck.bmp"),0,Pool.Managed);

    tex1 =·····(略)

    

     Texture的構造函數接受四個參數。第一個是用於渲染紋理的device。場景裏所有的資源(紋理,頂點緩衝,等等)都要和device發生聯繫。下一個參數Bitmap是我們獲取紋理數據的地方。第三個參數Usage,先前已經討論過它。最後一個參數是儲存紋理的內存池位置。方便起見,現在使用託管的內存池。Texture其他的構造函數包括:

     (注:此處略去一個在DX9c中已經不存在的構造函數)

     public Texture( Device device, int width, int height, int numLevels,Usage usage, Format format, Pool pool);

public Texture( Device device, Stream data, Usage usage,Pool pool);

 

第一個方法允許我們從“空白”開始創建一張紋理,可以指定它的高度、寬度,細節程度(number of levels of detail)。最後一個和我們使用的很相似,但使用流而不是位圖對象。當然,流中的數據要能被轉換爲位圖。TextureLoad類中還有一些關於加載位圖的有趣方法,我們將在下一張討論。

好了,現在已經定義且加載了位圖,是更新繪圖代碼的時候了,使用如下代碼代替先前的繪圖代碼;

DrawBox(angle / (float)Math.PI, angle / (float)Math.PI * 2.0f, angle / (float)Math.PI / 4.0f, 0.0f, 0.0f, 0.0f, tex);

     DrawBox(angle / (float)Math.PI, angle / (float)Math.PI / 2.0f, angle / (float)Math.PI * 4.0f, 5.0f, 0.0f, 0.0f, tex1);

     DrawBox(angle / (float)Math.PI, angle / (float)Math.PI * 4.0f, angle / (float)Math.PI / 2.0f, -5.0f, 0.0f, 0.0f, tex2);

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