[OpenGL]相机系统
目录
一、逻辑分析
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老师:bilibili赵新政老师赵新政的个人空间-赵新政个人主页-哔哩哔哩视频)
一、逻辑分析
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<glm::mat4>();
}
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<map>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::map<int,bool>m_key_map;//5.存储当前控制的摄像机Camera* m_camera;//6.记录相机缩放的速度float m_scale_speed = 0.0001f;};
camera_control.cpp
#include"camera_control.h"
#include<iostream>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 < -89.0f){m_pitch -= angle;return;}auto mat = glm::rotate(glm::mat4(1.0f), glm::radians(angle), m_camera->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;}
}