【OpenGL】笔记八、摄像机(View矩阵)

流程

上回说到了MVP矩阵的使用,但是在view矩阵的设置时比较粗略,view矩阵变换本质是转换到相机坐标系,是根据相机的位置变换的,上次我们只是将相机往z轴正方向移了三个单位而已,但是现实的情况是相机可能会以各种方式移动,现在就来正式讨论相机坐标系的变换

首先要知道,相机坐标系需要三个属性来定义,分别是位置坐标(原点),看向的方向(-z轴)和向上向量(y轴),这是默认的,知道了这些,就能根据叉乘等算出整个坐标系的轴的向量,然后就能根据这些求出世界坐标系到相机坐标系的变换了(知道了相机位置,求平移到原点的矩阵很容易,那旋转矩阵怎么求呢?逆向思维,先求相机坐标系到世界坐标系的变换,得到它的逆矩阵,然后根据旋转矩阵是正交矩阵,而正交矩阵的转置等于它的逆矩阵这个性质就可以求得我们要的矩阵了)

所以,可以调用lookat函数来创建view矩阵:

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));//位置、目标和上向量

接下来还能让相机环绕:

float radius = 10.0f;
float camX = sin(glfwGetTime()) * radius;
float camZ = cos(glfwGetTime()) * radius;
glm::mat4 view;
view = glm::lookAt(glm::vec3(camX, 0.0, camZ), glm::vec3(0.0, 0.0, 0.0), glm::vec3(0.0, 1.0, 0.0)); 

效果:

接下来我们要实现利用键盘控制相机平移的效果:

首先是规定相机的朝向,如果只改变相机位置,不改变相机目标的话,它的视角方向依然会朝向目标,所以首先把它的目标定为当前位置加一个方向,这样平移相机时它的朝向就不会变了

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 = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);

接下来我们在之前的processInput函数里加入输入检测,通过WASD来改变相机位置:

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;
}

效果:

实际情况下根据处理器的能力不同,有些人可能会比其他人每秒绘制更多帧,也就是以更高的频率调用processInput函数。结果就是,根据配置的不同,有些人可能移动很快,而有些人会移动很慢。当你发布你的程序的时候,你必须确保它在所有硬件上移动速度都一样。

所以我们记录每帧渲染的时间差,把它乘以我们的移动速度,这样渲染耗时越大,速度也就越快,就能保持所有硬件一致了

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;
  ...
}

解决完键盘移动,现在该考虑鼠标的作用了,之前的平移并不能帮助我们转动相机视角,现在我们来尝试利用捕捉鼠标位置来转动相机,目前只考虑pitch和yaw:

当pitch角确定时,由图:

得到相机目标方向为:

direction.y = sin(glm::radians(pitch)); 
direction.x = cos(glm::radians(pitch));
direction.z = cos(glm::radians(pitch));

同理对于yaw角,它的示意图如下

我们将其叠加在pitch角的结果上

direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw)); // 译注:direction代表摄像机的前轴(Front),这个前轴是和本文第一幅图片的第二个摄像机的方向向量是相反的
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));

有了两个角的公式,我们接下来将鼠标的移动转换为两个角的输入

首先隐藏光标并捕捉它,让它不会离开窗口

glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

接下来让GLFW监听鼠标移动事件(使用一个和窗口大小改变类似的回调函数)

void mouse_callback(GLFWwindow* window, double xpos, double ypos);

接下来注册这个函数

glfwSetCursorPosCallback(window, mouse_callback);

准备好了之后我们计算鼠标自上一帧的偏移量。我们必须先在程序中储存上一帧的鼠标位置,我们把它的初始值设置为屏幕的中心(屏幕的尺寸是800×600):

float lastX = 400, lastY = 300;

然后定义初始的yaw为-90,pitch为0(因为按照原公式来说,都设为0的话初始是向x轴正方向看的,我们需要的是初始向-z轴方向看):

float yaw = -90.0f;
float pitch = 0.0f;

一切准备就绪后就开始编写我们的鼠标回调函数吧:

首先为了防止鼠标进入屏幕时跳变,我们判断是不是第一次进入屏幕,是的话就设置初始位置值为当前值,这样偏移量就为0,就不会跳变

void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
    if(firstMouse)
    {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }
    ...

接下来计算相比上一帧的偏移量,并将它乘以一个敏感度,赋给偏移量,加在yaw和pitch角上(glfwSetCursorPosCallback返回给mouse_callback函数的 (x,y) 是鼠标相对于窗口左【上】角的位置,所以需要将 (ypos – lastY) 取反

    ...
    float xoffset = xpos - lastX;
    float yoffset = lastY - ypos; 
    lastX = xpos;
    lastY = ypos;

    float sensitivity = 0.05;
    xoffset *= sensitivity;
    yoffset *= sensitivity;

    yaw   += xoffset;
    pitch += yoffset;
    ...

然后一般FPS游戏中都不能抬低头超过90°,所以也要对pitch角做一个限制

    ...
    if(pitch > 89.0f)
        pitch = 89.0f;
    if(pitch < -89.0f)
        pitch = -89.0f;
    ...

最后就是用之前的公式根据yaw和pitch角计算相机方向

    ...
    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);
}

效果:

同样的,我们也可以通过鼠标滚轮来改变fov:

glfwSetScrollCallback(window, scroll_callback);
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;
}

效果:

2. 练习

2.1 看看你是否能够修改摄像机类,使得其能够变成一个真正的FPS摄像机(也就是说不能够随意飞行);你只能够呆在xz平面上

在鼠标回调函数中去除对pitch的赋值即可,让它保持初始值0

2.2 试着创建你自己的LookAt函数,其中你需要手动创建一个我们在一开始讨论的观察矩阵。用你的函数实现来替换GLM的LookAt函数,看看它是否还能一样地工作

因为mat4在内存中按列序存储,所以我们可以这样按序赋值

glm::mat4 lookat(glm::vec3 eyePos, glm::vec3 cameraTarget, glm::vec3 Up){
	glm::vec3 direction = eyePos - cameraTarget;
	glm::vec3 right = glm::cross(Up, direction);
	glm::mat4 out = glm::mat4(1.0f);
	out[0][0] = right.x; out[1][0] = right.y; out[1][0] = right.z;
	out[0][1] = Up.x; out[1][1] = Up.y; out[1][1] = Up.z;
	out[0][2] = direction .x; out[1][2] = direction .y; out[1][2] = direction .z;
	out = glm::translate(out, -eyePos);
	return out;
}

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
心中带点小风骚的头像心中带点小风骚普通用户
上一篇 2022年5月22日
下一篇 2022年5月22日

相关推荐