OpenGL学习笔记8-Camera

  • 时间:
  • 浏览:
  • 来源:互联网

Camera

Getting-started/Camera

在前一章中,我们讨论了视图矩阵以及如何使用视图矩阵在场景中移动(我们稍微向后移动了一点)。OpenGL本身并不熟悉相机的概念,但我们可以通过逆向移动场景中的所有对象来模拟相机,给人一种我们正在移动的错觉

在本章中,我们将讨论如何在OpenGL中设置一个摄像头。我们将讨论一个飞行风格的相机,允许你在一个3D场景中自由移动。我们还将讨论键盘和鼠标输入,并以一个自定义相机类结束。

Camera/View space

当我们谈论相机/视图空间时,我们谈论的是作为场景原点从摄像机视角看到的所有顶点坐标:视图矩阵将所有世界坐标转换为相对于摄像机位置和方向的视图坐标。要定义一个摄像机,我们需要它在世界空间中的位置,它注视的方向,一个指向右边的向量和一个指向上方的向量。细心的读者可能会注意到,我们实际上要创建一个有3个垂直单位轴的坐标系,以摄像机的位置为原点。

 

1. Camera position

获得相机的位置很容易。摄像机位置是世界空间中指向摄像机位置的向量。我们将摄像机设置在与前一章相同的位置:


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

别忘了正z轴是穿过屏幕向你这边移动的所以如果我们想让摄像机向后移动,我们就沿着正z轴移动。

2. Camera direction

下一个需要的矢量是相机的方向,例如它指向的方向。现在我们让摄像机指向场景的原点:(0,0,0)。还记得吗,如果我们把两个向量相减我们得到的向量是这两个向量的差值?从场景的原始向量中减去摄像机位置向量,从而得到我们想要的方向向量。对于视图矩阵的坐标系,我们希望它的z轴是正的,因为按照惯例(在OpenGL中)相机指向负的z轴,我们想要使方向向量负。如果我们改变减法顺序,我们现在得到一个指向摄像机正z轴的向量:


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

名称方向向量不是最好选择的名称,因为它实际上指向它的目标的相反方向

3. Right axis

我们需要的下一个向量是一个表示摄像机空间正x轴的右向量。为了得到正确的向量,我们使用了一个小技巧,首先指定一个向上的向量(在世界空间中)。然后我们对第2步中的向上向量和方向向量做一个叉乘。由于叉乘的结果是一个垂直于两个向量的向量,我们将得到一个指向正x轴方向的向量(如果我们改变叉乘的顺序,我们将得到一个指向负x轴的向量):


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

4. Up axis

现在我们已经有了x轴矢量和z轴矢量,检索指向相机正y轴的矢量就比较容易了:我们取右矢量和方向矢量的叉乘:


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

借助叉乘和一些技巧,我们能够创建出构成视图/摄像机空间的所有向量。对于更倾向于数学的读者,这个过程被称为线性代数中的Gram-SchmidtGram-Schmidt过程。使用这些摄像机向量,我们现在可以创建一个LookAt矩阵,它被证明对创建一个摄像机非常有用。

Look At

矩阵的一个伟大之处在于,如果你用3个垂直的(或非线性的)坐标轴来定义一个坐标空间,你可以用这3个坐标轴加上一个平移向量来创建一个矩阵,你可以通过将它与这个矩阵相乘来将任意向量变换到那个坐标空间。这就是LookAt矩阵所做的,现在我们有3个垂直轴和一个位置向量来定义摄像机空间,我们可以创建我们自己的LookAt矩阵:

其中RR为右矢量,UU为上矢量,DD为方向矢量,PP为摄像机位置矢量。注意,旋转(左矩阵)和平移(右矩阵)部分是反向的(分别调换和否定),因为我们想要旋转和平移世界的方向与我们想要摄像机移动的方向相反。使用这个视点矩阵作为我们的视图矩阵,有效地将所有世界坐标转换为我们刚刚定义的视图空间。LookAt矩阵就像它说的那样:它创建一个查看给定目标的视图矩阵。

幸运的是,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));

ookAt函数分别需要一个位置、目标和向上矢量。这个例子创建的视图矩阵与我们在前一章中创建的视图矩阵相同。

在深入研究用户输入之前,让我们先在场景周围旋转摄像机,让它看起来有点古怪。我们保持场景的目标为(0,0,0)。我们使用了一点三角学知识来创建一个x和z坐标来表示圆上的一个点我们将使用这些来表示我们的摄像机位置。通过在一段时间内重新计算x和y坐标,我们将遍历圆周上的所有点,从而使摄像机围绕场景旋转。使用GLFW的glfwGetTime函数在每一帧中创建一个新的视图矩阵:


const 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矩阵如何工作的感觉。另外,如果卡住了,请检查源代码source code 。

Walk around

在一个场景周围摆动相机是有趣的,但更有趣的是做所有的运动,自己!首先,我们需要设置一个摄像机系统,所以在程序顶部定义一些摄像机变量是有用的:


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。方向是当前位置加上我们刚刚定义的方向向量。这确保了无论我们如何移动,摄像机都能一直注视目标方向。当我们按下一些键时,让我们通过更新cameraPos向量来稍微处理一下这些变量。

我们已经定义了一个processInput函数来管理GLFW的键盘输入,所以让我们添加一些额外的关键命令:


void processInput(GLFWwindow *window)
{
    ...
    const float cameraSpeed = 0.05f; // adjust accordingly
    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键时,相机的位置就会相应地更新。如果我们想向前或向后移动,我们要从按速度值缩放的位置向量中添加或减去方向向量。如果我们想要横向移动我们做一个叉乘来创建一个正确的向量然后沿着正确的向量移动。当使用相机时,这会产生熟悉的扫射效果。

注意,我们将得到的右向量归一化。如果我们不将这个向量归一化,那么得到的叉乘可能会根据cameraFront变量返回不同大小的向量。如果我们不规格化矢量,我们会根据相机的方向而不是以一致的运动速度慢或快地移动。

到目前为止,你应该已经能够稍微移动相机了,尽管速度是特定于系统的,所以你可能需要调整cameraSpeed。

Movement speed

目前,我们在走动时使用一个常数值来表示移动速度。理论上,这似乎很好,但在实践中,人类的机器有不同的处理能力,结果就是有些人每秒渲染的帧数比其他人多得多。每当一个用户比另一个用户呈现更多的帧时,他也会更频繁地调用processInput。结果是,有些人移动得很快,有些人移动得很慢,这取决于他们的设置。在交付应用程序时,您希望确保它在所有类型的硬件上运行相同的程序。

图形应用程序和游戏通常跟踪一个deltatime变量,该变量存储渲染最后一帧所花费的时间。然后将所有速度与这个三角函数值相乘。结果是当我们有一个大的三角时间在一个坐标系中,意味着最后一个坐标系花费的时间比平均时间长,那个坐标系的速度也会稍微高一些来平衡它。当使用这种方法时,不管你有一个非常快或慢的pc,相机的速度将相应地平衡,所以每个用户将有相同的体验。

为了计算deltaTime值,我们跟踪两个全局变量:


float deltaTime = 0.0f;	// Time between current frame and last frame
float lastFrame = 0.0f; // Time of last frame

在每一帧中,我们计算新的deltaTime值,以便以后使用:


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

现在我们有了deltaTime,我们可以在计算速度时考虑它:


void processInput(GLFWwindow *window)
{
    float cameraSpeed = 2.5f * deltaTime;
    [...]
}

因为我们使用deltaTime相机现在会以每秒2.5个单位的恒定速度移动。与前面的部分一起,我们现在应该有一个更加平滑和一致的相机系统移动的场景:

 

现在我们有了一个相机,它在任何系统中行走和看起来都一样快。同样,如果卡住了,检查源代码source code 。我们将看到deltaTime值经常返回任何与移动相关的内容。

Look around

仅仅使用键盘键来移动并不是那么有趣。特别是因为我们不能转身,使运动受到限制。这就是鼠标的用处!

为了查看场景,我们必须根据鼠标的输入改变cameraFront矢量。但是,基于鼠标旋转改变方向向量有点复杂,需要一些三角学知识。如果你不懂三角函数,不用担心,你可以直接跳到代码段,然后粘贴到你的代码中;如果你想知道更多,你可以以后再来。

Euler angles 欧拉角

欧拉角是3个可以代表3D中任意旋转的值,是由Leonhard Euler在18世纪定义的。有三个欧拉角:俯仰、偏航和横摇。下面的图片给了他们一个视觉上的意义:

 

俯仰是描述我们在第一张图片中向上或向下看的角度。第二幅图显示了偏航值代表了我们向左或向右看的幅度。滚动表示我们有多少滚动,主要用于太空飞行相机。每个欧拉角都由一个值表示,通过这三个值的组合,我们可以计算出三维中任意的旋转向量。

对于我们的摄像机系统,我们只关心偏航和俯仰的值,所以我们不会在这里讨论滚转值。给定一个俯仰和一个偏航值,我们可以将它们转换成一个表示新的方向向量的三维向量。将偏航和俯仰值转换为方向矢量的过程需要一点三角学知识。我们从一个基本的例子开始:

让我们先复习一下,检查一下直角三角形的一般情况(一条边是90度角):

如果我们定义斜边长度1我们从三角(三角学)知道adjacant边的长度是和反面的长度是。根据给定的角度,这给了我们一些获取直角三角形x和y边长度的一般公式。我们用这个来计算方向矢量的分量。

让我们想象一下这个相同的三角形,但是现在从顶部的角度来看它,它的邻边和对边平行于场景的x轴和z轴(就像向下看y轴一样)。

 

如果我们把偏航角想象成从x边开始的逆时针角我们可以看到x边的长度与cos(偏航)有关。同样的,z边的长度与sin(yaw)的关系

如果我们利用这个知识和一个给定的偏航值,我们可以使用它来创建一个摄像机方向矢量:


glm::vec3 direction;
direction.x = cos(glm::radians(yaw)); // Note that we convert the angle to radians first
direction.z = sin(glm::radians(yaw));

这解决了我们如何从偏航值得到三维方向矢量,但俯仰也需要包括在内。现在我们来看看y轴边假设我们在xz平面上

 

同样地,从这个三角形我们可以看到方向的y分量等于sin(pitch)所以我们把它填进去:


direction.y = sin(glm::radians(pitch));  

然而,从节距三角形中我们也可以看到xz边受到cos(节距)的影响,所以我们需要确保这也是方向矢量的一部分。包括这些,我们得到了从偏航和俯仰欧拉角转换的最终方向矢量:


direction.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
direction.y = sin(glm::radians(pitch));
direction.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));

这给了我们一个公式来转换偏航和俯仰值到一个三维方向矢量,我们可以使用它来观察周围。

我们已经建立了场景世界,所以所有东西都在负z轴方向上。然而,如果我们观察x和z偏航三角形,我们可以看到,一个反射角的反射角为0会导致相机指向正x轴的方向向量。为了确保相机默认指向负z轴,我们可以给偏航一个默认值顺时针旋转90度。正角度逆时针旋转,所以我们设置默认的偏航值为:


yaw = -90.0f;

你现在可能想知道:我们如何设置和修改这些偏航和俯仰值?

Mouse input

偏航和俯仰值是通过鼠标(或控制器/操纵杆)的运动获得的,其中鼠标的水平运动影响偏航,而鼠标的垂直运动影响俯仰。其思想是存储最后一帧的鼠标位置,并计算当前帧中鼠标值的变化。水平或垂直差越高,我们更新的俯仰或偏航值就越多,因此相机应该移动的越多。

首先,我们将告诉GLFW它应该隐藏并捕获光标。捕获光标意味着,一旦应用程序有了焦点,鼠标光标将停留在窗口的中心(除非应用程序失去焦点或退出)。我们可以通过一个简单的配置调用来做到这一点:


glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);  

在此调用之后,无论我们将鼠标移动到哪里,它都将不可见,并且不应该离开窗口。这是完美的FPS相机系统。

为了计算俯仰和偏航值,我们需要告诉GLFW听鼠标移动事件。我们通过创建一个回调函数的原型如下:


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

这里xpos和ypos表示当前鼠标位置。当我们在GLFW中注册回调函数时,mouse_callback函数就会被调用:


glfwSetCursorPosCallback(window, mouse_callback);  

当处理鼠标输入一个fly风格的相机,有几个步骤,我们必须采取之前,我们能够完全计算相机的方向矢量:

  1. Calculate the mouse's offset since the last frame.计算鼠标从最后一帧开始的偏移量。
  2. Add the offset values to the camera's yaw and pitch values.将偏移值添加到相机的偏航和俯仰值中。
  3. Add some constraints to the minimum/maximum pitch values.给最小/最大音高值添加一些约束。
  4. Calculate the direction vector.计算方向向量。

 

第一步是计算鼠标自上一帧以来的偏移量。首先,我们必须存储最后的鼠标位置在应用程序中,我们初始化在屏幕的中心(屏幕大小为800 * 600):


float lastX = 400, lastY = 300;

然后在鼠标的回调函数中计算最后一帧和当前帧之间的偏移量:


float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // reversed since y-coordinates range from bottom to top
lastX = xpos;
lastY = ypos;

const float sensitivity = 0.1f;
xoffset *= sensitivity;
yoffset *= sensitivity;

注意,我们用灵敏度值乘以偏移值。如果我们忽略了这个乘法,鼠标移动就会太强大;根据你的喜好摆弄你的敏感值。

接下来,我们将偏移值添加到全局声明的俯仰和偏航值中:


yaw   += xoffset;
pitch += yoffset;  

在第三步中,我们想要给相机添加一些约束,这样用户就不会做出奇怪的相机移动(当方向矢量与向上的world方向平行时,也会导致LookAt翻转)。音高需要加以限制,这样用户在观看时不能高于89度(在90度时我们可以看到LookAt翻转),也不能低于-89度。这保证了用户可以抬头看天空或看脚下,但不能看得更远。约束的工作方式是当欧拉值违反约束时,用其约束值替换欧拉值:


if(pitch > 89.0f)
  pitch =  89.0f;
if(pitch < -89.0f)
  pitch = -89.0f;

注意,我们没有对偏航值设置约束,因为我们不想在水平旋转中约束用户。然而,如果你喜欢的话,添加偏航约束也很容易。

第四步,也是最后一步,利用上一节的公式计算实际方向向量:


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

这个经过计算的方向向量包含了通过鼠标移动计算的所有旋转。因为cameraFront向量已经包含在glm的lookAt函数中,所以我们将其设置为go。.

如果您现在运行代码,您将注意到,每当窗口第一次接收到您的鼠标光标的焦点时,摄像机会突然出现一个大的跳跃。造成这种突然跳转的原因是,当您的光标进入窗口时,就会调用鼠标回调函数,其xpos和ypos的位置与您的鼠标从屏幕进入的位置相同。这通常是一个距离屏幕中心很远的位置,导致较大的偏移量,从而产生较大的移动跳跃。我们可以通过定义一个全局bool变量来检查这是否是我们第一次接收鼠标输入来绕过这个问题。如果是第一次,我们将鼠标初始位置更新为新的xpos和ypos值。鼠标移动结果将使用新输入的鼠标位置坐标计算偏移量:


if (firstMouse) // initially set to true
{
    lastX = xpos;
    lastY = ypos;
    firstMouse = false;
}

最后的代码变成:


void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
    if (firstMouse)
    {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }
  
    float xoffset = xpos - lastX;
    float yoffset = lastY - ypos; 
    lastX = xpos;
    lastY = ypos;

    float sensitivity = 0.1f;
    xoffset *= sensitivity;
    yoffset *= sensitivity;

    yaw   += xoffset;
    pitch += yoffset;

    if(pitch > 89.0f)
        pitch = 89.0f;
    if(pitch < -89.0f)
        pitch = -89.0f;

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

我们走吧!给它一个旋转,你会看到我们现在可以自由移动通过我们的3D场景!

Zoom

作为一个额外的相机系统,我们还将实现一个缩放界面。在前一章我们说过视场或fov很大程度上决定了我们可以看到的场景。当视场变小时,场景的投影空间也变小。这个较小的空间投射在同一个NDC上,给人一种放大的错觉。为了放大,我们将使用鼠标滚轮。类似于鼠标移动和键盘输入,我们有一个回调函数鼠标滚动:


void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    Zoom -= (float)yoffset;
    if (Zoom < 1.0f)
        Zoom = 1.0f;
    if (Zoom > 45.0f)
        Zoom = 45.0f; 
}

滚动时,yoffset值告诉我们垂直滚动的数量。当scroll_callback函数被调用时,我们改变全局声明的fov变量的内容。因为45.0是默认的fov值,所以我们想将缩放级别限制在1.0和45.0之间。

现在我们必须将透视投影矩阵上传到GPU的每一帧,但是这次使用fov变量作为它的视场:


projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);  

最后,不要忘记注册滚动回调函数:


glfwSetScrollCallback(window, scroll_callback); 

就是这样了。我们实现了一个简单的摄像系统,允许在3D环境中自由移动。

 

请随意进行一些试验,如果您无法完成,请将您的代码与源代码source code. 进行比较。

Camera class

在接下来的章节中,我们将经常使用相机来轻松地观察周围的场景,并从各个角度看到结果。然而,由于相机代码会在每一章占用大量的空间,我们将抽象它的细节,并创建我们自己的相机对象,用一些整洁的小额外部分为我们做大部分工作。不像Shader这一章,我们不会带你去创建camera类,但是如果你想知道内部工作方式,我们会提供(完全注释的)源代码。

像着色器对象一样,我们完全在一个头文件中定义camera类。你可以在这里here找到camera类;在本章之后,你应该能够理解代码。建议你至少检查一下这个课程,作为你如何创建自己的相机系统的例子。

我们介绍的摄像系统是一个飞行一样的相机,适合大多数用途,工作良好的欧拉角度,但要小心创建不同的摄像系统,如FPS相机,或飞行模拟相机。每个摄像系统都有它自己的技巧和怪癖,所以一定要仔细阅读。例如,这个飞行相机不允许俯仰值高于或等于90度,当我们考虑滚动值时,静态向上向量(0,1,0)不起作用。

使用新的camera对象的源代码的更新版本可以在这里here. 找到。

Exercises

 

  • See if you can transform the camera class in such a way that it becomes a true fps camera where you cannot fly; you can only look around while staying on the xz plane: solution.
  • Try to create your own LookAt function where you manually create a view matrix as discussed at the start of this chapter. Replace glm's LookAt function with your own implementation and see if it still acts the same: solution.

本文链接http://element-ui.cn/article/show-34389.aspx