当前位置:   article > 正文

【Unity3D】缩放、平移、旋转场景_unity触摸屏,缩放,旋转,移动?

unity触摸屏,缩放,旋转,移动?

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

  1. using UnityEngine;
  2. public class SceneController : MonoBehaviour {
  3. private Texture2D[] cursorTextures; // 鼠标样式: 箭头、小手、眼睛
  4. private Transform cam; // 相机
  5. private float nearPlan; // 近平面
  6. private Vector3 preMousePos; // 上一帧的鼠标坐标
  7. private int cursorStatus = 0; // 鼠标样式状态
  8. private bool isDraging = false; // 是否在拖拽中
  9. private void Awake() {
  10. string[] mouseIconPath = new string[]{"MouseIcon/0_arrow", "MouseIcon/1_hand", "MouseIcon/2_eye"};
  11. cursorTextures = new Texture2D[mouseIconPath.Length];
  12. for(int i = 0; i < mouseIconPath.Length; i++) {
  13. cursorTextures[i] = Resources.Load<Texture2D>(mouseIconPath[i]);
  14. }
  15. cam = Camera.main.transform;
  16. Vector3 angle = cam.eulerAngles;
  17. cam.eulerAngles = new Vector3(angle.x, angle.y, 0); // 使camp.right指向水平方向
  18. nearPlan = Camera.main.nearClipPlane;
  19. }
  20. private void Update() {
  21. cursorStatus = GetCursorStatus();
  22. // 更新鼠标样式, 第二个参数表示鼠标点击位置在图标中的位置, zero表示左上角
  23. Cursor.SetCursor(cursorTextures[cursorStatus], Vector2.zero, CursorMode.Auto);
  24. UpdateScene(); // 更新场景(Ctrl+Scroll: 缩放场景, Ctrl+Drag: 平移场景, Alt+Drag: 旋转场景)
  25. }
  26. private int GetCursorStatus() { // 获取鼠标状态(0: 箭头, 1: 小手, 2: 眼睛)
  27. if (isDraging) {
  28. return cursorStatus;
  29. }
  30. if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.LeftControl)) {
  31. return 1;
  32. }
  33. if (Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.LeftAlt)) {
  34. return 2;
  35. }
  36. return 0;
  37. }
  38. private void UpdateScene() { // 更新场景(Ctrl+Scroll: 缩放场景, Ctrl+Drag: 平移场景, Alt+Drag: 旋转场景)
  39. float scroll = Input.GetAxis("Mouse ScrollWheel");
  40. if (!isDraging && cursorStatus == 1 && Mathf.Abs(scroll) > 0) { // 缩放场景
  41. ScaleScene(scroll);
  42. } else if (Input.GetMouseButtonDown(0)) {
  43. preMousePos = Input.mousePosition;
  44. isDraging = true;
  45. } else if (Input.GetMouseButtonUp(0)) {
  46. isDraging = false;
  47. } else if (Input.GetMouseButton(0)) {
  48. Vector3 offset = Input.mousePosition - preMousePos;
  49. if (cursorStatus == 1) { // 移动场景
  50. MoveScene(offset);
  51. } else if (cursorStatus == 2) { // 旋转场景
  52. RotateScene(offset);
  53. }
  54. preMousePos = Input.mousePosition;
  55. }
  56. }
  57. private void ScaleScene(float scroll) { // 缩放场景
  58. cam.position += cam.forward * scroll;
  59. }
  60. private void MoveScene(Vector3 offset) { // 平移场景
  61. cam.position -= (cam.right * offset.x / 100 + cam.up * offset.y / 100);
  62. }
  63. private void RotateScene(Vector3 offset) { // 旋转场景
  64. Vector3 rotateCenter = GetRotateCenter(0);
  65. cam.RotateAround(rotateCenter, Vector3.up, offset.x / 3); // 水平拖拽分量
  66. cam.LookAt(rotateCenter);
  67. cam.RotateAround(rotateCenter, -cam.right, offset.y / 5); // 竖直拖拽分量
  68. }
  69. private Vector3 GetRotateCenter(float planeY) { // 获取旋转中心
  70. if (Mathf.Abs(cam.forward.y) < float.Epsilon || Mathf.Abs(cam.position.y) < float.Epsilon) {
  71. return cam.position + cam.forward * (nearPlan + 1 / nearPlan);
  72. }
  73. float t = (planeY - cam.position.y) / cam.forward.y;
  74. float x = cam.position.x + t * cam.forward.x;
  75. float z = cam.position.z + t * cam.forward.z;
  76. return new Vector3(x, planeY, z);
  77. }
  78. }

        说明:SceneController 脚本组件挂在相机下,鼠标图标如下,需要放在 Resouses/MouseIcon 目录下, 并且需要在 Inspector 窗口将其 Texture Type 属性调整为 Cursor。

3 运行效果

         通过 Ctrl+Scroll 缩放场景,Ctrl+Drag 平移场景,Alt+Drag 旋转场景 ,效果如下:

4 优化

        第 2 节中场景变换存在以下问题,本节将对这些问题进行优化。

  • 竖直方向平移场景时,会抬高或降低相机高度;
  • 竖直方向旋转场景时,如果相机垂直朝向地面,就会出现窗口急速晃动问题,因为旋转中心出现了跳变。

        针对问题一,将相机的上方向量(camera.up)投影到水平面上,再用投影向量计算相机前后平移的偏移量。

        针对问题二,使用一个全局变量实时保存并更新旋转中心的位置,并通过相机周转和自传(两者旋转角度和方向相等)实现水平和竖直方向旋转场景,避免使用 LookAt,因为相机不一定一直朝向旋转中心(如:相机焦点不在地图里)。

        SceneController.cs

  1. using UnityEngine;
  2. public class SceneController : MonoBehaviour {
  3. private const float MAX_HALF_EDGE_X = 5f; // 地图x轴方向半边长
  4. private const float MAX_HALF_EDGE_Z = 5f; // 地图z轴方向半边长
  5. private Texture2D[] cursorTextures; // 鼠标样式: 箭头、小手、眼睛
  6. private Transform cam; // 相机
  7. private float planeY = 0f; // 地面高度
  8. private Vector3 rotateCenter; // 旋转中心
  9. private Vector3 focusCenter; // 相机在地面上的焦点中心
  10. private bool isFocusInMap; // 相机焦点是否在地图里
  11. private Vector3 preMousePos; // 上一帧的鼠标坐标
  12. private int cursorStatus = 0; // 鼠标样式状态
  13. private bool isDraging = false; // 是否在拖拽中
  14. private void Awake() {
  15. string[] mouseIconPath = new string[] { "MouseIcon/0_arrow", "MouseIcon/1_hand", "MouseIcon/2_eye" };
  16. cursorTextures = new Texture2D[mouseIconPath.Length];
  17. for (int i = 0; i < mouseIconPath.Length; i++) {
  18. cursorTextures[i] = Resources.Load<Texture2D>(mouseIconPath[i]);
  19. }
  20. cam = Camera.main.transform;
  21. Vector3 angle = cam.eulerAngles;
  22. cam.eulerAngles = new Vector3(angle.x, angle.y, 0); // 使camp.right指向水平方向
  23. rotateCenter = new Vector3(0, planeY, 0);
  24. focusCenter = new Vector3(0, planeY, 0);
  25. }
  26. private void Update() {
  27. cursorStatus = GetCursorStatus();
  28. // 更新鼠标样式, 第二个参数表示鼠标点击位置在图标中的位置, zero表示左上角
  29. Cursor.SetCursor(cursorTextures[cursorStatus], Vector2.zero, CursorMode.Auto);
  30. UpdateScene(); // 更新场景(Ctrl+Scroll: 缩放场景, Ctrl+Drag: 平移场景, Alt+Drag: 旋转场景)
  31. }
  32. private int GetCursorStatus() { // 获取鼠标状态(0: 箭头, 1: 小手, 2: 眼睛)
  33. if (isDraging)
  34. {
  35. return cursorStatus;
  36. }
  37. if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.LeftControl))
  38. {
  39. return 1;
  40. }
  41. if (Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.LeftAlt))
  42. {
  43. return 2;
  44. }
  45. return 0;
  46. }
  47. private void UpdateScene() { // 更新场景(Ctrl+Scroll: 缩放场景, Ctrl+Drag: 平移场景, Alt+Drag: 旋转场景)
  48. float scroll = Input.GetAxis("Mouse ScrollWheel");
  49. if (!isDraging && cursorStatus == 1 && Mathf.Abs(scroll) > 0) { // 缩放场景
  50. ScaleScene(scroll);
  51. }
  52. else if (Input.GetMouseButtonDown(0)) {
  53. preMousePos = Input.mousePosition;
  54. UpdateRotateCenter();
  55. isDraging = true;
  56. }
  57. else if (Input.GetMouseButtonUp(0)) {
  58. isDraging = false;
  59. }
  60. else if (Input.GetMouseButton(0)) {
  61. Vector3 offset = Input.mousePosition - preMousePos;
  62. if (cursorStatus == 1) { // 移动场景
  63. MoveScene(offset);
  64. }
  65. else if (cursorStatus == 2) { // 旋转场景
  66. RotateScene(offset);
  67. }
  68. preMousePos = Input.mousePosition;
  69. }
  70. }
  71. private void ScaleScene(float scroll) { // 缩放场景
  72. cam.position += cam.forward * scroll;
  73. }
  74. private void MoveScene(Vector3 offset) { // 平移场景
  75. Vector3 horVec = Vector3.ProjectOnPlane(cam.right, Vector3.up).normalized;
  76. Vector3 verVec = Vector3.ProjectOnPlane(cam.up, Vector3.up).normalized;
  77. cam.position -= (horVec * offset.x / 100 + verVec * offset.y / 100);
  78. }
  79. private void RotateScene(Vector3 offset) { // 旋转场景
  80. float hor = offset.x / 3;
  81. float ver = -offset.y / 5;
  82. cam.RotateAround(rotateCenter, Vector3.up, hor); // 相机绕旋转中心水平旋转
  83. cam.RotateAround(rotateCenter, cam.right, ver); // 相机绕旋转中心竖直旋转
  84. // 由于transform.RotateAround方法中已经进行了物体姿态调整, 因此以下语句是多余的
  85. // cam.RotateAround(cam.position, Vector3.up, hor); // 相机自转, 使其朝向旋转中心
  86. // cam.RotateAround(cam.position, cam.right, ver); // 相机自转, 使其朝向旋转中心
  87. }
  88. private void UpdateRotateCenter() { // 更新旋转中心
  89. UpdateFocusStatus();
  90. if (!isFocusInMap) {
  91. return;
  92. }
  93. rotateCenter.x = Mathf.Clamp(focusCenter.x, -MAX_HALF_EDGE_X, MAX_HALF_EDGE_X);
  94. rotateCenter.z = Mathf.Clamp(focusCenter.z, -MAX_HALF_EDGE_Z, MAX_HALF_EDGE_Z);
  95. }
  96. private void UpdateFocusStatus() { // 更新焦点状态
  97. isFocusInMap = true;
  98. Vector3 vec1 = new Vector3(0, planeY - cam.position.y, 0);
  99. Vector3 vec2 = cam.forward;
  100. if (Mathf.Abs(vec1.y) < float.Epsilon || Mathf.Abs(vec2.y) < float.Epsilon) {
  101. isFocusInMap = false;
  102. return;
  103. }
  104. float angle = Vector3.Angle(vec1, vec2);
  105. if (angle >= 90) { // 相机在地面以上并且朝天, 或在地面以下并且朝下
  106. isFocusInMap = false;
  107. return;
  108. }
  109. float t = (planeY - cam.position.y) / vec2.y;
  110. focusCenter.x = cam.position.x + t * vec2.x;
  111. focusCenter.z = cam.position.z + t * vec2.z;
  112. if (Mathf.Abs(focusCenter.x) > MAX_HALF_EDGE_X || Mathf.Abs(focusCenter.z) > MAX_HALF_EDGE_Z) { // 相机焦点不在地图区域内
  113. isFocusInMap = false;
  114. }
  115. }
  116. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/107907
推荐阅读
相关标签
  

闽ICP备14008679号