上一篇文章《渲染工作流与任务提交》对应的代码: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
坐标变换时图形学的第一个重难点,并且贯穿整个图形学和游戏引擎。我们看到的各种复杂的空间变换,本质都是坐标的变换。
如果你是图形学领域或者游戏领域的初学者,建议你花一两天时间,认真的把坐标变换的计算理解好,为后续的学习打一个好的基础。做到知其然,亦知其所以然。