這篇文章主要參考以下兩篇博客:
上面的第一篇是理論的講解,第二篇有實例代碼,我在后面會給出自己寫的主函數,依賴的類可以從第二篇參考中下載。
我這篇文主要談我個人對OpenGL中實現3D漫游的思路的理解。經過這些天的學習,主要是研究別人寫的代碼和網上的的博客,我初步理解了OpenGL中對於多方位觀察物體的實現策略。其實,對於3維坐標變換,每個人都可以有自己的理解方式,有的人喜歡研究一堆矩陣,有些人喜歡從空間幾何的角度去理解。
一 要實現3D漫游,第一步就要確定投影方向
OpenGL首先定義了一個世界坐標系(xyz),還有一個UVN坐標系,兩個都是右手系。觀察者總是沿着N軸負方向去觀察xyz,而物體也總是放在xyz中的。在OpenGL中,xyz其實是一個與物體保持相對位置不動的坐標系,而UVN坐標系其實是一個與觀察點位置保持相對不動的坐標系。那么怎樣才能全方位觀察物體呢?無外乎兩個思路:1,讓xyz繞着UVN轉;2,讓UVN繞着xyz轉。從OpenGL提供的接口函數及其實現的效果來看,其采用的是前者。
OpenGL中,屏幕中所呈現的視平面其實是UOV平面,屏幕上方為V軸正方向,屏幕右方為U軸正方向,屏幕由里到外為N軸正方向。因此視線方向總是沿N軸負方向。要想從某個特殊的角度去觀察xyz坐標系(物體),只需要確定一個變換矩陣,將xyz先繞着UVN旋轉,然后再沿着NO的方向平移,就能一覽無余了。其實又可以這樣理解,最初xyz與UVN其實是重合的,然后讓xyz先乘一個旋轉矩陣,再乘一個平移矩陣,這樣xyz就以一個特定的姿態出現在視野中了,准確的說是,物體在UOV平面產生了一個特定的投影。
下面是一些變換矩陣:
1.平移矩陣:
其逆矩陣從平移過程去思考的話,顯然是反向平移對應的矩陣,因此其逆矩陣只是在x,y,z前面加個負號。
2.繞x,y,z軸旋轉的矩陣:
三個旋轉矩陣Rx,Ry,Rz,它們的列項向量都是兩兩互相垂直,並且都是單位向量,所以Rx,Ry,Rz都是正交矩陣,它們的逆就是其自身的轉置。並且可以證明:有限個正交矩陣的乘積仍為正交矩陣。
3.繞空間某一向量(x, y, z)旋轉的矩陣。注意向量是有方向的,旋轉規則仍是右手螺旋定則:
二 投影方向確定了,下一步就是確定投影面的位置,並且還需要在投影面中選取一個區域,該區域就是最終顯示在屏幕上的圖像
下面這張圖我認為很清晰地展示了投影面的確定過程
上圖采用的是透視投影,OpenGL中還有一種平行投影,分別由函數
void gluPerspective (GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zFar )
void glOrtho( GLdouble left, GLdouble right,
GLdouble bottom, GLdouble top,
GLdouble near_val, GLdouble far_val )
確定投影的參數。gluPerspective參數中的fovy就是圖中的視角,以度為單位,zNear為近平面距離,zFar為遠平面距離。
選定投影區域后,會將該區域的坐標標准化到[-1, 1]之間,再通過函數void glViewport( GLint x, GLint y, GLsizei width, GLsizei height )
將標准化的坐標映射到圖像坐標,映射公式如下
圖中xnd,ynd是歸一化后的坐標,xw,yw是對應的圖像坐標,以像素為單位,當xnd=-1,ynd=-1時,映射的是圖像左下角的坐標,即以像素為單位時,圖像坐下角坐標為(0, 0)。下面是一段實現3D漫游的代碼,按x鍵讓茶壺繞U軸旋轉,按y鍵讓茶壺繞V軸旋轉,也可以用鼠標控制茶壺旋轉。代碼中依賴的矩陣類可從參考博客中獲得。

1 #include <GL/glut.h> 2 #include "Matrices.h" 3 #include "Vectors.h" 4 5 // GLUT CALLBACK functions 6 void displayCB(); 7 void reshapeCB(int w, int h); 8 void timerCB(int millisec); 9 void keyboardCB(unsigned char key, int x, int y); 10 void mouseCB(int button, int stat, int x, int y); 11 void mouseMotionCB(int x, int y); 12 void initGL(); 13 void initLights(); 14 void drawAxis(float size = 0.6f); 15 16 const int SCREEN_WIDTH = 600; 17 const int SCREEN_HEIGHT = 600; 18 const float CAMERA_DISTANCE = 1.5f; 19 const int TEXT_WIDTH = 8; 20 const int TEXT_HEIGHT = 13; 21 const float DEG2RAD = 3.141593f / 180; 22 23 // global variables 24 int screenWidth; 25 int screenHeight; 26 bool mouseLeftDown; 27 bool mouseRightDown; 28 float mouseX, mouseY; 29 float cameraAngleX; 30 float cameraAngleY; 31 float cameraDistance; 32 Matrix4 matrixView; 33 Matrix4 matrixModel; 34 Matrix4 matrixModelView; 35 Matrix4 matrixProjection; 36 37 int main(int argc, char **argv) 38 { 39 // init global vars 40 screenWidth = SCREEN_WIDTH; 41 screenHeight = SCREEN_HEIGHT; 42 mouseLeftDown = mouseRightDown = false; 43 mouseX = mouseY = 0; 44 cameraAngleX = cameraAngleY = 0; 45 cameraDistance = CAMERA_DISTANCE; 46 47 glutInit(&argc, argv); 48 glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH ); // display mode 49 glutInitWindowSize(screenWidth, screenHeight); // window size 50 glutInitWindowPosition(100, 100); // window location 51 glutCreateWindow(argv[0]); // param is the title of window 52 53 glEnable(GL_DEPTH_TEST); 54 glutReshapeFunc(reshapeCB); 55 glutDisplayFunc(displayCB); 56 glutTimerFunc(33, timerCB, 33); // redraw only every given millisec 57 58 glutKeyboardFunc(keyboardCB); 59 glutMouseFunc(mouseCB); 60 glutMotionFunc(mouseMotionCB); 61 62 glutMainLoop(); 63 64 return 0; 65 } 66 67 void drawAxis(float size) 68 { 69 glDepthFunc(GL_ALWAYS); // to avoid visual artifacts with grid lines 70 71 // draw axis 72 glLineWidth(3); 73 glBegin(GL_LINES); 74 glColor3f(1, 0, 0); 75 glVertex3f(0, 0, 0); 76 glVertex3f(size, 0, 0); 77 glColor3f(0, 1, 0); 78 glVertex3f(0, 0, 0); 79 glVertex3f(0, size, 0); 80 glColor3f(0, 0, 1); 81 glVertex3f(0, 0, 0); 82 glVertex3f(0, 0, size); 83 glEnd(); 84 glLineWidth(1); 85 86 // draw arrows(actually big square dots) 87 glPointSize(5); 88 glBegin(GL_POINTS); 89 glColor3f(1, 0, 0); 90 glVertex3f(size, 0, 0); 91 glColor3f(0, 1, 0); 92 glVertex3f(0, size, 0); 93 glColor3f(0, 0, 1); 94 glVertex3f(0, 0, size); 95 glEnd(); 96 glPointSize(1); 97 98 // restore default settings 99 glDepthFunc(GL_LEQUAL); 100 } 101 102 void displayCB() 103 { 104 // clear buffer 105 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 106 107 // transform camera 108 matrixView.translate(0, 0, cameraDistance); 109 matrixView.rotate(cameraAngleX, 1, 0, 0); //cameraAngleX 110 matrixView.rotate(cameraAngleY, 0, 1, 0); //cameraAngleY 111 matrixView.translate(0, 0, -cameraDistance); 112 cameraAngleX = 0; 113 cameraAngleY = 0; 114 glLoadMatrixf(matrixView.get()); 115 116 glColor3f(1.0f, 0.3f, 1.0f); 117 glutWireTeapot(0.6f); 118 drawAxis(); 119 120 glutSwapBuffers(); 121 } 122 123 void reshapeCB(int w, int h) 124 { 125 // set viewport to be the entire window 126 glViewport(0, 0, (GLsizei)w, (GLsizei)h); 127 128 glMatrixMode(GL_PROJECTION); 129 gluPerspective(80.0f, (float)(w)/h, 0.5f, 10.0f); // FOV, AspectRatio, NearClip, FarClip 130 131 // switch to modelview matrix in order to set scene 132 glMatrixMode(GL_MODELVIEW); 133 matrixView.identity(); 134 matrixView.translate(0, 0, -cameraDistance); 135 glLoadMatrixf(matrixView.get()); 136 } 137 138 void timerCB(int millisec) 139 { 140 glutTimerFunc(millisec, timerCB, millisec); 141 glutPostRedisplay(); 142 } 143 144 void keyboardCB(unsigned char key, int x, int y) 145 { 146 switch(key) 147 { 148 case 'x': 149 cameraAngleX = 3; 150 break; 151 case 'X': 152 cameraAngleX = -3; 153 break; 154 case 'y': 155 cameraAngleY = 3; 156 break; 157 case 'Y': 158 cameraAngleY = -3; 159 break; 160 case 27: // ESCAPE 161 exit(0); 162 break; 163 default: 164 break; 165 } 166 } 167 168 void mouseCB(int button, int state, int x, int y) 169 { 170 mouseX = x; 171 mouseY = y; 172 173 if(button == GLUT_LEFT_BUTTON) 174 { 175 if(state == GLUT_DOWN) 176 { 177 mouseLeftDown = true; 178 } 179 else if(state == GLUT_UP) 180 mouseLeftDown = false; 181 } 182 else if(button == GLUT_RIGHT_BUTTON) 183 { 184 if(state == GLUT_DOWN) 185 { 186 mouseRightDown = true; 187 } 188 else if(state == GLUT_UP) 189 mouseRightDown = false; 190 } 191 } 192 193 void mouseMotionCB(int x, int y) 194 { 195 if(mouseLeftDown) 196 { 197 cameraAngleY = (x - mouseX); 198 cameraAngleX = (y - mouseY); 199 mouseX = x; 200 mouseY = y; 201 } 202 if(mouseRightDown) 203 { 204 matrixView.translate(0, 0, cameraDistance); 205 cameraDistance += (y - mouseY) * 0.01f; 206 mouseY = y; 207 matrixView.translate(0, 0, -cameraDistance); 208 } 209 }
效果如下圖: