摄像机
OpenGL本身没有摄像机(Camera)的概念,但可以通过把场景中的所有物体往相反方向移动的方式来模拟出摄像机,产生一种在移动的感觉,而不是场景在移动。
当讨论摄像机(Camera)的时候,是在讨论以摄像机的视角作为场景原点时场景中所有的顶点坐标:观察矩阵把所有的世界坐标变换为相对于摄像机位置与方向的观察坐标。要定义一个摄像机,需要它在世界空间中的位置、观察的方向、一个指向它右侧的向量以及一个指向它上方的向量。
1. 摄像机位置
获取摄像机位置很简单。摄像机位置简单来说就是世界空间中一个指向摄像机位置的向量。
2. 摄像机方向
下一个需要的向量是摄像机的方向,这里指的是摄像机指向哪个方向。现在让摄像机指向场景原点:(0, 0, 0)。用场景原点向量减去摄像机位置向量的结果就是摄像机的指向向量。由于知道摄像机指向z轴负方向,但希望方向向量(Direction Vector)指向摄像机的z轴正方向。如果交换相减的顺序,就会获得一个指向摄像机正z轴方向的向量。
3. 右轴
需要的另一个向量是一个右向量(Right Vector),它代表摄像机空间的x轴的正方向。为获取右向量需要先使用一个小技巧:先定义一个上向量(Up Vector)。接下来把上向量和第二步得到的方向向量进行叉乘。两个向量叉乘的结果会同时垂直于两向量,因此会得到指向x轴正方向的那个向量(如果交换两个向量叉乘的顺序就会得到相反的指向x轴负方向的向量)。
4. 上轴
现在已经有了x轴向量和z轴向量,获取一个指向摄像机的正y轴向量就相对简单了:把右向量和方向向量进行叉乘。
//摄像机位置
QVector3D cameraPos=QVector3D(0.0f,0.0f,2.0f);
//摄像机方向
QVector3D cameraTarget = QVector3D(0.0f,0.0f,0.0f);
QVector3D cameraDirection =QVector3D(cameraPos-cameraTarget);
cameraDirection.normalize();
//右轴
QVector3D up=QVector3D(0.0f,1.0f,0.0f);
QVector3D cameraRight =QVector3D::crossProduct(up,cameraDirection);
cameraRight.normalize();
//上轴
QVector3D cameraUp=QVector3D::crossProduct(cameraDirection,cameraRight);
Look At
其中R是右向量,U是上向量,D是方向向量,P是摄像机位置向量。注意,位置向量是相反的,因为最终希望把世界平移到与自身移动的相反方向。把这个LookAt矩阵作为观察矩阵可以很高效地把所有世界坐标变换到刚刚定义的观察空间。LookAt矩阵就像它的名字表达的那样:它会创建一个看着(Look at)给定目标的观察矩阵。
QMatrix4x4 view;
view.lookAt(cameraPos,cameraPos+cameraFront,up);
LookAt函数需要一个位置、目标和上向量。它会创建一个和在上一节使用的一样的观察矩阵。
自由移动
让摄像机绕着场景转的确很有趣,但是让自己移动摄像机会更有趣!首先必须设置一个摄像机系统,所以在程序前面定义一些摄像机变量很有用。
首先将摄像机位置设置为之前定义的cameraPos。方向是当前的位置加上刚定义的方向向量。这样能保证无论怎么移动,摄像机都会注视着目标方向。让摆弄一下这些向量,在按下某些按钮时更新cameraPos向量。
void testopengl::keyPressEvent(QKeyEvent *event)
{
float cameraSpeed=2.5f*TIMEOUTMSEC/1000.0f;
switch (event->key()) {
case Qt::Key_W: cameraPos+=cameraSpeed*cameraFront;
qDebug() << "Up pressed, cameraPos:" << cameraPos;
break;
case Qt::Key_S: cameraPos-=cameraSpeed*cameraFront;break;
case Qt::Key_D: cameraPos+=cameraSpeed*cameraRight;break;
case Qt::Key_A: cameraPos-=cameraSpeed*cameraRight;break;
default:
break;
}
shaderProgram.bind();
update();
}
当按下WASD键的任意一个,摄像机的位置都会相应更新。如果希望向前或向后移动,就把位置向量加上或减去方向向量。如果希望向左右移动,使用叉乘来创建一个右向量(Right Vector),并沿着它相应移动就可以了。这样就创建了使用摄像机时熟悉的横移(Strafe)效果。
视角移动
为了能够改变视角,需要根据鼠标的输入改变cameraFront向量。
- 当鼠标移动时,是mouseMoveEvent被触发。
- 计算鼠标移动的位移量,并将其应用到yaw和pitch上。
- 根据新的yaw和pitch更新摄像机前方向量cameraFont。
- 调用update()重新绘制窗口内容,以应用新的视角。
当用户移动鼠标时,视角会随之改变,模拟第一人称视角的摄像机控制效果。
void testopengl::mouseMoveEvent(QMouseEvent *event)
{
//yaw和pitch分别表示摄像机的左右(水平)和上下(垂直)旋转角度。yaw初始化为-90.0f,使摄像机初始时朝向-Z轴
static float yaw=-90;
static float pitch=0;
//lastPos 保存上一次鼠标的位置,初始化为窗口的中心点
static QPoint lastPos(width()/2,height()/2);
auto currentPos=event->pos();
//deltaPos 表示当前鼠标位置与上一次鼠标位置的差值,即鼠标的位移量
deltaPos=currentPos-lastPos;
//更新lastPos为当前鼠标位置,以便下次计算鼠标位移时使用
lastPos=currentPos;
//sensitivity 控制鼠标移动对视角改变的灵敏度,值越大,鼠标移动对视角影响越大
float sensitivity=0.1f;
deltaPos *=sensitivity;
//yaw根据鼠标水平移动量(deltaPos.x())更新
//pitch根据鼠标垂直移动量(deltaPos.y())更新
yaw+=deltaPos.x();
pitch-=deltaPos.y();
//为了避免摄像机视角超过上下90度,限制pitch的范围在-89.0f到89.0f之间
if(pitch>89.0f)pitch =89.0f;
if(pitch<-89.0f)pitch =-89.0f;
cameraFront.setX(cos(yaw*PI/180)*cos(pitch*PI/180));
cameraFront.setY(sin(pitch*PI/180));
cameraFront.setZ(sin(yaw*PI/180)*cos(pitch*PI/180));
cameraFront.normalize();
update();
}
运行结果:
缩放
作为摄像机系统的一个附加内容,实现一个缩放(Zoom)接口。在之前的教程中说视野(Field of View)或fov定义了看到场景中多大的范围。当视野变小时,场景投影出来的空间就会减小,产生放大(Zoom In)了的感觉。会使用鼠标的滚轮来放大。与鼠标移动、键盘输入一样,需要一个处理鼠标滚轮事件的函数
void testopengl::wheelEvent(QWheelEvent *event)
{
if(fov >= 1.0f&&fov<=75.0f)
fov -= event->angleDelta().y()/120;//滚轮一步是120
if(fov<=1.0f) fov=1.0f;
if(fov>=75.0f) fov=75.0f;
}
运行结果: