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轴旋转和平移来实现,因此红宝书旧版上实现飞行员相机的代码为:
- void pilotView{GLdouble planex, GLdouble planey,
- GLdouble planez, GLdouble roll,
- GLdouble pitch, GLdouble heading)
- {
- glRotated(roll, 0.0, 0.0, 1.0);
- glRotated(pitch, 0.0, 1.0, 0.0);
- glRotated(heading, 1.0, 0.0, 0.0);
- glTranslated(-planex, -planey, -planez);
- }
注意,这里角度roll等没有取反,也是可以的,这只是一个习惯而已。我们计算视变换矩阵使用如下公式:
其中Rx、Ry、Rz分别表示绕x,y,z轴的旋转,T表示移动到相机到位置position的平移。
针对这一公式有两种代码实现。
实现一(参考自:Modern OpenGL 04 - Cameras, Vectors & Input):
- glm::mat4 FPCamera::getCameraOrientation()
- {
- glm::mat4 orientation;
- orientation = glm::rotate(orientation,
- -this->verticalAngle,glm::vec3(1.0,0.0,0.0));//pitch angle
- orientation = glm::rotate(orientation,
- -this->horizontalAngle,glm::vec3(0.0,1.0,0.0));//yaw angle
- return orientation;
- }
- glm::mat4 FPCamera::getViewMatrix()
- {
- return getCameraOrientation()*glm::translate(glm::mat4(),-this->position);
-
- }
实现二(参考自:Understanding the View Matrix):
- // Pitch should be in the range of [-90 ... 90] degrees and yaw
- // should be in the range of [0 ... 360] degrees.
- glm::mat4 FPCamera::getViewMatrix( glm::vec3 eye, float pitch, float yaw )
- {
- // If the pitch and yaw angles are in degrees,
- // they need to be converted to radians. Here
- // I assume the values are already converted to radians.
- float cosPitch = cos(this->degreeToRadians(pitch));
- float sinPitch = sin(this->degreeToRadians(pitch));
- float cosYaw = cos(this->degreeToRadians(yaw));
- float sinYaw = sin(this->degreeToRadians(yaw));
-
- glm::vec3 xaxis(cosYaw, 0, -sinYaw);
- glm::vec3 yaxis (sinYaw * sinPitch, cosPitch, cosYaw * sinPitch );
- glm::vec3 zaxis(sinYaw * cosPitch, -sinPitch, cosPitch * cosYaw);
- // Create a 4x4 view matrix from the right, up, forward and eye position vectors
- float matrix[16] = {
- xaxis.x, yaxis.x,zaxis.x,0 , //column 1
- xaxis.y, yaxis.y,zaxis.y,0 , //column 2
- xaxis.z, yaxis.z,zaxis.z,0 , //column 3
- -glm::dot( xaxis, eye ), -glm::dot( yaxis, eye ),
- -glm::dot( zaxis, eye ), 1 //column 4
- };
- return glm::make_mat4(matrix);
- }
这两种方式的区别在于,第一种利用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)。因此可以这样书写代码:
- glm::mat4 FPCamera::getCameraOrientation()
- {
- glm::mat4 orientation;
- orientation = glm::rotate(orientation,
- -this->verticalAngle,glm::vec3(1.0,0.0,0.0));//pitch angle
- orientation = glm::rotate(orientation,
- -this->horizontalAngle,glm::vec3(0.0,1.0,0.0));//yaw angle
- return orientation;
- }
- glm::vec3 FPCamera::getForwardDir()
- {
- glm::vec4 forward = glm::inverse(getCameraOrientation())*glm::vec4(0.0,0.0,-1,0.0);
- return glm::vec3(forward);
- }
- glm::vec3 FPCamera::getUpDir()
- {
- glm::vec4 up = glm::inverse(getCameraOrientation())*glm::vec4(0.0,1.0,0.0,0.0);
- return glm::vec3(up);
- }
- glm::vec3 FPCamera::getSideDir()
- {
- glm::vec4 side = glm::inverse(getCameraOrientation())*glm::vec4(1.0,0.0,0.0,0.0);
- return glm::vec3(side);
- }
也可以根据旋转矩阵,直接计算出方向向量,例如forward向量可以计算如下:
- glm::vec3 FPCamera::forward() const {
- glm::vec4 forward;
- forward.x = -sin(DegreesTORadians(yawAngle))*cos( DegreesTORadians(pitchAngle));
- forward.y = sin(DegreesTORadians(pitchAngle));
- forward.z = -cos(DegreesTORadians(pitchAngle))*cos(DegreesTORadians(yawAngle));
- return glm::vec3(forward);
- }
这里代码中提到的三角函数公式,为什么是这个样子? 可以参阅OpenGL Angles to Axes推导的旋转矩阵后自行演算一遍,以加深理解。唯一需要注意的是,不同作者的代码中旋转角度的正负号有所不同,这样计算过程可能稍有不同,注意区别。
同时在设计相机类时,我们也希望把投影矩阵包含进去,这个也很简单,利用glm::Perspective即可实现。
3.一个可运行的实例
很多教程已经开始使用GLFW来管理窗口,鉴于我之前一直使用的FreeGLUT来实现窗口管理(以后会采用GLFW),因此这里仍然给出FreeGLUT实现的部分代码供参考。
- //绘制回调函数
- void display( void )
- {
- glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
- glUseProgram(programId);
- glBindVertexArray(vaoId);
- //设置纹理
- glActiveTexture(GL_TEXTURE0);
- glBindTexture(GL_TEXTURE0,textureId);
- glUniform1i(samplerId, 0);
- //设置投影矩阵
- glm::mat4 projection = fpCamera.getProjectionMatrix();
- glUniformMatrix4fv(projectionMatrixId,1,GL_FALSE,glm::value_ptr(projection));
- //设置视变换矩阵
- glm::mat4 view = fpCamera.getViewMatrix();
- glUniformMatrix4fv(viewMatrixId,1,GL_FALSE,glm::value_ptr(view));
- //设置模型变换矩阵
- for(int i=0;i<ARRAY_COUNT(instanceOffset);i++)
- {
- glutil::MatrixStack modelMatrix;
- modelMatrix.Translate(instanceOffset[i].x,instanceOffset[i].y,0.0);
- modelMatrix.Rotate(glm::vec3(0.0,1.0,0.0),angle*(i+1));
- modelMatrix.Scale(0.3);
- glUniformMatrix4fv(modelMatrixId,1,GL_FALSE,glm::value_ptr(modelMatrix.Top()));
- //绘制立方体
- glDrawArrays(GL_TRIANGLES,0,36);
- }
- glUseProgram(0);
- glBindVertexArray(0);
- glutSwapBuffers();
- }
- //调整窗口大小回调函数
- void reshape(int w,int h)
- {
- glViewport(0,0,(GLsizei)w,(GLsizei)h);
- fpCamera.setAspectRatio((GLfloat)w/(GLfloat)h);
- }
- //键盘按键回调函数
- void keyboardAction( unsigned char key, int x, int y )
- {
- float deltaTime = 1;
- switch( key )
- {
- case 033: // Escape key
- exit( EXIT_SUCCESS );
- break;
- //前后移动
- case 'w':
- fpCamera.moveForward(moveSpeed*deltaTime);
- glutPostRedisplay();
- break;
- case 's':
- fpCamera.moveBackward(moveSpeed*deltaTime);
- glutPostRedisplay();
- break;
- //左右移动
- case 'a':
- fpCamera.strafeLeft(moveSpeed*deltaTime);
- glutPostRedisplay();
- break;
- case 'd':
- fpCamera.strateRight(moveSpeed*deltaTime);
- glutPostRedisplay();
- break;
- //上下移动
- case 'z':
- fpCamera.offsetCameraPosition(moveSpeed*-glm::vec3(0,1,0));
- glutPostRedisplay();
- break;
- case 'x':
- fpCamera.offsetCameraPosition(moveSpeed*glm::vec3(0,1,0));
- glutPostRedisplay();
- break;
- case 0x20:
- spinPause = ! spinPause;//空格键暂停立方体旋转
- glutPostRedisplay();
- break;
- }
- }
- //鼠标回调函数
- void mouseMoveAction(int button, int state, int x, int y)
- {
- if (button == GLUT_LEFT_BUTTON) {
-
- // 鼠标移动后松开时,更新窗口
- if (state == GLUT_UP) {
- int widowWitdh = glutGet(GLUT_WINDOW_WIDTH);
- int widowHeight = glutGet(GLUT_WINDOW_HEIGHT);
- float horizMovement = (x - mousePos.x) * mouseSensitivity;
- float vertMovement = (y - mousePos.y) * mouseSensitivity;
- //更新相机角度
- fpCamera.offsetCameraAngles(horizMovement,vertMovement);
- glutPostRedisplay();
- mousePos = glm::vec2(-1,-1);
- }
- else {// state = GLUT_DOWN
- mousePos.x = x;
- mousePos.y = y; //刚按下鼠标左键时,记录鼠标位置
- }
- }
- }
- //鼠标滚轮回调函数
- void mouseWheelAction( int wheel, int direction, int x, int y )
- {
- float fov = fpCamera.getFovAngle();
- if (direction > 0)
- {
- // 放大
- fov += mouseWheelSensitivity;
- if(fov > 130.0f) fov = 130.0f;
- fpCamera.setFovAngle(fov);
- }
- else
- {
- //缩小
- fov -= mouseWheelSensitivity;
- if(fov < 5.0f) fov = 5.0f;
- fpCamera.setFovAngle(fov);
- }
- glutPostRedisplay();
- }
实例效果:
|