OpenGL超级宝典学习笔记——操作矩阵
为了更强大的功能和灵活性,我们有时需要直接操作矩阵。在OpenGL中4x4的矩阵用包含16个浮点数值的一维数组来表示,而不是用二维的4x4的数组来表示。OpenGL之所以这么做,因为使用一维数组更高效。当然OpenGL也支持二维数组的表示方式。而且要特别注意的是在矩阵中是使用列主序遍历数组的,即按列逐个遍历数组中的元素。
事实上,这个矩阵里的16个值代表着空间中的一个特定的位置和三个轴的朝向(相对于视点坐标系)。前3列是方向向量分别代表着3个轴的朝向(绝大多数情况下,这3个向量是正交的),第四列用于平移变换、glTranslate函数就是把数值填到这一列中。这个4x4的矩阵相当于一个坐标系统的位置和方向,如果把一个顶点(用列向量的形式)与这个矩阵相乘,得到的结果是一个变换到该坐标系统的新顶点。这意味着空间中的任意一个点和方向,能够用唯一的4x4的矩阵表示。如果你把物体中的所有顶点都乘以这个矩阵,那么你就是把整个物体变换到空间中指定的位置和朝向(我的理解是可以用这个矩阵所代表的坐标系统来表示你的整个物体)。
PS:注意最后一行的元素除了最后一个为1之外,其余为0。
加载矩阵
你可以使用下面的两个函数来加载你的列主序的矩阵到投影矩阵,模型视图矩阵或者纹理矩阵栈中。
glLoadMatrixf(GLfloat* m);
glLoadMatrixd(GLdouble* m);
绝大多数的OpenGL的实现是使用单精度的浮点数来计算管道中的数据的。使用双精度的形式会带来一定的性能开销。
下面的代码相当于调用glLoadIdentity函数。
// 加载单位矩阵
GLfloat m[] = { 1.0f, 0.0f, 0.0f, 0.0f, // X 列
0.0f, 1.0f, 0.0f, 0.0f, // Y 列
0.0f, 0.0f, 1.0f, 0.0f, // Z 列
0.0f, 0.0f, 0.0f, 1.0f }; // 平移列
glMatrixMode(GL_MODELVIEW);
glLoadMatrixf(m);
相对应的OpenGL还提供了加载行主序的矩阵的两个函数
void glLoadTransposeMatrixf(GLfloat *m);
void glLoadTransposeMatrixd(GLdouble *m);
手工执行变换
一个高级的例子:
1: void RenderScene(void)
2: {
3: M3DMatrix44f transformationMatrix; //保存旋转矩阵
4: static GLfloat yRot = 0.0f; // 旋转的角度
5: yRot += 0.5f;
6:
7: glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
8:
9: // 构造一个矩阵
10: m3dRotationMatrix44(transformationMatrix, m3dDegToRad(yRot), 0.0f, 1.0f, 0.0f);
11: transformationMatrix[12] = 0.0f;
12: transformationMatrix[13] = 0.0f;
13: transformationMatrix[14] = -2.5f;
14: //画圆环
15: DrawTorus(transformationMatrix);
16:
17: glutSwapBuffers();
18: }
transformationMatrix[12] = 0.0f;
transformationMatrix[13] = 0.0f;
transformationMatrix[14] = -2.5f;
这三个是执行平移变换.相当于glTranslatef(0.0f, 0.0f, –2.5f); m3dRotationMatrix44函数如下
1: void m3dRotationMatrix44(M3DMatrix44f m, float angle, float x, float y, float z)
2: {
3: float mag, s, c;
4: float xx, yy, zz, xy, yz, zx, xs, ys, zs, one_c;
5:
6: s = float(sin(angle));
7: c = float(cos(angle));
8:
9: mag = float(sqrt( x*x + y*y + z*z ));
10:
11: // Identity matrix
12: if (mag == 0.0f) {
13: m3dLoadIdentity44(m);
14: return;
15: }
16:
17: // Rotation matrix is normalized
18: x /= mag;
19: y /= mag;
20: z /= mag;
21:
22: #define M(row,col) m[col*4+row]
23:
24: xx = x * x;
25: yy = y * y;
26: zz = z * z;
27: xy = x * y;
28: yz = y * z;
29: zx = z * x;
30: xs = x * s;
31: ys = y * s;
32: zs = z * s;
33: one_c = 1.0f - c;
34:
35: M(0,0) = (one_c * xx) + c;
36: M(0,1) = (one_c * xy) - zs;
37: M(0,2) = (one_c * zx) + ys;
38: M(0,3) = 0.0f;
39:
40: M(1,0) = (one_c * xy) + zs;
41: M(1,1) = (one_c * yy) + c;
42: M(1,2) = (one_c * yz) - xs;
43: M(1,3) = 0.0f;
44:
45: M(2,0) = (one_c * zx) - ys;
46: M(2,1) = (one_c * yz) + xs;
47: M(2,2) = (one_c * zz) + c;
48: M(2,3) = 0.0f;
49:
50: M(3,0) = 0.0f;
51: M(3,1) = 0.0f;
52: M(3,2) = 0.0f;
53: M(3,3) = 1.0f;
54:
55: #undef M
56: }
最终的效果如下:
手动执行变换在碰撞检测,平截头体剔除,以及一些特效算法中会用到。
使用相机和角色在OpenGL中移动
在场景中移动的物体成为角色,就像舞台剧上的演员一样。角色有他们自己的变换,不仅仅是相对于世界坐标系(视点坐标系)的变换,也可以相对于其他角色坐标系的变换。每个角色都有自己的参考帧和自己的坐标系(物体坐标系)。在物体坐标系和世界坐标系之间的转换是非常有用的。
角色帧
一个简单灵活的表示角色的方式是用一个包含一个空间中的位置,一个指向前面的向量以及一个指向上面的向量(第三个向量可以通过计算得到)。使用这些量就可以唯一地标识空间中一个特定的位置和方向。
typedef float M3DMatrix44f[16]; // A 4 X 4 matrix, column major (floats) - OpenGL style
class GLFrame { protected: M3DVector3f vLoaction; M3DVector3f vUp; M3DVector3f vForward; public: … };
使用这样的一个参考帧来表示一个物体的位置和方向是非常有用的。我们可以使用这些数据直接创建一个4x4的变换矩阵。其中向上的向量代表y列向量,向前的向量代表z列向量,位置则代表移动列向量。这样只缺少了x向量。因为我们知道这3个轴是互相垂直的,因此可以有由y和z向量的叉乘来计算x列向量。
void GLFrame::GetMatrix(M3DMatrix44f mMatrix, bool bRotationOnly = false) { //计算列向量,叉乘 M3DVector3f vXAxis; m3dCorssProduct(vXAxis, vUp, vForward); //把各个向量转换为矩阵的列向量, X列 m3dSetMatrixColumn44(matrix, vXAxis, 0); matrix[3] = 0.0f; //y列 m3dSetMatrixColumn44(matrix, vUp, 1); matrix[7] = 0.0f; //z列 m3dSetMatrixColumn44(matrix, vForward, 2); matrix[11] = 0.0f; //只包含旋转不移动 if(bRotationOnly = true) { matrix[12] = 0.0f; matrix[13] = 0.0f; matrix[14] = 0.0f; } else m3dSetMatrixColumn44(matrix, vOrigin, 3); matrix[15] = 1.0f; }
欧拉角表示法
参考《3D数学基础_图形与游戏开发》
欧拉角的基本思想是讲角位移分解为三个互相垂直轴的三个旋转组成的序列。
以下都是使用左手法则。“heading-pitch-bank” heading为绕y轴的旋转量,绕惯性坐标系y轴的旋转。向右旋转为正。pitch为绕x轴的旋转量。物体坐标系的x轴,不是原惯性坐标系的x轴。向下旋转为正方向。bank为绕z轴的旋转量。物体坐标系的z轴。逆时针为正方向。
PS:当我们说的旋转顺序是"heading-pitch-bank”时,是指从惯性坐标系到物体坐标系。如果从物体坐标系变换到惯性坐标系,旋转的顺序就是相反的
关于欧拉角的其他约定
- 一组常用的术语是roll-pitch-yaw,其中roll等价于bank, yaw基本上等价于heading。它的顺序和heading-pitch-bank的顺序相反。它定义了向量从物体坐标系到惯性坐标系的变换旋转顺序。(事实上,yaw和heading还是有技术上的差别,yaw是绕物体坐标系y轴的旋转,heading是绕惯性坐标系y轴的旋转。,因为这里的旋转是在物体坐标系y轴和惯性坐标系y轴重合是进行的,所以这个区别并不重要)
- 任意三个坐标轴都能作为旋转轴。
- 决定每个旋转的正方向时不一定必须遵守左手或右手法则。
- 旋转可以以不同的顺序进行。但heading-pitch-bank顺序最为实用。heading度量绕竖直轴的旋转,它之所以有意义主要是因为我们所在的环境经常有某种形式的“地面”,一般料将绕惯性坐标系的x或z轴的旋转没有什么意义。pitch度量水平方向的倾角,bank度量的是绕z轴的旋转量
欧拉角的优点
- 欧拉角容易使用,它用三个数来代表绕三个轴旋转的角度。角度符合人类的思维习惯。heading-pitch-bank系统就能直接地描述出偏差的角度。当需要显示方位或键盘输入方位时,欧拉角是唯一的选择。
- 最简洁的表达方式
- 任意三个数都是合法的。
欧拉角的缺点
- 给定方位的表达方式不唯一
- 两个角度间求插值非常困难
基本问题
- 将一个角度加上360的倍数,并不会改变方位。
- 由三个角度不互相独立而导致。如先heading45再pitch90,这与先pitch90再bank45是等价的。
解决方法:讲heading和bank限制在+180到-180之间,pitch限制在+90到-90之间。这种现象,角度为+-90的第二次旋转将使得第一次和第三次旋转的旋转轴相同,称作万向锁。为了消除限制欧拉角的这种别名现象,规定万向锁情况下由heading完成绕竖直轴的全部旋转。
欧拉角总结
照相机管理
OpenGL中并不真正存在照相机变换。相机作为一个隐喻,帮助我们理解如何管理3D环境中的视点。照相机可以想象为一种物体,在空间中具有某个位置和特定方向。
应用照相机变换,我们要使用照相机的角色变换并进行反转。这样当我们把相机向后移时就相当于向前移动整个场景。向左旋转相机则相当于向右旋转整个场景。
glu库中包含了一个函数用于创建相机变换,它使用的数据与上面定义的帧结构数据相同。
void gluLookAt(GLdouble eyex, GLdouble eyey, GLdouble eyez,
GLdouble centerx, GLdouble centery, GLdouble centerz,
GLdouble upx, GLdouble upy, GLdouble upz);
这个函数接受一个观察点的位置,一个在观察点正前方的一个点,以及向上的方向向量。
渲染一个指定的场景如下图: