游戏引擎从零开始(19)-正交相机

上一篇文章《渲染工作流与任务提交》对应的代码:renderer flow & command commit

前言

3D空间坐标系和相机的实现,往深了讲,内容还挺多的,一篇文章内无法一一道明。对OpenGL的坐标系、Camera不熟悉的读者,建议读一读下面的文章。

参考文章:

本章节主要讲工程与代码实现。

相机(camera)

相机表示人眼的视角,从正面和侧面看一个场景,能看到的画面是不一样的。实际上在OpenGL中,并没有Camera的概念,移动相机的视角,本质上是将场景中所有的元素反方向移动、旋转。

所以,Camera具体是什么呢?是一个矩阵,描述场景元素旋转、位移的4x4的矩阵。

最常见的相机有两种:正交相机和透视相机。透视相机符合人眼"近大远小"的特征,平行线在远处相汇于一点。

相比透视相机正交相机更简单一些,一般用于2D显示(辅助信息),比如游戏中,人物的血条不会随着人物距离变大变小。

加入Camera后的整体流程

之前的绘制流程不变,Renderer通过Camera获取变换矩阵ViewProjectionMatrix,之后在绘制阶段设置到Shader中。

  • beginScene:准备相机参数
  • 第一个Submit:更新Shader、提交顶点数据
  • 第二个Submit:更新Shader、提交顶点数据
  • ...
  • EndScene(现在为空实现)

流程示意图如下:

正交相机(OrthographicCamera)实现

正交相机实际上是由"正交投影矩阵(m_ProjectionMatrix)"和"相机变换矩阵(m_ViewMatrix)"两个矩阵相乘得到(m_ViewProjectionMatrix)。

完整的坐标变换按先后顺序,包含模型变换(model)、相机变换(camera/view)、投影变换(projection),简称MVP变换。这里我们还没有涉及model变换。

OrthographicCamera的封装就是为了方便的获取m_ViewProjectionMatrix。

正交矩阵由left、right、top、bottom、near、far等6个参数定义,定义的是一个立方体的6个面,只有这个立方体里的元素能被绘制到屏幕,如下图所示:

这里开始涉及一些图形学的知识了,不熟悉的建议回到文章开头读读我推荐的坐标和相机的文章。

OrthographicCamera中定义了位移Position和旋转Rotation。这里我们暂时仅实现2D的渲染器,所以旋转只有个一个float值,表示我们只能在xy平面内旋转,即只能关于z轴旋转(z轴与屏幕垂直)。

每次修改了位移和Rotation都会调用RecalculateViewMatrix()更新相机矩阵。

一般我们把模型的变换(model)、相机变换(也叫view变换)、投影变换(projection)统称为mvp矩阵

头文件和定义如下:

Sandbox/Hazel/src/Hazel/Renderer/OrthographicCamera.h

c++ 复制代码
#pragma once

#include <glm/glm.hpp>
namespace Hazel {
    class OrthographicCamera {
    public:
        OrthographicCamera(float left, float right, float bottom, float top);

        const glm::vec3& GetPosition() const {return m_Position;}
        void SetPosition(const glm::vec3& position){
            m_Position = position;
            RecalculateViewMatrix();
        }

        float GetRotation() const {return m_Rotation;}
        void SetRotation(float rotation) {
            m_Rotation = rotation;
            RecalculateViewMatrix();
        }

        const glm::mat4& GetProjectionMatrix() const {return m_ProjectionMatrix;}
        const glm::mat4& GetViewMatrix() const {return m_ViewMatrix;}
        const glm::mat4& GetViewProjectionMatrix() const {return m_ViewProjectionMatrix;}

    private:
        void RecalculateViewMatrix();
    private:
        glm::mat4 m_ProjectionMatrix;
        glm::mat4 m_ViewMatrix;
        glm::mat4 m_ViewProjectionMatrix;

        glm::vec3 m_Position = {0.0f, 0.0f, 0.0f};
        float m_Rotation = 0.0f;
    };

}

glm库中有现成的生成正交矩阵的方法glm::ortho。完整的正交相机投影矩阵为:

m_ViewProjectionMatrix = m_ProjectionMatrix * m_ViewMatrix

矩阵乘法为从右往左计算。每次更新相机的位移和旋转时,都需要重新计算,计算的顺序先算右边的camera矩阵(m_ViewMatrix),注意结果要求逆,因为最后矩阵是反方向作用在场景元素上。

Sandbox/Hazel/src/Hazel/Platform/OpenGL/OpenGLRendererAPI.cpp

c++ 复制代码
#include "OrthographicCamera.h"
#include <glm/gtc/matrix_transform.hpp>

namespace Hazel {

    OrthographicCamera::OrthographicCamera(float left, float right, float bottom, float top)
    : m_ProjectionMatrix(glm::ortho(left, right, bottom, top, -1.0f, 1.0f)), m_ViewMatrix(1.0f)
    {
        m_ViewProjectionMatrix = m_ProjectionMatrix * m_ViewMatrix;
    }

    void OrthographicCamera::RecalculateViewMatrix() {
        glm::mat4 transform = glm::translate(glm::mat4(1.0f), m_Position) *
                glm::rotate(glm::mat4(1.0f), glm::radians(m_Rotation), glm::vec3(0, 0, 1));
        m_ViewMatrix = glm::inverse(transform);
        m_ViewProjectionMatrix = m_ProjectionMatrix * m_ViewMatrix;
    }
}

Shader中增加相机矩阵

Shader中更新mvp矩阵(m:model,v:view/camera,p,投影矩阵),这里我们还没有对model进行变换,仅有view、projection。

Sandbox/Hazel/src/Hazel/Renderer/Shader.h

c++ 复制代码
#include <glm/glm.hpp>
...
class Shader {
public:
    ...
    void UploadUniformMat4(const std::string& name, const glm::mat4& matrix);

};

Sandbox/Hazel/src/Hazel/Renderer/Shader.cpp

c++ 复制代码
...
#include <glm/gtc/type_ptr.hpp>
...
void Shader::UploadUniformMat4(const std::string &name, const glm::mat4 &matrix) {
    GLint location = glGetUniformLocation(m_RendererID, name.c_str());
    glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(matrix));
}

注意matrix要通过glm::value_ptr转成指针的形式。

Renderer里增加Camera

对Renderer里的代码,同步做一些调整,加上camera的逻辑,增加结构体SceneData描述场景属性和数据,暂时仅有相机投影矩阵: Sandbox/Hazel/src/Hazel/Renderer/Renderer.h

c++ 复制代码
#pragma once

#include "RendererAPI.h"
#include "OrthographicCamera.h"
#include "Shader.h"

namespace Hazel {


    class Renderer {
    public:

        static void BeginScene(OrthographicCamera& camera);
        static void EndScene();
        static void Submit(const std::shared_ptr<Shader>& shader, const std::shared_ptr<VertexArray>& vertexArray);

        inline static RendererAPI::API GetAPI() {
            return RendererAPI::GetAPI();
        }

        struct SceneDatta {
            glm::mat4 ViewProjectionMatrix;
        };

        static SceneDatta* m_SceneData;
    };
}
c++ 复制代码
#include "./RenderCommand.h"

namespace Hazel{
    Renderer::SceneDatta* Renderer::m_SceneData = new Renderer::SceneDatta;
    void Renderer::BeginScene(OrthographicCamera(& camera)) {
        m_SceneData->ViewProjectionMatrix = camera.GetViewProjectionMatrix();
    }

    void Renderer::Submit(const std::shared_ptr<Shader>& shader, const std::shared_ptr<VertexArray> &vertexArray) {
        shader->Bind();
        shader->UploadUniformMat4("u_ViewProjection", m_SceneData->ViewProjectionMatrix);
        vertexArray->Bind();
        RenderCommand::DrawIndexed(vertexArray);
    }
    ...
}

Application增加Camera

c++ 复制代码
...
#include "Renderer/OrthographicCamera.h"

namespace Hazel{
class Application {
    ...
    OrthographicCamera m_Camera;
    ...

Application.cpp中调整的比较多,主要修改了VertexSrc,增加了uniform变量u_ViewProjection,另外调整了Submit的逻辑,将shader绑定和更新viewProjection的逻辑封装到Submit中了,代码显的更内聚了。

Sandbox/Hazel/src/Hazel/Application.cpp

c++ 复制代码
    Application::Application() : m_Camera(-1.6f, 1.6f, -0.9f, 0.9f)
    {
        s_Instance = this;
        m_Window = std::unique_ptr<Window>(Window::Create());
        // 绑定类成员函数,需要传this
	@@ -71,13 +72,16 @@ namespace Hazel{
            #version 330 core
            layout(location = 0) in vec3 a_Position;
            layout(location = 1) in vec4 a_Color;
            uniform mat4 u_ViewProjection;
            out vec3 v_Position;
            out vec4 v_Color;
            void main()
            {
                v_Position = a_Position;
                v_Color = a_Color;
                gl_Position = u_ViewProjection * vec4(a_Position, 1.0);
            }
        )";

	@@ -99,11 +103,14 @@ namespace Hazel{
        std::string blueShaderVertexSrc = R"(
            #version 330 core
            layout(location = 0) in vec3 a_Position;
            uniform mat4 u_ViewProjection;
            out vec3 v_Position;
            void main() {
                v_Position = a_Position;
                gl_Position = u_ViewProjection * vec4(a_Position, 1.0);
            }
        )";

	@@ -166,14 +173,12 @@ namespace Hazel{
            RenderCommand::SetClearColor({0.45f, 0.55f, 0.60f, 1.00f});
            RenderCommand::Clear();

            m_Camera.SetPosition({0.5f, 0.5f, 0.0f});
            m_Camera.SetRotation(45.0f);

            Renderer::BeginScene(m_Camera);
            Renderer::Submit(m_BlueShader,m_SquareVA);
            Renderer::Submit(m_Shader, m_VertexArray);

            Renderer::EndScene();

Application中设置了camera的position和旋转

c++ 复制代码
m_Camera.SetPosition({0.5f, 0.5f, 0.0f});
m_Camera.SetRotation(45.0f);

相机对着0.5、0.5的坐标,则相当于绘制的元素往左移动了0.5。

代码运行正常的话,能看到场景发生了位移和旋转。

完整代码&总结

完整代码修改参考:add Orthographic camera

坐标变换时图形学的第一个重难点,并且贯穿整个图形学和游戏引擎。我们看到的各种复杂的空间变换,本质都是坐标的变换。

如果你是图形学领域或者游戏领域的初学者,建议你花一两天时间,认真的把坐标变换的计算理解好,为后续的学习打一个好的基础。做到知其然,亦知其所以然。

相关推荐
Ysjt | 深1 小时前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++
ephemerals__1 小时前
【c++丨STL】list模拟实现(附源码)
开发语言·c++·list
Microsoft Word2 小时前
c++基础语法
开发语言·c++·算法
一只小小汤圆2 小时前
opencascade源码学习之BRepOffsetAPI包 -BRepOffsetAPI_DraftAngle
c++·学习·opencascade
legend_jz2 小时前
【Linux】线程控制
linux·服务器·开发语言·c++·笔记·学习·学习方法
嘿BRE3 小时前
【C++】几个基本容器的模拟实现(string,vector,list,stack,queue,priority_queue)
c++
ö Constancy3 小时前
c++ 笔记
开发语言·c++
fengbizhe4 小时前
笔试-笔记2
c++·笔记
徐霞客3204 小时前
Qt入门1——认识Qt的几个常用头文件和常用函数
开发语言·c++·笔记·qt