上一章我们完成了:

  • RendererAPI层的抽象
  • RendererAPI中对于OpenGL的特例化
  • Renderer中对于RendererAPI的调用
  • Application对于Renderer内容的替换

代码已上传至我的github

drunklx/XEngine: my GameEngine

这一章我们来完成:

  • 正交相机的实现
  • 沙盒迁移

正交相机的实现

正交相机的定义

正交相机(Orthographic Camera)是一种在计算机图形学和3D渲染中常用的投影方式。与透视相机不同,正交相机不会产生近大远小的透视效果,所有物体无论距离远近都以相同的大小呈现。这种投影方式通常用于工程制图、CAD设计或2D游戏开发。

正交相机的特点

  • 无透视变形:所有平行线在投影后仍保持平行,不会因距离而收敛。
  • 均匀缩放:物体的大小不受距离影响,适合需要精确尺寸表现的场景。
  • 投影矩阵简单:通过定义视景体的左右、上下、近远平面直接映射到屏幕空间。

正交相机的应用场景

  • 工程制图:用于机械设计、建筑蓝图等需要精确尺寸的场景。
  • 2D游戏开发:在横版或俯视角游戏中保持物体比例一致。
  • UI渲染:在3D场景中渲染UI元素时避免透视变形。

正交相机的参数设置

正交相机的视景体通常由以下参数定义:

  • left/right:视景体的左右边界(X轴范围)。
  • top/bottom:视景体的上下边界(Y轴范围)。
  • near/far:视景体的近远裁剪平面(Z轴范围)。

在代码中(如OpenGL或Unity),正交相机的投影矩阵可通过以下公式生成:
\text{OrthoMatrix}= \begin{bmatrix} \frac{2}{\text{right}-\text{left}} & 0 & 0 & - \frac{right+left}{right-left} \\0&\frac{2}{top-bottom}&0& -\frac{top+bottom}{top-bottom} \\0&0&\frac{-2}{far-near}&-\frac{far+near}{far-near} \\0&0&0&1 \end{bmatrix}   

如果对投影矩阵推导感兴趣的话可以参考这篇文章。

OpenGL Projection Matrixhow to construct OpenGL projection matrixhttp://www.songho.ca/opengl/gl_projectionmatrix.html

正交相机与透视相机的对比

特性 正交相机 透视相机
投影效果 无透视变形,物体大小一致 近大远小,符合人眼视觉
适用场景 精确尺寸表现(如CAD) 真实感渲染(如3D游戏)
矩阵复杂度 线性变换,简单 非线性变换,需透视除法

实现示例(Unity引擎)

在Unity中设置正交相机:

  1. 选择摄像机对象,在Inspector面板中将Projection模式改为Orthographic
  2. 调整Size参数控制视景体高度(宽度自动根据屏幕比例计算)。
// 脚本动态调整正交相机大小
Camera.main.orthographic = true;
Camera.main.orthographicSize = 5; // 视景体高度的一半

从上面来看,要构建一个正交矩阵,我们需要上下左右前后,我们将条件限定一下,现在是一个平面,我们从z轴向里看,能看到xy平面,然后规定我们的相机只能在z轴上进行旋转。作为一个2D摄像机来使用,这样就无需考虑远近屏幕了。

作为相机它需要存储自己的透视矩阵(ProjectionMetrix)以及视野矩阵(ViewMetrix),因此需要对这两者进行get&set。同时通过二者相乘得到相机矩阵。我们能够获取到它的位置,由于它只能绕z轴旋转,因此,它的方向向量永远是(0,0,-1),而我们只需要知道它的具体位置就能够计算出它的视野矩阵,而透视矩阵是一创建就确定好的,后续也可以通过改变它的长宽来改变。

对于视野矩阵的云云,可以参考LearnOpenGL的文章。

摄像机 - LearnOpenGL CNhttps://learnopengl-cn.github.io/01%20Getting%20started/09%20Camera/

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


	private:
		glm::vec3 m_Position = { 0.0f, 0.0f, 0.0f };
		float m_Rotation = 0.0f;
		glm::mat4 m_ProjectionMatrix;
		glm::mat4 m_ViewMatrix;
		glm::mat4 m_ViewProjectionMatrix;
	public:
		inline const glm::mat4& GetProjectionMatrix() const { return m_ProjectionMatrix; }
		inline const glm::mat4& GetViewMatrix() const { return m_ViewMatrix; }
		inline const glm::mat4& GetViewProjectionMatrix() const { return m_ViewProjectionMatrix; }
		void SetProjection(float left, float right, float bottom, float top);
		void SetPosition(const glm::vec3& position);
		void SetRotation(float rotation);
	private:
		void UpdateViewProjectionMatrix();
	};

}
OrthographicCamera.cpp
#include "xepch.h"
#include "OrthographicCamera.h"
#include <glm/gtx/transform.hpp>
namespace XEngine
{
	OrthographicCamera::OrthographicCamera(float left, float right, float bottom, float top)
		: m_Rotation(0.0f), m_ProjectionMatrix(glm::ortho(left, right, bottom, top)), m_ViewMatrix(1.0f)
	{
		m_ViewProjectionMatrix = m_ProjectionMatrix * m_ViewMatrix;
	}
	void OrthographicCamera::SetProjection(float left, float right, float bottom, float top)
	{
		m_ProjectionMatrix = glm::ortho(left, right, bottom, top);
		m_ViewProjectionMatrix = m_ProjectionMatrix * m_ViewMatrix;
	}
	void OrthographicCamera::SetPosition(const glm::vec3& position)
	{
		m_Position = position;
		UpdateViewProjectionMatrix();
	}
	void OrthographicCamera::SetRotation(float rotation)
	{
		m_Rotation = rotation;
		UpdateViewProjectionMatrix();
	}
	void OrthographicCamera::UpdateViewProjectionMatrix()
	{
		glm::mat4 transform = glm::translate(glm::mat4(1.0f), m_Position) *
			glm::rotate(glm::mat4(1.0f), glm::radians(m_Rotation), glm::vec3(0.0f, 0.0f, 1.0f));
		m_ViewMatrix = glm::inverse(transform);

		m_ViewProjectionMatrix = m_ProjectionMatrix * m_ViewMatrix;
	}
}

glm库的方法很强大,比如ortho函数直接生成投影矩阵,以及通过translate生成位移矩阵,通过rotate函数生成旋转矩阵等。注意这里在计算完transform矩阵后,调用了glm::inverse,要将矩阵求逆。为什么要将矩阵求逆呢,因为我们始终是假设矩阵是禁止的,那么对于相机的一切运动,物体都是相当于相对于相机往反方向运动,因此要将物体变换到相机坐标系就需要对viewmatrix求逆。

然后我们对这个相机进行适配,将它应用到我们的代码中。

Renderer.h
#pragma once
#include "RendererCommand.h"
#include "OrthographicCamera.h"
#include "Shader.h"
namespace XEngine
{

	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(); }			//注意静态函数使用时的作用域标识
	private:		//注意静态函数使用时的作用域标识
		struct SceneData {
			glm::mat4 ViewProjectionMatrix;
		};
		static SceneData* m_SceneData;
	};
}
Renderer.cpp
#include "xepch.h"
#include "Renderer.h"

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

	void Renderer::EndScene()
	{
	}

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

}

在渲染器中,我们将相机传入,获取相机的VP矩阵,这样就可以在渲染时,将VP矩阵的值传递给着色器的顶点着色器进行渲染。我们在着色器中新增通过名称设置Uniform的方法,基本上就是调用一下OpenGL的接口设置着色器的Uniform变量。

然后在application中存储我们的相机以及应用,因为只有几行代码,这里就不做展示了。

测试一下,完美。

可以通过改变相机的位置来改变渲染效果。

改变相机长度,图像变窄了。

改变相机宽度,图像变胖了。

改变相机中心,图像被移出去了哈哈哈。

沙盒迁移

之前在application中临时引入了那些库以及写了那些渲染功能,现在我们要全部迁移到Sandbox里去,保证我们的application只做他该做的事。

我们将对Renderer中的引入放在了XEngine的头文件中,然后把之前渲染的功能都移到了Sandbox中ExampleLayer内部。

Sandbox.cpp/ExampeLayer.cpp

void OnUpdate() override {
	//X_TRACE("ExampleLayer::OnUpdate");
	XEngine::RendererCommand::Clear();
	XEngine::RendererCommand::SetClearColor({ 0.1f,0.1f,0.1f,1.0f });
	m_Camera.SetPosition(m_CameraPosition);
	m_Camera.SetRotation(m_CameraRotation);

	XEngine::Renderer::BeginScene(m_Camera);
	XEngine::Renderer::Submit(blueShader, std::shared_ptr<XEngine::VertexArray>(squVAO));
	XEngine::Renderer::Submit(m_Shader, m_VertexArray);
	XEngine::Renderer::EndScene();
}

然后运行看一下效果

嗯,完美。

相机的移动与旋转

我们在Sandbox中实现一下通过按键实现相机的移动。

通过在ExampleLayer中,使用变量存储一下我的相机的变量(因为Position和Rotation是私有的,我们没办法直接修改,只能通过Set来进行修改)。

然后在update函数中实现一下。

if (XEngine::Input::IsKeyPressed(X_KEY_UP)) {
	m_CameraPosition.y+= m_CameraMoveSpeed;
}
else if (XEngine::Input::IsKeyPressed(X_KEY_DOWN))
{
	m_CameraPosition.y-= m_CameraMoveSpeed;
}
if (XEngine::Input::IsKeyPressed(X_KEY_RIGHT))
{
	m_CameraPosition.x += m_CameraMoveSpeed;
}
else if (XEngine::Input::IsKeyPressed(X_KEY_LEFT))
{
	m_CameraPosition.x -= m_CameraMoveSpeed;
}
if (XEngine::Input::IsKeyPressed(X_KEY_A))
{
	m_CameraRotation -= m_CameraRotateSpeed;
}
else if (XEngine::Input::IsKeyPressed(X_KEY_D))
{
	m_CameraRotation += m_CameraRotateSpeed;
}
m_Camera.SetPosition(m_CameraPosition);
m_Camera.SetRotation(m_CameraRotation);

基本上就是修改一下Camera的属性,然后基于我们之前的实现,它可以自动更新VP矩阵。

测试一下。

正交相机测试

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐