OpenGL程序员计算机杂谈

从0开始的OpenGL学习(九)-FPS摄像机

2017-10-21  本文已影响853人  闪电的蓝熊猫

本文主要解决一个问题:

如何创建一个FPS摄像机?

引言

在前一章中,我们讨论了观察矩阵以及如何使用变换矩阵移动场景(虽然仅仅是往后移了一点点)。本章中,我们要创建一个类似FPS的摄像机,它可以移动,可以转头,可以变焦(狙击枪里开放大镜效果)。

在这章中,你会看到

观察(摄像机)空间

就像前一章说的那样,观察空间其实是以摄像机为原点,以摄像机观察的方向为-z轴方向的坐标系统。而观察矩阵的作用,就是将场景中的物体从世界坐标转换到观察坐标。要定义一个摄像机系统,我们需要它在世界空间中的位置,它的朝向,以及一个向上方向的向量。

观察坐标系统原理
1、相机位置

相机位置就是一个简单的向量,表示其在世界空间中的位置。我们把它设置成和前一章一样的位置。

glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 4.0f);

别忘了OpenGL是右手坐标系,摄像机是往-z轴方向看的

2、光线方向

作为朝向的反方向,我称它为光线方向(物体反射光摄入观察者眼睛的方向)。计算的方式很简单,将相机位置向量和观察目标点向量做减法就可以了。我们使用世界坐标原点(默认点)作为我们的观察目标点。

glm::vec3 cameraTarget  glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);
3、Right轴

我们下一个需要的向量是Right向量,它表示坐标系统中的x轴正方向。要计算这个Right向量,我们要用到之前学的一点小技巧:向量叉乘。Right向量必须要垂直于光线方向,因此,它必须要和光线方向与世界坐标系统的y轴组成的平面垂直。这就帮了我们的大忙,根据叉乘规则,我们只需要将y轴的单位向量与光线方向向量做叉乘就可以了。

glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 cameraRight  = glm::normalize(glm::cross(up, cameraDirection));
4、Up轴

现在,我们有了x轴和z轴,y轴已经呼之欲出了。没错,只需要用z轴向量叉乘x轴向量就可以了!

glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);

叉乘真是个好东西!

好,坐标系统的三个轴都有了,马上开始生成观察矩阵。

观察矩阵

用矩阵的最大好处就是当你有了坐标空间的3个轴之后,再加上一个位置向量就可以创造一个变换矩阵。用这个矩阵乘上任何向量都可以将这个向量转换到观察坐标系中。我们集齐了这些条件,可以召唤神龙了:

观察矩阵

R表示Right向量,U表示Up向量,D表示光线方向,P表示位置向量。注意,位置向量取的是它的反方向,因为物体需要朝着摄像机相反的方向移动才行。

总结一下我们需要用到的数据:摄像机的位置,摄像机的观察目标(可以生成光线方向),还有世界空间的Up向量。使用这些数据,通过计算,我们就可以生成任意的观察矩阵。非常幸运的是,glm已经帮我们封装好了一个函数,调用它,我们可以直接获取到观察矩阵(而且不用担心出错!)。

glm::mat4 view;
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 4.0f),
                              glm::vec3(0.0f, 0.0f, 0.0f),
                              glm::vec3(0.0f, 1.0f, 0.0f));

验证一下函数的效果。我们把摄像机的位置放在半径为10的圆上,让它的观察点始终在世界空间原点上,并且,摄像机会不断地在圆上移动。

float radius = 10.0f;
float camX = sin(glfwGetTime()) * radius;
float camZ = cos(glfwGetTime()) * radius;
glm::mat4 view;
view = glm::lookAt(glm::vec3(camX, 0.0f, camZ), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
运行效果截图

是不是很赞?

移动相机

让相机在场景中转圈是挺有趣的,不过更有趣的还是我们自己来控制相机的移动。第一步,我们要来创建一个相机系统,这需要我们在程序开始的时候定义一些关于相机的变量。

glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 4.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);

观察矩阵就会变成这个样子:

view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);

我们希望摄像机的朝向不变而不是观察目标不变,所以观察点就变成cameraPos+cameraFront。现在,我们就要用键盘操作移动!

在我们之前定义的processInput函数的最后添加一些代码

float cameraSpeed = 0.05f; //移动速度
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
    cameraPos += cameraSpeed * cameraFront;

if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
    cameraPos -= cameraSpeed * cameraFront;

if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
    cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp) * cameraSpeed);

if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
    cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp) * cameraSpeed);

这样,我们可以使用WASD键来控制前后左右的移动了。

等等,是不是还露了点什么?对了,时间!这段代码纯粹是基于按键和代码运行速度来控制的,如果机子不好,代码运行慢点移动的速度也会变慢,这就不太科学了。因此,我们引入时间来计算移动的距离。

先定义两个全局的变量,用来保存上一帧绘制的时间以及两帧之间的间隔时间。

float deltaTime = 0.0f;  //两帧之间的间隔时间
float lastFrame = 0.0f;  //上一帧绘制的时间

然后,每一帧都更新这两个数值:

float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;

最后,在processInput中使用这个数值

float cameraSpeed = 2.5f * deltaTime; //移动速度

编译运行。

运行效果

在左右方向上移动地非常快,笔者也试过调小2.5f这个数值,但是经过尝试,即便是将2.5调成0.01在左右方向上移动地还是很快,而前后方向上就太慢了。

环顾四周

只用WASD控制移动还不算一个完整的FPS摄像机,我们还要能转头才行!

要实现转头的功能呢,我们就要对cameraFront向量进行改变了。不过对方向向量的改变比较复杂,还涉及要一些三角学的知识。如果你不了解三角学,跳过下面这一段也无妨,直接到代码的地方,等你想了解原理的时候再回来。

欧拉角

欧拉角是绕着三条轴旋转的一个值(欧拉这个名字应该很熟悉吧)。一共有3中欧拉角,分别是:pitch、yaw和roll。(避免歧义,直接用英文。)


欧拉角

pitch表示我们平时抬头低头的动作,yaw表示左看右看,roll表示,嗯,二哈打滚就是这种效果,咱不适合。每个欧拉角组合起来之后,我们可以表示任何旋转。

作为一个FPS摄像机,我们只需要pitch和yaw两种旋转就行了。通过三角计算,将方向向量设置成新值。

pitch计算

上图就是pitch旋转的计算方法。我们的初始方向为(0, 0, -1)。当我们想要转动pitch角度时,z坐标就等于-cos(pitch),y坐标就等于sin(pitch),因为我们假定了斜边长度为1,只考虑其方向。

yaw计算

类似的,计算yaw的方法也是如此,z坐标等于-cos(yaw),x坐标等于-sin(yaw)。

将两个旋转整合起来:
x = -sin(yaw)
y = sin(pitch)
z = -cos(pitch) * cos(yaw)

鼠标输入

pitch和yaw的值是通过鼠标的移动得到的,水平方向上的移动代表了yaw的值,垂直方向上的移动代表了pitch的值。我们需要保存上一次鼠标的位置,这样可以通过计算和这次鼠标位置的差值算出转动的角度。不过首先,我们我们需要把鼠标的光标隐藏起来,并且捕获鼠标消息。

glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);  
glfwSetCursorPosCallback(window, mouse_callback);  

mouse_callback是响应鼠标消息的回调函数,原型如下:

void mouse_callback(GLFWwindow* window, double xpos, double ypos);

window表示捕获的窗口,xpos表示x坐标,ypos表示y坐标。

为了计算一个方向向量,我们需要做这么几件事:

  1. 计算鼠标相对于上一次的位置偏移。
  2. 将偏移值累加到摄像机的yaw和pitch值中去。
  3. 添加一些旋转的限制
  4. 计算方向向量

先看代码

if (firstMouse) {  //设置初始位置,防止突然跳到某个方向上
    lastX = xPos;
    lastY = yPos;
    firstMouse = false;
}

float xoffset = lastX - xPos;   //别忘了,在窗口中,左边的坐标小于右边的坐标,而我们需要一个正的角度
float yoffset = lastY - yPos;   //同样,在窗口中,下面的坐标大于上面的坐标,而我们往上抬头的时候需要一个正的角度
lastX = xPos;
lastY = yPos;

float sensitivity = 0.05f;  //旋转精度
xoffset *= sensitivity;
yoffset *= sensitivity;

yaw += xoffset;
pitch += yoffset;

if (pitch > 89.0f)  //往上看不能超过90度
    pitch = 89.0f;
if (pitch < -89.0f)  //往下看也不能超过90度
    pitch = -89.0f;

glm::vec3 front;
front.x = -sin(glm::radians(yaw));
front.y = sin(glm::radians(pitch));
front.z = -cos(glm::radians(pitch)) * cos(glm::radians(yaw));
cameraFront = glm::normalize(front);

为了防止突然跳到某个方向,我们在鼠标刚开始的时候对它的位置进行设置。
接下来,计算与上次位置的偏移量,然后乘上旋转精度得到旋转的角度值。
然后,将旋转角度累加到pitch和yaw值中去,并且,设置pitch的最大和最小值。
最后,根据我们上面推倒的公式,计算方向向量,并将其规范化。

将这段代码写入到mouse_callback函数中,编译运行!

运行效果

这正是我们想要的!如果现实不对,可以下载源码比较。这里只提供本章写的源码,资源以及其他代码可以到上一篇文章中下载。

变焦

变焦功能,就是狙击枪的放大镜头。通过改变视野值来达到效果,将fov值变小,我们就能看到远方更精细的画面,将fov值变大,我们就可以看到更广的画面,当然也失去了精度优势。

那么我们如何获得fov的改变值呢?答案是通过鼠标滚轮消息来模拟!

//鼠标滚轮消息回调
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) {
    if (fov >= 1.0 && fov <= 45.0)
        fov -= yoffset;
    if (fov <= 1.0)
        fov = 1.0;
    if (fov >= 45.0)
        fov = 45.0;
}

当滚轮往前的时候,yoffset为正,使得fov值变小,物体变大变精细。相反,当滚轮往后的时候,yoffset为负,使fov值变大,物体变小视野变广。

当然,必不可少的一项在之前注册这个滚轮回调函数。

glfwSetScrollCallback(window, scroll_callback); 

于是,我们的投影矩阵就变成了:

projection = glm::perspective(glm::radians((float)fov), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);

非常简单!编译运行,你就能通过滚轮来变焦了。

变焦操作
如果在显示上遇到麻烦,请参照源码

封装类

之后的例子中,我们会经常用到这个摄像机来观察显示效果,所以,将它封装成类是聪明的做法。限于篇幅,就不再列出详细的代码了, 不过后面会给出源码,有兴趣的童鞋可以自己看内部的实现。

检查一遍类是否可用是一个非常好的习惯,摄像机类的源码在这里,主文件的源码在这里

我们现在封装的这个类可以满足大部分需求,但它并不是没有缺陷的。一个重要的问题就是万向节死锁。要解决这个问题,我们之后可以使用四元数的方法,现在先卖个关子。

总结

本章我们学了观察矩阵的内部原理,也通过一些三角学知识实现了一个简单的FPS摄像机,成果斐然!下一篇文章会对到目前为止所学到的内容进行总结梳理,毕竟知识不在多而在融会贯通。

下一篇
目录
上一篇

参考资料:
www.learningopengl.com(非常好的网站,建议仔细学习)

上一篇下一篇

猜你喜欢

热点阅读