3D UI
遊戲中經常出現一些斜向放置的UI,這些UI在世界空間中擺放,被透視攝像機所觀察。如下圖中的設置。
擺放一組帶有角度的UI:
下圖是旋轉後的UI,因爲使用透視相機的緣故,這裏可以看到明顯的立體效果。
但我們經常會想控制這些UI的透視,使他們的透視中心不再是屏幕的中心。下圖就是將透視中心移動到右上角的效果。
實現
上圖效果可以類比爲,一個普通的透視畫面,截取畫面左下角的一塊圖案,而這個圖案就是屏幕顯示的部分。這裏我們可能很快想到通過攝像機的Viewport Rect設置來實現這種效果,但是因爲我們希望在UI中使用這個效果,Viewport Rect會影響到很多Canvas相關的設置和屏幕顯示位置,使事情變得更加複雜。
於是我們想到另一個辦法,改變攝像機的矩陣,正常的相機矩陣如下圖所示:
我們可以改變攝像機矩陣,使其不再是一個對稱的形體;即近裁面中點、遠裁面中點、攝像機位置,三點不再共線。
用以下角度,在正交空間觀察:
攝像機位置相對Canva平面的位置就是透視中心,現在透視中心位於正中央,做如下圖改變,透視中心變爲了右上角,得到了上面那張透視圖:
這裏可以看到,攝像機矩陣變了,從而透視發生了變化。
我們希望輸入屏幕的相對位置,得到一個不會影響Canvas相關設置的結果,但是上圖中可以明顯看到Canvas(相機正前方的白框)已經與顯示內容分離了,其實這裏爲了不影響UI縮放等邏輯,中間加了一層轉換:
Convert便是轉換,它使用四周對齊,尺寸與CanvasPlane一致,通過變換位置使其位於變化後的攝像機矩陣中。
代碼
注意:下面的計算默認攝像機旋轉角度是(0,0,0)。
using UnityEngine;
[ExecuteInEditMode]
public class CameraMatrixSetter : MonoBehaviour
{
[Header("視覺偏移"), Tooltip("相對屏幕中心的偏移,-1f - 1f的範圍是屏幕部分。"), SerializeField]
private Vector2 offset;
public Vector2 Offset
{
get
{
return offset;
}
set
{
offset = value;
Refresh();
}
}
public Canvas canvas;
public Camera worldCamera;
public Transform convertTransform;
private void OnEnable()
{
Refresh();
}
private void OnDisable()
{
worldCamera.ResetProjectionMatrix();
convertTransform.position = new Vector3(0, 0, canvas.planeDistance) + worldCamera.transform.position;
}
#if UNITY_EDITOR
private void OnValidate()
{
Refresh();
UnityEditor.SceneView.RepaintAll();
}
private void Update()
{
Refresh();
}
#endif
public void Refresh()
{
if (canvas == null || worldCamera == null)
return;
Vector2 canvasSize = ((RectTransform)canvas.transform).sizeDelta * canvas.transform.lossyScale;
Vector2 canvasOffsetSize = -offset * canvasSize;
Vector2 nearPlaneOffset = canvasOffsetSize * worldCamera.nearClipPlane / canvas.planeDistance;
worldCamera.ResetProjectionMatrix();
FrustumPlanes decomposeProjection = worldCamera.projectionMatrix.decomposeProjection;
decomposeProjection.left += nearPlaneOffset.x;
decomposeProjection.right += nearPlaneOffset.x;
decomposeProjection.top += nearPlaneOffset.y;
decomposeProjection.bottom += nearPlaneOffset.y;
Matrix4x4 frustumMatrix4x4 = Matrix4x4.Frustum(decomposeProjection);
worldCamera.projectionMatrix = frustumMatrix4x4;
UpdateCanvas(canvasOffsetSize);
}
private void UpdateCanvas(Vector2 canvasSize)
{
if (convertTransform)
{
convertTransform.position = new Vector3(canvasSize.x, canvasSize.y, canvas.planeDistance) + worldCamera.transform.position;
}
}
}