【OpenGL学习】camera and camera control

news2024/11/15 10:17:41

摄像机

游戏中的相机可以理解为与现实中的相机类似,可以捕获对应的游戏画面。Camera在游戏引擎中一般也会展示为现实中相机的模型,使用时有两种实现方式,一种以组件形式挂载在Character上,一种则是单独存在。通常来讲,我们会对相机主体的位置和角度进行操作,以达到不同的设计目的。在本节中,我们来实现OpenGL中相机的创建。

一、观察空间(view space)

当我们讨论摄像机/观察空间(Camera/View Space)的时候,是在讨论以摄像机的视角作为场景原点时场景中所有的顶点坐标:观察矩阵把所有的世界坐标变换为相对于摄像机位置与方向的观察坐标,直观理解就是观察矩阵将摄像机移动到原点,并且把摄像机的坐标轴和世界坐标的坐标轴重合。

如何定义一个摄像机?需要定义下面的三个属性:

  • 相机(眼睛)的位置(eye position) e
  • 观察方向(gaze direction)g
  • 视点正上方的方向(view-up vector)t

有了上面呢三个属性,我们便可以构建相机坐标系了:

首先先得到相机坐标系的 w 轴,对应标准坐标系下的 z 轴:

w = − g ∣ ∣ g ∣ ∣ \pmb{w} = {-\pmb{g}\over\pmb{||g||}} w=∣∣g∣∣g

然后通过向量 tw 计算得到水平轴 u

u = t × w ∣ ∣ t × w ∣ ∣ \pmb{u} = \frac{\pmb{t \times w}}{||\pmb{t \times w}||} u=∣∣t×w∣∣t×w

最后将 wu 叉乘得到坐标系的竖直分量 v

v = w × u ∣ ∣ w × u ∣ ∣ \pmb{v} = \frac{\pmb{w \times u}}{||\pmb{w \times u}||} v=∣∣w×u∣∣w×u

这里不直接用 **t **当作摄像机坐标系的竖直分量的原因是:我们的摄像机可能是歪着头看的,如下图,这个时候如果把 t 作为竖直坐标就错了。

202004041746395

有了相机空间坐标系,要如何把顶点从模型空间转化到相机空间呢?也就是我们的 view 矩阵要怎么得到呢?

  • 将相机位置移动至原点
  • 通过旋转矩阵将二者坐标系重合(将摄像机坐标旋转到世界坐标系)

这两个操作可能不是很好理解,试想将原本和标准坐标系重合的相机移动到现在坐标系的位置:先旋转和相机坐标系重合,然后移动到视点位置,对应的变换矩阵如下:

在这里插入图片描述

那么将摄像机和标准坐标系重合对应的矩阵为上述矩阵的逆矩阵:

20200404175636223

通过将转换到世界空间的坐标顶点左乘上述矩阵,则可以将顶点转化到相机空间中。

二、创建摄像机

首先先定义一个变量存储相机的位置,把摄像机位置设置为上一节中相同的位置:

	glm::vec3 camera_pos = glm::vec3(0.0f, 0.0f, 3.0f);

不要忘记正z轴是从屏幕指向你的,如果我们希望摄像机向后移动,我们就沿着z轴的正方向移动。

接着需要定义摄像机的观察方向和视点正上方的方向:

	glm::vec3 camera_target = glm::vec3(0.0f, 0.0f, 0.0f);
	glm::vec3 camera_direction = glm::normalize(camera_pos - camera_target);
	glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
	glm::vec3 camera_right = glm::cross(up, camera_direction);
	glm::vec3 camera_up = glm::cross(camera_direction, up);

glm 允许我们用 lookAt 函数来定义 view 矩阵:

//view
	glm::mat4 view = glm::lookAt(
		camera_pos, //position
		camera_pos + camera_front //target
		up //up
	);

glm::LookAt函数需要一个位置、目标和上向量。用该 view 矩阵替换我们之前创建的 view 矩阵,运行可以得到和之前相同的结果。

三、使相机动起来

现在我们想让相机围着场景中的物体旋转,保持 y 坐标不动,让 x 和 z 坐标沿着一个圆心为原点的圆旋转:

首先回想一下极坐标的转换公式:

在这里插入图片描述
在这里插入图片描述

这里我们使用时间变量作为 θ \theta θ 来改变相机的 x 坐标和 z 坐标:

		float radius = 10.0f;
		camera_pos.x = cos(time_value) * radius; //x = r.sin(theta)
		camera_pos.z = sin(time_value) * radius; //z = r.cos(theta)
		camera_front = glm::vec3(0.0f, 0.0f, 0.0f) - camera_pos;


		view = glm::lookAt(
			camera_pos, //position
			camera_pos + camera_front, //target
			up //up
		);

如果一切正常,你会得到如下的结果:

在这里插入图片描述

四、通过键盘输入控制相机移动

如果想使用键盘控制相机的移动,首先要做的是将我们上面定义的相机属性定义为全局变量:

static glm::vec3 camera_pos = glm::vec3(0.0f, 0.0f, 3.0f);
static glm::vec3 camera_front = glm::vec3(0.0f, 0.0f, -1.0f);
static glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);

用这三个变量在渲染循环中来构造我们的 view 矩阵:

	view = glm::lookAt(
			camera_pos, //position
			camera_pos + camera_front, //target
			up //up
		);

检查下能不能正常观察到物体:

在这里插入图片描述

然后,在之前实现的 process_input 函数中,添加用于控制相机的代码:

//camera controller
	float camera_speed = 0.05f;
	if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
		camera_pos += camera_speed * glm::normalize(camera_front);
	if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
		camera_pos -= camera_speed * glm::normalize(camera_front);
	if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
		camera_pos -= camera_speed * glm::normalize(glm::cross(camera_front, up));
	if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
		camera_pos += camera_speed * glm::normalize(glm::cross(camera_front, up));

其中定义了控制相机移动速度的变量 camera_speed ,要注意的是,实际情况下根据处理器的能力不同,有些人可能会比其他人每秒绘制更多帧,也就是以更高的频率调用process_input函数,所以即使设定相同的相机速度,在不同电脑上的运行速度也可能不一样,因此需要根据我们自己的电脑手动进行调整。

当我们按下WASD键的任意一个,摄像机的位置都会相应更新。如果我们希望向前或向后移动,我们就把位置向量加上或减去方向向量。如果我们希望向左右移动,我们使用叉乘来创建一个右向量(Right Vector),并沿着它相应移动就可以了。这样就创建了使用摄像机时熟悉的横移(Strafe)效果。

注意,右向量需要进行标准化。如果没对这个向量进行标准化,最后的叉乘结果会根据camera_front返回大小不同的向量。如果我们不对向量进行标准化,我们就得根据摄像机的朝向不同加速或减速移动了,但如果进行了标准化移动就是匀速的。

五、Delta Time

TimeStep 实现的原理主要是计算并记录当前帧和上一帧之间经历的时长,然后通过这个间隔来控制我们将照相机或者物体移动时候的速度。虽然不同机器执行一次Loop函数的用时不同,但只要把每一帧里的运动,跟该帧所经历的时间相乘,就能抵消因为帧率导致的数据不一致的问题。

我们使用变量 delta_time来跟踪每一帧运行所需要的时间,用last_frame 来跟踪上一帧的时间,创建全局变量:

//delta time
static float delta_time = 0.0f;
static float last_frame = 0.0f;

然后在渲染循环中更新这两个变量:

//update delta time
		float current_frame = (float)glfwGetTime();
		delta_time = current_frame - last_frame;
		last_frame = current_frame;

最后给 camera_speed 加上这个 delta_time,这样,当两帧之间时间较长的时候,会以更快的速度进行移动来弥补,这样有了一个在任何系统上移动速度都一样的摄像机。

六、添加视角移动:

就像一般的FPS游戏一样,我们通过鼠标移动来控制视角的移动,我们之前定义的 camera_front 变量,就可以改变相机的视点朝向,所以我们通过鼠标的输入来完成对 camera_front 的旋转即可控制视角移动。

6.1 欧拉角

来简单复习一下欧拉角,欧拉角是我们使用旋转时候较为简单的一种方式。

==欧拉角(Euler Angle)==是可以表示3D空间中任何旋转的3个值,由莱昂哈德·欧拉(Leonhard Euler)在18世纪提出。一共有3种欧拉角:俯仰角(Pitch)、偏航角(Yaw)和滚转角(Roll),下面的图片展示了它们的含义:

camera_pitch_yaw_roll

俯仰角是描述我们如何往上或往下看的角,一般情况下是沿着 x 轴旋转。偏航角表示往左和往右看的程度,一般是沿着 y 轴旋转。滚转角代表如何翻滚摄像机,通常在太空飞船的摄像机中使用。每个欧拉角都有一个值来表示,把三个角结合起来就能够计算3D空间中任何的旋转向量了。

在这里插入图片描述

如上图所示,对于空间中的某个向量,代表了我们当前相机的朝向,我们假设其为 direction ,其坐标按照如图所示的方法求:

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

6.2 鼠标输入

我们通过检测鼠标的输入来进行偏航角和俯仰角的设置,水平移动影响偏航角,竖直移动影响俯仰角。原理就是,储存上一帧鼠标的位置,在当前帧中我们当前计算鼠标位置与上一帧的位置相差多少。如果水平/竖直差别越大那么俯仰角或偏航角就改变越大,也就是摄像机需要移动更多的距离。

首先隐藏鼠标的光标,并聚焦到当前窗口上,通过函数 glfwSetInputMode

glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

接着创建监听鼠标移动事件的回调函数,在此之前,先使用GLFW注册回调函数:

glfwSetCursorPosCallback(window, mouse_callback);

回调函数如下:

void mouse_callback(GLFWwindow* window, double x_pos, double y_pos)
{
	float x_offset = x_pos - last_x;
	float y_offset = last_y - y_pos; // 这里是相反的,因为y是从下向上增加,而 pitch 从下向上减小
	last_x = x_pos;
	last_y = y_pos;

	float sensitivity = 0.005f;//根据实际运动速度调节
	x_offset *= sensitivity;
	y_offset *= sensitivity;

	yaw += x_offset;
	pitch += y_offset;
	//never make pitch >= 90.f 
	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));
	camera_front = glm::normalize(front);

}

窗口第一次获取焦点的时候摄像机会突然跳一下。这个问题产生的原因是,在你的鼠标移动进窗口的那一刻,鼠标回调函数就会被调用,这时候的xpos和ypos会等于鼠标刚刚进入屏幕的那个位置。这通常是一个距离屏幕中心很远的地方,因而产生一个很大的偏移量,所以就会跳了。我们可以简单的使用一个bool变量检验我们是否是第一次获取鼠标输入,如果为true,那么我们先把鼠标的初始位置更新为xpos和ypos值,接下来的鼠标移动就会使用刚进入的鼠标位置坐标来计算偏移量了:

if (first_mouse)
	{
		last_x = x_pos;
		last_y = y_pos;
		first_mouse = false;
	}

然后来设置通过鼠标滚轮控制缩放,视野(Field of View)或fov定义了我们可以看到场景中多大的范围。当视野变小时,场景投影出来的空间就会减小,产生放大(Zoom In)了的感觉。

鼠标滚轮的回调函数如下:

void scroll_callback(GLFWwindow* window, double x_offset, double y_offset)
{
	if (fov >= 1.0f && fov <= 45.0f)
		fov -= y_offset;
	if (fov < 1.0f)
		fov = 1.0f;
	if (fov > 45.0f)
		fov = 45.0f;
}

然后我们在每一帧中更新我们的 projection 矩阵:

		projection = glm::perspective(glm::radians(fov), (float)screen_width / (float)screen_height, 0.1f, 100.0f);

注册回调函数:

glfwSetScrollCallback(window, scroll_callback);

现在可以运行查看结果了。

(视频太大这里就不放了,可以自己运行查看效果)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/176866.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Flutter 基础-下

一、shared_preferences shared_preferences 是一个本地数据缓存库&#xff08;类似 AsyncStorage&#xff09; https://pub.dev/packages/shared_preferences 使用步骤 在 pubsepc.yaml 中添加 shared_preferences 依赖安装依赖&#xff08;pub get | flutter packages get |…

Git使用Merge和Rebase区别及心得技巧

git rebase命令常常因为江湖上关于它是一种Git魔法命令的名声而导致Git新手对它敬而远之&#xff0c;但是事实上如果一个团队能够正确使用的话&#xff0c;它确实可以让生活变得更简单。在这篇文章中我们会比较git rebase和经常与之相提并论的git merge命令&#xff0c;并且在真…

【回望2022,走向2023】一个双非二本非科班的学生的旅途

目录 1.自我介绍 2.高考与暑假 梦想 幻灭 决心 暑假 3.大一上学期 4.奋进之路 5.展望未来 1.自我介绍 我是一个双非本科的大一学生&#xff0c;在2023年的新春之际&#xff0c;借着CSDN的这次年度总结活动&#xff0c;来好好回顾一下&#xff0c;2022这个平凡却又不乏…

css 2D转换

文章目录一、什么是2D转换二、rotate() 方法&#xff08;旋转&#xff09;三、translate() 方法&#xff08;位移&#xff09;四、scale() 方法&#xff08;缩放&#xff09;五、skew() 方法 &#xff08;倾斜&#xff09;一、什么是2D转换 在二维空间下对元素进行移动、缩放、…

面试官问我有没有分布式系统开发经验,我一脸懵圈…

目录 从单块系统说起团队越来越大&#xff0c;业务越来越复杂分布式出现&#xff1a;庞大系统分而治之分布式系统所带来的技术问题一句话总结&#xff1a;什么是分布式系统设计和开发经验补充说明&#xff1a;中间件系统及大数据系统 前言 现在有很多Java技术方向的同学在找工…

深度学习网络---YOLO系列

深度学习网络—YOLO yolov1&#xff08;仅适用一个卷积神经网络端到端地实现检测物体的目的&#xff09; 首先将输入图片resize到448448&#xff0c;然后送入CNN网络&#xff0c;最后处理预测的结果得到检测的目标&#xff1b;yolov1的具体思想是将全图划分为SS的格子&#xf…

结构型模式-外观模式

1.概述 有些人可能炒过股票&#xff0c;但其实大部分人都不太懂&#xff0c;这种没有足够了解证券知识的情况下做股票是很容易亏钱的&#xff0c;刚开始炒股肯定都会想&#xff0c;如果有个懂行的帮帮手就好&#xff0c;其实基金就是个好帮手&#xff0c;支付宝里就有许多的基…

智能的本质不是数据算法算力和知识

编者按&#xff1a;人机之间未解决的大部分问题不是统计问题&#xff0c;而是统计概率分布外的问题。人是自然的&#xff0c;又不是自然的&#xff0c;还是社会的&#xff0c;人类和机器都可以作为认知的载体&#xff0c;但认知的性质是不同的&#xff0c;一个是生命的认知&…

GA6-BGSM/GPRS模块介绍

GA6-BGSM/GPRS模块简介GA6-B是一个4频的GSM/GPRS模块&#xff0c;工作的频段为&#xff1a;EGSM 900MHz、 GSM850MHz和DCS1800, PCS1900。GA6-B支持GPRS multi-slot class 10/ class 8&#xff08;可选&#xff09;和 GPRS 编码格式CS-1, CS-2, CS-3 and CS-4。模块的尺寸只有2…

SelectPdf for .NET 22.0 Crack

SelectPdf for .NET 是一个专业的 PDF 库&#xff0c;可用于创建、编写、编辑、处理和读取 PDF 文件&#xff0c;而无需在 .NET 应用程序中使用任何外部依赖项。使用此 .NET PDF 库&#xff0c;您可以实现丰富的功能&#xff0c;从头开始创建 PDF 文件或完全通过 C#/VB.NET 处理…

python数据结构——栈、队列

python数据结构——栈、队列、树和算法栈栈的操作队列单端队列操作双端队列操作链表或者顺序表的使用场景&#xff1a; 当数据需要后进先出&#xff0c;来构建栈或者先进先出&#xff0c;构建队列时 栈或者队列之内的数据可以以顺序表或者链表的方式进行存储 python内置的数据…

Python基础学习 -- 模块与包

1、模块每一个py文件都可以理解为一个模块&#xff0c;模块可以增加项目的可读性2、新建一个名为算数.py文件&#xff0c;代码内容如下&#xff1a;print("算数模块被加载&#xff01;") def 加法(a,b):print(ab)3、新建一个main.py文件&#xff0c;调用模块的内容第…

Vue TypeScript 使用eval函数的坑

正常情况下&#xff0c;项目里不会用eval函数&#xff0c;但是万一要调用一个全局的js库&#xff0c;就需要用eval做些骚操作&#xff0c;这个时候编译会提示&#xff1a; is strongly discouraged as it poses security risks and may cause issues with minification. 警告是…

Java多线程(二)——ReentrantLock源码解析(补充1——从AQS中唤醒的线程)

ReentrantLock源码解析&#xff08;补充1&#xff09; 上一章仅介绍了 ReentrantLock 的常用方法以及公平锁、非公平锁的实现。这里对上一章做一些补充。主要是&#xff1a; AQS 中阻塞的线程被唤醒后的执行流程 &#xff08;本篇讲述&#xff09; 可打断的锁 lock.lockInter…

【QT5.9】与MFC对比学习笔记-感悟篇2【2023.01.23】

是对QT的分析&#xff0c;不仅局限于QT。 二者区别 天下文章一大抄&#xff0c;技术也一样。MFC是对Windows系统API进行的封装&#xff0c;是以视类与文档类为核心的框架设计。微软20年前就已经把MVC玩的很6了&#xff0c;还有控件、动态库等等技术都是微软爸爸先搞出来的。若…

Kubernetes:认识 K8s开源 Web/桌面 客户端工具 Headlamp

写在前面 分享一个 k8s 客户端开源项目 Headlamp 给小伙伴博文内容涉及&#xff1a; Headlamp 桌面/集群 Web 端安装启动导入集群简单查看集群信息 理解不足小伙伴帮忙指正 我所渴求的&#xff0c;無非是將心中脫穎語出的本性付諸生活&#xff0c;為何竟如此艱難呢 ------赫尔曼…

第八层:模板

文章目录前情回顾模板模板的概念模板的特点模板分类函数模板作用语法函数模板的使用注意事项普通函数和函数模板的区别普通函数和函数模板的调用规则优先调用普通函数空模板强调函数模板函数模板可以发生重载函数模板产生更好的匹配时模板的局限性类模板作用语法类模板实例化对…

Redis在秒杀场景的作用

秒杀业务特点&#xff1a;限时限量&#xff0c;业务系统要处理瞬时高并发请求&#xff0c;Redis是必需品。 秒杀可分成秒杀前、秒杀中和秒杀后三阶段&#xff0c;每个阶段的请求处理需求不同&#xff0c;Redis具体在秒杀场景的哪个环节起到作用呢&#xff1f; 1 秒杀负载特征…

Java-数据结构-二叉树<三>

承接上文&#xff1a; Java-数据结构-二叉树&#xff1c;一&#xff1e; Java-数据结构-二叉树&#xff1c;二&#xff1e; 一. 二叉树的简单介绍 见Java-数据结构-二叉树&#xff1c;一&#xff1e; 二. 二叉树的典型代码实现 见Java-数据结构-二叉树&#xff1c;一&#x…

4. RNN网络架构解读|词向量模型|模型整体框架|训练数据构建|CBOW和Skip-gram模型|负采样方案

文章目录RNN网络架构解读词向量模型模型整体框架训练数据构建CBOW和Skip-gram模型负采样方案RNN网络架构解读 递归神经网络实际上就是普通的神经网络的部分进行修改更新&#xff1a;实际上常用于时间序列的更新。或者就是自然处理中 X序列代表着时间序列&#xff0c;x0是一个时…