> 技术文档 > 精通 C++游戏动画编程(OpenGL 和 Vulkan 的高级游戏动画技术) - 6 扩展相机处理功能

精通 C++游戏动画编程(OpenGL 和 Vulkan 的高级游戏动画技术) - 6 扩展相机处理功能

欢迎来到第 6 章 ! 在第 5 章中,我们实现了保存和加载应用程序配置的功能。首先,我们探讨了数据类型、文件格式以及需要保存到文件中的数据。然后,我们实现了一个解析器类,用于以 YAML 文件格式读写配置文件。在本章结束时,通过使用yaml-cpp库,所有模型、实例以及全局设置都被存储在一个 YAML 文件中,并且所有数据都可以被读回应用程序,使我们能够继续构建虚拟世界。

本章将优化相机配置。前两步将扩展应用程序以支持多相机操作,并添加多种相机类型。随后我们将实现第一人称和第三人称视角相机,仿照真实游戏中的选定实例。接着添加固定式相机,实现对虚拟场景的监控式观察。最后将添加热键切换相机功能,同时支持正交投影和基于鼠标滚轮的视野调整。

在本章中,我们将涵盖以下主题:

  • 添加多相机支持
  • 创建不同相机类型
  • 实现第一人称与第三人称相机
  • 添加固定摄像机
  • 切换不同摄像机及其配置

添加多台相机

在第 3 章中,我们在用户界面添加了一个按钮用于跳转到任意实例。但无论选择哪个实例,我们仍会以相同的角度和距离着陆,想要返回到地图上不同模型组成的完美构图几乎不可能。你可以记录摄像机参数或截图保存,但这远非理想方案。

能够在场景中添加几乎无限数量的相机,让我们可以创建令人惊叹的地图与模型组合,并随时返回该视角。通过添加不同类型的相机,我们还能实现更多功能——一个第三人称追踪实例的相机;另一个以等轴测视角展示整个地图的相机;甚至还有通过实例虚拟眼睛观察虚拟世界的相机——只需按下快捷键或选择菜单即可切换。

所有这些功能点都将在本章末尾实现。现在,让我们从第一步开始,为应用程序添加多个相机对象。

从单一相机到相机阵列

目前,我们的应用程序中仅有一个摄像头,定义在Camera类中,位于tools文件夹内。该摄像头提供了对虚拟世界的自由视角。我们可以在所有三个轴上移动,并围绕其中两个轴旋转。围绕指向屏幕内部的轴(横滚)旋转视角,对于模型和动画查看器应用来说目前意义不大,因为这样只会看到头部侧倾的效果。此外,在没有地平线等固定参考的情况下,在三维空间中导航摄像头可能会相当困难。因此,我们仅实现了上下移动(仰角)和围绕垂直轴的旋转(方位角)。升级摄像头旋转功能并添加围绕第三轴旋转的鼠标或键盘控制,将作为练习留给您来完成。

相机位置和两个旋转角度的值存储在 OpenGL 的OGLRenderData结构体中,以及 Vulkan 的VkRenderData结构体中:

float rdViewAzimuth = 330.0f;float rdViewElevation = -20.0f;glm::vec3 rdCameraWorldPosition = glm::vec3(2.0f, 5.0f, 7.0f);

为支持多相机,我们需要一个简单的 std::vector 类元素和一个 int 值,用于指明当前选中的相机。 由于这些设置更接近模型和模型实例而非渲染,我们将把新的相机向量存储在 ModelAndInstanceData 结构体中。 为匹配新内容,我们将把 ModelAndInstanceData 结构体重命名为 ModelInstanceCamData :

struct ModelInstanceCamData { ... std::vector<std::shared_ptr> micCameras{}; int micSelectedCamera = 0; ...

通过使用 IDE 的重构功能,重命名ModelAndInstanceData结构体及类和函数中的变量,仅需几次鼠标点击和文本编辑即可完成。

除了新的结构体名称外,我们还将文件从ModelAndInstanceData.h重命名为ModelInstanceCamData.h,并将文件从model文件夹移动到opengl文件夹(Vulkan 对应的是vulkan文件夹)。最终,头文件的存储位置是个人偏好的问题,但使用渲染器所在文件夹作为中心位置很有意义,因为我们主要从渲染器访问该结构体。

UserInterface类中,我们在名为ImGui::CollapsingHeaderCameras定义内添加了一个包含可用相机名称的组合框。组合框的代码可以从模型或动画剪辑选择中获取并调整。

提取相机设置

与实例设置类似,我们将主摄像头设置提取到一个名为CameraSettings的独立结构体中。使用包含摄像头变量的独立结构体,可以更便捷地一次性读取或应用所有摄像头相关设置,而无需通过 setter 和 getter 逐个访问。

CameraSettings结构体位于头文件CameraSettings.h中,该文件存放于tools文件夹:

struct CameraSettings{ std::string csCamName = \"Camera\"; glm::vec3 csWorldPosition = glm::vec3(0.0f); float csViewAzimuth = 0.0f; float csViewElevation = 0.0f};

除了摄像头名称外,我们从世界坐标位置和两个视角参数开始设置:方位角和仰角。

Camera类中,需要包含新的CameraSettings.h头文件,并添加名为mCamSettings的新私有成员变量。原先存储位置、方位角和仰角的三个旧变量可被移除。所有访问这三个位置和视角参数的方法都需要修改,改为在新mCamSettings变量中存储和检索数值。

我们需要为新的CameraSettings添加 getter 和 setter 方法。这些 getter 和 setter 将使我们能够像处理模型实例一样处理相机,通过简单的变量赋值来操作相机设置。

调整渲染器

由于渲染器需要更新相机的位置和视角,我们还需要升级一些方法来使用选定的相机。

第一步总是获取当前相机的指针并读取CameraSettings以便于访问和修改:

 std::shared_ptr cam = mModelInstCamData.micCameras.at( mModelInstCamData.micSelectedCamera); CameraSettings camSettings = cam->getCameraSettings();

如果我们更改了任何值,必须将设置存储回相机:

 cam->setCameraSettings(camSettings);

然后,在handleMousePositionEvents()方法中,我们从旧的mRenderData变量中更改所有变量,如下面代码所示:

 mRenderData.rdViewAzimuth += mouseMoveRelX / 10.0;

新的camSettings变量,包含新的相机设置,如下所示:

 camSettings.csViewAzimuth += mouseMoveRelX / 10.0f;

渲染器的draw()方法中也需要进行类似的更改。

首先,我们从渲染器类中移除私有的mCamera成员变量,因为我们将不再使用单一摄像头。然后,我们获取摄像头的指针并读取当前摄像头设置。

现在,摄像头的更新将不再通过旧的mCamera变量进行:

 mCamera.updateCamera(mRenderData, deltaTime);

取而代之的是,我们通过cam指针来更新当前选中的摄像头:

 cam->updateCamera(mRenderData, deltaTime);

对于投影矩阵,我们使用新的camSettings变量来读取当前配置的视场角:

 mProjectionMatrix = glm::perspective( glm::radians(static_cast( camSettings.csFieldOfView)), static_cast(mRenderData.rdWidth) / static_cast(mRenderData.rdHeight), 0.01f, 500.0f);

我们同样通过访问cam指针来读取更新后的视图矩阵:

 mViewMatrix = cam->getViewMatrix();

最后,在渲染器的centerInstance()方法中,对相机moveCameraTo()方法的调用也必须进行调整。我们不再使用旧的mCamera变量,如下方代码所示:

 mCamera.moveCameraTo(...);

现在,我们直接在micCameras向量中访问当前相机:

mModelInstCamData.micCameras.at( mModelInstCamData.micSelectedCamera)->moveCameraTo(...);

在此处提取指向当前相机的指针没有意义,因为这仅是对camera实例的单一操作。

将自由相机设为默认相机

与空模型和空实例类似,我们应确保micCameras向量中始终至少存在一个相机。避免空数组可以省去大量边界检查,而始终可用的自由相机在新配置或所有现有相机被移除后是个很好的功能。

为了简化默认的自由相机操作,将在渲染器类中添加一个名为loadDefaultFreeCam()的新方法:

void OGLRenderer::loadDefaultFreeCam() { mModelInstCamData.micCameras.clear();

首先,我们清空包含所有摄像头的向量。接着,我们创建一个具有默认值的新摄像头设置对象,将这些设置应用到摄像头上,并将该摄像头添加为首个实例:

 std::shared_ptr freeCam = std::make_shared(); CameraSettings freeCamSettings{}; freeCamSettings.csCamName = \"FreeCam\"; freeCamSettings.csWorldPosition = glm::vec3(5.0f); freeCamSettings.csViewAzimuth = 310.0f; freeCamSettings.csViewElevation = -15.0f; freeCam->setCameraSettings(freeCamSettings); mModelInstCamData.micCameras.emplace_back(freeCam); mModelInstCamData.micSelectedCamera = 0;}

您可以根据需要调整默认自由相机的设置。前面代码片段中显示的设置只是将世界坐标系的原点居中,使加载的第一个模型实例显示在屏幕中央。

最后,我们将选中的相机设置为零,即我们新添加相机的索引。

每当需要移除所有摄像头并添加默认摄像头时(例如创建新配置时),我们只需调用loadDefaultFreeCam()即可。

在用户界面中,当选中摄像头实例 0 时,应通过调用ImGui::BeginDisabled()ImGui::EndDisabled()来包裹名称字段,从而禁用对默认自由摄像头的名称修改。

图 6.1展示了摄像头部分的最终用户界面: