ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2021-03-05 21:01:49  阅读:213  来源: 互联网

标签:glm OpenGL 0.0 摄像机 vec3 FPS pitch 向量


本文主要解决一个问题:

如何创建一个FPS摄像机?

1.引言

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

在这章中,你会看到

  • 观察空间变换的内部原理
  • 键盘操纵摄像机前后左右移动的方法
  • 鼠标操纵摄像机上下左右转动的方法
  • 实现变焦的方式
  • 将摄像机功能封装成类(该死,好久没这么有创造性的封装一个类了,码农当太久脑子都秀逗了。)

2.观察(摄像机)空间

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

2.1 相机位置

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

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

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

2.2 光线方向

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

glm::vec3 cameraTarget  glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);

2.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));

2.4 Up轴

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

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

叉乘真是个好东西!

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

3.观察矩阵

用矩阵的最大好处就是当你有了坐标空间的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的圆上,让它的观察点始终在世界空间原点上,并且,摄像机会不断地在圆上移动。

参考源代码:

https://gitee.com/pengrui2009/open-gl-study/blob/master/chapter9/square_9_1.cc

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));

运行效果截图
是不是很赞?

参考源代码:

https://gitee.com/pengrui2009/open-gl-study/blob/master/chapter9/square_9_2.cc

4. 移动相机

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

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在左右方向上移动地还是很快,而前后方向上就太慢了。

参考源代码:

https://gitee.com/pengrui2009/open-gl-study/blob/master/chapter9/square_9_3.cc

5.环顾四周

只用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)*cos(pitch)
y = sin(pitch)
z = -cos(pitch) * cos(yaw)

6.鼠标输入

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坐标。

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

  • 计算鼠标相对于上一次的位置偏移。
  • 将偏移值累加到摄像机的yaw和pitch值中去。
  • 添加一些旋转的限制
  • 计算方向向量
    先看代码
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)) * cos(glm::radians(pitch));
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函数中,编译运行!
运行效果
参考源代码:

https://gitee.com/pengrui2009/open-gl-study/blob/master/chapter9/square_9_4.cc

7.变焦

变焦功能,就是狙击枪的放大镜头。通过改变视野值来达到效果,将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);

非常简单!编译运行,你就能通过滚轮来变焦了。
变焦操作
参考源代码:

https://gitee.com/pengrui2009/open-gl-study/blob/master/chapter9/square_9_6.cc

8.封装类

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

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

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

参考源代码:

https://gitee.com/pengrui2009/open-gl-study/blob/master/chapter9/square_9_7.cc

9.总结

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

本章节源代码:

https://gitee.com/pengrui2009/open-gl-study/tree/master/chapter9

作者:闪电的蓝熊猫
链接:https://www.jianshu.com/p/bc09f44e0856
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

标签:glm,OpenGL,0.0,摄像机,vec3,FPS,pitch,向量
来源: https://blog.csdn.net/pengrui18/article/details/114410524

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有