这一课中我们来看一看如何在三维场景中对场景中的物体进行各式各样的变换,并且在保持场景的立体感的情况下将其渲染到屏幕上去!常见的方法是用矩阵表示每一个变换, 之后将它们挨个儿相乘,最后乘以顶点位置产生最后的结果!接下来的每一章都将着重介绍一种转变。
这一课中我们先来看一个平移变换,平移变换的任务的将一个对象沿着一个任意长度和方向的向量移动,我们假设你想将下图中左边的三角形移到右边的位置:
要实现这个效果的一个方法就是提供一个偏移向量(上图中是(1,1))作为一个一致变量传递给着色器,仅仅为每个需要处理的顶点加上这个偏移向量即可。然而,这与我们所想要的将一组矩阵通过相乘得到一个综合矩阵的思想背道而驰。
除此之外,在后面的教程中你将看到平移通常并不是第一个变换,所以你得在平移变换前让位置向量乘上代表平移之前的变化矩阵,然后乘上这个平移变换,最后乘上代表平移之后的变换的矩阵。所以说上面的方法太笨拙了。
最好的方法是找到一个代表平移的矩阵,让它参与到所有矩阵的乘法运算中。但是你能找到一个矩阵,使得这个矩阵和左图三角形中底部的点(0,0)相乘后得到结果(1,1)吗?事实是你使用 2D 矩阵无法做到这一点,对于(0,0,0)用 3D 矩阵也不能做到这一点!
总的来说,我们需要的是一个矩阵 M,给定一个点 P 坐标(x,y,z)和一个向量 V(v1,v2,v3)后,能提供 M*P=P1(x+v1,y+v2,z+v3)。简单的来说就是矩阵 M 将 P 平移到 P+V 的位置。在 P1 中我们可以看到它的每个分量都是来自 P 的一个分量和 V 中与之相对应的分量之和。如果我们将变换矩阵设置为单位矩阵 I,我们会得到如下结果:
I * P=P(x,y,z)
所以看起来像是我们应该从单位矩阵开始,然后针对等式右边的每个分量(...+V1,...+V2, ...+V3),我们需要找到如何才能完成这样的变换。让我们来看看这个被改进的单位矩阵的样子:
从上面这个矩阵我们可以得出两个结论:
我们需要找到一个矩阵以满足下列右侧的计算:
所以我们需要在已知 a-f 为 0 的情况下找到上图所示加上 v1,v2,v3 的方法。这样最后的结果才会是我们平移后的向量。
从这个等式看上去貌似我们需要给矩阵增加第四列,但是这样做的话我们的计算将是无效的,因为我们不能用 3*4 的矩阵乘上 3*1 的向量!
矩阵相乘的规则是只允许 n*m 和 m*n 形式的矩阵相乘。所以我们不得不给我们的向量增加第四个分量。我们最好将向量的第四个分量设置为 1,因为这样当我们将 v1-v3 放到矩阵的第四列时,相乘之后并不会改变 v1、v2、v3 的大小,因为它们与 1 相乘!
但是由上面的规则可知,反过来 4*1 矩阵和 3*4 矩阵相乘仍然是无效的!通过给矩阵增加第四行使它成为一个 4*4 矩阵,这样就能够实现了!最后,这就是我们的平移矩阵:
现在,即使 x、y 和 z 都为 0 我们仍然可以平移它们到任何位置。
使用四维向量来表示三维向量的做法称作齐次坐标,这对3D图形学来说普遍而又实用。向量的第四个分量称之为 “w” 。事实上,我们在以前教程中见到的着色器中的内置变量 gl_Position 就是一个四维向量,w 分量会在将3D场景投影到 2D 平面时起重要作用!通用的做法是: w = 1 时表示点,w = 0 时表示向量。原因是点可以被平移但是向量不行,你可以改变向量的长度和方向,但是无论向量的起点在哪里,所有具有相同长度和方向的向量是被视为相同的!所以你可以简单地用原点作为所有向量的起点!当我们将 w 设置为0时,乘以变换矩阵后的向量还会是相同的向量。
structMatrix4f { float m[4][4]; };
我们将一个 4*4 的矩阵定义添加到 math_3d.h 中。从现在起,它将被运用到大多数转变矩阵中。
GLuintgWorldLocation;
我们使用这个句柄来获取着色器中的世界矩阵一致变量地址!我们称之为“世界”是因为我们所做的是,在我们的虚拟世界坐标系统中将一个物体移动(变换)到我们想要的位置。
Matrix4fWorld; World.m[0][0] = 1.0f; World.m[0][1] =0.0f; World.m[0][2] = 0.0f; World.m[0][3] = sinf(Scale); World.m[1][0] = 0.0f; World.m[1][1] =1.0f; World.m[1][2] = 0.0f; World.m[1][3] = 0.0f; World.m[2][0] = 0.0f; World.m[2][1] =0.0f; World.m[2][2] = 1.0f; World.m[2][3] = 0.0f; World.m[3][0] = 0.0f; World.m[3][1] =0.0f; World.m[3][2] = 0.0f; World.m[3][3] = 1.0f;
在渲染函数中,我们创建了一个 4*4 矩阵并根据上面的推导对其进行初始化!我们设置 v2 和 v3 为 0,因为我们希望物体在 y、z 坐标上没有变化,我们将 v1 的值设置为 sin 函数的结果,Scale的值在每一帧中都是不断变化的,这使得 X 坐标的值会在 -1 到 1 的范围内波动。现在我们需要把矩阵加载到着色器中。
glUniformMatrix4fv(gWorldLocation,1, GL_TRUE, &World.m[0][0]);
这是另外一个 glUniform 函数的例子,用来加载数据到着色器的一致变量中。这个特定的函数可以加载 4*4 的矩阵,也有用于加载 2*2, 3*3, 3*2, 2*4, 4*2, 3*4 和 4\3的版本。第一个参数是一致变量的位置(在着色器程序编译之后由 glGetUniformLocation() 函数返回的结果)。第二个参数代表我们更新的矩阵的数量。一个矩阵我们用 1,但是我们也可以调用这个函数一次更新多个矩阵。第三个参数很容易迷惑新手。它表明提供矩阵是按行优先还是列优先的! 关键在于 C/C++ 语言默认就是行优先的!这意味着当你给二维数组填充数据时,它们在内存中一行一行的排列,并且最上面的一行在低地址处!例如,看下面的数组:
int a[2][3]; a[0][0]= 1; a[0][1]= 2; a[0][2]= 3; a[1][0]= 4; a[1][1]= 5; a[1][2]= 6;
直观看来上这个数组看起来像下面的矩阵:
1 2 3 4 5 6
而在内存中的排列是这样的:1 2 3 4 5 6(1在最低地址)
所以我们设定函数 glUniformMatrix4fv() 第三个参数是 GL_TRUE 是因为我们以行优先的方式提供矩阵的。我们也可以将第三个参数为 GL_FALSE,但是这样的话我们需要转置矩阵的值,因为 C/C++ 中内存的排列仍然是行优先,但是 OpenGL 将认为我们提供的前四个值实际上是一个矩阵的列,并做相应处理。第四个参数是矩阵在内存的开始地址!
在着色器中的其余代码
Uniform mat4 gWorld;
这是一个 4*4 的矩阵类型的一致变量。也有 mat2 和 mat3。
gl_Position= gWorld * vec4(Position, 1.0);
我们添加到顶点缓冲区中的三角形顶点的位置属性是一个三维向量,但是之前我们知道对于一个点,其 W 分量应该为 1。所以这里有两种选择:
第一个选择没有优势,因为每个顶点位置属性需要消耗额外的四字节内存,但是我们知道那部分的内容一直是 1。较之而言,在VBO中维持三个分量的点,之后在着色器中为其添加第四个分量的方法就高效很多。在GLSL中通过使用 ‘vec4(Position, 1.0)’ 完成这个扩充。
我们将矩阵与这个顶点向量相乘,最后将其结果传入 gl_Position 中。总之本例中,每一帧我们都生成一个变换矩阵使得对象沿着 X 轴平移,并且这个平移的距离在[-1,1]之间波动。着色器将每个顶点的位置与此矩阵相乘,结果使物体左右移动。在大多数情况下,在顶点着色器完成处理后,三角形的一边会超出规范化空间,这时候裁剪器将把超出的那部分裁剪掉。这样我们就只能看到位于规范化空间内部的部分。