在渲染中,有很多的计算是在视角空间中完成的。这是因为光照通常是在这个空间中计算的,否则,像一些依赖视角的效果,比如高光,将会比较难以实现。
因此,我们需要一种方法将法线转换到视角空间。我们可以通过下面的写法把一个顶点转换到视角空间:
vertexEyeSpace = gl_ModelViewMatrix * gl_Vertex;
那么,我们为什么不能直接把这个应用到法线向量呢?首先,法线是一个3维向量,而ModelView
矩阵是一个4x4矩阵;其次,因为法线是一个向量,所以我们只需要转换它的方向。ModelView
矩阵左上方的3x3子矩阵包含了方向的信息,那么我们为什么不能使用这个子矩阵去转换法线呢?
这可以通过下面的代码很简单的实现:
normalEyeSpace = vec3(gl_ModelViewMatrix * vec4(gl_Normal,0.0));
但上面的代码只在部分情况下可以正常工作。
让我们来看下一个潜在的问题:
在上面的图中,我们看到一个带了一个法线和切线向量的三角形。在下面的图中,显示了如果ModelView矩阵包含了非均匀缩放会发生什么样的偏差。
注意:如果是均匀缩放,那么法线的方向将会保持,虽然长度会受到影响,但可以简单的通过标准化修复。
在上面的图中,ModelView
矩阵应用到了所有的向量,包括法线,结果是显而易见的错误的:转换后的法线不再与表面垂直了。
我们知道,一个向量可以表示为两个点之间的差异。比如切线向量,它可以通过计算三角形一条边上的两个顶点之间的差距来获得。如果P1和P2是定义一条边的两个顶点,那么我们知道:
T = P2 - P1
如果把这个向量表示为包含4个元素且最后一个元素为0的元组,那么我们可以把这个等式两边都乘以ModelView
矩阵
T * ModelView = (P2 - P1) * ModelView
结果是
T * ModelView = P2 * ModelView - P1 * ModelView
T' = P'2 - P'1
因为P'1和P'2为转换后的三角形的顶点,所以T'仍然与三角形的边相切。因此,ModelView
保持了切线。但它却没有保持法线。
使用和T向量一样的方法,我们可以找到两个点Q1和Q2,让以下等式成立
N = Q2 - Q1
主要的问题是,就如上面的图所展示的那样,由转换后的的点定义的向量,Q2 - Q1,不一定仍然与边垂直。法线向量不是如切线向量那样,定义为两个点之间的差距,它定义为垂直于一个表面的向量。
现在我们知道了,我们不能在所有情况下使用ModelView
来转换法线向量。那么我们应该使用什么样的矩阵呢?
假设G为一个3X3的矩阵,让我们看看如何计算它来正确的转换法线向量。
我们知道,在矩阵转换之前,因为切线和法线向量是垂直的,所以T.N = 0。我们也知道,转换之后,它们也一定要垂直,所以N'.T'也一定要等于0。我们把ModelView
的左上方的3x3矩阵称为M矩阵,T可以安全的乘以M(T是一个向量,所以w元素是0).
我们假设G是正确转换法线向量N的矩阵,那么就有了下面的等式
N'.T' = (GN).(MT) = 0
把点乘转换为叉乘,那么
(GN).(MT) = (GN)T * (MT)
注意为了叉乘向量,第一个向量的转置是必须的。我们知道两个矩阵相乘的转置,等于转置后矩阵的相乘,但顺序要相反,因此:
(GN)T * (MT) = NTGTMT
我们前面已经说过了N和T的点乘是0,所以如果
GTM = I
那么我们有
N'.T' = N.T = 0
这正是我们要的,所以我们可以根据M来计算G。
GTM = I <==> G = (M-1)T
因此M矩阵的逆矩阵的转置,是正确转换法线的矩阵。
前面我们说直接使用ModelView
矩阵在部分情况下可以正常工作。当ModelView
左上方的3x3矩阵是正交矩阵的时候,我们有:
M-1 = MT ==> G == M
这是因为正交矩阵的转置与逆矩阵是一样的。那么什么是正交矩阵呢?正交矩阵就是矩阵的每行/列都是单位长度,并且互相垂直的矩阵。这意味这两个向量,被正交矩阵转换前后,它们之间的角度是一样的,因此转换后的法线依然垂直于切线。此外,它还保持了向量的长度。
那么我们如何保证M是正交的呢?只要我们限制我们的几何变换仅限于旋转和位移,比如在OpenGL程序中我们仅仅使用glRotate
和glTranslate
,不使用glScale
,那么可以确保M是正交的。注意:gluLookAt
也创建了一个正交矩阵。