使用 XNA/3D 開發/著色器和效果建立遊戲
存在畫素著色器和頂點著色器。首先,您需要了解它們之間的區別、工作原理以及它們能為您做什麼。然後,您需要學習著色器語言 HLSL、其語法以及如何使用它。特別是如何從程式中呼叫它。最後,您還將學習一個名為 FXComposer 的程式,它將向您展示如何載入效果、它們的 HLSL 程式碼是什麼、如何修改它以及如何匯出並在遊戲中使用完成的著色器。
在過去,計算機生成的圖形是由影片硬體中的所謂固定功能管道 (FFP) 生成的。此管道僅提供以特定順序執行的一組有限的操作。這對於像遊戲這樣的圖形應用程式日益增長的複雜性來說不夠靈活。
這就是為什麼引入了一種新的 圖形管道 來替代這種硬編碼方法。新模型仍然具有一些固定元件,但它引入了所謂的著色器。著色器在渲染螢幕上的場景中發揮主要作用,並且可以輕鬆地交換、程式設計和適應程式設計師的需求。這種方法提供了完全的創造力,但也對圖形程式設計師提出了更多責任。
著色器有兩種型別:頂點著色器 和 畫素著色器(在 OpenGL 中稱為片段著色器)。隨著 DirectX 10 和 OpenGL 3.2 的推出,第三種類型的著色器也出現了:幾何著色器,它透過基於現有頂點建立額外的、新的頂點來提供更廣泛的可能性。
著色器描述和計算頂點或畫素的屬性。頂點著色器處理頂點及其屬性:它們在螢幕上的位置、每個頂點的紋理座標、它的顏色等等。
畫素著色器處理頂點著色器(光柵化的片段)的結果,並描述畫素的屬性:它的顏色、與螢幕上其他畫素相比的深度(z 深度)及其 alpha 值。
如今,有三種類型的著色器以特定順序執行以渲染最終影像。該方案顯示了每種著色器在將資料從 XNA 傳送到 GPU 並最終渲染影像的過程中所扮演的角色及其順序。此過程稱為 GPU 工作流

頂點著色器是用於透過使用數學運算來操作頂點資料的特殊函式。為此,頂點著色器將 XNA 中的頂點資料作為輸入。該資料包含頂點在三維世界中的位置、它的顏色(如果它有顏色)、它的法線向量及其紋理座標。使用頂點著色器,可以操作這些資料,但只會更改值,不會更改資料儲存方式。
每個頂點著色器的最基本功能是將每個頂點的位置從虛擬空間中的三維位置轉換為螢幕上的二維位置。這透過使用檢視、世界和投影矩陣進行矩陣乘法來完成。
頂點著色器還計算頂點在二維螢幕上的深度(z 緩衝區深度),以便不會丟失有關物件深度原始三維資訊,並且更靠近檢視者的頂點顯示在位於其他頂點後面的頂點前面。頂點著色器可以操作所有輸入屬性,例如位置、顏色、法線向量和紋理座標,但它不能建立新的頂點。但頂點著色器可用於更改檢視物件的方式。霧、運動模糊和熱浪效果都可以使用頂點著色器進行模擬。
管道中的下一步是新的但可選的幾何著色器。幾何著色器可以根據已傳送到 GPU 的頂點向網格新增新的頂點。使用此方法的一種方法稱為幾何 細分,它是在特定程式的基礎上向現有表面新增更多三角形的過程,以使其更詳細,更美觀。
使用幾何著色器而不是高多邊形模型可以節省大量的 CPU 時間,因為不必由 CPU 處理併發送到 GPU 所有應該在螢幕上顯示的頂點。在某些情況下,多邊形數量可以減少一半或四分之一。
如果未使用幾何著色器,則頂點著色器的輸出將直接傳送到光柵化器。如果使用了幾何著色器,則輸出在新增新頂點後也會發送到光柵化器。
光柵化器獲取處理後的頂點,並將它們轉換為片段(多邊形的畫素大小部分)。無論是點、線還是多邊形基元,此階段都會生成片段以“填充”多邊形並插值所有顏色和紋理座標,以便為每個片段分配適當的值。
之後,畫素著色器(DirectX 使用術語“畫素著色器”,而 OpenGL 使用術語“片段著色器”)將針對每個片段進行呼叫。畫素著色器計算單個畫素的顏色,並用於漫射光照(場景照明)、凹凸貼圖、法線貼圖、鏡面光照和模擬反射。畫素著色器通常用於為表面提供它們在現實生活中具有的效果。
畫素著色器的結果是具有特定顏色的畫素,該畫素將傳遞到輸出合併器,最後繪製到螢幕上。
頂點著色器和畫素著色器之間的主要區別在於,頂點著色器用於更改幾何體(頂點)的屬性並將其轉換為 2D 螢幕。相反,畫素著色器用於更改生成畫素的外觀,目的是建立表面效果。
在 XNA 中使用 BasicEffect 類進行程式設計
[edit | edit source]如果您想為模型製作簡單的效果和照明,Basic Class XNA 非常有用且有效。它的工作方式類似於固定功能管道 (FFP),它提供了有限且不靈活的操作。
要使用 BasicEffect 類,我們首先需要在遊戲類的頂部宣告 BasicEffect 的例項。
BasicEffect basicEffect;
此例項應在 Initiliaze() 方法中初始化,因為我們希望在程式啟動時初始化它。如果我們在其他地方執行此操作,可能會導致效能問題。
basicEffect =
new BasicEffect(graphics.GraphicsDevice, null);
接下來,我們在遊戲類中實現一些方法來使用 BasicEffect 類繪製模型。使用 BasicEffect 類,我們不必為每個變數建立 EffectParameter 物件。相反,我們可以直接將這些值分配給 BasicEffect 的屬性。
private void DrawWithBasicEffect
(Model model, Matrix world, Matrix view, Matrix proj){
basicEffect.World = world;
basicEffect.View = view;
basicEffect.Projection = proj;
basicEffect.LightingEnabled = true;
basicEffect.DiffuseColor = new Vector3(1.0f, 1.0f, 1.0f);
basicEffect.SpecularColor = new Vector3(0.2f, 0.2f, 0.2f);
basicEffect.SpecularPower = 5.0f;
basicEffect.AmbientLightColor =
new Vector3(0.5f, 0.5f, 0.5f);
basicEffect.DirectionalLight0.Enabled = true;
basicEffect.DirectionalLight0.DiffuseColor = Vector3.One;
basicEffect.DirectionalLight0.Direction =
Vector3.Normalize(new Vector3(1.0f, 1.0f, -1.0f));
basicEffect.DirectionalLight0.SpecularColor = Vector3.One;
basicEffect.DirectionalLight1.Enabled = true;
basicEffect.DirectionalLight1.DiffuseColor =
new Vector3(0.5f, 0.5f, 0.5f);
basicEffect.DirectionalLight1.Direction =
Vector3.Normalize(new Vector3(-1.0f, -1.0f, 1.0f));
basicEffect.DirectionalLight1.SpecularColor =
new Vector3(0.5f, 0.5f, 0.5f);
}
在所有必要的屬性都分配後。現在我們的模型應該使用 BasicEffect 類繪製。由於模型中可能包含多個網格,因此我們使用 foreach 迴圈迭代模型的每個網格。
private void DrawWithBasicEffect
(Model model, Matrix world, Matrix view, Matrix proj){
....
foreach (ModelMesh meshes in model.Meshes)
{
foreach (ModelMeshPart parts in meshes.MeshParts)
parts.Effect = basicEffect;
meshes.Draw();
}
}
要在 XNA 中檢視我們的模型,我們只需在 Draw() 方法中呼叫我們的方法。
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
DrawWithBasicEffect(myModel, world, view, proj);
base.Draw(gameTime);
}
使用 BasicEffect 類繪製紋理
[edit | edit source]要使用 BasicEffect 類繪製紋理,我們必須啟用 alpha 屬性。之後,我們可以將紋理分配給模型。
basicEffect.TextureEnabled = true;
basicEffect.Texture = myTexture;
使用 BasicEffect 類建立透明度
[edit | edit source]首先,我們將透明度值分配給 basicEffect 屬性。
basicEffect.Alpha = 0.5f;
然後,我們必須告訴 GraphicsDevice 使用以下程式碼在 Draw() 方法中啟用透明度。
protected void Draw(){
.....
GraphicsDevice.RenderState.AlphaBlendEnable = true;
GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
GraphicsDevice.RenderState.DestinationBlend = Blend.InverseSourceAlpha;
DrawWithBasicEffect(model,world,view,projection)
GraphicsDevice.RenderState.AlphaBlendEnable = false;
.....
}
在 XNA 中程式設計您自己的 HLSL 著色器
[edit | edit source]著色語言
[edit | edit source]著色器是可程式設計的,為此,已經開發了 C 類高階程式語言的幾種變體。
高階著色語言 (HLSL) 由 Microsoft 為 Microsoft Direct3D API 開發。它使用 C 語法,我們將它與 XNA 框架一起使用。
其他著色語言包括自 OpenGL 2.0 起提供的 GLSL(OpenGL 著色語言)和 Cg(C for Graphics),這是由 Nvidia 與 Microsoft 合作開發的另一種高階著色語言,它與 HLSL 非常相似。Cg 受 FX Composer 支援,FX Composer 將在本文稍後部分介紹。
高階著色語言 (HLSL) 及其在 XNA 中的使用
[edit | edit source]XNA 中的著色器是用 HLSL 編寫的,並存儲在所謂的 effect 檔案中,其副檔名為 .fx。最好將所有著色器儲存在一個單獨的資料夾中。因此,在 Visual C# 的解決方案資源管理器中,在內容節點中建立一個名為“Shaders”的新資料夾。要建立新的 Effect fx 檔案,只需右鍵單擊新的“Shaders”資料夾,然後選擇新增→新建項。在新專案對話方塊中,選擇“Effect 檔案”,併為檔案指定適當的名稱。
新的 effect 檔案將已經包含一些應該可以工作的基本著色器程式碼,但是在這章中,我們將從頭開始編寫著色器,因此可以刪除已生成的程式碼。
HLSL Effect 檔案的結構 (*.fx)
[edit | edit source]如前所述,HLSL 使用 C 語法,可以透過宣告變數、結構體和編寫函式來進行程式設計。HLSL 中的著色器通常由四個不同的部分組成。
變數宣告
[edit | edit source]包含引數和固定常量的變數宣告。這些變數可以從使用著色器的 XNA 應用程式設定。
示例
float4 AmbienceColor = float4(0.5f, 0.5f, 0.5f, 1.0f);
使用此語句,將宣告一個新的全域性變數並對其進行賦值。HLSL 提供標準的 c 資料型別,如 float、string 和 struct,但也提供其他特定於著色器的用於向量、矩陣、取樣器、紋理等的資料型別。官方參考:MSDN
在示例中,我們聲明瞭一個四維向量,它用於定義顏色。顏色由代表 4 個通道(紅色、綠色、藍色、alpha)的 4 個值表示,範圍為 0.0 到 1.0。變數可以具有任意名稱。
資料結構
[edit | edit source]著色器將用來輸入和輸出資料的結構體。通常,這兩種結構體是:一種用於輸入頂點著色器,另一種用於輸出頂點著色器。頂點著色器的輸出隨後將用作畫素著色器的輸入。通常,畫素著色器的輸出不需要結構體,因為它已經是最終結果。如果包含幾何著色器,則需要其他結構體,但我們只檢視由頂點著色器和畫素著色器組成的最基本示例。結構體可以具有任意名稱。
示例
struct VertexShaderInput
{
float4 Position : POSITION0;
};
此資料結構包含一個名為 Position(或任何其他名稱)的四維向量型別變數。
變數名後面的 POSITION0 是一個所謂的語義。輸入和輸出結構體中的所有變數都必須透過語義進行標識。可以在 HLSL 官方參考中找到列表:MSDN
著色器函式
[edit | edit source]著色器函式的實現及其背後的邏輯。通常,這包括一個用於頂點著色器的函式和一個用於畫素著色器的函式。
示例
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
return AmbienceColor;
}
函式與 C 中的函式一樣:它們可以具有引數和返回值。在本例中,我們有一個名為 PixelShaderFunction(名稱可以任意)的函式,它以 VertexShaderOutput 物件作為輸入,並返回語義 COLOR0 型別為 float4(代表 4 個顏色通道的四維向量)的值。
技巧
[edit | edit source]技巧類似於著色器的 main() 方法,它告訴顯示卡何時使用哪個著色器函式。技巧可以有多個通道,它們使用不同的著色器函式,因此螢幕上的最終影像可以由多個通道組成。
示例
technique Ambient
{
pass Pass1
{
VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_1_1 PixelShaderFunction();
}
}
這個示例技術名為 Ambient,只有一個 pass。在這個 pass 中,頂點和畫素著色器函式被分配,並且著色器版本(在本例中為 1.1)被指定。
第一次嘗試:一個簡單的環境著色器
[edit | edit source]
最簡單的著色器是所謂的環境著色器,它只是將一個固定顏色分配給物體的每個畫素,所以只能看到它的輪廓。讓我們嘗試實現一個環境著色器作為第一個嘗試。
我們從一個空的 .fx 檔案開始,它可以有任意檔名。頂點著色器需要三個場景矩陣來根據三維座標計算螢幕上某個頂點的二維位置。所以我們需要在 fx 檔案中定義三個矩陣作為變數
float4x4 WorldMatrix;
float4x4 ViewMatrix;
float4x4 ProjectionMatrix;
float4 AmbienceColor = float4(0.5f, 0.5f, 0.5f, 1.0f);
型別為 float4x4 的變數是一個四維矩陣。另一個變數是一個四維向量,用來確定環境光顏色(在本例中為灰色)。環境色的顏色值為浮點值,表示 RGBA 通道,其中最小值為 0,最大值為 1。
接下來我們需要頂點著色器的輸入和輸出結構
struct VertexShaderInput
{
float4 Position : POSITION0;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
};
因為它是一個非常簡單的著色器,所以目前它們唯一包含的資料是虛擬三維空間中頂點的座標(VertexShaderInput)和螢幕上二維空間中頂點的變換後的座標(VertexShaderOutput)。POSITION0 是兩個位置的語義型別。
現在我們需要新增著色器計算本身。這在兩個函式中完成。首先是頂點著色器函式
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4 worldPosition = mul(input.Position, World);
float4 viewPosition = mul(worldPosition, View);
output.Position = mul(viewPosition, Projection);
return output;
}
這是最基本的頂點著色器函式,每個頂點著色器都應該看起來類似。儲存在 input 中的位置透過與三個場景矩陣相乘進行變換,然後作為結果返回。輸入型別為 VertexShaderInput,輸出型別為 VertexShaderOutput。所使用的矩陣乘法函式(mul)是 HLSL 語言的一部分。
現在我們只需要給畫素著色器提供頂點著色器計算出來的位置,並用環境色(基於環境強度)對它進行著色。畫素著色器在另一個函式中實現,該函式返回最終畫素顏色,資料型別為 float4,語義型別為 COLOR0
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
return AmbienceColor;
}
所以應該清楚為什麼最終結果中物體的每個畫素都具有相同的顏色:因為我們在著色器中還沒有任何燈光,並且所有三維資訊都丟失了。
為了使我們的著色器完整,我們需要一個所謂的技術,它類似於著色器的 main() 方法,也是 XNA 在使用著色器渲染物體時呼叫的函式
technique Ambient
{
pass Pass1
{
VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_1_1 PixelShaderFunction();
}
}
一個技術有一個名稱(在本例中為 Ambient),可以從 XNA 中直接呼叫。一個技術也可以有多個 pass,但在這種簡單的情況下,我們只需要一個 pass。在一個 pass 中,精確地定義了我們的著色器檔案中哪個函式是頂點著色器,哪個函式是畫素著色器。我們在這裡不使用幾何著色器,因為與頂點和畫素著色器相比,它只是可選的。此外,還確定了要使用的著色器版本,因為著色器模型在不斷發展,並且添加了新功能。可能的版本是:1.0 到 1.3、1.4、2.0、2.0a、2.0b、3.0、4.0。
對於簡單的環境光照,我們只需要版本 1.1,但對於反射和其他更高階的效果,需要畫素著色器版本 2.0。
完整的著色器程式碼
float4x4 WorldMatrix;
float4x4 ViewMatrix;
float4x4 ProjectionMatrix;
float4 AmbienceColor = float4(0.5f, 0.5f, 0.5f, 1.0f);
struct VertexShaderInput
{
float4 Position : POSITION0;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
};
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4 worldPosition = mul(input.Position, WorldMatrix);
float4 viewPosition = mul(worldPosition, ViewMatrix);
output.Position = mul(viewPosition, ProjectionMatrix);
return output;
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
return AmbienceColor;
}
technique Ambient
{
pass Pass1
{
VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_1_1 PixelShaderFunction();
}
}
現在著色器檔案已完成,可以儲存,我們只需要讓我們的 XNA 應用程式使用它來渲染物體。
首先,必須定義一個型別為 Effect 的新的全域性變數。每個 Effect 物件都用於引用一個位於 fx 檔案中的著色器。
Effect myEffect;
在用於從內容資料夾中載入內容(如模型、紋理等)的方法中,也需要載入著色器檔案(在本例中,它是資料夾 Shaders 中的 Ambient.fx 檔案)
myEffect = Content.Load<Effect>("Shaders/Ambient");
現在 Effect 已準備好使用。要使用我們自己的著色器繪製模型,我們需要實現一個用於該目的的方法
private void DrawModelWithEffect(Model model, Matrix world, Matrix view, Matrix projection)
{
foreach (ModelMesh mesh in model.Meshes)
{
foreach (ModelMeshPart part in mesh.MeshParts)
{
part.Effect = myEffect;
myEffect.Parameters["World"].SetValue(world * mesh.ParentBone.Transform);
myEffect.Parameters["View"].SetValue(view);
myEffect.Parameters["Projection"].SetValue(projection);
}
mesh.Draw();
}
}
該方法將模型和用於描述場景的三個矩陣作為引數。它遍歷模型中的網格,然後遍歷網格中的網格部分。對於每個部分,它將我們的新 myEffect 物件分配給名為“Effect”的屬性。
但在著色器準備好使用之前,我們需要為它提供必需的引數。透過使用 myEffect 物件的 Parameters 集合,我們可以訪問之前在著色器檔案中定義的變數,併為它們提供一個值。我們透過使用 SetValue() 方法將三個主矩陣分配給著色器中等效的變數。之後,網格就可以使用 ModelMesh 類的 Draw() 方法進行繪製。
因此,新的方法 DrawModelWithEffect() 現在可以用於每個型別為 Model 的模型,以使用我們自定義的著色器在螢幕上繪製它!結果可以在圖片中看到。如你所見,模型的每個畫素都具有相同的顏色,因為我們還沒有使用任何燈光、紋理或效果。
也可以透過使用 Parameters 集合和 SetValue() 方法直接在 XNA 中更改著色器的固定變數。例如,要更改 XNA 應用程式中著色器中的環境色,需要使用以下語句
myEffect.Parameters["AmbienceColor"].SetValue(Color.White.ToVector4());
漫射著色
[edit | edit source]

漫射著色以來自光發射器的光渲染物體,並從物體表面向所有方向反射(它擴散)。這就是賦予大多數物體陰影的原因,使其具有明亮的亮部和較暗的暗部,從而產生簡單的環境著色器中丟失的三維效果。現在,我們將修改之前環境著色器,使其也支援漫射著色。實現漫射著色的方法有兩種,一種方法使用頂點著色器,另一種方法使用畫素著色器。我們將看看頂點著色器變體。
我們需要在之前的環境著色器檔案中新增三個新變數
float4x4 WorldInverseTransposeMatrix;
float3 DiffuseLightDirection = float3(-1.0f, 0.0f, 0.0f);
float4 DiffuseColor = float4(1.0f, 1.0f, 1.0f, 1.0f);
變數 WorldInverseTransposeMatrix 是另一個矩陣,用於計算。它是世界矩陣逆矩陣的轉置。在只有環境光照的情況下,我們不必關心頂點的法線向量,但隨著漫射光照的加入,這個矩陣變得必要,它用於變換頂點的法線以進行光照計算。
另外兩個變數用於定義漫射光來自的方向(第一個值為 X,第二個值為 Y,第三個值為 Z,位於三維空間中)以及從渲染物體表面反射出來的漫射光的顏色。在本例中,我們簡單地使用白色,並且燈光在虛擬空間中沿 x 軸方向發射。
VertexShaderInput 和 VertexShaderOutput 的結構也需要一些小的修改。我們必須將以下變數新增到結構 VertexShaderInput 中,以便在頂點著色器輸入中獲取當前頂點的法線向量
float4 NormalVector : NORMAL0;
並且我們在結構 VertexShaderOutput 中添加了一個用於顏色的變數,因為我們將在頂點著色器中計算漫射著色,這將導致需要傳遞給畫素著色器的顏色
float4 VertexColor : COLOR0;
要在頂點著色器中進行漫射光照,我們必須在 VertexShaderFunction 中新增一些程式碼
float4 normal = normalize(mul(input.NormalVector, WorldInverseTransposeMatrix));
float lightIntensity = dot(normal, DiffuseLightDirection);
output.VertexColor = saturate(DiffuseColor * lightIntensity);
使用這段程式碼,我們變換頂點的法線,使其相對於物體在世界中的位置(第一行新程式碼)。在第二行中,計算表面法線向量與照射它的燈光之間的夾角。HLSL 語言提供了一個 dot() 函式,用於計算兩個向量的點積,這可以用來測量兩個向量之間的夾角。在本例中,角度等於表面上頂點的光強。最後,透過將漫射顏色乘以強度來計算當前頂點的顏色。這種顏色儲存在 VertexShaderOutput 結構的 VertexColor 屬性中,它稍後會傳遞給畫素著色器。
最後,我們必須更改 PixelShaderFunction 返回的值
return saturate(input.VertexColor + AmbienceColor);
它簡單地獲取我們在頂點著色器中計算出來的顏色,並將環境分量新增到其中。函式 saturate 由 HLSL 提供,以確保顏色在 0 到 1 之間的範圍內。
你可能希望使 AmbienceColor 分量稍微暗一些,這樣它對最終顏色的影響就不會那麼大。這也可以透過定義一個強度變數來調節顏色的強度來實現。但現在我們將保持簡單明瞭,並在以後討論這個問題。
完整的著色器程式碼
float4x4 WorldMatrix;
float4x4 ViewMatrix;
float4x4 ProjectionMatrix;
float4 AmbienceColor = float4(0.2f, 0.2f, 0.2f, 1.0f);
// For Diffuse Lightning
float4x4 WorldInverseTransposeMatrix;
float3 DiffuseLightDirection = float3(-1.0f, 0.0f, 0.0f);
float4 DiffuseColor = float4(1.0f, 1.0f, 1.0f, 1.0f);
struct VertexShaderInput
{
float4 Position : POSITION0;
// For Diffuse Lightning
float4 NormalVector : NORMAL0;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
// For Diffuse Lightning
float4 VertexColor : COLOR0;
};
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4 worldPosition = mul(input.Position, WorldMatrix);
float4 viewPosition = mul(worldPosition, ViewMatrix);
output.Position = mul(viewPosition, ProjectionMatrix);
// For Diffuse Lightning
float4 normal = normalize(mul(input.NormalVector, WorldInverseTransposeMatrix));
float lightIntensity = dot(normal, DiffuseLightDirection);
output.VertexColor = saturate(DiffuseColor * lightIntensity);
return output;
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
return saturate(input.VertexColor + AmbienceColor);
}
technique Diffuse
{
pass Pass1
{
VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_1_1 PixelShaderFunction();
}
}
著色器檔案就這些。要在 XNA 中使用新的著色器,我們必須對使用著色器渲染物體的 XNA 應用程式進行一個補充
我們必須在 XNA 中設定著色器的 WorldInverseTransposeMatrix 變數。因此,在 DrawModelWithEffect 方法中,在使用 SetValue() 設定 myEffect 物件的其他引數的部分,我們必須設定 WorldInverseTransposeMatrix。但在設定它之前,需要計算它。為此,我們將反轉然後轉置我們應用程式的世界矩陣(它首先與物體的變換相乘,所以一切都處於正確的位置)。
Matrix worldInverseTransposeMatrix = Matrix.Transpose(Matrix.Invert(mesh.ParentBone.Transform * world));
myEffect.Parameters["WorldInverseTransposeMatrix"].SetValue(worldInverseTransposeMatrix);
這就是 XNA 程式碼中需要更改的所有內容。現在你應該有不錯的漫射光照。你可以在圖片中看到結果。請記住,這個著色器已經使用了漫射和環境光照,這就是為什麼模型的暗部只是灰色而不是黑色。
如果我們將畫素著色器修改為只返回頂點顏色而不新增環境光,則場景看起來不同(第二張圖片)
return saturate(input.VertexColor);
模型中沒有光照的暗部現在完全是黑色,因為它們不再新增環境分量。
紋理著色器
[edit | edit source]
根據紋理座標在物體上應用和渲染紋理也是透過著色器完成的。為了使之前的漫射著色器適應紋理,我們必須新增以下變數
texture ModelTexture;
sampler2D TextureSampler = sampler_state {
Texture = (ModelTexture);
MagFilter = Linear;
MinFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
ModelTexture 是 HLSL 資料型別 texture,它儲存應該在模型上渲染的紋理。型別為 sampler2D 的另一個變數與紋理相關聯。取樣器告訴顯示卡如何從紋理檔案中提取一個畫素的顏色。取樣器包含五個屬性
- 紋理:要使用的紋理檔案。
- MagFilter + MinFilter: 用於縮放紋理的過濾器。一些過濾器比其他過濾器更快,其他過濾器看起來更好。可能的值包括:Linear、None、Point、Anisotropic。
- AddressU + AddressV: 確定當 U 或 V 座標不在正常範圍內(介於 0 和 1 之間)時該怎麼做。可能的值包括:Clamp、BorderColor、Wrap、Mirror。
我們使用 Linear 過濾器,它速度快且 Clamp,如果 U/V 值小於 0,它只使用值 0;如果 U/V 值大於 1,它只使用值 1。
接下來,我們在頂點著色器的輸出和輸入結構體中新增紋理座標,以便頂點著色器可以收集這種資訊並轉發給畫素著色器。
新增到結構體 VertexShaderInput 中
float2 TextureCoordinate : TEXCOORD0;
並新增到結構體 VertexShaderOutput 中
float2 TextureCoordinate : TEXCOORD0;
兩者都是 float2 型別(二維向量),因為我們只需要儲存兩個分量:U 和 V。這兩個變數也具有語義型別 TEXCOORD0。
將紋理的顏色應用於物件的過程發生在畫素著色器中,而不是在頂點著色器中。因此,在 VertexShaderFunction 中,我們只需將紋理座標從輸入中取出並放入輸出中
output.TextureCoordinate = input.TextureCoordinate;
在 PixelShaderFunction 中,我們執行以下操作
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
float4 VertexTextureColor = tex2D(TextureSampler, input.TextureCoordinate);
VertexTextureColor.a = 1;
return saturate(VertexTextureColor * input.VertexColor + AmbienceColor);
}
該函式現在根據紋理計算畫素的顏色。此外,顏色的 alpha 值在第二行單獨設定,因為 TextureSampler 沒有從紋理中獲取 alpha 值。
最後,在 return 語句中,頂點的紋理顏色乘以漫射顏色(將漫射陰影新增到紋理顏色),並像往常一樣新增環境顏色。
我們還需要在 technique 函式中進行更改。新的 PixelShaderFunction 現在對畫素著色器版本 1.1 來說太複雜了,因此需要將其設定為版本 2.0
PixelShader = compile ps_2_0 PixelShaderFunction();
紋理著色器的完整著色器程式碼
float4x4 WorldMatrix;
float4x4 ViewMatrix;
float4x4 ProjectionMatrix;
float4 AmbienceColor = float4(0.1f, 0.1f, 0.1f, 1.0f);
// For Diffuse Lightning
float4x4 WorldInverseTransposeMatrix;
float3 DiffuseLightDirection = float3(-1.0f, 0.0f, 0.0f);
float4 DiffuseColor = float4(1.0f, 1.0f, 1.0f, 1.0f);
// For Texture
texture ModelTexture;
sampler2D TextureSampler = sampler_state {
Texture = (ModelTexture);
MagFilter = Linear;
MinFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
struct VertexShaderInput
{
float4 Position : POSITION0;
// For Diffuse Lightning
float4 NormalVector : NORMAL0;
// For Texture
float2 TextureCoordinate : TEXCOORD0;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
// For Diffuse Lightning
float4 VertexColor : COLOR0;
// For Texture
float2 TextureCoordinate : TEXCOORD0;
};
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4 worldPosition = mul(input.Position, WorldMatrix);
float4 viewPosition = mul(worldPosition, ViewMatrix);
output.Position = mul(viewPosition, ProjectionMatrix);
// For Diffuse Lightning
float4 normal = normalize(mul(input.NormalVector, WorldInverseTransposeMatrix));
float lightIntensity = dot(normal, DiffuseLightDirection);
output.VertexColor = saturate(DiffuseColor * lightIntensity);
// For Texture
output.TextureCoordinate = input.TextureCoordinate;
return output;
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
// For Texture
float4 VertexTextureColor = tex2D(TextureSampler, input.TextureCoordinate);
VertexTextureColor.a = 1;
return saturate(VertexTextureColor * input.VertexColor + AmbienceColor);
}
technique Texture
{
pass Pass1
{
VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_2_0 PixelShaderFunction();
}
}
XNA 中的更改
在 XNA 程式碼中,我們必須透過宣告一個 Texture2D 物件來新增一個新的紋理
Texture2D planeTexture;
透過載入內容節點中先前新增的影像來載入紋理(在本例中,載入內容節點解決方案資源管理器中“Images”資料夾中的名為“planetextur.png”的檔案)
planeTexture = Content.Load<Texture2D>("Images/planetextur");
最後,在通常的繪製方法中將新紋理分配給著色器變數 ModelTexture
myEffect.Parameters["ModelTexture"].SetValue(planeTexture);
然後,物件應該具有紋理、漫射陰影和環境陰影,如示例影像所示。
帶有鏡面光和反射的高階陰影
[edit | edit source]
現在讓我們建立一個新的更復雜的特效,它看起來非常漂亮逼真,可以用來模擬金屬等光亮表面。我們將結合紋理著色器、鏡面著色器和反射著色器。反射著色器將反射預定義的環境
鏡面光在模型表面上新增光亮斑點以模擬光滑度。它們具有照射在表面上的光的顏色。
鏡面光與我們之前使用過的著色器的區別在於,它不僅受光線來自的方向的影響,還受觀看者觀察物件的方向的影響。因此,當相機在場景中移動時,鏡面光會在表面上移動。
反射著色器也是如此,根據觀看者的位置,物體表面的反射會發生變化。
像真實世界一樣計算反射意味著計算光線從表面反彈的單個光線(一種稱為光線追蹤的技術)。這需要大量的計算能力,這就是我們在 XNA 等即時計算機圖形中使用更簡單方法的原因。我們使用的技術稱為環境對映,並將環境影像對映到物體表面。當觀看者的位置發生變化時,這種環境對映會移動,從而產生反射的錯覺。這有一些侷限性,例如,物體只反射預定義的環境影像,而不是真實的場景。因此,玩家和其他所有移動模型都不會被反射。這有一些侷限性,但在即時應用程式中並不太明顯。
環境貼圖可以與場景的背景天空貼圖相同。有關背景天空貼圖的更多資訊,請參閱另一篇文章:Game Creation with XNA/3D Development/Skybox。如果環境貼圖與背景天空貼圖相同,它將適合場景並看起來準確,但是您可以使用任何看起來適合場景中模型的環境對映。
以下更改的基礎是之前開發的紋理著色器。對於鏡面光,需要新增以下變數
float ShininessFactor = 10.0f;
float4 SpecularColor = float4(1.0f, 1.0f, 1.0f, 1.0f);
float3 ViewVector = float3(1.0f, 0.0f, 0.0f);
ShininessFactor 定義了表面的光亮度。低值代表具有寬闊表面高光的表面,應該用於不太光亮的表面。高值代表更光亮的表面,如具有小而非常強烈的表面高光的金屬。理論上,鏡子將具有無限的值。
SpecularColor 指定鏡面光的顏色。在本例中,我們使用白光。
ViewVector 是一個變數,它將在執行時從 XNA 應用程式計算和設定。它告訴著色器觀看者正在看哪個方向。
對於反射著色器,我們需要新增環境紋理和取樣器作為變數
Texture EnvironmentTexture;
samplerCUBE EnvironmentSampler = sampler_state
{
texture = <EnvironmentTexture>;
magfilter = LINEAR;
minfilter = LINEAR;
mipfilter = LINEAR;
AddressU = Mirror;
AddressV = Mirror;
};
EnvironmentTexture 是環境影像,它將作為反射對映到我們的物體上。這次使用的是立方體取樣器,它與之前使用的二維取樣器略有不同。它假定提供的紋理是為在立方體上渲染而建立的。
VertexShaderInput 結構體不需要進行任何更改,但需要在 VertexShaderOutput 結構體中新增兩個新變數
float3 NormalVector : TEXCOORD1;
float3 ReflectionVector : TEXCOORD2;
NormalVector 只是單個頂點的法線向量,它直接來自輸入。反射向量是在頂點著色器中計算的,並在畫素著色器中使用,以將環境貼圖的正確部分分配到表面。兩者都具有語義型別 TEXCOORD。已經存在一個型別為 TEXCOORD0(TextureCoordinate)的變數,因此我們繼續計數到 1 和 2。
在 VertexShaderFunction 中,我們必須新增以下命令
// For Specular Lighting
output.NormalVector = normal;
// For Reflection
float4 VertexPosition = mul(input.Position, WorldMatrix);
float3 ViewDirection = ViewVector - VertexPosition;
output.ReflectionVector = reflect(-normalize(ViewDirection), normalize(normal));
首先,將先前計算的當前頂點的法線向量寫入輸出,因為它將在後面的畫素著色器中用於鏡面陰影。
對於反射,世界中的頂點位置以及觀看者看向頂點的方向將被計算出來。然後使用 HLSL 函式 reflect() 計算反射向量,該函式使用先前計算的法線向量和 ViewDirection 向量的歸一化值。
在 PixelShaderFunction 中,我們為鏡面值新增以下計算
float3 light = normalize(DiffuseLightDirection);
float3 normal = normalize(input.NormalVector);
float3 r = normalize(2 * dot(light, normal) * normal - light);
float3 v = normalize(mul(normalize(ViewVector), WorldMatrix));
float dotProduct = dot(r, v);
float4 specular = SpecularColor * max(pow(dotProduct, ShininessFactor), 0) * length(input.VertexColor);
因此,要計算鏡面高光,需要漫射光方向、法線、視角向量和亮度。最終結果是包含鏡面分量的另一個向量。
這個鏡面分量與反射一起新增到 PixelShaderFunction 末尾的 return 語句中
return saturate(VertexTextureColor * texCUBE(EnvironmentSampler, normalize(input.ReflectionVector)) + specular * 2);
在本例中,我們去掉了漫射和環境分量,因為在本例中,它們對於演示來說沒有必要,而且沒有它們看起來更好。沒有漫射光分量,看起來光線來自四面八方,並在光亮的金屬上反射。
因此,在 return 語句中,使用紋理顏色以及反射和鏡面高光(乘以 2 以使其更強烈)。
完成的著色器程式碼
float4x4 WorldMatrix;
float4x4 ViewMatrix;
float4x4 ProjectionMatrix;
float4 AmbienceColor = float4(0.1f, 0.1f, 0.1f, 1.0f);
// For Diffuse Lightning
float4x4 WorldInverseTransposeMatrix;
float3 DiffuseLightDirection = float3(-1.0f, 0.0f, 0.0f);
float4 DiffuseColor = float4(1.0f, 1.0f, 1.0f, 1.0f);
// For Texture
texture ModelTexture;
sampler2D TextureSampler = sampler_state {
Texture = (ModelTexture);
MagFilter = Linear;
MinFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
// For Specular Lighting
float ShininessFactor = 10.0f;
float4 SpecularColor = float4(1.0f, 1.0f, 1.0f, 1.0f);
float3 ViewVector = float3(1.0f, 0.0f, 0.0f);
// For Reflection Lighting
Texture EnvironmentTexture;
samplerCUBE EnvironmentSampler = sampler_state
{
texture = <EnvironmentTexture>;
magfilter = LINEAR;
minfilter = LINEAR;
mipfilter = LINEAR;
AddressU = Mirror;
AddressV = Mirror;
};
struct VertexShaderInput
{
float4 Position : POSITION0;
// For Diffuse Lightning
float4 NormalVector : NORMAL0;
// For Texture
float2 TextureCoordinate : TEXCOORD0;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
// For Diffuse Lightning
float4 VertexColor : COLOR0;
// For Texture
float2 TextureCoordinate : TEXCOORD0;
// For Specular Shading
float3 NormalVector : TEXCOORD1;
// For Reflection
float3 ReflectionVector : TEXCOORD2;
};
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4 worldPosition = mul(input.Position, WorldMatrix);
float4 viewPosition = mul(worldPosition, ViewMatrix);
output.Position = mul(viewPosition, ProjectionMatrix);
// For Diffuse Lighting
float4 normal = normalize(mul(input.NormalVector, WorldInverseTransposeMatrix));
float lightIntensity = dot(normal, DiffuseLightDirection);
output.VertexColor = saturate(DiffuseColor * lightIntensity);
// For Texture
output.TextureCoordinate = input.TextureCoordinate;
// For Specular Lighting
output.NormalVector = normal;
// For Reflection
float4 VertexPosition = mul(input.Position, WorldMatrix);
float3 ViewDirection = ViewVector - VertexPosition;
output.ReflectionVector = reflect(-normalize(ViewDirection), normalize(normal));
return output;
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
// For Texture
float4 VertexTextureColor = tex2D(TextureSampler, input.TextureCoordinate);
VertexTextureColor.a = 1;
// For Specular Lighting
float3 light = normalize(DiffuseLightDirection);
float3 normal = normalize(input.NormalVector);
float3 r = normalize(2 * dot(light, normal) * normal - light);
float3 v = normalize(mul(normalize(ViewVector), WorldMatrix));
float dotProduct = dot(r, v);
float4 specular = SpecularColor * max(pow(dotProduct, ShininessFactor), 0) * length(input.VertexColor);
return saturate(VertexTextureColor * texCUBE(EnvironmentSampler, normalize(input.ReflectionVector)) + specular * 2);
}
technique Reflection
{
pass Pass1
{
VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_2_0 PixelShaderFunction();
}
}
要在 XNA 中使用新的著色器,我們需要在繪製方法中從 XNA 設定 2 個額外的著色器變數
myEffect.Parameters["ViewVector"].SetValue(viewDirectionVector);
myEffect.Parameters["EnvironmentTexture"].SetValue(environmentTexture);
但首先,應該先宣告和載入物件的 environmentTexture(與往常一樣)
TextureCube environmentTexture;
environmentTexture = Content.Load<TextureCube>("Images/Skybox");
與模型紋理不同,這種紋理不是 Texture2D 型別,而是 TextureCube 型別,因為在本例中,我們使用背景天空貼圖作為環境貼圖。背景天空貼圖不僅包含一張影像(就像普通紋理一樣),而是六張不同的影像,這些影像對映到立方體的每個側面。影像必須以正確的角度配合在一起,並且必須無縫連線。您可以在此處找到一些背景天空貼圖:RB Whitaker 背景天空貼圖
其次,用於在反射著色器中設定 ViewVector 變數的 viewDirectionVector 應該在類中宣告為欄位
Vector3 viewDirectionVector = new Vector3(0, 0, 0);
它可以用這種方式計算
viewDirectionVector = cameraPositionVector – cameraTargetVector;
其中 cameraPositionVector 是一個包含相機當前位置的 3D 向量,cameraTargetVector 是另一個包含相機目標座標的向量。例如,如果相機只是看著虛擬空間中的 0,0,0 點,計算將更短
viewDirectionVector = cameraPositionVector;
//or
viewDirectionVector = new Vector3(eyePositionX, eyePositionY, eyePositionZ);
在 XNA 遊戲中進行所有這些更改後,反射應該像圖片中那樣。但外觀在很大程度上取決於使用的環境貼圖。
其他引數
[edit | edit source]另一個好主意是為著色器的強度引入引數。例如,而不是在上面的漫射著色器中畫素著色器函式的 return 語句中簡單地返回環境顏色
return saturate(input.VertexColor + AmbienceColor);
可以返回
return saturate(input.VertexColor + AmbienceColor * AmbienceIntensity);
其中 AmbienceIntensity 是介於 0.0 和 1.0 之間的浮點數。這樣就可以輕鬆地調整顏色的強度。這可以對我們到目前為止計算的每個元件(環境、漫射、紋理顏色、鏡面強度、反射分量)進行。

到目前為止,我們一直在使用 3D 著色器,但 2D 著色器也是可能的。2D 影像可以透過 Photoshop 等圖片編輯軟體進行修改和處理,以調整其對比度、顏色並應用濾鏡。同樣的效果可以透過 2D 著色器來實現,這些著色器應用於整個輸出影像,該影像是渲染場景的結果。
可實現的效果型別的示例
- 簡單的顏色修改,例如使場景變為黑白,反轉顏色通道,使場景呈現棕褐色等。
- 調整顏色,在場景中營造溫暖或冷色調。
- 使用模糊濾鏡模糊螢幕,以建立特殊效果。
- Bloom 效應:一種流行的效應,它在影像中非常明亮的物體周圍產生光暈,模擬攝影中已知的效應。
因此,我們首先在 Visual Studio 中建立一個新的著色器檔案(將其命名為 Postprocessing .fx)並插入以下程式碼以進行後處理
texture ScreenTexture;
sampler TextureSampler = sampler_state
{
Texture = <ScreenTexture>;
};
float4 PixelShaderFunction(float2 TextureCoordinate : TEXCOORD0) : COLOR0
{
float4 pixelColor = tex2D(TextureSampler, TextureCoordinate);
pixelColor.g = 0;
pixelColor.b = 0;
return pixelColor;
}
technique Grayscale
{
pass Pass1
{
PixelShader = compile ps_2_0 PixelShaderFunction();
}
}
如您所見,對於後處理,我們只需要一個畫素著色器。後處理是透過將場景的渲染影像作為紋理提供來處理的,然後畫素著色器將該紋理用作輸入資訊,進行處理並返回。
該函式只有一個輸入引數(紋理座標)並返回語義型別 COLOR0 的顏色向量。在這個例子中,我們只是讀取當前紋理座標(也就是螢幕座標)處的畫素顏色,並將綠色和藍色通道設定為 0,這樣就只剩下紅色通道了。然後我們返回顏色值。
現在,在 XNA 中使用這個 2D 著色器有點棘手。首先,我們需要在 Game 類中建立以下物件
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
RenderTarget2D renderTarget;
Effect postProcessingEffect;
GraphicsDeviceManager 和 SpriteBatch 物件很可能已經在一個現有專案中建立了。但是,RenderTarget2D 和 Effect 物件必須宣告。
檢查 GraphicsDeviceManager 物件是否在建構函式中初始化
graphics = new GraphicsDeviceManager(this);
並且 SpriteBatch 物件在 LoadContent() 方法中初始化。我們剛剛建立的新著色器檔案也應該在此方法中載入
spriteBatch = new SpriteBatch(GraphicsDevice);
postProcessingEffect = Content.Load<Effect>("Shaders/Postprocessing");
最後,確保 RenderTarget2D 物件在 Initialize() 方法中初始化
renderTarget = new RenderTarget2D(
GraphicsDevice,
GraphicsDevice.PresentationParameters.BackBufferWidth,
GraphicsDevice.PresentationParameters.BackBufferHeight,
1,
GraphicsDevice.PresentationParameters.BackBufferFormat
);
現在,我們需要一個方法將當前場景繪製到紋理(以渲染目標的形式)而不是螢幕上
protected Texture2D DrawSceneToTexture(RenderTarget2D currentRenderTarget) {
// Set the render target
GraphicsDevice.SetRenderTarget(0, currentRenderTarget);
// Draw the scene
GraphicsDevice.Clear(Color.Black);
drawModelWithTexture(model, world, view, projection);
// Drop the render target
GraphicsDevice.SetRenderTarget(0, null);
// Return the texture in the render target
return currentRenderTarget.GetTexture();
}
在這個方法中,我們使用的是使用 3D 著色器(在本例中為:drawModelWithTexture())的繪製函式。所以我們仍然使用所有 3D 著色器來先渲染場景,但是我們不是直接顯示這個結果,而是將它渲染到一個紋理上,並在 Draw() 方法中對其進行一些後處理。之後,處理過的紋理將顯示在螢幕上。所以用這個擴充套件 Draw() 方法
protected override void Draw(GameTime gameTime)
{
Texture2D texture = DrawSceneToTexture(renderTarget);
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);
postProcessingEffect.Begin();
postProcessingEffect.CurrentTechnique.Passes[0].Begin();
spriteBatch.Draw(texture, new Rectangle(0, 0, 1024, 768), Color.White);
postProcessingEffect.CurrentTechnique.Passes[0].End();
postProcessingEffect.End();
spriteBatch.End();
base.Draw(gameTime);
}

首先,正常的場景被渲染到一個名為 texture 的紋理中。然後,一個精靈批處理程式與包含我們新的後處理著色器的 postProcessing 效應一起啟動。然後,紋理在精靈批處理程式上渲染,應用了 postProcessing 效應。
效果應該像圖片中那樣。
另一個可以通過後處理著色器實現的簡單效果是將彩色影像轉換為灰度影像,然後將其縮減為 4 種顏色,從而產生卡通效果。為此,我們著色器檔案中的 PixelShaderFunction 應該如下所示
float4 PixelShaderFunction(float2 TextureCoordinate : TEXCOORD0) : COLOR0
{
float4 pixelColor = tex2D(TextureSampler, TextureCoordinate);
float average = (pixelColor.r + pixelColor.g + pixelColor.b) / 3;
if (average > 0.95){
average = 1.0;
} else if (average > 0.5){
average = 0.7;
} else if (average > 0.2){
average = 0.35;
} else{
average = 0.1;
}
pixelColor.r = average;
pixelColor.g = average;
pixelColor.b = average;
return pixelColor;
}
灰度影像透過計算紅色、綠色和藍色通道的平均值並使用此值作為所有三個通道的值來生成。之後,平均值還被縮減為 4 個不同的值之一。最後,輸出的紅色、綠色和藍色通道被設定為縮減後的值。影像為灰度,因為紅色、綠色和藍色通道的值相同。

建立透明度著色器很簡單。我們可以從上面的漫射著色器示例開始。首先,我們需要一個名為 alpha 的變數來確定透明度。該值應在 1(不透明)到 0(完全透明)之間。為了實現透明度著色器,我們只需要對 PixelShaderFunction 進行一些修改。在完成所有光照計算後,我們必須將 alpha 值分配給結果顏色屬性。
float alpha = 0.5f;
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
float4 color = saturate(input.VertexColor + AmbienceColor);
color.a = alpha;
return color;
}
要啟用 alpha 混合,我們必須在技術中新增一些程式碼
technique Tranparency {
pass p0 {
AlphaBlendEnable = TRUE;
DestBlend = INVSRCALPHA;
SrcBlend = SRCALPHA;
VertexShader = compile vs_2_0 std_VS();
PixelShader = compile ps_2_0 std_PS();
}
}
完整的透明度著色器
float4x4 WorldMatrix;
float4x4 ViewMatrix;
float4x4 ProjectionMatrix;
float4 AmbienceColor = float4(0.2f, 0.2f, 0.2f, 1.0f);
// For Diffuse Lightning
float4x4 WorldInverseTransposeMatrix;
float3 DiffuseLightDirection = float3(-1.0f, 0.0f, 0.0f);
float4 DiffuseColor = float4(1.0f, 1.0f, 1.0f, 1.0f);
struct VertexShaderInput
{
float4 Position : POSITION0;
// For Diffuse Lightning
float4 NormalVector : NORMAL0;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
// For Diffuse Lightning
float4 VertexColor : COLOR0;
};
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4 worldPosition = mul(input.Position, WorldMatrix);
float4 viewPosition = mul(worldPosition, ViewMatrix);
output.Position = mul(viewPosition, ProjectionMatrix);
// For Diffuse Lightning
float4 normal = normalize(mul(input.NormalVector, WorldInverseTransposeMatrix));
float lightIntensity = dot(normal, DiffuseLightDirection);
output.VertexColor = saturate(DiffuseColor * lightIntensity);
return output;
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
float4 color = saturate(input.VertexColor + AmbienceColor);
color.a = alpha;
return color;
}
technique Diffuse
{
pass Pass1
{
AlphaBlendEnable = TRUE;
DestBlend = INVSRCALPHA;
SrcBlend = SRCALPHA;
VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_1_1 PixelShaderFunction();
}
}
一些其他流行的著色器及其簡短描述。
-
應用於球體的均勻表面的凹凸貼圖
-
透過使用法線貼圖將細節新增到低多邊形模型
-
卡通著色器
凹凸貼圖用於模擬原本均勻的多邊形表面的凹凸,使表面看起來更真實,併除了紋理之外還賦予其一些結構。凹凸貼圖是透過載入包含凹凸資訊的另一張紋理並使用此資訊擾亂表面法線來實現的。表面的原始法線會根據凹凸貼圖中的偏移值而改變。凹凸貼圖是灰度影像。
凹凸貼圖現在已被法線貼圖取代。法線貼圖也用於在原本均勻的多邊形表面上建立凹凸和結構。但與凹凸貼圖相比,法線貼圖可以更好地處理法線的急劇變化。
法線貼圖與凹凸貼圖的思路類似:載入另一張紋理並使用它來改變法線。但法線貼圖不只是用偏移值來改變法線,而是使用多通道(RGB)貼圖來完全替換現有的法線。法線貼圖中每個畫素的 R、G 和 B 值對應於頂點法向量的 X、Y、Z 座標。
法線貼圖的進一步發展稱為視差貼圖。
卡通著色器用於以卡通風格渲染 3D 場景,使其看起來像是手繪的。卡通著色可以透過使用多通道著色器在 XNA 中實現,該著色器在多個通道中構建結果影像。

要建立卡通著色器,我們可以從漫射著色器開始。卡通著色器背後的基本思想是,光強將被劃分為多個級別。在這個例子中,我們將強度建立為 5 個級別。為了表示亮度級別,我們需要一個名為 toonthresholds 的陣列變數,並且為了確定級別之間的邊界,我們使用陣列 toonBrightnessLevels。
float ToonThresholds[4] = {0.95,0.5, 0.2, 0.03 };
float ToonBrightnessLevels[5] = { 1.0, 0.8, 0.6, 0.35, 0.01 };
現在,我們在 PixelShader 中實現光強度的分類,並將它們分配到相應的顏色中。
float4 std_PS(VertexShaderOutput input) : COLOR0 {
float lightIntensity = dot(normalize(DiffuseLightDirection),
input.normal);
if(lightIntensity < 0)
lightIntensity = 0;
float4 color = tex2D(colorSampler, input.uv) *
DiffuseLightColor * DiffuseIntensity;
color.a = 1;
if (lightIntensity > ToonThresholds[0])
color *= ToonBrightnessLevels[0];
else if ( lightIntensity > ToonThresholds[1])
color *= ToonBrightnessLevels[1];
else if ( lightIntensity > ToonThresholds[2])
color *= ToonBrightnessLevels[2];
else if ( lightIntensity > ToonThresholds[3])
color *= ToonBrightnessLevels[3];
else
color *= ToonBrightnessLevels[4];
return color;
}
完整的卡通著色器
float4x4 World : World < string UIWidget="None"; >;
float4x4 View : View < string UIWidget="None"; >;
float4x4 Projection : Projection < string UIWidget="None"; >;
texture colorTexture : DIFFUSE <
string UIName = "Diffuse Texture";
string ResourceType = "2D";
>;
float3 DiffuseLightDirection = float3(1, 0, 0);
float4 DiffuseLightColor = float4(1, 1, 1, 1);
float DiffuseIntensity = 1.0;
float ToonThresholds[4] = {0.95,0.5, 0.2, 0.03 };
float ToonBrightnessLevels[5] = { 1.0, 0.8, 0.6, 0.35, 0.01 };
sampler2D colorSampler = sampler_state {
Texture = <colorTexture>;
FILTER = MIN_MAG_MIP_LINEAR;
AddressU = Wrap;
AddressV = Wrap;
};
struct VertexShaderInput {
float4 position : POSITION0;
float3 normal :NORMAL0;
float2 uv : TEXCOORD0;
};
struct VertexShaderOutput {
float4 position : POSITION0;
float3 normal : TEXCOORD1;
float2 uv : TEXCOORD0;
};
VertexShaderOutput std_VS(VertexShaderInput input) {
VertexShaderOutput output;
float4 worldPosition = mul(input.position, World);
float4 viewPosition = mul(worldPosition, View);
output.position = mul(viewPosition, Projection);
output.normal = normalize(mul(input.normal, World));
output.uv = input.uv;
return output;
}
float4 std_PS(VertexShaderOutput input) : COLOR0 {
float lightIntensity = dot(normalize(DiffuseLightDirection),
input.normal);
if(lightIntensity < 0)
lightIntensity = 0;
float4 color = tex2D(colorSampler, input.uv) *
DiffuseLightColor * DiffuseIntensity;
color.a = 1;
if (lightIntensity > ToonThresholds[0])
color *= ToonBrightnessLevels[0];
else if ( lightIntensity > ToonThresholds[1])
color *= ToonBrightnessLevels[1];
else if ( lightIntensity > ToonThresholds[2])
color *= ToonBrightnessLevels[2];
else if ( lightIntensity > ToonThresholds[3])
color *= ToonBrightnessLevels[3];
else
color *= ToonBrightnessLevels[4];
return color;
}
technique Toon {
pass p0 {
VertexShader = compile vs_2_0 std_VS();
PixelShader = compile ps_2_0 std_PS();
}
}
FX Composer 是一個用於著色器創作的整合開發環境。使用 FX Composer 建立我們自己的著色器非常有用。使用 Fx Composer,我們可以很快看到結果,並且對著色器進行一些實驗非常有效。

在這個例子中,我使用的是 FX Composer 2.5 版。將 FX Composer 庫用於你自己的 XNA 中是一項非常簡單的任務。讓我們以一個例子開始。開啟 FX Composer 並建立一個新專案。在材質上右鍵單擊,選擇“從檔案新增材質”,然後選擇 metal.fx。
您只需要將 metal.fx 中的所有程式碼複製並建立一個新的效果在您的 XNA 專案中,並將所有內容替換為 metal fx 中的程式碼。您也可以將 metal.fx 檔案複製到您的 XNA 專案中。
從這裡開始,我們只需要根據 metal.fx 中的變數對 XNA 類進行一些修改。
在 metal.fx 中,您可以看到以下程式碼
// transform object vertices to world-space:
float4x4 gWorldXf : World < string UIWidget="None"; >;
// transform object normals, tangents, & binormals to world-space:
float4x4 gWorldITXf : WorldInverseTranspose < string UIWidget="None"; >;
// transform object vertices to view space and project them in perspective:
float4x4 gWvpXf : WorldViewProjection < string UIWidget="None"; >;
// provide transform from "view" or "eye" coords back to world-space:
float4x4 gViewIXf : ViewInverse < string UIWidget="None"; >;
在我們的 XNA 類中,我們必須更改 ParameterEffect 的名稱。
Matrix InverseWorldMatrix = Matrix.Invert(world);
Matrix ViewInverse = Matrix.Invert(view);
effect.Parameters["gWorldXf"].SetValue(world);
effect.Parameters["gWorldITXf"].SetValue(InverseWorldMatrix);
effect.Parameters["gWvpXf"].SetValue(world*view*proj);
effect.Parameters["gViewIXf"].SetValue(ViewInverse);
我們還必須更改 XNA 類中的技術名稱。因為 XNA 使用 directX9,所以我們選擇“技術簡單”。
effect.CurrentTechnique = effect.Techniques["Simple"];
現在,您可以使用金屬效果執行程式碼。
完整的函式
private void DrawWithMetalEffect(Model model, Matrix world, Matrix view, Matrix proj){
Matrix InverseWorldMatrix = Matrix.Invert(world);
Matrix ViewInverse = Matrix.Invert(view);
effect.CurrentTechnique = effect.Techniques["Simple"];
effect.Parameters["gWorldXf"].SetValue(world);
effect.Parameters["gWorldITXf"].SetValue(InverseWorldMatrix);
effect.Parameters["gWvpXf"].SetValue(world*view*proj);
effect.Parameters["gViewIXf"].SetValue(ViewInverse);
foreach (ModelMesh meshes in model.Meshes)
{
foreach (ModelMeshPart parts in meshes.MeshParts)
parts.Effect = basicEffect;
meshes.Draw();
}
}

為了在 XNA 中建立粒子效果,我們使用點精靈。點精靈是一個可調整大小的紋理頂點,它始終面向相機。我們使用點精靈渲染粒子的原因有很多。
- 點精靈只使用一個頂點。對於數千個粒子,它可以減少大量的頂點。
- 無需儲存或設定對映 UV 座標。它會自動完成。
- 點精靈始終面向相機。因此,我們無需擔心角度和檢視。
建立點精靈著色器非常簡單,我們只需要在畫素著色器中進行一些實現來定義紋理座標。
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
float2 uv;
uv = input.uv.xy;
return tex2D(Sampler, uv);
}
在頂點著色器中,我們只需要為頂點返回 POSITION0。
float4 VertexShader(float4 pos : POSITION0) : POSITION0
{
return mul(pos, WVPMatrix);
}
為了啟用點精靈並設定點精靈的屬性,我們在技術中執行此操作。
technique Technique1
{
pass Pass1
{
sampler[0] = (Sampler);
PointSpriteEnable = true;
PointSize = 16.0f;
AlphaBlendEnable = true;
SrcBlend = SrcAlpha;
DestBlend = One;
ZWriteEnable = false;
VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_1_1 PixelShaderFunction();
}
}
完整的點精靈著色器
float4x4 World;
float4x4 View;
float4x4 Projection;
float4x4 WVPMatrix;
texture spriteTexture;
sampler Sampler = sampler_state
{
Texture = <spriteTexture>;
magfilter = LINEAR;
minfilter = LINEAR;
mipfilter = LINEAR;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
float2 uv :TEXCOORD0;
};
float4 VertexShaderFunction(float4 pos : POSITION0) : POSITION0
{
return mul(pos, WVPMatrix);
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
float2 uv;
uv = input.uv.xy;
return tex2D(Sampler, uv);
}
technique Technique1
{
pass Pass1
{
sampler[0] = (Sampler);
PointSpriteEnable = true;
PointSize = 32.0f;
AlphaBlendEnable = true;
SrcBlend = SrcAlpha;
DestBlend = One;
ZWriteEnable = false;
VertexShader = compile vs_1_1 VertexShaderFunction();
PixelShader = compile ps_1_1 PixelShaderFunction();
}
}
現在讓我們轉到 game1.cs 檔案。首先,我們需要宣告並載入效果和紋理。為了儲存位置頂點,我們使用 VertexPositionColor 元素陣列。頂點的位置應使用隨機數初始化。
Effect pointSpriteEffect;
VertexPositionColor[] positionColor;
VertexDeclaration vertexType;
Texture2D textureSprite;
Random rand;
const int NUM = 50;
....
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
textureSprite = Content.Load<Texture2D>
("Images//texture_particle");
pointSpriteEffect = Content.Load<Effect>
("Effect//PointSprite");
pointSpriteEffect.Parameters
["spriteTexture"].SetValue(textureSprite);
positionColor = new VertexPositionColor[NUM];
vertexType = new VertexDeclaration(graphics.GraphicsDevice,
VertexPositionColor.VertexElements);
rand = new Random();
for (int i = 0; i < NUM; i++) {
positionColor[i].Position =
new Vector3(rand.Next(400) / 10f,
rand.Next(400) / 10f, rand.Next(400) / 10f);
positionColor[i].Color = Color.BlueViolet;
}
}
下一步,我們建立 DrawPointsprite 方法來繪製粒子。
public void DrawPointsprite() {
Matrix world = Matrix.Identity;
pointSpriteEffect.Parameters
["WVPMatrix"].SetValue(world*view*projection);
graphics.GraphicsDevice.VertexDeclaration = vertexType;
pointSpriteEffect.Begin();
foreach (EffectPass pass in
pointSpriteEffect.CurrentTechnique.Passes)
{
pass.Begin();
graphics.GraphicsDevice.DrawUserPrimitives
<VertexPositionColor>(
PrimitiveType.PointList,
positionColor,
0,
positionColor.Length);
pass.End();
}
pointSpriteEffect.End();
}
我們在 Draw() 方法中呼叫 DrawPointSprite() 方法。
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
DrawPointsprite();
base.Draw(gameTime);
}
為了使位置動態化,我們在 Update() 方法中進行了一些實現。
protected override void Update(GameTime gameTime)
{
positionColor[rand.Next(0, NUM)].Position =
new Vector3(rand.Next(400) / 10f,
rand.Next(400) / 10f, rand.Next(400) / 10f);
positionColor[rand.Next(0, NUM)].Color = Color.White;
base.Update(gameTime);
}
這是一個非常簡單的點精靈著色器。您可以使用動態大小和顏色建立更復雜的點精靈。
完整的 game1.cs
namespace MyPointSprite
{
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Matrix view, projection;
Effect pointSpriteEffect;
VertexPositionColor[] positionColor;
VertexDeclaration vertexType;
Texture2D textureSprite;
Random rand;
const int NUM = 50;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{
view =Matrix.CreateLookAt
(Vector3.One * 40, Vector3.Zero, Vector3.Up);
projection =
Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4,
4.0f / 3.0f, 1.0f, 10000f);
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
textureSprite =
Content.Load<Texture2D>("Images//texture_particle");
pointSpriteEffect =
Content.Load<Effect>("Effect//PointSprite");
pointSpriteEffect.Parameters
["spriteTexture"].SetValue(textureSprite);
positionColor = new VertexPositionColor[NUM];
vertexType = new VertexDeclaration
(graphics.GraphicsDevice, VertexPositionColor.VertexElements);
rand = new Random();
for (int i = 0; i < NUM; i++) {
positionColor[i].Position =
new Vector3(rand.Next(400) / 10f,
rand.Next(400) / 10f, rand.Next(400) / 10f);
positionColor[i].Color = Color.BlueViolet;
}
}
protected override void Update(GameTime gameTime)
{
positionColor[rand.Next(0, NUM)].Position =
new Vector3(rand.Next(400) / 10f,
rand.Next(400) / 10f, rand.Next(400) / 10f);
positionColor[rand.Next(0, NUM)].Color = Color.Chocolate;
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
DrawPointsprite();
base.Draw(gameTime);
}
public void DrawPointsprite() {
Matrix world = Matrix.Identity;
pointSpriteEffect.Parameters
["WVPMatrix"].SetValue(world*view*projection);
graphics.GraphicsDevice.VertexDeclaration = vertexType;
pointSpriteEffect.Begin();
foreach (EffectPass pass in
pointSpriteEffect.CurrentTechnique.Passes)
{
pass.Begin();
graphics.GraphicsDevice.DrawUserPrimitives
<VertexPositionColor>(
PrimitiveType.PointList,
positionColor,
0,
positionColor.Length);
pass.End();
}
pointSpriteEffect.End();
}
}
}
HLSL 簡介和一些更高階的示例 最後訪問時間:2011 年 6 月 9 日
另一個 HLSL 簡介 最後訪問時間:2011 年 6 月 9 日
關於如何在 XNA 中使用著色器的非常出色且詳細的教程 最後訪問時間:2012 年 1 月 15 日
微軟官方 HLSL 參考 最後訪問時間:2011 年 6 月 9 日
- Leonhard Palm:基礎、GPU 管道、畫素和頂點著色器、HLSL、XNA 示例
- DR 212:BasicEffect 類、透明度著色器、卡通著色器、FX Composer、粒子效果