使用著色器和著色器資源
是時候瞭解如何使用著色器和著色器資源來開發適用于 Windows 8 的 Microsoft DirectX 遊戲。 我們已瞭解如何設定圖形裝置和資源,或許您甚至已開始修改其管線。 現在讓我們看看圖元和頂點著色器。
如果您不熟悉著色器語言,快速討論會依序進行。 著色器是小型的低階程式,會在圖形管線的特定階段編譯和執行。 其專長是非常快速的浮點數學運算。 最常見的著色器程式如下:
- 頂點著色器 - 針對場景中的每個頂點執行。 這個著色器會在呼叫應用程式提供給它的頂點緩衝區元素上運作,而且將點陣化為圖元位置的 4 元件位置向量最少。
- 圖元著色器 - 針對轉譯目標中的每個圖元執行。 這個著色器會接收來自先前著色器階段的點陣化座標(在最簡單的管線中,這會是頂點著色器),並傳回該圖元位置的色彩(或其他 4 元件值),然後寫入轉譯目標。
此範例包含非常基本的頂點和圖元著色器,這些著色器只會繪製幾何,以及新增基本光源計算的較複雜著色器。
著色器程式是以 Microsoft 高階著色器語言 (HLSL) 撰寫。 HLSL 語法看起來很像 C,但沒有指標。 著色器程式必須非常精簡且有效率。 如果您的著色器編譯為太多指示,則無法執行,並傳回錯誤。 (請注意,允許的指示確切數目是 的 一部分Direct3D 功能層級 。)
在 Direct3D 中,著色器不會在執行時間編譯;編譯器的其餘部分時,會編譯它們。 當您使用 Microsoft Visual Studio 2013 編譯應用程式時,HLSL 檔案會編譯成 CSO (.cso) 檔案,您的應用程式在繪製之前必須載入並放置於 GPU 記憶體中。 在封裝應用程式時,請務必將這些 CSO 檔案包含在您的應用程式中;它們是資產,就像網格和紋理一樣。
瞭解 HLSL 語意
在繼續之前,請務必花點時間討論 HLSL 語意,因為它們通常是新 Direct3D 開發人員的混淆點。 HLSL 語意是字串,可識別應用程式與著色器程式之間傳遞的值。 雖然它們可以是各種可能字串中的任何一種,但最佳做法是使用類似 POSITION
或 的字串,或 COLOR
表示使用方式。 當您建構常數緩衝區或輸入配置時,會指派這些語意。 您也可以將介於 0 到 7 之間的數位附加至語意,以便針對類似的值使用不同的暫存器。 例如:COLOR0、COLOR1、COLOR2...
前面加上 「SV_」 的語意是 著色器程式寫入的系統值 語意;您的遊戲本身(在 CPU 上執行)無法修改它們。 這些語意通常包含來自圖形管線中其他著色器階段之輸入或輸出的值,或是由 GPU 產生。
此外,當語意用來指定著色器階段的輸入或輸出時, SV_
語意有不同的行為。 例如, SV_POSITION
(輸出)包含頂點著色器階段期間轉換的頂點資料,而 SV_POSITION
( input ) 則包含 GPU 在點陣化階段內插補的圖元位置值。
以下是一些常見的 HLSL 語意:
POSITION
( n ) 適用于頂點緩衝區資料。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 )。
- s *#*:取樣器的註冊。 (取樣器會定義紋理資源中紋素的查閱行為。
例如,圖元著色器的 HLSL 可能會採用紋理和取樣器作為輸入,並具有類似這樣的宣告。
Texture2D simpleTexture : register(t0);
SamplerState simpleSampler : register(s0);
您必須將常數緩衝區指派給暫存器,當您設定管線時,您會將常數緩衝區附加至您在 HLSL 檔案中指派給的相同位置。 例如,在上一個主題中,對 VSSetConstantBuffers 的呼叫 表示第一個參數的 '0'。 這會告訴 Direct3D 附加常數緩衝區資源以註冊 0,這符合 HLSL 檔案中暫存器(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 的方向,維持其長度—wheras 插補會改為跨越兩個向量端點之間的直線。
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++ 進行開發的其他問題:
相關主題