分享

OpenGL学习脚印: 虚拟相机控制2(camera control)

 方海龙的书馆 2014-10-28

  OpenGL学习脚印: 虚拟相机控制2(camera control)

                                 ------第一人称相机(First Person Camera)

写在前面

                上一节对投影中裁剪平面和投影平面宽高比有了一些认识,本节从视变换的角度来构造适合于人机交互的虚拟相机系统,这里以构造一个第一人称相机(First person camera)为例来帮助理解。第一人称相机网上已经有很多可利用代码,文中给出了一些参考链接,因此这里不再列出全部参看代码。本节的主旨在于着重理解相机控制的原理和知道如何用代码实现。

         

1.构造虚拟相机的原理

             首先了解OpenGL中控制虚拟相机的一般方法。OpenGL旧版中一直使用:

     void gluLookAt(GLdouble eyeX,  GLdouble eyeY,  GLdouble eyeZ,  GLdouble centerX,  GLdouble centerY,  GLdouble centerZ,  GLdouble upX,  GLdouble upY,  GLdouble upZ);

     这个函数来控制虚拟相机。这里的三个参数分别为观察者眼睛位置,也称为视点eyePos;场景的观察参考点,也称为目标点targetPos;以及用户指定的相机朝上向量,一般情况下朝上向量设置为(0.0,1.0,0.0)。这里还是将虚拟相机的原理图给出:

    

      通过这个函数我们要完成什么样的目的呢?

      在《OpenGL学习脚印: 关于gluLookAt函数的理解》一节推导了整个过程后明白

       gluLookAt通过指定观察者和观察参考点来构造观察正向forward,同时通过指定朝上向量vup,来构造相机坐标系的up方向,通过forward和up叉积来构造第三个方向,我们称之为side方向。这样eyePos和forward(翻转该方向)、up方向、side方向构成一个新的坐标系,这就是虚拟相机坐标系。而求取视变换矩阵view,可以看做两步,第一步将世界坐标系旋转和平移到与虚拟相机坐标系重合,这个矩阵称为M=T*R;第二步求M矩阵的逆矩阵,该矩阵即为所求的视变换矩阵,也就是: view = inverse(T*R) = inverse(R)*inverse(T)。

              这个函数已经能完成基本任务了,包括指定视点位置和场景观察点位置,以及观察的朝上向量。我们可以在程序中动态修改这些参数来完成相机的控制。

                 但是这样仍然不能很好的用于人机交互,通常使用的第一人称相机,通过键盘WASD等键和鼠标来控制虚拟相机更加方便,因此需要改进我们的相机控制。

2.第一人称相机的交互目标 

     要想构造适合人机交互的相机类,必须明确我们需要实现的目标。

     第一人称相机的目标包括:键盘来移动相机,是相机前后左右移动,通过鼠标来控制相机绕xy轴转动角度,通过鼠标滚轮来实现缩放。这些运动从相机来看如下图所示(整理自A Camera Class for OpenGL John McGuiness课件):

    

           这里我们实现第一人称相机,滚转角roll则暂时不需要(飞行器相机需要)。与gluLookAt不同,我们这次构造的相机坐标系,默认情况下,即是forward(0.0,0.0,-1.0),side(1.0,0.0,0.0),和up(0.0,1.0,0.0),一开始可以看做与世界坐标系重合(当然需要翻转forward方向)。通过绕xy轴旋转和平移来实现,因此红宝书旧版上实现飞行员相机的代码为:

      

  1. void pilotView{GLdouble planex, GLdouble planey,  
  2.                GLdouble planez, GLdouble roll,  
  3.                GLdouble pitch, GLdouble heading)  
  4. {  
  5.       glRotated(roll, 0.0, 0.0, 1.0);  
  6.       glRotated(pitch, 0.0, 1.0, 0.0);  
  7.       glRotated(heading, 1.0, 0.0, 0.0);  
  8.       glTranslated(-planex, -planey, -planez);  
  9. }  
注意,这里角度roll等没有取反,也是可以的,这只是一个习惯而已。我们计算视变换矩阵使用如下公式:


其中Rx、Ry、Rz分别表示绕x,y,z轴的旋转,T表示移动到相机到位置position的平移。

针对这一公式有两种代码实现。

实现一(参考自:Modern OpenGL 04 - Cameras, Vectors & Input):

  1. glm::mat4 FPCamera::getCameraOrientation()  
  2. {  
  3.     glm::mat4 orientation;  
  4.     orientation = glm::rotate(orientation,  
  5.         -this->verticalAngle,glm::vec3(1.0,0.0,0.0));//pitch angle  
  6.     orientation = glm::rotate(orientation,  
  7.         -this->horizontalAngle,glm::vec3(0.0,1.0,0.0));//yaw angle  
  8.     return orientation;  
  9. }  
  10. glm::mat4 FPCamera::getViewMatrix()  
  11. {  
  12.      return getCameraOrientation()*glm::translate(glm::mat4(),-this->position);  
  13.       
  14. }  

实现二(参考自:Understanding the View Matrix):

  1. // Pitch should be in the range of [-90 ... 90] degrees and yaw  
  2. // should be in the range of [0 ... 360] degrees.  
  3. glm::mat4 FPCamera::getViewMatrix( glm::vec3 eye, float pitch, float yaw )  
  4. {  
  5.     // If the pitch and yaw angles are in degrees,  
  6.     // they need to be converted to radians. Here  
  7.     // I assume the values are already converted to radians.  
  8.     float cosPitch = cos(this->degreeToRadians(pitch));  
  9.     float sinPitch = sin(this->degreeToRadians(pitch));  
  10.     float cosYaw = cos(this->degreeToRadians(yaw));  
  11.     float sinYaw = sin(this->degreeToRadians(yaw));  
  12.    
  13.     glm::vec3 xaxis(cosYaw, 0, -sinYaw);  
  14.     glm::vec3 yaxis (sinYaw * sinPitch, cosPitch, cosYaw * sinPitch );  
  15.     glm::vec3 zaxis(sinYaw * cosPitch, -sinPitch, cosPitch * cosYaw);  
  16.     // Create a 4x4 view matrix from the right, up, forward and eye position vectors  
  17.     float matrix[16] = {  
  18.         xaxis.x, yaxis.x,zaxis.x,0 , //column 1  
  19.         xaxis.y, yaxis.y,zaxis.y,0 , //column 2  
  20.         xaxis.z, yaxis.z,zaxis.z,0 , //column 3  
  21.         -glm::dot( xaxis, eye ), -glm::dot( yaxis, eye ),  
  22.         -glm::dot( zaxis, eye ), 1  //column 4  
  23.     };  
  24.     return glm::make_mat4(matrix);  
  25. }  

这两种方式的区别在于,第一种利用rotate和translate组合构造,第二种方式直接计算出其结果,两者构造的矩阵是等价的。

这里还有一个问题,在移动相机时,一种方式是计算移动距离在每个方向的分量,然后修改相机位置,参看A C++ Camera Class for Simple OpenGL FPS Controls另一种方式利用相机的三个方向向量,利用位移向量来计算相机位置,参看Modern OpenGL 04 - Cameras, Vectors & Input

使用第二种方式时,需要计算forward,up,side三个方向向量,这三个向量的求取的方法即是:利用在旋转定位相机时的旋转矩阵来乘以原始的三个方向向量。原始三个向量方向forward(0.0,0.0,-1.0),side(1.0,0.0,0.0),和up(0.0,1.0,0.0)。因此可以这样书写代码:

  1. glm::mat4 FPCamera::getCameraOrientation()  
  2. {  
  3.     glm::mat4 orientation;  
  4.     orientation = glm::rotate(orientation,  
  5.         -this->verticalAngle,glm::vec3(1.0,0.0,0.0));//pitch angle  
  6.     orientation = glm::rotate(orientation,  
  7.         -this->horizontalAngle,glm::vec3(0.0,1.0,0.0));//yaw angle  
  8.     return orientation;  
  9. }  
  10. glm::vec3 FPCamera::getForwardDir()  
  11. {     
  12.     glm::vec4 forward = glm::inverse(getCameraOrientation())*glm::vec4(0.0,0.0,-1,0.0);  
  13.     return glm::vec3(forward);  
  14. }  
  15. glm::vec3 FPCamera::getUpDir()  
  16. {  
  17.     glm::vec4 up = glm::inverse(getCameraOrientation())*glm::vec4(0.0,1.0,0.0,0.0);  
  18.     return glm::vec3(up);  
  19. }  
  20. glm::vec3 FPCamera::getSideDir()  
  21. {  
  22.     glm::vec4 side = glm::inverse(getCameraOrientation())*glm::vec4(1.0,0.0,0.0,0.0);  
  23.     return glm::vec3(side);  
  24. }  

也可以根据旋转矩阵,直接计算出方向向量,例如forward向量可以计算如下:

  1. glm::vec3 FPCamera::forward() const {  
  2.     glm::vec4 forward;  
  3.     forward.x = -sin(DegreesTORadians(yawAngle))*cos( DegreesTORadians(pitchAngle));  
  4.     forward.y =  sin(DegreesTORadians(pitchAngle));  
  5.     forward.z = -cos(DegreesTORadians(pitchAngle))*cos(DegreesTORadians(yawAngle));  
  6.     return glm::vec3(forward);  
  7. }  


      这里代码中提到的三角函数公式,为什么是这个样子? 可以参阅OpenGL Angles to Axes推导的旋转矩阵后自行演算一遍,以加深理解。唯一需要注意的是,不同作者的代码中旋转角度的正负号有所不同,这样计算过程可能稍有不同,注意区别。

        同时在设计相机类时,我们也希望把投影矩阵包含进去,这个也很简单,利用glm::Perspective即可实现。

3.一个可运行的实例

           很多教程已经开始使用GLFW来管理窗口,鉴于我之前一直使用的FreeGLUT来实现窗口管理(以后会采用GLFW),因此这里仍然给出FreeGLUT实现的部分代码供参考。

            

  1. //绘制回调函数  
  2. void display( void )  
  3. {     
  4.     glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);  
  5.     glUseProgram(programId);  
  6.     glBindVertexArray(vaoId);  
  7.     //设置纹理  
  8.     glActiveTexture(GL_TEXTURE0);  
  9.     glBindTexture(GL_TEXTURE0,textureId);  
  10.     glUniform1i(samplerId, 0);  
  11.     //设置投影矩阵  
  12.     glm::mat4  projection = fpCamera.getProjectionMatrix();  
  13.     glUniformMatrix4fv(projectionMatrixId,1,GL_FALSE,glm::value_ptr(projection));  
  14.     //设置视变换矩阵  
  15.     glm::mat4 view = fpCamera.getViewMatrix();  
  16.     glUniformMatrix4fv(viewMatrixId,1,GL_FALSE,glm::value_ptr(view));  
  17.     //设置模型变换矩阵  
  18.     for(int i=0;i<ARRAY_COUNT(instanceOffset);i++)  
  19.     {  
  20.        glutil::MatrixStack modelMatrix;  
  21.        modelMatrix.Translate(instanceOffset[i].x,instanceOffset[i].y,0.0);  
  22.        modelMatrix.Rotate(glm::vec3(0.0,1.0,0.0),angle*(i+1));  
  23.        modelMatrix.Scale(0.3);  
  24.        glUniformMatrix4fv(modelMatrixId,1,GL_FALSE,glm::value_ptr(modelMatrix.Top()));  
  25.        //绘制立方体  
  26.        glDrawArrays(GL_TRIANGLES,0,36);  
  27.     }  
  28.     glUseProgram(0);  
  29.     glBindVertexArray(0);  
  30.     glutSwapBuffers();  
  31. }  
  32. //调整窗口大小回调函数  
  33. void reshape(int w,int h)  
  34. {  
  35.     glViewport(0,0,(GLsizei)w,(GLsizei)h);  
  36.     fpCamera.setAspectRatio((GLfloat)w/(GLfloat)h);  
  37. }  
  38. //键盘按键回调函数  
  39. void keyboardAction( unsigned char key, int x, int y )  
  40. {     
  41.     float deltaTime = 1;  
  42.     switch( key )  
  43.     {  
  44.         case 033:  // Escape key  
  45.             exit( EXIT_SUCCESS );  
  46.             break;  
  47.         //前后移动  
  48.         case 'w':  
  49.             fpCamera.moveForward(moveSpeed*deltaTime);  
  50.             glutPostRedisplay();  
  51.             break;  
  52.         case 's':  
  53.             fpCamera.moveBackward(moveSpeed*deltaTime);  
  54.             glutPostRedisplay();  
  55.             break;  
  56.         //左右移动  
  57.         case 'a':  
  58.             fpCamera.strafeLeft(moveSpeed*deltaTime);  
  59.             glutPostRedisplay();  
  60.             break;  
  61.         case 'd':  
  62.             fpCamera.strateRight(moveSpeed*deltaTime);  
  63.             glutPostRedisplay();  
  64.             break;  
  65.         //上下移动  
  66.         case 'z':  
  67.             fpCamera.offsetCameraPosition(moveSpeed*-glm::vec3(0,1,0));  
  68.             glutPostRedisplay();  
  69.             break;  
  70.         case 'x':  
  71.             fpCamera.offsetCameraPosition(moveSpeed*glm::vec3(0,1,0));  
  72.             glutPostRedisplay();  
  73.             break;  
  74.         case 0x20:  
  75.             spinPause = ! spinPause;//空格键暂停立方体旋转  
  76.             glutPostRedisplay();  
  77.             break;  
  78.     }  
  79. }  
  80. //鼠标回调函数  
  81. void mouseMoveAction(int button, int state, int x, int y)  
  82. {     
  83.     if (button == GLUT_LEFT_BUTTON) {  
  84.   
  85.         // 鼠标移动后松开时,更新窗口  
  86.         if (state == GLUT_UP) {  
  87.              int widowWitdh = glutGet(GLUT_WINDOW_WIDTH);  
  88.              int widowHeight = glutGet(GLUT_WINDOW_HEIGHT);  
  89.              float horizMovement = (x - mousePos.x) * mouseSensitivity;  
  90.              float vertMovement  = (y - mousePos.y) * mouseSensitivity;  
  91.              //更新相机角度  
  92.              fpCamera.offsetCameraAngles(horizMovement,vertMovement);  
  93.              glutPostRedisplay();  
  94.              mousePos = glm::vec2(-1,-1);  
  95.         }  
  96.         else  {// state = GLUT_DOWN  
  97.             mousePos.x = x;  
  98.             mousePos.y = y; //刚按下鼠标左键时,记录鼠标位置  
  99.         }  
  100.     }  
  101. }  
  102. //鼠标滚轮回调函数  
  103. void mouseWheelAction( int wheel, int direction, int x, int y )  
  104. {     
  105.     float fov = fpCamera.getFovAngle();  
  106.     if (direction > 0)  
  107.     {  
  108.         // 放大  
  109.         fov += mouseWheelSensitivity;  
  110.         if(fov > 130.0f) fov = 130.0f;  
  111.         fpCamera.setFovAngle(fov);  
  112.     }  
  113.     else  
  114.     {  
  115.         //缩小  
  116.         fov -= mouseWheelSensitivity;  
  117.         if(fov < 5.0f) fov = 5.0f;  
  118.         fpCamera.setFovAngle(fov);  
  119.     }  
  120.     glutPostRedisplay();  
  121. }  

实例效果:

  


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多