目录
一、逻辑分析
1.1正交投影相机与透视投影相机
1.2轨迹球相机控制和游戏相机控制
二、相机(Camera)的设计
2.1Camera基类设计
2.2正交投影相机(OrthographicCamera)的设计
2.3透视投影相机(PerspectiveCamera)的设计
三、相机控制(Camera Control)的设计
3.1相机控制基类(CameraControl)的设计
3.2轨迹球相机控制(TrackballCameraControl)类设计
3.3游戏相机控制(GameCameraControl)类的设计
前言:刚学OpenGL,主要想要记录下学到的东西,也当作一个笔记,部分理解可能有偏差,也是不全面的,如果有发现问题的话也很高兴大家可以指正,我会尽快修改的,在后续学习过程中,也会进行相应的补充与修改。(课件截图来源于bilibili赵新政老师)
(OpenGL老师:赵新政的个人空间-赵新政个人主页-哔哩哔哩视频)
一、逻辑分析
1.1正交投影相机与透视投影相机
首先知道,相机会与两个关键量相关,分别是视图变换矩阵与投影矩阵,这两个变量决定了点的最终位置。因而,我们肯定需要从相机中去获得这两个变量。
视图变换变换矩阵与相机的位置,朝向,与穹顶相关,投影矩阵又分为正交投影和透视投影,但是两种投影矩阵的计算与相机本身的位置和朝向并无直接关联。那么,相机就可以分出两种相机,一个是正交投影相机,另一个则是透视投影相机。
在实现层面,我们就可以充分利用C++的多态特性,以Camera为基类,去分别实现OrthographicCamera和PerspectiveCamera。其中Camera持有相机的基础位置信息与方向信息,对外暴露获取视图矩阵与投影矩阵的接口。而派生出来的相机则需要持有构建相机所需要的基础数据,并重写获取投影矩阵的接口。
1.2轨迹球相机控制和游戏相机控制
摄像机确定了之后,便需要有一个控制器CameraControl去控制摄像机。而摄像机的变换是与当前的鼠标键盘等操作相关的,因而必须要记录下当前的按键与鼠标信息。同时提供一个on_update接口,来在程序运行的每一帧当中更新摄像机。
轨迹球相机控制:在3D建模、场景编辑的过程中,往往会需要相机围绕着一个目标点进行旋转、缩放、平移。
游戏相机控制:基于玩家的移动而移动(部分场景也会根据剧情或脚本自动运动,如过场动画镜头)
这里也是相同的实现思路,以CameraControl为基类,分别再去实现TrackballCameraControl和GameCameraControl的内容,在里面实现其对应的pitch,yaw逻辑与按键控制逻辑
二、相机(Camera)的设计
2.1Camera基类设计
从相机的基本信息讲,需要包含他的位置与本地坐标系(分别是up轴,right轴,front轴),而实际上front轴可以通过up向量与right向量叉乘得到,所以只需要记录其中两个向量(这里使用up和right)即可。
从相机的功能角度讲,我们需要从中获取视图矩阵(ViewMatrix)与投影矩阵(Projection),这对于任何相机而言都是一样的,所以必须要对外获取这两个矩阵的接口。此外,相机同时可以拥有放缩的功能,外界想要进行放缩就必须用到摄像机的相关信息,因而也需要提供进行放缩的接口。
其中,投影矩阵与放缩的逻辑因投影方式而异,需要设计成虚函数,再在具体的相机中进行实现
camera.h
#pragma once
#include"../../glframework/core.h"
class Camera
{
public:
Camera();
~Camera();
glm::mat4 get_view_matrix();
virtual glm::mat4 get_projection_matrix();
virtual void scale(float delta_scale);
public:
glm::vec3 m_position{ 0.0f, 0.0f, 100.0f };
glm::vec3 m_up{ 0.0f, 1.0f, 0.0f };
glm::vec3 m_right{ 1.0f, 0.0f, 0.0f };
};
camera.cpp
#include"camera.h"
Camera::Camera()
{
}
Camera::~Camera()
{
}
glm::mat4 Camera::get_view_matrix()
{
glm::vec3 front = glm::cross(m_up, m_right);
glm::vec3 center=m_position+front;
return glm::lookAt(m_position, center, m_up);
}
glm::mat4 Camera::get_projection_matrix()
{
return glm::identity();
}
void Camera::scale(float delta_scale)
{
}
2.2正交投影相机(OrthographicCamera)的设计
正交投影相机的逻辑基于正交投影的运算。其在生成正交投影矩阵的时候需要提供left,right,bottom,top,near,far这六个变量,因而他需要持有这六个变量,在构造正交投影相机的时候对变量进行赋值,在需要获取投影矩阵的时候只需要传入相关值并返回即可。
而对于正交投影矩阵的缩放操作,其本质上是去按比例修改正交投影盒的投影平面,其中为了确保可视范围不变,不可以修改其far与near变量。因而我们会需要记录一个缩放相关值m_scale。而在实际的操作当中,我们需要使用滚轮去对该数值进行加减更新,而当数值为负数的时候,缩放比例并不是负数,因而最终的缩放比例的计算是使用2的m_scale次方进行计算。这样就满足了:
(1)当m_scale=0的时候,没有进行缩放
(2)当m_scale<0的时候,进行缩小
(3)当m_scale>0的时候,进行放大
orthographic_camera.h
#pragma once
#include "camera.h"
class OrthographicCamera :public Camera
{
public:
OrthographicCamera(float l, float r, float t, float b, float n, float f);
~OrthographicCamera();
glm::mat4 get_projection_matrix()override;
void scale(float delta_scale)override;
public:
float m_left = 0.0f;
float m_right = 0.0f;
float m_bottom = 0.0f;
float m_top = 0.0f;
float m_near = 0.0f;
float m_far = 0.0f;
float m_scale{ 0.0f };
};
orthographic.cpp
#include"orthographic_camera.h"
OrthographicCamera::OrthographicCamera(float left, float right, float bottom, float top, float near, float far)
:m_left(left), m_right(right), m_bottom(bottom), m_top(top), m_near(near), m_far(far)
{
}
OrthographicCamera::~OrthographicCamera()
{
}
glm::mat4 OrthographicCamera::get_projection_matrix()
{
float scale=std::pow(2.0f, m_scale);
return glm::ortho(m_left*scale, m_right*scale, m_bottom*scale, m_top*scale, m_near, m_far);
}
void OrthographicCamera::scale(float delta_scale)
{
m_scale+= delta_scale;
}
2.3透视投影相机(PerspectiveCamera)的设计
从相同的思路出发,透视投影相机去提供透视变换矩阵的时候需要提供四个数据fovy,aspect,near,far,这些都是必须的变量,在构造透视投影相机的时候进行赋值,在获取的时候调用的时候传入相关值并返回相应地矩阵。
对于透视投影相机的放缩操作,可以通过相机在其朝向方向上进行移动来实现,当相机朝前方移动的时候,可视物体将会放大,反之缩小。
perspective_camera.h
#pragma once
#include"camera.h"
class PerspectiveCamera :public Camera
{
public:
PerspectiveCamera(float fovy, float aspect, float near, float far);
~PerspectiveCamera();
glm::mat4 get_projection_matrix()override;
void scale(float delta_scale)override;
public:
float m_fovy = 0.0f;
float m_aspect = 0.0f;
float m_near = 0.0f;
float m_far = 0.0f;
};
perspective.cpp
#include"perspective_camera.h"
PerspectiveCamera::PerspectiveCamera(float fov, float aspect, float near, float far)
:m_fovy(fov), m_aspect(aspect), m_near(near), m_far(far)
{
}
PerspectiveCamera::~PerspectiveCamera()
{
}
glm::mat4 PerspectiveCamera::get_projection_matrix()
{
//传入的是角度,需要转化为弧度
return glm::perspective(glm::radians(m_fovy), m_aspect, m_near, m_far);
}
void PerspectiveCamera::scale(float delta_scale)
{
auto front = glm::cross(m_up, m_right);
m_position += front * delta_scale;
}
三、相机控制(Camera Control)的设计
3.1相机控制基类(CameraControl)的设计
CameraControl的主要作用便是让相机响应外界消息,进行相应的旋转,平移等操作。而为了达成这一目的,就需要持有鼠标与键盘按键状态,设置其根据外界值变化的幅度比例,并提供相应的按键响应函数来获取外设的输入信息,同时更新按键状态。
camera_control.h
#pragma once
#include"../../glframework/core.h"
#include"camera.h"
#include
class CameraControl
{
public:
CameraControl();
~CameraControl();
virtual void on_mouse(int button, int action, double xpos, double ypos);
virtual void on_cursor(double xpos, double ypos);
virtual void on_key(int key, int action, int mods);
virtual void on_scroll(float offset);
//每一帧渲染之前都要进行调用,每一帧更新的行为可以放在这里
virtual void on_update();
void set_camera(Camera* camera) { m_camera = camera; }
void set_sensitivity(float sensitivity) { m_sensitivity = sensitivity; }
protected:
//1.鼠标按键状态
bool m_left_down = false;
bool m_right_down = false;
bool m_middle_down = false;
//2.当前鼠标位置
float m_current_x = 0.0f;
float m_current_y = 0.0f;
//3.敏感度
float m_sensitivity = 0.2f;
//4.记录键盘相关按键的按下状态
std::mapm_key_map;
//5.存储当前控制的摄像机
Camera* m_camera;
//6.记录相机缩放的速度
float m_scale_speed = 0.0001f;
};
camera_control.cpp
#include"camera_control.h"
#include
CameraControl::CameraControl()
{
}
CameraControl::~CameraControl()
{
}
void CameraControl::on_mouse(int button, int action, double xpos, double ypos)
{
//1.判断当前的按键是否按下
bool pressed = (action == GLFW_PRESS) ? true : false;
//2.如果按下,记录当前按下的位置
if (pressed)
{
m_current_x = xpos;
m_current_y = ypos;
}
//3.根据按下的鼠标按键不同,激活不同的记录
switch (button)
{
case GLFW_MOUSE_BUTTON_LEFT:
m_left_down = pressed;
break;
case GLFW_MOUSE_BUTTON_RIGHT:
m_right_down = pressed;
break;
case GLFW_MOUSE_BUTTON_MIDDLE:
m_middle_down = pressed;
break;
default:
break;
}
}
void CameraControl::on_cursor(double xpos, double ypos)
{
}
void CameraControl::on_key(int key, int action, int mods)
{
//过滤掉repeat的情况
if(action == GLFW_REPEAT)
return;
//1.检测按下或者抬起,给到一个变量
bool pressed = (action == GLFW_PRESS) ? true : false;
//2.记录在key_map中
m_key_map[key] = pressed;
}
void CameraControl::on_update()
{
}
void CameraControl::on_scroll(float offset)
{
}
3.2轨迹球相机控制(TrackballCameraControl)类设计
在轨迹球相机控制中,要实现(从视觉上来说的效果):
(1)点击鼠标左键能够翻转当前物体
(2)点击鼠标中间进行对物体进行移动
(3)滑动滚轮进行放缩
对于(1)操作,实际上是相机绕着其right向量和世界坐标的up轴进行旋转,纵向的变换为pitch,而横向的变换为yaw。
其中pitch操作与yaw操作均会改变相机的当前位置,而pitch操作不会改变right向量,yaw操作不会改变front向量指向(始终指向中心物体),因而其对于pitch操作需要用旋转矩阵乘以up向量,而front向量是经过叉乘计算得到,也会跟着改变。对于yaw操作,则是用旋转矩阵分别乘以up、right向量,front向量通过叉乘计算得到,其结果不会发生变化。其中,pitch操作受到鼠标指针纵向变化量的影响,而yaw操作则受到横向变化量的影响。
对于(2)操作,则是对相机位置位置进行改变,只需要在up、right方向上加上变化值。
对于(3)操作,则是对先前封装好的scale函数直接进行调用。
trackball_camera_control.h
#pragma once
#include"camera_control.h"
class TrackballCameraControl : public CameraControl
{
public:
TrackballCameraControl();
~TrackballCameraControl();
//父类当中的函数是否需要重写
void on_cursor(double xpos, double ypos) override;
void on_scroll(float offset)override;
private:
void pitch(float angle);
void yaw(float angle);
private:
float m_move_speed = 0.002f;
};
trackball_camera_control.cpp
#include"trackball_camera_control.h"
TrackballCameraControl::TrackballCameraControl()
{
}
TrackballCameraControl::~TrackballCameraControl()
{
}
void TrackballCameraControl::on_cursor(double xpos, double ypos)
{
if (m_left_down)
{
//调整相机的各类参数
//1.计算经线跟纬线旋转的增量角度(正负都有可能)
float delta_x = (xpos - m_current_x) * m_sensitivity;
float delta_y = (ypos - m_current_y) * m_sensitivity;
//2.分开pitch跟yaw各自计算
pitch(-delta_y);
yaw(-delta_x);
}
else if (m_middle_down)
{
float delta_x = (xpos - m_current_x) * m_move_speed;
float delta_y = (ypos - m_current_y) * m_move_speed;
m_camera->m_position += m_camera->m_up * delta_y;
m_camera->m_position -= m_camera->m_right * delta_x;
}
m_current_x = xpos;
m_current_y = ypos;
}
void TrackballCameraControl::pitch(float angle)
{
//绕着m_right向量在旋转
auto mat = glm::rotate(glm::mat4(1.0f), glm::radians(angle), m_camera->m_right);
//影响当前相机的up向量和位置
m_camera->m_up = mat * glm::vec4(m_camera->m_up, 0.0f);
m_camera->m_position = mat * glm::vec4(m_camera->m_position, 1.0f);
}
void TrackballCameraControl::yaw(float angle)
{
//绕着m_front向量在旋转
auto mat = glm::rotate(glm::mat4(1.0f), glm::radians(angle), glm::vec3(0.0f, 1.0f, 0.0f));
//影响当前相机的up向量和位置
m_camera->m_up = mat * glm::vec4(m_camera->m_up, 0.0f);
m_camera->m_right = mat * glm::vec4(m_camera->m_right, 0.0f);
m_camera->m_position = mat * glm::vec4(m_camera->m_position, 1.0f);
}
void TrackballCameraControl::on_scroll(float offset)
{
m_camera->scale(m_scale_speed * offset);
}
3.3游戏相机控制(GameCameraControl)类的设计
在游戏相机控制中,要实现(从视觉上来说的效果):
(1)鼠标右键按下的时候实现相机视角的俯仰(pitch)和偏转(yaw)
(2)通过WASD的按键移动当前相机的位置
对于(1)的实现,依旧是只需要关注相机是在围绕着哪一个轴进行旋转,并修改其他量即可。在这里由于不会影响到相机的位置,所以位置信息不需要进行更改
对于(2)的实现,则是根据当前按键的状态值去计算运动的方向,在进行归一化之后,对当前相机的坐标位置进行更新。其中尤其需要注意在归一化过程中分母为0的情况需要进行特殊判断
game_camera_control.h
#pragma once
#include"camera_control.h"
class GameCameraControl : public CameraControl
{
public:
GameCameraControl();
~GameCameraControl();
void on_cursor(double xpos, double ypos) override;
void on_update()override;
void set_speed(float speed){ m_speed = speed;}
private:
void pitch(float angle);
void yaw(float angle);
private:
float m_pitch{ 0.0f };
float m_speed{ 0.01f };
};
game_camera_control.cpp
#include"game_camera_control.h"
GameCameraControl::GameCameraControl()
{
m_sensitivity = 0.002f;
}
GameCameraControl::~GameCameraControl()
{
}
void GameCameraControl::on_cursor(double xpos, double ypos)
{
float delta_x = (xpos - m_current_x) * m_sensitivity;
float delta_y = (ypos - m_current_y) * m_sensitivity;
if (m_right_down)
{
pitch(delta_y);
yaw(delta_x);
}
m_current_x = xpos;
m_current_y = ypos;
}
void GameCameraControl::pitch(float angle)
{
m_pitch += angle;
if (m_pitch > 89.0f|| m_pitch m_right);
m_camera->m_up = mat * glm::vec4(m_camera->m_up, 0.0f);
}
void GameCameraControl::yaw(float angle)
{
auto mat=glm::rotate(glm::mat4(1.0f), glm::radians(angle), glm::vec3(0.0f, 1.0f, 0.0f));
m_camera->m_right = mat * glm::vec4(m_camera->m_right, 0.0f);
m_camera->m_up = mat * glm::vec4(m_camera->m_up, 0.0f);
}
void GameCameraControl::on_update()
{
glm::vec3 direction(0.0f);
auto front=glm::cross(m_camera->m_up, m_camera->m_right);
auto right=m_camera->m_right;
if (m_key_map[GLFW_KEY_W])direction += front;
if (m_key_map[GLFW_KEY_S])direction -= front;
if (m_key_map[GLFW_KEY_A])direction -= right;
if (m_key_map[GLFW_KEY_D])direction += right;
//归一化
if (glm::length(direction) != 0)
{
direction=glm::normalize(direction);
m_camera->m_position += direction * m_speed;
}
}