close
文章出處

原文地址:https://mellinoe.wordpress.com/2017/02/08/designing-a-3d-rendering-library-for-net-core/
作者:ERIC MELLINO
翻譯:楊曉東(Savorboard)

第一篇文章請看:http://www.cnblogs.com/savorboard/p/net-core-game-engine.html

在第二篇文章中,我將探索Veldrid,這個庫為Crazy Core的游戲引擎中的所有3D和2D渲染提供支持。我將討論這個庫的作用,我為什么建立它,以及它是如何工作的。

注意:對于本文中討論的一些內容,建議對圖形API有基本的了解。對于初學者,我建議查看下面的示例代碼,以獲得所涉及概念的一般概念。

使用像.NET這樣的托管語言最明顯的好處之一是,您的程序可以立即移植到支持該運行時的任何系統。一旦您開始使用本地原生庫,或者依賴于其他特定于平臺的功能,此優點就會消失。那么,你如何設計一個硬件加速的3D應用程序,它能夠運行在各種操作系統和各種圖形API?好吧,你做一個抽象層,并屏蔽不利的代碼!與任何編程抽象一樣,必須非常仔細地進行權衡以隱藏復雜性,同時仍然保持強大的和表達性的編程模型。有了Veldrid,我有幾個打到的目標和非必須目標:

VELDRID的目標

  • 允許您編寫不綁定到任何特定圖形API的抽象代碼。 提供Direct3D 11和OpenGL 3+的具體實現。

  • 遵循通常的圖形API模式。Veldrid不發明自己的符號或quirkiness(圖形API是足夠多的)。

  • 更快。 不要增加大部分的不必要的開銷。鼓勵在正常呈現循環期間不分配內存的模式,否則分配最小內存。

VELDRID的非必須目標

  • 允許您在不知道3D圖形概念的情況下編程3D圖形。Veldrid的接口比具體的API稍微更抽象,像OpenGL或D3D,但是暴露了相同的概念。

  • 公開單個API的所有功能。通過Veldrid暴露的概念應該可以用所有后端表達; 沒有非常好的理由,不什么應該拋出NotSupportedException。對于相同的概念,不同的性能特征是可以預期的(在允許范圍內),只要行為不是不可觀察的。

特性集

  • 可編程的頂點,片段和幾何著色器
  • 頂點和索引緩沖區,包括多個輸入頂點緩沖區
  • 一個靈活的材料系統,具有頂點布局和著色器變量管理
  • 索引和實例化渲染
  • 可自定義混合,深度模板和光柵化狀態
  • 可定制的幀緩沖區和渲染目標
  • 2D和cubemap紋理

向我展示代碼

現在這一切都很好,但是使用Veldrid的程序實際上是什么樣子?更一般的是:它甚至意味著使用抽象渲染庫?為了幫助展示,我創建了適當命名的“ Veldrid微小演示 ”。讓我們走一遍代碼,看看它是如何工作的。整個項目鏈接到那些誰想要修補它。它使用新的基于MSBuild的工具為.NET核心,所以構建它是容易,快速,萬無一失。

設置窗口


bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
OpenTKWindow window = new SameThreadWindow();
RenderContext rc;
if (isWindows && !args.Contains("opengl"))
{
    rc = new D3DRenderContext(window)
}
else
{
    rc = new OpenGLRenderContext(window);
}
window.Title = "Veldrid TinyDemo";

哇,我們做了一個空白的窗口。驚人!關于“RenderContext”的其他東西是什么?所有這些方法是什么,我用它做什么?簡單地說,RenderContext是表示計算機圖形設備的核心對象。它是允許您創建GPU資源,控制設備狀態和執行低級繪圖操作的對象。

創建設備資源

此演示在屏幕中心繪制旋轉的3D立方體。為了做到這一點,我們需要先創建幾個GPU資源。在Veldrid中,所有圖形資源都使用ResourceFactory創建,可從RenderContext訪問。這些資源對于以前寫過圖形代碼的任何人都會很熟悉。我們需要:

  • 包含多維數據集網格頂點的頂點緩沖區
  • 包含立方體網格索引的索引緩沖區
  • “material”,它是一個含有復合對象的
  • 頂點著色器和片段著色器。
  • 頂點數據的輸入布局的描述。
  • 所使用的全局著色器參數的描述。
VertexBuffer vb = rc.ResourceFactory.CreateVertexBuffer(
    Cube.Vertices,
    new VertexDescriptor(VertexPositionColor.SizeInBytes, 2),
    isDynamic:false);
IndexBuffer ib = rc.ResourceFactory.CreateIndexBuffer(
    Cube.Indices,
    isDynamic: false);

創建一個VertexBuffer,其中包含靜態Cube類及包含簡單的3D多維數據集數據。創建一個IndexBuffer包含立方體網格的靜態索引數據。

DynamicDataProvider<Matrix4x4> viewProjection = new DynamicDataProvider<Matrix4x4>();

DynamicDataProvider是一個簡單的抽象,便于將數據傳輸到全局著色器參數。在這個簡單的例子中,我們只有兩個數據,我們需要發送到頂點著色器:相機的視圖和投影矩陣。為了簡單起見,我將這些組合成一個Matrix4x4。


Material material = rc.ResourceFactory.CreateMaterial(rc,
    "vertex", "fragment",
    new MaterialVertexInput(VertexPositionColor.SizeInBytes,
        new MaterialVertexInputElement(
            "Position", VertexSemanticType.Position, VertexElementFormat.Float3),
        new MaterialVertexInputElement(
            "Color", VertexSemanticType.Color, VertexElementFormat.Float4)),
    new MaterialInputs<MaterialGlobalInputElement>(
        new MaterialGlobalInputElement(
            "ViewProjectionMatrix", MaterialInputType.Matrix4x4, viewProjection)),
    MaterialInputs<MaterialPerOjbectInputElement>.Empty,
    MaterialTextureInputs.Empty);

可以說是示例中最復雜的部分,這創建了上面描述的“material”對象。創建此資源需要這幾個信息:

  • 頂點和片段著色器的名稱。在這種情況下,它們簡單地稱為“vertex”和“fragment”。
  • 頂點輸入數據的每個元素的描述。我們的立方體每頂點只有兩個數據:3D位置和顏色。
  • 全局著色器輸入的說明。如上所述,我們只有一個緩沖區保存一個組合的視圖 - 投影矩陣。

Drawing

現在我們有了所有的GPU資源,我們可以畫出一些東西!在這個演示中,渲染發生在一個非常簡單的循環中。在循環的每次迭代時改變著色器參數,以便給立方體旋轉的外觀。


while(window.Exists)
{
    InputSnapshot  snapshot = window.GetInputSnapshot(); //處理窗口事件。
    rc.ClearBuffer(); //清除屏幕。

    rc.SetViewport(0,0,window.Width,window.Height); //確保視口覆蓋整個窗口,以防它被調整大小。
    float  timeFactor = Environmental.TickCount / 1000f ; //得到粗略的時間估計。
    viewProjection.Data =
        //根據當前時間創建一個旋轉的相機矩陣。
        Matrix4x4.CreateLookAt(
            new  Vector3(2 *(float)Math.Sin(timeFactor),(float)Math.Sin(timeFactor),2 *(float)Math.Cos(timeFactor)
            Vector3.Zero,//總是看世界的起源。
            Vector3UnitY)
        //將它與透視投影矩陣組合。
        * Matrix4x4.CreatePerspectiveFieldOfView(1.05f,(浮動)window.Width / window.Height。5F,10F);
    rc.setVertexBuffer(vb); //附加多維數據集頂點緩沖區。
    rc.SetIndexBuffer(ib); //附加立方體索引緩沖區。
    rc.SetMaterial(material); //附加材料。
    rc.DrawIndexedPrimitives(Cube.Indices.Length); //繪制多維數據集。

    rc.SwapBuffers(); //交換回緩沖區并將場景呈現到窗口。
}}

首先,屏幕被清除,并且視口被設置為覆蓋整個屏幕。早些時候,我說我們將渲染一個“旋轉3D立方體”。更準確地說,雖然,攝影機本身圍繞著坐在世界原點的靜態立方體旋轉。當“ viewProjection.Data ”被賦值時,矩陣值被傳播到頂點著色器的 “viewProjection”變量中。我們將我們先前創建的三個資源綁定到RenderContext,調用DrawIndexedPrimitives,然后交換上下文的后臺緩沖區,它將呈現的場景呈現給窗口。

在上面的代碼中一個明顯的事情是,沒有提到任何具體的圖形API(除了上下文創建)。所有示例代碼都將在OpenGL和Direct3D上工作和運行相同。完整的項目可以在GitHub上的項目頁面上找到 ; 我鼓勵你下載它并且嘗試運行!

場景的背后

在這些調用背后都發生了什么?讓我們用兩個例子深入一點。

VertexBuffer vb = rc.ResourceFactory.CreateVertexBuffer(
    Cube.Vertices,
    new VertexDescriptor(VertexPositionColor.SizeInBytes, 2),
    isDynamic:false);

熟悉OpenGL的人將知道頂點緩沖區存儲在稱為VBO的特殊對象中,熟悉Direct3D的人員使用通用的“緩沖區”來存儲大量不同的東西。當OpenGL后端被要求創建一個VertexBuffer時,它會為你創建一個VBO,填充你的頂點數據,并存儲該緩沖區的輔助信息。Direct3D后端通過創建填充 ID3D11Buffer對象來做同樣的事情。

“VertexBuffer”本身是一個接口,用于顯示對頂點緩沖區有用的操作,例如設置頂點數據,檢索它,以及將緩沖區映射到CPU的地址空間。該Direct3D11和OpenGL此后端的每個返回一個VertexBuffer,一個自己版本衍生的D3DVertexBuffer 或OpenGLVertexBuffer,他們的操作是通過特定的調用到每個這些圖形API的實現。這種相同的模式用于Veldrid中可用的所有圖形資源。

下一個例子是從主渲染循環:

rc.DrawIndexedPrimitives(Cube.Indices.Length); //繪制多維數據集。

具體來說,這是什么?讓我們來看看 OpenGL 的代碼:

public  override  void  DrawIndexedPrimitives(int  count,int  startingIndex)
{
    PreDrawCommand();
    DrawElementsType  elementsType =((OpenGLIndexBuffer)IndexBuffer).ElementsType;
    int  indexSize = OpenGLFormats.GetIndexFormatSize(elementsType);
    GL.DrawElements(_primitiveType,count,elementsType,new  IntPtr(startingIndex * indexSize));
}}

DrawIndexedPrimitives被翻譯成單個呼叫glDrawElements,并且參數被從存儲在RenderContext(原始類型)以及當前綁定的IndexBuffer(索引數據的格式)的狀態中拉出。

Direct3D的后臺做了什么?

public override void DrawIndexedPrimitives(int count, int startingIndex, int startingVertex)
{
    _deviceContext.DrawIndexed(count, startingIndex, startingVertex);
}

該調用簡單地轉換為ID3D11DeviceContext :: DrawIndexed。當Vertex和IndexBuffers綁定到RenderContext時,所有其他相關狀態已經設置。

如果你看了代碼,有一件事你會注意到,雖然大多數圖形資源在Veldrid被返回并且作為接口交換,代碼在每個后端將它們作為強類型的對象。例如,D3D后端總是假定它將傳遞D3DVertexBuffer或D3DShader。這意味著,如果由于某種原因嘗試將OpenGLVertexBuffer傳遞到D3DRenderContext,您將遇到災難性的異常。在帖子結束關于這個設計決定有關于我的想法。

哪些工作正常,哪些不是

庫是如何呈現我所要達到的目標呢?這是相當不錯的事情:

  • API是連貫的,并且暴露了一個好的功能集,同時保持API的封裝。
  • 這些概念是相似的,你可以通常遵循OpenGL或D3D教程,并將這些概念很容易地映射到Veldrid。
  • 在后端代碼中有足夠數量的“API泄漏”可能被黑客攻擊。OpenGL和D3D是相似的,我可以在大多數差異,而不失去大量的功能或速度。
  • 示例:如果幀緩沖區未綁定深度紋理,則OpenGL需要(全局)禁用深度測試。D3D似乎不關心這個,或在內部處理它。因此,當無深度幀緩沖器被綁定時,OpenGL后端禁用全局深度測試狀態,即使當前綁定的深度狀態應該被啟用。這種類型的問題不會泄漏到使用庫的最終用戶,但它確實會使一個干凈的實現變得有點丑。
  • 性能好。這不是“zero-cost abstraction”,但是抽象足夠薄。
  • 單獨的后端能夠跟蹤GPU狀態,延遲或省略沒有效果的呼叫。例如,如果使用相同的頂點數據一個接一個渲染的兩個對象。那么第二個對象對SetVertexBuffer()和SetIndexBuffer()的調用將基本上是無操作的,避免了昂貴的GPU狀態變化。
  • OpenTK和SharpDX都是非常好的,薄的,快速的包裝器為相應的圖形API。在需要時調用它們的開銷很小。
  • 在后端之間切換是微不足道的。該Veldrid RenderDemo 支持在運行OpenGL和Direct3D之間切換(無需重新啟動)。

另一方面,這里是我在使用庫后的幾個我的項目中的幾個最大的問題:

  • 沒有統一的著色器代碼。您需要單獨編寫GLSL和HLSL代碼,這樣做的方式與D3D和OpenGL后端的工作方式相同。這意味著著色器需要暴露相同的輸入(統一/常量緩沖區),相同的頂點布局,相同的紋理輸入等。其他人如何處理?
  • Unity,Xenko:這些使用自定義著色語言。這是一個干凈的解決方案,但是巨大比我做的更復雜。
  • MonoGame,Unreal:自動著色器轉換。這里的方法是根據需要將單個著色器語言翻譯成許多。這可能相當簡單,取決于你愿意接受多少晦澀的語法。
  • 材質規格很詳細。上面的Tiny Demo的例子顯示了創建一個簡單的Material對象的詳細程度。有可能所有必要的信息可以通過著色器反射(使用OpenGL和D3D),但我沒有這樣做。
  • 沒有多線程支持。OpenGL是眾所周知的(不可用的)多線程,但D3D11后端可以很容易地與重新設計的API線程。
  • 資源創建是不尋常的,因為不使用構造函數。如果沒有每個對象中的間接級別,或者使用重新設計的程序集架構,這將很難解決(請參閱“Veldrid v2的想法”中的最后一個要點)。
  • 有一些泄漏到API中的東西應該放到另一個幫助庫中。一個更清潔的設計只會在核心庫中包含非常低級的概念,而其他的則在頂層。

“VELDRID V2” 的一些想法

Veldrid的初始版本對我非常有用,我學到了很多東西。潛在“v2”版本的庫我已經建立了一個很長的改進列表。

對庫的最明顯的改進是添加額外的后端實現。理想情況下,該庫的下一代版本將至少支持OpenGL ES和Vulkan以及現有的D3D11和OpenGL 3+后端。最重要的是,這將給我選擇在iOS和Android上運行,這是目前無法使用D3D或“完整”的OpenGL。實際上,這將是實施最昂貴的功能,但也是最有影響力的。

正如我上面提到的,初始庫的一個明顯的問題是它不支持多線程渲染。像Vulkan這樣的API被明確地設計為用于多線程應用程序, 很明顯,線程是解決現代圖形庫的一個重要問題。在較小的程度上,甚至direct3d11,這已經在Veldrid支持,具有在我的庫中未使用的線程功能。我懷疑這個功能自然會落在下一代設計的支持Vulkan和其他現代圖形API的庫。

我已經在Veldrid的當前版本提到材料的問題,這是一個顯然需要在v2中進行大修的領域。很難說,改進的版本將看起來是什么樣子,像沒有為庫的其余部分設計,但至少它需要減少冗長的代碼,和改進當前版本的一些缺陷。

由于上述特性很可能需要重新構建庫的大部分代碼,我認為另一個核心部分需重新考慮,即在公共API中使用接口和抽象類將是有趣的。Veldrid是一個單個程序集,它包含單個API不可知界面的多個實現。這意味著您可以在運行時而不是部署時決定是使用Direct3D還是OpenGL,還可以在運行時切換API。另一方面,由于涉及接口和虛分派(virtual dispatch),該方法帶有一定級別的運行時開銷。大多數其他3D圖形層使用編譯時專門化,而不是運行時/接口專門化。我想探討是否可以使用替代方法,涉及“誘餌和轉換”技術用于一些PCL項目。自定義AssemblyLoadContext可用于加載使用特定圖形API的特定版本的Veldrid.dll。這將允許您保留當前方法的靈活性,而不需要接口或一些虛分派(virtual dispatch)。

Veldrid是一個在我的GitHub頁面可以獲得的開源項目。它使用新的基于MSBuild 的.NET Core 工具,可以從任何針對.NET Standard 1.5或更高版本的項目中使用。


本文地址:http://www.cnblogs.com/savorboard/p/designing-a-3d-rendering-library-for-net-core.html
本譯文僅用于學習和交流目的。非商業轉載請注明譯者、出處,并保留文章在譯言的完整鏈接。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 AutoPoster 的頭像
    AutoPoster

    互聯網 - 大數據

    AutoPoster 發表在 痞客邦 留言(0) 人氣()