直接上代码
核心工具代码:
using UnityEngine;
namespace Extension
{
public static class CameraFocusExtension
{
/// <summary>
/// 通过Render获取包围盒
/// </summary>
/// <param name="component"></param>
/// <returns></returns>
public static Bounds GetBoundsWithRender(this Component component)
{
var bounds = new Bounds(component.transform.position, Vector3.zero);
foreach (Renderer renderer in component.GetComponentsInChildren<Renderer>())
{
bounds.Encapsulate(renderer.bounds);
}
return bounds;
}
/// <summary>
/// 直接聚焦目标
/// </summary>
/// <param name="mainCamera">摄像机</param>
/// <param name="distanceOffset">摄像机位移偏差</param>
/// <param name="bounds">目标包围盒</param>
/// <param name="isLockRotation">锁定旋转</param>
public static void FocusBounds(this Camera mainCamera, Bounds bounds,float distanceOffset = 0,bool isLockRotation = false)
{
mainCamera.GetInfoOnFocusBounds(bounds,out var cameraPosition,out Quaternion cameraRotation,distanceOffset,isLockRotation);
mainCamera.transform.position = cameraPosition;
mainCamera.transform.rotation = cameraRotation;
}
/// <summary>
/// 聚焦目标,并让聚焦物体位于RectTransform内
/// 注意,此方法会锁定物体旋转
/// </summary>
/// <param name="mainCamera"></param>
/// <param name="bounds"></param>
/// <param name="rect"></param>
/// <param name="distanceOffset"></param>
public static void FocusBounds(this Camera mainCamera, Bounds bounds,RectTransform rect, float distanceOffset = 0
)
{
//计算比例
mainCamera.GetInfoOnFocusBounds(bounds,rect,out var cameraPosition,out Quaternion cameraRotation,distanceOffset);
mainCamera.transform.position = cameraPosition;
mainCamera.transform.rotation = cameraRotation;
}
/// <summary>
/// 获得聚焦时摄像机应该处于的坐标,不会控制Camera移动和旋转
/// 注意,此坐标已经默认摄像机朝向物体,不需要应用后又旋转摄像机
/// </summary>
/// <param name="mainCamera">摄像机</param>
/// <param name="bounds">目标包围盒</param>
/// <param name="cameraRotation">摄像机旋转角度</param>
/// <param name="distanceOffset">摄像机位移偏差</param>
/// <param name="cameraPosition">摄像机坐标</param>
/// <param name="isLockRotation">是否锁定旋转,锁定下返回的是摄像机当前的旋转值</param>
/// <returns></returns>
public static void GetInfoOnFocusBounds(this Camera mainCamera, Bounds bounds,out Vector3 cameraPosition,out Quaternion cameraRotation,float distanceOffset = 0,bool isLockRotation = false)
{
Vector3[] vertices = new Vector3[8];
var cameraTransform = mainCamera.transform;
var targetCenter = bounds.center;
if (isLockRotation)
{
cameraPosition = bounds.center - cameraTransform.forward*20;
cameraRotation = cameraTransform.rotation;
}
else
{
cameraPosition = cameraTransform.position;
cameraRotation = Quaternion.LookRotation(targetCenter-cameraPosition);
}
var centerDirection = targetCenter - cameraPosition;
//计算Bounds盒八个点中,距离轴心最远的点
var res =GetMaxOutPointAndOffset(bounds,centerDirection, cameraPosition,targetCenter,ref vertices);
var outPoint = res.Item1;
var viewAngle = GetCameraViewPortAngle(mainCamera, cameraTransform.forward);
var centerDistance = Vector3.Distance(targetCenter, cameraPosition);
var pointDistance = GetDistanceToLine(outPoint, cameraPosition, centerDirection);
var d = Mathf.Tan(viewAngle* Mathf.Deg2Rad) * centerDistance;
d = (pointDistance / d) * centerDistance;
d += res.Item2;
cameraPosition = targetCenter + (-centerDirection.normalized * (d * (1+distanceOffset)));
}
public static void GetInfoOnFocusBounds(this Camera mainCamera, Bounds bounds,RectTransform rectTransform,out Vector3 cameraPosition,out Quaternion cameraRotation,float distanceOffset = 0)
{
Vector3[] vertices = new Vector3[8];
var screenRect = Screen.safeArea;
var rect = rectTransform.rect;
rect.width *= rectTransform.localScale.x;
rect.height *= rectTransform.localScale.y;
var cameraTransform = mainCamera.transform;
var targetCenter = bounds.center;
var mul = new Vector2(rect.width / screenRect.width, rect.height / screenRect.height);
Vector3 v1 = rectTransform.position;
Vector3 v2 = screenRect.center;
v1.z = 10;
v2.z = 10;
var focusOffsetToScreen =mainCamera.ScreenToWorldPoint(v1) - mainCamera.ScreenToWorldPoint(v2);
cameraPosition = bounds.center - cameraTransform.forward*10 - focusOffsetToScreen;
cameraRotation = cameraTransform.rotation;
var centerDirection = targetCenter - cameraPosition;
//计算Bounds盒八个点中,距离轴心最远的点
var res =GetMaxOutPointAndOffset(bounds,centerDirection, cameraPosition,targetCenter,ref vertices);
var outPoint = res.Item1;
var viewAngle = GetCameraViewPortAngle(mainCamera, cameraTransform.forward) *Mathf.Min(mul.x,mul.y);
var centerDistance = Vector3.Distance(targetCenter, cameraPosition);
var pointDistance = GetDistanceToLine(outPoint, cameraPosition, centerDirection);
var d = Mathf.Tan(viewAngle* Mathf.Deg2Rad) * centerDistance;
d = (pointDistance / d) * centerDistance;
d += res.Item2;
cameraPosition = targetCenter + (-centerDirection.normalized * (d * (1+distanceOffset)));
}
static (Vector3,float) GetMaxOutPointAndOffset(Bounds targetBounds,Vector3 centerDirection,Vector3 currentPosition,Vector3 targetCenter,ref Vector3[] vertices)
{
float maxDistance = 0;
float distanceOffset = 0;
Vector3 point = default;
Vector3 min = targetBounds.min;
Vector3 max = targetBounds.max;
vertices[0] = new Vector3(min.x, min.y, min.z);
vertices[1] = new Vector3(min.x, min.y, max.z);
vertices[2] = new Vector3(min.x, max.y, min.z);
vertices[3] = new Vector3(min.x, max.y, max.z);
vertices[4] = new Vector3(max.x, min.y, min.z);
vertices[5] = new Vector3(max.x, min.y, max.z);
vertices[6] = new Vector3(max.x, max.y, min.z);
vertices[7] = new Vector3(max.x, max.y, max.z);
foreach (var v in vertices)
{
var dis = GetSqrDistanceToLine(v, currentPosition, centerDirection);
if (dis > maxDistance)
{
maxDistance = dis;
point = v;
}
//计算点到中心的向量
var dir = v - targetCenter;
distanceOffset += Vector3.Project(dir, centerDirection).magnitude;
}
distanceOffset /= 8;
return (point,distanceOffset);
}
static float GetSqrDistanceToLine(Vector3 point, Vector3 point2,Vector3 direction)
{
var pointDir = point - point2;
var f = Vector3.Project(pointDir, direction) + point2;
var dis = Vector3.SqrMagnitude(point- f);
return dis;
}
static float GetDistanceToLine(Vector3 point, Vector3 point2,Vector3 direction)
{
var pointDir = point - point2;
var f = Vector3.Project(pointDir, direction) + point2;
var dis = Vector3.Distance(point, f);
return dis;
}
static float GetCameraViewPortAngle(Camera camera,Vector3 centerDirection)
{
float minAngle = 0;
var planes = GeometryUtility.CalculateFrustumPlanes(camera);
for (int i = 0; i < 4; i++)
{
var plane = planes[i];
//计算投影
var p = Vector3.ProjectOnPlane(centerDirection, plane.normal);
var angle = Vector3.Angle(p, centerDirection);
if (minAngle == 0)
minAngle = angle;
else if(angle < minAngle)
minAngle = angle;
}
return minAngle;
}
}
}
然后是示例代码:
#if UNITY_EDITOR
using Extension;
using UnityEngine;
using UnityEngine.InputSystem;
[ExecuteInEditMode]
public class CameraFocusSample : MonoBehaviour
{
public Transform targetTransform;
public Bounds targetBounds;
public RectTransform rectTransform;
public bool useRect = false;
public bool updateBuildBounds = true;
public bool updateFocus = false;
/// <summary>
/// 虽然场景里会出现锁定时但能旋转不能位移
/// 但这是正常现象,因为这个参数是决定聚焦时是否改变摄像机旋转值的
/// </summary>
public bool isLockRotation= false;
public float distanceOffset = 0;
public Camera mainCamera;
private void Update()
{
if (updateBuildBounds && targetTransform != null)
{
targetBounds = targetTransform.GetBoundsWithRender();
}
if (mainCamera == null)
mainCamera = Camera.main;
else if(updateFocus)
{
if (useRect)
mainCamera.FocusBounds(targetBounds,rectTransform,distanceOffset);
else
mainCamera.FocusBounds(targetBounds,distanceOffset,isLockRotation);
}
}
[ContextMenu("Focus")]
private void Focus()
{
if (mainCamera == null)
mainCamera = Camera.main;
mainCamera.GetInfoOnFocusBounds(targetBounds,out var position,out var rotation,distanceOffset,isLockRotation);
mainCamera.transform.position = position;
mainCamera.transform.rotation = rotation;
}
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.cyan;
Gizmos.DrawWireCube(targetBounds.center,targetBounds.size);
var focusRect = rectTransform.rect;
var z = Vector3.Distance(Camera.main.transform.position, targetTransform.position);
var screenRect = Screen.safeArea;
Vector3 v1 = rectTransform.position;
v1.z = z;
Vector3 v2 = screenRect.center;
v2.z = z;
v1 = mainCamera.ScreenToWorldPoint(v1);
v2 = mainCamera.ScreenToWorldPoint(v2);
Gizmos.DrawCube(v1,Vector3.one);
Gizmos.DrawCube(v2,Vector3.one);
Gizmos.color = Color.red;
Gizmos.DrawLine(v1,v2);
var focusOffsetToScreen =v1 -v2;
Gizmos.DrawCube(Camera.main.transform.position - focusOffsetToScreen,Vector3.one);
}
}
#endif
示例图: