OPENGL学习笔记之九

#OPENGL学习笔记之九
2017/12/18

阅读材料来自learnopengl.com以及learnopengl-cn.github.io

OpenGL本身没有 摄像机(Camera) 的概念,但我们可以通过把场景中的所有物体往相反方向移动的方式来模拟出摄像机的行为,感觉是我们在移动而不是场景。

摄像机/观察空间(Camera/View Space) 意味着以摄像机的视角作为场景原点时观察场景中所有的顶点坐标:观察矩阵把所有的世界坐标变换为相对于摄像机位置与方向的观察坐标。定义一个摄像机我们需要确定它在世界空间中的位置、观察的方向、一个指向它右测的向量以及一个指向它上方的向量,如图所示
摄像机观察坐标

1. 摄像机位置
摄像机位置简单来说就是世界空间中一个指向摄像机位置的向量,代码设为上一节内容的相同位置

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

z轴是从屏幕指向我们的,如果我们希望摄像机向后移动,我们就沿着z轴的正方向,反之则反。

2. 摄像机方向
这里摄像机的方向指的是摄像机指向的方向,根据向量预算,用场景原点向量减去摄像机位置向量的结果就是摄像机的指向向量,也就是说摄像机指向z轴负方向。但我们希望**方向向量(Direction Vector)**指向摄像机的z轴正方向。如果我们交换相减的顺序,我们就会获得一个指向摄像机正z轴方向的向量:

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

方向向量(Direction Vector)实际上指向从它到目标向量的相反方向,之前大图蓝色的方向向量(即方向向量)大概指向z轴的正方向,与摄像机实际指向的方向正好相反。

3. 右轴
右向量(Right Vector)代表摄像机空间的x轴的正方向。为获取右向量,我们先定义一个上向量(Up Vector)。然后把上向量和第二步得到的方向向量进行叉乘。两个向量叉乘的结果会同时垂直于两向量,因此我们会得到指向x轴正方向的那个向量:

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

4. 上轴
我们把右向量方向向量进行叉乘就能获得指向摄像机的正y轴向量:

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

5.Look At矩阵
LookAt = \begin{bmatrix} \color{red}{R_x} & \color{red}{R_y} & \color{red}{R_z} & 0 \ \color{green}{U_x} & \color{green}{U_y} & \color{green}{U_z} & 0 \ \color{blue}{D_x} & \color{blue}{D_y} & \color{blue}{D_z} & 0 \ 0 & 0 & 0 & 1 \end{bmatrix} * \begin{bmatrix} 1 & 0 & 0 & -\color{purple}{P_x} \ 0 & 1 & 0 & -\color{purple}{P_y} \ 0 & 0 & 1 & -\color{purple}{P_z} \ 0 & 0 & 0 & 1 \end{bmatrix}

GLM已经为我们提供了这些基本操作,我们只要定义一个摄像机位置,一个目标位置和一个表示世界空间中的上向量的向量(即计算右向量使用的那个上向量)。接着GLM就会创建一个LookAt矩阵

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

glm::LookAt函数的参数需要一个位置、目标和上向量三个向量

我们这里实现一个小操作,让摄像机绕着场景旋转,将注视点保持在坐标(0,0,0):
利用几何知识,我们在每一帧创建一个x和z坐标,它会代表圆上的一点然后我们将其作为摄像机所处的坐标。我们预先定义这个圆的半径radius,在每次渲染迭代中使用GLFW的glfwGetTime函数重新创建观察矩阵,来扩大这个圆。

float radius = 10.0f;
float camX = sin(glfwGetTime()) * radius;
float camZ = cos(glfwGetTime()) * radius;
glm::mat4 view;
view = glm::lookAt(glm::vec3(camX, 0.0, camZ), glm::vec3(0.0, 0.0, 0.0), glm::vec3(0.0, 1.0, 0.0)); 

通过这一小段代码,摄像机现在会随着时间流逝围绕场景转动了。一定要尝试调整参数理解LookAt矩阵的意义。

6.自由移动
我们定义一些摄像机变量,方便以后的摄像机移动

glm::vec3 cameraPos   = glm::vec3(0.0f, 0.0f,  3.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp    = glm::vec3(0.0f, 1.0f,  0.0f);
//相应的lookAt函数要改为
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);

首先将摄像机位置设置为之前定义的cameraPos。方向是当前的位置加上刚刚定义的方向向量。这样能保证无论怎么移动,摄像机都会注视着目标方向

对按键设置的代码为:

void processInput(GLFWwindow *window)
{
    ...
    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;//与上同理
}

7.移动速度
因为处理器的能力不同,帧的绘制频率(即调用processInput函数的频率)不同,之前定义的移动速度cameraSpeed这个常量对不同机器不适配。
图形程序和游戏通常会跟踪一个**时间差(Deltatime)**变量,它储存了渲染上一帧所用的时间。我们把所有速度都去乘以deltaTime值。结果就是,如果我们的deltaTime很大,就意味着上一帧的渲染花费了更多时间,所以这一帧的速度需要变得更高来平衡渲染所花去的时间。使用这种方法时,无论你的电脑快还是慢,摄像机的速度都会相应平衡,这样每个用户的体验就都一样了。

//我们跟踪两个全局变量来计算出deltaTime值:
float deltaTime = 0.0f; // 当前帧与上一帧的时间差
float lastFrame = 0.0f; // 上一帧的时间
//在每一帧中我们计算出新的deltaTime以备后用。
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
//现在我们有了deltaTime,在计算速度的时候可以将其考虑进去了:
void processInput(GLFWwindow *window)
{
  float cameraSpeed = 2.5f * deltaTime;
  ...
}

8.视角移动
为了能够改变视角增加转向功能,我们需要根据鼠标的输入改变cameraFront向量。
**欧拉角(Euler Angle)**是可以表示3D空间中任何旋转的3个值,由莱昂哈德·欧拉(Leonhard Euler)在18世纪提出。一共有3种欧拉角:俯仰角(Pitch)偏航角(Yaw)滚转角(Roll),如下图所示:
欧拉角
俯仰角描述上下,偏航角表示左右,滚转角代表左倾或者右倾。每个欧拉角都有一个值来表示,把三个角结合起来我们就能够计算三维空间中任何的旋转向量了。

对于摄像机系统来说,我们只关心俯仰角和偏航角。给定一个俯仰角和偏航角,我们可以把它们转换为一个代表新的方向向量的三维向量。

最方便的理解就是空间直角坐标系的计算,在这个教程中,y轴为我们国内高数题竖直向上的z轴,x与y形成国内高数题的xy平面.

//教程要求先把角度转换成弧度,可能是glm版本特性原因我们这里可以直接带入角度计算
direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw)); //direction代表摄像机的前轴(Front),这个前轴是和第一幅图片的第二个摄像机的方向向量是相反的
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));

9.鼠标输入
偏航角和俯仰角是通过鼠标(或手柄)移动获得的,水平的移动影响偏航角,竖直的移动影响俯仰角。它的原理就是,储存上一帧鼠标的位置,在当前帧中我们当前计算鼠标位置与上一帧的位置相差多少。如果水平/竖直差别越大那么俯仰角或偏航角就改变越大,也就是摄像机需要移动更多的距离。

首先我们要告诉GLFW,它应该隐藏光标,并捕捉(Capture)它。捕捉光标表示的是,如果焦点在你的程序上,光标应该停留在窗口中(除非程序失去焦点或者退出)。我们可以用一个简单地配置调用来完成:

glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);//在调用这个函数之后,无论我们怎么去移动鼠标光标既不会显示也不会离开窗口。对于FPS摄像机系统来说非常完美。

void mouse_callback(GLFWwindow* window, double xpos, double ypos);//xpos和ypos代表当前鼠标的位置

glfwSetCursorPosCallback(window, mouse_callback);//为了计算俯仰角和偏航角,我们注册一个回调函数让GLFW监听鼠标移动事件

在处理FPS风格摄像机的鼠标输入的时候,我们必须在最终获取方向向量之前做下面这几步:

  • 计算鼠标距上一帧的偏移量。
  • 把偏移量添加到摄像机的俯仰角和偏航角中。
  • 对偏航角和俯仰角进行最大和最小值的限制。
  • 计算方向向量。
float lastX = 400, lastY = 300;//在程序中储存上一帧的鼠标位置,这里初始值设置为屏幕的中心(屏幕的尺寸是800x600):

//在回调函数中输入以下代码
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // 注意这里是相反的,因为y坐标是从底部往顶部依次减小的,还记得之前关于屏幕坐标的图就知道
lastX = xpos;
lastY = ypos;
float sensitivity = 0.05f;
xoffset *= sensitivity;
yoffset *= sensitivity;
//接下来我们把偏移量加到全局变量pitch和yaw上:
yaw   += xoffset;
pitch += yoffset;
//我们需要给摄像机添加一些限制,对于俯仰角,要让用户不能看向高于89度的地方(在90度时视角会发生逆转,所以我们把89度作为极限),同样也不允许小于-89度。这样能够保证用户只能看到天空或脚下,但是不能超越这个限制。
if(pitch > 89.0f)
  pitch =  89.0f;
if(pitch < -89.0f)
  pitch = -89.0f;
//最后,通过俯仰角和偏航角来计算以得到真正的方向向量:
glm::vec3 front;
front.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw));
front.y = sin(glm::radians(pitch));
front.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
cameraFront = glm::normalize(front);

在你的鼠标移动进窗口的那一刻,鼠标回调函数就会被调用,这时候的xpos和ypos会等于鼠标刚刚进入屏幕的那个位置。这通常是一个距离屏幕中心很远的地方,因而产生一个很大的偏移量,摄像机会突然跳一下,解决这个问题可以用以下代码在回调函数中进行第一次检测:

if(firstMouse) // 这个bool变量初始时是设定为true的
{
    lastX = xpos;
    lastY = ypos;
    firstMouse = false;
}

10.缩放
作为我们摄像机系统的一个附加内容,我们还会来实现一个**缩放(Zoom)**接口。视野(Field of View)fov定义了我们可以看到场景中多大的范围。当视野变小时,场景投影出来的空间就会减小,产生放大(Zoom In)了的感觉。我们会使用鼠标的滚轮来放大。与鼠标移动、键盘输入一样,我们需要一个鼠标滚轮的回调函数:

//当scroll_callback函数被调用后,我们改变全局变量fov变量的内容。因为45.0f是默认的视野值,我们将会把缩放级别(Zoom Level)限制在1.0f到45.0f
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
  if(fov >= 1.0f && fov <= 45.0f)
    fov -= yoffset;//yoffset值代表我们竖直滚动的大小。
  if(fov <= 1.0f)
    fov = 1.0f;
  if(fov >= 45.0f)
    fov = 45.0f;
}

//之后我们要修改投影矩阵,每一帧都必须把透视投影矩阵上传到GPU
projection = glm::perspective(glm::radians(fov), (float)width / (float)height, 0.1f, 100.0f);

//最后记得注册鼠标滚轮回调函数
glfwSetScrollCallback(window, scroll_callback);

11.摄像机类
和着色器一样,我们可以把摄像机的诸多功能封装到类里声明和调用。
这里是摄像机类参考代码
这里是更新摄像机类后的所有源码

注意,使用欧拉角的摄像机系统并不完美。根据你的视角限制或者是配置,你仍然可能引入万向节死锁问题。最好的摄像机系统是使用四元数(Quaternions)的,但我们将会把这个留到后面讨论。(译注:这里可以查看四元数摄像机的实现)
关于欧拉角与万向节死锁,这里可以参考这篇博文
https://www.cnblogs.com/driftingclouds/p/6540222.html

你可能感兴趣的:(学习笔记)