本文主要解决一个问题:

如何创建一个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旋转的计算方法。我们的初始方向为(0, 0, -1)。当我们想要转动pitch角度时,z坐标就等于-cos(pitch),y坐标就等于sin(pitch),因为我们假定了斜边长度为1,只考虑其方向。

类似的,计算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
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

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

  1. 从0开始的OpenGL学习(八)-显示3D立方体

    本文主要解决的问题是: 如何在OpenGL中显示一个3D盒子? 欢迎来到3D世界! 学了这么多东西,还只能画一些三角形和矩形,是不是感觉特别憋屈?"我是来学3D的,为啥到现在还都是2D的图片 ...

  2. OpenGL学习(九)阴影映射(shadowMapping)

    目录 写在前面 阴影映射原理简介 封装 Camera 类 帧缓冲 阴影映射 准备工作 创建帧缓冲与深度纹理附件 从光源方向进行渲染 正常地渲染场景 如何查找bug(⚠重要) 多纹理传送 查看深度纹理数 ...

  3. Android OpenGL ES 学习(九) – 坐标系统和实现3D效果

    OpenGL 学习教程 Android OpenGL ES 学习(一) – 基本概念 Android OpenGL ES 学习(二) – 图形渲染管线和GLSL Android OpenGL ES 学 ...

  4. 【我的OpenGL学习进阶之旅】OpenGL ES 3.0新功能

    目录 1.1 纹理 1.2 着色器 1.3 几何形状 1.4 缓冲区对象 1.5 帧缓冲区 OpenGL ES 2.0 开创了手持设备可编程着色器的时代,在驱动大量设备的游戏.应用程序和用户接口中获得 ...

  5. OpenGL学习(十)天空盒

    目录 写在前面 天空盒简介 创建立方体贴图 渲染一个立方体 立方体贴图着色器 开始绘制天空盒 完整代码 着色器 c++ 写在前面 上一篇博客回顾:OpenGL学习(九)阴影映射(shadowMappi ...

  6. 【我的OpenGL学习进阶之旅】【持续更新】关于学习OpenGL的一些资料

    目录 一.相关书籍 OpenGL 方面 C方面 NDK 线性代数 二.相关博客 2.0 一些比较官方的链接 2.1 OpenGL着色器语言相关 2.2 [[yfan]](https://segment ...

  7. openGL学习之glut库的使用

    对于初学者来说,做小项目用的glut已经很足够了,它的定义是用于简单程序和初学者学习使用的简单的.容易的.小的. vs2015创建空项目: 然后在项目中添加glut中include路径: 先创建一个调 ...

  8. 【OpenGL学习笔记⑧】——键盘控制正方体+光源【冯氏光照模型 光照原理 环境光照+漫反射光照+镜面光照】

    ✅ 重点参考了 LearnOpenGL CN 的内容,但大部分知识内容,小编已作改写,以方便读者理解. 文章目录 零. 成果预览图 一. 光照原理与投光物的配置 1.1 光照原理 1.2 投光物 二. ...

  9. OpenGL 学习笔记 II:初始化 API,第一个黑窗,游戏循环和帧率,OpenGL 默认垂直同步,glfw 帧率

    前情提要: 上一篇: OpenGL 学习笔记 I:OpenGL glew glad glfw glut 的关系,OpenGL 状态机,现代操作系统的窗口管理器,OpenGL 窗口和上下文 OpenGL ...

最新文章

  1. Servlet实现的三种方法
  2. 需要大量设计的软件如何进行敏捷开发
  3. [渝粤教育] 西南科技大学 管理学原理 在线考试复习资料(4)
  4. 任玉刚:让你的职业迷茫从哪来回哪去
  5. 触摸屏是怎么控制PLC的?
  6. 计算机无法访问家庭组内打印机,Win7电脑无法连接共享打印机拒绝访问怎么办...
  7. 错误记录-java idea执行k8s https api报错 should not be presented in certificate_request
  8. DP1.2 硬件规范——硬件/Lenovo
  9. confluence开发,实现与现有单点登录sso系统对接。
  10. 什么叫少儿机器人编程
  11. Uni-App - 学习心得
  12. 简明GISer Python学习指南
  13. React + Ts项目搭建
  14. 使用UltraISO制作U盘安装盘的方法
  15. soi cmos技术及其应用_微生物污水处理技术及其应用
  16. java 判断是否信用卡_《Java语言程序设计》编程练习6.31(财务应用程序:信用卡号的合法性)...
  17. 航班系统C语言程序流程图,飞机订票系统(C语言代码及流程图)
  18. 服务器多开虚拟机对网络要求,虚拟机多开到一定数量后网络不稳定或没网
  19. 高清视频会议需要哪些基础设备
  20. uniapp开发微信小程序实现语音识别,使用微信同声传译插件,

热门文章

  1. 虎牙数据分析-可视化-爬虫-GUI界面结合
  2. 2012,做一个现实的理想主义者
  3. iscroll.js移动端滚动插件
  4. 计算机校本课程方案,浅论信息技术校本课程的编写与实施
  5. kali设置中文及安装拼音输入法
  6. “火山论剑”之且用且珍惜- 浅说DFT工程师三大法宝的使用
  7. 【shaderforge学习笔记】 Hue节点(色相节点)
  8. 侧方移位路线图及完全功略
  9. 改变视频剪辑的播放速度
  10. jmeter组合场景_如何将不同的HomeKit产品组合到房间,区域和场景中