XNA 4.0 Phong Shading 實作 shader implement

XNA 4.0 Phong Shading 實作 shader implement

接下來就是進行實作的部分,時做的部分這邊我只說明實作可能會想問的問題,詳細內容各位可以下載原始的程式碼。

(本範例程式碼,包含使用基本的basicEffect特效將模型顯示)

模型的部分,這邊我是使用OBJ檔案格式,在XNA Creater club上面有一個範例,裡面是使用自定的content pipeline來將obj檔案資訊分析出來,這邊我們就直接運用它即可。

在renderer.cs的程式碼中,裡面有一個函數use_BasicEffect程式碼如下

   1:  public void use_BasicEffect()
   2:  {
   3:      for (int i = 0; i < scene_model.Length; i++)
   4:      {
   5:           foreach (ModelMesh mesh in scene_model[i].Meshes)
   6:           {
   7:               foreach (BasicEffect effect in mesh.Effects)
   8:               {
   9:                   effect.EnableDefaultLighting();
  10:                   effect.World = Matrix.Identity;
  11:                   effect.View = camera.View;
  12:                   effect.Projection = camera.Projection;
  13:                }
  14:                mesh.Draw();
  15:            }
  16:       }
  17:  }

 

上面程式碼有使用camera.cs這些相關的資源可以在右邊的地方可以找到。(component.rar)

首先,我們先來看shader code的部分,你可以在content的資料夾按下右鍵"增加新的項目",會出現下圖的圖式
image

選擇Effect file,然後給定一個你所指定的名稱。
確定以後,他會幫你自動產生一個可以執行的shader code如下所示

   1:  float4x4 World;
   2:  float4x4 View;
   3:  float4x4 Projection;
   4:   
   5:  // TODO: add effect parameters here.
   6:   
   7:  struct VertexShaderInput
   8:  {
   9:      float4 Position : POSITION0;
  10:   
  11:      // TODO: add input channels such as texture
  12:      // coordinates and vertex colors here.
  13:  };
  14:   
  15:  struct VertexShaderOutput
  16:  {
  17:      float4 Position : POSITION0;
  18:   
  19:      // TODO: add vertex shader outputs such as colors and texture
  20:      // coordinates here. These values will automatically be interpolated
  21:      // over the triangle, and provided as input to your pixel shader.
  22:  };
  23:   
  24:  VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
  25:  {
  26:      VertexShaderOutput output;
  27:   
  28:      float4 worldPosition = mul(input.Position, World);
  29:      float4 viewPosition = mul(worldPosition, View);
  30:      output.Position = mul(viewPosition, Projection);
  31:   
  32:      // TODO: add your vertex shader code here.
  33:   
  34:      return output;
  35:  }
  36:   
  37:  float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
  38:  {
  39:      // TODO: add your pixel shader code here.
  40:   
  41:      return float4(1, 0, 0, 1);
  42:  }
  43:   
  44:  technique Technique1
  45:  {
  46:      pass Pass1
  47:      {
  48:          // TODO: set renderstates here.
  49:   
  50:          VertexShader = compile vs_2_0 VertexShaderFunction();
  51:          PixelShader = compile ps_2_0 PixelShaderFunction();
  52:      }
  53:  }

 

float4x4的型態代表的是一個 4 by 4的陣列,也就是我們在XNA中用的matrix。這邊宣告了三個,分別是World、View、Projection。代表的意義如下:
World: 世界矩陣,用來做縮放平移旋轉等操作,另一個簡單的說法是將模型從自己的空間(object space)轉換到世界座標(world coordinate)。通常我們會在XNA的code中使用Matrix.Identity。不過可以根據情況做模型的縮放平移等操作。
View: 這個矩陣主要做的事情就是,將觀察者的位置(view position)轉換到原點。
Projection: 將投影的大小調整到[-1, 1]之間。

接下來是Struct的部分,這邊的語法都跟C很像,所以懂C的話應該不會太困難看懂才是

struct VertexShaderInput
{
    float4 Position : POSITION0;

    // TODO: add input channels such as texture
    // coordinates and vertex colors here.
};
這邊是指你宣告了一個結構VertexShaderInput,然後這個結構中包含了一個float4的資訊,而它所指定的型態就是POSITION。float4是變數型態,Position是指變數名稱,分號後面指的是語意(給HLSL知道說現在是甚麼東西需要處理)。語意的部分有蠻多項目的,詳情可以參考MSDN,不過常用的通常是POSITION、NORMAL、TEXCOORD。

函數的部分跟C很像結構通常是:
傳回的變數型態 函數名稱(傳入的變數型態 變數名稱)
{
}

最後面的一個看起來有點怪的東西,這邊需要特別說明

technique Technique1
{
    pass Pass1
    {
        // TODO: set renderstates here.

        VertexShader = compile vs_2_0 VertexShaderFunction();
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

technique指的是說現在你在XNA,如果你要指定使用哪個技術。
像是effect.Technique["Technique1"];
這樣就是說我現在要使用Technique1的shader技術,然後在Technique1裡面,又可能包含很多的Pass,所以我們也可以指定說我們現在要用哪個pass。EX: effect.Technique["Technique"].Pass["Pass1"].Apply();

在pass1中裡面有兩行,有兩個關鍵字VertexShader和PixelShader這兩個名稱是不可以變動的!!分別是說現在編譯的shader code各自的函數是分別送給VertexShader和PixelShader處理。

後面的compile的部分,就是說現在是使用第幾版的shader model,目前最新的shader model是到5.0,不過XNA只能支援到3.a所以這部分大家要稍微注意一下。

簡介完上面的shader code以後,我們現在要寫phong shading話,我們先看看phong shading的計算公式,如下:
I_p = k_a i_a + \sum_\mathrm{m \; \in \; lights} (k_d (L_m \cdot N) i_d + k_s (R_m \cdot V)^{\alpha}i_s).

由上面公式知道,我們需要傳入光的位置、攝影機的位置,和模型的材質資訊等等。至於頂點和normal這些我們可以讓pipeline幫我們送入就好了。

所以我們在shader code的上方需要加入以下的程式碼:

// TODO: add effect parameters here.
float3 light_position;
float3 light_diffuse;
float3 light_specular;

float4 kd;
float3 ks;
float ns;

float3 camera_position;

然後,在VertexShader的input中,我們需要將結構稍微做些調整,因為我們需要把normal送進來。

struct VertexShaderInput
{
    float4 Position : POSITION0;

    // TODO: add input channels such as texture
    // coordinates and vertex colors here.
    float3 Normal : NORMAL0;
};

在output的地方也要調整,至於output過後,接下來就是交給pipeline去做內插了,而我們希望頂點跟法向量都可以被內差。所以輸出的vertex shader結構也要做些調整。

struct VertexShaderOutput
{
    float4 Position : POSITION0;

    // TODO: add vertex shader outputs such as colors and texture
    // coordinates here. These values will automatically be interpolated
    // over the triangle, and provided as input to your pixel shader.
    float4 Pos : TEXCOORD0;
    float3 Nor : TEXCOORD1;
};
這邊輸出的部分語意是使用TEXCOORD而非使用COLOR,因為用COLOR內差會有問題,然後後面的數字分別是說輸出的channel,記得數字不可以一樣!

接下來,在pixel shader部分就是進行phong lighting的計算。

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    // TODO: add your pixel shader code here.
    float3 value = 0;

    float3 Light_Direction = normalize(light_position - input.Pos);

    float NdotL = saturate(dot(normalize(input.Nor), Light_Direction));

    if(NdotL>0)
    {
        value += light_diffuse*kd*NdotL;
        float3 E = normalize(camera_position - input.Pos);
        float3 R = reflect(-Light_Direction, normalize(input.Nor));

        // blinn
        float3 H = normalize(Light_Direction+E);

        float specular = pow(saturate(dot(E,normalize(R))),ns);

        // blinn
        //float specular = pow(saturate(dot(input.Nor,H)),ns);
        value+= light_specular*ks*specular;
    }

    return float4(value, 1);
}

這邊程式碼我就不多做說明,因為主要就是計算上述所說的公式…
可能需要注意的就是這邊所指的向量方向,這邊所說的向量方向,全部都是"往外"發射的向量,像是E這個向量來說,就是"眼睛-頂點做標"。

接下來,在XNA的程式碼我們就需要一個Effect的特效變數,並且用content pipeline將他載入近來。

完成以後,在繪製模型的地方我們就希望能夠套入Phong shading的效果。

我們需要先傳入一些變數到shader去

   1:  phong_effect.Parameters["World"].SetValue(Matrix.Identity);
   2:  phong_effect.Parameters["View"].SetValue(camera.View);
   3:  phong_effect.Parameters["Projection"].SetValue(camera.Projection);
   4:  phong_effect.Parameters["camera_position"].SetValue(camera.Position);
   5:  phong_effect.Parameters["light_position"].SetValue(light_position);
   6:  phong_effect.Parameters["light_diffuse"].SetValue(light_diffuse);
   7:  phong_effect.Parameters["light_specular"].SetValue(light_specular);

Parameters裡面的""變數名稱,必須跟Shader code是相互對應的!

像是KD KS NS這些變數,我們必須要從模型裡面獲得才行,所以我們在繪製模型的時候在抓取資訊。

   1:  foreach (Model model in scene_model)
   2:  {
   3:        foreach (ModelMesh mesh in model.Meshes)
   4:        {
   5:             foreach (ModelMeshPart meshPart in mesh.MeshParts)
   6:             {
   7:                  phong_effect.Parameters["kd"].SetValue(meshPart.Effect.Parameters["DiffuseColor"].GetValueVector4());
   8:                  phong_effect.Parameters["ks"].SetValue(meshPart.Effect.Parameters["SpecularColor"].GetValueVector3());
   9:                  phong_effect.Parameters["ns"].SetValue(meshPart.Effect.Parameters["SpecularPower"].GetValueSingle());
  10:   
  11:                  phong_effect.Techniques[0].Passes[0].Apply();
  12:                  GraphicsDevice.Indices = meshPart.IndexBuffer;
  13:                  GraphicsDevice.SetVertexBuffer(meshPart.VertexBuffer);
  14:                  GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList,meshPart.VertexOffset, 0, meshPart.NumVertices, meshPart.StartIndex, meshPart.PrimitiveCount);
  15:             }
  16:             mesh.Draw();
  17:         }
  18:  }

接下來執行以後就可以得到下面圖片的結果
image
可以看到黃色的BOX有specular的效果

如果沒有用phong lighting效果如下
image

詳細內容還是建議各位可以參考一下範例程式碼
下載

SHADER這邊就是要多多練習,需要習慣一下會比較容易上手。

對於shader有問題大家也可以一起討論看看~~