3D Computer Grapihcs Using OpenGL - 17 添加相机(旋转)
在11节我们说过,MVP矩阵中目前只应用了两个矩阵,World to View 矩阵被省略了,这就导致我们的画面没有办法转换视角。
本节我们将添加这一环节,让相机可以旋转。
为了实现这一目的,我们添加一个相机类, Camera类。
Camera.h:
1 #pragma once 2 #include <glm\glm.hpp> 3 4 class Camera 5 { 6 private: 7 glm::vec3 position; 8 glm::vec3 viewDirection; 9 const glm::vec3 UP; 10 glm::vec2 oldMousePosition; 11 12 public: 13 Camera(); 14 glm::mat4 getWorldToViewMatrix() const; 15 void mouseUpdate(const glm::vec2& newMousePosition); 16 };
Camera.cpp:
1 #include "Camera.h" 2 #include "glm\gtx\transform.hpp" 3 4 Camera::Camera(): 5 viewDirection(0.0f,0.0f,-1.0f), 6 UP(0.0f,1.0f,0.0f) 7 { 8 9 } 10 11 glm::mat4 Camera::getWorldToViewMatrix() const 12 { 13 return glm::lookAt(position, position + viewDirection, UP); 14 } 15 16 void Camera::mouseUpdate(const glm::vec2 & newMousePosition) 17 { 18 glm::vec2 mouseDelta = newMousePosition - oldMousePosition; 19 if (glm::length(mouseDelta) > 10.0f) 20 { 21 oldMousePosition = newMousePosition; 22 return; 23 } 24 25 26 viewDirection = glm::mat3(glm::rotate(mouseDelta.x * 0.01f, UP)) * viewDirection; 27 28 oldMousePosition = newMousePosition; 29 }
glm::lookAt
构建Camera类的最终目的是提供一个 World to view 转换矩阵,这个矩阵可以使用一个函数 glm::lookAt 来构造。
glm::lookAt需要三个参数:
- 相机在世界中的位置坐标
- 相机的观察目标
- 相机的“上”方向
Camera类
Camera类中定义了这些成员:
- position- 表示相机的位置
- viewDirection - 表示相机的视线方向
- UP - 一个常量,用于表示世界的上方
- oldMousePosition - 表示上一次鼠标的位置
- getWorldToViewMatrix()函数 - 用于返回World to View转换矩阵
- mouseUpdate() 函数,计算两次更新之间鼠标的位置变化,并根据此变化更新world to view矩阵
其中前三个成员正好可以提供给getWorldToViewmatrix用于返回world to view矩阵,唯一需要做点计算的是第二个参数,lookAt函数需要的是一个目标,而目标位置可以使用相机位置加上视线方向"模拟"出来,实际上只要朝向是我们需要的,我们并不用关心真正的“目标”是什么。
为什么不直接提供一个“目标位置”的成员呢?
原因是我们需要“旋转”相机,而旋转操作的结果是“方向”向量。这点在Camera.cpp的26行体现出来了。如果我们直接提供的是“目标位置”,这里的计算就无法进行了。
第26行之所以给mouseDelta.x乘以0.01,是对旋转的速度进行了细节的调整。
19-23行是为了避免鼠标离开屏幕后,再次进入时产生的跳跃。
MyGlWindow类的修改
MyGlWindow.h
- 引入了Camera.h
- 新增一个Camera类型的成员camera
- override了一个 mouseMoveEvent函数,这个函数是QWidget类的虚函数,在鼠标按下以后会持续调用
- 把transformMatrixBufferID提取到类成员中。
最终代码:
1 #pragma once 2 #include <QtOpenGL\qgl.h> 3 #include <string> 4 #include "Camera.h" 5 6 class MyGlWindow :public QGLWidget 7 { 8 protected: 9 void sendDataToOpenGL(); 10 void installShaders(); 11 void initializeGL(); 12 void paintGL(); 13 GLuint transformMatrixBufferID; 14 Camera camera; 15 std::string ReadShaderCode(const char* fileName); 16 void mouseMoveEvent(QMouseEvent*); 17 };
MyGlWindow.cpp
- 头文件包含 <Qt3DInput\qmouseevent.h>
- 修改sendDataToOpenGL()函数
- 修改paintGL()函数
- 重新实现mouseMoveEvent()函数
修改后的代码:
1 #include <gl\glew.h> 2 #include "MyGlWindow.h" 3 #include <iostream> 4 #include <fstream> 5 #include <glm\gtc\matrix_transform.hpp> 6 #include <glm\gtx\transform.hpp> 7 #include <ShapeGenerator.h> 8 #include <Qt3DInput\qmouseevent.h> 9 10 11 GLuint programID; 12 GLuint numIndices; 13 14 void MyGlWindow::sendDataToOpenGL() 15 { 16 17 ShapeData shape = ShapeGenerator::makeCube(); 18 19 GLuint vertexBufferID; 20 glGenBuffers(1, &vertexBufferID); 21 glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID); 22 glBufferData(GL_ARRAY_BUFFER, shape.vertexBufferSize(), shape.vertices, GL_STATIC_DRAW); 23 24 glEnableVertexAttribArray(0); 25 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 6, 0); 26 27 28 GLuint indexBufferID; 29 glGenBuffers(1, &indexBufferID); 30 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferID); 31 glBufferData(GL_ELEMENT_ARRAY_BUFFER, shape.indexBufferSize(), shape.indices, GL_STATIC_DRAW); 32 33 glEnableVertexAttribArray(1); 34 glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 6, (char*)(sizeof(GLfloat) * 3)); 35 36 numIndices = shape.numIndices; 37 shape.cleanUp(); 38 39 //instancing 40 41 glGenBuffers(1, &transformMatrixBufferID); 42 glBindBuffer(GL_ARRAY_BUFFER, transformMatrixBufferID); 43 44 glBufferData(GL_ARRAY_BUFFER, sizeof(glm::mat4) * 2, 0, GL_DYNAMIC_DRAW); 45 glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)(sizeof(float) * 0)); 46 glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)(sizeof(float) * 4)); 47 glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)(sizeof(float) * 8)); 48 glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)(sizeof(float) * 12)); 49 glEnableVertexAttribArray(2); 50 glEnableVertexAttribArray(3); 51 glEnableVertexAttribArray(4); 52 glEnableVertexAttribArray(5); 53 glVertexAttribDivisor(2, 1); 54 glVertexAttribDivisor(3, 1); 55 glVertexAttribDivisor(4, 1); 56 glVertexAttribDivisor(5, 1); 57 } 58 59 void MyGlWindow::installShaders() 60 { 61 //... 62 } 63 64 void MyGlWindow::initializeGL() 65 { 66 //... 67 } 68 69 void MyGlWindow::paintGL() 70 { 71 72 glm::mat4 projectionMatrix = glm::perspective(30.0f, ((float)width()) / height(), 0.1f, 10.0f); 73 74 glm::mat4 fullTransforms[] = 75 { 76 projectionMatrix * camera.getWorldToViewMatrix() * glm::translate(glm::vec3(0.0f, 0.0f, -3.0f)) * glm::rotate(54.0f,glm::vec3(1.0f, 0.0f, 0.0f)), 77 projectionMatrix * camera.getWorldToViewMatrix() * glm::translate(glm::vec3(2.0f, 0.0f, -4.0f)) * glm::rotate(126.0f, glm::vec3(0.0f, 1.0f, 0.0f)) 78 }; 79 80 glBufferData(GL_ARRAY_BUFFER, sizeof(fullTransforms), fullTransforms, GL_DYNAMIC_DRAW); 81 82 glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); 83 glViewport(0, 0, width(), height()); 84 glDrawElementsInstanced(GL_TRIANGLES, numIndices, GL_UNSIGNED_SHORT, 0, 2); 85 } 86 87 88 std::string MyGlWindow::ReadShaderCode(const char* fileName) 89 { 90 //... 91 } 92 93 void MyGlWindow::mouseMoveEvent(QMouseEvent * e) 94 { 95 camera.mouseUpdate(glm::vec2(e->x(), e->y())); 96 repaint(); 97 }
72-80行是从sendDataToOpenGL中复制过来的,因为现在需要持续性修改和更新,所以要移动到paintGL函数中。
我们能看到在fullTransforms数组中,两个元素中都在中间增加了camera.getWorldToViewMatrix()。
80行和44行内容基本一样,除了44行没有提供任何数据,而80行提供了实际的数据。这里和之前将的glBufferSubData()的用法是一样的,之所以没有用glBufferSubData,原因是它的作用是改变buffer中的一部分数据,而在这里我们全部都要改变,所以就使用glBufferData是很合适的。
另外44行和80行的函数最后一个参数都改成了GL_DYANMIC_DRAW,原因是绘制的内容要频繁更新。
93行开始实现了mouseMoveEvent函数,该函数是声明在QWidget中的一个虚函数,只要鼠标按下并移动,就会触发这个函数,鼠标事件以参数的形式传入函数。在这里我们首先更新world to view矩阵,然后调用repaint()函数重新绘制画面。
最终的结果就是在画布上按下鼠标并左右移动的时候,我们的视角也左右旋转了。
加入上下旋转
根据左右旋转的方法,可以很方便的加入上下旋转,上下旋转和左右旋转的唯一区别是旋转轴不一样:左右旋转是绕“上”方向轴旋转,这个“上”很容易提供,就是世界坐标的上。而上下旋转是绕相机局部坐标的x轴旋转,这个坐标轴我们无法使用世界坐标,因为它不是固定的,随着相机在其他轴向上的旋转,这个轴会发生变化。
这里我们可以使用一个数学方法来计算这个轴,我们的“上”方向是固定的,而“前”方向也是计算出来了的(就是viewDirection),这样我们可以使用“向量的叉乘”来得到一个垂直于“上和前方向构成的平面”的向量,也就是“朝向相机左或者右方向的向量”,也就是相机的局部坐标的x方向或者-x方向。
叉乘可以使用glm::cross()函数来实现。因此,上下旋转的矩阵可以这样构建:
glm::vec3 pitchAxis = glm::cross(viewDirection, UP); glm::vec3 pitchMatrix = glm::rotate(mouseDelta.y * 0.01f, pitchAxis)
第一行定义的是旋转轴,第二行就是绕这个旋转轴进行的变化,注意这里用的是mouseDelta的y分量。
我们看一下最终修改的代码:
Camera.cpp的 mouseUpdate函数:
1 void Camera::mouseUpdate(const glm::vec2 & newMousePosition) 2 { 3 glm::vec2 mouseDelta = newMousePosition - oldMousePosition; 4 if (glm::length(mouseDelta) > 50.0f) 5 { 6 oldMousePosition = newMousePosition; 7 return; 8 } 9 10 glm::vec3 pitchAxis = glm::cross(viewDirection, UP); 11 12 viewDirection = glm::mat3( 13 glm::rotate(mouseDelta.x * 0.01f, UP) * 14 glm::rotate(mouseDelta.y * 0.01f, pitchAxis) 15 ) * viewDirection; 16 17 18 oldMousePosition = newMousePosition; 19 }
注意我们把绕两个轴向旋转的变换结合到一个表达式里了。
最终效果就是相机可以在上下左右方向自由移动了。
潜在编译错误:
如果编译时出现"cannot open source file "QObject" 等错误提示,需要定位到#include <QObject>等相关语句,在路径前增加QtCore路径:
例如:#include <QtCore/QObject>
这段代码出现在qt的头文件中qmouseevent.h和qkeyevent.h中。尚不清楚这是源码中的错误还是我的设置问题。
后面章节出现类似问题的话解决方法一样。