OpenGL學(xué)習(xí)腳印: 關(guān)于gluLookAt函數(shù)的理解
寫在前面
本節(jié)借助gluLookAt函數(shù),推導(dǎo)世界坐標(biāo)轉(zhuǎn)換到照相機(jī)坐標(biāo)的一種方法,重點在于理解UVN相機(jī)坐標(biāo)系,以及變換矩陣的推導(dǎo)。限于筆者水平,如果錯誤請糾正我。
gluLookAt函數(shù)提供給用戶完成模式變換(model-view transformation)中,在將模型坐標(biāo)系轉(zhuǎn)換都世界坐標(biāo)系后,進(jìn)行世界坐標(biāo)系到照相機(jī)坐標(biāo)系的轉(zhuǎn)換。實際上,照相機(jī)的定位也是在世界坐標(biāo)系下定義的,這里的轉(zhuǎn)換,可以理解為: 從照相機(jī)的角度解釋世界坐標(biāo)系中物體的坐標(biāo)。通過構(gòu)造一個UVN坐標(biāo)系來簡化這一轉(zhuǎn)換。
先直觀感受下UVN,UVN坐標(biāo)系中的照相機(jī)模型如下圖所示:

借助下圖正式定義UVN相機(jī)坐標(biāo)系:

與UVN相關(guān)的概念包括:
- 相機(jī)位置,或者叫做視點(eyepoint):
觀察參考點 (View Reference Point)
- 相機(jī)鏡頭方向,通過觀察平面的法向量指定: 觀察平面法向量VPN (View Plane Normal)
- 相機(jī)頂部正朝向:VUV (View Up Vector)
形象的表達(dá)為:

gluLookAt函數(shù)原型為:
- void gluLookAt(GLdouble eyeX, GLdouble eyeY, GLdouble eyeZ,
-
- GLdouble centerX, GLdouble centerY, GLdouble centerZ,
-
- GLdouble upX, GLdouble upY, GLdouble upZ);
官網(wǎng)關(guān)于此函數(shù)的描述:
gluLookAt 通過指定一個視點、表面場景中心的參考點以及up向量來構(gòu)造一個視變換矩陣。
這個矩陣將代表場景中心的參考點映射到-Z軸,視點映射成為原點。當(dāng)使用一個特定的投影矩陣時,場景的中心就映射到視口的中心。類似地,由up向量描述的方向投影到投影平面成為+y軸,這樣它在視口中向上指向。up向量必須不能與從視點到參考點的直線平行。
那么如何確定u-v-n坐標(biāo)系呢?計算公式如下:

這里需要注意: OpenGL中使用的相機(jī)坐標(biāo)系是右手坐標(biāo)系,UVN坐標(biāo)系是左手坐標(biāo)系。在構(gòu)造實際變換矩陣的過程中,OpenGL
需要將-n軸翻轉(zhuǎn)為相機(jī)坐標(biāo)系的+z軸,uv軸定位相機(jī)坐標(biāo)系的+x和+y軸。這與推導(dǎo)相機(jī)變換矩陣一文最后的結(jié)果矩陣有所不同。
如何構(gòu)造視變換矩陣?
視變換就是在相機(jī)坐標(biāo)系下解釋世界坐標(biāo)系下的點。而這個變換矩陣的構(gòu)造,可以看做將相機(jī)坐標(biāo)系變換到與原來的世界坐標(biāo)系重合。而將世界坐標(biāo)系變換到與相機(jī)坐標(biāo)系重合,可以看做是這個所求變換的逆過程。
將世界坐標(biāo)系變換到與相機(jī)坐標(biāo)系重合,實際上進(jìn)行了兩個步驟: 第一步將世界坐標(biāo)系旋轉(zhuǎn)一定角度記作變換R,再將世界坐標(biāo)系平移到視點位置記作T,那么這個變換矩陣記為M=TR。要將世界坐標(biāo)系的點變換到照相機(jī)坐標(biāo)系下,需要使用矩陣M的逆矩陣,即: inverse(M)=inverse(R)*inverse(T)。即所求變換矩陣為inverse(M)。
平移矩陣的逆矩陣形式簡單,就是取平移量(eyex,eyey,eyez)的相反數(shù),即:

那么現(xiàn)在的關(guān)鍵是如何求出旋轉(zhuǎn)矩陣R?
上面我們構(gòu)造的UVN坐標(biāo)系u-v-n3個基向量可以構(gòu)造成矩陣:

注意這里對n軸進(jìn)行了翻轉(zhuǎn),構(gòu)成右手照相機(jī)坐標(biāo)系。
怎么看這個矩陣A呢,矩陣A實際上代表的就是一個旋轉(zhuǎn)矩陣(從矩陣形式上看出)。
旋轉(zhuǎn)矩陣的一個特點就是它是正交矩陣,即有inverse(A) = transpose(A).(A^-1 = A^T)
很多教材和博客都說,這里A矩陣可以看做是將世界坐標(biāo)系轉(zhuǎn)換到與照相機(jī)坐標(biāo)系重合時的旋轉(zhuǎn)矩陣,這一點怎么理解呢?
個人理解,矩陣A第四列為0,0,0,1,可以看做是世界坐標(biāo)系和照相機(jī)坐標(biāo)系原點重合;根據(jù)《OpenGL學(xué)習(xí)腳印: 理解坐標(biāo)系及坐標(biāo)變換(上) 》中所講,矩陣前3列即變換后的基向量,那么這個基向量(都是單位向量)是如何計算出來的呢?就是通過旋轉(zhuǎn)原來的世界坐標(biāo)系的基向量來構(gòu)造的。因此,可以說矩陣A代表的就是將世界坐標(biāo)系旋轉(zhuǎn)到與相機(jī)坐標(biāo)系重合時的旋轉(zhuǎn)矩陣R,即R
= A。
則inverse(R) = inverse(A) = transpose(A) 即為:

所以gluLookAt所求變換矩陣inverse(M)為:

gluLookAt的默認(rèn)值是(0, 0, 0, 0, 0,-1, 0, 1, 0);通過計算可得出:u=(1,0,0),v=(0,1,0),n=(0,0,-1),這樣構(gòu)成的矩陣M^-1即為單位矩陣。
下面通過代碼來驗證下結(jié)論。代碼繪制一個立方體,設(shè)置為透視投影,并通過gluLookAt設(shè)置相機(jī)方位來查看立方體。
注意,為了便于觀察視變換矩陣,這里并沒有進(jìn)行其他模型變換;手動計算矩陣時使用了數(shù)學(xué)庫glm來進(jìn)行向量點積和叉積運(yùn)算。
- //計算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時,使用該函數(shù)初始化GLEW
- userInit();//自定義的初始化函數(shù)
- glutReshapeFunc(reshape);
- glutDisplayFunc( display );
- glutKeyboardFunc( keyboardAction );
- glutMainLoop();
- return 0;
- }
- //自定義初始化函數(shù)
- void userInit()
- {
- glClearColor( 0.0, 0.0, 0.0, 0.0 );
- glColor4f(0.6f,0.5f,0.0,0.0);
- }
- //設(shè)置視變換矩陣
- 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);
- //構(gòu)造n軸
- glm::vec3 nvec(target-eye);
- nvec=glm::normalize(nvec);
- //構(gòu)造u軸
- vup = glm::normalize(vup);
- glm::vec3 uvec = glm::cross(nvec,vup);
- uvec=glm::normalize(uvec);
- //構(gòu)造v軸
- glm::vec3 vvec = glm::cross(uvec,nvec);
- vvec=glm::normalize(vvec);
- //設(shè)置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);
-
- //注意這行數(shù)據(jù)
- 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);
-
- //手動構(gòu)造視變換矩陣
- 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);
-
- //打印當(dāng)前模視變換矩陣內(nèi)容
- 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");
- }
- }
- //繪制回調(diào)函數(shù)
- void display( void )
- {
- glClear( GL_COLOR_BUFFER_BIT);//清除顏色緩存
- glLineWidth(2.0);
- glutWireCube(1.0);
- glutSwapBuffers();
- }
- //鍵盤按鍵回調(diào)函數(shù)
- 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
至此證明了上述推導(dǎo)的矩陣確實為OpenGL中使用的視變換矩陣。
關(guān)于OpenGL gluLookAt官網(wǎng)提供了參考實現(xiàn),可以查看:
GluLookAt code;
另外關(guān)于頂部正朝向vup的理解有更好的通俗解釋,可以查看:Opengl---gluLookAt函數(shù)詳解
。
|