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;
}
}
}