DirectX11 With Windows SDK--15 几何着色器初探
前言
從這一部分開始,感覺就像是踏入了無人深空一樣,在之前初學DX11的時候,這部分內容都是基本上跳過的,現在打算重新認真地把它給拾回來。
DirectX11 With Windows SDK完整目錄
Github項目源碼
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。
幾何著色器
首先用一張圖來回顧一下渲染管線的各個階段,目前為止我們接觸的著色器有頂點著色器和像素著色器,而接觸到的渲染管線階段有:輸入裝配階段、頂點著色階段、光柵化階段、像素著色階段、輸出合并階段。
可以看到,幾何著色器是我們在將頂點送入光柵化階段之前,可以操作頂點的最后一個階段。它同樣也允許我們編寫自己的著色器代碼。幾何著色器可以做如下事情:
但它也有缺點,幾何著色器輸出的頂點數據很可能是有較多重復的,從流輸出拿回到頂點緩沖區的話會占用較多的內存空間。它本身無法輸出索引數組。
幾何著色階段會收到一系列代表輸入幾何體類型的頂點,然后我們可以自由選擇其中的這些頂點信息,然后交給流輸出對象重新解釋成新的圖元類型(或者不變),傳遞給流輸出階段或者是光柵化階段。而幾何著色器僅能夠接受來自輸入裝配階段提供的頂點信息,對每個頂點進行處理,無法自行決定增減頂點。
注意:離開幾何著色器的頂點如果要傳遞給光柵化階段,需要包含有轉換到齊次裁剪坐標系的坐標信息(語義為SV_POSITION的float4向量)
可編程的幾何著色器
從一個看似沒什么用的幾何著色器代碼入手
若我們直接從VS項目新建一個幾何著色器文件,則可以看到下面的代碼:
struct GSOutput {float4 pos : SV_POSITION; };[maxvertexcount(3)] void main(triangle float4 input[3] : SV_POSITION, inout TriangleStream< GSOutput > output ) {for (uint i = 0; i < 3; i++){GSOutput element;element.pos = input[i];output.Append(element);} }ps. 可能有些人會對void main的寫法表示不爽,比如說我。不過這不是C語言的主函數......
若在輸入裝配階段指定使用TriangleList圖元的話,初步觀察該代碼,實際上你可以發現其實該著色器只是把輸入的頂點按原樣輸出給流輸出對象,即跟什么都沒做(咸魚)有什么區別。。不過從這份代碼里面就已經包含了幾何著色器所特有的絕大部分語法了。
首先,幾何著色器是根據圖元類型來進行調用的,若使用的是TriangleList,則每一個三角形的三個頂點都會作為輸入,觸發幾何著色器的調用。這樣一個TriangleList解釋的30個頂點會觸發10次調用。
對于幾何著色器,我們必須要指定它每次調用所允許輸出的最大頂點數目。我們可以使用屬性語法來強行修改著色器行為:
[maxvertexcount(N)]
這里N就是每次調用允許產出的最大頂點數目,然后最終輸出的頂點數目不會超過N的值。maxvertexcount的值應當盡可能的小。
關于性能上的表現,我根據龍書提供的引用找到了對應的說明文檔:
NVIDIA08
雖然是10年前的文檔,這里說到:在GeForce 8800 GTX,一個幾何著色器的調用在輸出1到20個標量的時候可以達到最大運行性能表現,但是當我們指定最大允許輸出標量的數目在27到40個時,性能僅達到峰值的50%。比如說,如果頂點的聲明如下:
struct V0 {float3 pos : POSITION;float2 tex : TEXCOORD; };這里每個頂點就已經包含了5個標量了,如果以它作為輸出類型,則maxvertexcount為4的時候就可以達到理論上的峰值性能(20個標量)。
但如果頂點類型中還包含有float3類型的法向量,每個頂點就額外包含了3個標量,這樣在maxvertexcount為4的時候就輸出了32個標量,只有50%的峰值性能表現。
這份文檔已經將近10年了,對于那時候的顯卡來說使用幾何著色器可能不是一個很好的選擇,不過當初的顯卡也早已不能和現在的顯卡相提并論了。
注意:
在HLSL編譯器里,如果設置的maxvertexcount過大,會直接收到編譯錯誤:
然后代碼中的triangle是用于指定輸入的圖元類型,具體支持的關鍵字如下:
| point | Point list |
| line | Line list or line strip |
| triangle | Triangle list or triangle strip |
| lineadj | Line list with adjacency or line strip with adjacency |
| triangleadj | Triangle list with adjacency or triangle strip with adjacency |
具體的圖元類型可以到第2章回顧:點擊此處
而參數類型可以是用戶自定義的結構體類型,或者是向量(float4)類型。從頂點著色器傳過來的頂點至少會包含一個表示齊次裁剪坐標的向量。
參數名inupt實際上用戶是可以任意指定的。
對于該輸入參數的元素數目,取決于前面聲明的圖元類型:
| point | [1] 每次只能處理1個頂點 |
| line | [2] 一個線段必須包含2個頂點 |
| triangle | [3] 一個三角形需要3個頂點 |
| lineadj | [4] 一個鄰接線段需要4個頂點 |
| triangleadj | [6] 一個鄰接三角形需要6個頂點 |
而第二個參數必須是一個流輸出對象,而且需要被指定為inout可讀寫類型。可以看到,它是一個類模板,模板的形參指定要輸出的類型。流輸出對象有如下三種:
| PointStream | 一系列點的圖元 |
| LineStream | 一系列線段的圖元 |
| TriangleStream | 一系列三角形的圖元 |
流輸出對象都具有下面兩種方法:
| Append | 向指定的流輸出對象添加一個輸出的數據 |
| RestartStrip | 在以線段或者三角形作為圖元的時候,默認是以strip的形式輸出的, 如果我們不希望下一個輸出的頂點與之前的頂點構成新圖元,則需要調用此方法來重新開始新的strip。若希望輸出的圖元類型也保持和原 來一樣的TriangleList,則需要每調用3次Append方法后就調用一次RestartStrip。 |
注意:
在開始前,先放出Basic.hlsli文件的內容:
#include "LightHelper.hlsli"cbuffer CBChangesEveryFrame : register(b0) {matrix g_World;matrix g_WorldInvTranspose; }cbuffer CBChangesOnResize : register(b1) {matrix g_Proj; }cbuffer CBChangesRarely : register(b2) {DirectionalLight g_DirLight[5];PointLight g_PointLight[5];SpotLight g_SpotLight[5];Material g_Material;matrix g_View;float3 g_EyePosW;float g_CylinderHeight; }struct VertexPosColor {float3 PosL : POSITION;float4 Color : COLOR; };struct VertexPosHColor {float4 PosH : SV_POSITION;float4 Color : COLOR; };實戰1: 將一個三角形分割成三個三角形
現在我們的目標是把一個三角形分裂成三個三角形:
這也為以后實現分形做為基礎。建議讀者可以先自行嘗試編寫著色器代碼再來對比。在編寫好著色器代碼后,
要給渲染管線綁定好一切所需的資源才能夠看到效果。
HLSL代碼
Triangle_VS.hlsl, Triangle_GS.hlsl和Triangle_PS.hlsl的實現如下:
// Triangle_VS.hlsl #include "Basic.hlsli"VertexPosHColor VS(VertexPosColor vIn) {matrix worldViewProj = mul(mul(g_World, g_View), g_Proj);VertexPosHColor vOut;vOut.Color = vIn.Color;vOut.PosH = mul(float4(vIn.PosL, 1.0f), worldViewProj);return vOut; } // Triangle_GS.hlsl #include "Basic.hlsli"[maxvertexcount(9)] void GS(triangle VertexPosHColor input[3], inout TriangleStream<VertexPosHColor> output) {//// 將一個三角形分裂成三個三角形,即沒有v3v4v5的三角形// v1// /\// / \// v3/____\v4// /\xxxx/\// / \xx/ \// /____\/____\// v0 v5 v2VertexPosHColor vertexes[6];int i;[unroll]for (i = 0; i < 3; ++i){vertexes[i] = input[i];vertexes[i + 3].Color = (input[i].Color + input[(i + 1) % 3].Color) / 2.0f;vertexes[i + 3].PosH = (input[i].PosH + input[(i + 1) % 3].PosH) / 2.0f;}[unroll]for (i = 0; i < 3; ++i){output.Append(vertexes[i]);output.Append(vertexes[3 + i]);output.Append(vertexes[(i + 2) % 3 + 3]);output.RestartStrip();} } // Triangle_PS.hlsl #include "Basic.hlsli"float4 PS(VertexPosHColor pIn) : SV_Target {return pIn.Color; }這里輸入和輸出的圖元類型都是一致的,但無論什么情況都一定要注意設置好maxvertexcount的值,這里固定一個三角形的三個頂點輸出9個頂點(構成三個三角形),并且每3次Append就需要調用1次RestartStrip。
實戰2: 通過圓線構造圓柱體側面
已知圖元類型為LineStrip,現在有一系列連續的頂點構成圓線(近似圓弧的連續折線),構造出圓柱體的側面。即輸入圖元類型為線段,輸出一個矩形(兩個三角形)。
思路: 光有頂點位置還不足以構造出圓柱體側面,因為無法確定圓柱往哪個方向延伸。所以我們還需要對每個頂點引入所在圓柱側面的法向量,通過叉乘就可以確定上方向/下方向并進行延伸了。
HLSL代碼
Cylinder_VS.hlsl, Cylinder_GS.hlsl和Cylinder_PS.hlsl的實現如下:
// Cylinder_VS.hlsl #include "Basic.hlsli"VertexPosHWNormalColor VS(VertexPosNormalColor vIn) {VertexPosHWNormalColor vOut;matrix viewProj = mul(g_View, g_Proj);float4 posW = mul(float4(vIn.PosL, 1.0f), g_World);vOut.PosH = mul(posW, viewProj);vOut.PosW = posW.xyz;vOut.NormalW = mul(vIn.NormalL, (float3x3) g_WorldInvTranspose);vOut.Color = vIn.Color;return vOut; } // Cylinder_GS.hlsl #include "Basic.hlsli"// 一個v0v1線段輸出6個三角形頂點 [maxvertexcount(6)] void GS(line VertexPosHWNormalColor input[2], inout TriangleStream<VertexPosHWNormalColor> output) {// *****************************// 要求圓線是順時針的,然后自底向上構造圓柱側面 // --> v2____v3// ______ |\ |// / \ | \ |// \______/ | \ |// <-- |___\|// v1(i1) v0(i0)float3 upDir = normalize(cross(input[0].NormalW, (input[1].PosW - input[0].PosW)));VertexPosHWNormalColor v2, v3;matrix viewProj = mul(g_View, g_Proj);v2.PosW = input[1].PosW + upDir * g_CylinderHeight;v2.PosH = mul(float4(v2.PosW, 1.0f), viewProj);v2.NormalW = input[1].NormalW;v2.Color = input[1].Color;v3.PosW = input[0].PosW + upDir * g_CylinderHeight;v3.PosH = mul(float4(v3.PosW, 1.0f), viewProj);v3.NormalW = input[0].NormalW;v3.Color = input[0].Color;output.Append(input[0]);output.Append(input[1]);output.Append(v2);output.RestartStrip();output.Append(v2);output.Append(v3);output.Append(input[0]); } // Cylinder_PS.hlsl #include "Basic.hlsli"float4 PS(VertexPosHWNormalColor pIn) : SV_Target {// 標準化法向量pIn.NormalW = normalize(pIn.NormalW);// 頂點指向眼睛的向量float3 toEyeW = normalize(g_EyePosW - pIn.PosW);// 初始化為0 float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);// 只計算方向光ComputeDirectionalLight(g_Material, g_DirLight[0], pIn.NormalW, toEyeW, ambient, diffuse, spec);return pIn.Color * (ambient + diffuse) + spec; }實戰3: 畫出頂點的法向量
畫出頂點的法向量可以幫助你進行調試,排查法向量是否出現了問題。這時候圖元的類型為PointList,需要通過幾何著色器輸出一個線段(兩個頂點)。由于頂點中包含法向量,剩下的就是要自行決定法向量的長度。
下圖的法向量長度為0.5
HLSL代碼
Normal_VS.hlsl, Normal_GS.hlsl和Normal_PS.hlsl的實現如下:
// Normal_VS.hlsl #include "Basic.hlsli"VertexPosHWNormalColor VS(VertexPosNormalColor vIn) {VertexPosHWNormalColor vOut;matrix viewProj = mul(g_View, g_Proj);float4 posW = mul(float4(vIn.PosL, 1.0f), g_World);vOut.PosH = mul(posW, viewProj);vOut.PosW = posW.xyz;vOut.NormalW = mul(vIn.NormalL, (float3x3) g_WorldInvTranspose);vOut.Color = vIn.Color;return vOut; } // Normal_GS.hlsl #include "Basic.hlsli"[maxvertexcount(2)] void GS(point VertexPosHWNormalColor input[1], inout LineStream<VertexPosHWNormalColor> output) {matrix viewProj = mul(g_View, g_Proj);VertexPosHWNormalColor v;// 防止資源爭奪v.PosW = input[0].PosW + input[0].NormalW * 0.01f;v.NormalW = input[0].NormalW;v.PosH = mul(float4(v.PosW, 1.0f), viewProj);v.Color = input[0].Color;output.Append(v);v.PosW = v.PosW + input[0].NormalW * 0.5f;v.PosH = mul(float4(v.PosW, 1.0f), viewProj);output.Append(v); } // Normal_PS.hlsl #include "Basic.hlsli"float4 PS(VertexPosHWNormalColor pIn) : SV_TARGET {return pIn.Color; }C++代碼的部分變化
BasicEffect的變化
變化如下:
class BasicEffect : public IEffect { public:BasicEffect();virtual ~BasicEffect() override;BasicEffect(BasicEffect&& moveFrom) noexcept;BasicEffect& operator=(BasicEffect&& moveFrom) noexcept;// 獲取單例static BasicEffect& Get();// 初始化Basic.hlsli所需資源并初始化渲染狀態bool InitAll(ID3D11Device * device);//// 渲染模式的變更//// 繪制三角形分裂void SetRenderSplitedTriangle(ID3D11DeviceContext * deviceContext);// 繪制無上下蓋的圓柱體void SetRenderCylinderNoCap(ID3D11DeviceContext * deviceContext);// 繪制所有頂點的法向量void SetRenderNormal(ID3D11DeviceContext * deviceContext);//// 矩陣設置//void XM_CALLCONV SetWorldMatrix(DirectX::FXMMATRIX W);void XM_CALLCONV SetViewMatrix(DirectX::FXMMATRIX V);void XM_CALLCONV SetProjMatrix(DirectX::FXMMATRIX P);//// 光照、材質和紋理相關設置//// 各種類型燈光允許的最大數目static const int maxLights = 5;void SetDirLight(size_t pos, const DirectionalLight& dirLight);void SetPointLight(size_t pos, const PointLight& pointLight);void SetSpotLight(size_t pos, const SpotLight& spotLight);void SetMaterial(const Material& material);void XM_CALLCONV SetEyePos(DirectX::FXMVECTOR eyePos);// 設置圓柱體側面高度void SetCylinderHeight(float height);// 應用常量緩沖區和紋理資源的變更void Apply(ID3D11DeviceContext * deviceContext);private:class Impl;std::unique_ptr<Impl> pImpl; };BasicEffect::SetRenderSplitedTriangle方法--渲染分裂的三角形
該方法處理的是圖元TriangleList。因為后續的方法處理的圖元不同,在調用開始就得設置回正確的圖元。也請確保輸入裝配階段提供好需要分裂的三角形頂點。
void BasicEffect::SetRenderSplitedTriangle(ID3D11DeviceContext * deviceContext) {deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);deviceContext->IASetInputLayout(pImpl->m_pVertexPosColorLayout.Get());deviceContext->VSSetShader(pImpl->m_pTriangleVS.Get(), nullptr, 0);deviceContext->GSSetShader(pImpl->m_pTriangleGS.Get(), nullptr, 0);deviceContext->RSSetState(nullptr);deviceContext->PSSetShader(pImpl->m_pTrianglePS.Get(), nullptr, 0);}BasicEffect::SetRenderCylinderNoCap方法--渲染圓柱側面
該方法處理的是圖元LineStrip,確保輸入的一系列頂點和法向量能夠在同一平面上。若提供的頂點集合按順時針排布,則會自底向上構建出圓柱體,反之則是自頂向下構建。
這里需要關閉背面裁剪,因為我們也可以看到圓柱側面的內部(沒有蓋子)。
void BasicEffect::SetRenderCylinderNoCap(ID3D11DeviceContext * deviceContext) {deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINESTRIP);deviceContext->IASetInputLayout(pImpl->m_pVertexPosNormalColorLayout.Get());deviceContext->VSSetShader(pImpl->m_pCylinderVS.Get(), nullptr, 0);deviceContext->GSSetShader(pImpl->m_pCylinderGS.Get(), nullptr, 0);deviceContext->RSSetState(RenderStates::RSNoCull.Get());deviceContext->PSSetShader(pImpl->m_pCylinderPS.Get(), nullptr, 0);}BasicEffect::SetRenderNormal方法--渲染法向量
該方法處理的圖元是PointList,確保輸入的頂點要包含法向量。
void BasicEffect::SetRenderNormal(ID3D11DeviceContext * deviceContext) {deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST);deviceContext->IASetInputLayout(pImpl->m_pVertexPosNormalColorLayout.Get());deviceContext->VSSetShader(pImpl->m_pNormalVS.Get(), nullptr, 0);deviceContext->GSSetShader(pImpl->m_pNormalGS.Get(), nullptr, 0);deviceContext->RSSetState(nullptr);deviceContext->PSSetShader(pImpl->m_pNormalPS.Get(), nullptr, 0);}GameApp類的變化
該項目包含上面三種實戰內容,需要用戶去指定當前播放的模式。
首先聲明部分變化如下:
class GameApp : public D3DApp { public:enum class Mode { SplitedTriangle, CylinderNoCap, CylinderNoCapWithNormal };public:GameApp(HINSTANCE hInstance);~GameApp();bool Init();void OnResize();void UpdateScene(float dt);void DrawScene();private:bool InitResource();void ResetTriangle();void ResetRoundWire();private:ComPtr<ID2D1SolidColorBrush> m_pColorBrush; // 單色筆刷ComPtr<IDWriteFont> m_pFont; // 字體ComPtr<IDWriteTextFormat> m_pTextFormat; // 文本格式ComPtr<ID3D11Buffer> m_pVertexBuffer; // 頂點集合int m_VertexCount; // 頂點數目Mode m_ShowMode; // 當前顯示模式BasicEffect m_BasicEffect; // 對象渲染特效管理};GameApp::ResetTriangle方法--重設為三角形頂點
void GameApp::ResetTriangle() {// ******************// 初始化三角形//// 設置三角形頂點VertexPosColor vertices[] ={{ XMFLOAT3(-1.0f * 3, -0.866f * 3, 0.0f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) },{ XMFLOAT3(0.0f * 3, 0.866f * 3, 0.0f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },{ XMFLOAT3(1.0f * 3, -0.866f * 3, 0.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) }};// 設置頂點緩沖區描述D3D11_BUFFER_DESC vbd;ZeroMemory(&vbd, sizeof(vbd));vbd.Usage = D3D11_USAGE_IMMUTABLE;vbd.ByteWidth = sizeof vertices;vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;vbd.CPUAccessFlags = 0;// 新建頂點緩沖區D3D11_SUBRESOURCE_DATA InitData;ZeroMemory(&InitData, sizeof(InitData));InitData.pSysMem = vertices;HR(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffer.ReleaseAndGetAddressOf()));// 三角形頂點數m_VertexCount = 3; }GameApp::ResetRoundWire方法--重設為圓線頂點
void GameApp::ResetRoundWire() {// ****************** // 初始化圓線// 設置圓邊上各頂點// 必須要按順時針設置// 由于要形成閉環,起始點需要使用2次// ______// / \// \______///VertexPosNormalColor vertices[41];for (int i = 0; i < 40; ++i){vertices[i].pos = XMFLOAT3(cosf(XM_PI / 20 * i), -1.0f, -sinf(XM_PI / 20 * i));vertices[i].normal = XMFLOAT3(cosf(XM_PI / 20 * i), 0.0f, -sinf(XM_PI / 20 * i));vertices[i].color = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);}vertices[40] = vertices[0];// 設置頂點緩沖區描述D3D11_BUFFER_DESC vbd;ZeroMemory(&vbd, sizeof(vbd));vbd.Usage = D3D11_USAGE_IMMUTABLE;vbd.ByteWidth = sizeof vertices;vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;vbd.CPUAccessFlags = 0;// 新建頂點緩沖區D3D11_SUBRESOURCE_DATA InitData;ZeroMemory(&InitData, sizeof(InitData));InitData.pSysMem = vertices;HR(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffer.ReleaseAndGetAddressOf()));// 線框頂點數m_VertexCount = 41; }GameApp類剩余部分可以在項目源碼中查看。
最終效果如下:
DirectX11 With Windows SDK完整目錄
Github項目源碼
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。
posted on 2019-05-05 09:57 NET未來之路 閱讀(...) 評論(...) 編輯 收藏轉載于:https://www.cnblogs.com/lonelyxmas/p/10811380.html
總結
以上是生活随笔為你收集整理的DirectX11 With Windows SDK--15 几何着色器初探的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 思岚科技邀你2017日本东京国际机器人展
- 下一篇: (翻译)禁用按钮不应变灰的原因