DrawPrimitiveUP 與 DrawPrimitive

在D3D中,一共有三種基本圖元,分別是點、線和三角形。點是最簡單的圖元,由它可以構成一種叫點列(point list)的圖元類型。線是由兩個不重合的點構成的,一些不相連的線組成的集合就叫線列(line list),而一些首尾相連但不形成環路的線的集合就叫線帶(line strips)。同理,單獨的三角形集合就叫三角形列(triangle list),類似於線帶的三角形集合就叫三角形帶(triangle strips),另外,如果多個三角形共用一個頂點作爲它們的一個頂點的話,那麼這個集合就叫三角形扇(triangle fans)。還是畫圖比較容易理解吧:   
   這些圖元有什麼用呢?基本上我們可以使用這些圖元來畫我們想要的任何物體。例如畫一個四方形可以使用三角形帶來畫,畫一個圓則使用三角形扇。

   現在介紹一種不需要頂點緩衝來渲染的方法,就是使用IDirect3DDevice9::DrawPrimitiveUP函數。UP就是User Pointer的意思,也即是說要使用用戶定義的內存空間。
  HRESULT DrawPrimitiveUP(
   D3DPRIMITIVETYPE PrimitiveType,
   unsigned int PrimitiveCount,
   const void *pVertexStreamZeroData,
   unsigned int VertexStreamZeroStride
  );  
   PrimitiveType:要繪畫的圖元的種類。就是上面介紹的那六種類型。
   PrimitiveCount:要繪畫的圖元的數量。假設有n個頂點信息,繪畫的圖元類型是點列的話,那麼圖元的數量就是n;如果繪畫的圖元類型是線列的話,那麼圖元的數量就是n/2;如果是線帶的話就是n-1;三角形列就是n/3;三角形帶就是n-2;三角形扇出是n-2。
  pVertexStreamZeroData:存儲頂點信息的數組指針
  VertexStreamZeroStride:頂點的大小


  。使用頂點緩衝來繪畫圖元
  很多時候我們使用頂點來定義圖形之後,就把這些頂點信息放進頂點緩衝裏面,然後再進行渲染。使用點頂緩衝的好處以及如何創建頂點緩衝我已經在上一章已講過了,現在講講怎麼把頂點緩衝裏面的圖元給畫出來。其實也很簡單,和上面的IDirect3DDevice9::DrawPrimitiveUP函數差不多,我們使用IDirect3DDevice9::DrawPrimitive函數。不過在使用這個函數之前,我們得告訴設備我們使用哪個數據源,使用IDirect3DDevice9::SetStreamSource函數可以設定數據源。
  HRESULT SetStreamSource(
   UINT StreamNumber,
   IDirect3DVertexBuffer9 *pStreamData,
   UINT OffsetInBytes,
   UINT Stride
  );  
   StreamNumber:設置和哪個數據流梆定。如果使用單數據流的話,這裏設爲0。最多支持16個數據流。
  pStreamData:要綁定的數據。也就是我們創建的頂點緩衝區裏面的數據。
  OffsetInBytes:設置從哪個字節開始讀起。如果要讀整個緩衝區裏面的數據的話,這裏設爲0。
  Stride:單個數據元素的大小。如果數據源是頂點緩衝的話,那麼這裏就是每個頂點信息的大小(Sizeof(vertex))。
  設置好數據源後,就可以使用IDirect3DDevice9::DrawPrimitive來繪畫了。
  HRESULT DrawPrimitive(
   D3DPRIMITIVETYPE PrimitiveType,
   unsigned int StartVertex,
   unsigned int PrimitiveCount
  );  
  PrimitiveType:要繪畫的圖元的種類。
  StarVertex: 設置從頂點緩衝區中的第幾個頂點畫起。沒有特殊情況當然是想把全部的頂點畫出來啦,所以一般這裏設置從0開始。
  PrimitiveCount:要繪畫的圖元的數量。



最初只因DXSDK文檔裏說了句推薦用Vertex Buffer而不要用DrawPrimitiveUP(C#裏叫DrawUserPrimitive),DrawPrimitiveUP很快被描繪成傳說中的瘟疫,人人都在警告不要接近它。估計有人會想過,既然DrawPrimitiveUP這麼不好,爲什麼還要提供它,難道只是爲了顯示DX也可以像OpenGL一樣簡單地畫三角形?

記得當年我就是抱着這種想法在網上狂搜,功夫不負有心人,還真找到了。不過看了很不好意思,人家上來就批判不實際測試、以訛傳訛的問題,我也比較懶,沒動手測一下。那麼爲了和我一樣的懶人,我把問題用中文解釋一遍。

首先,DrawPrimitiveUP內部其實就是一個dynamic vertex buffer(動態頂點緩衝),和我們自己實現一個動態頂點緩衝沒區別。一般情況下,DrawPrimitiveUP和用動態頂點緩衝的效率也沒多大區別。也就是說DrawPrimitiveUP其實很高效的,而且簡單易用。Irrlicht引擎幾乎所有繪製都用的DrawPrimitiveUP,也很快的。

那爲什麼不推薦用?

原因一:DX8發佈時顯存容量已經有了很大提高,靜態頂點緩衝可以緩存在顯存或AGP內存裏,從而節省帶寬佔用。所以推薦能用靜態頂點緩衝的一定要用靜態的。靜態的可以比DrawPrimitiveUP和動態頂點緩衝都快很多。

原因二:DrawPrimitiveUP相對動態頂點緩衝而言,需要將用戶內存裏的頂點數據複製到內部動態頂點緩衝,即多了一次複製,如果頂點數量較大,複製開銷也會加大。但很多程序裏的動態緩衝設計並不太好,爲了抽象或方便使用,也會複製一次數據。所以便喪失了這條優勢。

原因三:這個比較複雜,我們知道一幀內Batch(批)的數量直接影響CPU的佔用率,1G處理器30FPS下每幀700Batch左右就會佔用100%CPU。每個設置設備狀態到發出繪製命令的轉換都將產生一個Batch。動態頂點緩衝的推薦使用模式是一個可以合併Batch的模式,即不斷地填充頂點數據,但不立刻繪製,在緩衝填滿時才提交繪製一次,當然能合併的前提是各個batch都使用相同的設備狀態,即紋理、材質、RenderStates、變換矩陣等。

原因四:DrawPrimitiveUP只支持一個頂點流。這其實是個不算是原因的原因。當然是只用一個頂點流時才用它。

綜上所述,這其實是個優化問題,用動態頂點緩衝有可能做更多的優化,但如果做得不好,會比DrawPrimitiveUP差。如果正確使用了,但沒有進一步的優化或者引擎的用法不具備可優化的特性,那麼也就和DrawPrimitiveUP效率相當。

但事實上用動態頂點緩衝做錯了的也很多,最常見的就是沒有正確使用Lock標誌位,用鎖定靜態緩衝的方法鎖定,根本得不到動態緩衝的效果。另外用C#和MDX的,如果用返回數組的Lock方法重載,也完全沒有意義,因爲在內部整個緩衝被複制到數組,Unlock時再複製回去。即使用GraphicsStream寫頂點數據也很慢,因爲會導致大量的Boxing,只有直接用指針寫數據才能發揮動態緩衝的優勢。

怎麼纔算做得好,DXSDK裏有明確的樣例,爲了懶人,我帖出來:
    // 用法 1
    // 每次繪製拋棄整個頂點緩衝內容並重新填充幾千個頂點
    // 可能包含多個物體,有可能需要按設備狀態分幾次DrawPrimitive

    // 計算需要填充的字節數
    UINT nSizeOfData = nNumberOfVertices * m_nVertexStride;

    // 拋棄並重新填充
    CONST DWORD dwLockFlags = D3DLOCK_DISCARD;
   
    // 鎖定頂點緩衝內存
    BYTE* pBytes;
    if( FAILED( m_pVertexBuffer->Lock( 0, 0, &pBytes, dwLockFlags ) ) )
        return false;
   
    // 將頂點數據複製到頂點緩衝
    memcpy( pBytes, pVertices, nSizeOfData );
    m_pVertexBuffer->Unlock();

    // 繪製
    m_pDevice->DrawPrimitive( D3DPT_TRIANGLELIST, 0, nNumberOfVertices/3)

    // 用法 2
    // 對多個物體複用一個頂點緩衝

    // 計算需要填充的字節數
    UINT nSizeOfData = nNumberOfVertices * m_nVertexStride;

    // 如果頂點緩衝內的剩餘空間可以容納要填充的頂點數量,則指定不覆蓋原有數據
    DWORD dwLockFlags = D3DLOCK_NOOVERWRITE;
   
    // 檢查頂點緩衝空間是否用光
    if( m_nNextVertexData > m_nSizeOfVB - nSizeOfData )
    {
        // 沒有足夠的空間,拋棄原有數據重新開始
        dwLockFlags = D3DLOCK_DISCARD;
        m_nNextVertexData = 0;
    }
   
    // 鎖定頂點緩衝內存
    BYTE* pBytes;
    if( FAILED( m_pVertexBuffer->Lock( (UINT)m_nNextVertexData, nSizeOfData,
               &pBytes, dwLockFlags ) ) )
        return false;
   
    // 將頂點數據複製到頂點緩衝
    memcpy( pBytes, pVertices, nSizeOfData );
    m_pVertexBuffer->Unlock();

    // 繪製
    m_pDevice->DrawPrimitive( D3DPT_TRIANGLELIST,
               m_nNextVertexData/m_nVertexStride, nNumberOfVertices/3)

    // 計算下一次的寫入位置
    m_nNextVertexData += nSizeOfData;

當然,這只是個正確用法樣例,優化起來可還是會面目全非的。如果你的D3D功夫不夠劍豪劍聖級別,大可安心地用DrawPrimitiveUP,對付一些雜碎三角面,也大可不必殺雞用牛刀,用DrawPrimitiveUP剁幾下就行了。另外注意測試的時候一定要用硬件模式測,軟件模式的結果是完全不同的。




對於頂點數據很少變化的情況下,不推薦使用DrawPrimitiveUP,但是在頂點數據經常變化的時候,或者對程序的性能要求不高的場合下,DrawPrimitiveUP是個不錯的選擇。實際上OpenGL默認的工作模式和DrawPrimitiveUP是一樣的,也沒見性能差多少。

我的GUI系統用的是DrawPrimitiveUP,因爲頂點數據是動態更新的。

DrawPrimitiveUP的內部實現也是創建了一個動態的buffer,然後自動填充調用DrawPrimitive渲染,可以自己做是一樣的,但是系統優化的更好。

有人做過測試,在GUI這種動態更新頂點數據的場合下,使用DrawPrimitiveUP的速度比使用普通的頂點緩衝區要快很多

 

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