11.2、解码矩阵
所有可能存在的3D坐标系都能只使用16个值来编码。这些值被组织为内存中的;16个连续的浮点值的数组,参见图11-1。但是数学标记法通常会把一个3D矩阵表示为一个4X4的网格值矩阵。图11-1显示了内存中的值和数学标记法中的值之间的相互关系。

每个矩阵会编码三个单位矢量,这三个单位矢量描述了一个坐标系的正的X、Y、Z坐标轴的方向。第四个矢量确定了坐标原点的位置。图11-2标示了每个矢量。轴方向和原点的结合实际上定义了一个坐标系。
图11-3显示了一个“单位”矩阵。一个单位矩阵与任何其他矩阵级联(乘)的结果等于其他矩阵。单位矢量{1.0,0.0, 0.O}定义了编码在单位矩阵中的X轴。单位矢量的长度通常等于1.0。单位矢量{0.0,1.0, 0.0} 定义了编码在单位矩阵中的Y轴。单位矢量{0.0,0.0,1.0}定义了编码在单位矩阵中的Z轴。单位矩阵的坐标轴原点在位 置{0.0, 0.0, 0.0}处。


矩阵定义了相对坐标系。你必须要回答这个问题,“相对于什么”,才能解释编码在矩阵中的值。图11-4显示了OpenGL 默认坐标系是怎么与一个含有3D内容的全屏CocoaTouchUIView对齐的。OpenGL原点位于视图的中心。OpenGL的X轴的方向是平行于视图的顶边和底边的,并且X轴的值是向右增加的。OpenGL的Y轴是平行于视图的左 边和右边的,并且Y轴的值向上增加。OpenGL的Z轴值是沿着从视图指向观察者的一个矢量增加的。

OpenGL应用会定义一个相对于默认坐标系的新坐标系。定义了一个原点在默认坐标系左下角的新3D坐标系的应用会创建了一个与图11-5中显示的类似的矩阵。图11-5的原点在默认坐标系的{-1.0, -1.0,0.0} 位置处。

相对于另一个坐标系移动一个坐标系的原点叫做“平移”,这是第5章介绍的基本变换之一。平移只会影响一个矩阵的坐标轴‘原点” 部分。其他两个基本变换,旋转和缩放,只会影响编码在一个矩阵中的“ 轴矢量”。例如,旋转会相对于另一个坐标系改变每个轴矢量所指的方向。缩放会相对于另一个坐标系改变每个轴矢量的长度。图11-6展示了一个坐标系在经过平移、旋转和缩放后所发生的变化。例子OpenGLES_Ch5_4 提供了一个用于帮助你测试和可视化坐标系变换的累加效果的用户界面。

矩阵中的第四列是干什么的?截止到现在没有一个图片曾使用第四列来做任何事情。原来第四列只用在包含透视变换的矩阵中。OpenGL一直保持了两个单独的矩阵,projectionMatrix和modelviewMatrix,部分是为了保持变换的编码简单性。按照约定,只有projectionMatrix 会包含透视变换。当遵守这个约定时,在涉及modelviewMatrix的所有计算中都可以随意的忽略掉第四列。
注意 modelviewMatrix和projectionMatrix总是会级联在一起创建一个单独的modelview-ProjectionMatrix,以在OpenGL ES 2.0 Shading Language程序中使用。model-view变换和投影变换之间的传统区别并不能表明它们之间有任何重要的数学区别。历史证明这个区别是很方便的,因为一些常见的计算只使用其中一个矩阵。例如,在灯光方程式中的法向量变换就需要一个继承自modelviewMatrix的特定矩阵。
11.2.1、从平截体获取矩阵
给定一个由视平截体定义的视点,可以很容易地构造出对应的modelviewMatrix。视平截体是在第8章介绍过,并在第9章进一步讲解的。为了理解矩阵与平截体是怎么联系在一起的,请思考一下一个平截体是如何表示的。从第9章开始,本书中的例子开始使用AGLKFrustum数据结构和用来操作这个数 据结构的函数。尤其是AGLKFrustumSetPositionAndDirection()函数,这个函数会指定一个平截体的位置和方向。
void AGLKFrustumSetPositionAndDirection
(
AGLKFrustum *frustumPtr,
GLKVector3 eyePosition,
GLKVector3 lookAtPosition,
GLKVector3 upVector)
AGLKFrustumSetPositionAndDirection()方法会计算从eyePosition到lookAtPosition的视线方向。这个平截体的Z轴矢量是一个指向视线方向的单位矢量。
const GLKVector3 lookAtVector =
GLKVector3Subtract(eyePosition, lookAtPosition);
NSCAssert(0.0f < AGLKVector3LengthSquared(lookAtVector),
@"Invalid eyeLookPosition parameter");
frustumPtr->zUnitVector = GLKVector3Normalize(lookAtVector);
任意两个非平行单位矢量的矢量积会产生第三个与前两个矢量垂直的单位矢量。矢量积是在第4章介绍的,11.4.3节会再做介绍。给定一个上方向和一个视线方向,那么平截体的X轴矢量是“上”矢量和视线”矢量的矢量积。图11-7显示了上方向、视线方向和计算出来的平截体X轴矢量之间的关系。

平截体的X轴是视线矢量和上矢量之间的标准化矢量积。如下代码中的zNormal-Vector是标准化视线矢量。
// The frustum's X axis is the cross product of the
// normalized “up” vector and the frustum's Z axis
frustumPtr->xUnitVector = GLKVector3CrossProduct(
GLKVector3Normalize(upVector),
frustumPtr->zUnitVector);
传递给AGLKFrustumSetPositionAndDirection()函数的upVector 不能总是不加更改地作为平截体的Y轴,因为这个upVector可能不是一个单位矢量,而且可能并不与x轴和Z轴都垂直。平截体的真正Y轴是通过计算Z轴和X轴的矢量积得到的。
// The frustum's Y axis is the cross product of the
// frustum's Z axis and the frustum's X axis.
frustumPtr->yUnitVector = GLKVector3CrossProduct(
frustumPtr->zUnitVector,
frustumPtr->xUnitVector);
图11-8显示了平截体的轴矢量和对应矩阵之间的关系。这个平截体的坐标轴提供了对应于这个平截体的“视线”model-view矩阵中的轴矢量。图11-8中的坐标轴原点源自这个平截体的eyePosition。
下面的GLKMatrix4 AGLKFrustumMakeModelview(const AGLKFrustum * frustumPtr)函数会返回一个编码了指定平截体的位置和方向的modet-view矩阵。

GLKMatrix4 AGLKFrustumMakeModelview
(
const AGLKFrustum *frustumPtr)
{
NSCAssert(AGLKFrustumHasDimention(frustumPtr),
@"Invalid frustumPtr parameter");
const GLKVector3 eyePosition = frustumPtr->eyePosition;
const GLKVector3 xNormal = frustumPtr->xUnitVector;
const GLKVector3 yNormal = frustumPtr->yUnitVector;
const GLKVector3 zNormal = frustumPtr->zUnitVector;
const GLfloat xTranslation = GLKVector3DotProduct(
xNormal, eyePosition);
const GLfloat yTranslation = GLKVector3DotProduct(
yNormal, eyePosition);
const GLfloat zTranslation = GLKVector3DotProduct(
zNormal, eyePosition);
GLKMatrix4 m = {
// X Axis Y Axis Z Axis
xNormal.x, yNormal.x, zNormal.x, 0.0f,
xNormal.y, yNormal.y, zNormal.y, 0.0f,
xNormal.z, yNormal.z, zNormal.z, 0.0f,
// Axis Origin
-xTranslation, -yTranslation, -zTranslation, 1.0f
};
return m;
}
一个平截体的model-view矩阵的平移可以表示为这个平截体的定义所在的坐标系的位移。图11-5是对这个关系的最简单的可视化说明。换句话说,一个坐标系的原点被定义为另一个坐标系中的一个位置,另一个坐标系指的是新坐标系的相对坐标系。平移会移动一个坐标系的原点。平移量是在AGLKFrustumMakeModelview()函数中反求 出来的,因为要把上一个坐标系中的眼睛位置转换成新坐标系的原点{0,0,0}位置就有必要减去眼睛位置的每个分量。
11.2.2 透视
就像AGLKFrustumSetPositionAndDirection)函数会设置一个平截体的位置和方向,AGLKFrustumSetPerspective(AGLKFrustum * frustumPtr, GLfloat fieldOfViewRad, GLfloat aspectRatio, GLfloat nearDistance, GLfloat farDistance)函数用于定义一个平截体的形状。图 11-9显示了AGLKFrustumSetPerspective(参数与结果平截体之间的几何关系。

这个平截体的形状确定了与这个平截体对应的“ 透视”投影矩阵。下面的GLKMatrix4AGLKFrustumMakePerspective(const AGLKFrustum *frustumPtr) 函数会返回与这个平截 体对应的一个投影矩阵。
extern GLKMatrix4 AGLKFrustumMakePerspective
(
const AGLKFrustum *frustumPtr
)
{
NSCAssert(AGLKFrustumHasDimention(frustumPtr),
@"Invalid frustumPtr parameter");
const GLfloat cotan =
1.0f / frustumPtr->tangentOfHalfFieldOfView;
const GLfloat nearZ = frustumPtr->nearDistance;
const GLfloat farZ = frustumPtr->farDistance;
GLKMatrix4 m = {
cotan / frustumPtr->aspectRatio, 0.0f, 0.0f, 0.0f,
0.0f, cotan, 0.0f, 0.0f,
0.0f, 0.0f, (farZ + nearZ) / (nearZ - farZ), -1.0f,
0.0f, 0.0f, (2.0f * farZ * nearZ) / (nearZ - farZ), 0.0f
};
return m;
}
在AGLKFrustumMakePerspective()函数中计算的1/2视野角度的余切值,代表了一个物体的宽度和高度与这个物体跟坐标系原点之间的距离的关系。这个余切值控制了透视图是怎么让远处的物体比近处的物体显得更小的。减小视野角度可以获得放大物体的视觉效果,从而使它们看起来更近一些,就好像是通过望远镜来看它们。
11.2.3 矢量的坐标轴分量
两个矢量的标量积会提取每个矢量在另一个矢量方向上的分量。标量积是在第4章介绍的。假设一个由两个矢量(矢量A和矢量B)定义的三角形,参见图11-10。
遵循直角三角形的标准命名约定,矢量A是这个三角形的“ 临边”,矢量B是这个三角形的斜边。对于一个角度为θ的直角三角形来说,角度θ的余弦值等于临边长除以斜边长。非常方便的是,标量积运算会 返回相同的值,即临边长除以斜边长所得的值,不需

要任何三角学知识。
图11-11提供了可视化两个矢量的标量积的另一种方式。在图11-11中,单位矢量A与X轴单位矢量的标量积提供了单位矢量A在X轴方向,上的长度部分。单位矢量A与Y轴单位矢量的标量积提供了单位矢量A在Y轴方向上的长度部分。
获得一个矢量在另一个矢量方向上的长度的能力有无数的用途。请记住标量积运算适用于任意两个矢量。实际上这个运算是没有必要包含一个坐标轴矢量的。例如,图11-12使用一条虚线图示了单位矢量A在单位矢量B方向上的部分。换句话说,在图11-12中显示的虚线与单位矢量B的交点指示了单位矢量A与单位矢量B的标量积。


在GLKit的GLKVector3.h文件中,苹果公司提供了计算两个3D矢量的标量积的C代码。这个代码看起来会与下面的代码相似。其中标量积的计算只使用了乘法和加法,而这两种运算是GPU执行最快的运算。
float GLKVector3DotProduct(GLKVector3 vectorLeft, GLKVector3 vectorRight)
{
return vectorLeft.v[0] * vectorRight.v[0] + vectorLeft.v[1] * vectorRight.v[1] + vectorLeft.v[2] * vectorRight.v[2];
}
11.2.4 点变换
当你有了一个矩阵后,可以用来干什么呢?矩阵可以用于计算与另一个坐标系中的同一个点的位置对应的一个点在一个坐标系中的位置。从一个坐标系向另一个坐标系转换一个点的位置的操作被称为“变换”或者“投影”。 两个术语是可互换的。换句话说,任意一个点都可以基于各种坐标系定义。在一个坐标系的{55.5, 11.0,100.2} 位置处的点可能在另一个坐标系的{10.0,-100.0, 20.0} 位置处。
回想一下,任意一个点的位置都可以表示为从坐标系原点到这个点的一个矢量。要变换一个矢量,请用一个矩阵乘以这个矢量。结果是与这个矩阵相关的坐标系内的相同矢量。本书中的所有iOS例子几乎从未使用Objective-C或者C代码来变换矢量,因为这个运算GPU执行得最好。本书中的很多OpenGL ES 2.0 Shading Language例子会包含“ gl_ Position = u_mvpMatrix * vec4(a_position, 1.0);”语句,一个位置乘以modelviewProjectionMatrix (mvpMatrix) 矩阵的结果是在OpenGL默认坐标系中表示 的同一个位置。
如果你需要用C代码来变换一个矢量,苹果公司的GLKit框架提供了函数GLKM-atrix4MultiplyVector3(GLKMatrix4 matrixLeft, GLKVector3 vectorRight)和GLKMatrix4-MultiplyVector3 WithTranslation(GLKMatrix4 matrixLeft, GLK Vector3 vectorRight)。函数GLKMatrix4MultiplyVector3()会执行变换,并忽略编码在这个矩阵的坐标轴原点部分中的任意平移。GLKMatrix4MultiplyVector3 WithTranslation()函数会在变换中包括平移。问题是没有任何数学上定义的运算能够把一个4X4矩阵乘以一个三分量矢量。这两个GLKit函数都会把3D矢量扩展为一个四分量矢量。GLKMatrix4MultiplyVector3函数会使用0来扩展矢量以转换3D矢量{x,y,z},为4D矢量{x,y,z,0.0}。当用0分量乘;以编码在这个矩阵的坐标轴原点部分中的平移值时,平移就会无效。GLKMatrix4Multiply-Vector3WithTranslation()函数会扩展3D矢量为一个4D矢量{x, y, z,1.0}。 当这个4D矢量乘以这个矩阵时,编码在这个矩阵的坐标轴原点部分中的平移值会乘以1.0,并 且因此会包含平移转换。
你可以在苹果公司的GLKit的GLKMatrix4.h文件中看到实现4D矢量变换的C代码。下面的函数实现突出显示了用来计算变换矢量的每个分量的运算代码。
GLKMatrix4 GLKMatrix4Multiply(GLKMatrix4 matrixLeft, GLKMatrix4 matrixRight) {
GLKMatrix4 m;
m.m[0] = matrixLeft.m[0] * matrixRight.m[0] + matrixLeft.m[4] * matrixRight.m[1] + matrixLeft.m[8] * matrixRight.m[2] + matrixLeft.m[12] * matrixRight.m[3];
m.m[4] = matrixLeft.m[0] * matrixRight.m[4] + matrixLeft.m[4] * matrixRight.m[5] + matrixLeft.m[8] * matrixRight.m[6] + matrixLeft.m[12] * matrixRight.m[7];
m.m[8] = matrixLeft.m[0] * matrixRight.m[8] + matrixLeft.m[4] * matrixRight.m[9] + matrixLeft.m[8] * matrixRight.m[10] + matrixLeft.m[12] * matrixRight.m[11];
m.m[12] = matrixLeft.m[0] * matrixRight.m[12] + matrixLeft.m[4] * matrixRight.m[13] + matrixLeft.m[8] * matrixRight.m[14] + matrixLeft.m[12] * matrixRight.m[15];
m.m[1] = matrixLeft.m[1] * matrixRight.m[0] + matrixLeft.m[5] * matrixRight.m[1] + matrixLeft.m[9] * matrixRight.m[2] + matrixLeft.m[13] * matrixRight.m[3];
m.m[5] = matrixLeft.m[1] * matrixRight.m[4] + matrixLeft.m[5] * matrixRight.m[5] + matrixLeft.m[9] * matrixRight.m[6] + matrixLeft.m[13] * matrixRight.m[7];
m.m[9] = matrixLeft.m[1] * matrixRight.m[8] + matrixLeft.m[5] * matrixRight.m[9] + matrixLeft.m[9] * matrixRight.m[10] + matrixLeft.m[13] * matrixRight.m[11];
m.m[13] = matrixLeft.m[1] * matrixRight.m[12] + matrixLeft.m[5] * matrixRight.m[13] + matrixLeft.m[9] * matrixRight.m[14] + matrixLeft.m[13] * matrixRight.m[15];
m.m[2] = matrixLeft.m[2] * matrixRight.m[0] + matrixLeft.m[6] * matrixRight.m[1] + matrixLeft.m[10] * matrixRight.m[2] + matrixLeft.m[14] * matrixRight.m[3];
m.m[6] = matrixLeft.m[2] * matrixRight.m[4] + matrixLeft.m[6] * matrixRight.m[5] + matrixLeft.m[10] * matrixRight.m[6] + matrixLeft.m[14] * matrixRight.m[7];
m.m[10] = matrixLeft.m[2] * matrixRight.m[8] + matrixLeft.m[6] * matrixRight.m[9] + matrixLeft.m[10] * matrixRight.m[10] + matrixLeft.m[14] * matrixRight.m[11];
m.m[14] = matrixLeft.m[2] * matrixRight.m[12] + matrixLeft.m[6] * matrixRight.m[13] + matrixLeft.m[10] * matrixRight.m[14] + matrixLeft.m[14] * matrixRight.m[15];
m.m[3] = matrixLeft.m[3] * matrixRight.m[0] + matrixLeft.m[7] * matrixRight.m[1] + matrixLeft.m[11] * matrixRight.m[2] + matrixLeft.m[15] * matrixRight.m[3];
m.m[7] = matrixLeft.m[3] * matrixRight.m[4] + matrixLeft.m[7] * matrixRight.m[5] + matrixLeft.m[11] * matrixRight.m[6] + matrixLeft.m[15] * matrixRight.m[7];
m.m[11] = matrixLeft.m[3] * matrixRight.m[8] + matrixLeft.m[7] * matrixRight.m[9] + matrixLeft.m[11] * matrixRight.m[10] + matrixLeft.m[15] * matrixRight.m[11];
m.m[15] = matrixLeft.m[3] * matrixRight.m[12] + matrixLeft.m[7] * matrixRight.m[13] + matrixLeft.m[11] * matrixRight.m[14] + matrixLeft.m[15] * matrixRight.m[15];
return m;
}
在GLKMatrix4MultiplyVector4()函数中的计算看起来很熟悉,这并非巧合。它们执行的是四维的标量积计算。使用一个矩阵变换一个点会提取从这个坐标系原点到这个点的矢量的分量。这些目标分量是与编码在这个矩阵中的每个坐标轴对齐的分量。变换的X坐标是在这个矩阵的X轴方向上的矢量部分。变换的Y坐标是在这个矩阵的Y轴方向上的矢量部分。变换的Z坐标是在这个矩阵的Z轴方向上的矢量部分。在GLKMatrix4MultiplyVector4()函数的实现中的粗体代码突出了四分量矢量的最后一个w分量与编码在矩阵坐标轴原点的平移值之间的相互作用。
11.2.5 转置矩阵和逆矩阵
矩阵定义了一个坐标系与另一个之间的关系。这个关系可以翻转。一个把坐标系A映射到坐标系B的矩阵可以被转置为映射坐标系B到坐标系A。转置一个矩阵是通过切换一个矩阵的行和列来完成的。图11-13描述了一个矩阵以及这个矩阵对应的转置矩阵。请注意在图11-13中沿着矩阵A对角线的阴影矩阵元素与矩阵A转置矩阵对角线位置的元素是相同的。思考转置操作的一个方法是把转置矩阵看做是源矩阵的一个倒影,与源矩阵沿对角线对称。还要注意的是转置矩阵的转置会再次产生源矩阵,在图11-13中矩阵A的转置的转置会再次产生矩阵A。

GLKit提供了用于返回矩阵的转置的GLKMatrix4 GLKMatrix4Transpose(GLKMatrix4 matrix)函数。
一个矩阵的逆是与源矩阵相乘会产生单位矩阵的矩阵。给定矩阵A和它的逆矩阵B,那么矩阵A乘以矩阵B会产生单位矩阵。对于常见矩阵来说,计算矩阵的逆需要做很多工作。但是,当一个矩阵的所有坐标轴被施加了一致的缩放时,矩阵的逆就 等于矩阵的转置。并不是所有的矩阵都是可逆的。例如,如果一个矩阵包含一个值为0的缩放因素,那么这个矩阵就没有逆矩阵。
GLKit提供了GLKMatrix4 GLKMatrix4Invert(GLKMatrix4 matrix, bool *isInvertible)函数。其中的isInvertible参数会返回一个布尔值,来告诉你这个矩阵是否可逆。
GLKit还提供了GLKMatrix4InvertAndTranspose(GLKMatrix4 matrix, bool *isInvertible)方法,这个方法会返回矩阵的逆的转置(如果有的话)并会通过isInvertible参数告 诉你这个矩阵是否可逆。当使用标准OpenGL灯光方程时,基于法向量的灯光的正确计算需要用到model-view矩阵的逆的转置。GLKBaseEffect类会按需计算model-view矩阵的逆的转置以返回baseeffect的normalMatrix属性。在例子OpenGLES_Ch7_3中的UtilityArmatureBaseEffect类和第10章例子中的UtilityModelEffect类都会使用GLKMatrix4InvertAndTranspose()方法来计算一个mormalMatrix属性。这些例子间接地展示了在OpenGL ES Shading Language程序中的灯光方程是怎么使用normalMatrix的。