一,學習目标
1.了解如何以數學方式描述平面上的點所“面對”的方向,以使我們确定線與平面之 間的入射角。
2.學習如何正确變換法線向量。
二、法線向量與計算法線向量
平面法線(face normal):是描述多邊形所朝方向的機關向量(即,它與多邊形上的所有 點互相垂直),如下圖 (a)所示。
表面法線(surface normal):是與物體表面上的點的正切平面 (tangent plane)互相垂直的機關向量,如下圖 (b) 所示。表面法線确定了表面上的點“面對” 的方向。
(a)平面法線與平面上的所有點互相垂直。(b)表面法線與物體表面上的點的正切平面互相垂直。
當進行光照計算時,我們必須為三角形網格表面上的每個點求解表面法線,以确定光線與網格表面在該點位置上的入射角度。為了獲得表面法線,我們必須為每個頂點指定表面法線(這些法線稱為頂點法線)。然後在光栅化階段,這些頂點法線會在三角形表面上進行線性插值,使三角形表面上的每個點都獲得一個表面法線。
圖 p0和 p1是線段的兩個頂點,n0和 n1是對應的頂點法線。點 p 是通過線性插值(權重平均值)得到的線段上的一點,n 是點 p 的法線向量,它介于兩個頂點法線之間。也就是, 當存在一個位置使 p =p0 + t (p1 –p0 ) 時,n =n0 + t(n1 –n0)。為了簡單起見,我們隻解釋了線段的法線插值,但是一概念可以被直接擴充為 3D 三角形的法線插值。
一個三角形有兩個面。我們使用如下約定來區分這兩個面。假設三角形的頂點按照 v0、 v1、v2的順序排列,我們這樣來計算三角形的法線 n:
帶有法線向量的面為正面,而另一個面為背面。下圖說明了這一概念。
左邊的三角形正對我們的觀察點,而右邊的三角形背對我們的觀察點。
下面的函數可以根據三角形的 3 個頂點來計算三角形正面的平面法線。
void ComputeNormal(const XMVector3& p0, const XMVector3& p1,
const XMVector3& p2, XMVector3& out)
{
XMVector3 u = p1 - p0;
XMVector3 v = p2 - p0;
XMVector3Cross(&out, &u, &v); //U與V相乘輸出為out
XMVector3Normalize (&out, &out); //規範化向量out
}
對于一個微分曲面來說,我們可以使用微積分計算曲面上的點的法線。但遺憾的是,三角形網格不是微分曲面。我們通常使用一種稱為頂點法線平均值(vertex normal averaging) 的技術求解三角形網格上的頂點法線。對于網格上的任意頂點 v 來說,v 的頂點法線 n 等于 以 v 為共享頂點的每個多邊形的平面法線的平均值。例如在下圖中,網格上的四個多邊形 共享頂點 v;是以,v 的頂點法線為:
圖中間的頂點由相鄰的 4 個多邊形共享,我們通過計算這 4 個多邊形平面法線的平均 值就可以估算出該頂點的法線。
不僅如此,還有一個更巧妙的計算頂點法線方法,即權重平均值計算方法。
平均值計算公式:以每個多邊形的 面積作為權值,計算權重平均值(這樣,面積較大的多邊形會占有較大的權重,而面積較小 的多邊形會占有較小的權重)。
下面的僞代碼說明了在給出一個三角形網格的頂點清單和索引清單時,如何計算該平均值:
// 輸入:
// 1.一個頂點數組(mVertices),每個頂點都有一個位置分量(pos)和
// 一個法線分量(normal).
// 2.一個索引數組(mIndices)。
// 處理網格中的每個三角形:
for(DWORD i = 0; i < mNumTriangles; ++i)
{
// 第 i 個三角形的索引
UNIT i0 = mIndices[i*3+0];
UNIT i1 = mIndices[i*3+1];
UNIT i2 = mIndices[i*3+2];
// 第 i 個三角形的頂點
Vertex v0 = mVertices[i0];
Vertex v1 = mVertices[i1];
Vertex v2 = mVertices[i2];
// 計算面法線
Vector3 e0 = v1.pos - v0.pos;
Vector3 e1 = v2.pos - v0.pos;
Vector3 faceNormal Cross( &e0, &e1);
// 這個三角形共享了以下三個頂點,
// 是以要将面法線加入到這些頂點法線的平均值中。
mVertices[i0].normal += faceNormal;
mVertices[i1].normal += faceNormal;
mVertices[i2].normal += faceNormal;
} // 對每個頂點 v,我們已經将共享 v 的所有三角形的面法線相加了,
// 是以現在隻需歸一化即可。
for(UNIT i = 0; i < mNumVertices; ++i)
mVertices[i].normal = Normalize(&mVertices[i].normal));
對法線向量進行變換
在下圖 a 中,正切向量 u = v1− v0垂直于法線向量 n。當我們對這兩個向量應用一個不成比例的縮放變換 A 時,我們可以從下圖 b 中看到,變換之後的切線向量 uA = v1A – v0A 不再垂直于變換之後的法線向量 nA。
圖 (a)變換之前的表面法線。(b)當 x 軸上的機關長度增大兩倍後,法線不再垂直于表面。 ©通過計算縮放變換的逆轉置矩陣,我們可以得到正确的變換結果。
是以我們的問題是:當給出一個用于變換點和(非法線)向量的變換矩陣 A 時,如何 求出一個專門用來變換法線向量的變換矩陣 B,使變換之後的切線向量和法線向量依然保持 垂直關系(即 uA·uB = 0)。要解決一問題,讓我們先從一些已知條件開始:我們知道法線 向量 n 直于切線向量 u。
經過上面的推導之後,使用 B = (A-1)T(A 的逆轉置矩陣)來變換法線向量,就可以使它與變換之後的切線向量依然保持垂直關系。 注意,當變換矩陣為正交矩陣(AT = A-1)時,B = (A-1)T = (AT)T = A;也就是,我們不必計算逆轉置矩陣,直接用 A 來代替 B 即可。總之,當以一個非等比變換矩陣對法線向量 進行變換時,我們必須使用該矩陣的逆轉置矩陣。
在 MathHelper.h 中有一個輔助方法用于計算逆轉置矩陣:
static XMMATRIX InverseTranspose(CXMMATRIX M)
{
// 逆轉置矩陣僅用于法線。是以将矩陣中的平移行
// 設定為 0,這樣就不會影響逆轉置矩陣的計算——因為我們
// 不需要對平移進行逆轉置運算。
XMMATRIX A = M;
A.r[3] = XMVectorSet(0.0f,0.0f,0.0f,1.0f);
XMVECTOR det = XMMatrixDeterminant(A);
return XMMatrixTranspose(XMMatrixInverse(&det, A));
}
} 因為逆轉置隻用于變換矢量,而平移是作用在點上的,是以需要從矩陣中排除平移因素。 但是,3.2.1 節告訴我們,為了防止矢量被平移操作影響,它的 w 應設定為 0(使用齊次坐 标)。是以,我們無須将矩陣中的平移行歸零。但問題是,如果我們将逆轉置矩陣和另一個 不包含非等比例縮放的矩陣相乘,例如視矩陣(A-1-)TV,轉置後位于(A-1)T第 4 列的平移項導 緻結果錯誤。是以,我們将平移項清零就是為了預防這個錯誤。正确的方法是使用((AV)-1)T 對法線進行變換。下面是一個縮放和平移矩陣的例子,第 4 列經過逆轉置後并不是[0,0,0,1]T。
注意:即使使用逆轉置變換,法線向量也有可能會失去機關長度;是以,在變換之後必須重新規範化法線向量。
四、蘭伯特餘弦定理
通常來說垂直照向平面的線比從側面照向平面的線更加強烈。
假設有一塊很小的區域 dA。當法線向量 n 與光照向量 L 平行時,區域 dA 受到的光 線照射最多。随着 n 和 L 之間的夾角θ逐漸增大,區域 dA 受到的光線照射量會越來越少(因 為很多光線都無法照射到 dA 表面上了)。
我們可以從這個概念中推導出一個函數,根據頂點法線和光照向量之間的夾角傳回不同的光照強度。(注意,光照向量是從表面指向光源的向量;也就是,它與線的傳播方向正好 相反。)當頂點法線與照向量完全重疊時(即,它們的角度為 0º時),該函數傳回最大強度值;随着頂點法線與照向量之間的夾角逐漸增大,該函數傳回的強度值會越來越小。當θ>90 º時,說明光線照射的是物體背面,此時我們應該将強度設定為 0。蘭伯特(Lambert)
餘弦定理給出了上述函數的定義:f(θ) = max(cosθ,0) = max(L·n,0)
其中,L 和 n 是機關向量。下圖是 f(θ)的曲線圖。我們可以看到,随着θ的變化,強 度在 0.0 到 1.0(即,0%到 100%)之間變化。
當−2≤θ≤ 2 時,函數 f(θ) = max(cosθ,0) = max(L·n,0)的曲線圖。注意,π/2≈1.57