最近在学习OpenGL,把学习的一些过程写在这里,希望与大家共同分享讨论。欢迎光临我的个人网站Orient一起讨论学习。这里是我的GitHub,如果您喜欢,不妨点个赞?☺
在OpenGL中并没有摄像机(Camera)的概念,因此引入一个虚拟的摄像机,并自定义一个摄像机类。
摄像机/观察空间
在讨论摄像机/观察空间(Camera/View Space),其实就讨论以摄像机的视角作为场景原点时场景中所有顶点的坐标。使用观察矩阵可以把所有的世界坐标变幻为相对于摄像机位置与方向的观察坐标。因此定义一个摄像机,我们需要它在世界中的位置、观察方向、一个指向它右侧的向量以及一个指向它上方的向量。其实就是以摄像机为原点创建了一个如下图所示的坐标系:
1、摄像机位置
获取摄像机位置很简单,就是世界空间中一个指向摄像机位置的向量。我们仍然设置为之前的摄像机位置:
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
注意Z轴的方向
2、摄像机方向
我们先让摄像机指向世界场景原点: (0, 0, 0)。摄像机方向由摄像机的坐标减去它指向点的坐标得到:
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);
注意:其实摄像机方向与摄像机的指向刚好相反
3、右轴
我们需要获取一个右向量(Right Vector),它代表摄像机空间的X轴正方向。右向量的获取:先定义一个世界空间坐标中的上向量(Up Vector),然后将其与上步得到的摄像机方向向量进行叉乘:
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
cameraRight = glm::normalize(glm::cross(up, cameraDirection));
如果两向量的叉乘顺序交换将会得到方向相反的一个X轴负方向向量
4、上轴
将摄像机方向向量与右向量叉乘即可得到:
glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);
我们已经创建了所有构成观察/摄像机空间的向量。接下来我们使用这些向量就可以构建一个LookAt
矩阵
LookAt
使用矩阵的好处之一:如果使用了3个相互垂直的轴定义了一个坐标空间,那么可以用这3个轴外加一个平移向量来创建一个矩阵,并且可以用这个矩阵乘以任何向量来将其变换到那个坐标空间。LookAt
矩阵正是如此。
其中R是右向量,U是上向量,D是方向向量,P是摄像机位置向量。(注意:位置向量是相反的,因为我们最终希望把世界平移到与我们自身移动的相反方向)这个LookAt
矩阵可以很高效地把所有世界坐标变幻到刚定义的观察空间。
GLM
提供了这样的支持:我们只需要定义一个摄像机位置,一个目标位置和一个表示世界空间中上向量的向量,GLM
就会创建一个LookAt矩阵,我们把它当作我们的观察矩阵:
glm::mat4 view;
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
自由移动摄像机
首先我们设置一个摄像机系统,定义一些变量:
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);
LookAt
:
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
位置为之前定义的cameraPos,方向是当前位置加上刚定义的方向向量。这样就能保证无论怎么移动,摄像机都会注视着目标方向。
在GLFW键盘输入定义的函数processInput
中添加几个需要检查的按键命令:
void processInput(GLFWwindow *window)
{
...
float cameraSpeed = 0.05f; // adjust accordingly
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;
}
这里对右向量进行了标准化。如果没有对这个向量进行标准化,最后的叉乘结果很根据cameraFront
变量返回大小不同的变量。摄像机移动的速度将会发生改变。标准化后,摄像机移动就是匀速的
移动速度
上面的移动速度是个常量,讲道理是会一直匀速移动没有问题。但实际情况是每个处理器的能力不同,有些人每秒绘制的帧数可能要比别人多,或者少,也就是以更高/低的频率调用processInput
函数。结果就是有些人可能移动很快,有些很慢。
图形程序和游戏通常会跟踪一个时间差(Deltatime)变量,它存储了渲染上一帧所用时间。我们把所有速度都去乘以deltaTime
值,结果就是如果我们的deltaTime
很大,就意味着上一帧的渲染花费了更多时间,所以这一帧的速度需要更高去平衡渲染所花去的时间。因此我们得到一个相对稳定的移动速度:
float deltaTime = 0.0f; // 当前帧与上一帧时间差
float lastTime = 0.0f; // 上一帧的时间
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
void processInput(GLFWwindow * window)
{
float cameraSpeed = 2.5f * deltaTime;
}
因此我们的得到了一个更流畅点的摄像机系统。
视角移动
我们根据鼠标的输入改变cameraFront向量,从而可以改变摄像机的视角。
摄像机视角的改变可以通过改变欧拉角:俯仰角(Pitch)、偏航角(Yaw)和滚转角(Roll):
俯仰角描述我们如何往上或往下看的角。偏航角表示我们往左和往右看的程度。滚转角代表我们如何翻滚摄像机。在这里我们不涉及翻滚角
我们通过三角函数来将角度转换为方向向量:
基于俯仰角:
direction.y = sin(glm::radians(pitch)); // 先把角度转为弧度
direction.x = cos(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)); /
基于俯仰角与偏航角:
direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw)); // 先把角度转为弧度
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
鼠标输入
偏航角与俯仰角是通过鼠标/手柄移动获得的。水平移动影响偏航角,竖直移动影响俯仰角。原理就是:存储上一帧鼠标的位置,在当前帧中计算鼠标位置与上一帧的位置相差多少。如果水平/竖直差别越大那么俯仰角或偏航角就改变越大。
首先告诉GLFW隐藏光标并捕捉它:
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
接下来申明一个鼠标监听函数:
/*
*p2、p3:鼠标当前位置
*/
void mouse_callback(GLFWwindow *window, double xpos, double ypos)
当用GLFW注册了回调函数后,鼠标一移动mouse_callback
函数就会被调用:
glfwSetCursorPosCallback(window, mouse_callback);
在处理FPS风格摄像机的鼠标输入时,必须在最终获取方向向量之前做下面几步:
- 计算鼠标距上一帧的偏移量
- 把偏移量添加到摄像机的俯仰角和偏航角中
- 对偏航角和俯仰角进行最大和最小值的限制
- 计算方向向量
第一步是计算鼠标自上一帧的偏移量。须先在程序中存储上一帧鼠标的位置,这里将其设置在屏幕中心(屏幕尺寸:800 x 600):
float lastX = 400, lastY = 300;
然后在鼠标回调函数中计算当前帧和上一帧鼠标位置的偏移量:
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // y坐标是从底部往顶部依次增大
lastX = xpos;
lastY = ypos;
float sensitivity = 0.05f; //灵敏度
xoffset *= sensitivity;
yoffset *= sensitivity;
接下来把偏移量加到全局变量pitch
和yaw
上:
yaw += xoffset;
pitch += yoffset;
第三部给摄像机添加一些限制,防止其发生奇怪的移动(同时避免一些奇怪的问题)。对于俯仰角,要让用户不能看向高于89度的地方(在90度时视角发生逆转,所以把89度作为极限),同样也不允许小于-89度。在值超过极限值的时候将其改为极限值来实现:
if (pitch > 89.0f)
pitch = 89.0f;
if (pitch < -89.0f)
pitch = -89.0f;
最后一步,通过俯仰角和偏航角计算得到真正的方向向量:
glm::vec3 front;
front.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw));
front.y = sin(glm::radians(pitch));
front.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
cameraFront = glm::normalize(front);
为了防止在运行代码开始,窗口第一次获取焦点时摄像机的抖动,设置一个bool
变量检验是否第一次获取鼠标输入,若是,则把鼠标初始位置更新为xpos
和ypos
值:
if (firstMouse) //这个变量初始值是true
{
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 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));
cameraFront = glm::normalize(front);
}
缩放
视野(Field of View)或fov
定义了我们能够看到的场景范围。当视野变小时,场景投影出来的空间就会见效,产生放大(Zoom In)的感觉,这里使用鼠标的滚轮来放大。同样申明一个鼠标滚轮的回调函数:
void scroll_callback(GLFWwindow *window, double xoffset, double yoffset)
{
if (fov >= 1.0f && fov <= 45.0f)
fov -= yoffset;
if (fov <= 1.0f)
fov = 1.0f;
if (fov >= 45.0f)
fov = 45.0f;
}
以上设置了缩放范围限制在1.0f
和45.0f
之间
现在须每一帧都必须把透视矩阵上传到GPU,但现在使用fov变量作为它的视野:
projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);
记得注册鼠标滚轮回调函数:
glfwSetScrollCallback(window, scroll_callback);
摄像机类
最后的最后,我们把这个摄像机进行一次封装,以便以后调用。摄像机类可以在这里找到。
摄像机完整的项目文件可以在这里找到。
如果本项目对您有所帮助,希望能够获得您的 star。万分感谢!