#if !UNITY_2019_3_OR_NEWER
#define CINEMACHINE_PHYSICS
#endif
using UnityEngine;
using System.Collections.Generic;
using Cinemachine.Utility;
using UnityEngine.Serialization;
using System;
namespace Cinemachine
{
#if CINEMACHINE_PHYSICS
///
/// An add-on module for Cinemachine Virtual Camera that post-processes
/// the final position of the virtual camera. Based on the supplied settings,
/// the Collider will attempt to preserve the line of sight
/// with the LookAt target of the virtual camera by moving
/// away from objects that will obstruct the view.
///
/// Additionally, the Collider can be used to assess the shot quality and
/// report this as a field in the camera State.
///
[DocumentationSorting(DocumentationSortingAttribute.Level.UserRef)]
[AddComponentMenu("")] // Hide in menu
[SaveDuringPlay]
[ExecuteAlways]
[DisallowMultipleComponent]
[HelpURL(Documentation.BaseURL + "manual/CinemachineCollider.html")]
public class CinemachineCollider : CinemachineExtension
{
/// Objects on these layers will be detected.
[Header("Obstacle Detection")]
[Tooltip("Objects on these layers will be detected")]
public LayerMask m_CollideAgainst = 1;
/// Obstacles with this tag will be ignored. It is a good idea to set this field to the target's tag
[TagField]
[Tooltip("Obstacles with this tag will be ignored. It is a good idea to set this field to the target's tag")]
public string m_IgnoreTag = string.Empty;
/// Objects on these layers will never obstruct view of the target.
[Tooltip("Objects on these layers will never obstruct view of the target")]
public LayerMask m_TransparentLayers = 0;
/// Obstacles closer to the target than this will be ignored
[Tooltip("Obstacles closer to the target than this will be ignored")]
public float m_MinimumDistanceFromTarget = 0.1f;
///
/// When enabled, will attempt to resolve situations where the line of sight to the
/// target is blocked by an obstacle
///
[Space]
[Tooltip("When enabled, will attempt to resolve situations where the line of sight "
+ "to the target is blocked by an obstacle")]
[FormerlySerializedAs("m_PreserveLineOfSight")]
public bool m_AvoidObstacles = true;
///
/// The raycast distance to test for when checking if the line of sight to this camera's target is clear.
///
[Tooltip("The maximum raycast distance when checking if the line of sight to this camera's target is clear. "
+ "If the setting is 0 or less, the current actual distance to target will be used.")]
[FormerlySerializedAs("m_LineOfSightFeelerDistance")]
public float m_DistanceLimit;
///
/// Don't take action unless occlusion has lasted at least this long.
///
[Tooltip("Don't take action unless occlusion has lasted at least this long.")]
public float m_MinimumOcclusionTime;
///
/// Camera will try to maintain this distance from any obstacle.
/// Increase this value if you are seeing inside obstacles due to a large
/// FOV on the camera.
///
[Tooltip("Camera will try to maintain this distance from any obstacle. Try to keep this value small. "
+ "Increase it if you are seeing inside obstacles due to a large FOV on the camera.")]
public float m_CameraRadius = 0.1f;
/// The way in which the Collider will attempt to preserve sight of the target.
public enum ResolutionStrategy
{
/// Camera will be pulled forward along its Z axis until it is in front of
/// the nearest obstacle
PullCameraForward,
/// In addition to pulling the camera forward, an effort will be made to
/// return the camera to its original height
PreserveCameraHeight,
/// In addition to pulling the camera forward, an effort will be made to
/// return the camera to its original distance from the target
PreserveCameraDistance
};
/// The way in which the Collider will attempt to preserve sight of the target.
[Tooltip("The way in which the Collider will attempt to preserve sight of the target.")]
public ResolutionStrategy m_Strategy = ResolutionStrategy.PreserveCameraHeight;
///
/// Upper limit on how many obstacle hits to process. Higher numbers may impact performance.
/// In most environments, 4 is enough.
///
[Range(1, 10)]
[Tooltip("Upper limit on how many obstacle hits to process. Higher numbers may impact performance. "
+ "In most environments, 4 is enough.")]
public int m_MaximumEffort = 4;
///
/// Smoothing to apply to obstruction resolution. Nearest camera point is held for at least this long.
///
[Range(0, 2)]
[Tooltip("Smoothing to apply to obstruction resolution. Nearest camera point is held for at least this long")]
public float m_SmoothingTime;
///
/// How gradually the camera returns to its normal position after having been corrected.
/// Higher numbers will move the camera more gradually back to normal.
///
[Range(0, 10)]
[Tooltip("How gradually the camera returns to its normal position after having been corrected. "
+ "Higher numbers will move the camera more gradually back to normal.")]
[FormerlySerializedAs("m_Smoothing")]
public float m_Damping;
///
/// How gradually the camera moves to resolve an occlusion.
/// Higher numbers will move the camera more gradually.
///
[Range(0, 10)]
[Tooltip("How gradually the camera moves to resolve an occlusion. "
+ "Higher numbers will move the camera more gradually.")]
public float m_DampingWhenOccluded;
/// If greater than zero, a higher score will be given to shots when the target is closer to
/// this distance. Set this to zero to disable this feature
[Header("Shot Evaluation")]
[Tooltip("If greater than zero, a higher score will be given to shots when the target is closer to this distance. "
+ "Set this to zero to disable this feature.")]
public float m_OptimalTargetDistance;
/// See whether an object is blocking the camera's view of the target
/// The virtual camera in question. This might be different from the
/// virtual camera that owns the collider, in the event that the camera has children
/// True if something is blocking the view
public bool IsTargetObscured(ICinemachineCamera vcam)
{
return GetExtraState(vcam).targetObscured;
}
/// See whether the virtual camera has been moved nby the collider
/// The virtual camera in question. This might be different from the
/// virtual camera that owns the collider, in the event that the camera has children
/// True if the virtual camera has been displaced due to collision or
/// target obstruction
public bool CameraWasDisplaced(ICinemachineCamera vcam)
{
return GetCameraDisplacementDistance(vcam) > 0;
}
/// See how far the virtual camera wa moved nby the collider
/// The virtual camera in question. This might be different from the
/// virtual camera that owns the collider, in the event that the camera has children
/// True if the virtual camera has been displaced due to collision or
/// target obstruction
public float GetCameraDisplacementDistance(ICinemachineCamera vcam)
{
return GetExtraState(vcam).previousDisplacement.magnitude;
}
void OnValidate()
{
m_DistanceLimit = Mathf.Max(0, m_DistanceLimit);
m_MinimumOcclusionTime = Mathf.Max(0, m_MinimumOcclusionTime);
m_CameraRadius = Mathf.Max(0, m_CameraRadius);
m_MinimumDistanceFromTarget = Mathf.Max(0.01f, m_MinimumDistanceFromTarget);
m_OptimalTargetDistance = Mathf.Max(0, m_OptimalTargetDistance);
}
///
/// Cleanup
///
protected override void OnDestroy()
{
RuntimeUtility.DestroyScratchCollider();
base.OnDestroy();
}
/// This must be small but greater than 0 - reduces false results due to precision
const float k_PrecisionSlush = 0.001f;
///
/// Per-vcam extra state info
///
class VcamExtraState
{
public Vector3 previousDisplacement;
public Vector3 previousCameraOffset;
public Vector3 previousCameraPosition;
public float previousDampTime;
public bool targetObscured;
public float occlusionStartTime;
public List debugResolutionPath;
public void AddPointToDebugPath(Vector3 p)
{
#if UNITY_EDITOR
if (debugResolutionPath == null)
debugResolutionPath = new List();
debugResolutionPath.Add(p);
#endif
}
// Thanks to Sebastien LeTouze from Exiin Studio for the smoothing idea
float m_SmoothedDistance;
float m_SmoothedTime;
public float ApplyDistanceSmoothing(float distance, float smoothingTime)
{
if (m_SmoothedTime != 0 && smoothingTime > Epsilon)
{
float now = CinemachineCore.CurrentTime;
if (now - m_SmoothedTime < smoothingTime)
return Mathf.Min(distance, m_SmoothedDistance);
}
return distance;
}
public void UpdateDistanceSmoothing(float distance)
{
if (m_SmoothedDistance == 0 || distance < m_SmoothedDistance)
{
m_SmoothedDistance = distance;
m_SmoothedTime = CinemachineCore.CurrentTime;
}
}
public void ResetDistanceSmoothing(float smoothingTime)
{
float now = CinemachineCore.CurrentTime;
if (now - m_SmoothedTime >= smoothingTime)
m_SmoothedDistance = m_SmoothedTime = 0;
}
};
/// Inspector API for debugging collision resolution path
public List> DebugPaths
{
get
{
List> list = new List>();
List extraStates = GetAllExtraStates();
foreach (var v in extraStates)
if (v.debugResolutionPath != null && v.debugResolutionPath.Count > 0)
list.Add(v.debugResolutionPath);
return list;
}
}
///
/// Report maximum damping time needed for this component.
///
/// Highest damping setting in this component
public override float GetMaxDampTime()
{
return Mathf.Max(m_Damping, Mathf.Max(m_DampingWhenOccluded, m_SmoothingTime));
}
/// This is called to notify the extension that a target got warped,
/// so that the extension can update its internal state to make the camera
/// also warp seamlessy. Base class implementation does nothing.
/// The object that was warped
/// The amount the target's position changed
public override void OnTargetObjectWarped(Transform target, Vector3 positionDelta)
{
var states = GetAllExtraStates();
for (int i = 0; i < states.Count; ++i)
{
var extra = states[i];
extra.previousCameraPosition += positionDelta;
}
}
///
/// Callback to do the collision resolution and shot evaluation
///
/// The virtual camera being processed
/// The current pipeline stage
/// The current virtual camera state
/// The current applicable deltaTime
protected override void PostPipelineStageCallback(
CinemachineVirtualCameraBase vcam,
CinemachineCore.Stage stage, ref CameraState state, float deltaTime)
{
if (stage == CinemachineCore.Stage.Body)
{
var extra = GetExtraState(vcam);
extra.targetObscured = false;
extra.debugResolutionPath?.RemoveRange(0, extra.debugResolutionPath.Count);
if (m_AvoidObstacles)
{
var initialCamPos = state.CorrectedPosition;
// Rotate the previous collision correction along with the camera
var dampingBypass = Quaternion.Euler(state.PositionDampingBypass);
extra.previousDisplacement = dampingBypass * extra.previousDisplacement;
// Calculate the desired collision correction
Vector3 displacement = PreserveLineOfSight(ref state, ref extra);
if (m_MinimumOcclusionTime > Epsilon)
{
// If minimum occlusion time set, ignore new occlusions until they've lasted long enough
float now = CinemachineCore.CurrentTime;
if (displacement.AlmostZero())
extra.occlusionStartTime = 0; // no occlusion
else
{
if (extra.occlusionStartTime <= 0)
extra.occlusionStartTime = now; // occlusion timer starts now
if (now - extra.occlusionStartTime < m_MinimumOcclusionTime)
displacement = extra.previousDisplacement;
}
}
// Apply distance smoothing - this can artificially hold the camera closer
// to the target for a while, to reduce popping in and out on bumpy objects
if (m_SmoothingTime > Epsilon && state.HasLookAt)
{
Vector3 pos = initialCamPos + displacement;
Vector3 dir = pos - state.ReferenceLookAt;
float distance = dir.magnitude;
if (distance > Epsilon)
{
dir /= distance;
if (!displacement.AlmostZero())
extra.UpdateDistanceSmoothing(distance);
distance = extra.ApplyDistanceSmoothing(distance, m_SmoothingTime);
displacement += (state.ReferenceLookAt + dir * distance) - pos;
}
}
if (displacement.AlmostZero())
extra.ResetDistanceSmoothing(m_SmoothingTime);
// Apply additional correction due to camera radius
var cameraPos = initialCamPos + displacement;
var lookAt = state.HasLookAt ? state.ReferenceLookAt : cameraPos;
displacement += RespectCameraRadius(cameraPos, lookAt);
// Apply damping
float dampTime = m_DampingWhenOccluded;
if (deltaTime >= 0 && VirtualCamera.PreviousStateIsValid && m_DampingWhenOccluded + m_Damping > Epsilon)
{
// To ease the transition between damped and undamped regions, we damp the damp time
var dispSqrMag = displacement.sqrMagnitude;
dampTime = dispSqrMag > extra.previousDisplacement.sqrMagnitude ? m_DampingWhenOccluded : m_Damping;
if (dispSqrMag < Epsilon)
dampTime = extra.previousDampTime - Damper.Damp(extra.previousDampTime, dampTime, deltaTime);
if (dampTime > 0)
{
bool bodyAfterAim = false;
if (vcam is CinemachineVirtualCamera)
{
var body = (vcam as CinemachineVirtualCamera).GetCinemachineComponent(CinemachineCore.Stage.Body);
bodyAfterAim = body != null && body.BodyAppliesAfterAim;
}
var prevDisplacement = bodyAfterAim ? extra.previousDisplacement
: lookAt + dampingBypass * extra.previousCameraOffset - initialCamPos;
displacement = prevDisplacement + Damper.Damp(displacement - prevDisplacement, dampTime, deltaTime);
}
}
state.PositionCorrection += displacement;
cameraPos = state.CorrectedPosition;
// Adjust the damping bypass to account for the displacement
if (state.HasLookAt && VirtualCamera.PreviousStateIsValid)
{
var dir0 = extra.previousCameraPosition - state.ReferenceLookAt;
var dir1 = cameraPos - state.ReferenceLookAt;
if (dir0.sqrMagnitude > Epsilon && dir1.sqrMagnitude > Epsilon)
state.PositionDampingBypass = UnityVectorExtensions.SafeFromToRotation(
dir0, dir1, state.ReferenceUp).eulerAngles;
}
extra.previousDisplacement = displacement;
extra.previousCameraOffset = cameraPos - lookAt;
extra.previousCameraPosition = cameraPos;
extra.previousDampTime = dampTime;
}
}
// Rate the shot after the aim was set
if (stage == CinemachineCore.Stage.Aim)
{
var extra = GetExtraState(vcam);
extra.targetObscured = IsTargetOffscreen(state) || CheckForTargetObstructions(state);
// GML these values are an initial arbitrary attempt at rating quality
if (extra.targetObscured)
state.ShotQuality *= 0.2f;
if (!extra.previousDisplacement.AlmostZero())
state.ShotQuality *= 0.8f;
float nearnessBoost = 0;
const float kMaxNearBoost = 0.2f;
if (m_OptimalTargetDistance > 0 && state.HasLookAt)
{
float distance = Vector3.Magnitude(state.ReferenceLookAt - state.FinalPosition);
if (distance <= m_OptimalTargetDistance)
{
float threshold = m_OptimalTargetDistance / 2;
if (distance >= threshold)
nearnessBoost = kMaxNearBoost * (distance - threshold)
/ (m_OptimalTargetDistance - threshold);
}
else
{
distance -= m_OptimalTargetDistance;
float threshold = m_OptimalTargetDistance * 3;
if (distance < threshold)
nearnessBoost = kMaxNearBoost * (1f - (distance / threshold));
}
state.ShotQuality *= (1f + nearnessBoost);
}
}
}
Vector3 PreserveLineOfSight(ref CameraState state, ref VcamExtraState extra)
{
Vector3 displacement = Vector3.zero;
if (state.HasLookAt && m_CollideAgainst != 0
&& m_CollideAgainst != m_TransparentLayers)
{
Vector3 cameraPos = state.CorrectedPosition;
Vector3 lookAtPos = state.ReferenceLookAt;
RaycastHit hitInfo = new RaycastHit();
displacement = PullCameraInFrontOfNearestObstacle(
cameraPos, lookAtPos, m_CollideAgainst & ~m_TransparentLayers, ref hitInfo);
Vector3 pos = cameraPos + displacement;
if (hitInfo.collider != null)
{
extra.AddPointToDebugPath(pos);
if (m_Strategy != ResolutionStrategy.PullCameraForward)
{
Vector3 targetToCamera = cameraPos - lookAtPos;
pos = PushCameraBack(
pos, targetToCamera, hitInfo, lookAtPos,
new Plane(state.ReferenceUp, cameraPos),
targetToCamera.magnitude, m_MaximumEffort, ref extra);
}
}
displacement = pos - cameraPos;
}
return displacement;
}
Vector3 PullCameraInFrontOfNearestObstacle(
Vector3 cameraPos, Vector3 lookAtPos, int layerMask, ref RaycastHit hitInfo)
{
Vector3 displacement = Vector3.zero;
Vector3 dir = cameraPos - lookAtPos;
float targetDistance = dir.magnitude;
if (targetDistance > Epsilon)
{
dir /= targetDistance;
float minDistanceFromTarget = Mathf.Max(m_MinimumDistanceFromTarget, Epsilon);
if (targetDistance < minDistanceFromTarget + Epsilon)
displacement = dir * (minDistanceFromTarget - targetDistance);
else
{
float rayLength = targetDistance - minDistanceFromTarget;
if (m_DistanceLimit > Epsilon)
rayLength = Mathf.Min(m_DistanceLimit, rayLength);
// Make a ray that looks towards the camera, to get the obstacle closest to target
Ray ray = new Ray(cameraPos - rayLength * dir, dir);
rayLength += k_PrecisionSlush;
if (rayLength > Epsilon)
{
if (m_Strategy == ResolutionStrategy.PullCameraForward && m_CameraRadius >= Epsilon)
{
if (RuntimeUtility.SphereCastIgnoreTag(lookAtPos + dir * m_CameraRadius,
m_CameraRadius, dir, out hitInfo,
rayLength - m_CameraRadius, layerMask, m_IgnoreTag))
{
var desiredResult = hitInfo.point + hitInfo.normal * m_CameraRadius;
displacement = desiredResult - cameraPos;
}
}
else
{
if (RuntimeUtility.RaycastIgnoreTag(ray, out hitInfo, rayLength, layerMask, m_IgnoreTag))
{
// Pull camera forward in front of obstacle
float adjustment = Mathf.Max(0, hitInfo.distance - k_PrecisionSlush);
displacement = ray.GetPoint(adjustment) - cameraPos;
}
}
}
}
}
return displacement;
}
Vector3 PushCameraBack(
Vector3 currentPos, Vector3 pushDir, RaycastHit obstacle,
Vector3 lookAtPos, Plane startPlane, float targetDistance, int iterations,
ref VcamExtraState extra)
{
// Take a step along the wall.
Vector3 pos = currentPos;
Vector3 dir = Vector3.zero;
if (!GetWalkingDirection(pos, pushDir, obstacle, ref dir))
return pos;
Ray ray = new Ray(pos, dir);
float distance = GetPushBackDistance(ray, startPlane, targetDistance, lookAtPos);
if (distance <= Epsilon)
return pos;
// Check only as far as the obstacle bounds
float clampedDistance = ClampRayToBounds(ray, distance, obstacle.collider.bounds);
distance = Mathf.Min(distance, clampedDistance + k_PrecisionSlush);
if (RuntimeUtility.RaycastIgnoreTag(ray, out var hitInfo, distance,
m_CollideAgainst & ~m_TransparentLayers, m_IgnoreTag))
{
// We hit something. Stop there and take a step along that wall.
float adjustment = hitInfo.distance - k_PrecisionSlush;
pos = ray.GetPoint(adjustment);
extra.AddPointToDebugPath(pos);
if (iterations > 1)
pos = PushCameraBack(
pos, dir, hitInfo,
lookAtPos, startPlane,
targetDistance, iterations-1, ref extra);
return pos;
}
// Didn't hit anything. Can we push back all the way now?
pos = ray.GetPoint(distance);
// First check if we can still see the target. If not, abort
dir = pos - lookAtPos;
float d = dir.magnitude;
if (d < Epsilon || RuntimeUtility.RaycastIgnoreTag(
new Ray(lookAtPos, dir), out _, d - k_PrecisionSlush,
m_CollideAgainst & ~m_TransparentLayers, m_IgnoreTag))
return currentPos;
// All clear
ray = new Ray(pos, dir);
extra.AddPointToDebugPath(pos);
distance = GetPushBackDistance(ray, startPlane, targetDistance, lookAtPos);
if (distance > Epsilon)
{
if (!RuntimeUtility.RaycastIgnoreTag(ray, out hitInfo, distance,
m_CollideAgainst & ~m_TransparentLayers, m_IgnoreTag))
{
pos = ray.GetPoint(distance); // no obstacles - all good
extra.AddPointToDebugPath(pos);
}
else
{
// We hit something. Stop there and maybe take a step along that wall
float adjustment = hitInfo.distance - k_PrecisionSlush;
pos = ray.GetPoint(adjustment);
extra.AddPointToDebugPath(pos);
if (iterations > 1)
pos = PushCameraBack(
pos, dir, hitInfo, lookAtPos, startPlane,
targetDistance, iterations-1, ref extra);
}
}
return pos;
}
RaycastHit[] m_CornerBuffer = new RaycastHit[4];
bool GetWalkingDirection(
Vector3 pos, Vector3 pushDir, RaycastHit obstacle, ref Vector3 outDir)
{
Vector3 normal2 = obstacle.normal;
// Check for nearby obstacles. Are we in a corner?
float nearbyDistance = k_PrecisionSlush * 5;
int numFound = Physics.SphereCastNonAlloc(
pos, nearbyDistance, pushDir.normalized, m_CornerBuffer, 0,
m_CollideAgainst & ~m_TransparentLayers, QueryTriggerInteraction.Ignore);
if (numFound > 1)
{
// Calculate the second normal
for (int i = 0; i < numFound; ++i)
{
if (m_CornerBuffer[i].collider == null)
continue;
if (m_IgnoreTag.Length > 0 && m_CornerBuffer[i].collider.CompareTag(m_IgnoreTag))
continue;
Type type = m_CornerBuffer[i].collider.GetType();
if (type == typeof(BoxCollider)
|| type == typeof(SphereCollider)
|| type == typeof(CapsuleCollider))
{
Vector3 p = m_CornerBuffer[i].collider.ClosestPoint(pos);
Vector3 d = p - pos;
if (d.magnitude > Vector3.kEpsilon)
{
if (m_CornerBuffer[i].collider.Raycast(
new Ray(pos, d), out m_CornerBuffer[i], nearbyDistance))
{
if (!(m_CornerBuffer[i].normal - obstacle.normal).AlmostZero())
normal2 = m_CornerBuffer[i].normal;
break;
}
}
}
}
}
// Walk along the wall. If we're in a corner, walk their intersecting line
Vector3 dir = Vector3.Cross(obstacle.normal, normal2);
if (dir.AlmostZero())
dir = Vector3.ProjectOnPlane(pushDir, obstacle.normal);
else
{
float dot = Vector3.Dot(dir, pushDir);
if (Mathf.Abs(dot) < Epsilon)
return false;
if (dot < 0)
dir = -dir;
}
if (dir.AlmostZero())
return false;
outDir = dir.normalized;
return true;
}
const float k_AngleThreshold = 0.1f;
float GetPushBackDistance(Ray ray, Plane startPlane, float targetDistance, Vector3 lookAtPos)
{
float maxDistance = targetDistance - (ray.origin - lookAtPos).magnitude;
if (maxDistance < Epsilon)
return 0;
if (m_Strategy == ResolutionStrategy.PreserveCameraDistance)
return maxDistance;
if (!startPlane.Raycast(ray, out var distance))
distance = 0;
distance = Mathf.Min(maxDistance, distance);
if (distance < Epsilon)
return 0;
// If we are close to parallel to the plane, we have to take special action
float angle = Mathf.Abs(UnityVectorExtensions.Angle(startPlane.normal, ray.direction) - 90);
if (angle < k_AngleThreshold)
distance = Mathf.Lerp(0, distance, angle / k_AngleThreshold);
return distance;
}
static float ClampRayToBounds(Ray ray, float distance, Bounds bounds)
{
float d;
if (Vector3.Dot(ray.direction, Vector3.up) > 0)
{
if (new Plane(Vector3.down, bounds.max).Raycast(ray, out d) && d > Epsilon)
distance = Mathf.Min(distance, d);
}
else if (Vector3.Dot(ray.direction, Vector3.down) > 0)
{
if (new Plane(Vector3.up, bounds.min).Raycast(ray, out d) && d > Epsilon)
distance = Mathf.Min(distance, d);
}
if (Vector3.Dot(ray.direction, Vector3.right) > 0)
{
if (new Plane(Vector3.left, bounds.max).Raycast(ray, out d) && d > Epsilon)
distance = Mathf.Min(distance, d);
}
else if (Vector3.Dot(ray.direction, Vector3.left) > 0)
{
if (new Plane(Vector3.right, bounds.min).Raycast(ray, out d) && d > Epsilon)
distance = Mathf.Min(distance, d);
}
if (Vector3.Dot(ray.direction, Vector3.forward) > 0)
{
if (new Plane(Vector3.back, bounds.max).Raycast(ray, out d) && d > Epsilon)
distance = Mathf.Min(distance, d);
}
else if (Vector3.Dot(ray.direction, Vector3.back) > 0)
{
if (new Plane(Vector3.forward, bounds.min).Raycast(ray, out d) && d > Epsilon)
distance = Mathf.Min(distance, d);
}
return distance;
}
static Collider[] s_ColliderBuffer = new Collider[5];
Vector3 RespectCameraRadius(Vector3 cameraPos, Vector3 lookAtPos)
{
Vector3 result = Vector3.zero;
if (m_CameraRadius < Epsilon || m_CollideAgainst == 0)
return result;
Vector3 dir = cameraPos - lookAtPos;
float distance = dir.magnitude;
if (distance > Epsilon)
dir /= distance;
// Pull it out of any intersecting obstacles
RaycastHit hitInfo;
int numObstacles = Physics.OverlapSphereNonAlloc(
cameraPos, m_CameraRadius, s_ColliderBuffer,
m_CollideAgainst, QueryTriggerInteraction.Ignore);
if (numObstacles == 0 && m_TransparentLayers != 0
&& distance > m_MinimumDistanceFromTarget + Epsilon)
{
// Make sure the camera position isn't completely inside an obstacle.
// OverlapSphereNonAlloc won't catch those.
float d = distance - m_MinimumDistanceFromTarget;
Vector3 targetPos = lookAtPos + dir * m_MinimumDistanceFromTarget;
if (RuntimeUtility.RaycastIgnoreTag(new Ray(targetPos, dir),
out hitInfo, d, m_CollideAgainst, m_IgnoreTag))
{
// Only count it if there's an incoming collision but not an outgoing one
Collider c = hitInfo.collider;
if (!c.Raycast(new Ray(cameraPos, -dir), out hitInfo, d))
s_ColliderBuffer[numObstacles++] = c;
}
}
if (numObstacles > 0 && distance == 0 || distance > m_MinimumDistanceFromTarget)
{
var scratchCollider = RuntimeUtility.GetScratchCollider();
scratchCollider.radius = m_CameraRadius;
Vector3 newCamPos = cameraPos;
for (int i = 0; i < numObstacles; ++i)
{
Collider c = s_ColliderBuffer[i];
if (m_IgnoreTag.Length > 0 && c.CompareTag(m_IgnoreTag))
continue;
// If we have a lookAt target, move the camera to the nearest edge of obstacle
if (distance > m_MinimumDistanceFromTarget)
{
dir = newCamPos - lookAtPos;
float d = dir.magnitude;
if (d > Epsilon)
{
dir /= d;
var ray = new Ray(lookAtPos, dir);
if (c.Raycast(ray, out hitInfo, d + m_CameraRadius))
newCamPos = ray.GetPoint(hitInfo.distance) - (dir * k_PrecisionSlush);
}
}
if (Physics.ComputePenetration(
scratchCollider, newCamPos, Quaternion.identity,
c, c.transform.position, c.transform.rotation,
out var offsetDir, out var offsetDistance))
{
newCamPos += offsetDir * offsetDistance;
}
}
result = newCamPos - cameraPos;
}
// Respect the minimum distance from target - push camera back if we have to
if (distance > Epsilon && m_MinimumDistanceFromTarget > Epsilon)
{
float minDistance = Mathf.Max(m_MinimumDistanceFromTarget, m_CameraRadius) + k_PrecisionSlush;
Vector3 newOffset = cameraPos + result - lookAtPos;
if (newOffset.magnitude < minDistance)
result = lookAtPos - cameraPos + dir * minDistance;
}
return result;
}
bool CheckForTargetObstructions(CameraState state)
{
if (state.HasLookAt)
{
Vector3 lookAtPos = state.ReferenceLookAt;
Vector3 pos = state.CorrectedPosition;
Vector3 dir = lookAtPos - pos;
float distance = dir.magnitude;
if (distance < Mathf.Max(m_MinimumDistanceFromTarget, Epsilon))
return true;
Ray ray = new Ray(pos, dir.normalized);
if (RuntimeUtility.RaycastIgnoreTag(ray, out _,
distance - m_MinimumDistanceFromTarget,
m_CollideAgainst & ~m_TransparentLayers, m_IgnoreTag))
return true;
}
return false;
}
static bool IsTargetOffscreen(CameraState state)
{
if (state.HasLookAt)
{
Vector3 dir = state.ReferenceLookAt - state.CorrectedPosition;
dir = Quaternion.Inverse(state.CorrectedOrientation) * dir;
if (state.Lens.Orthographic)
{
if (Mathf.Abs(dir.y) > state.Lens.OrthographicSize)
return true;
if (Mathf.Abs(dir.x) > state.Lens.OrthographicSize * state.Lens.Aspect)
return true;
}
else
{
float fov = state.Lens.FieldOfView / 2;
float angle = UnityVectorExtensions.Angle(dir.ProjectOntoPlane(Vector3.right), Vector3.forward);
if (angle > fov)
return true;
fov = Mathf.Rad2Deg * Mathf.Atan(Mathf.Tan(fov * Mathf.Deg2Rad) * state.Lens.Aspect);
angle = UnityVectorExtensions.Angle(dir.ProjectOntoPlane(Vector3.up), Vector3.forward);
if (angle > fov)
return true;
}
}
return false;
}
}
#endif
}