在上一章节中我们讨论了视图(view)矩阵的用法,我们用这种方式来移动场景。在OpenGL中并没有摄像机的概念,但我们可以通过反向移动我们的场景来模拟摄像机。
视图空间view矩阵将世界空间的坐标转化到视图空间,也就是摄像机观察到的空间。为了定义一个摄像机,我们需要它在世界空间的位置,它指向的观察方向,它的右侧方向的向量,它指向上方的向量,且这些向量都是单位向量。
1.摄像机的位置
摄像机的位置向量是一个在世界空间中由原点指向该摄像机位置的向量:
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f); 2.摄像机的指向这个向量代表摄像机所指向的方向,在这里我们让摄像机指向原点。我们通过摄像机的位置向量减去目标位置的向量来获取指向摄像机+z轴的方向向量:
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f); glm::vec3 cameraDirection = glm::normaliza(cameraPos - cameraTarget); 3.右轴向我们需要一个摄像机的右方向向量来代表摄像机的+x轴轴向。为了获得这个向量,我们先定义摄像机的yz平面,+z方向的向量即上面摄像机的指向,我们定义一个世界空间的+y单位向量
,这两个向量构成yz平面,将这两个向量叉乘即获得摄像机的右轴向向量: glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection)); 4.上轴向接下来将指向与右轴向叉乘就简单的获取了摄像机的上轴向向量:
glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);通过上述的方法我们就获得了几个描述视图空间的向量。通过这些向量我们可以构造一个LookAt矩阵来创建一个摄像机。
LookAt 在这里,view矩阵就是这个LookAt矩阵,我们这样构成:
首先初始化摄像机:
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f); glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f); glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);构建view矩阵:
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);cameraPos+cameraFront保证摄像机永远指向目标方向。接下来设置一些键盘响应(WASD),来前后左右移动摄像机:
void processInput(GLFW* window) { ...//之前的设定 const float cameraSpeed = 0.05f; if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) cameraPos += cameraSpeed * cameraFront; if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) cameraPos -= cameraSpeed * cameraFront; if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed; if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed; } 移动速度由于每个人的电脑硬件性能不同,每两帧画面渲染间隔时间会有不同,这对实际摄像机的移动速度有很大的影响。我们通过计算帧与帧之间的时间来定义摄像机移动速度,这样可以消除这种影响:
//定义 float deltaTime = 0.0f; //间隔时间 float lastFrame = 0.0f; //上一帧渲染时间 //计算间隔时间 float currentFrame = glfwGetTime(); deltaTime = currentFrame - lastFrame; lastFrame = currentFrame; // 修改摄像机速度 void processInput(GLFWwindow* window) { float cameraSpeed = 2.5f * deltaTime; ... }运行程序,我们就会发现可以前后左右移动摄像机。这里给出原文源码供参考,以及预期结果。
在场景中四周环看为了转动摄像头,我们可以通过捕捉鼠标移动并修改之前设定的cameraFront来实现。下面讲解一下原理。
欧拉角欧拉角包含三个描述旋转的值,俯仰角(pitch),偏航角(yaw),自旋角(roll):
在这个案例中,我们不考虑摄像机的自旋。下面来讲俯仰角和偏航角转化为我们的向量表示。
俯仰角是摄像机绕自身的x轴旋转的角度,摄像机坐落在xz平面,平面图如下:
我们可以得到摄像机指向的x分量:cos(yaw),z分量:sin(yaw)。
将这两个角度的影响结合起来就是:
如果yaw角初始为0,摄像机将朝向+z轴,为避免这种情况,我们将yaw角初始设为-90度。
设置鼠标响应 我们设置鼠标的水平移动影响偏航角,垂直移动影响俯仰角。实现方法是存储上一帧鼠标的位置,并与当前帧的鼠标位置计算差值,获取差值的x,y分量,并用分量影响俯仰角和偏航角。
我们通过GLFW的glfwSetInputMode设置鼠标输入:
设置后,光标将一直处于窗口的中心位置。接着设置回调函数:
void mouse_callback(GLFWwindow* window, double xpos, double ypos);通过下面的方法在渲染循环中使用回调函数:
glfwSetCursorPosCallback(window, mouse_callback);在回调函数中,我们先计算鼠标的偏移,我们将上一帧的初始位置设置在窗口中心,接着设置降低偏移的影响(太高的话,摄像头会转的很快),最后计算俯仰角和偏航角:
float lastX = 400, lastY = 300; float xoffset = xpos - lastX; float yoffset = lastY - ypos; // 由于窗口的原点在左上角,Y轴反向 lastX = xpos; lastY = ypos; const float sensitivity = 0.05f; xoffset *= sensitivity; yoffset *= sensitivity; yaw += xoffset; pitch += yoffset;最后,为了避免摄像机翻转(如果摄像机的指向与世界空间代表正上的轴向平行会造成LookAt矩阵反转),我们不能让pitch角超过89度,也不能小于-89度:
if(pitch > 89.0f) pitch = 89.0f; if(pitch < -89.0f) pitch = -89.0f;最后设置方向向量:
glm::vec3 direction; direction.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch)); direction.y = sin(glm::radians(pitch)); direction.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch)); cameraFront = glm::normalize(direction);接着会有一个问题,如果就这么运行程序会发现刚开始会有一个镜头的快速偏移,这是由于一开始xPos和yPos等于你刚进入程序时的位置,可能非常远离窗口中心。我们可以设置一个bool变量来判断是否是第一次设置鼠标输入:
if(firstMouse) //初始设置 { lastX = xpos; lastY = ypos; firstMouse = false; }最后的回调函数如下:
void mouse_callback(GLFWwindow* window, double xpos, double ypos) { if(firstMouse) { lastX = xpos; lastY = ypos; firstMouse = false; } float xoffset = xpos - lastX; float yoffset = lastY - ypos; lastX = xpos; lastY = ypos; float sensitivity = 0.05; xoffset *= sensitivity; yoffset *= sensitivity; yaw += xoffset; pitch += yoffset; if(pitch > 89.0f) pitch = 89.0f; if(pitch < -89.0f) pitch = -89.0f; glm::vec3 direction; direction.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch)); direction.y = sin(glm::radians(pitch)); direction.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch)); cameraFront = glm::normalize(direction); } 添加镜头缩放结合之前对于透视投影的学习,我们可以通过鼠标滚轮修改透视角来缩放镜头。这里设置滚轮回调函数:
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) { if(fov > 1.0f && fov < 45.0f) fov -= yoffset; else if(fov <= 1.0f) fov = 1.0f; else if(fov >= 45.0f) fov = 45.0f; } 滚轮的y偏移告诉我们垂直方向偏移了多少,减去这段偏移来设置透视角,并将透视角设置在我们设定的45度以内,同时不能小于1度。
接着,将修改的透视角应用到投影矩阵中:
别忘了在渲染循环中使用回调函数:
glfwSetScrollCallback(window, scroll_callback); 这里给出原文源码参考:Code,以及效果的实现:结果。
当然,为了方便之后的学习,我们像Shader.h一样将摄像机的相关代码封装起来:Camera.h
最后,请多多参考原文:https://learnopengl.com/Getting-started/Camera。