【Unity3D】缩放、平移、旋转场景
1 前言
场景缩放、平移、旋转有两种实现方案,一种是对场景中所有物体进行同步变换,另一种方案是对相机的位置和姿态进行变换。
对于方案一,如果所有物体都在同一个根对象下(其子对象或孙子对象),那么只需要对根对象施加变换就可以实现场景变换;如果有多个根对象,那就需要对所有根对象施加变换。该方案实现简单,但是会破坏场景中对象的尺寸、位置、姿态,不符合现实世界的规则。如:对场景施加缩放变换后,又新增了一个对象,但是该对象不是放在同一个根目录下,就会让用户感觉新增对象的尺寸超出意外;如果有多个根对象,就会存在多个参考系(每个根对象一个参考系),增加场景中对象的控制难度。
对于方案二,通过变换相机的位置和姿态,让用户感觉场景中所有对象在同步缩放、平移、旋转。该方案实现较困难,但是不会破环场景中对象的尺寸、位置、姿态,更贴近真实世界的规则,也不需要将所有对象都放在同一个根对象下。
方案二明显优于方案一,本文将详细介绍其原理和实现。原理如下:
1)场景缩放原理
利用相机的透视原理(详见→透视变换原理),即相机拍摄到的图片呈现近大远小的效果,将相机靠近和远离场景,从而实现放大和缩小场景的效果。
2)场景平移原理
相机成像是在近平面上,如果扩展近平面的范围,相机拍摄的范围也就越大,将近平面平移到相机位置上,记为平面 S,将相机在 S 平面上平移,就会实现场景平移效果。
3)场景旋转原理
在 Unity3D Scene 窗口,通过按 Alt 键 + 鼠标拖拽,可以旋转场景。场景旋转包含两种情况,鼠标沿水平方向拖拽、鼠标沿竖直方向拖拽。
当鼠标沿水平方向拖拽时,笔者通过多次实验观察,发现如下规律:当场景缩放到某个值时,旋转场景时,屏幕中心位置的物体(在相机的正前方)在场景旋转过程中始终处在屏幕中心,并且旋转轴的方向始终是 Y 轴方向。因此可以得出结论:旋转中心在相机正前方(forward),旋转轴沿 Y 轴方向。
旋转中心的 y 值最好与地图的 y 值相等,如果场景中没有地图,可以取旋转中心为:cam.position + cam.forward * (nearPlan + 1 / nearPlan),当然,用户也可以取其他值。已知旋转中心的 y 值,可以按照以下公式推导出 x、z 值:
当鼠标沿竖直方向拖拽时,旋转中心在相机位置,旋转轴沿相机的左边(-right)。
本文代码资源见→缩放、平移、旋转场景。
2 代码实现
SceneController.cs
using UnityEngine;
public class SceneController : MonoBehaviour {
private Texture2D[] cursorTextures; // 鼠标样式: 箭头、小手、眼睛
private Transform cam; // 相机
private float nearPlan; // 近平面
private Vector3 preMousePos; // 上一帧的鼠标坐标
private int cursorStatus = 0; // 鼠标样式状态
private bool isDraging = false; // 是否在拖拽中
private void Awake() {
string[] mouseIconPath = new string[]{"MouseIcon/0_arrow", "MouseIcon/1_hand", "MouseIcon/2_eye"};
cursorTextures = new Texture2D[mouseIconPath.Length];
for(int i = 0; i < mouseIconPath.Length; i++) {
cursorTextures[i] = Resources.Load<Texture2D>(mouseIconPath[i]);
}
cam = Camera.main.transform;
Vector3 angle = cam.eulerAngles;
cam.eulerAngles = new Vector3(angle.x, angle.y, 0); // 使camp.right指向水平方向
nearPlan = Camera.main.nearClipPlane;
}
private void Update() {
cursorStatus = GetCursorStatus();
// 更新鼠标样式, 第二个参数表示鼠标点击位置在图标中的位置, zero表示左上角
Cursor.SetCursor(cursorTextures[cursorStatus], Vector2.zero, CursorMode.Auto);
UpdateScene(); // 更新场景(Ctrl+Scroll: 缩放场景, Ctrl+Drag: 平移场景, Alt+Drag: 旋转场景)
}
private int GetCursorStatus() { // 获取鼠标状态(0: 箭头, 1: 小手, 2: 眼睛)
if (isDraging) {
return cursorStatus;
}
if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.LeftControl)) {
return 1;
}
if (Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.LeftAlt)) {
return 2;
}
return 0;
}
private void UpdateScene() { // 更新场景(Ctrl+Scroll: 缩放场景, Ctrl+Drag: 平移场景, Alt+Drag: 旋转场景)
float scroll = Input.GetAxis("Mouse ScrollWheel");
if (!isDraging && cursorStatus == 1 && Mathf.Abs(scroll) > 0) { // 缩放场景
ScaleScene(scroll);
} else if (Input.GetMouseButtonDown(0)) {
preMousePos = Input.mousePosition;
isDraging = true;
} else if (Input.GetMouseButtonUp(0)) {
isDraging = false;
} else if (Input.GetMouseButton(0)) {
Vector3 offset = Input.mousePosition - preMousePos;
if (cursorStatus == 1) { // 移动场景
MoveScene(offset);
} else if (cursorStatus == 2) { // 旋转场景
RotateScene(offset);
}
preMousePos = Input.mousePosition;
}
}
private void ScaleScene(float scroll) { // 缩放场景
cam.position += cam.forward * scroll;
}
private void MoveScene(Vector3 offset) { // 平移场景
cam.position -= (cam.right * offset.x / 100 + cam.up * offset.y / 100);
}
private void RotateScene(Vector3 offset) { // 旋转场景
Vector3 rotateCenter = GetRotateCenter(0);
cam.RotateAround(rotateCenter, Vector3.up, offset.x / 3); // 水平拖拽分量
cam.LookAt(rotateCenter);
cam.RotateAround(rotateCenter, -cam.right, offset.y / 5); // 竖直拖拽分量
}
private Vector3 GetRotateCenter(float planeY) { // 获取旋转中心
if (Mathf.Abs(cam.forward.y) < float.Epsilon || Mathf.Abs(cam.position.y) < float.Epsilon) {
return cam.position + cam.forward * (nearPlan + 1 / nearPlan);
}
float t = (planeY - cam.position.y) / cam.forward.y;
float x = cam.position.x + t * cam.forward.x;
float z = cam.position.z + t * cam.forward.z;
return new Vector3(x, planeY, z);
}
}
说明:SceneController 脚本组件挂在相机下,鼠标图标如下,需要放在 Resouses/MouseIcon 目录下, 并且需要在 Inspector 窗口将其 Texture Type 属性调整为 Cursor。
3 运行效果
通过 Ctrl+Scroll 缩放场景,Ctrl+Drag 平移场景,Alt+Drag 旋转场景 ,效果如下:
4 优化
第 2 节中场景变换存在以下问题,本节将对这些问题进行优化。
- 竖直方向平移场景时,会抬高或降低相机高度;
- 竖直方向旋转场景时,如果相机垂直朝向地面,就会出现窗口急速晃动问题,因为旋转中心出现了跳变。
针对问题一,将相机的上方向量(camera.up)投影到水平面上,再用投影向量计算相机前后平移的偏移量。
针对问题二,使用一个全局变量实时保存并更新旋转中心的位置,通过相机周转和自传(两者旋转角度和方向相等)实现水平和竖直方向旋转场景,避免使用 LookAt,因为相机不一定一直朝向旋转中心(如:相机焦点不在地图里)。
SceneController.cs
using UnityEngine;
public class SceneController : MonoBehaviour {
private const float MAX_HALF_EDGE_X = 5f; // 地图x轴方向半边长
private const float MAX_HALF_EDGE_Z = 5f; // 地图z轴方向半边长
private Texture2D[] cursorTextures; // 鼠标样式: 箭头、小手、眼睛
private Transform cam; // 相机
private float planeY = 0f; // 地面高度
private Vector3 rotateCenter; // 旋转中心
private Vector3 focusCenter; // 相机在地面上的焦点中心
private bool isFocusInMap; // 相机焦点是否在地图里
private Vector3 preMousePos; // 上一帧的鼠标坐标
private int cursorStatus = 0; // 鼠标样式状态
private bool isDraging = false; // 是否在拖拽中
private void Awake() {
string[] mouseIconPath = new string[] { "MouseIcon/0_arrow", "MouseIcon/1_hand", "MouseIcon/2_eye" };
cursorTextures = new Texture2D[mouseIconPath.Length];
for (int i = 0; i < mouseIconPath.Length; i++) {
cursorTextures[i] = Resources.Load<Texture2D>(mouseIconPath[i]);
}
cam = Camera.main.transform;
Vector3 angle = cam.eulerAngles;
cam.eulerAngles = new Vector3(angle.x, angle.y, 0); // 使camp.right指向水平方向
rotateCenter = new Vector3(0, planeY, 0);
focusCenter = new Vector3(0, planeY, 0);
}
private void Update() {
cursorStatus = GetCursorStatus();
// 更新鼠标样式, 第二个参数表示鼠标点击位置在图标中的位置, zero表示左上角
Cursor.SetCursor(cursorTextures[cursorStatus], Vector2.zero, CursorMode.Auto);
UpdateScene(); // 更新场景(Ctrl+Scroll: 缩放场景, Ctrl+Drag: 平移场景, Alt+Drag: 旋转场景)
}
private int GetCursorStatus() { // 获取鼠标状态(0: 箭头, 1: 小手, 2: 眼睛)
if (isDraging)
{
return cursorStatus;
}
if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.LeftControl))
{
return 1;
}
if (Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.LeftAlt))
{
return 2;
}
return 0;
}
private void UpdateScene() { // 更新场景(Ctrl+Scroll: 缩放场景, Ctrl+Drag: 平移场景, Alt+Drag: 旋转场景)
float scroll = Input.GetAxis("Mouse ScrollWheel");
if (!isDraging && cursorStatus == 1 && Mathf.Abs(scroll) > 0) { // 缩放场景
ScaleScene(scroll);
}
else if (Input.GetMouseButtonDown(0)) {
preMousePos = Input.mousePosition;
UpdateRotateCenter();
isDraging = true;
}
else if (Input.GetMouseButtonUp(0)) {
isDraging = false;
}
else if (Input.GetMouseButton(0)) {
Vector3 offset = Input.mousePosition - preMousePos;
if (cursorStatus == 1) { // 移动场景
MoveScene(offset);
}
else if (cursorStatus == 2) { // 旋转场景
RotateScene(offset);
}
preMousePos = Input.mousePosition;
}
}
private void ScaleScene(float scroll) { // 缩放场景
cam.position += cam.forward * scroll;
}
private void MoveScene(Vector3 offset) { // 平移场景
Vector3 horVec = Vector3.ProjectOnPlane(cam.right, Vector3.up).normalized;
Vector3 verVec = Vector3.ProjectOnPlane(cam.up, Vector3.up).normalized;
cam.position -= (horVec * offset.x / 100 + verVec * offset.y / 100);
}
private void RotateScene(Vector3 offset) { // 旋转场景
float hor = offset.x / 3;
float ver = -offset.y / 5;
cam.RotateAround(rotateCenter, Vector3.up, hor); // 相机绕旋转中心水平旋转
cam.RotateAround(rotateCenter, cam.right, ver); // 相机绕旋转中心竖直旋转
// 由于transform.RotateAround方法中已经进行了物体姿态调整, 因此以下语句是多余的
// cam.RotateAround(cam.position, Vector3.up, hor); // 相机自转, 使其朝向旋转中心
// cam.RotateAround(cam.position, cam.right, ver); // 相机自转, 使其朝向旋转中心
}
private void UpdateRotateCenter() { // 更新旋转中心
UpdateFocusStatus();
if (!isFocusInMap) {
return;
}
rotateCenter.x = Mathf.Clamp(focusCenter.x, -MAX_HALF_EDGE_X, MAX_HALF_EDGE_X);
rotateCenter.z = Mathf.Clamp(focusCenter.z, -MAX_HALF_EDGE_Z, MAX_HALF_EDGE_Z);
}
private void UpdateFocusStatus() { // 更新焦点状态
isFocusInMap = true;
Vector3 vec1 = new Vector3(0, planeY - cam.position.y, 0);
Vector3 vec2 = cam.forward;
if (Mathf.Abs(vec1.y) < float.Epsilon || Mathf.Abs(vec2.y) < float.Epsilon) {
isFocusInMap = false;
return;
}
float angle = Vector3.Angle(vec1, vec2);
if (angle >= 90) { // 相机在地面以上并且朝天, 或在地面以下并且朝下
isFocusInMap = false;
return;
}
float t = (planeY - cam.position.y) / vec2.y;
focusCenter.x = cam.position.x + t * vec2.x;
focusCenter.z = cam.position.z + t * vec2.z;
if (Mathf.Abs(focusCenter.x) > MAX_HALF_EDGE_X || Mathf.Abs(focusCenter.z) > MAX_HALF_EDGE_Z) { // 相机焦点不在地图区域内
isFocusInMap = false;
}
}
}
声明:本文转自【Unity3D】缩放、平移、旋转场景
【Unity3D】缩放、平移、旋转场景的更多相关文章
- Three.js 保存camera(视角)设置到数据库,包括场景的缩放、旋转、移动等
最近在做的项目中遇到需要保存当前的3d管道视角设置的问题,用户希望在对3d场景内的管道进行了缩放.旋转.移动之后可以将场景当前的视角状态保存在数据库中,并在下次加载时读取. 经过不断的尝试和研究,在同 ...
- OpenCV2:图像的几何变换,平移、镜像、缩放、旋转(2)
在OpenCV2:图像的几何变换,平移.镜像.缩放.旋转(1)主要介绍了图像变换中的向前映射.向后映射.处理变换过程中浮点坐标像素值的插值算法,并且基于OpenCV2实现了两个简单的几何变换:平移和镜 ...
- OpenCV2:图像的几何变换,平移、镜像、缩放、旋转(1)
图像的几何变换是在不改变图像内容的前提下对图像像素的进行空间几何变换,主要包括了图像的平移变换.镜像变换.缩放和旋转等.本文首先介绍了图像几何变换的一些基本概念,然后再OpenCV2下实现了图像的平移 ...
- osg矩阵变换节点-----平移旋转缩放
osg矩阵变换节点-----平移旋转缩放 转自:http://www.cnblogs.com/ylwn817/articles/1973396.html 平移旋转缩放这个三个是osg矩阵操作中,最常见 ...
- osg中使用MatrixTransform来实现模型的平移/旋转/缩放
osg中使用MatrixTransform来实现模型的平移/旋转/缩放 转自:http://www.cnblogs.com/kekec/archive/2011/08/15/2139893.html# ...
- OpenGL立方体在世界坐标系中_缩放_旋转_平移_顶点片源着色器_光照作用_棋盘纹理贴图
读取bmp等图片格式中的像素还有难度,就先用这个棋盘图象素来弄了 代码打错一个就一直First-chance exception ,貌似还有一个要用q或者Q才能成功退出,不知道缺少哪句,我用窗口红叉退 ...
- View的平移、缩放、旋转以及位置、坐标系
原创 2015年05月12日 13:15:29 标签: Android / Scroll / Scale / Translation / Rotation 24733 Android开发中,经常会接触 ...
- 【C#/WPF】Image图片的Transform变换:平移、缩放、旋转
WPF中图像控件Image的变换属性Transform: 平移 缩放 旋转 即要想实现图片的平移.缩放.旋转,是修改它所在的Image控件的Transform变换属性. 下面在XAML中定义了Imag ...
- (转)Unity3D研究院之将场景导出XML或JSON或二进制并且解析还原场景
自:http://www.xuanyusong.com/archives/1919 导出Unity场景的所有游戏对象信息,一种是XML一种是JSON.本篇文章我们把游戏场景中游戏对象的.旋转.缩放.平 ...
- Unity3d 动态加载场景物件与缓存池的使用
聊聊Unity3d动态加载场景物件那些事儿. 众所周知,在策划或美术设计完游戏场景地图后,一个场景中可能会存在成千上万个小的物件,比如石头,木箱子,油桶,栅栏等等等等,这些物件并不是游戏中的道具,仅仅 ...
随机推荐
- 【SHELL】在指定格式的文件中查找字符串
在指定格式的文件中查找字符串 grep -nr "string" --include=*.{c,cpp,h} 在排除指定格式的文件中查找字符串 grep -nr "str ...
- window-子系统-ubuntu
window-子系统-ubuntu 1. 背景 提供类Linux开发环境(命令行.文件系统.进程管理.网路) 2. 安装 A. wsl 安装 下载链接: https://wslstorestorage ...
- [转帖]使用 TiDB 读取 TiFlash
https://docs.pingcap.com/zh/tidb/stable/use-tidb-to-read-tiflash 本文档介绍如何使用 TiDB 读取 TiFlash 副本. TiDB ...
- [转帖]【存储测试】cosbench存储性能测试工具
一.前言 参考资料: https://blog.csdn.net/QTM_Gitee/article/details/100067724 https://github.com/intel-cloud/ ...
- [转帖]armv6、armv7、armv7s、armv8、armv64及其i386、x86_64区别
ARM处理器指令集 一. 苹果模拟器指令集: 指令集 分析 i386 针对intel通用微处理器32架构的 x86_64 针对x86架构的64位处理器 i386|x86_64 是Mac处理器的指令集, ...
- [转帖]Linux系统下cpio命令详解
简介 cpio主要是解压或者将文件压缩到指定文件中即copy-in和copy-out模式. 参数说明 参数 参数说明 -i copy-in模式,解压文件 -o copy-out模式,即压缩文件 -d ...
- [转帖]setsockopt(setsockopt的使用方法及注意事项)
http://xingzuo.aitcweb.com/9156453.html 1. setsockopt简介 setsockopt是一个系统调用函数,用于设置套接字选项.套接字是指通信的两个端点之间 ...
- PHP GC回收机制详解
前言 GC的全称是Garbage Collection也就是垃圾回收的意思,在PHP中,是使用引用计数和回收周期来自动管理内存对象的,当一个对象被设置为NULL,或者没有任何指针指向时,他就会变成垃圾 ...
- elementUI(datepicker)限制日日期的选择
指定起始日期,后选的将会受到先选的限制 参考地址 https://www.jianshu.com/p/c59c8ef6c500 实现方法不难,利用了 change 事件,动态改变 picker-opt ...
- interface{}类型 + fmt.Sprintf() 导致栈逃逸
作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 对部分代码进行了栈逃逸检查: go build -gcfl ...