前言
從這一部分開始,感覺就像是踏入了無人深空一樣,在之前初學DX11的時候,這部分內容都是基本上跳過的,現在打算重新認真地把它給拾回來。
DirectX11 With Windows SDK完整目錄
歡迎加入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
的值應當盡可能的小。
關於性能上的表現,我根據龍書提供的引用找到了對應的說明文檔:
雖然是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年了,對於那時候的顯卡來說使用幾何着色器可能不是一個很好的選擇,不過當初的顯卡也早已不能和現在的顯卡相提並論了。
注意:
maxvertexcount
的值應當設置到盡可能小的值,因為它將直接決定幾何着色器的運行效率。- 幾何着色器的每次調用最多只能處理1024個標量,對於只包含4D位置向量的頂點來說也只能處理256個頂點。
- 幾何着色器輸入的結構體類型不允許超過128個標量,對於只包含4D位置向量的頂點來說也只能包含32個頂點。
在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 v2
VertexPosHColor 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();
// 初始化所需資源
bool InitAll(ID3D11Device * device);
//
// 渲染模式的變更
//
// 默認狀態來繪制
void SetRenderDefault(ID3D11DeviceContext * deviceContext);
// Alpha混合繪制
void SetRenderAlphaBlend(ID3D11DeviceContext * deviceContext);
// 無二次混合
void SetRenderNoDoubleBlend(ID3D11DeviceContext * deviceContext, UINT stencilRef);
// 僅寫入模板值
void SetWriteStencilOnly(ID3D11DeviceContext * deviceContext, UINT stencilRef);
// 對指定模板值的區域進行繪制,采用默認狀態
void SetRenderDefaultWithStencil(ID3D11DeviceContext * deviceContext, UINT stencilRef);
// 對指定模板值的區域進行繪制,采用Alpha混合
void SetRenderAlphaBlendWithStencil(ID3D11DeviceContext * deviceContext, UINT stencilRef);
// 2D默認狀態繪制
void Set2DRenderDefault(ID3D11DeviceContext * deviceContext);
// 2D混合繪制
void Set2DRenderAlphaBlend(ID3D11DeviceContext * deviceContext);
//
// 矩陣設置
//
void XM_CALLCONV SetWorldMatrix(DirectX::FXMMATRIX W);
void XM_CALLCONV SetViewMatrix(DirectX::FXMMATRIX V);
void XM_CALLCONV SetProjMatrix(DirectX::FXMMATRIX P);
void XM_CALLCONV SetReflectionMatrix(DirectX::FXMMATRIX R);
void XM_CALLCONV SetShadowMatrix(DirectX::FXMMATRIX S);
void XM_CALLCONV SetRefShadowMatrix(DirectX::FXMMATRIX RefS);
//
// 光照、材質和紋理相關設置
//
// 各種類型燈光允許的最大數目
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 SetTexture(ID3D11ShaderResourceView * texture);
void SetEyePos(const DirectX::XMFLOAT3& eyePos);
//
// 狀態開關設置
//
void SetReflectionState(bool isOn);
void SetShadowState(bool isOn);
// 應用常量緩沖區和紋理資源的變更
void Apply(ID3D11DeviceContext * deviceContext) override;
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完整目錄
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。