摄像机

最近在学习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;

接下来把偏移量加到全局变量pitchyaw上:

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变量检验是否第一次获取鼠标输入,若是,则把鼠标初始位置更新为xposypos值:

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.0f45.0f之间

现在须每一帧都必须把透视矩阵上传到GPU,但现在使用fov变量作为它的视野:

projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);

记得注册鼠标滚轮回调函数:

glfwSetScrollCallback(window, scroll_callback);

摄像机类

最后的最后,我们把这个摄像机进行一次封装,以便以后调用。摄像机类可以在这里找到。

摄像机完整的项目文件可以在这里找到。

如果本项目对您有所帮助,希望能够获得您的 star。万分感谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容