美文网首页OpenGL程序员计算机杂谈
从0开始的OpenGL学习(九)-FPS摄像机

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

作者: 闪电的蓝熊猫 | 来源:发表于2017-10-21 20:46 被阅读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(非常好的网站,建议仔细学习)

    相关文章

      网友评论

      • DoneEI:老哥 能分享一下这一篇的源码吗 你的链接失效了 谢谢啦
      • a6e800540107:你的x方向角推导有误吧
        闪电的蓝熊猫:@麦田里的守望者_c5f9 你说的对,斜边是变化后的值,cos(pitch)。我说怎么转上去有些不对呢,谢谢你的提醒,不然我还是错的。文章已经改正了。
        a6e800540107:@闪电的蓝熊猫 我看了一下教程,pitch角不是定义在yz平面上,pitch的斜边是1,yaw的斜边是应该是cos(pitch)了
        闪电的蓝熊猫:@麦田里的守望者_c5f9 哪里有误?
      • 寰宸:老哥,这一步,所有的都设置了,有些小疑问:第一次移动鼠标时还是跳出场景外了,这是咋回事啊~(代码基本上一致)
        闪电的蓝熊猫:@虚弥空 这个问题我之前也遇到过,八成是位置没有设对,你在仔细检查检查摄像机控制部分的代码就好了。
        寰宸:@闪电的蓝熊猫 啊,一开始编译运行出现了效果图,然后鼠标动一下就不见了,得四处移动鼠标才能找回来
        闪电的蓝熊猫:跳出场景外是什么意思???

      本文标题:从0开始的OpenGL学习(九)-FPS摄像机

      本文链接:https://www.haomeiwen.com/subject/xbeuuxtx.html