是時候瞭解如何使用著色器和著色器資源來開發適用於 Windows 8 的 Microsoft DirectX 遊戲。 我們已瞭解如何設定圖形裝置和資源,或許您甚至已開始修改其管線設定。 現在讓我們看看像素和頂點著色器。
如果您不熟悉著色器語言,我們有必要進行快速討論。 著色器是小型的低階程式,會在圖形管線的特定階段編譯和執行。 其專長是非常快速的浮點數學運算。 最常見的著色器程式如下:
- 頂點著色器— 針對場景中的每個頂點執行。 這個著色器在由呼叫應用程式提供的頂點緩衝區元素上運作,並會產生一個至少包含 4 個組件的定位向量,該向量將被轉換為像素位置。
- 像素著色器— 針對轉譯目標中的每個像素執行。 這個著色器會接收來自先前著色器階段的點陣化座標(在最簡單的管線中,這會是頂點著色器),並傳回該圖元位置的色彩(或其他 4 元件值),然後寫入轉譯目標。
此範例包含非常基本的頂點和像素著色器,這些著色器只會繪製幾何,以及新增基本光源計算的較複雜著色器。
著色器程式是以Microsoft高階著色器語言 (HLSL) 撰寫。 HLSL 語法看起來很像 C,但沒有指標。 著色器程序必須非常精簡且有效率。 如果您的著色器編譯為太多指示,則無法執行,並傳回錯誤。 (請注意,允許的指令確切數目是 Direct3D 功能層級的一部分。
在 Direct3D 中,著色器不會在執行時編譯,而是在編譯程式其餘部分時被編譯。 當您使用visual Studio 2013 Microsoft編譯應用程式時,HLSL 檔案會編譯成 CSO (.cso) 檔案,您的應用程式在繪製之前必須先載入並放在 GPU 記憶體中。 在封裝應用程式時,請務必將這些 CSO 檔案包含在您的應用程式中;它們是資產,就像網格和紋理一樣。
瞭解 HLSL 語意
在繼續之前,請務必花點時間討論 HLSL 語意,因為它們通常是新 Direct3D 開發人員的混淆點。 HLSL 語意是字串,可識別應用程式與著色器程式之間傳遞的值。 雖然這些字串可以是各種可能的字串,但最佳做法是使用表示使用方式的字串,例如 POSITION 或 COLOR。 當您建構常數緩衝區或輸入配置時,會指派這些語意。 您也可以將介於 0 到 7 之間的數位附加至語意,以便針對類似的值使用不同的緩存器。 例如:COLOR0、COLOR1、COLOR2...
前面加上 「SV_」 的語意 系統值 著色器程式所寫入的語意;您的遊戲本身 (在 CPU 上執行) 無法修改它們。 這些語意通常包含來自圖形管線中其他著色器階段之輸入或輸出的值,或是由 GPU 產生。
此外,當 SV_ 語意用於指定著色器階段的輸入或輸出時,其行為會有所不同。 例如,SV_POSITION(輸出)包含頂點著色器階段期間轉換的頂點數據,而 SV_POSITION(輸入)包含 GPU 在點陣化階段內插補的圖元位置值。
以下是一些常見的 HLSL 語意:
- 頂點緩衝區數據的
POSITION(與)。SV_POSITION提供圖元位置給像素著色器,而且無法由您的遊戲撰寫。 -
NORMAL(n) 適用於頂點緩衝區所提供的一般數據。 -
TEXCOORD(n) 用於提供給著色器之紋理 UV 座標數據。 -
COLOR(n)代表提供給著色器的 RGBA 色彩資料。 請注意,其處理方式與協調數據相同,包括在點陣化期間插入值;語意只會協助您識別其為色彩數據。 -
SV_Target[n] 將資料從像素著色器寫入到目標紋理或其他像素緩衝區。
當我們檢閱範例時,我們將會看到 HLSL 語意的一些範例。
從常數緩衝區讀取
如果該緩衝區附加至其階段作為資源,任何著色器都可以從常數緩衝區讀取。 在此範例中,只有頂點著色器會指派常數緩衝區。
常數緩衝區會在兩個位置宣告:在C++程序代碼中,以及將存取它的對應 HLSL 檔案中。
以下是在C++程序代碼中宣告常數緩衝區結構的方式。
typedef struct _constantBufferStruct {
DirectX::XMFLOAT4X4 world;
DirectX::XMFLOAT4X4 view;
DirectX::XMFLOAT4X4 projection;
} ConstantBufferStruct;
在C++程序代碼中宣告常數緩衝區的結構時,請確定所有數據都與16位元組界限正確對齊。 若要這樣做,最簡單的方式是使用 DirectXMath 類型,例如 XMFLOAT4 或 XMFLOAT4X4,如範例程式代碼所示。 您也可以藉由宣告靜態斷言來防範未對齊的緩衝區:
// Assert that the constant buffer remains 16-byte aligned.
static_assert((sizeof(ConstantBufferStruct) % 16) == 0, "Constant Buffer size must be 16-byte aligned");
如果 ConstantBufferStruct 未對齊 16 位元組,這行程式代碼會在編譯時造成錯誤。 如需常數緩衝區對齊和封裝的詳細資訊,請參閱 常數變數的封裝規則。
現在,以下是在頂點著色器 HLSL 中宣告常數緩衝區的方式。
cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
matrix mWorld; // world matrix for object
matrix View; // view matrix
matrix Projection; // projection matrix
};
所有緩衝區──無論是常數、紋理、取樣器或其他類型──都必須要有一個已定義的暫存器,以便 GPU 可以存取它們。 每個著色器階段最多可允許15個常數緩衝區,而且每個緩衝區最多可以保存4,096個常數變數。 register-usage 宣告語法如下:
- b*#*:常數緩衝區的緩存器(cbuffer)。
- t*#*:紋理緩衝區的寄存器(tbuffer)。
- *#*:取樣器的暫存器。 (取樣器定義了紋理資源中紋素的查閱行為。)
例如,圖元著色器的 HLSL 可能會採用紋理和取樣器作為輸入,並具有類似這樣的宣告。
Texture2D simpleTexture : register(t0);
SamplerState simpleSampler : register(s0);
您必須將常數緩衝區指派給緩存器,當您設定管線時,您會將常數緩衝區附加至您在 HLSL 檔案中指派給的相同位置。 例如,在上一個主題中,呼叫 VSSetConstantBuffers 表示第一個參數的 '0'。 這會告訴 Direct3D 將常數緩衝區資源綁定到綁定點 0,這與緩衝區在 HLSL 檔案中分配給 register(b0) 的對應一致。
從頂點緩衝區讀取
頂點緩衝區會將場景物件的三角形數據提供給頂點著色器。 如同常數緩衝區,頂點緩衝區結構會使用類似的封裝規則,在C++程序代碼中宣告。
typedef struct _vertexPositionColor
{
DirectX::XMFLOAT3 pos;
DirectX::XMFLOAT3 color;
} VertexPositionColor;
Direct3D 11 中沒有頂點數據的標準格式。 相反地,我們會使用描述元來定義自己的頂點數據佈局;使用 D3D11_INPUT_ELEMENT_DESC 結構的數組來定義數據欄位。 在這裡,我們會顯示簡單的輸入配置,其描述與上述結構相同的頂點格式:
D3D11_INPUT_ELEMENT_DESC iaDesc [] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT,
0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
hr = device->CreateInputLayout(
iaDesc,
ARRAYSIZE(iaDesc),
bytes,
bytesRead,
&m_pInputLayout
);
如果您在修改範例程式代碼時將數據新增至頂點格式,請務必更新輸入配置,否則著色器將無法解譯它。 您可以修改頂點設定,如下所示:
typedef struct _vertexPositionColorTangent
{
DirectX::XMFLOAT3 pos;
DirectX::XMFLOAT3 normal;
DirectX::XMFLOAT3 tangent;
} VertexPositionColorTangent;
在此情況下,您會修改輸入配置定義,如下所示。
D3D11_INPUT_ELEMENT_DESC iaDescExtended[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT,
0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TANGENT", 0, DXGI_FORMAT_R32G32B32_FLOAT,
0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
hr = device->CreateInputLayout(
iaDesc,
ARRAYSIZE(iaDesc),
bytes,
bytesRead,
&m_pInputLayoutExtended
);
每個輸入配置元素定義前面都會加上字串,例如 「POSITION」 或 「NORMAL」,也就是本主題稍早討論的語意。 就像是一個句柄,可協助 GPU 在處理頂點時識別該元素。 為您的頂點元素選擇一般且有意義的名稱。
就像常數緩衝區一樣,頂點著色器具有傳入頂點元素的對應緩衝區定義。 (這就是為什麼我們在建立輸入配置時提供頂點著色器資源的參考 - Direct3D 會使用著色器的輸入結構來驗證每個頂點數據配置。請注意,輸入配置定義與這個 HLSL 緩衝區宣告之間的語意如何相符。 不過,COLOR 加上了“0”。 如果您只有一個在版面配置中宣告的 COLOR 元素,則不需要新增 0,但如果您在未來選擇新增更多色彩元素,則附加它是一個好的做法。
struct VS_INPUT
{
float3 vPos : POSITION;
float3 vColor : COLOR0;
};
在著色器之間傳遞數據
著色器會接受輸入類型,並在執行時從主要函式傳回輸出類型。 針對上一節中定義的頂點著色器,輸入類型是VS_INPUT結構,我們定義了相符的輸入配置和C++結構。 這個結構的陣列可用來在 createCube 方法的 中建立頂點緩衝區。
頂點著色器會返回一個PS_INPUT結構,該結構至少必須包含4個元件(float4)的最終頂點位置。 該位置值必須宣告為具有系統值語意 SV_POSITION,這樣 GPU 才能擁有執行下一個繪圖步驟所需的數據。 請注意,頂點著色器輸出與像素著色器輸入之間沒有 1:1 的對應;頂點著色器會針對指定的每個頂點傳回一個結構,但像素著色器會針對每個像素執行一次。 這是因為每個頂點數據會先通過點陣化階段。 這個階段會決定您要繪製的圖形「遮罩」哪些像素,計算每個像素的頂點數據插值,然後針對每個像素呼叫一次像素著色器。 插補是點陣化輸出值時的預設行為,特別是對於正確處理輸出向量數據(光向量、每個頂點常態和正切值及其他)。
struct PS_INPUT
{
float4 Position : SV_POSITION; // interpolated vertex position (system value)
float4 Color : COLOR0; // interpolated diffuse color
};
檢閱頂點著色器
範例頂點著色器非常簡單:將頂點(位置和色彩)輸入,其中位置從模型座標轉換為透視投影座標,並將其(連同色彩)傳回到光柵化器。 請注意,色彩值會與位置數據一起插補,為每個圖元提供不同的值,即使頂點著色器未對色彩值執行任何計算也一樣。
VS_OUTPUT main(VS_INPUT input) // main is the default function name
{
VS_OUTPUT Output;
float4 pos = float4(input.vPos, 1.0f);
// Transform the position from object space to homogeneous projection space
pos = mul(pos, mWorld);
pos = mul(pos, View);
pos = mul(pos, Projection);
Output.Position = pos;
// Just pass through the color data
Output.Color = float4(input.vColor, 1.0f);
return Output;
}
更複雜的頂點著色器,例如一個用於設定物件頂點以進行 Phong 明暗處理的著色器,看起來可能會更像這樣。 在此情況下,我們會利用向量和常態插補到近似平滑表面的事實。
// A constant buffer that stores the three basic column-major matrices for composing geometry.
cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
matrix model;
matrix view;
matrix projection;
};
cbuffer LightConstantBuffer : register(b1)
{
float4 lightPos;
};
struct VertexShaderInput
{
float3 pos : POSITION;
float3 normal : NORMAL;
};
// Per-pixel color data passed through the pixel shader.
struct PixelShaderInput
{
float4 position : SV_POSITION;
float3 outVec : POSITION0;
float3 outNormal : NORMAL0;
float3 outLightVec : POSITION1;
};
PixelShaderInput main(VertexShaderInput input)
{
// Inefficient -- doing this only for instruction. Normally, you would
// premultiply them on the CPU and place them in the cbuffer.
matrix mvMatrix = mul(model, view);
matrix mvpMatrix = mul(mvMatrix, projection);
PixelShaderInput output;
float4 pos = float4(input.pos, 1.0f);
float4 normal = float4(input.normal, 1.0f);
float4 light = float4(lightPos.xyz, 1.0f);
//
float4 eye = float4(0.0f, 0.0f, -2.0f, 1.0f);
// Transform the vertex position into projected space.
output.gl_Position = mul(pos, mvpMatrix);
output.outNormal = mul(normal, mvMatrix).xyz;
output.outVec = -(eye - mul(pos, mvMatrix)).xyz;
output.outLightVec = mul(light, mvMatrix).xyz;
return output;
}
檢視像素著色器
此範例中的這個像素著色器可能是您在圖元著色器中可以擁有的絕對最小程式代碼數量。 它會使用於點陣化期間產生的插補像素色彩數據,並將其作為輸出返回,然後寫入至渲染目標。 多麼無聊!
PS_OUTPUT main(PS_INPUT In)
{
PS_OUTPUT Output;
Output.RGBColor = In.Color;
return Output;
}
重要部分是傳回值上的系統值語意 SV_TARGET。 它表示輸出應寫入主要渲染目標,這是提供給交換鏈以進行顯示的材質緩衝區。 這是像素著色器所必需的 - 如果沒有來自像素著色器的色彩數據,Direct3D 將無法顯示任何內容!
執行 Phong 光影的較複雜圖元著色器範例可能如下所示。 由於向量和常態已插補,因此我們不需要根據每個像素計算它們。 然而,由於插值的工作原理,我們確實必須重新歸一化它們;從概念上講,我們需要逐漸將向量從頂點 A 的方向「旋轉」到頂點 B 的方向,保持其長度,而插值則在兩個向量端點之間切割一條直線。
cbuffer MaterialConstantBuffer : register(b2)
{
float4 lightColor;
float4 Ka;
float4 Kd;
float4 Ks;
float4 shininess;
};
struct PixelShaderInput
{
float4 position : SV_POSITION;
float3 outVec : POSITION0;
float3 normal : NORMAL0;
float3 light : POSITION1;
};
float4 main(PixelShaderInput input) : SV_TARGET
{
float3 L = normalize(input.light);
float3 V = normalize(input.outVec);
float3 R = normalize(reflect(L, input.normal));
float4 diffuse = Ka + (lightColor * Kd * max(dot(input.normal, L), 0.0f));
diffuse = saturate(diffuse);
float4 specular = Ks * pow(max(dot(R, V), 0.0f), shininess.x - 50.0f);
specular = saturate(specular);
float4 finalColor = diffuse + specular;
return finalColor;
}
在另一個範例中,圖元著色器會採用自己的常數緩衝區,其中包含光線和材質資訊。 頂點著色器中的輸入配置將會展開以包含一般數據,而該頂點著色器的輸出預期會包含頂點、光線和檢視座標系統中頂點常態的轉換向量。
如果您有具有指定暫存器的紋理緩衝區和取樣器(分別為t 和 s),您也可以在像素著色器中存取它們。
Texture2D simpleTexture : register(t0);
SamplerState simpleSampler : register(s0);
struct PixelShaderInput
{
float4 pos : SV_POSITION;
float3 norm : NORMAL;
float2 tex : TEXCOORD0;
};
float4 SimplePixelShader(PixelShaderInput input) : SV_TARGET
{
float3 lightDirection = normalize(float3(1, -1, 0));
float4 texelColor = simpleTexture.Sample(simpleSampler, input.tex);
float lightMagnitude = 0.8f * saturate(dot(input.norm, -lightDirection)) + 0.2f;
return texelColor * lightMagnitude;
}
著色器是非常強大的工具,可用來產生程序資源,例如陰影貼圖或雜訊紋理。 事實上,進階技術需要您更抽象地思考紋理,而不是視覺元素,而是做為緩衝區。 它們會保存高度資訊之類的數據,或作為多階段效果處理一部分,可以在最終像素著色器階段或特定幀中取樣的其他數據。 多重取樣是功能強大的工具和許多現代視覺效果的骨幹。
後續步驟
希望您已熟悉 DirectX 11,並準備好開始處理您的專案。 以下是一些連結,可以協助您解答有關使用 DirectX 和 C++ 進行開發的其他問題:
相關主題