1. 程式人生 > >Direct3D基礎——光照的組成、材質、頂點法線、光源

Direct3D基礎——光照的組成、材質、頂點法線、光源

為了增強所繪製的場景的真實感,我們會為場景增加光照,使用光照來描物體的立體感。使用光照的時候,我們就無需描述頂點的顏色,Direct3D會將頂點送入光照計算引擎,然後引擎根據光源型別,材質以及物體表面和相對於光源的朝向等等,計算出內個頂點的顏色值。基於某種光照模型然後計算出頂點的顏色,效果更nice。

光照的組成

環境光

這種型別的光經過其他物體表面反射到達所觀察物體,並且照亮場景。比如:某一個物體的一部分被照亮,但是該物體並沒有處在光源的直接照射下,之所以被照亮,是因為有其他物體反射光源的光,然後到達自己身上,再被照亮的。這類光叫做環境光。

漫射光

這種型別的光沿著特定的方向傳播。當漫射光到達一定表面的時候,沿著物體表面的所有方向均勻反射,所以無論從哪一個方向觀察,表面的亮度都是相同的,所以採用漫射光的模型的時候,不需要考慮觀察者的位置。所以漫射光方程中只需要考慮光的傳播方向以及表面的朝向。從一個光源發出的光,大多就是這種型別的光。

鏡面光

這類光沿著特定的方向傳播,當光線到達表面之後,然後嚴格的按照另外一個方向進行,從而形成只能在一定角度才能觀察到的高亮度照射,所以在鏡面光方程中我們不僅要光照的入射方向和物體的表面朝向,還要考慮觀察者的方向,鏡面光可以模擬物體中的高光點,例如:當光線照射到物體表面所形成的的比較亮的照射

鏡面光的計算量比環境光和漫射光的計算量要大得多,所以Direct3D專門為其設定了開關選項,預設情況下,Direct3D是不計算鏡面光的反射的。

使用狀態機將有關狀態設定為true,就會將鏡面光照開啟:

Device->SetRenderState(D3DRS_SOECULARENABLE,true);

材質

在現實世界中,物體的顏色是由其反射的光的顏色決定的。一個物體反射所有的紅色光,吸收所有的非紅色光,那麼這個物體就呈現於紅色光。Direct3D中允許我們通過定義物體的材質來模擬同樣的現象。材質允許我們定義物體的表面對各種光的反射比例,在程式碼中使用D3DMATERIAL9來表示:

typedef struct D3DMATERIAL9
{
	D3DCOLORVALUE Diffuse;	//指定材質對漫射光的反射率
	D3DCOLORVALUE Ambient;	//指定材質對環境光的反射率
	D3DCOLORVALUE Specular;	//指定材質對鏡面光的反射率
	D3DCOLORVALUE Emissive;	//該分量用於增強物體的亮度,使之看起來可以自己發光
	float Power;			//指定鏡面高光的銳度,該值越大,高光點的銳度越大
}D3DMATERIAL9,*LPD3DMATERIAL9;

假如現在有一個紅色的球體,我們想讓他只去反射紅色的光,應該向下面這樣定義:

D3DMATERIAL9 red;
::ZeroMemory(&red, sizeof(red));
red.Diffuse  = D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f);
red.Ambient  = D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f);
red.Specular = D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f);
red.Emissive = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);//用於增強物體亮度的分量
red.Power = 5.0f;	//指物體高光點的銳度

上面這個例子中,我們將綠色和藍色光的反射率設定為0,意思就是:對綠色光和藍色光全部吸收,對紅色光的反射率為100%。也就是說我們可以控制材質在各種型別光的照射下,那種顏色的光會被反射。

注意的一點:如果使用一個藍色光去照射一個只反射紅色的球體的話,因為藍色光被全部吸收,紅色光為0,沒有反射出來的紅色光,所以球體是不能被照亮的。但一個球體吸收全部的顏色的時候成:黑色。當一個物體可以百分之百的反射紅色光、綠色光、藍色光的時候,物體成白色。

為了減輕我們初始化的工作,我們可以再d3dUtility.h/cpp中新增如下使用函式以及全域性材質質量:

	const D3DXCOLOR WHITE(D3DCOLOR_XRGB(255, 255, 255));
	const D3DXCOLOR BLACK(D3DCOLOR_XRGB(0,0,0));
	const D3DXCOLOR RED(D3DCOLOR_XRGB(255, 0, 0));
	const D3DXCOLOR GREEN(D3DCOLOR_XRGB(0,255,0));
	const D3DXCOLOR BLUE(D3DCOLOR_XRGB(0,0,255));
	const D3DXCOLOR YELLOW(D3DCOLOR_XRGB(255,255,0));
	const D3DXCOLOR CYAN(D3DCOLOR_XRGB(0,255,255));
	const D3DXCOLOR MAGENTA(D3DCOLOR_XRGB(255,0,255));

    
    D3DMATERIAL9 d3d::IniMtrl(D3DCOLOR a, D3DCOLOR d, D3DCOLOR s, D3DCOLOR e, float p)
    {
	    D3DMATERIAL9 mtrl;
	    mtrl.Ambient = a;
	    mtrl.Diffuse = d;
	    mtrl.Specular = s;
	    mtrl.Emissive = e;
	    mtrl.Power = p;

	    return mtrl;
    }

    //下面的五種材質都是物體亮度和鏡面高光點的銳度為0的材質
    //白色材質,因為對環境光,漫反射光,鏡面光的反射率都是反射白色光
	const D3DMATERIAL9 WHITE_MTRL = InitMtrl(WHITE, WHITE, WHITE, BLACK,8.0f);
	const D3DMATERIAL9 RED_MTRL = InitMtrl(RED, RED, RED, BLACK, 8.0f);
	const D3DMATERIAL9 GREEN_MTRL = InitMtrl(GREEN, GREEN, GREEN, BLACK, 8.0f);
	const D3DMATERIAL9 BLUE_MTRL = InitMtrl(BLUE, BLUE, BLUE, BLACK, 8.0f);
	const D3DMATERIAL9 YELLOW_MTRL = InitMtrl(YELLOW, YELLOW, YELLOW, BLACK, 8.0f);

因為頂點的結構中不包含材質的屬性,所以我們必須要對當前繪製的圖元的材質進行設定:

//定義一個藍色材質和一個紅色材質
D3DMATERIAL9 blueMaterial, redMaterial;

//省略一些舒適化材質內容的程式碼
//這裡省去的就是在材質結構體裡面填充他的鏡面光,漫反射光、環境光之中的反射率
.
.
.

//設定材質
Device->SetMaterial(&blueMaterial);
//畫一個球,現在畫出來的就是藍色球
drawsphere();

//設定材質
Device->SetMaterial(&redMaterial);
//畫一個球,現在畫出來的就是紅色球
drawsphere();

頂點法線

面法線是描述一個多邊形朝向的向量。頂點法線是描述構成多邊形頂點的法線。

Direct3D需要知道每個頂點的朝向,也就是每個頂點的頂點法向量,來計算光照達到表面的入射角。

一個多邊形的頂點法線和麵法線不一定就是相同的,下面就是例子:

為了描述一個帶頂點法線的頂點,我們的頂點結構就必須要改變:

struct Vertex{
    float x,y,z;
    float _nx,_ny,_nz;
    static const DWORD FVF;
};
const DWORD Vertex::FVF = D3DFVF_XYZ | D3DFVF_NORMAL

在結構裡面,我們去掉了頂點的顏色資訊,因為我們可以通過光照來計算顏色的資訊。

對於簡單的物體,比如立方體和球體,我們可以直接的觀察得到頂點的法線,而對於比較複雜物體,我們就需要通過一種方法來計算。

假定有一個三角形,由頂點p0,p1,p2構成,現在我們來計算每一個頂點的法線:n0,n1,n2。

最簡單的方法就是三角形的面法線,然後將這個面法線當做每個頂點的頂點法線。

首先我們來計算三角形內部的兩個向量:

p1 - p0 = u;

p2 - p0 = v;

則面法線就是:

n = u × v;

由於每一個頂點的發向量都相等,那麼n0 = n1 = n2 = n;

下面是一個C語言寫的計算三個頂點的所構成的面的面法向量。該函式中假設指定的頂點的順序是順時針繞序,如果是逆時針繞序的話,計算出來的法向量就是相反方向的。

void ComputeNormal (D3DVECTOR3 * p0,D3DVECTOR3 * p1,D3DVECTOR3 * p2,D3DVECTOR3 * pOut)
{
    D3DVECTOR3 u = *p1 - *p0;
    D3DVECTOR3 v = *p2 - *p0;
    //求叉積
    D3DVec3Cross(out,&u,&v);
    //返回3D向量的規格化向量
    D3DVec3Normal(out,out);
}

但是當使用三角形表面逼近表示曲面的時候,將單個的面法向量當做頂點的法向量不可能產生一個平滑的向量。現在我們的改進方法就是求出所有使用這個點的那些面的法向量,然後進行求均值。例如:為了求出點v的頂點法向量Vn,我們需要求出所有共享頂點V的三角形的面法向量,然後對這些面法向量求均值。

Vn = 1/3(n0 + n1 + n2)

再變換完成之後,可能有一些頂點法向量已經不是規範化的了,所以做好的方法就是改變繪製狀態來使得所有的向量重新規範化。

Device->SetRenderState(D3DRS_NORMALIZENORMALS, true);

光源

Direct3D支援三種光源的形式。

點光源:該光源在世界座標系中的位置固定,並均勻的向所有方向發射光線。

方向光:該光源沒有位置,發射的光線互相平行,沿著某一特定方向傳播

聚光燈:這種光源和手電筒類似,,發出的光程錐形,沿著特定的方向傳播,錐形有兩個角度φ和θ,θ描述內部角度,φ描述外部角度

程式碼中的光源結構使用結構體:D3DLIGHT9表示。

typedef struct D3DLIGHT9 {
	D3DLIGHTTYPE Type;		//光源型別:D3DLIGHT_POINT、D3DLIGHT_SPOT(聚光燈)、    D3DLIGHT_DIRECTIONAL
	D3DCOLORVALUE Diffuse;	//光源發出的漫反射光的顏色
	D3DCOLORVALUE Specular;	//光源發出的鏡面光的顏色
	D3DCOLORVALUE Ambient;	//光源發出的環境光的顏色
	D3DVECTOR Position;		//光源在世界座標中的位置,對於方向光該引數無意義,
	D3DVECTOR Direction;	//描述光在世界座標中的傳播方向的向量,對於點光源該引數無意義
	float Range;			//光線消亡之前,所能達到的最大的光程,對於方向光,該引數無意義
	float Falloff;			//該值僅用於聚光燈,該引數定義了光強從內錐形到外錐形的衰減,一般取:1.0f
	float Attenuation0;		//光線的常量衰減係數
	float Attenuation1;		//光線的線性衰減係數
	float Attenuation2;		//光線的2次距離衰減係數
	float Theta;			//僅用於聚光燈。指定了內部錐形的圓錐角,單位是:弧度
	float Phi;				//僅用於聚光燈。指定了外部錐形的圓錐角,單位是:弧度
}D3DLIGHT9, *LPD3DLIGHT;

與材質的結構體:D3DMATERIAL9的結構體初始化類似,當您需要一個比較簡單的光源的時候,初始化是很麻煩的一件事,我們就可以在d3dUtility.h/.cpp中加入初始化的函式:

namespace d3d 
{
	D3DLIGHT9 InitDirectjonaLight(D3DXVECTOR3* direction, D3DXCOLOR* color);
	D3DLIGHT9 InitPointLight(D3DXVECTOR3* position, D3DXCOLOR* color);
	D3DLIGHT9 InitSpotLight(D3DXVECTOR3* position, D3DXVECTOR3* direction, D3DCOLOR* color);
}

//簡單實現一下方向光:
D3DLIGHT9 d3d::InitDirectionLight(D3DVECTOR3* direction, D3DXCOLOR* color)
{
	D3DLIGHT9 Light;
	::ZeroMemory(&Light, sizeof(Light));

	Light.Type = D3DLIGHT_DIRECTIONAL;
	Light.Ambient = *color * 0.4f;	//光源發出的環境光的顏色
	Light.Diffuse = *color;			//光源發出的漫反射光的顏色
	Light.Specular = *color * 0.6f; //光源發出的鏡面光的顏色
	Light.Direction = *direction;   //描述光源在世界座標系中傳播方向的向量

	return Light;
}

建立一個平行於x軸的方向光:

D3DXVECTOR3 dir(1.0f, 0.0f, 0.0f);
D3DXCOLOR c = d3d::WHITE;
D3DLIGHT9 dirLight = d3d::InitDirectionalLight(&dir, &c);

當我們對D3DLIGHT9的物件例項化完畢之後,我們需要在Direct3D所維護的一個光源內部列表中對所有我們要使用的光源進行註冊。

//對使用的光源進行註冊
Device->SetLight(
    0,        //要設定的燈光列表中的元素
    &light    //要設定的燈光,剛才已經初始化好之後的
)

//一旦光源註冊成功,我們就可以對其進行開關狀態進行控制
Device->LightEnable(
	0,			//燈光列表中的元素啟用或者禁用
	true		//true = 啟用,false = 禁用
)