using UnityEngine; using System.Collections.Generic; using Cinemachine.Utility; using UnityEngine.Serialization; using System; namespace Cinemachine { /// /// 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(15, DocumentationSortingAttribute.Level.UserRef)] [ExecuteInEditMode] [AddComponentMenu("")] // Hide in menu [SaveDuringPlay] public class CinemachineCollider : CinemachineExtension { /// The Unity layer mask against which the collider will raycast. [Header("Obstacle Detection")] [Tooltip("The Unity layer mask against which the collider will raycast")] 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; /// 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 = 0f; /// /// 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; /// /// The gradualness of collision resolution. Higher numbers will move the /// camera more gradually away from obstructions. /// [Range(0, 10)] [Tooltip("The gradualness of collision resolution. Higher numbers will move the camera more gradually away from obstructions.")] [FormerlySerializedAs("m_Smoothing")] public float m_Damping = 0; /// 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 = 0; /// See wheter 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(CinemachineVirtualCameraBase vcam) { return GetExtraState(vcam).colliderDisplacement > 0; } private void OnValidate() { m_DistanceLimit = Mathf.Max(0, m_DistanceLimit); 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() { base.OnDestroy(); CleanupCameraCollider(); } /// This must be small but greater than 0 - reduces false results due to precision const float PrecisionSlush = 0.001f; // Per-vcam extra state info class VcamExtraState { public Vector3 m_previousDisplacement; public float colliderDisplacement; public bool targetObscured; public List debugResolutionPath; public void AddPointToDebugPath(Vector3 p) { #if UNITY_EDITOR if (debugResolutionPath == null) debugResolutionPath = new List(); debugResolutionPath.Add(p); #endif } }; /// 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) list.Add(v.debugResolutionPath); return list; } } /// Callcack to to the collision resolution and shot evaluation protected override void PostPipelineStageCallback( CinemachineVirtualCameraBase vcam, CinemachineCore.Stage stage, ref CameraState state, float deltaTime) { VcamExtraState extra = null; if (stage == CinemachineCore.Stage.Body) { extra = GetExtraState(vcam); extra.targetObscured = false; extra.colliderDisplacement = 0; extra.debugResolutionPath = null; } // Move the body before the Aim is calculated if (stage == CinemachineCore.Stage.Body) { if (m_AvoidObstacles) { Vector3 displacement = PreserveLignOfSight(ref state, ref extra); if (m_Damping > 0 && deltaTime >= 0) { Vector3 delta = displacement - extra.m_previousDisplacement; delta = Damper.Damp(delta, m_Damping, deltaTime); displacement = extra.m_previousDisplacement + delta; } extra.m_previousDisplacement = displacement; state.PositionCorrection += displacement; extra.colliderDisplacement += displacement.magnitude; } } // Rate the shot after the aim was set if (stage == CinemachineCore.Stage.Aim) { extra = GetExtraState(vcam); extra.targetObscured = CheckForTargetObstructions(state); // GML these values are an initial arbitrary attempt at rating quality if (extra.targetObscured) state.ShotQuality *= 0.2f; if (extra.colliderDisplacement > 0) 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); } } } private Vector3 PreserveLignOfSight(ref CameraState state, ref VcamExtraState extra) { Vector3 displacement = Vector3.zero; if (state.HasLookAt) { Vector3 cameraPos = state.CorrectedPosition; Vector3 lookAtPos = state.ReferenceLookAt; Vector3 pos = cameraPos; Vector3 dir = pos - lookAtPos; float targetDistance = dir.magnitude; float minDistanceFromTarget = Mathf.Max(m_MinimumDistanceFromTarget, Epsilon); if (targetDistance > minDistanceFromTarget) { dir.Normalize(); 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 most distant obstruction Ray ray = new Ray(pos - rayLength * dir, dir); rayLength += PrecisionSlush; if (rayLength > Epsilon) { RaycastHit hitInfo; if (RaycastIgnoreTag(ray, out hitInfo, rayLength)) { // Pull camera forward in front of obstacle float adjustment = Mathf.Max(0, hitInfo.distance - PrecisionSlush); pos = ray.GetPoint(adjustment); extra.AddPointToDebugPath(pos); if (m_Strategy != ResolutionStrategy.PullCameraForward) { pos = PushCameraBack( pos, dir, hitInfo, lookAtPos, new Plane(state.ReferenceUp, cameraPos), targetDistance, m_MaximumEffort, ref extra); } } } } if (m_CameraRadius > Epsilon) pos += RespectCameraRadius(pos, state.ReferenceLookAt); else if (mCameraColliderGameObject != null) CleanupCameraCollider(); displacement = pos - cameraPos; } return displacement; } private bool RaycastIgnoreTag(Ray ray, out RaycastHit hitInfo, float rayLength) { while (Physics.Raycast( ray, out hitInfo, rayLength, m_CollideAgainst.value, QueryTriggerInteraction.Ignore)) { if (m_IgnoreTag.Length == 0 || !hitInfo.collider.CompareTag(m_IgnoreTag)) return true; // Pull ray origin forward in front of tagged obstacle Ray inverseRay = new Ray(ray.GetPoint(rayLength), -ray.direction); if (!hitInfo.collider.Raycast(inverseRay, out hitInfo, rayLength)) break; // should never happen! rayLength = hitInfo.distance - PrecisionSlush; if (rayLength < Epsilon) break; ray.origin = inverseRay.GetPoint(rayLength); } return false; } private 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 + PrecisionSlush); RaycastHit hitInfo; if (RaycastIgnoreTag(ray, out hitInfo, distance)) { // We hit something. Stop there and take a step along that wall. float adjustment = hitInfo.distance - 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; RaycastHit hitInfo2; if (d < Epsilon || RaycastIgnoreTag(new Ray(lookAtPos, dir), out hitInfo2, d - PrecisionSlush)) return currentPos; // All clear ray = new Ray(pos, dir); extra.AddPointToDebugPath(pos); distance = GetPushBackDistance(ray, startPlane, targetDistance, lookAtPos); if (distance > Epsilon) { if (!RaycastIgnoreTag(ray, out hitInfo, distance)) { 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 - 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; } private RaycastHit[] m_CornerBuffer = new RaycastHit[4]; private 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 = PrecisionSlush * 5; int numFound = Physics.SphereCastNonAlloc( pos, nearbyDistance, pushDir.normalized, m_CornerBuffer, 0, m_CollideAgainst.value, QueryTriggerInteraction.Ignore); if (numFound > 1) { // Calculate the second normal for (int i = 0; i < numFound; ++i) { 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 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; float distance; if (!startPlane.Raycast(ray, out 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(Vector3.Angle(startPlane.normal, ray.direction) - 90); if (angle < AngleThreshold) distance = Mathf.Lerp(0, distance, angle / AngleThreshold); return distance; } 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; } private Collider[] mColliderBuffer = new Collider[5]; private SphereCollider mCameraCollider; private GameObject mCameraColliderGameObject; private Vector3 RespectCameraRadius(Vector3 cameraPos, Vector3 lookAtPos) { Vector3 result = Vector3.zero; int numObstacles = Physics.OverlapSphereNonAlloc( cameraPos, m_CameraRadius, mColliderBuffer, m_CollideAgainst, QueryTriggerInteraction.Ignore); if (numObstacles > 0) { if (mCameraColliderGameObject == null) { mCameraColliderGameObject = new GameObject("Cinemachine Collider Collider"); mCameraColliderGameObject.hideFlags = HideFlags.HideAndDontSave; mCameraColliderGameObject.transform.position = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue); mCameraColliderGameObject.SetActive(true); mCameraCollider = mCameraColliderGameObject.AddComponent(); } mCameraCollider.radius = m_CameraRadius; for (int i = 0; i < numObstacles; ++i) { Collider c = mColliderBuffer[i]; if (m_IgnoreTag.Length > 0 && c.CompareTag(m_IgnoreTag)) continue; Vector3 dir; float distance; if (Physics.ComputePenetration( mCameraCollider, cameraPos, Quaternion.identity, c, c.transform.position, c.transform.rotation, out dir, out distance)) { result += dir * distance; // naive, but maybe enough } } } return result; } private void CleanupCameraCollider() { if (mCameraColliderGameObject != null) DestroyImmediate(mCameraColliderGameObject); mCameraColliderGameObject = null; mCameraCollider = null; } private 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); RaycastHit hitInfo; if (RaycastIgnoreTag(ray, out hitInfo, distance - m_MinimumDistanceFromTarget)) return true; } return false; } } }