4.2、计算有多少光线照向每个三角形
在渲染时无论是使用OpenGL ES 1.x灯光模拟还是备选方案,光线与3D几何图形的相互作用都起了重要作用。光线与几何图形相互作用的关键是要计算出每个几何物体照射和发散出来多少光线。要做到这一点,就需要计算出每个三角形有多么接近于与光的方向相垂直的方向。线性代数提供了解决方案。三角形可以由三个顶点来定义。第1章介绍了怎么计算任意两个顶点之间的矢量。因此使用一个顶点和两个矢量来定义一个三角形也是可行的,参见图4-3。
如在第1章提到的,很多方便的运算都可以用矢量来完成。最重要的矢量运算之一是矢量积:提供两个矢量,这两个矢量的矢量积定义了第三个垂直于前两个矢量的矢量,参见图4-4。


下面的方程式使用减法和乘法代替三角函数来计算两个矢量的矢量积; GPU可以快速地执行减法和乘法运算。
VectorC.x = VectorA.y * VectorB.z 一VectorA.z * VectorB.y;
VectorC.y = VectorA.z * VectorB.x一VectorA.x * VectorB.z;
VectorC.z = VectorA.x * VectorB.y - VectorA.y * VectorB.x;
注意 VectorBxVectorA的矢量积和VectorAXVectorB矢量积的方向是相反的。一个常见的错误是无意中计算出了相反的矢量积,这会使光线扭曲并使每个三角形的前面变成后面。记住矢量顺序的最简单的方法是“右手原则”,在网页: http://en.wikipedia.org/wiki/Right-hand rule 上做了很好的解释。如果你正俯视一个三角形,三角形的顶点是以逆时针顺序计数的。如图4-4所示,从所有矢量交接的顶点开始,以逆时针方向的下一个顶点是矢量A末尾的那个顶点,最后一个顶点在矢量B的末尾。
光线计算依赖于表面法向量(surfacenormalvector),或者简称法向量(normalvector)。可以为任何一个三角形计算出一个法向量:法向量的方向垂直于一个三角形的平面并且法向量可以使用定义三角形的任意两个矢量的矢量积计算出来。法向量也是单位向量,这意味着一个法向量的大小(也称为长度)总是1.0。
任何矢量都可以转换成一个单位向量,通过用这个矢量的长度除以这个矢量的每个分量。结果是一个与原先的矢量方向相同的并且长度等于1.0的新矢量。因此,为了计算一个法向量,首先需要计算矢量积向量,然后用这个矢量积向量的长度除以矢量积的每个分量。这个操作是如此的常见以至于转换矢量为单位矢量通常被称为“标准化”操作。例子程序OpenGLES_Ch4_1使用下面的代码来计算法向量:
// Returns a unit vector in the same direction as the cross
// product of vectorA and VectorB
GLKVector3 SceneVector3UnitNormal(
const GLKVector3 vectorA,
const GLKVector3 vectorB)
{
return GLKVector3Normalize(
GLKVector 3CrossProduct(vectorA, vectorB));
}
矢量积是用GLKit的GLKVector3.h头文件内的一个内联函数来计算的。下面的实现匹配实际的GLKit实现并且已经为显示做了格式化:
// Returns the Cross Product vectorA x vectorB
GLKVector3 GLKVector3CrossProduct(
GLKVector3 vectorA,
GLKVector3 vectorB)
{
GLKVector3 result = {
vectorA.y * vectorB.z - vectorA.z * vectorB.y,
vectorA.z * vectorB.x - vectorA.x * vectorB.z,
vectorA.x * vectorB.y - vectorA.y * vectorB.x
};
return result;
}
GLKit的GLKVector3Normalize (GLKVector3 vectorA)函数需要多点解释。这个函数的目的是返回一个与矢量A方向相同但是大小等于1.0的单位向量。一个矢量的大小 是这个矢量的长度,这个长度可以用标准距离公式 $\sqrt{vectorA. x2 + vectorA. y2 + vectorA.z2}$计算得到。
下面的对于SceneVector3Length()和SceneVector3Normalize()函数的实现与对应的GLKVector3Length()和GLKVector3Normalize()函数的实现类似。在产品代码中应该使用GLKit函数以受益于苹果在将来的框架版本中提供的维护和性能的增强。SceneVector3实现为显示格式化了代码并显示了算术错误检查:
////
// Returns the length a.k.a. magnitude of vectorA
GLfloat sceneVector3Length(const GLKVector3 vectorA) {
GLfloat length = 0.0f;
GLfloat lengthSquared =
(vectorA.x * vectorA.x) +
(vectorA.y * vectorA.y) +
(vectorA.z * vectorA.z);
if(FLT_ EPSILON < lengthSquared) { // avoid square root of zero error if lengthSquared
// is too small
length = sqrtf (lengthSquared);
return length;
}
在前面的SceneVector3Length()函数中使用的常量FLT_FPSILON是在iOs SDK提供的标准float.h头文件中定义的。FLT_EPSILON常量是一个非常小的在浮点数学计算的过程中不会被舍人为0的正数。重要的是要确lengthSquared不会太接近于0以防止不经意间计算了0的平方根。
在知道了一个矢量的长度以后,又实现了SceneVector3Normalize(GLKVector3vectorA)函数来缩放矢量A为一个单位向量,具体代码如下:
// Returns a Unit Vector with the same direction as vectorA
GLKVector3 SceneVector 3Normalize(GLKVector3 vectorA)
{
const GLfloat length = SceneVector3Length(vectorA);
float
oneOverLength = 0.0f;
if(FLT_ EPSILON < length) { // avoid divide by zero if length too small
oneOverLength = 1.0f / length;
GLKvector3 result = {
vectorA.x * oneOverLength;
vectorA.y * oneOverLength;
vectorA.z * oneOverLength;
return result;
}
投射到三角形上的光线的数量可以通过确定光线的方向与法向量的方向之间的角度轻松计算出来。再次,线性代数提供了一个快速方便的解决方案。两个单位矢量之间的标量积可以计算出介于两个矢量之间的角的余弦,参见图4-5。介于一个光线方向上的单位向量和一个三角形的表面法向量之间的角度的余弦决定了照射到三角形上的光线的数量。如果三角形垂直于光线,那么表面法向量会平行于光线的方向,因此它们之间的 角度就是0度。0.0 度的余弦值是1.0,这意味着最大强度的光会照射到三角形上。如果这个三角形平行于光线的方向,那么介于光线和表面法向量之间的角度就是90.0度或者-90.0度(余弦值都是0.0) , 因此没有光线会照射到三角形上。0.0 度和90.0度之间I的角度的余弦值在0.0至1.0之间,它决定了照射到三角形上的光线的数量。

OpenGL ES程序为每个顶点指定了单独的法向量。法向量通常计算一次,然后与顶点的位置和纹理坐标一起被保存起来。标准OpenGLES1.1灯光模拟会使用下面的方程式用GPU为每个顶点计算光线的方向与法向量之间的标量积。
DotProduct = (VectorA.x * VectorB.x) + (VectorA.y * VectorB.y) +
(VectorA.z * VectorB.z); ;
如果一个三角形的三个顶点被赋予了相同的法向量,这叫做平面法线,灯光模拟会让三角形显得平坦。如果每个顶点的法向量是包含顶点的所有三角形的法向量的一个平均值,灯光模拟会创建三角形被轻微弯曲的错觉,如图4-6所示。
