一般游戏里都有一个相机当做玩家眼睛的作用
负责移动旋转,查看游戏世界的内容
相机的核心就是拿到观察矩阵,做世界空间的坐标变换
之前已经实现了3D显示,也就是相机的功能
当相机位置变化的时候,重新求出观察矩阵就是关键
怎么求矩阵?
a)获取相机的位置
b)获取相机z方向(注意:是指向屏幕外面的反方向)
c)获取相机x方向
d)获取相机y方向
e)用这3个轴外加一个平移向量来创建一个矩阵
f)用这个矩阵乘以任何向量来将其变换到那个坐标空间(LookAt矩阵)
详情见代码注释:
//相机位置
// 轴是从屏幕指向你的,如果希望摄像机向后移动,就沿着z轴的正方向移动。
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
//相机z方向
// 注意:是指向屏幕外面的反方向
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);
//相机x方向(只需考虑xz平面,取相对于xz平面的up向量叉乘)
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));
//相机y方向
glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);
//使用3个相互垂直的轴定义了一个坐标空间,可以用这3个轴外加一个平移向量来创建一个矩阵
// 用这个矩阵乘以任何向量来将其变换到那个坐标空间。这就是LookAt矩阵
//
// LookAt 矩阵
// [ Rx Ry Rz 0 ] * [ 1 0 0 -Px ]
// [ Ux Uy Uz 0 ] * [ 0 1 0 -Py ]
// [ Dx Dy Dz 0 ] * [ 0 0 1 -Pz ]
// [ 0 0 0 1 ] * [ 0 0 0 1 ]
//
// 其中R是右向量,U是上向量,D是方向向量,P是摄像机位置向量
//glm 提供了lookAt接口,返回LookAt矩阵
// 参数1:相机位置
// 参数2:目标位置
// 参数3:世界空间中向上的向量
return glm::lookAt(cameraPos, cameraPos + cameraDirection, cameraUp);
这就是基础思想。
如何实现相机控制呢?
OpenGL本身没有摄像机(Camera)的概念
可以通过把场景中的所有物体往相反方向移动的方式来模拟出摄像机
产生一种我们在移动的感觉,而不是场景在移动(其实就是场景在移动)
但是现在相机的位置和方向数据都是写死的,游戏里一般都是玩家控制的
需要监听 GLFWWindow的回调:
1.键盘按键回调:WASD控制相机移动
核心思想:
a)监听WASD按键
b)修改相机的cameraPos,根据输入方向变化
代码如下:
键盘按键回调
processInput(window)
processInput 实现
static void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
//键盘WASD,负责控制相机位置移动
float cameraSpeed = 2.5f * deltaTime;
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
camera.ProcessKeyboard(Camera_Movement::FORWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
camera.ProcessKeyboard(Camera_Movement::BACKWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
camera.ProcessKeyboard(Camera_Movement::LEFT, deltaTime);
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
camera.ProcessKeyboard(Camera_Movement::RIGHT, deltaTime);
}
相机移动实现,就是根据移动方向改变Pos
void ProcessKeyboard(Camera_Movement direction, float deltaTime)
{
//deltaTime时间差,用来算平均速度的
// 有些人可能会比其他人每秒绘制更多帧
// deltaTime很大,就意味着上一帧的渲染花费了更多时间,所以这一帧的速度需要变得更高来平衡渲染所花去的时间
// 保证摄像机的速度都会相应平衡,这样每个用户的体验就都一样了
float velocity = MovementSpeed * deltaTime;
if (direction == FORWARD)
Position += Front * velocity;
if (direction == BACKWARD)
Position -= Front * velocity;
if (direction == LEFT)
Position -= Right * velocity;
if (direction == RIGHT)
Position += Right * velocity;
}
2.1鼠标按键回调:按下才开始控制旋转
2.2鼠标位置回调:坐标差值设置旋转欧拉角度
鼠标事件监听
//鼠标按键回调
glfwSetMouseButtonCallback(window, mouse_button_callback);
//鼠标位置回调
glfwSetCursorPosCallback(window, cursor_pos_callback);
使用鼠标点击,负责控制是否启用旋转
static bool enable_rotate;
static void mouse_button_callback(GLFWwindow* window, int button, int action, int mods)
{
std::cout << button << "," << action << endl;
if (action == GLFW_PRESS)
enable_rotate = true;
if (action == GLFW_RELEASE)
enable_rotate = false;
}
鼠标位置,记录差值
static float lastX;
static float lastY;
static void cursor_pos_callback(GLFWwindow* window, double xpos, double ypos)
{
if (!enable_rotate)
{
lastX = (float)xpos;
lastY = (float)ypos;
return;
}
float xoffset = (float)xpos - lastX;
float yoffset = lastY - (float)ypos; // 注意Y是相反的,因为y坐标是从底部往顶部依次增大的
lastX = (float)xpos;
lastY = (float)ypos;
camera.ProcessMouseMovement(xoffset, yoffset, true);
}
使用鼠标位置,负责控制相机旋转
void ProcessMouseMovement(float xoffset, float yoffset, bool constrainPitch = true)
{
xoffset *= MouseSensitivity;
yoffset *= MouseSensitivity;
Yaw += xoffset;
Pitch += yoffset;
if (constrainPitch)
{
//限制俯仰角(上下旋转范围)
Zoom = glm::clamp(Zoom, -89.0f, 89.0f);
}
updateCameraVectors();
}
updateCameraVectors 实现,更新相机欧拉角向量
void updateCameraVectors()
{
//欧拉角(Euler Angle)
// 俯仰角(Pitch)、偏航角(Yaw)和滚转角(Roll)
// 对应 X、Y、Z
//对于摄像机系统,一般只关心俯仰角和偏航角,
//1.在xz平面上,看向y轴(俯仰角)
// 根据俯仰角求出方向向量的坐标
//direction.x = cos(glm::radians(pitch));
//direction.y = sin(glm::radians(pitch));
//direction.z = cos(glm::radians(pitch));
//2.在xz平面,忽略y轴(偏航角)
// 根据偏航角求出方向向量的坐标
//direction.x = cos(glm::radians(yaw));
//direction.y = 0;
//direction.z = sin(glm::radians(yaw));
//两个相叠加,就可以根据俯仰角+偏航角求出方向向量
//direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw));
//direction.y = sin(glm::radians(pitch));
//direction.z = cos(glm::radians(pitch)) 8 sin(glm::radians(yaw))
//下面的问题就是:求出俯仰角+偏航角
//根据鼠标的输入计算
// 水平的移动影响偏航角,竖直的移动影响俯仰角
// 储存上一帧鼠标的位置,在当前帧中我们当前计算鼠标位置与上一帧的位置相差多少
glm::vec3 front;
front.x = cos(glm::radians(Yaw)) * cos(glm::radians(Pitch));
front.y = sin(glm::radians(Pitch));
front.z = sin(glm::radians(Yaw)) * cos(glm::radians(Pitch));
Front = glm::normalize(front);
Right = glm::normalize(glm::cross(Front, WorldUp));
Up = glm::normalize(glm::cross(Right, Front));
}
3.鼠标滚轮回调:缩放设置相机FOV
鼠标事件监听
//鼠标滚轮回调
glfwSetScrollCallback(window, scroll_callback););
使用鼠标滚轮,记录滚动范围
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
camera.ProcessMouseScroll((float)yoffset);
}
使用鼠标滚轮,负责相机缩放 FOV
void ProcessMouseScroll(float yoffset)
{
Zoom -= (float)yoffset;
//限制FOV范围在,1-60之间
Zoom = glm::clamp(Zoom, 1.0f, 60.0f);
}
如何使用?
最后在渲染循环中,更新观察矩阵传给shader即可
//渲染循环
while (!glfwWindowShouldClose(window))
{
// ...
float time = (float)glfwGetTime();
deltaTime = time - lastFrame;
lastFrame = time;
//更新相机观察矩阵
glm::mat4 view = camera.GetViewMatrix();
testShader.setM4("view", glm::value_ptr(view));
glm::mat4 projection = glm::mat4(1.0f);
//更新fov
float fov = camera.Zoom;
projection = glm::perspective(glm::radians(fov), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
testShader.setM4("projection", glm::value_ptr(projection));
// glDraws ...
glfwSwapBuffers(window);
glfwPollEvents();
}