為DrawPrimitiveUP(DrawUserPrimitive)洗冤 轉載

來源:互聯網
上載者:User

在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的速度比使用普通的頂點緩衝區要快很多

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.