摄像机在着色器方面来说就是提供观察矩阵和投影矩阵,它的一些交互可以实时改变这两个矩阵。
因为涉及矩阵操作,我们使用介绍过的GLM库。
首先构建摄像机类:
namespace Dragon
{
class Camera
{
public:
Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 5.0f), glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f), float farPlane = 0.1f, float nearPlane = 100.0f);
Camera(float posX, float posY, float posZ, float upX, float upY, float upZ, float nearPlane, float farPlane);
~Camera() = default;
Camera(Camera& camera)
{
m_Position = camera.m_Position;
m_Front = camera.m_Front;
m_Up = camera.m_Up;
m_Right = camera.m_Right;
m_WorldUp = camera.m_WorldUp;
m_Zoom = camera.m_Zoom;
m_NearPlane = camera.m_NearPlane;
m_FarPlane = camera.m_FarPlane;
m_AspectRatio = camera.m_AspectRatio;
}
inline glm::mat4 GetViewMatrix() { return glm::lookAt(m_Position, m_Position + m_Front, m_Up); }
inline glm::mat4 GetProjectionMatrix() {return glm::perspective((float)glm::radians(m_Zoom), m_AspectRatio, m_NearPlane, m_FarPlane);}
glm::vec3 GetPosition() { return m_Position; }
void SetPosition(glm::vec3 position) { m_Position = position; }
glm::vec3 GetFront() { return m_Front; }
void SetFront(glm::vec3 front) { m_Front = front; }
glm::vec3 GetUp() { return m_Up; }
void SetUp(glm::vec3 up) { m_Up = up; }
glm::vec3 GetRight() { return m_Right; }
void SetRight(glm::vec3 right) { m_Right = right; }
glm::vec3 GetWorldUp() { return m_WorldUp; }
void SetWorldUp(glm::vec3 worldUp) { m_WorldUp = worldUp; }
float GetZoom() { return m_Zoom; }
void SetZoom(float zoom) { m_Zoom = zoom; }
float GetAspectRatio() { return m_AspectRatio; }
void SetAspectRatio(float aspectRatio) { m_AspectRatio = aspectRatio; }
float GetNearPlane() { return m_NearPlane; }
void SetNearPlane(float nearPlane) { m_NearPlane = nearPlane; }
float GetFarPlane() { return m_FarPlane; }
void SetFarPlane(float farPlane) { m_FarPlane = farPlane; }
private:
//摄像机参数
glm::vec3 m_Position;
glm::vec3 m_Front;
glm::vec3 m_Up;
glm::vec3 m_Right;
glm::vec3 m_WorldUp;
float m_NearPlane;
float m_FarPlane;
float m_AspectRatio = 1280.0f/720.f;
float m_Zoom = 45.0f;
};
}
重点不在这些get
|set
方法上。构造函数中,就像我们之前介绍过的,摄像机有一个位置,指向前的矢量,指向上的矢量,指向右的矢量,然后有一个指向世界坐标系上的矢量用于摄像机的矢量计算。GetViewMatrix()
函数使用glm::lookAt
方法来构建view矩阵,GetProjectionMatrix()
函数使用glm::perspective
来构建透视投影,也可以使用正交投影,也可以同时使用,搞一个切换参数。
构造函数实现:
namespace Dragon
{
Camera::Camera(glm::vec3 position, glm::vec3 up, float nearPlane, float farPlane)
:m_Front(glm::vec3(0.0f, 0.0f, -1.0f))
{
m_NearPlane = nearPlane;
m_FarPlane = farPlane;
m_Position = position;
m_WorldUp = up;
m_Up = m_WorldUp;
}
Camera::Camera(float posX, float posY, float posZ, float upX, float upY, float upZ, float nearPlane, float farPlane)
: m_Front(glm::vec3(0.0f, 0.0f, -1.0f))
{
m_NearPlane = nearPlane;
m_FarPlane = farPlane;
m_Position = glm::vec3(posX, posY, posZ);
m_WorldUp = glm::vec3(upX, upY, upZ);
m_Up = m_WorldUp;
}
}
一个摄像机类完成,接下来构建CameraController
类:
namespace Dragon
{
class CameraController
{
public:
CameraController(float aspectRatio);
void OnUpdate(Timestep ts);
void OnEvent(Event& e);
Camera& GetCamera() { return m_Camera; }
const Camera& GetCamera()const { return m_Camera; }
Timestep& GetTimestep(){return m_Timestep;};
void SetTimestep(Timestep& ts) { m_Timestep = ts; }
private:
bool OnMouseScrolled(MouseScrolledEvent& e);
bool OnWindowResized(WindowResizeEvent& e);
bool OnMouseMoved(MouseMovedEvent& e);
bool OnKeyBoard(KeyPressedEvent& e);
bool OnKeyReleased(KeyReleasedEvent& e);
bool OnMouseClicked(MouseButtonPressedEvent& e);
bool OnMouseReleased(MouseButtonReleasedEvent& e);
void UpdateCameraVectors();
private:
Camera m_Camera;
Camera m_LastCamera;
glm::vec3 m_CameraPosition = glm::vec3(0.0f,0.0f,5.0f);
float m_AspectRatio;
float m_CameraTranslationSpeed = 5.0f;
float m_ZoomLevel = 45.0f;
float m_MouseSensitivity = 0.01f;
float m_Yaw = -90.0f;
float m_Pitch = 0.0f;
bool firstMouse = true;
float lastX , lastY ;
Timestep m_Timestep;
bool m_Quit = true;
bool m_CTRL = false;
bool m_ALT = false;
bool m_MouseLeft = false;
bool m_MouseRight = false;
bool m_MouseMiddle = false;
};
}
注意这里我们使用的Timestep
类,它的定义如下:
namespace Dragon
{
class Timestep
{
public:
Timestep(float time = 0.0f)
:m_Time(time)
{
}
operator float() const { return m_Time; }
float GetSeconds() const { return m_Time; }
float GetMilliseconds() const { return m_Time * 1000.0f; }
private:
float m_Time;
};
}
主要是用来获取时间的,将它用于计算间隔时间的话,可以减少帧率变化的影响,间隔的计算在Application
类中:
void Application::Run()
{
while (m_Running)
{
if(!m_Cursor)
glfwSetInputMode((GLFWwindow*)m_Window->GetNativeWindow(), GLFW_CURSOR, GLFW_CURSOR_HIDDEN);
else
glfwSetInputMode((GLFWwindow*)m_Window->GetNativeWindow(), GLFW_CURSOR, GLFW_CURSOR_DISABLED);
float time = (float)glfwGetTime();
Timestep timestep = time - m_LastFrameTime;
m_LastFrameTime = time;
for (Layer* layer : m_LayerStack)
layer->OnUpdate(timestep);
m_ImGuiLayer->Begin();
for (Layer* layer : m_LayerStack)
layer->OnImGuiRender();
m_ImGuiLayer->End();
m_Window->OnUpdate();
}
}
计算的deltatime用于层的update()
函数。
CameraController
类设置了许多事件函数:
private:
bool OnMouseScrolled(MouseScrolledEvent& e);
bool OnWindowResized(WindowResizeEvent& e);
bool OnMouseMoved(MouseMovedEvent& e);
bool OnKeyBoard(KeyPressedEvent& e);
bool OnKeyReleased(KeyReleasedEvent& e);
bool OnMouseClicked(MouseButtonPressedEvent& e);
bool OnMouseReleased(MouseButtonReleasedEvent& e);
void UpdateCameraVectors();
在事件系统那一节介绍过它们的用法。
实现中:
bool CameraController::OnMouseScrolled(MouseScrolledEvent& e)
{
if (m_ZoomLevel >= 1.0f && m_ZoomLevel <= 90.0f)
m_ZoomLevel -= e.GetYOffset();
if (m_ZoomLevel <= 1.0f)
m_ZoomLevel = 1.0f;
if (m_ZoomLevel >= 90.0f)
m_ZoomLevel = 90.0f;
m_Camera.SetZoom(m_ZoomLevel);
return false;
}
鼠标滚轮可以改变摄像机焦距,使用事件类对象来捕捉鼠标滚轮偏移。
bool CameraController::OnWindowResized(WindowResizeEvent& e)
{
m_Camera.SetAspectRatio(float(e.GetWidth()) / float(e.GetHeight()));
return false;
}
窗口缩放要改变摄像机大小的比例。
bool CameraController::OnMouseMoved(MouseMovedEvent& e)
{
if (!m_Quit)
{
float xPos, yPos;
xPos = e.GetX();
yPos = e.GetY();
if (firstMouse)
{
lastX = xPos;
lastY = yPos;
firstMouse = false;
}
float xOffset = 0.0f;
float yOffset = 0.0f;
xOffset = (xPos - lastX) * m_MouseSensitivity;
yOffset = (lastY - yPos) * m_MouseSensitivity;
lastX = xPos;
lastY = yPos;
if (m_ALT)
{
if (m_MouseLeft)
{
m_Yaw += xOffset*5;
m_Pitch += yOffset*5;
if (m_Pitch > 89.0f)
m_Pitch = 89.0f;
if (m_Pitch < -89.0f)
m_Pitch = -89.0f;
}
if (m_MouseMiddle)
{
m_Camera.SetPosition(m_Camera.GetPosition() - m_Camera.GetRight() * (xOffset*20 * m_Timestep));
m_Camera.SetPosition(m_Camera.GetPosition() - m_Camera.GetUp() * (yOffset *20* m_Timestep));
}
}
}
else if (m_Quit)
{
float xPos, yPos;
xPos = e.GetX();
yPos = e.GetY();
if (firstMouse)
{
lastX = xPos;
lastY = yPos;
firstMouse = false;
}
float xOffset = 0.0f;
float yOffset = 0.0f;
xOffset = (xPos - lastX) * m_MouseSensitivity;
yOffset = (lastY - yPos) * m_MouseSensitivity;
lastX = xPos;
lastY = yPos;
m_Yaw += xOffset;
m_Pitch += yOffset;
if (m_Pitch > 89.0f)
m_Pitch = 89.0f;
if (m_Pitch < -89.0f)
m_Pitch = -89.0f;
}
return false;
}
对于摄像机的移动,这里提供了两种方式,第一是FPS游戏类的摄像机移动,第二是大部分三维相关软件的ALT
类控制,两种方式都捕捉鼠标的移动来改变俯仰角和倾斜角,使用欧拉角来控制摄像机很方便。
bool CameraController::OnKeyBoard(KeyPressedEvent& e)
{
if (m_Quit)
{
if (e.GetKeyCode() == DG_KEY_A)
m_Camera.SetPosition(m_Camera.GetPosition() - m_Camera.GetRight() * (m_CameraTranslationSpeed * m_Timestep));
if (e.GetKeyCode() == DG_KEY_D)
m_Camera.SetPosition(m_Camera.GetPosition() + m_Camera.GetRight() * (m_CameraTranslationSpeed * m_Timestep));
if (e.GetKeyCode() == DG_KEY_W)
m_Camera.SetPosition(m_Camera.GetPosition() + m_Camera.GetFront() * (m_CameraTranslationSpeed * m_Timestep));
if (e.GetKeyCode() == DG_KEY_S)
m_Camera.SetPosition(m_Camera.GetPosition() - m_Camera.GetFront() * (m_CameraTranslationSpeed * m_Timestep));
}
if (e.GetKeyCode() == DG_KEY_Q)
{
if (m_Quit)
{
m_LastCamera = m_Camera;
firstMouse = true;
m_Quit = !m_Quit;
}
else
{
m_Camera = m_LastCamera;
m_Quit = !m_Quit;
}
}
if (!m_Quit)
{
if (e.GetKeyCode() == DG_KEY_LEFT_CONTROL)
m_CTRL = true;
if (e.GetKeyCode() == DG_KEY_LEFT_ALT)
m_ALT = true;
}
return false;
}
键盘按键也是配合上面提到了两种方式,FPS方式是使用WASD
来控制前后左右,另一种方式是使用CTRL
和ALT
,Q
键可以切换两种方式。
这里使用了m_LastCamera
和m_Camera
来配合解决一个问题,即在切换方式时(从三维方式到FPS方式),摄像机的的参数会切换为初始状态的问题,使用这两个变量可以在切换时存储当前摄像机状态,并在之后赋予,还记得我们之前摄像机类的一个函数吗:
Camera(Camera& camera)
{
m_Position = camera.m_Position;
m_Front = camera.m_Front;
m_Up = camera.m_Up;
m_Right = camera.m_Right;
m_WorldUp = camera.m_WorldUp;
m_Zoom = camera.m_Zoom;
m_NearPlane = camera.m_NearPlane;
m_FarPlane = camera.m_FarPlane;
m_AspectRatio = camera.m_AspectRatio;
}
这是一个重写的复制函数,用来帮助解决我的切换问题。
bool CameraController::OnKeyReleased(KeyReleasedEvent& e)
{
if (!m_Quit)
{
if (e.GetKeyCode() == DG_KEY_LEFT_CONTROL)
m_CTRL = false;
if (e.GetKeyCode() == DG_KEY_LEFT_ALT)
m_ALT = false;
}
return false;
}
键盘按键松开函数。
bool CameraController::OnMouseClicked(MouseButtonPressedEvent& e)
{
if (!m_Quit)
{
if (e.GetMouseButton() == DG_MOUSE_BUTTON_LEFT)
m_MouseLeft = true;
if (e.GetMouseButton() == DG_MOUSE_BUTTON_RIGHT)
m_MouseRight = true;
if (e.GetMouseButton() == DG_MOUSE_BUTTON_MIDDLE)
m_MouseMiddle = true;
}
return false;
}
鼠标按键事件在三维模式下使用。
bool CameraController::OnMouseReleased(MouseButtonReleasedEvent& e)
{
if (!m_Quit)
{
if (e.GetMouseButton() == DG_MOUSE_BUTTON_LEFT)
m_MouseLeft = false;
if (e.GetMouseButton() == DG_MOUSE_BUTTON_RIGHT)
m_MouseRight = false;
if (e.GetMouseButton() == DG_MOUSE_BUTTON_MIDDLE)
m_MouseMiddle = false;
}
return false;
}
鼠标按键松开事件,和上面的函数配合,用于切换按键的状态。
void CameraController::UpdateCameraVectors()
{
glm::vec3 front;
front.x = cos(glm::radians(m_Yaw)) * cos(glm::radians(m_Pitch));
front.y = sin(glm::radians(m_Pitch));
front.z = sin(glm::radians(m_Yaw)) * cos(glm::radians(m_Pitch));
m_Camera.SetFront(glm::normalize(front));
m_Camera.SetWorldUp(glm::vec3(0.0f, 1.0f, 0.0f));
m_Camera.SetRight(glm::normalize(glm::cross(m_Camera.GetFront(), m_Camera.GetWorldUp())));
m_Camera.SetUp(glm::cross(m_Camera.GetRight(), m_Camera.GetFront()));
}
最后是摄像机矢量更新函数,使用新计算来的两个欧拉角来更新相关的矢量。
摄像机的更新函数:
void CameraController::OnUpdate(Timestep ts)
{
m_Timestep = ts;
UpdateCameraVectors();
//m_CameraTranslationSpeed = m_ZoomLevel;
}
用于设置时间间隔,以及更新摄像机矢量。
事件函数:
void CameraController::OnEvent(Event& e)
{
EventDispatcher dispatcher(e);
dispatcher.Dispatch(DG_BIND_EVENT_FN(CameraController::OnMouseScrolled));
dispatcher.Dispatch(DG_BIND_EVENT_FN(CameraController::OnWindowResized));
dispatcher.Dispatch(DG_BIND_EVENT_FN(CameraController::OnMouseMoved));
dispatcher.Dispatch(DG_BIND_EVENT_FN(CameraController::OnKeyBoard));
dispatcher.Dispatch(DG_BIND_EVENT_FN(CameraController::OnMouseClicked));
dispatcher.Dispatch(DG_BIND_EVENT_FN(CameraController::OnMouseReleased));
dispatcher.Dispatch(DG_BIND_EVENT_FN(CameraController::OnKeyReleased));
}
主要是创建事件批处理对象来派送编写好的事件响应函数。
在客户端的层类中,构造函数中初始化摄像机控制器类对象:
ExampleLayer()
: Layer("Example"), m_CameraController(1280.0 / 720.f)
在OnUpdate
函数中,调用摄像机控制器的更新函数:
void OnUpdate(Dragon::Timestep ts) override
{
m_CameraController.OnUpdate(ts);
至此,摄像机组件完成。下一节介绍材质系统。
项目github地址:https://github.com/Dragon-Baby/Dragon