Link

渲染架构已准备好加载完整场景,但如果我们有一个固定的相机,那就没什么用处了。让我们设置一个带有鼠标外观的交互式飞行相机,以便我们可以探索我们加载的关卡。

相机更像是一个游戏玩法层对象。我们将把它添加到 VulkanEngine 中,但在真实的架构中,您可能不希望在引擎本身内进行输入事件和游戏逻辑,而是只存储一个包含渲染参数的相机结构,当您更新游戏逻辑时,您会刷新这些矩阵,以便它们可以用于渲染。

Camera 结构看起来像这样。我们将它添加到 camera.h 中

#include <vk_types.h>
#include <SDL_events.h>

class Camera {
public:
    glm::vec3 velocity;
    glm::vec3 position;
    // vertical rotation
    float pitch { 0.f };
    // horizontal rotation
    float yaw { 0.f };

    glm::mat4 getViewMatrix();
    glm::mat4 getRotationMatrix();

    void processSDLEvent(SDL_Event& e);

    void update();
};

我们实际上不会保留矩阵存储,而是在需要时计算它。这样我们保证矩阵始终与参数同步。

我们保留一个速度向量来跟踪 WASD 按键状态,并可能稍后添加加速度。Position 包含相机在世界中的位置。

我们不会使用完整的旋转矩阵,而是只运行俯仰 + 偏航。这模仿了虚幻引擎相机之类的工作方式,并使其更容易处理 FPS 逻辑。

在函数方面,我们有一个 Update() 调用,它将按速度修改位置,以及一个充当输入逻辑的 processSDLEvent。

让我们在 camera.cpp 上编写这些

#include <camera.h>
#include <glm/gtx/transform.hpp>
#include <glm/gtx/quaternion.hpp>

void Camera::update()
{
    glm::mat4 cameraRotation = getRotationMatrix();
    position += glm::vec3(cameraRotation * glm::vec4(velocity * 0.5f, 0.f));
}

void Camera::processSDLEvent(SDL_Event& e)
{
    if (e.type == SDL_KEYDOWN) {
        if (e.key.keysym.sym == SDLK_w) { velocity.z = -1; }
        if (e.key.keysym.sym == SDLK_s) { velocity.z = 1; }
        if (e.key.keysym.sym == SDLK_a) { velocity.x = -1; }
        if (e.key.keysym.sym == SDLK_d) { velocity.x = 1; }
    }

    if (e.type == SDL_KEYUP) {
        if (e.key.keysym.sym == SDLK_w) { velocity.z = 0; }
        if (e.key.keysym.sym == SDLK_s) { velocity.z = 0; }
        if (e.key.keysym.sym == SDLK_a) { velocity.x = 0; }
        if (e.key.keysym.sym == SDLK_d) { velocity.x = 0; }
    }

    if (e.type == SDL_MOUSEMOTION) {
        yaw += (float)e.motion.xrel / 200.f;
        pitch -= (float)e.motion.yrel / 200.f;
    }
}

在计算位置更新时,我们正在使用旋转矩阵,这是因为速度将处于相机相对空间中,以便按下 W 向前移动。“向前”的含义取决于旋转。

在输入逻辑方面,SDL 为我们提供了输入事件,用于按键按下/释放和鼠标移动。我们将速度设置为按键按下事件的正确值,按键释放将其设置为 0。这并没有完全正确地处理输入,因为如果您同时按下 W 和 S,然后释放其中一个,移动就会停止,这会让人感觉有点尴尬。改进它是留给读者的练习。

对于鼠标移动,我们将鼠标的水平和垂直移动累积到俯仰和偏航属性中。

此代码中的移动是帧相关的,因为我们没有考虑引擎的速度。这样做是为了简单起见,如果您想改进它,您需要将 deltaTime(帧之间的时间)传递给 update() 函数,并将速度乘以它。在本教程中,由于我们在交换链中使用的选项,我们或多或少将 FPS 锁定到显示器速度,并且我们没有渲染足够的数据来减慢引擎速度。

矩阵函数看起来像这样

glm::mat4 Camera::getViewMatrix()
{
    // to create a correct model view, we need to move the world in opposite
    // direction to the camera
    //  so we will create the camera model matrix and invert
    glm::mat4 cameraTranslation = glm::translate(glm::mat4(1.f), position);
    glm::mat4 cameraRotation = getRotationMatrix();
    return glm::inverse(cameraTranslation * cameraRotation);
}

glm::mat4 Camera::getRotationMatrix()
{
    // fairly typical FPS style camera. we join the pitch and yaw rotations into
    // the final rotation matrix

    glm::quat pitchRotation = glm::angleAxis(pitch, glm::vec3 { 1.f, 0.f, 0.f });
    glm::quat yawRotation = glm::angleAxis(yaw, glm::vec3 { 0.f, -1.f, 0.f });

    return glm::toMat4(yawRotation) * glm::toMat4(pitchRotation);
}

对于旋转矩阵,我们正在计算 2 个四元数。一个将是水平旋转,另一个是垂直旋转。我们为此使用俯仰和偏航属性,并使用它来组合我们想要的旋转矩阵。

在视图矩阵上,我们通过组合旋转矩阵和平移矩阵来计算相机的最终变换矩阵。然后我们反转矩阵。这是因为要获得真实的相机矩阵,您所做的实际上不是移动相机,而是以与相机移动相反的方式移动世界,因为矩阵是在着色器中累积的。

这真的是全部内容,所以让我们将其挂钩到 VulkanEngine 类中

首先我们需要将相机添加为成员

#include <camera.h>

class VulkanEngine{

     Camera mainCamera;
}

现在我们将其挂钩到渲染器中。首先进入 run() 函数,我们将在其中挂钩 SDL 事件。

 // Handle events on queue
 while (SDL_PollEvent(&e) != 0) {
     // close the window when user alt-f4s or clicks the X button
     if (e.type == SDL_QUIT)
         bQuit = true;

     mainCamera.processSDLEvent(e);
     ImGui_ImplSDL2_ProcessEvent(&e);
 }

接下来,在 update_scene() 函数中,我们添加相机逻辑以更新并将相机矩阵复制到渲染器 sceneData 结构中。

void VulkanEngine::update_scene()
{
    mainCamera.update();

    glm::mat4 view = mainCamera.getViewMatrix();

    // camera projection
    glm::mat4 projection = glm::perspective(glm::radians(70.f), (float)_windowExtent.width / (float)_windowExtent.height, 10000.f, 0.1f);

    // invert the Y direction on projection matrix so that we are more similar
    // to opengl and gltf axis
    projection[1][1] *= -1;

    sceneData.view = view;
    sceneData.proj = projection;
    sceneData.viewproj = projection * view;
}

从 init() 函数的末尾,我们将设置相机的初始变量。相机将朝向原点,因此 0,0,0 坐标处的网格应该是可见的。

mainCamera.velocity = glm::vec3(0.f);
	mainCamera.position = glm::vec3(0, 0, 5);

mainCamera.pitch = 0;
mainCamera.yaw = 0;

就是这样,我们现在有了一个基本的交互式相机。尝试使用它并环顾四周,您将能够看到我们之前在世界中拥有的网格,使用 WASD 移动相机,并使用鼠标环顾四周。

下一步: GLTF 场景节点