Dx effect 文件 .fx 與 Shader - 頂點着色和像素着色


[Nebula3 幀渲染Frame系統]





[補充說明 .fx 文件]


這里簡要說明一下 fx 文件, fx 文件是渲染管線的配置文件,

主要由三部分組成:變量聲明,管道狀態technique, pass,渲染函數, 

1.  變量聲明在文件開始,這些變量可以在運行時操作,格式如下,

type id : tag = {initiliaze value} ;  

其中 type 必須是 HLSL 可識別的類型, id 必須唯一, tag 用來表示變量的用途,  后面是初始值

2. 管道狀態, 一個 fx 文件可以配置多個 technique,  一個 technique 可以配置多個 pass 特殊如下,

technique (tch_uniqueId)

{

     pass (pass_uniqueId)

     {

          Texture[0] = <diffuseTexture>;

          VertexShader = compile vs_2_2 vsMain()

          PiexlShader = compile ps_2_2 psMain()

     }

}

配置的 technique , pass 可以根據 顯卡的支持,以及應用程序的需求,選擇性的激活相應的 technique 和 pass

3. 渲染函數,   這個就是傳統意義上的着色程序,定點着色,片段着色等

創建

至此我們大概知道了 fx 目的是干嘛的,然后通過如下的方法使用,先需要創建一個Effect對應的對象,

LPD3DXEFFECT anEffect;
D3DXCreateEffectFromFile(gDevice,"fxfname.fx",NULL,NULL,0,NULL,&anEffect,NULL)

然后 找到對應的 technique, 可以用如下的方法

D3DXHANDLE hTech;
anEffect->FindNextValidTechnique(NULL,&hTech);

N3 把每個 technique 分解到不同的 特性組, 激活的時候根據特效組選擇相應的 , 具體實現如下:

technique niqueId <string Mask  = "feature">

{

     pass uniqueId

     {

     }

}

D3DXHANDLE hFeatureAnnotation = this->d3d9Effect->GetAnnotationByName(this->hTechnique, "Mask");

hr = this->d3d9Effect->GetString(hFeatureAnnotation, &strPtr);

讓一個 technique 和 一個 Mask 標注的 feature 綁定,然后通過 feature 來激活 technique。

使用

對於 D3DXEFFECT 按照 如下結構使用:

if (SUCCEEDED(anEffect->SetTechnique(hTech)))
{
   UINT numPasses;
   nEffect->Begin(&passes,0);

for (UINT i=0;i<numPasses;i++)

{
    anEffect->BeginPass(i); // Set the pass
    // render geometry e.g. DrawIndexedPrimitive
    anEffect->EndPass();
}

   anEffect->End();

}

交互

對於與EFFECT的交互,設置Effect文件中定義的變量,可以通過如下的接口:

anEffect->SetTexture("t0",texture1);
anEffect->SetMatrix("world",&worldMatrix);
anEffect->SetMatrix("camera",&viewMatrix);
anEffect->SetVector("var1", v);

這里有一個非常有用的特性,就是變量可以設置成共享的,共享的范圍是一個 effect pool, 這樣只用設置一次變量,pool 中的所有 effect 就都有了它的值。

總結

effect文件完全把渲染管線的狀態設置解耦出來了,以前渲染流程中的各種狀態都是根據資源類型先在代碼中寫死了的。現在全部在配置文件中。

dx的 .x 文件是可以包含 .fx 的。mesh 和 renderstate 的對應關系。

最后這里推薦幾個用來編寫fx和shader的軟件

dx 的 effectEdit

nv 的 fx composer

atm 的 rendermonkey

[補充說明 shader]


着色程序無處不在,這里從概念觸發,補充一下基本定義。

在渲染場景的時候,顯卡需要處理場景的幾何數據和紋理信息。在很久很久以前,顯卡基本沒有封裝幾個算法來處理這些傳遞給他的這些數據,這個時代就是固定管線時代,簡稱FFP(fixed function pipeline),在這個時代,程序只是在渲染前,選擇顯卡中封裝的固定管線,然后設置顯卡的各種可控狀態,用這種方式導致的問題就是,很難創造一些獨特畫面的游戲,因為程序員控制不了渲染數據的處理過程,只能用固定的方法,推送數據給顯卡。為了解決這個問題,嘗試對顯卡深度的自由定制,皮克斯科技有限公司在2001年,成功的推出了第一款具備着色能力的渲染硬件。

着色通常是通過兩種方式,頂點着色用來操作頂點數據,像素着色用來操作像素數據。sm5.0時代已經多了一種數據,叫做幾何數據,就是很多頂點組成的具備一定幾何意義的頂點序列。在着色時代,着色代碼被加載到顯存,直接干預顯卡的渲染管線。開始的時候,着色代碼是以匯編的形式出現,在推出一段時間后,就出現了幾種類C的高級語言,這些高級語言可以被編譯成顯卡識別的匯編代碼。比如微軟給dx定制的HLSL(High-Level Shading Language) 和 OpenGL 定制的 GLSL( OpenGL Shading  Language)。顯卡的硬件廠商也提供了一些高級語言,比如Nvidia的 Cg 以及 ATI的 ASHLI ( Advanced Shading Language Interface)。大家期望在將來只會由一個通用的着色語言(HLSL和Cg實際上是同一個語言,只是因為各個公司的品牌目的而叫的不同的名字)。 這里集中在DirectX上的渲染描述,所以在下面的描述中,shaders 都是以 HLSL語言編寫,應用於Direct3D上。

DirectX10 需要特別說明一點, DirectX10 提供的是 SM 4.0, 只在Vista平台上面運行,提供了如下的幾個特性:

  • 統一的着色片段 - 不區分是定點着色還是像素着色
  • 幾何着色 - 能夠在GPU中生成新的頂點數據
  • 資源虛擬化 - 顯卡也支持虛擬顯存,也就是顯卡在顯存不夠的時候,直接使用主機的內存

Direct3D 渲染管線

為了知道怎么編寫着色代碼,我們需要先知道這些着色代碼是怎么和3D渲染管線結合。

Wolfgang Engel 寫了非常多的關於shader的號文章,強烈建議你去GameDev上看下他的文章,了解下shaders是怎么切入渲染管線的, 而且如果你想在shading這一塊做深入的研究和探索的話,也強烈建議你去拜讀一下他的書《Programming Vertex and Pixel Shaders》。

一個簡化版本的渲染管線如下:

  • 應用程序
    • 場景管理
    • 網格頂點和曲面
  • 頂點 操作
    • 變換和光照
    • 裁剪
  • 像素操作
    • 單元和光柵化
    • 着色和多層紋理操作
    • 霧化,Alpha測試,深度緩存,抗鋸齒
    • 顯示設備繪圖
其中 
  • 應用程序 是指游戲程序本身,游戲程序本身根據游戲邏輯管理實體對象,維護游戲場景,對網格數據進行細分,並對場景進行高層裁剪等。
  • 通過前面的步驟,就會提交頂點和紋理等一些數據到顯卡,顯卡根據應用程序傳遞給它的相關矩陣進行頂點變換和頂點光照計算,接着顯卡會裁剪掉任何視口之外的多邊形和幾何片段。
  • 這些變換后的數據接着被單元和光柵化后被傳遞給像素操作后最終傳遞給顯示設備進行繪制操作。

這里面提到了一些不那么明顯的名詞,單元化和光柵化,我們知道頂點數據是一個三維的坐標,而計算機顯示的時候是平面的,所以這里面就有一個比較重要的對應關系,而且單獨的一個頂點也不能在設備中進行關聯繪制,頂點都是屬於某一個基本單元,一個三角形或者一個曲面,所以在經歷了頂點操作后,像素顏色的具體操作前,需要把相關的頂點關聯起來,組成他們本來的幾何基本單元,這樣,我們才知道怎么繪制每一個頂點。在頂點組成基本的幾何單元后,要把這個幾何單元繪制到屏幕上,【為了簡化這里直接把單元限定在三角形】還需要一個映射和插值過來,比如一個三維的幾何三角形,我們需要先把三個頂點映射到屏幕上,然后三個頂點在屏幕上構建的三角形區域也就是我們看到的三維三角形的一個二維映射,而根據三個頂點,我們還需要插值很多像素出來,填滿整個三角形。這樣由三個3維頂點組成的幾何三角形,就被映射到了屏幕上的一個三角區域。整個過程中,有兩個很重要的步驟,把頂點組成三角形,並同時把三個頂點映射到屏幕上,在屏幕上的三個點,構建一個像素區域,像素區域中的每個像素的相關顏色數據都需要根據已知的三個頂點所在的像素數據插值而來。而且每一個像素都可以反推到一個或者多個三維的頂點。這么一長段就是非常直觀的表達,單元和光柵化操作,對於光柵化其實還有更深入的一些操作,比如像素點坐標系的變化,從視口坐標到設備坐標等。

有了前面的細說,我們看下着色是在管線的那些地圖起作用:

  • 應用程序
    • 場景管理
    • 網格頂點和曲面
  • 頂點 操作
    • 變換和光照 T&L or    VERTEX SHADER <------
    • 裁剪
  • 像素操作
    • 單元和光柵化
    • 着色和多層紋理操作 or PIXEL SHADER <-----
    • 霧化,Alpha測試,深度緩存,抗鋸齒
    • 顯示設備繪圖

從上面的示例我們可以知道,如果我們寫一個頂點着色片段,我們就可以控制管線當中的 T&L操作,而且提供了頂點着色片段后,我們必須對頂點數據進行這些操作,不能再直接把數據傳遞給顯卡了。像素着色片段可以操作三角形中的每一個像素並且控制貼圖等操作。注意像素着色這個步驟是出現在alpha 測試前的,所以我們處理的像素在經歷像素着色后有可能會在后面的測試中被丟棄。

我們不能控制管線中的所有步驟,在將來可能會出現其他類型的着色片段,讓我們在管線中取得更多的控制權,比如,幾何着色片段,【這個已經有了,在sm4.0中】

重要注意事項

  • 頂點着色片段一次只操作一個頂點
  • 頂點着色片段不能增加頂點
  • 像素着色片段一次只操作一個像素
  • 像素着色片段不能增加像素

在游戲中使用着色片段

Direct3D 有一個非常便利的 effects 類,用來解析和使用.fx文件。 這個運行你在運行時編輯和激活着色片段。不同的顯卡支持的特性不一樣,所以很難寫一些在所有平台上通用而且復雜的着色片段,但是在使用effect文件后,你可以寫一系列的shader,然后根據不同的顯卡激活相應的shader。DX的SDK帶了一個叫 Effect Edit的工具,可以加載和查看effect文件,這個非常便利,可以讓你即刻看到變更的顯示效果。當你深入學習后,你可能需要更加專業和強大的工具,這個時候你可以看下 ATI 公司的免費IDE軟件 RenderMonkey, 這個軟件可以導出.fx文件。

對頂點着色和像素着色有了一些初步認識后,下面將分別深入兩個,進行一些研究。

[補充說明頂點着色片段 Vertex Shader]


這里講更加深入的描述shaders中的Vertex Shader。頂點着色能夠操作頂點相關的數據。 這小節分為如下幾個小節:

  • 頂點數據
  • 應用頂點Shader
  • 頂點着色例子
    • 例子1 簡單的頂點坐標變換
    • 例子2 繪制一個飄動的旗子
    • 例子3 頂點的光照計算
  • 總結

頂點數據

頂點數據的格式依賴於我們自己的應用程序。比如我們為每個頂點傳遞一個position, 一個 normal 以及對於需要貼圖的幾何結構傳遞紋理的uv和動態光照參數,我們也可以只給每個頂點一個坐標以及顏色數據。用固定管線,我們可以聲明一個自定義的頂點數據的結構體,然后通過Direct3D的FVF聲明,構造一個結構體對應的標志位來表達我們的頂點結構體的格式,讓固定管線認識我們的頂點結構體的格式。 這里我們既然是用着色程序,我們就可以如下使用頂點聲明頂點結構體:

struct TVertex
{
   D3DXVECTOR3 position;
   D3DXVECTOR3 Normal;
   D3DXVECTOR3 Tex;
};

然后我們用DirectX3D的FVF方式構建一個結構體的格式描述,

const D3DVERTEXELEMENT9 dec[4] =
{
  {0, 0,  D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION,0},
  {0, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL,  0},
  {0, 24, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD,0},
  D3DDECL_END()
};

上述的描述結構體中,每一行都對應我們頂點結構體中的一個成員變量,聲明的格式為:

{ Stream, Offset, Type, Method, Usage, UsageIndex }

格式的細節說明參考DX的文檔中關於 D3DVERTEXELEMENT9的說明。其中的Type對應到D3DDECLTYPE中枚舉的類型之一,其中Method描述的是tessellator怎么處理頂點數據,其中Usage是告訴數據項的用途,用途可以是 坐標, 法線, UV坐標,切線,融合權重等。注意,如果我們想再頂點數據中傳遞我們自己的數據,我們可以用D3DDECLUSAGE_TEXCOORD來描述數據項的用途。在格式描述的最后一行需要必須放在一個D3DDECL_END。

有了上面的頂點數據格式描述后,我們需要在設備中創建一個聲明,如下聲明:

IDirect3DVertexDeclaration9 m_vertexDeclaration;
gDevice->CreateVertexDeclaration(dec,&m_vertexDeclaration);

如果上述的調用成功后(對於那些返回HRESULT的D3D函數,你最好是每個都檢查一下返回值),m_vertexDeclaration就會指向相應的Direct3D對象。有個頂點數據格式聲明,在我們進行具體的渲染,把頂點數據的個buffer傳遞給設備的時候,我們需要用m_vertexDeclaration告訴設備我們頂點buffer的格式。也就是進行如下調用:

gDevice->SetStreamSource( 0, m_vb,0, sizeof(TVertex));
gDevice->SetVertexDeclaration(m_vertexDeclaration);
// Render

應用頂點Shader

我們可以用接口 D3DXCompileShaderFromFile 從文件編譯寫好的.fx文件,然后用接口 CreateVertexShader 創建VertexShader,最后通過接口 SetVertexShader 為渲染過程設置頂點Shader,並通過接口 SetVertexShaderConstantX 與Shader進行交互,設置Shader中的常量。這里就不細節描述這個應用過程了,因為前面已經說過怎么用effect文件來處理這個過程,而且用effect文件更優。

頂點着色例子

既然已經知道怎么聲明頂點數據,以及怎么應用頂點着色代碼到渲染過程中,接下來我們將關注一下着色代碼本身。

在開始編寫HLSL的時候,你可能會發現C與HLSL中的最大區別是在內在類型上,在着色代碼中我們用一些類似float2, float3的類型,不過HLSL也是對C的大部分類型是支持的,比如float, int, bool, double等,着色代碼中,大部分C的操作符也是可以用的。對於循環有一點點不一樣,HLSL中沒有最大值的指令。

頂點着色有兩類輸入數據,uniform數據代表的是常理,會存儲在常理寄存器中,還有一個varying數據,這個數據會存儲在輸入寄存器中。在編寫HLSL的時候,我們需要告訴編譯器數據的類型,好讓編譯器把輸入放入正確類型的寄存器中。這種類型的區別,我們是通過使用輸入語義來表達的,比如NORMAL, POSITION, COLOR 等。對於 Uniform數據,他不需要指定語義,因為他是放在常理寄存器中。下面我們看一些具體的代碼片段。

頂點着色例子1 頂點坐標變換

這個着色代碼,包含在一個effect文件中,通過下面的鏈接 SimpleVShader.fx. 下載:你可以把她加載到 DirectX的Effect Edit中,如果是2004Dec版本后的DX,以及么有Effect Edit,可以找替代軟件RenderMonkey 或者 fx composer 。用老版本的Effect Edit ,你可以在界面左邊看到effect文件的代碼,而在右邊看到effect文件應用到一個老虎模型上的效果。這樣就可以有非常直觀的視角觀察着色代碼的渲染效果。如下圖所示:

Image(117)

// transformations provided by the app, constant Uniform data
float4x4 matWorldViewProj: WORLDVIEWPROJECTION;

// the format of our vertex data
struct VS_OUTPUT
{
  float4 Pos  : POSITION;
};

// Simple Vertex Shader - carry out transformation
VS_OUTPUT VS(float4 Pos  : POSITION)
{
  VS_OUTPUT Out = (VS_OUTPUT)0;
  Out.Pos = mul(Pos,matWorldViewProj);
  return Out;
}

第一行定義了一個輸入常量 MatWorldViewProj, 這是一個4*4的矩陣。把頂點從模型空間變換到視點空間,需要對頂點依次執行下面的變換,顯示世界矩陣,變換到世界坐標系,然后是視點矩陣和投影矩陣,更多細節參考關於矩陣的章節。我們可以為上述的每一個矩陣定義一個輸入常量,我們也可以為上述矩陣的乘積定義一個矩陣傳給着色代碼。語義 WORLDVIEWPROJECTION 就是告訴Effect Edit程序,這個常量可以由應用程序設置。

前面提到了,頂點着色接受一個頂點數據作為輸入,頂點早色對輸入的頂點進行一些修改,然后把數據返回到渲染管線,所以我們也需要顯示的定義出我們着色代碼的輸出結構體,我們定義了一個結構體 VS_OUTPUT ,用來存儲一個3D坐標,並且用語義 POSITION 告訴編譯器這個varying數據放到正常的寄存器中作為頂點的坐標數據。其他的類型的語義還有PSIZE, FOG, COLORn and TEXCOORDn., n一般不要超過8,以前的顯卡最多就支持8個。

然后接下來是頂點着色的入口函數,在函數中,我們必須清晰的寫出所有輸入和輸出參數,如代碼中所示,我們的頂點着色函數,輸出一個 VS_OUTPUT 結構體,並且接受一個 POSITION。

下面的一行,我們生命了一個類型的輸出變量,並且用0初始化它。然后我們通過矩陣 matWorldViewProj對頂點執行標准的 world * view * projection 變換。最后返回變換后的頂點數據。上面的着色代碼其實是一個非常簡單的着色程序。

頂點着色例子2 飄動的旗子

這個例子展示了怎么操作頂點數據,來實現一個旗子飄動的效果。通過下面的鏈接 VShader3.zip下載着色代碼和紋理。

Image(118)

在這個例子中,我們只是操作了一下頂點的左邊,就制作了非常逼真的旗子飄動效果。為了實現這種飄動,我們需要一個連續變化的輸入作為角度,然后傳遞給sin函數,這里我選擇的是時間。當然你可以使用任何你想要的變量來作為輸入,並且你也可以在着色代碼外計算這個變量,然后當做uniform傳遞進來。

effect文件第一部分什么一些全局的Uniform變量,並且通過語義把它們鏈接到effect Edit上,類似在Fx Composer 中生命變量的UI編輯配置,比如:  float4x4 matWorld : WORLD;        聲明了一個叫做matWorld的變量,並且告訴effect edit要把世界矩陣設置到變量里面,相當於要在游戲中做如下的調用: dxEffect->SetMatrix("matWorld",&mat))).                            

我們希望着色代碼輸出變換后的頂點坐標以及一個相應的紋理坐標, 所以我們定義如下的輸出結構體:

struct VS_OUTPUT
{
  float4 Pos : POSITION;
  float2 tex : TEXCOORD0;
};

我們的着色程序接受頂點變換前的坐標和一個相應的紋理坐標作為輸入,然后輸出變換后的坐標,同時把UV坐標直接輸出。函數頭如下:

VS_OUTPUT VS(float4 Pos : POSITION,float2 tex : TEXCOORD0)

接下來我們需要一個連續性的輸入作為角度值,來進行sin求值,因此我們用一個模除,把值變到 0 到 360度,代碼如下:

float angle=(time%360)*2;

在例子代碼中,我對角度值做了一點點縮放,讓旗子飄動得更快,你可以嘗試不同的縮放值,查看不一樣的效果。你也可以傳遞其他的uniform變量來做角度值,查看不一樣的效果。

這里使用的3D模型是一個平面網格,位於x, y平面上,z值指向平面里面,所以讓旗子飄動的話,只需改變頂點的z值。如下代碼所示:

Pos.z  = sin( Pos.x+angle);

上述的代碼就會給出一個波浪效果,如下:

Image(119)

這個飄動只會在一個方向上,為了更逼真一點,我們可以讓z值依賴 ,x 和 y 兩個維度,如下計算:

Pos.z += sin( Pos.y/2+angle);

上述對於y進行了一個除2操作,只是一個普通的參數,你可以參數不同的值,來嘗試不同的效果。如下:

Image(120)

上面的飄動效果已經有所改善,看起啦更加真實一些。然而還有一個可以改善的是,旗子的左邊被綁定到旗桿上,這個時候要求左邊不能飄動,這個時候,

可以做如下的一個變更,讓x值越小的時候,z值的變化也越小。

Pos.z *= Pos.x * 0.09f;

這個時候就會得到如下效果:

Image(121)

頂點着色例子3 頂點光照

下面看一下稍微復雜一點點的着色代碼,在這個例子中,我們會對頂點做一些光照計算和頂點貼圖計算,你可以在這個鏈接 VShader2.fx.下載代碼。

Image(122)

effect edit 裝載指定的模型和紋理,燈光后,操作場景中代表燈光的黃色箭頭,可以實時觀察燈光對場景的影響,因為我們需要計算光照,所以我們需要聲明和初始化一些光照相關的參數,我用float4來存儲需要的燈光的red, green, blue 和  alpha。如下所示:

// light intensity
float4 I_a = { 0.1f, 0.1f, 0.1f, 0.1f };    // ambient
float4 I_d = { 1.0f, 1.0f, 1.0f, 1.0f };    // diffuse
float4 I_s = { 1.0f, 1.0f, 1.0f, 1.0f };    // specular

除了上述的變量,我們也需要定義模型材質對於光源的一些反射參數,如下:

// material reflectivity
float4 k_a : MATERIALAMBIENT = { 1.0f, 1.0f, 1.0f, 1.0f };    // ambient
float4 k_d : MATERIALDIFFUSE = { 1.0f, 1.0f, 1.0f, 1.0f };    // diffuse
float4 k_s : MATERIALSPECULAR= { 1.0f, 1.0f, 1.0f, 1.0f };    // specular
float  n   : MATERIALPOWER = 32.0f;                           // power

現在回到具體的着色計算的代碼段,我們的着色代碼需要輸出頂點變換后的坐標,計算后的diffuse 和 specular顏色值,和頂點的UV坐標。所以我們定義一個如下的輸出結構體:

struct VS_OUTPUT
{
    float4 Pos  : POSITION;
    float4 Diff : COLOR0;
    float4 Spec : COLOR1;
    float2 Tex  : TEXCOORD0;
};

着色代碼的輸入為,模型空間的頂點坐標,頂點的法線,以及頂點的UV坐標,在頂點着色階段,我們沒有用UV坐標做任何計算,只是簡單的把它作為輸出參數傳遞給像素着色。着色入口如下所示:

VS_OUTPUT VS(
    float3 Pos  : POSITION,
    float3 Norm : NORMAL,
    float2 Tex  : TEXCOORD0)

接下來是頂點着色的函數體,和前面的例子一樣,我們創建一個輸出變量的實例,並用0初始化這個結構體。和前面的例子不一樣的地方是,我們定義了三個單獨的矩陣,分別是世界矩陣,視點矩陣(也叫視圖矩陣),和投影矩陣,而不是傳遞一個模型視點投影矩陣。接下來的兩行,我們把頂點坐標從模型空間變到視點空間:

float4x4 WorldView = mul(World, View);
float3 P=mul(float4(Pos, 1), (float4x3)WorldView);  // position (view space)

講解光照計算的公式已經超出了本文的范圍,有很多地方,很詳細的描述了光照計算的具體原理,參見這個 Implementing Lighting Models with HLSL鏈接。當討論像素着色的時候,也會再次討論光照計算公式。這里先貼上光照計算的着色代碼:

float3 N = normalize(mul(Norm, (float3x3)WorldView)); // normal (view space)
float3 R = normalize(2 * dot(N, L) * N - L);          // reflection vector
float3 V = -normalize(P);                             // view direction

Out.Pos  = mul(float4(P, 1), Projection);             // position (projected)
Out.Diff = I_a * k_a + I_d * k_d * max(0, dot(N, L)); // diffuse + ambient
Out.Spec = I_s * k_s * pow(max(0, dot(R, V)), n/4);   // specular

這里不講解光照的理論,我們這里只是對上述代碼做一個簡要的流程說明,上述代碼中,我們把光照計算放在視點空間中進行,其中N代表在視圖空間的法線,R是視圖空間的發射向量。其中函數 Normalize 對向量執行正規化操作,而dot 執行向量的點乘。

代碼中的最后三行填充輸出結構體,首先把頂點左邊從視圖空間變到投影空間,然后把ambient和diffuse的和當做diffuse顏色。其中Ambient不依賴於光源的法向也不依賴頂點表面材質,只是一個簡單的強度化顏色值。其中Diffuse依賴於模型表面,等於intensity * colour,並且與頂點和光源的角度值成比例關系。縮放的比例因子由頂點法向和頂點到光源的向量的點乘決定。最后specular的顏色值由 pow(x,y) 計算。

總結

  • 通過頂點着色片段,我們可以把頂點計算算法插入顯卡的渲染管線代替顯卡本身硬件實現的算法
  • 頂點着色,截取渲染管線中的頂點相關的數據作為輸入,然后對頂點數據進行修改后,可以把數據繼續輸出到顯卡的渲染管線
  • 頂點着色代碼一次只處理一個頂點
  • 頂點着色中,我們可以修改頂點相關的所有數據,因為我們的着色代碼是替換了顯卡渲染管線的T&L操作,所以頂點着色代碼最少要把頂點坐標從模型空間變換到視圖空間
  • 我們可以定義一些uniform 數據,作為頂點着色中的常量,並且顯卡也會把這些數據放入常量寄存器,這些unifrom數據,是可以在應用程序中運行時被設值的。在着色代碼中,我們也可以定義一些varying 數據,比如頂點坐標,顏色值等,不過我們必須為這些數據指定語義,好讓編輯器知道數據的用途。

希望上面的幾條,以及讓你對使用和編寫頂點着色程序有一個大概的了解。關於頂點着色的內容還是很豐富的,如果你要更加深入的了解相關信息,建議你閱讀Wlfgame Engel的樹《Programming Vertex and Pixel Shaders》( Resources )。

當頂點數據從着色代碼出來后,會依次進入顯卡渲染管線中的,裁剪階段,單元化階段,和光柵化階段,然后才會進入像素着色階段。進入像素着色階段后,我們就可以寫像素着色代碼來代替顯卡的像素着色算法了。下面的一小節就會變數,像素着色 : Pixel Shaders

[補充說明像素着色片段 Pixel Shader]


閱讀者節前,需要先了解和。着色程序中,頂點着色和像素着色有很多共同的概念,這本小節中,只會討論他們之間不同的地方。本小節分為如下段落:

  • 介紹
  • 像素着色的輸入
  • 像素着色的輸出
  • 應用像素着色片段
  • 像素着色的例子
    • 像素着色例子1 Ambient Light
    • 像素着色例子2 Diffuse Light
  • 總結

介紹

像素着色允許你操作渲染管線中關於像素的材質上色和貼圖上色的這個過程。像素着色片段接受采樣后的像素點數據,包括像素點的顏色,z值(深度),以及貼圖數據。你也可以接受渲染管線前面一些階段存在的數據,比如法線等。在像素着色代碼中你可以寫相關的紋理操作指令。

像素着色的輸入

和頂點着色一樣,我們可以用語義來聲明變量的用途,像素着色中可以使用的語義包括:

  • VFACE - 圖元
  • VPOS - 像素坐標 (x, y)
  • COLORn - 顏色值
  • TEXCOORDn 紋理坐標(貼圖坐標)

其中n是一個整數,標明的相關寄存器的索引值。其中TEXCOORDn也可以用來傳遞紋理坐標意外的自定義數據

像素着色的輸出

當我們結束着色代碼對像素數據的處理后,我們需要輸出像素相關的一些數據,同樣,這里我們也使用語義來告訴變量的用途,讓后面的渲染管線繼續執行。

  • COLORn - 標明是render target n 的顏色數據
  • DEPTH - 深度值(z值)

應用像素着色片段

可以用接口 D3DXCompileShaderFromFile,編譯像素着色代碼,然后用接口 CreatePixelShader 創建像素着色片段,用接口SetPixelShader. 設置管線的像素着色入口,可以通過接口 SetPixelShaderConstantX. 與像素着色片段進行交互。當然我們更加願意通過effect文件來應用像素着色片段。

像素着色的例子

下面我們看一些非常簡單的例子,逐像素光照。

像素着色例子1 - Ambient Light ( 環境光照)

Ambient light 就是場景中的自然光,它來自於場景中所有對象對光線的多次反射,環境光,沒有方向,沒有坐標,所以它對場景中所有幾何體進行同樣的光照。計算環境光,只需要考慮環境光的強度,以及光的顏色值。下面就是環境光的計算公式:

I = A intensity * A colour

其中, A intensity是一個浮點數,標明的是光的強度,A colour是一個顏色值,包括紅,綠,藍,和alpha四個分量。

我們的着色代碼簡單的輸出一個顏色值,所以我們定義如下的輸出結構體:

struct PSOutput
{
    float4 colour : COLOR;
};

我們的像素着色片段看起來如下:

PSOutput PS()

注意,從一個着色代碼返回值,有多種反射,如果我們的輸出只是一個顏色值,而不是一個結構體,我們可以如下聲明我們的着色函數:

float 4 PS(): COLOR

在這里例子中,我們用結構體的方式返回顏色數據,下個例子我們用直接輸出的方式返回顏色數據。因此在這個例子中,着色代碼中,我們需要創建一個輸出結構體的實例,並用0初始化,如下所示:

PSOutput Out=(PSOutput)0;

然后我們把環境光的強度和顏色值,在代碼中硬編碼如下:

float Aintensity=0.8f;
float4 Acolour=float4(1.0,0.5,0.5,1.0);

然后是環境光的計算,把計算的結果放入結構體,並返回:

Out.colour=Aintensity*Acolour;
return Out;

Image(123)

這樣一個簡單的像素着色就已經完成了。你可以從這個鏈接下載代碼,然后把它放入effect edit觀察效果。

像素着色例子2 - Diffuse Light (漫反射光照)

在1760年,Lambert最早用一個光照模型來描述場景中的有向光。在這個光照模型中,物體表面的光照計算與觀察者的位置無關,因此,這個光照模型通常被用來模擬一些粗糙材質的表面的光照。反射光的強度依賴於光的方向和物體表面法線方向之間的夾角。例如,如果光直射物體表面,並且物體表面是一個平面,這樣物體就會被完整的照亮。但是,如果光照的方向和物體表面的朝向相反的話,物體表面就沒有光照。

Image(124)

如果L  是光照向量(光照射的方向), N 是物體表面的法線,當LN 的方向一致的時候:也就是物體表面與光照方向垂直 cos(theta)=1,就會發生最大強度的漫反射。兩個方向之間的角度越小,反射強度也越小,因此反射光的強度與 cos(theta)成比例。 為了實現漫反射的這種特性,我們用如下的點成:

N.L = ||N||.||L||*cos(theta)

如果光照的方向向量和物體表面的法線向量是單位向量(在具體應用中,我們肯定會保證這一點),上述公式就會被退化為:

N.L=cos(theta)

然后我們就可以在着色低嗎中用如下的公式計算我們像素點的漫反射的顏色值:

Dintensity * Dcolour * N.L

為了完成這個例子,我們將寫一個頂點着色片段,一個像素着色片段,展示怎么在頂點着色片段中計算好我們在像素着色片段中用到的相關數據。你可以從下面的鏈接下載着色的effect文件 PShaderExample2.fx.。

頂點着色,在頂點着色中,我們接受如下的結構體作為輸入:

struct VS_OUTPUT{
    float4 Pos : POSITION;
    float3 Light : TEXCOORD0;
    float3 Norm : TEXCOORD1;
};

然后對頂點坐標進行標准變換,然后把光照方向向量歸一化,然后把頂點法線變換到世界坐標系,並進行歸一化,具體代碼如下:

VS_OUTPUT VS(float4 Pos : POSITION, float3 Normal : NORMAL)
{
    VS_OUTPUT Out = (VS_OUTPUT)0;
    Out.Pos = mul(Pos, matWorldViewProj); // transform Position
    Out.Light = normalize(vecLightDir); // normalised light vector
    Out.Norm = -normalize(mul(Normal, matWorld)); // transform Normal and normalize
    return Out;
}

注意上述代碼中的 matWorldViewProj 是應用程序設置的,並且指定了語義 WORLDVIEWPROJECTION ,告訴effect Edit對變量填充正確的值。

像素着色,在這個像素着色中,我們值輸出一個簡單的顏色值,但是從渲染管線接受一個光照向量以及一個法線向量作為輸入,注意到這里我們使用的是 TEXCOORDn 語義。這一次我們沒有用結構體來做着色函數的返回對象,而是直接返回一個單一的顏色值。

float4 PS(float3 Light: TEXCOORD0, float3 Norm : TEXCOORD1) : COLOR

在這個着色代碼中,我們綜合考慮環境光和漫反射光,這樣具體代碼如下:

float4 result=Dintensity*Dcolour*(dot(Norm,Light));
// Add the diffuse result to the ambient below for return
return Aintensity*Acolour+result;

上述計算環境光的時候,我們在代碼中硬編碼了環境光的強度和顏色值,在具體的應用中,你肯定不會這樣寫,你肯定會定義相應的uniform變量,然后由應用程序傳遞進來。

總結

像素着色運行你操作渲染管線的像素,顏色數據的計算階段。他們對於逐像素光照計算是非常便利的,而且也非常方便應用其他高級的像素操作指令。希望上面的描述已經讓你對像素着色有了一個基本的了解,這個地方,也希望在不久的將來能夠增加一些像素着色的例子。

進階閱讀

如果要了解更多細節,和高級應用,可以看一下Wlfgame Engel的書,去下面的鏈接 books 可以發現一些推薦的相關書籍。




如果對於fx文件以及shader不怎么了解,建議閱讀下面兩個文章:

http://www.toymaker.info/Games/html/effects_files.html

http://www.toymaker.info/Games/html/shaders.html

這里推薦一下這個網站,這個是提賽德大學的游戲開發技能課程的老師建立的一個個人網站。網站內容非常豐富。值得收藏。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM