Unity相机控制

相机的控制无非移动和旋转,每种操作各3个轴6个方向,一共12种方式。在某些需要快速验证的项目或Demo里常常需要丝滑的控制相机调试效果。相机控制虽然不是什么高深的技术,但是要写的好用还是很磨人的。

锁定Z轴的旋转

一个自由的相机可以绕 X,Y,Z 轴旋转,正常情况下用6个按键加上 transform.Rotate api 就可以搞定了。这里要注意的是要使用本地坐标系,transform.Rotate 默认就是本地坐标系。比如我们可以用上下左右方向键和鼠标左键来控制相机的旋转。

void Update()
{if (Input.GetMouseButton(0)){var axis_x = Input.GetAxis("Mouse X") * 10;var axis_y = Input.GetAxis("Mouse Y") * 10;transform.localRotation *= Quaternion.Euler(-axis_y, axis_x, 0);}if (Input.GetKey(KeyCode.UpArrow)){transform.Rotate(Vector3.left, Time.deltaTime * 100);}else if (Input.GetKey(KeyCode.DownArrow)){transform.Rotate(Vector3.right, Time.deltaTime * 100);}if (Input.GetKey(KeyCode.LeftArrow)){transform.Rotate(Vector3.down, Time.deltaTime * 100);}else if (Input.GetKey(KeyCode.RightArrow)){transform.Rotate(Vector3.up, Time.deltaTime * 100);}
}

但是在某些情况下我希望实现一种类似第一人称的视角,既相机可以左右看,上下看,但是不能歪头,也就是要锁定 Z 轴的旋转。即便上面我们没有 Z 方向的旋转,但是实际上 X 轴和 Y 轴的旋转也会引入 Z 轴的旋转,让人感觉相机极难控制,在不添加 Z 轴旋转的情况下,相机很容易就歪了,还很难正回来。比如我们按上左下右的顺序旋转相机,当相机回到原点时,镜头已经歪到姥姥家了。

在这里插入图片描述

锁定 Z 轴旋转就是把 Z 向角度设置为0,我们添加一个锁定 Z 轴的函数。

private void LockZRotate()
{var euler = transform.eulerAngles;euler.z = 0;transform.eulerAngles = euler;
}

然后在 Update 的最后调用 LockZRotate 即可。但是这样也会有问题,绕 X 轴的旋转在 ±90° 范围内是正常的,一旦到达 90°,Z 轴向正上或正下,再继续转就转不动了,视角会向电风扇一样疯转旋转。

在这里插入图片描述

虽然我们没有旋转 Z 轴,但是 Unity 会根据旋转重新解算欧拉角,这种情况下继续旋转,经过 Unity 的解算,Z 轴上的角度就不是 0 了,但是我们又立刻将 Z 轴的角度置 0 了,导致 Unity 无法继续旋转,最终变成了直升机效果。

要迈过这道坎我们可以用世界坐标系去旋转。也就是给 Rotate 函数加上 Space.World 参数,对于鼠标旋转的情况,只需要将四元数的乘法顺序调换一下就可以了。

void Update()
{if (Input.GetMouseButton(0)){var axis_x = Input.GetAxis("Mouse X") * 10;var axis_y = Input.GetAxis("Mouse Y") * 10;transform.rotation = Quaternion.Euler(-axis_y, axis_x, 0) * transform.rotation;}if (Input.GetKey(KeyCode.UpArrow)){transform.Rotate(Vector3.left, Time.deltaTime * 100, Space.World);}else if (Input.GetKey(KeyCode.DownArrow)){transform.Rotate(Vector3.right, Time.deltaTime * 100, Space.World);}if (Input.GetKey(KeyCode.LeftArrow)){transform.Rotate(Vector3.down, Time.deltaTime * 100, Space.World);}else if (Input.GetKey(KeyCode.RightArrow)){transform.Rotate(Vector3.up, Time.deltaTime * 100, Space.World);}LockZRotate();
}

Rotate 函数 Space.World 参数是指定旋转轴的坐标空间的,Vector3.up + Space.World 相当于 transform.up + Space.Self
在这里插入图片描述
可以看到可以正常绕 X 轴旋转超过 90°,而且相机始终是正的,天空始终在画面上面。似乎是正常了,严格来说是当相机的 X 轴和世界的 X 轴重合的时候是正常的,也就是说其实还是不对。

当我们先绕 Y 轴旋转 90° 后,此时相机的 Z 轴与世界的 X 轴重合,此时当我们再想绕 X 轴旋转时,但实际上面的代码变成了绕相机的 Z 轴旋转,但是 Z 轴的旋转被我们锁定了,根本转不动,于是相机 X 方向的旋转就被锁死在这里了。这只是最极端的情况,事实上当相机的 X 轴偏离世界的 X 轴时,X 方向的旋转就都不正常了。
在这里插入图片描述
有一种办法是把 X 轴的旋转限制在 ±90° 范围内,也就是不让人“倒立”。可是妥协不是我想要的,我想要倒立,倒立过去之后还要保持镜头是正的。

回到最初按上左下右顺序旋转相机的例子,当我们在编辑模式下的 Inspector 面板中重复这个操作时,一切却很正常,相机回到了原点,镜头也没有歪。

唉?什么情况?

这并不是什么玄学,问题还是那个问题,Unity 会重新解算欧拉角。当我们在 Inspector 面板里面操作时,转哪个轴就只转那一个轴,不会重新解算,也不会动到其他轴,井水不犯河水。

这就意味这我们也可以模拟这个过程,手动记录下相机的初始欧拉角,然后转哪个轴就加减哪个角,最后将欧拉角赋值给相机就可以了。让我们重新写一个函数来专门负责旋转。

public Vector3 euler;
private void RotateTransformAngle(float x = 0, float y = 0, float z = 0)
{euler.x = (euler.x + x) % 360;euler.y = (euler.y + y) % 360;euler.z = 0;transform.localEulerAngles = euler;
}

然后将旋转也替换成这个函数,RotateTransformAngle 函数已经锁定了 Z 轴,所以 LockZRotate 函数也不用再调用了。

void Update()
{if (Input.GetMouseButton(0)){var axis_x = Input.GetAxis("Mouse X") * 10;var axis_y = Input.GetAxis("Mouse Y") * 10;RotateTransformAngle(-axis_y, axis_x);}if (Input.GetKey(KeyCode.UpArrow)){RotateTransformAngle(x: -Time.deltaTime * 100);}else if (Input.GetKey(KeyCode.DownArrow)){RotateTransformAngle(x: Time.deltaTime * 100);}if (Input.GetKey(KeyCode.LeftArrow)){RotateTransformAngle(y: -Time.deltaTime * 100);}else if (Input.GetKey(KeyCode.RightArrow)){RotateTransformAngle(y: Time.deltaTime * 100);}//LockZRotate();
}

现在上左下右确实没问题了,镜头不会再歪了,但是新的问题也出现了。当相机绕 X 轴旋转 180° 时,我们真的“倒立”了,不能说没有歪,简直歪到极点了。
在这里插入图片描述

要让相机镜头始终是正的,实际上等价于让相机的 Y 轴始终朝上,可以把 Y 轴想象成人的头,所谓的“正”,也就是人头冲上。有什么东西是始终朝上的吗?当然有,那就是世界的 Y 轴。我们可以加一个判断,当相机的 Y 轴和世界的 Y 轴反向时,将相机的 Y 轴反转,我们可以使用点乘来实现这个判断。

private void RotateTransformAngle(float x = 0, float y = 0, float z = 0)
{euler.x = (euler.x + x) % 360;euler.y = (euler.y + y) % 360;euler.z = 0;transform.localEulerAngles = euler;if (Vector3.Dot(transform.up, Vector3.up) < 0){transform.rotation = Quaternion.LookRotation(transform.forward, -transform.up);}
}

在这里插入图片描述

嗯,现在我们会翻跟斗,但是不会倒立了。如果我们始终锁定 Z 轴,到这里其实就可以结束了。但问题是并不是所有情况下我们都应该锁定 Z 轴,万一需要 Z 轴的旋转呢?我们可以加一个开关来控制 Z 轴是否锁定。

public bool lockz;
private void RotateTransformAngle(float x = 0, float y = 0, float z = 0)
{euler.x = (euler.x + x) % 360;euler.y = (euler.y + y) % 360;if (lockz){euler.z = 0;}else{euler.z = (euler.z + z) % 360;}transform.localEulerAngles = euler;if (lockz && (Vector3.Dot(transform.up, Vector3.up) < 0)){transform.rotation = Quaternion.LookRotation(transform.forward, -transform.up);}
}

然后我们还需要再添加两个按键 QW 来旋转 Z 轴,并在旋转 Z 轴时,自动解锁 Z 轴旋转,顺便加一个按键 z 来重新锁定 Z 轴。

void Update() {...if (Input.GetKey(KeyCode.Q)){lockz = false;RotateTransformAngle(z: -Time.deltaTime * 100);}if (Input.GetKey(KeyCode.W)){lockz = false;RotateTransformAngle(z: Time.deltaTime * 100);}if (Input.GetKeyDown(KeyCode.Z)){lockz = true;}
}

这下总没问题了,吧?当我们先绕 X 轴旋转 180°,然后再转动 Z 轴时,神奇的事情发生了,瞬间天地倒转,又倒立了。

【图】

这个问题的原因很简单,因为当我们绕 X 轴旋转超过 90° 时,Y 轴发生了一次反转,也就是相机绕 Z 轴旋转了 180°,但是这个信息并未被记录到我们手动管理的欧拉角中。此时当我们绕 Z 轴旋转时,其实是基于未反转的 Z 方向角度在修改,所以镜头会突然倒转。

当锁定 Z 轴时,Z 方向的欧拉角只有可能是 0° 或 180°,要解决这个问题,我们需要一个只有 0 和 1 两种状态的变量来记录相机 Y 轴的翻转状态。1 bit 二进制数就刚好满足我们的需求,只需要不断的加一,它就会在 0 和 1 之间不断翻转。之所以要记录下这个状态,是因为当我们重新锁定相机时,需要将 Z 向欧拉角恢复到解锁前的状态,而不是简单的直接置 0。

public bool lockz;
public byte flipz;
private void RotateTransformAngle(float x = 0, float y = 0, float z = 0)
{euler.x = (euler.x + x) % 360;euler.y = (euler.y + y) % 360;euler.z = 0;if (lockz){euler.z = (flipz & 0x1) * 180;}else{euler.z = (euler.z + z) % 360;}transform.localEulerAngles = euler;if (lockz && (Vector3.Dot(transform.up, Vector3.up) < 0)){transform.rotation = Quaternion.LookRotation(transform.forward, -transform.up);euler.z = (flipz++ & 0x1) * 180;}
}

在这里插入图片描述

好了,这次是真的没有问题了。

移动速度加成

相机移动相比于旋转要简单的多,直接使用 transform.Translate 函数就可以了,而且每个方向都可以自由移动。

与旋转不同的是移动的范围要更广阔,对于旋转,每个轴的旋转角度只会在 0 ~ 360° 之间,但是移动的范围几乎是无限的。这就带来了一个问题,当我们想移动很远的距离时,要“走”很久才能到。

简单,走快点就好了。但是也不能一开始就走很快,因为我们并不能确定当用户按下移动键时,是想去很远的地方,还是只想凑近一点。因此启动速度不能太快,否则我们很难准确控制相机到达想去的地方。

解决这个问题我们可以记录下用户按下移动键的时长,然后根据按键按下的时间计算一个移动速度加成。刚开始时没有任何加成,如果用户一直按着键盘不撒手,那就逐渐给一个更大加成,让相机移动的越来越快。最后当加成到达一个上限时,保持住不在变大。

你可能已经想到了,这不就是一个分段函数吗?的确,我们需要的确实是一个分段函数。
y={1x<axa<x<b10x>by = \begin{cases} 1 & x < a \\ x & a < x < b \\ 10 & x > b \end{cases} y=1x10x<aa<x<bx>b
在这里插入图片描述

但是分段函数是不平滑的,而且我们还想让变化有一些非线性。有这么一个函数,在 x=0x=0x=0 附近函数值为 111,随着 xxx 的增大,函数值逐渐增大,最后在 +∞+\infty+ 处趋于 111
y=e−1x2y=e^{-\frac{1}{x^2}} y=ex21
在这里插入图片描述
因为函数值在 [0,1][0,1][0,1] 范围内,因此我们很容易把他缩放到 [1,M][1, M][1,M] 范围内。同时我们还可以加一些参数来调整函数的增长速度和底部宽度。
y=1+(M−1)e−txry=1+(M-1)e^{-\frac{t}{x^r}} y=1+(M1)exrt
除了键盘操作,用鼠标滚轮来前后移动相机也是实用的操作,此时我们在算加成时,不是用滚动时间,而是用滚轮连续同方向滚动的距离。

goto与环绕

除了移动与旋转,我们还可以实现一些快捷操作,比如用鼠标点击一个点,让相机看向并移动到这个点“前面”,或者移动相机变成环绕这个点移动。这些功能的实现并不难,使用 LookAtRotateAround 就能实现了。需要注意的是要让相机平滑的看向并移动到目标点,需要进行插值,否则镜头会生硬的跳过去。

最终我们会实现下面的功能:

鼠标按键功能
左键上下左右旋转相机
上下左右旋转相机
滚轮X Y Z绕指定轴旋转相机
左键A D L-Shift Space相机上下左右环绕点击的点
W S A D L-Shift Space相机前后左右上下移动
滚轮相机前后移动
中键相机上下左右移动
右键相机Goto点击的点
Z锁定 Z 轴旋转

源码

源码以 .unitypackage 的形式放到了CSDN,可以直接导入使用。

https://download.csdn.net/download/puss0/91565511

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

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

相关文章

vue2 使用liveplayer加载视频

vue2 使用liveplayer加载视频 官网: https://www.liveqing.com/docs/manuals/LivePlayer.html支持WebRTC/MP4播放;支持m3u8/HLS播放;支持HTTP-FLV/WS-FLV/RTMP播放;支持直播和点播播放;支持播放器快照截图;支持点播多清晰度播放;支持全屏或比例显示;自动检测IE浏览器兼容播放;支…

JavaScript语法树简介:AST/CST/词法/语法分析/ESTree/生成工具

AST简介 在平时的开发中&#xff0c;经常会遇到对JavaScript代码进行检查或改动的工具&#xff0c;例如ESLint会检查代码中的语法错误&#xff1b;Prettier会修改代码的格式&#xff1b;打包工具会将不同文件中的代码打包在一起等等。这些工具都对JavaScript代码本身进行了解析…

Java函数式编程之【基本数据类型流】

一、基本数据类型与基本数据的包装类 在Java编程语言中&#xff0c;int、long和double等基本数据类型都各有它们的包装类型Integer、Long和Double。 基本数据类型是Java程序语言内置的数据类型&#xff0c;可直接使用。 而包装类型则归属于普通的Java类&#xff0c;是对基本数据…

.NET Core部署服务器

1、以.NET Core5.0为例&#xff0c;在官网下载 下载 .NET 5.0 (Linux、macOS 和 Windows) | .NET 根据自己需求选择x64还是x86&#xff0c;记住关键下载完成还需要下载 Hosting Bundel &#xff0c;否则不成功 2、部署https将ssl证书放在服务器上&#xff0c;双击导入&#…

YOLO---04YOLOv3

YOLOV3 论文地址&#xff1a;&#xff1a;【https://arxiv.org/pdf/1804.02767】 YOLOV3 论文中文翻译地址&#xff1a;&#xff1a;【YOLO3论文中文版_yolo v3论文 中文版-CSDN博客】 YOLOv3 在实时性和精确性在当时都是做的比较好的&#xff0c;并在工业界得到了广泛应用 …

Qt知识点3『自定义属性的样式表失败问题』

问题1&#xff1a;自定义类中的自定义属性&#xff0c;如何通过样式表来赋值除了QT自带的属性&#xff0c;我们自定义的类中如果有自定义的静态属性&#xff0c;也可以支持样式表&#xff0c;如下 &#xff1a; Q_PROPERTY(QColor myBorderColor READ getMyBorderColor WRITE s…

RDQS_c和RDQS_t的作用及区别

&#x1f501; LPDDR5 中的 RDQS_t 和 RDQS_c — 复用机制详解 &#x1f4cc; 基本角色 引脚名 读操作&#xff08;READ&#xff09;作用 写操作&#xff08;WRITE&#xff09;作用&#xff08;当启用Link ECC&#xff09; RDQS_t Read DQS True&#xff1a;与 RDQS_c…

测试分类:详解各类测试方式与方法

前言&#xff1a;为什么要将测试进行分类呢&#xff1f;软件测试是软件生命周期中的⼀个重要环节&#xff0c;具有较高的复杂性&#xff0c;对于软件测试&#xff0c;可以从不同的角度加以分类&#xff0c;使开发者在软件开发过程中的不同层次、不同阶段对测试工作进行更好的执…

新手docker安装踩坑记录

最近在学习docker&#xff0c;安装和使用折腾了好久&#xff0c;在这里记录一下。下载# 依赖安装 sudo apt update sudo apt install -y \ca-certificates \curl \gnupg \lsb-release# 使用清华镜像源&#xff08;Ubuntu 24.04 noble&#xff09; echo \"deb [arch$(dpkg …

TOGAF指南1

1.TOGAF标准简介 TOGAF&#xff08;The Open Group Architecture Framework&#xff09;就像是一个企业架构的“操作手册”。它帮助企业设计、搭建和维护自己的“系统地图”&#xff0c;确保不同部门、技术、业务目标能像齿轮一样协调运转。 它的核心是&#xff1a; 用迭代的方…

[Linux入门] Linux 防火墙技术入门:从 iptables 到 nftables

目录 一、防火墙基础&#xff1a;netfilter 与 iptables 的关系 1️⃣什么是 netfilter&#xff1f; 2️⃣什么是 iptables&#xff1f; 二、iptables 核心&#xff1a;五链四表与规则体系 1️⃣什么是 “链”&#xff08;Chain&#xff09;&#xff1f; 2️⃣ 什么是 “…

函数fdopendir的用法

以下是关于 fdopendir 函数的详细解析&#xff0c;结合其核心功能、参数说明及典型应用场景&#xff1a;&#x1f50d; ‌一、函数功能与原型‌‌核心作用‌将已打开的目录文件描述符&#xff08;fd&#xff09;转换为目录流指针&#xff08;DIR*&#xff09;&#xff0c;用于后…

[源力觉醒 创作者计划]_文心4.5开源测评:国产大模型的技术突破与多维度能力解析

声明&#xff1a;文章为本人真实测评博客&#xff0c;非广告&#xff0c;并没有推广该平台 &#xff0c;为用户体验文章 一起来轻松玩转文心大模型吧&#x1f449; 文心大模型免费下载地址 一、引言&#xff1a;文心4.5开源——开启多模态大模型新时代 2025年6月30日&#x…

微信小程序无法构建npm,可能是如下几个原因

安装位置的问题&#xff0c;【npm安装在cd指定位置】小程序缓存的问题退出小程序&#xff0c;重新构建即可

从 MyBatis 到 MyBatis - Plus:@Options 注解的那些事儿

在 MyBatis 以及 MyBatis - Plus 的开发过程中&#xff0c;注解的使用是提升开发效率和实现特定功能的关键。今天我们就来聊聊 Options 注解&#xff0c;以及在 MyBatis - Plus 中它的使用场景和替代方案。 一、MyBatis 中的 Options 注解 在 MyBatis 框架中&#xff0c;Option…

转换图(State Transition Diagram)和时序图(Sequence Diagram)画图流程图工具

针对程序员绘制状态转换图&#xff08;State Transition Diagram&#xff09;和时序图&#xff08;Sequence Diagram&#xff09;的需求&#xff0c;以下是一些好用的工具推荐&#xff0c;涵盖在线工具、桌面软件和基于文本的工具&#xff0c;适合不同场景和偏好。这些工具在易…

基于php的在线酒店管理系统(源代码+文档+PPT+调试+讲解)

课题摘要在旅游住宿行业数字化转型的背景下&#xff0c;传统酒店管理存在房态更新滞后、预订渠道分散等问题。基于 PHP 的在线酒店管理系统&#xff0c;凭借其开发高效、兼容性强的特点&#xff0c;构建集客房管理、预订处理、客户服务于一体的综合性管理平台。 系统核心功能包…

视频质量检测中卡顿识别准确率↑32%:陌讯多模态评估框架实战解析

原创声明本文为原创技术解析&#xff0c;核心技术参数与架构设计引用自《陌讯技术白皮书》&#xff0c;禁止未经授权的转载与改编。一、行业痛点&#xff1a;视频质量检测的现实挑战在实时流媒体、在线教育、安防监控等领域&#xff0c;视频质量直接影响用户体验与业务可信度。…

流式输出阻塞原因及解决办法

流式输出不懂可看这篇文章&#xff1a;流式输出&#xff1a;概念、技巧与常见问题 正常情况&#xff0c;如下代码所示&#xff1a; async def event_generator():# 先输出数字1yield "data: 1\n\n"# 然后每隔2秒输出数字2&#xff0c;共输出10次for i in range(10):…

linux系统----Ansible中的playbook简单应用

目录 Playbooks中tasks语法使用 1、file 创建文件&#xff1a;touch 创建目录&#xff1a;directory 2、lineinfile 修改文件某一行文本 3、replace 根据正则表达式替换文件内容&#xff08;指定换字符串&#xff09; 5、template/copy 模板作用类似于copy&#xff0…