一,学习目标
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