Unity3D相機操控(完整模擬Scene視圖操作)

1. 需求

  一直想把Scene視圖相機的操作複製到Game視圖來,之前工作實現了一部分,但是不完善,前幾天晚上抽空重新寫了個。
  在寫的過程中遇到一些問題,這裏記錄一下。
  Scene視圖的操作總結如下:
  1. 正交/透視視圖的緩動切換
   2. 滾動鼠標滾輪能夠拉近、縮遠
  3. 按住鼠標中鍵能夠上下左右拖拽相機
  4. ALT+鼠標左鍵 或 按住鼠標右鍵 可360查看場景
  5. 正視圖、左視圖、俯視圖等
  6. 按住F能夠聚焦物體
  7. ALT+鼠標右鍵 拖動鼠標能夠放大縮小視角
  8. 選中物體後,按住Shift+F,相機會一直跟隨物體
  這裏實現了1~7,8沒實現。

2. 效果

先看看效果吧。
正交/透視切換。
正交透視切換
360°查看。
360°查看
聚焦和各種視圖。
聚焦及各種視圖
中鍵拖拽。
在這裏插入圖片描述
縮放視角。
縮放視角

3. 邏輯梳理

3.1 正交/透視視圖的緩動切換

  實現相機正交視圖和透視視圖緩動切換的核心是對相機的投影矩陣進行插值(從相機當前的投影矩陣插值到正交投影矩陣或者透視投影矩陣)
  分兩個步驟:

  1. 根據相機的參數算出相機對應的透視投影矩陣和正交投影矩陣,如fieldOfView/aspect/orthographicSize/farClipPlane/nearClipPlane這些參數。
    計算透視投影矩陣的公式如下。
    透視攝像機的參數對透視投影視錐體的影響
    透視投影矩陣計算公式
 // 透視投影矩陣.
 Matrix4x4 defaultPersProjMat = new Matrix4x4();
 //defaultPersProjMat = Matrix4x4.Perspective(fov, aspect, near, far);           // 透視矩陣也可直接調用這個來計算.
 defaultPersProjMat.SetRow(0, new Vector4(1.0f / (tan * aspect), 0, 0, 0));
 defaultPersProjMat.SetRow(1, new Vector4(0, 1.0f / tan, 0, 0));
 defaultPersProjMat.SetRow(2, new Vector4(0, 0, -(far + near) / (far - near), -2 * far * near / (far - near)));
 defaultPersProjMat.SetRow(3, new Vector4(0, 0, -1, 0));

  計算正交投影矩陣的公式如下。
正交攝像機的參數對正交投影視錐體的影響
正交投影矩陣計算公式

// 正交投影矩陣.
Matrix4x4 defaultOrthProjMat = new Matrix4x4();
defaultOrthProjMat.SetRow(0, new Vector4(1.0f / (aspect * size), 0, 0, 0));
defaultOrthProjMat.SetRow(1, new Vector4(0, 1.0f / size, 0, 0));
defaultOrthProjMat.SetRow(2, new Vector4(0, 0, -2f / (far - near), -(far + near) / (far - near)));
defaultOrthProjMat.SetRow(3, new Vector4(0, 0, 0, 1));
  1. 對投影矩陣進行插值
    對矩陣插值其實也就是對矩陣中的各行進行插值。
public static Matrix4x4 MatrixLerp(Matrix4x4 from, Matrix4x4 to, float t)
{
    t = Mathf.Clamp01(t);
    Matrix4x4 lerpMatrix = new Matrix4x4();
    for (int i = 0; i < 4; i++)
    {
        lerpMatrix.SetRow(i, Vector4.Lerp(from.GetRow(i), to.GetRow(i), t));
    }
    return lerpMatrix;
}

  有個特別重要的點。
  相機的fieldOfView和orthographicSize這兩個參數必須同時修改,其中一個參數變了,另外一個參數也要跟着變。
  相機的fieldOfView和orthographicSize這兩個參數必須同時修改,其中一個參數變了,另外一個參數也要跟着變。
  相機的fieldOfView和orthographicSize這兩個參數必須同時修改,其中一個參數變了,另外一個參數也要跟着變。
  否則,透視/正交視圖切換的時候會發現兩個視角看到的物體的大小不一樣。
  那麼透視相機的fieldOfView和正交相機的orthographicSize對應的關係是什麼呢?要把這個東西用文字表達清楚還真不是件容易的事,所以我畫了張圖,不知道大家能不能理解。如果沒明白,知道公式是這樣就行了。
fov與相機的關係
公式如下。

/// <summary>
/// 根據正交相機的size設置對應透視相機的fov
/// </summary>
/// <param name="size">正交相機高度的一半,即orthographicSize</param>
private float SetFovBySize(float size)
{
    float fov = 2.0f * Mathf.Atan(size / distance) * Mathf.Rad2Deg;
    m_Camera.fieldOfView = fov;
    return fov;
}

/// <summary>
/// 根據透視相機的fov設置對應正交相機的即orthographicSize
/// </summary>
/// <param name="fov">透視相機的fov</param>
private float SetSizeByFov(float fov)
{
    float size = distance * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
    m_Camera.orthographicSize = size;
    return size;
}

3.2 拉近縮遠

  核心就是更改相機到觀察物體的距離。
  注意,距離修改之後需要重新計算相機的透視投影矩陣和正交投影矩陣,從而保證正交/透視視圖切換的正確性。
  具體看源碼中的Zoom方法。

3.3 上下左右拖拽相機

  細心的讀者可能會發現,上面的效果展示圖中,無論我們鼠標如何移動,物體都跟隨着鼠標
  看了好多網上給的代碼,發現他們用的拖動係數都是固定的,如果我們相機拉遠或者拉近就會出現拖動鼠標,物體會移動得老遠或者移動得很慢的情況,如下圖這種樣子。
錯誤移動示例
  所以我做了修改,根據相機的遠近動態去調整拖動係數,以達到物體始終在鼠標下的效果。
  原理就是根據鼠標移動的像素換算成相機移動的距離,分析見3.1節畫的那張圖。我急着去喫飯,就不多說了哈。看代碼還不明白的話,留言就對了。逃。

    /// <summary>
    /// 計算拖拽距離.
    /// </summary>
    /// <param name="camera">相機</param>
    /// <param name="mouseDelta">鼠標移動距離</param>
    /// <param name="distance">相機距離觀察物體的距離</param>
    private Vector3 CalcDragLength(Camera camera, Vector2 mouseDelta, float distance)
    {
        float rectHeight = -1;
        float rectWidth = -1;
        if (camera.orthographic)
        {
            rectHeight = 2 * camera.orthographicSize;
            //rectWidth = rectHeight / camera.aspect;
        }
        else
        {
            rectHeight = 2 * distance * Mathf.Tan(camera.fieldOfView * 0.5f * Mathf.Deg2Rad);
        }
        rectWidth = Screen.width * rectHeight / Screen.height;
        Vector3 moveDir = -rectWidth / Screen.width * mouseDelta.x * camera.transform.right - rectHeight / Screen.height * mouseDelta.y * camera.transform.up;

        return moveDir;
    }

4 源碼

  把代碼掛在相機上就能夠使用了。兩個類CameraController和Utils.Math。
  項目鏈接。
  鏈接:https://pan.baidu.com/s/1qBsaHJgBg5OFqkBiE8lv5Q
  提取碼:mi6m。

using UnityEngine;
using System.Collections;

public class CameraController : MonoBehaviour
{
    /// <summary>
    /// 視角類型.
    /// </summary>
    private enum ViewType
    {
        _2D = 0,
        _3D
    }

    /// <summary>
    /// 鼠標按鍵.
    /// </summary>
    private enum MouseButton
    {
        /// <summary>
        /// 鼠標左鍵.
        /// </summary>
        LEFT = 0,
        /// <summary>
        /// 鼠標右鍵.
        /// </summary> 
        RIGHT,
        /// <summary>
        /// 鼠標中鍵.
        /// </summary>
        MIDDLE
    }

    /// <summary>
    /// 指針樣式.
    /// </summary>
    private enum CursorType
    {
        /// <summary>
        /// 默認樣式.
        /// </summary>
        DEFAULT = 0,
        /// <summary>
        /// 小手.
        /// </summary>
        HAND,
        /// <summary>
        /// 眼睛.
        /// </summary>
        EYE,
        /// <summary>
        /// 放大鏡.
        /// </summary>
        MAGNIFIER
    }

    #region 字段
    private const string MOUSESCROLLWHEEL = "Mouse ScrollWheel";        // 鼠標滾輪.
    private const string MOUSEX = "Mouse X";
    private const string MOUSEY = "Mouse Y";

    #region 輸入

    private bool leftAltKeyDown;                                        // 左Alt鍵-按下.
    private bool rightAltKeyDown;                                       // 右Alt鍵-按下.
    private bool leftAltKey;                                            // 左Alt鍵-長按.
    private bool rightAltKey;                                           // 右Alt鍵-長按.
    private bool leftAltKeyUp;                                          // 左Alt鍵-擡起.
    private bool rightAltKeyUp;                                         // 右Alt鍵-擡起.
    private bool leftMouseButtonDown;                                   // 鼠標左鍵-按下.
    private bool rightMouseButtonDown;                                  // 鼠標右鍵-按下.
    private bool leftMouseButton;                                       // 鼠標左鍵-長按.
    private bool rightMouseButton;                                      // 鼠標右鍵-長按.
    private bool rightMouseButtonUp;                                    // 鼠標右鍵-擡起.
    private bool middleMouseButton;                                     // 鼠標中鍵-長按.
    private bool middleMouseButtonUp;                                   // 鼠標中鍵-擡起.

    #endregion

    private Camera m_Camera;                                            // 相機.
    private Texture2D handCursorTexture;                                // 小手.
    private Texture2D eyeCursorTexture;                                 // 眼睛.
    private Texture2D magnifierCursorTexture;                           // 放大鏡.

    private Vector2 hotSpot = Vector2.zero;
    private CursorMode cursorMode = CursorMode.Auto;

    private float angle_X;                                              // 水平方向的角度值,繞Transform的y軸旋轉(左右)
    private float angle_Y;                                              // 豎直方向的角度值,繞Transform的x軸旋轉(上下)
    private float angle_Z;

    // 切換視角
    private Matrix4x4 defaultOrthProjMat;                               // 相機默認的正交投影矩陣.
    private Matrix4x4 defaultPersProjMat;                               // 相機默認的透視投影矩陣.
    private Matrix4x4 currentProjMat;                                   // 相機當前的投影矩陣.
    private bool isChangingViewType = false;                            // 正在切換視角.
    private float viewTypeLerpTime = 0f;                                // 插值時間.
    private ViewType viewType;                                          // 當前視角.

    [Header("Zoom")]
    // 相機拉近拉遠 Zoom
    [SerializeField]
    private float distance = 20f;                      // 相機與lookAroudPos點的距離.
    [SerializeField, Range(1f, 20f)]
    private float zoomSpeed = 10f;                                      // 滾輪縮放速度.
    [SerializeField]
    private float maxDistance = 100f;                  // 相機最遠距離.
    [SerializeField]
    private float minDistance = 1f;                    // 相機最近距離.

    [Header("360° Rotate")]
    // Alt+鼠標拖動, 360度觀察
    [SerializeField, Range(2f, 10f)]
    private float rotateSensitivity = 5f;                               // 旋轉靈敏度.
    private Vector3 lookAroundPos = Vector3.zero;

    // Alt+鼠標右鍵 放大縮小
    [Header("Field Of View")]
    private float minFov = 20f;                                         // 最小視角.
    private float maxFov = 135f;                                        // 最大視角.
    private Vector3 lastMousePosFov = Vector3.zero;
    [SerializeField, Range(0.01f, 0.1f)]
    private float fovSensitivity = 0.05f;                               // 視角靈敏度.

    // 選中物體聚焦
    private GameObject selectedObj = null;                              // 選中的物體.
    private float raycastMaxDistance = 1000f;
    private LayerMask raycastLayerMask;
    private RaycastHit raycastHit;

    [Header("Focus")]
    [SerializeField, Range(0.1f, 5f)]
    private float focusSpeed = 3.0f;                                    // 聚焦的速度.
    [SerializeField]
    private float focusDistance = 3f;                                   // 聚焦時與物體的距離.
    private bool isFocusing = false;                                    // 相機正看向物體中.
    private float focusTime = 0f;                                       // 聚焦時間.
    private Vector3 cameraInitForward;                                  // 相機最初的前方.
    private Vector3 focusTargetForward;                                 // 聚焦目標前方.
    private Vector3 focusInitPos;                                       // 聚焦前相機初始位置.
    private Vector3 focusTargetPos;                                     // 聚焦目標位置.
    #endregion

    #region Unity_Method

    private void Start()
    {
        Init();
    }

    private void LateUpdate()
    {
        // 更新輸入.
        UpdateInput();
        // 切換視角.
        SwitchViewType();
        // 拉近拉遠.
        Zoom();
        // 拖動相機.
        Drag();
        // 360°查看.
        LookAround();
        // 放大縮小. 和切換視角共同使用時有Bug.
        Magnifier();
        // 聚焦.
        Focus();
    }

    private void OnGUI()
    {
        if (GUI.Button(new Rect(Vector2.zero, new Vector2(100, 50)), "正交"))
        {
            SetViewType(ViewType._2D);
        }

        if (GUI.Button(new Rect(Vector2.up * 60, new Vector2(100, 50)), "透視"))
        {
            SetViewType(ViewType._3D);
        }

        if (GUI.Button(new Rect(Vector2.up * 120, new Vector2(100, 50)), "正視圖"))
        {
            Front();
        }

        if (GUI.Button(new Rect(Vector2.up * 180, new Vector2(100, 50)), "後視圖"))
        {
            Back();
        }

        if (GUI.Button(new Rect(Vector2.up * 240, new Vector2(100, 50)), "左視圖"))
        {
            Left();
        }

        if (GUI.Button(new Rect(Vector2.up * 300, new Vector2(100, 50)), "右視圖"))
        {
            Right();
        }

        if (GUI.Button(new Rect(Vector2.up * 360, new Vector2(100, 50)), "上視圖"))
        {
            Top();
        }

        if (GUI.Button(new Rect(Vector2.up * 420, new Vector2(100, 50)), "下視圖"))
        {
            Down();
        }
    }

    #endregion

    #region 初始化相關

    /// <summary>
    /// 初始化.
    /// </summary>
    private void Init()
    {
        m_Camera = GetComponent<Camera>();
        raycastLayerMask = 1 << LayerMask.NameToLayer("Default");

        RecalcDistance();
        CalculateProjMatrix();                  // 獲取相機默認矩陣.
        ResetLookAroundPos();                   // 重置觀察中心.
        LoadCursorTexture();                    // 加載鼠標圖標.
    }

    /// <summary>
    /// 重置觀察中心(觀察中心在相機的前方).
    /// </summary>
    private void ResetLookAroundPos()
    {
        lookAroundPos = m_Camera.transform.position + m_Camera.transform.rotation * (Vector3.forward * distance);
    }

    /// <summary>
    /// 加載鼠標指針圖標.
    /// </summary>
    private void LoadCursorTexture()
    {
        handCursorTexture = Resources.Load<Texture2D>("Textures/Hand");
        eyeCursorTexture = Resources.Load<Texture2D>("Textures/Eye");
        magnifierCursorTexture = Resources.Load<Texture2D>("Textures/Magnifier");
    }

    /// <summary>
    /// 設置鼠標樣式.
    /// </summary>
    private void SetCursor(CursorType cursorType)
    {
        switch (cursorType)
        {
            case CursorType.DEFAULT:
                Cursor.SetCursor(null, hotSpot, cursorMode);
                break;
            case CursorType.HAND:
                Cursor.SetCursor(handCursorTexture, hotSpot, cursorMode);
                break;
            case CursorType.EYE:
                Cursor.SetCursor(eyeCursorTexture, hotSpot, cursorMode);
                break;
            case CursorType.MAGNIFIER:
                Cursor.SetCursor(magnifierCursorTexture, hotSpot, cursorMode);
                break;
            default:
                Debug.LogError("未知指針類型.");
                break;
        }
    }

    /// <summary>
    /// 更新輸入.
    /// </summary>
    private void UpdateInput()
    {
        leftAltKeyDown = Input.GetKeyDown(KeyCode.LeftAlt);                            // 左Alt鍵-按下.
        rightAltKeyDown = Input.GetKeyDown(KeyCode.RightAlt);                          // 右Alt鍵-按下.
        leftAltKey = Input.GetKey(KeyCode.LeftAlt);                                    // 左Alt鍵-長按.
        rightAltKey = Input.GetKey(KeyCode.RightAlt);                                  // 右Alt鍵-長按.
        leftAltKeyUp = Input.GetKeyUp(KeyCode.LeftAlt);                                // 左Alt鍵-擡起.
        rightAltKeyUp = Input.GetKeyUp(KeyCode.RightAlt);                              // 右Alt鍵-擡起.
        rightMouseButtonDown = Input.GetMouseButtonDown((int)MouseButton.RIGHT);       // 鼠標右鍵-按下.
        leftMouseButtonDown = Input.GetMouseButtonDown((int)MouseButton.LEFT);         // 鼠標左鍵-按下.
        leftMouseButton = Input.GetMouseButton((int)MouseButton.LEFT);                 // 鼠標左鍵-長按.
        rightMouseButton = Input.GetMouseButton((int)MouseButton.RIGHT);               // 鼠標右鍵-長按.
        rightMouseButtonUp = Input.GetMouseButtonUp((int)MouseButton.RIGHT);           // 鼠標右鍵-擡起.

        middleMouseButton = Input.GetMouseButton((int)MouseButton.MIDDLE);             // 鼠標中鍵-按下.  
        middleMouseButtonUp = Input.GetMouseButtonUp((int)MouseButton.MIDDLE);         // 鼠標中鍵-擡起.
    }

    /// <summary>
    /// 計算距離.
    /// </summary>
    private void RecalcDistance()
    {
        Vector3 checkFarPos = m_Camera.transform.position + m_Camera.transform.rotation * (Vector3.forward * 1000);
        if (Physics.Linecast(m_Camera.transform.position, checkFarPos, out raycastHit))
        {
            // 如果相機中心有物體,則以物體到相機爲距離
            distance = raycastHit.distance;
            if (selectedObj == null)
            {
                selectedObj = raycastHit.collider.gameObject;
            }
        }
    }

    #endregion

    #region 切換視角

    /// <summary>
    /// 計算投影矩陣.
    /// </summary>
    private void CalculateProjMatrix()
    {
        currentProjMat = m_Camera.projectionMatrix;

        float aspect = m_Camera.aspect;
        float fov = m_Camera.fieldOfView;
        float size = m_Camera.orthographicSize;
        float far = m_Camera.farClipPlane;
        float near = m_Camera.nearClipPlane;
        float tan = Mathf.Tan(Mathf.Deg2Rad * fov * 0.5f);

        if (m_Camera.orthographic)
        {
            // 啓動時相機是正交的
            viewType = ViewType._2D;
            fov = SetFovBySize(size);
        }
        else
        {
            // 啓動時相機是透視的
            viewType = ViewType._3D;
            size = SetSizeByFov(fov);
        }

        // 透視投影矩陣.
        defaultPersProjMat = new Matrix4x4();
        //defaultPersProjMat = Matrix4x4.Perspective(fov, aspect, near, far);           // 透視矩陣也可直接調用這個來計算.
        defaultPersProjMat.SetRow(0, new Vector4(1.0f / (tan * aspect), 0, 0, 0));
        defaultPersProjMat.SetRow(1, new Vector4(0, 1.0f / tan, 0, 0));
        defaultPersProjMat.SetRow(2, new Vector4(0, 0, -(far + near) / (far - near), -2 * far * near / (far - near)));
        defaultPersProjMat.SetRow(3, new Vector4(0, 0, -1, 0));

        // 正交投影矩陣.
        defaultOrthProjMat = new Matrix4x4();
        defaultOrthProjMat.SetRow(0, new Vector4(1.0f / (aspect * size), 0, 0, 0));
        defaultOrthProjMat.SetRow(1, new Vector4(0, 1.0f / size, 0, 0));
        defaultOrthProjMat.SetRow(2, new Vector4(0, 0, -2f / (far - near), -(far + near) / (far - near)));
        defaultOrthProjMat.SetRow(3, new Vector4(0, 0, 0, 1));
    }

    /// <summary>
    /// 切換視角.
    /// </summary>
    private void SwitchViewType()
    {
        if (isChangingViewType)
        {
            viewTypeLerpTime += Time.deltaTime * 2.0f;
            if (viewType == ViewType._2D)
            {
                // 切換到正交視圖.
                currentProjMat = Utils.Math.MatrixLerp(currentProjMat, defaultOrthProjMat, viewTypeLerpTime);
            }
            else
            {
                // 切換到透視視圖.
                currentProjMat = Utils.Math.MatrixLerp(currentProjMat, defaultPersProjMat, viewTypeLerpTime);
            }
            m_Camera.projectionMatrix = currentProjMat;
            if (viewTypeLerpTime >= 1.0f)
            {
                isChangingViewType = false;
                viewTypeLerpTime = 0f;

                if (viewType == ViewType._2D)
                {
                    m_Camera.orthographic = true;
                }
                else
                {
                    m_Camera.orthographic = false;
                }
                m_Camera.ResetProjectionMatrix();
            }
        }
    }

    /// <summary>
    /// 設置將要切換的視角.
    /// </summary>
    /// <param name="targetViewType">目標視角.</param>
    private void SetViewType(ViewType targetViewType)
    {
        if (viewType != targetViewType)
        {
            viewType = targetViewType;
            isChangingViewType = true;
            viewTypeLerpTime = 0f;
        }
    }

    /// <summary>
    /// 根據正交相機的size設置對應透視相機的fov
    /// </summary>
    /// <param name="size">正交相機高度的一半,即orthographicSize</param>
    private float SetFovBySize(float size)
    {
        float fov = 2.0f * Mathf.Atan(size / distance) * Mathf.Rad2Deg;
        m_Camera.fieldOfView = fov;
        return fov;
    }

    /// <summary>
    /// 根據透視相機的fov設置對應正交相機的即orthographicSize
    /// </summary>
    /// <param name="fov">透視相機的fov</param>
    private float SetSizeByFov(float fov)
    {
        float size = distance * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
        m_Camera.orthographicSize = size;
        return size;
    }

    #endregion

    #region 拉近拉遠

    /// <summary>
    /// 拉近拉遠.
    /// </summary>
    /// <remarks>鼠標滾輪滾動,向上滾動拉近相機,向下滾動拉遠相機</remarks>
    private void Zoom()
    {
        float scrollWheelValue = Input.GetAxis(MOUSESCROLLWHEEL);
        if (!Utils.Math.IsEqual(scrollWheelValue, 0f))
        {
            // scrollWheelValue:滾輪向上爲+ 向下爲-
            // 同時滾輪向上,拉近相機;滾輪向下,拉遠相機
            distance -= scrollWheelValue * zoomSpeed;
            distance = Mathf.Clamp(distance, minDistance, maxDistance);

            m_Camera.transform.position = lookAroundPos - m_Camera.transform.rotation * (Vector3.forward * distance);

            // 相機距離改變後需同時修改fov或orthographicSize以及投影矩陣,保證正交/透視視圖切換效果
            CalculateProjMatrix();
        }
    }

    #endregion

    #region 拖動

    private bool isDraging = false;
    private Vector3 lastMousePos;

    /// <summary>
    /// 拖動.
    /// </summary>
    /// <remark>長按鼠標中鍵拖動</remark>
    private void Drag()
    {
        // 鼠標中鍵拖動相機.
        if (middleMouseButton)
        {
            if (isDraging == false)
            {
                isDraging = true;
                // 設置小手指針.
                SetCursor(CursorType.HAND);

                lastMousePos = Input.mousePosition;
            }
            else
            {
                Vector3 newMousePos = Input.mousePosition;
                Vector3 delta = newMousePos - lastMousePos;
                m_Camera.transform.position += CalcDragLength(m_Camera, delta, distance);
                lastMousePos = newMousePos;

                // 相機移動,重新設置目標位置.
                ResetLookAroundPos();
            }
        }
        if (middleMouseButtonUp)
        {
            isDraging = false;
            // 恢復默認指針.
            SetCursor(CursorType.DEFAULT);
        }
    }

    /// <summary>
    /// 計算拖拽距離.
    /// </summary>
    /// <param name="camera">相機</param>
    /// <param name="mouseDelta">鼠標移動距離</param>
    /// <param name="distance">相機距離觀察物體的距離</param>
    private Vector3 CalcDragLength(Camera camera, Vector2 mouseDelta, float distance)
    {
        float rectHeight = -1;
        float rectWidth = -1;
        if (camera.orthographic)
        {
            rectHeight = 2 * camera.orthographicSize;
            //rectWidth = rectHeight / camera.aspect;
        }
        else
        {
            rectHeight = 2 * distance * Mathf.Tan(camera.fieldOfView * 0.5f * Mathf.Deg2Rad);
        }
        rectWidth = Screen.width * rectHeight / Screen.height;
        Vector3 moveDir = -rectWidth / Screen.width * mouseDelta.x * camera.transform.right - rectHeight / Screen.height * mouseDelta.y * camera.transform.up;

        return moveDir;
    }

    #endregion

    #region 360°旋轉觀察

    /// <summary>
    /// 360°旋轉查看.
    /// </summary>
    /// <remark>Alt+鼠標左鍵 或 鼠標右鍵</remark>
    private void LookAround()
    {
        if (leftAltKeyDown || rightAltKeyDown || rightMouseButtonDown)
        {
            SetCursor(CursorType.EYE);

            angle_X = m_Camera.transform.eulerAngles.y;
            angle_Y = m_Camera.transform.eulerAngles.x;
            angle_Z = m_Camera.transform.eulerAngles.z;
        }

        if ((leftAltKey && leftMouseButton)
            || (rightAltKey && leftMouseButton)
            || (!leftAltKey && !rightAltKey && rightMouseButton))
        {
            float deltaX = Input.GetAxis(MOUSEX);
            float deltaY = Input.GetAxis(MOUSEY);

            // 相機朝前和朝後,與鼠標的滑動方向相反
            if ((angle_Y > 90f && angle_Y < 270f) || (angle_Y < -90 && angle_Y > -270f))
            {
                angle_X -= deltaX * rotateSensitivity;
            }
            else
            {
                angle_X += deltaX * rotateSensitivity;
            }
            angle_Y -= deltaY * rotateSensitivity;

            angle_X = Utils.Math.ClampAngle(angle_X, -365, 365);
            angle_Y = Utils.Math.ClampAngle(angle_Y, -365, 365);

            SetCameraPos();
        }

        if (leftAltKeyUp || rightAltKeyUp || rightMouseButtonUp)
        {
            SetCursor(CursorType.DEFAULT);
        }
    }

    private void SetCameraPos()
    {
        Quaternion rotation = Quaternion.Euler(angle_Y, angle_X, angle_Z);
        Vector3 dir = Vector3.forward * -distance;
        m_Camera.transform.rotation = rotation;
        m_Camera.transform.position = lookAroundPos + rotation * dir;
    }

    #endregion

    #region 聚焦選中物體

    /// <summary>
    /// 聚焦選中物體.
    /// </summary>
    private void Focus()
    {
        if (leftMouseButtonDown)
        {
            Ray ray = m_Camera.ScreenPointToRay(Input.mousePosition);

            // Scene視圖繪製射線方便測試.
            //Debug.DrawLine(ray.origin, ray.origin + ray.direction * raycastMaxDistance, Color.red, 3f);

            if (Physics.Raycast(ray, out raycastHit, raycastMaxDistance, raycastLayerMask))
            {
                selectedObj = raycastHit.collider.gameObject;
            }
            else
            {
                selectedObj = null;
                ResetLookAroundPos();
            }
        }

        if (selectedObj != null)
        {
            if (Input.GetKeyDown(KeyCode.F))
            {
                isFocusing = true;
                focusTime = 0f;
                cameraInitForward = m_Camera.transform.forward;
                focusTargetForward = Vector3.Normalize(selectedObj.transform.position - m_Camera.transform.position);

                focusInitPos = m_Camera.transform.position;
                distance = focusDistance;
                focusTargetPos = selectedObj.transform.position - focusTargetForward * distance;
            }
        }

        if (isFocusing)
        {
            focusTime += Time.deltaTime * focusSpeed;
            Vector3 forward = Vector3.Lerp(cameraInitForward, focusTargetForward, focusTime);
            m_Camera.transform.rotation = Quaternion.LookRotation(forward);

            m_Camera.transform.position = Vector3.Lerp(focusInitPos, focusTargetPos, focusTime);

            if (focusTime >= 1f)
            {
                // 聚焦完畢.
                focusTime = 0f;
                isFocusing = false;
                m_Camera.transform.rotation = Quaternion.LookRotation(focusTargetForward);
                m_Camera.transform.position = focusTargetPos;

                // 設置觀察中心.
                lookAroundPos = selectedObj.transform.position;
            }
        }
    }

    #endregion

    #region 各種視圖

    /// <summary>
    /// 正視圖.
    /// </summary>
    private void Front()
    {
        angle_X = 180;
        angle_Y = 0;

        SetCameraPos();
    }

    /// <summary>
    /// 後視圖.
    /// </summary>
    private void Back()
    {
        angle_X = 0;
        angle_Y = 0;

        SetCameraPos();
    }

    /// <summary>
    /// 右視圖.
    /// </summary>
    private void Right()
    {
        angle_X = -90;
        angle_Y = 0;

        SetCameraPos();
    }

    /// <summary>
    /// 左視圖.
    /// </summary>
    private void Left()
    {
        angle_X = 90;
        angle_Y = 0;

        SetCameraPos();
    }

    /// <summary>
    /// 上視圖(俯視圖).
    /// </summary>
    private void Top()
    {
        angle_X = 0;
        angle_Y = 90;

        SetCameraPos();
    }

    /// <summary>
    /// 下視圖.
    /// </summary>
    private void Down()
    {
        angle_X = 0;
        angle_Y = -90;

        SetCameraPos();
    }

    #endregion

    #region 放大/縮小

    /// <summary>
    /// 放大縮小.
    /// </summary>
    private void Magnifier()
    {
        if ((leftAltKey || rightAltKey) && rightMouseButton)
        {
            if (lastMousePosFov.Equals(Vector3.zero))
            {
                lastMousePosFov = Input.mousePosition;
                SetCursor(CursorType.MAGNIFIER);
            }

            float deltaX = lastMousePosFov.x - Input.mousePosition.x;

            if (viewType == ViewType._2D)
            {
                // 透視視圖改size
                float size = m_Camera.orthographicSize + deltaX * 0.01f;
                size = Mathf.Clamp(size, 1.0f, 8f);
                m_Camera.orthographicSize = size;

                // 調節size需同時修改相機透視模式下的fov
                SetFovBySize(size);
            }
            else
            {
                // 透視視圖改fov
                float fov = m_Camera.fieldOfView + deltaX * fovSensitivity;
                fov = Mathf.Clamp(fov, minFov, maxFov);
                m_Camera.fieldOfView = fov;

                // 調節了fov需同時修改相機正交模式下的size
                SetSizeByFov(fov);
            }

            lastMousePosFov = Input.mousePosition;
        }

        if (rightMouseButtonUp)
        {
            SetCursor(CursorType.DEFAULT);
            lastMousePosFov = Vector3.zero;

            // 重新計算投影矩陣.
            CalculateProjMatrix();
        }
    }

    #endregion
}

  輔助類。

using UnityEngine;

namespace Utils
{
    public static class Math
    {
        public static bool IsEqual(float a, float b)
        {
            if(Mathf.Abs(a - b) < 0.000001f)
            {
                return true;
            }
            return false;
        }

        public static Matrix4x4 MatrixLerp(Matrix4x4 from, Matrix4x4 to, float t)
        {
            t = Mathf.Clamp01(t);
            Matrix4x4 lerpMatrix = new Matrix4x4();
            for (int i = 0; i < 4; i++)
            {
                lerpMatrix.SetRow(i, Vector4.Lerp(from.GetRow(i), to.GetRow(i), t));
            }
            return lerpMatrix;
        }

        /// <summary>
        /// 對角度進行限制.
        /// </summary>
        public static float ClampAngle(float angle, float min, float max)
        {
            if (angle < -360)
                angle += 360;
            if (angle > 360)
                angle -= 360;
            return Mathf.Clamp(angle, min, max);
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章