OpenGL学习脚印: 关于gluLookAt函数的理解
写在前面
本节借助gluLookAt函数,推导世界坐标转换到照相机坐标的一种方法,重点在于理解UVN相机坐标系,以及变换矩阵的推导。限于笔者水平,如果错误请纠正我。
gluLookAt函数提供给用户完成模式变换(model-view transformation)中,在将模型坐标系转换都世界坐标系后,进行世界坐标系到照相机坐标系的转换。实际上,照相机的定位也是在世界坐标系下定义的,这里的转换,可以理解为: 从照相机的角度解释世界坐标系中物体的坐标。通过构造一个UVN坐标系来简化这一转换。
先直观感受下UVN,UVN坐标系中的照相机模型如下图所示:

借助下图正式定义UVN相机坐标系:

与UVN相关的概念包括:
- 相机位置,或者叫做视点(eyepoint):
观察参考点 (View Reference Point)
- 相机镜头方向,通过观察平面的法向量指定: 观察平面法向量VPN (View Plane Normal)
- 相机顶部正朝向:VUV (View Up Vector)
形象的表达为:

gluLookAt函数原型为:
- void gluLookAt(GLdouble eyeX, GLdouble eyeY, GLdouble eyeZ,
-
- GLdouble centerX, GLdouble centerY, GLdouble centerZ,
-
- GLdouble upX, GLdouble upY, GLdouble upZ);
官网关于此函数的描述:
gluLookAt 通过指定一个视点、表面场景中心的参考点以及up向量来构造一个视变换矩阵。
这个矩阵将代表场景中心的参考点映射到-Z轴,视点映射成为原点。当使用一个特定的投影矩阵时,场景的中心就映射到视口的中心。类似地,由up向量描述的方向投影到投影平面成为+y轴,这样它在视口中向上指向。up向量必须不能与从视点到参考点的直线平行。
那么如何确定u-v-n坐标系呢?计算公式如下:

这里需要注意: OpenGL中使用的相机坐标系是右手坐标系,UVN坐标系是左手坐标系。在构造实际变换矩阵的过程中,OpenGL
需要将-n轴翻转为相机坐标系的+z轴,uv轴定位相机坐标系的+x和+y轴。这与推导相机变换矩阵一文最后的结果矩阵有所不同。
如何构造视变换矩阵?
视变换就是在相机坐标系下解释世界坐标系下的点。而这个变换矩阵的构造,可以看做将相机坐标系变换到与原来的世界坐标系重合。而将世界坐标系变换到与相机坐标系重合,可以看做是这个所求变换的逆过程。
将世界坐标系变换到与相机坐标系重合,实际上进行了两个步骤: 第一步将世界坐标系旋转一定角度记作变换R,再将世界坐标系平移到视点位置记作T,那么这个变换矩阵记为M=TR。要将世界坐标系的点变换到照相机坐标系下,需要使用矩阵M的逆矩阵,即: inverse(M)=inverse(R)*inverse(T)。即所求变换矩阵为inverse(M)。
平移矩阵的逆矩阵形式简单,就是取平移量(eyex,eyey,eyez)的相反数,即:

那么现在的关键是如何求出旋转矩阵R?
上面我们构造的UVN坐标系u-v-n3个基向量可以构造成矩阵:

注意这里对n轴进行了翻转,构成右手照相机坐标系。
怎么看这个矩阵A呢,矩阵A实际上代表的就是一个旋转矩阵(从矩阵形式上看出)。
旋转矩阵的一个特点就是它是正交矩阵,即有inverse(A) = transpose(A).(A^-1 = A^T)
很多教材和博客都说,这里A矩阵可以看做是将世界坐标系转换到与照相机坐标系重合时的旋转矩阵,这一点怎么理解呢?
个人理解,矩阵A第四列为0,0,0,1,可以看做是世界坐标系和照相机坐标系原点重合;根据《OpenGL学习脚印: 理解坐标系及坐标变换(上) 》中所讲,矩阵前3列即变换后的基向量,那么这个基向量(都是单位向量)是如何计算出来的呢?就是通过旋转原来的世界坐标系的基向量来构造的。因此,可以说矩阵A代表的就是将世界坐标系旋转到与相机坐标系重合时的旋转矩阵R,即R
= A。
则inverse(R) = inverse(A) = transpose(A) 即为:

所以gluLookAt所求变换矩阵inverse(M)为:

gluLookAt的默认值是(0, 0, 0, 0, 0,-1, 0, 1, 0);通过计算可得出:u=(1,0,0),v=(0,1,0),n=(0,0,-1),这样构成的矩阵M^-1即为单位矩阵。
下面通过代码来验证下结论。代码绘制一个立方体,设置为透视投影,并通过gluLookAt设置相机方位来查看立方体。
注意,为了便于观察视变换矩阵,这里并没有进行其他模型变换;手动计算矩阵时使用了数学库glm来进行向量点积和叉积运算。
- //计算gluLookAt矩阵
-
- #include <GL/glew.h>
- #include <GL/freeglut.h>
- #include <glm/glm.hpp>
- #include <iostream>
- #pragma comment(lib,"freeglut.lib")
- #pragma comment(lib,"glew32.lib")
-
- void userInit();
- void display( void );
- void keyboardAction( unsigned char key, int x, int y );
- void reshape(int w,int h);
-
- int main( int argc, char **argv )
- {
- glutInit(&argc, argv);//初始化GLUT
-
- glutInitDisplayMode( GLUT_RGBA|GLUT_DOUBLE);
- glutInitWindowPosition(100,100);
- glutInitWindowSize( 512, 512 );
- glutCreateWindow( "gluLookAt demo" );
-
- glewInit();//使用GLEW时,使用该函数初始化GLEW
- userInit();//自定义的初始化函数
- glutReshapeFunc(reshape);
- glutDisplayFunc( display );
- glutKeyboardFunc( keyboardAction );
- glutMainLoop();
- return 0;
- }
- //自定义初始化函数
- void userInit()
- {
- glClearColor( 0.0, 0.0, 0.0, 0.0 );
- glColor4f(0.6f,0.5f,0.0,0.0);
- }
- //设置视变换矩阵
- void setViewMatrix(GLdouble *theMatrix,GLdouble eyex,GLdouble eyey,GLdouble eyez,
- GLdouble targetx,GLdouble targety,GLdouble targetz,
- GLdouble vupx,GLdouble vupy,GLdouble vupz)
- {
- glm::vec3 eye(eyex,eyey,eyez),target(targetx,targety,targetz),vup(vupx,vupy,vupz);
- //构造n轴
- glm::vec3 nvec(target-eye);
- nvec=glm::normalize(nvec);
- //构造u轴
- vup = glm::normalize(vup);
- glm::vec3 uvec = glm::cross(nvec,vup);
- uvec=glm::normalize(uvec);
- //构造v轴
- glm::vec3 vvec = glm::cross(uvec,nvec);
- vvec=glm::normalize(vvec);
- //设置4x4矩阵
- memset(theMatrix,0,sizeof(GLdouble)*16);
-
- theMatrix[0] = uvec.x;
- theMatrix[4] = uvec.y;
- theMatrix[8] = uvec.z;
- theMatrix[12 ] = -glm::dot(eye,uvec);
-
- theMatrix[1] = vvec.x;
- theMatrix[5] = vvec.y;
- theMatrix[9] = vvec.z;
- theMatrix[13] = -glm::dot(eye,vvec);
-
- //注意这行数据
- theMatrix[2] = -nvec.x;
- theMatrix[6] = -nvec.y;
- theMatrix[10] = -nvec.z;
- theMatrix[14] = glm::dot(eye,nvec);
-
- theMatrix[15] = 1.0;
- }
- void reshape(int w,int h)
- {
- glViewport(0,0,GLsizei(w),GLsizei(h));
- glMatrixMode(GL_PROJECTION);
- glLoadIdentity();
- gluPerspective(60.0,(GLfloat)w/(GLfloat)h,1.0,10.0);
- glMatrixMode(GL_MODELVIEW);
- glLoadIdentity();
-
- //gluLookAt(2.0,0.0,1.8,0.0,0.0,0.0,0.0,1.0,0.0);
-
- //手动构造视变换矩阵
- GLdouble theMatrix[16];
- setViewMatrix(theMatrix,2.0,0.0,1.8,0.0,0.0,0.0,0.0,1.0,0.0);
- glMultMatrixd(theMatrix);
-
- //打印当前模视变换矩阵内容
- GLdouble modelViewMat[16];
- glGetDoublev(GL_MODELVIEW_MATRIX,modelViewMat);
- for(int i = 0;i<4;i++)
- for(int j=0;j<4;j++)
- {
- fprintf(stdout,"%-.4f\t",modelViewMat[i+4*j]);
- if((j+1) %4 == 0) fprintf(stdout,"\n");
- }
- }
- //绘制回调函数
- void display( void )
- {
- glClear( GL_COLOR_BUFFER_BIT);//清除颜色缓存
- glLineWidth(2.0);
- glutWireCube(1.0);
- glutSwapBuffers();
- }
- //键盘按键回调函数
- void keyboardAction( unsigned char key, int x, int y )
- {
- switch( key ) {
- case 033: // Escape key
- case 'q': case 'Q':
- exit( EXIT_SUCCESS );
- break;
- }
- }
使用gluLookAt如下图左右所示:

手动计算视变换矩阵,效果如下图所示:

可以看出两者是一样的,二者的视变换矩阵打印出来均为:
0.6690 0.0000 -0.7433 0.0000
0.0000 1.0000 0.0000 0.0000
0.7433 0.0000 0.6690 -2.6907
0.0000 0.0000 0.0000 1.0000
至此证明了上述推导的矩阵确实为OpenGL中使用的视变换矩阵。
关于OpenGL gluLookAt官网提供了参考实现,可以查看:
GluLookAt code;
另外关于顶部正朝向vup的理解有更好的通俗解释,可以查看:Opengl---gluLookAt函数详解
。
|