C# 從零開始寫 SharpDx 應用 畫三角
在當前的畫面都是使用三角形,在開始就告訴大家如何畫三角,本文告訴大家如何用畫素著色器畫
本文是 SharpDX 系列部落格,更多部落格請點選SharpDX 系列
頂點
為了建立三角形,需要使用頂點。頂點就是在 3D 空間的點。通過頂點可以新增資料,很多使用的頂點都使用三個值,就是 xyz 來表示點在三維空間。大家都知道三角形有三個頂點,所以下面來建立三個頂點。
這裡的頂點的範圍是 0-1,所以可以使用下面程式碼創建出頂點
private Vector3[] _vertices = new Vector3[]
{new Vector3(-0.5f, 0.5f , 0.0f), new Vector3(0.5f, 0.5f, 0.0f), new Vector3(0.0f, -0.5f, 0.0f)};
這時會發現 Vector3 沒有定義,因為沒有安裝SharpDX.Mathematics
,如果使用的是 VisualStudio 2017 格式,那麼複製下面程式碼放在專案
<PackageReference Include="SharpDX.Mathematics" Version="3.1.1" />
如果不是就開啟 Nuget 安裝 SharpDX.Mathematics ,安裝之後引用using SharpDX
就可以使用這個類
頂點快取
現在的頂點資訊放在了記憶體,因為使用了上面程式碼建立。但是渲染的物件是在顯示卡,需要把記憶體的頂點資訊複製到顯示卡。為了做這個需要使用快取。在 DX ,可以使用快取,dx 會自動複製資訊到顯示卡。
下面使用快取來存放頂點資訊,這樣就會在使用資訊自動複製到顯示卡。先寫一個私有變數,通過這個變數把資訊放在快取,請看下面
private D3D11.Buffer _triangleVertexBuffer;
寫一個函式用來把 _vertices
轉換 _triangleVertexBuffer
,程式碼很簡單
private void InitializeTriangle()
{
_triangleVertexBuffer = D3D11.Buffer.Create<Vector3>(_d3DDevice, D3D11.BindFlags.VertexBuffer, _vertices);
}
這個函式需要在構造使用
// 其他被忽略的程式碼
public KikuSimairme()
{
_renderForm = new RenderForm();
_renderForm.ClientSize = new Size(Width, Height);
InitializeDeviceResources();
InitializeTriangle();
}
現在使用D3D.Buffer.Create
建立新的快取,這裡的Vector3
實際可以不需要傳。第一個引數 Direct3D 裝置就是建立資源的裝置,表示快取會在哪個裝置使用。第二個引數就是希望建立的型別,這裡寫的是頂點快取,這裡用的是 VertexBuffer ,除了這個還有 Constant buffer 和 IndexBuffer 。constant表明了constant buffer中的資料,在一次draw call的執行過程中都是不變的,用來從 CPU 傳資料到 GPU。而IndexBuffer是儲存索引編號的緩衝區。關於 Constant Buffer 請看Constant Buffer的高效使用,讓你碼出質量
注意快取是需要去掉
// 其他被忽略的程式碼
public void Dispose()
{
_renderTargetView.Dispose();
_swapChain.Dispose();
_d3DDevice.Dispose();
_d3DDeviceContext.Dispose();
_renderForm?.Dispose();
_triangleVertexBuffer.Dispose();
}
畫素著色器
為了畫出三角形,需要使用頂點著色器和畫素著色器。
使用這兩個著色器因為頂點著色器負責加工頂點集合,可以用來做變換,如移動旋轉頂點。而畫素著色器負責每個畫素,如何畫出每個畫素和紋理。
定義兩個私有變數,表示兩個著色器
private D3D11.VertexShader _vertexShader;
private D3D11.PixelShader _pixelShader;
建立的著色器需要使用 D3DCompiler 編譯著色器檔案,編譯檔案的速度很快
using SharpDX.D3DCompiler;
// 其他被忽略的程式碼
private void InitializeShaders()
{
using (var vertexShaderByteCode = ShaderBytecode.CompileFromFile("VertexShader.hlsl", "main", "vs_4_0", ShaderFlags.Debug))
{
_vertexShader = new D3D11.VertexShader(_d3DDevice, vertexShaderByteCode);
}
using (var pixelShaderByteCode = ShaderBytecode.CompileFromFile("PixelShader.hlsl", "main", "ps_4_0", ShaderFlags.Debug))
{
_pixelShader = new D3D11.PixelShader(_d3DDevice, pixelShaderByteCode);
}
}
可以從程式碼發現使用了兩個檔案,所以接下來就需要建立兩個檔案,這兩個檔案使用的是 hlsl 來寫,關於 hlsl 不屬於本文的內容,所以沒有詳細告訴大家,建議複製一下程式碼。這裡建立了著色器需要使用下面程式碼進行設定
// 其他被忽略的程式碼
_d3DDeviceContext.VertexShader.Set(_vertexShader);
_d3DDeviceContext.PixelShader.Set(_pixelShader);
_d3DDeviceContext.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList;
現在的 InitializeShaders 方法看起來就是如下
private void InitializeShaders()
{
using (var vertexShaderByteCode = ShaderBytecode.CompileFromFile("VertexShader.hlsl", "main", "vs_4_0", ShaderFlags.Debug))
{
_vertexShader = new D3D11.VertexShader(_d3DDevice, vertexShaderByteCode);
}
using (var pixelShaderByteCode = ShaderBytecode.CompileFromFile("PixelShader.hlsl", "main", "ps_4_0", ShaderFlags.Debug))
{
_pixelShader = new D3D11.PixelShader(_d3DDevice, pixelShaderByteCode);
}
_d3DDeviceContext.VertexShader.Set(_vertexShader);
_d3DDeviceContext.PixelShader.Set(_pixelShader);
_d3DDeviceContext.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList;
}
這裡還使用 PrimitiveTopology
設定如何畫出來,更多請看Primitive Topologies
因為用到了兩個特殊的檔案,現在右擊專案新增兩個文字。
然後建立一個文字檔案,注意文字的名字,一個是 PixelShader.hlsl 另一個是 VertexShader.hlsl ,需要點選新建項才可以建立文字。為什麼需要使用文字,因為這樣編譯選項就不需要自己選
現在就建立了兩個檔案,請看自己的工程是否存在下面兩個檔案
現在需要右擊兩個檔案 PixelShader.hlsl
和 VertexShader.hlsl
屬性,選擇輸出
開啟 VertexShader.hlsl
並且寫入下面程式碼
float4 main(float4 position : POSITION) : SV_POSITION
{
return position;
}
上面程式碼就是建立一個 main 函式,寫法和 C 差不多,具體的意思在這裡不會告訴大家,因為關於這個的寫法是很複雜,這裡複製就好
開啟 PixelShader.hlsl
輸入下面程式碼
float4 main(float4 position : SV_POSITION) : SV_TARGET
{
return float4(1.0, 0.0, 0.0, 1.0);
}
這裡也不解釋程式碼的意思
開啟 KikuSimairme 類,在建構函式新增 InitializeShaders 初始化
// 其他被忽略的程式碼
public KikuSimairme()
{
_renderForm = new RenderForm();
_renderForm.ClientSize = new Size(Width, Height);
InitializeDeviceResources();
InitializeTriangle();
InitializeShaders();
}
而且在清理的時候需要關閉 _vertexShader
,請看程式碼
public void Dispose()
{
// 其他被忽略的程式碼
_vertexShader.Dispose();
_pixelShader.Dispose();
}
如果在var pixelShaderByteCode = ShaderBytecode.CompileFromFile("PixelShader.hlsl", "main", "ps_4_0", ShaderFlags.Debug)
出現 System.IO.FileNotFoundException
,那麼就是 PixelShader.hlsl
右擊屬性沒有輸出到和 exe 相同的資料夾
輸入層
現在已經有了頂點快取和頂點資料。但是 DirectX 同樣需要知道資料的結構和每個頂點型別,所以需要使用輸入層。建立輸入層需要兩步,首先需要描述每個頂點,然後從頂點建立輸入層。
因為這裡就使用一個頂點集合,所以只需要建立一個輸入元素集合
private D3D11.InputElement[] _inputElements = new D3D11.InputElement[]
{
new D3D11.InputElement("POSITION", 0, Format.R32G32B32_Float, 0)
};
這裡的 POSITION
可以在 shader 的程式碼被識別,這個字串就是語義,用於匹配輸入的材質的簽名。第二個引數 0 就是語義槽的使用,表示使用哪個,在有多個POSITION
語義的例子才使用。第三個引數就是資料的型別,使用的元素是包括三個浮點數,所以使用 Float
,還記得為什麼是三個浮點數?原因在三維的空間使用三個浮點數可以表示一個點。
在剛才的初始化函式獲取簽名,通過編譯的程式碼
// 其他被忽略的程式碼
private void InitializeShaders()
{
ShaderSignature inputSignature;
using (var vertexShaderByteCode = ShaderBytecode.CompileFromFile("VertexShader.hlsl", "main", "vs_4_0", ShaderFlags.Debug))
{
inputSignature = ShaderSignature.GetInputSignature(vertexShaderByteCode);
// 其他被忽略的程式碼
}
// 其他被忽略的程式碼
}
建立輸入層的私有變數,建立輸入層需要輸入簽名和輸入元素
private D3D11.InputLayout _inputLayout;
private ShaderSignature _inputSignature;
private void InitializeShaders()
{
using (var vertexShaderByteCode =
ShaderBytecode.CompileFromFile("VertexShader.hlsl", "main", "vs_4_0", ShaderFlags.Debug))
{
_inputSignature = ShaderSignature.GetInputSignature(vertexShaderByteCode);
_vertexShader = new D3D11.VertexShader(_d3DDevice, vertexShaderByteCode);
}
_inputLayout = new D3D11.InputLayout(_d3DDevice, _inputSignature, _inputElements);
_d3DDeviceContext.InputAssembler.InputLayout = _inputLayout;
// 其他被忽略的程式碼
}
建立的程式碼第一個引數就是剛才使用的 D3D 裝置,第二個就是剛才的輸入簽名,最後一個就是輸入元素。
這裡建立了一個私有變數,最後還是需要去掉他
public void Dispose()
{
// 其他被忽略的程式碼
_inputLayout.Dispose();
_inputSignature.Dispose();
}
設定 ViewPort
在開始畫之前需要先設定 ViewPort ,在 DirectX 使用的座標是 Normalized Device Coordinates 左上角是 ,右下角是 ,建立私有變數用來放 ViewPort 程式碼
private Viewport _viewport;
private void InitializeDeviceResources()
{
// 其他被忽略的程式碼
_viewport = new Viewport(0, 0, Width, Height);
_d3DDeviceContext.Rasterizer.SetViewport(_viewport);
}
畫出頂點
在 Draw 畫出頂點
private void Draw()
{
_d3DDeviceContext.OutputMerger.SetRenderTargets(_renderTargetView);
_d3DDeviceContext.ClearRenderTargetView(_renderTargetView, ColorToRaw4(Color.Coral));
_d3DDeviceContext.InputAssembler.SetVertexBuffers(0,
new D3D11.VertexBufferBinding(_triangleVertexBuffer, Utilities.SizeOf<Vector3>(), 0));
_d3DDeviceContext.Draw(_vertices.Length, 0);
_swapChain.Present(1, PresentFlags.None);
RawColor4 ColorToRaw4(Color color)
{
const float n = 255f;
return new RawColor4(color.R / n, color.G / n, color.B / n, color.A / n);
}
}
上面程式碼 SetVertexBuffers 是告訴 _d3DDeviceContext
使用頂點快取,第二個引數就是告訴每個頂點的長度
使用 _d3DDeviceContext.Draw
可以從頂點快取畫出,第二個引數就是指定畫出的偏移,從那個頂點開始畫,第一個引數是畫多少個。如輸入 3,2
就是從第2個開始畫三個
執行程式碼
感謝三千提供圖片
我搭建了自己的部落格 https://lindexi.gitee.io/ 歡迎大家訪問,裡面有很多新的部落格。只有在我看到部落格寫成熟之後才會放在csdn或部落格園,但是一旦釋出了就不再更新
如果在部落格看到有任何不懂的,歡迎交流,我搭建了 dotnet 職業技術學院 歡迎大家加入