上一章里,我們詳細討論了HLSL著色語言的各方面。但并沒有實際展示如何編寫shader。雖然本書不是關于如何編寫shader的,但還是有必要編寫幾個簡單的shader,幫你深入了解HLSL。此外,在學習effect framework時,我們還會用到這些例子來闡述一些核心概念。
記住,對創建一個完整的shader來說,不僅僅是編寫shader代碼,還包括用適當的語義符設置一系列渲染狀態和變量。當然,由于目前你還缺乏編寫完整shader的一些知識,所以,這里只討論前者:也就是頂點和像素著色程序代碼。本書的后面會對這些代碼進行擴展。
最簡單的Shader
對于把物體渲染到屏幕上來說,有幾個基本的步驟是必須完成的。首先,需要接收輸入頂點的位置(頂點在世界坐標中的位置)并把它們轉變為屏幕坐標。通常使用world-view-projection矩陣來完成這個任務,它包含了把頂點從局部坐標映射為最終屏幕坐標的所有信息。現在開始,我們假設已經有這樣一個矩陣變量,并且名稱為view_proj_matrix。
先來定義一個把數據從頂點著色器傳遞給像素著色器的結構。我們把這個結構稱為VS_OUTPUT,當然,也可以是任何你喜歡的名字。目前,只需要把頂點位置數據添加到這個結構中。
struct VS_OUTPUT
{
float4 Pos: POSITION;
};
你應該注意到我們把POSITION語義連接到了Pos變量上,它將告訴effect系統如何把這個變量傳遞到像素著色器中。我會在下一章講解語義。現在只差頂點著色器代碼了。頂點著色器接收頂點位置,并使用view_proj_matrix矩陣對它進行變換,可以用內建的mul函數來完成這一步計算。我們把頂點著色器代碼放到一個名為vs_main的函數中:
VS_OUTPUT vs_main ( float4 inPos : POSITION)
{
VS_OUTPUT Out;
// output a transformed and projected vertex position
Out.Pos = mul ( view_proj_matrix , inPos);
return Out;
}
這里同樣使用了POSITION語義修飾輸入參數inPos。它將告訴頂點著色器把幾何體數據流信息映射為這個參數的輸入值。接下來進入完成這個簡單shader的第二步。現在你知道了頂點在屏幕上的位置,可以定義頂點的顏色了。最簡單的方法就是把所有頂點的顏色都設置為一個常量。通常像素著色器將返回一個float4類型的值來表示當前像素在屏幕上的顏色值,float4分量分別表示紅色,綠色,藍色和alpha值。我們把像素著色器代碼放到一個名為ps_main的函數中:
float4 ps_main ( void ) : COLOR
{
//Output constant color
float4 Color;
color[0] = color[3] = 1.0; // red and alpha on
color[1] = color[2] = 0.0;// Green and Blue off
return color;
}
這幾乎是最簡單的代碼了,注意我們用COLOR語義修飾了函數的返回值,它將告訴編譯器把函數返回值作為當前像素的顏色值。
著色
我們已經有渲染物體所需的最基本代碼了,如何把紋理映射到幾何體上,讓物體看起來更加真實呢?對于需要使用紋理的shader來說,需要有一個sampler類型的全局變量。在后面的章節中,我會教你如何使用語義和effect framework來設置紋理狀態。目前我們假設已經設置了好了紋理狀態:
sampler Texture0;
使用紋理之前,還需要知道知道對紋理的哪一部份進行采樣映射,因此,每個像素都必須有相應的紋理坐標。一般情況下,紋理坐標將作為幾何體信息的一部分輸入到頂點著色器中,經由頂點著色器計算處理之后,傳入到像素著色器中。通常使用TEXCOORDx語義來修飾作為參數傳遞的紋理坐標。這個語義將會告訴硬件如何在頂點和像素著色器之間交換數據。以下是修改之后的頂點著色器代碼:
struct VS_OUTPUT
{
float4 Pos : POSITION;
float2 Txr1: TEXCOORD0;
}
VS_OUTPUT vs_main(
float4 inPos : POSITION;
float2 Txr1 : TEXCOORD0)
{
VS_OUTPUT Out;
//Output our transformed and projected vertex position and texture coordinate
Out.Pos = mul ( view_proj_matrix, inPos);
Out.Txr1 = Txr1;
returen Out;
}
像素著色器也同樣簡單。在創建了sampler變量之后,可以使用HLSL的內建函數tex2D來對紋理進行采樣,代碼如下:
sampler Texture0;
float4 ps_main(
float4 inDiffuse : COLOR0,
float2 inTxr1 : TEXCOORD0) : COLOR0
{
//Output the color taken from our texture
return tex2D ( Texture0, inTxr1);
}
添加光照
雖然添加了紋理的對象看起來不錯,但顯然還不夠真實。在增加場景真實度的過程中,很重要的一步就是為對象添加光照。真實世界中,從太陽到燈泡,充滿了各種光。沒有了光線,就什么都看不到了。
雖然光照本身是一個相當復雜的主題,但在計算機圖形領域中,光通常被簡化為幾種基本類型:
l 環境光(Ambient lighting):場景中所有光源經過多次放射和折射之后,對場景總亮度貢獻的近似模擬。通常用它來減少場景中所需光源的數量,模擬出多光源下的照明效果。環境光通常是一個常量,對所有物體的作用效果都一樣。
l 漫反射光(Diffuse lighting):材質的微觀粗糙表面將導致在有所方向上均勻的反射入射光線。在任何角度接收到的反射光線強度都是相同的。
l 鏡面高光( Specular lighting):當材質表面相當光滑,粗糙度很低時,將以一種非均勻的方式反射光線。對鏡面高光來說,光線強度不但與入射光角度有關,和觀察者的角度也有關。
除了知道光線如何影響物體之外,你還需要如何對光源本身分類。雖然光總是由某個表面發出,比如太陽或燈泡表面,但你也可以把它們看作來自某個方向或某個點。
光照技術中,方向光是最簡單的類型。它們沒有位置信息,并且假設所有光線之間都是平行的,指向同一個方向。哪一種光源是這樣的呢?現實中并沒有這樣的光源。方向光是假設光源離物體無限遠時,照射到物體上的光線將近似于平行而得出的。
方向光最好的例子就是陽光。如果把太陽看作一個離地球上億千米的點光源,那么當陽光到達地球表面時已經近似于平行了,完全可以看作是方向光。
此外沒有位置信息表示方向光不隨距離而衰減。對方向光來說,要考慮的因素只有兩個:方向和光的顏色。看到這里你可能會問光線是如何影響物體表面的。如圖所示,光線照射到物體表面的強度只與入射光線和表面法線的角度有關。

知道了這些基礎知識,就可以用入射光的方向矢量和表面法線的點積以及燈光的顏色因子計算出物體表面上任意一點的光照強度和顏色。這讓我們得出了以下代碼:
Color = Light_Color * saturate ( dot ( Light_Direction, inNormal ) );
注意在上面的代碼中我們使用了saturate函數。它保證對于背對光線的面來說,獲得的光照強度不會為負值。當然,你也可以使用clamp函數,但是對于把值限制在0到1之間來說,saturate函數要更加高效。
一般來說,場景中大多數的光都來自于燈泡,火炬或類似的光源。仔細觀察一下這類光源,它們通常由一個很小的有限點發出,并且位于場景中的某個特定位置。簡化一下,你可以把這些光源都看作場景中的一個點,這就是點光源。
對這類光源來說,光線呈放射狀發出。這意味著只要物體和光源的距離相等,那么無論在哪個方向,所受到的影響都相同。由于表面的光照強度與光線和物體表面法線之間的關系有關,因此我們所要做的第一步就是計算出光線的方向。顯然,對于表面上的任意點來說,光線方向就等于從當前點的位置指向光源位置的矢量。對點光源來說,隨角度的衰減值如下:
//compute the normalized light direction vector and use it to determine the angular light attenuation
float3 Light_Direction = normal ( inPos – Light_Position);
float AngleAttn = saturate ( dot ( inNormal, Light_Direction) );
此外對于點光源來說,還需要考慮它在距離上的衰減。自然,需要計算光源到當前點的距離,使用如下代碼:
float Distance = length ( inPos – Light_Position);
通常情況下,點光源的衰減因子隨距離的平方成反比。但是為了獲得很多的可控性,可以調整公式,讓衰減和距離的二次多項式成反比,代碼如下:
//compute distance based attenuation. this is defined as
// attenuatin = 1 / ( a + b*distance + c * disctance * distance)
float DistAttn = saturate( 1 / ( LightAttenuation.x + LightAttenuation.y * Dist +
LightAttenuation.z * Dist));
現在把前面的代碼集成到頂點著色器中吧:
struct VS_OUTPUT
{
float4 Pos: POSITION;
float2 TexCoord: TEXCOORD0;
float2 Color: COLOR0;
};
float4 Light_PointDiffuse( float3 VertPos, float3 VertNorm, float3 LightPos, float4 LightColor,
float4 LightAttenuation)
{
//determine the distance from the light o the vertex and the direction
float3 LightDir = LightPos – VertPos;
float Dist = length(LightDir);
LightDir = LightDir / Dist;
//Compute distance based attenuation.
float DistAttn = saturate( 1 / ( LightAttenuation.x + LightAttenuation.y * Dist +
LightAttenuation.y * Dist*Dist));
//comopute angle based attenuation
float AngleAttn = saturate ( dot (VertNorm, LightDir));
// Computer the final lighting
return LightColor * DistAttn * AngleAttn;
}
VS_OUTPUT vs_main( float4 inPos: POSITION,
float3 inNormal: NORMAL,
float2 inTxr : TEXCOORD0)
{
VS_OUTPUT Out;
Out.Pos = mul ( view_proj_matrix, inPos);
Out.TexCoord = inTxr;
float4 Color = Light_PointDiffuse ( inPos, inNormal, Light_Position, Light1_Color, Light_Attenuation)
Out.Color = Color;
return Our;
}
我把計算點光源光照的代碼單獨放到了Light_PointDiffuse函數中,因此,當場景中有多個點光源時,你可以復用這段代碼。當然,我們在后面的章節會有這樣的例子。