【Unity3D】缩放、平移、旋转场景

时间:2022-11-13 17:51:30

1 前言

        场景缩放、平移、旋转有两种实现方案,一种是对场景中所有物体进行同步变换,另一种方案是对相机的位置和姿态进行变换。

        对于方案一,如果所有物体都在同一个根对象下(其子对象或孙子对象),那么只需要对根对象施加变换就可以实现场景变换;如果有多个根对象,那就需要对所有根对象施加变换。该方案实现简单,但是会破坏场景中对象的尺寸、位置、姿态,不符合现实世界的规则。如:对场景施加缩放变换后,又新增了一个对象,但是该对象不是放在同一个根目录下,就会让用户感觉新增对象的尺寸超出意外;如果有多个根对象,就会存在多个参考系(每个根对象一个参考系),增加场景中对象的控制难度。

        对于方案二,通过变换相机的位置和姿态,让用户感觉场景中所有对象在同步缩放、平移、旋转。该方案实现较困难,但是不会破环场景中对象的尺寸、位置、姿态,更贴近真实世界的规则,也不需要将所有对象都放在同一个根对象下。

        方案二明显优于方案一,本文将详细介绍其原理和实现。原理如下:

        1)场景缩放原理

        利用相机的透视原理(详见→透视变换原理),即相机拍摄到的图片呈现近大远小的效果,将相机靠近和远离场景,从而实现放大和缩小场景的效果。

        2)场景平移原理

        相机成像是在*面上,如果扩展*面的范围,相机拍摄的范围也就越大,将*面平移到相机位置上,记为平面 S,将相机在 S 平面上平移,就会实现场景平移效果。

        3)场景旋转原理

        在 Unity3D Scene 窗口,通过按 Alt 键 + 鼠标拖拽,可以旋转场景。场景旋转包含两种情况,鼠标沿水平方向拖拽、鼠标沿竖直方向拖拽。

        当鼠标沿水平方向拖拽时,笔者通过多次实验观察,发现如下规律:当场景缩放到某个值时,旋转场景时,屏幕中心位置的物体(在相机的正前方)在场景旋转过程中始终处在屏幕中心,并且旋转轴的方向始终是 Y 轴方向。因此可以得出结论:旋转中心在相机正前方(forward),旋转轴沿 Y 轴方向。

        在凸镜成像原理中,存在公式:【Unity3D】缩放、平移、旋转场景,其中 【Unity3D】缩放、平移、旋转场景 分别为物距、相距、焦距,当焦距固定时,物距和相距近似反比。在 Unity3D 透视成像中,不存在焦距的概念,因此可以假设旋转中心到*面的距离和相机到*面的距离成反比。笔者通过多此实验,验证该结论近似正确。

        当鼠标沿竖直方向拖拽时,旋转中心在相机位置,旋转轴沿相机的左边(-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; // 是否在拖拽中

    void Start() {
        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;
        nearPlan = Camera.main.nearClipPlane;
    }

    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();
        cam.RotateAround(rotateCenter, Vector3.up, offset.x / 3); // 水平拖拽分量
        cam.LookAt(rotateCenter);
        cam.RotateAround(rotateCenter, -cam.right, offset.y / 5); // 竖直拖拽分量
    }

    private Vector3 GetRotateCenter() { // 获取旋转中心
        return cam.position + cam.forward * (nearPlan + 1 / nearPlan);
    }
}

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

【Unity3D】缩放、平移、旋转场景

3 运行效果

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

【Unity3D】缩放、平移、旋转场景