[OpengGL] 摄像机[10]

英文原文:https://learnopengl.com/Getting-started/Camera

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

  在本章中,我们将讨论如何在 OpenGL 中设置相机。 我们将讨论一种可让您在 3D 场景中自由移动的飞行式相机。 我们还将讨论键盘和鼠标输入,并以自定义相机类结束。

相机/观察空间

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

1. 相机位置

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

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

不要忘记正 z 轴穿过屏幕朝向您,因此如果我们希望相机向后移动,我们将沿着正 z 轴移动。

2. 相机方向

  下一个需要的矢量是相机的方向,例如 它指向什么方向。 现在我们让相机指向场景的原点:(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.右轴

  我们需要的下一个向量是一个右向量,它表示相机空间的正 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.上轴

  现在我们有了 x 轴向量和 z 轴向量,检索指向相机正 y 轴的向量就相对容易了:我们取右向量和方向向量的叉积:

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

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

Look At

  矩阵的一大优点是,如果您使用 3 个垂直(或非线性)轴定义一个坐标空间,您可以创建一个包含这 3 个轴和一个平移向量的矩阵,并且您可以通过将任何向量与 这个矩阵。 这正是 LookAt 矩阵所做的,现在我们有 3 个垂直轴和一个位置向量来定义相机空间,我们可以创建我们自己的 LookAt 矩阵:

在这里插入图片描述
  其中 R 是右向量,U 是上向量,D 是方向向量,P 是相机的位置向量。 请注意,旋转(左矩阵)和平移(右矩阵)部分是反转的(分别转置和取反),因为我们想要在与我们希望相机移动的方向相反的方向上旋转和平移世界。 使用这个 LookAt 矩阵作为我们的视图矩阵可以有效地将所有世界坐标转换为我们刚刚定义的视图空间。 然后 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));

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

  在深入研究用户输入之前,让我们先通过围绕场景旋转相机来获得一点时髦。 我们将场景的目标保持在 (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));

  如果你运行这段代码,你应该得到这样的东西:

https://learnopengl.com/video/getting-started/camera_circle.mp4

  通过这个小代码片段,相机现在会随着时间的推移围绕场景旋转。 随意尝试半径和位置/方向参数,以了解此 LookAt 矩阵的工作原理。 另外,如果遇到困难,请检查源代码

随便走走

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

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。

移动速度

  目前,我们在四处走动时使用恒定值作为移动速度。 理论上这似乎很好,但实际上人们的机器具有不同的处理能力,其结果是有些人每秒能够渲染比其他人多得多的帧。 每当一个用户比另一个用户渲染更多的帧时,他也会更频繁地调用 processInput。 结果是有些人移动得非常快,有些人移动得非常慢,具体取决于他们的设置。 在发布您的应用程序时,您希望确保它在所有类型的硬件上运行相同。

  图形应用程序和游戏通常会跟踪一个 deltatime 变量,该变量存储渲染最后一帧所花费的时间。 然后我们将所有速度与这个 deltaTime 值相乘。 结果是,当我们在一帧中有一个大的 deltaTime 时,这意味着最后一帧花费的时间比平均时间长,该帧的速度也会稍微高一点以平衡它。 使用这种方法时,无论您的电脑速度非常快还是非常慢都没有关系,相机的速度会相应地得到平衡,因此每个用户都会有相同的体验。

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

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;
    [...]
}

  由于我们使用的是 deltaTime,因此摄像机现在将以每秒 2.5 个单位的恒定速度移动。 连同上一节,我们现在应该有一个更流畅、更一致的相机系统来在场景中移动:

https://learnopengl.com/video/getting-started/camera_smooth.mp4

  现在我们有了一台在任何系统上都能同样快速移动和观看的相机。 同样,如果您遇到困难,请检查源代码。 我们会看到 deltaTime 值经常返回与任何相关的运动。

环视四周

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

  要环顾场景,我们必须根据鼠标输入更改 cameraFront 向量。 然而,根据鼠标旋转改变方向向量有点复杂,需要一些三角学知识。 如果您不了解三角函数,请不要担心,您可以直接跳到代码部分并将它们粘贴到您的代码中; 如果您想了解更多,可以随时回来。

欧拉角

  欧拉角是 3 个值,可以表示 3D 中的任何旋转,由 Leonhard Euler 在 1700 年代的某个地方定义。 欧拉角有 3 个:俯仰角(pitch)、偏航角(yaw)和滚转角(roll)。 下图赋予了它们视觉上的意义:
在这里插入图片描述
  俯仰是描述我们在第一张图片中看到的向上或向下看多少的角度。 第二张图片显示偏航值,代表我们向左或向右看的幅度。 滚动表示我们在太空飞行相机中主要使用的滚动量。 每个欧拉角都由一个值表示,结合所有 3 个欧拉角,我们可以计算 3D 中的任何旋转矢量。

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

让我们先复习一下,看看一般的直角三角形情况(一边呈 90 度角):

在这里插入图片描述
  如果我们将斜边定义为长度 1,我们从三角学 (soh cah toa) 中知道相邻边的长度是 cos x/h=cos x/1=cos x 而对边的长度是 sin y/h=sin y/1=正弦y。 这为我们提供了一些通用公式,用于根据给定的角度检索直角三角形的 x 和 y 边的长度。 让我们用它来计算方向向量的分量。

  让我们想象一下同一个三角形,但现在从顶部视角看它,相邻边和相对边平行于场景的 x 轴和 z 轴(就像向下看 y 轴一样)。
在这里插入图片描述
  如果我们将偏航角可视化为从 x 侧开始的逆时针角度,我们可以看到 x 侧的长度与 cos(yaw) 相关。 同样,z 边的长度与 sin(yaw) 的关系。

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

glm::vec3 direction;
direction.x = cos(glm::radians(yaw)); // 请注意,我们先将角度转换为弧度
direction.z = sin(glm::radians(yaw));

  这解决了我们如何从偏航值获得 3D 方向向量的问题,但还需要包括俯仰角。 现在让我们看看 y 轴一侧,就像我们坐在 xz 平面上一样:
在这里插入图片描述
  类似地,从这个三角形我们可以看到方向的 y 分量等于 sin(pitch) 所以让我们填写:

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

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

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

  这为我们提供了一个公式,可将偏航和俯仰值转换为可用于环顾四周的 3 维方向向量。

  我们已经设置了场景世界,所以一切都定位在负 z 轴的方向。 但是,如果我们查看 x 和 z 偏航三角形,我们会看到 θ 为 0 会导致相机的方向矢量指向正 x 轴。 为确保相机默认指向负 z 轴,我们可以为偏航指定顺时针旋转 90 度的默认值。 正度数逆时针旋转,因此我们将默认偏航值设置为:

yaw = -90.0f;

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

鼠标输入

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

  首先,我们将告诉 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);  

  在为飞行式相机处理鼠标输入时,在我们能够完全计算相机的方向向量之前,我们必须采取几个步骤:

  1. 计算自上一帧以来鼠标的偏移量。
  2. 将偏移值添加到相机的偏航和俯仰值。
  3. 对最小/最大间距值添加一些约束。
  4. 计算方向向量。

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

float lastX = 400, lastY = 300;

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

float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // 反转,因为 y 坐标范围从底部到顶部
lastX = xpos;
lastY = ypos;

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

  请注意,我们将偏移值乘以灵敏度值。 如果我们省略这个乘法,鼠标移动会太强烈; 根据自己的喜好摆弄灵敏度值。

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

yaw   += xoffset;
pitch += yoffset;  

  在第三步中,我们想为相机添加一些约束,这样用户就无法进行奇怪的相机移动(一旦方向向量与世界向上方向平行,也会导致 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 函数中,我们可以开始了。

  如果您现在运行代码,您会注意到只要窗口第一次接收到鼠标光标的焦点,相机就会突然跳动。 这种突然跳转的原因是,一旦您的光标进入窗口,就会调用鼠标回调函数,其 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)
{
    fov -= (float)yoffset;
    if (fov < 1.0f)
        fov = 1.0f;
    if (fov > 45.0f)
        fov = 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 环境中自由移动。

在这里插入图片描述
随意尝试一下,如果您遇到困难,请将您的代码与源代码进行比较。

相机类

  在接下来的章节中,我们将始终使用相机轻松地环顾场景并从各个角度查看结果。 然而,由于相机代码会在每一章中占用大量空间,我们将对其细节进行一些抽象,并创建我们自己的相机对象,它可以为我们完成大部分工作,并提供一些简洁的额外功能。 与着色器章节不同,我们不会引导您创建相机类,但如果您想了解内部工作原理,则会为您提供(完全注释的)源代码。

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

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

可以在此处找到使用新相机对象的源代码的更新版本。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值