#if CINEMACHINE_PHYSICS using UnityEngine; using System.Collections.Generic; using System; namespace Unity.Cinemachine { /// /// An add-on module for CinemachineCamera that post-processes /// the final position of the camera. Based on the supplied settings, /// the Deoccluder will attempt to preserve the line of sight /// with the LookAt target of the camera by moving /// away from objects that will obstruct the view. /// /// Additionally, the Deoccluder can be used to assess the shot quality and /// report this as a field in the camera State. /// [AddComponentMenu("Cinemachine/Procedural/Extensions/Cinemachine Deoccluder")] [SaveDuringPlay] [ExecuteAlways] [DisallowMultipleComponent] [RequiredTarget(RequiredTargetAttribute.RequiredTargets.Tracking)] [HelpURL(Documentation.BaseURL + "manual/CinemachineDeoccluder.html")] public class CinemachineDeoccluder : CinemachineExtension, IShotQualityEvaluator { /// Objects on these layers will be detected. [Tooltip("Objects on these layers will be detected")] public LayerMask 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 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 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 MinimumDistanceFromTarget = 0.3f; /// Settings for deoccluding the camera when obstacles are present [Serializable] public struct ObstacleAvoidance { /// /// When enabled, will attempt to resolve situations where the line of sight to the /// target is blocked by an obstacle /// [Tooltip("When enabled, will attempt to resolve situations where the line of sight " + "to the target is blocked by an obstacle")] public bool Enabled; /// /// 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.")] public float 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 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 CameraRadius; /// Settings for resolving towards Follow target instead of LookAt. [Serializable] public struct FollowTargetSettings { /// Use the Follow target when resolving occlusions, instead of the LookAt target. [Tooltip("Use the Follow target when resolving occlusions, instead of the LookAt target.")] public bool Enabled; [Tooltip("Vertical offset from the Follow target's root, in target local space")] public float YOffset; } /// Use the Follow target when resolving occlusions, instead of the LookAt target. [EnabledProperty] public FollowTargetSettings UseFollowTarget; /// The way in which the Deoccluder 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 Deoccluder will attempt to preserve sight of the target. [Tooltip("The way in which the Deoccluder will attempt to preserve sight of the target.")] public ResolutionStrategy Strategy; /// /// 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 MaximumEffort; /// /// 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 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.")] public float 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 DampingWhenOccluded; internal static ObstacleAvoidance Default => new () { Enabled = true, DistanceLimit = 0, MinimumOcclusionTime = 0, CameraRadius = 0.4f, Strategy = ResolutionStrategy.PullCameraForward, MaximumEffort = 4, SmoothingTime = 0, Damping = 0.4f, DampingWhenOccluded = 0.2f }; } /// Settings for deoccluding the camera when obstacles are present [FoldoutWithEnabledButton] public ObstacleAvoidance AvoidObstacles; /// Settings for shot quality evaluation [Serializable] public struct QualityEvaluation { /// If enabled, will evaluate shot quality based on target distance and occlusion [Tooltip("If enabled, will evaluate shot quality based on target distance and occlusion")] public bool Enabled; /// If greater than zero, maximum quality boost will occur when target is this far from the camera [Tooltip("If greater than zero, maximum quality boost will occur when target is this far from the camera")] public float OptimalDistance; /// Shots with targets closer to the camera than this will not get a quality boost [Tooltip("Shots with targets closer to the camera than this will not get a quality boost")] public float NearLimit; /// Shots with targets farther from the camera than this will not get a quality boost [Tooltip("Shots with targets farther from the camera than this will not get a quality boost")] public float FarLimit; /// High quality shots will be boosted by this fraction of their normal quality [Tooltip("High quality shots will be boosted by this fraction of their normal quality")] public float MaxQualityBoost; internal static QualityEvaluation Default => new () { NearLimit = 5, FarLimit = 30, OptimalDistance = 10, MaxQualityBoost = 0.2f }; } /// If enabled, will evaluate shot quality based on target distance and occlusion [FoldoutWithEnabledButton] public QualityEvaluation ShotQualityEvaluation = QualityEvaluation.Default; List m_extraStateCache; /// 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 deoccluder, in the event that the camera has children /// True if something is blocking the view public bool IsTargetObscured(CinemachineVirtualCameraBase 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 deoccluder, 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 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 deoccluder, 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(CinemachineVirtualCameraBase vcam) { return GetExtraState(vcam).PreviousDisplacement.magnitude; } void OnValidate() { AvoidObstacles.DistanceLimit = Mathf.Max(0, AvoidObstacles.DistanceLimit); AvoidObstacles.MinimumOcclusionTime = Mathf.Max(0, AvoidObstacles.MinimumOcclusionTime); AvoidObstacles.CameraRadius = Mathf.Max(0, AvoidObstacles.CameraRadius); MinimumDistanceFromTarget = Mathf.Max(0.01f, MinimumDistanceFromTarget); ShotQualityEvaluation.NearLimit = Mathf.Max(0.1f, ShotQualityEvaluation.NearLimit); ShotQualityEvaluation.FarLimit = Mathf.Max(ShotQualityEvaluation.NearLimit, ShotQualityEvaluation.FarLimit); ShotQualityEvaluation.OptimalDistance = Mathf.Clamp( ShotQualityEvaluation.OptimalDistance, ShotQualityEvaluation.NearLimit, ShotQualityEvaluation.FarLimit); } private void Reset() { CollideAgainst = 1; IgnoreTag = string.Empty; TransparentLayers = 0; MinimumDistanceFromTarget = 0.3f; AvoidObstacles = ObstacleAvoidance.Default; ShotQualityEvaluation = QualityEvaluation.Default; } /// /// 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 : VcamExtraStateBase { public Vector3 PreviousDisplacement; public bool TargetObscured; public float OcclusionStartTime; public List DebugResolutionPath; public List OccludingObjects; public Vector3 PreviousCameraOffset; public Vector3 PreviousCameraPosition; public float PreviousDampTime; public void AddPointToDebugPath(Vector3 p, Collider c) { #if UNITY_EDITOR DebugResolutionPath ??= new (); DebugResolutionPath.Add(p); OccludingObjects ??= new (); OccludingObjects.Add(c); #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; } }; /// Debug API for discovering which objects are occluding the camera, /// and the path taken by the camera to ist deoccluded position. Note that /// this information is only collected while running in the editor. In the build, the /// return values will always be empty. This is for performance reasons. /// A container to hold lists of points representing the camera path. /// There will be one path per CinemachineCamera influenced by this deoccluder. /// This parameter may be null. /// A container to hold lists of Colliders representing the obstacles encountered. /// There will be one list per CinemachineCamera influenced by this deoccluder. /// This parameter may be null. public void DebugCollisionPaths(List> paths, List> obstacles) { paths?.Clear(); obstacles?.Clear(); m_extraStateCache ??= new(); GetAllExtraStates(m_extraStateCache); for (int i = 0; i < m_extraStateCache.Count; ++i) { var e = m_extraStateCache[i]; if (e.DebugResolutionPath != null && e.DebugResolutionPath.Count > 0) { paths?.Add(e.DebugResolutionPath); obstacles?.Add(e.OccludingObjects); } } } /// /// Report maximum damping time needed for this component. /// /// Highest damping setting in this component public override float GetMaxDampTime() { return AvoidObstacles.Enabled ? Mathf.Max(AvoidObstacles.Damping, Mathf.Max(AvoidObstacles.DampingWhenOccluded, AvoidObstacles.SmoothingTime)) : 0; } /// public override void OnTargetObjectWarped( CinemachineVirtualCameraBase vcam, Transform target, Vector3 positionDelta) { var extra = GetExtraState(vcam); 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 (AvoidObstacles.Enabled) { var initialCamPos = state.GetCorrectedPosition(); var up = state.ReferenceUp; var hasLookAt = state.HasLookAt(); var lookAtPoint = hasLookAt ? state.ReferenceLookAt : state.GetCorrectedPosition(); var hasResolutionTarget = GetAvoidanceResolutionTargetPoint(vcam, ref state, out var resolutionTargetPoint); var lookAtScreenOffset = hasLookAt ? state.RawOrientation.GetCameraRotationToTarget( lookAtPoint - initialCamPos, up) : Vector2.zero; // Rotate the previous collision correction along with the camera var dampingBypass = state.RotationDampingBypass; extra.PreviousDisplacement = dampingBypass * extra.PreviousDisplacement; // Calculate the desired collision correction var displacement = hasResolutionTarget ? PreserveLineOfSight(ref state, ref extra, resolutionTargetPoint) : Vector3.zero; if (AvoidObstacles.MinimumOcclusionTime > Epsilon) { // If minimum occlusion time set, ignore new occlusions until they've lasted long enough var 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 < AvoidObstacles.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 (hasResolutionTarget && AvoidObstacles.SmoothingTime > Epsilon) { var pos = initialCamPos + displacement; var dir = pos - resolutionTargetPoint; var distance = dir.magnitude; if (distance > Epsilon) { dir /= distance; if (!displacement.AlmostZero()) extra.UpdateDistanceSmoothing(distance); distance = extra.ApplyDistanceSmoothing(distance, AvoidObstacles.SmoothingTime); displacement += (resolutionTargetPoint + dir * distance) - pos; } } if (displacement.AlmostZero()) extra.ResetDistanceSmoothing(AvoidObstacles.SmoothingTime); // Apply additional correction due to camera radius var newCamPos = initialCamPos + displacement; if (AvoidObstacles.Strategy != ObstacleAvoidance.ResolutionStrategy.PullCameraForward) displacement += RespectCameraRadius(newCamPos, resolutionTargetPoint); // Apply damping float dampTime = AvoidObstacles.DampingWhenOccluded; if (deltaTime >= 0 && vcam.PreviousStateIsValid && AvoidObstacles.DampingWhenOccluded + AvoidObstacles.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 ? AvoidObstacles.DampingWhenOccluded : AvoidObstacles.Damping; if (dispSqrMag < Epsilon) dampTime = extra.PreviousDampTime - Damper.Damp(extra.PreviousDampTime, dampTime, deltaTime); var prevDisplacement = resolutionTargetPoint + dampingBypass * extra.PreviousCameraOffset - initialCamPos; displacement = prevDisplacement + Damper.Damp(displacement - prevDisplacement, dampTime, deltaTime); } state.PositionCorrection += displacement; newCamPos = state.GetCorrectedPosition(); // Adjust the damping bypass to account for the displacement if (hasLookAt) { // Restore the lookAt offset if (displacement.sqrMagnitude > Epsilon) { var q = Quaternion.LookRotation(lookAtPoint - newCamPos, up); state.RawOrientation = q.ApplyCameraRotation(-lookAtScreenOffset, up); } if (vcam.PreviousStateIsValid) { var dir0 = extra.PreviousCameraPosition - lookAtPoint; var dir1 = newCamPos - lookAtPoint; if (dir0.sqrMagnitude > Epsilon && dir1.sqrMagnitude > Epsilon) state.RotationDampingBypass = UnityVectorExtensions.SafeFromToRotation(dir0, dir1, up); } } extra.PreviousDisplacement = displacement; extra.PreviousCameraOffset = newCamPos - resolutionTargetPoint; extra.PreviousCameraPosition = newCamPos; extra.PreviousDampTime = dampTime; } } // Rate the shot after the aim was set if (stage == CinemachineCore.Stage.Finalize && ShotQualityEvaluation.Enabled && state.HasLookAt()) { var extra = GetExtraState(vcam); extra.TargetObscured = state.IsTargetOffscreen() || IsTargetObscured(state); if (extra.TargetObscured) state.ShotQuality *= 0.2f; if (!extra.PreviousDisplacement.AlmostZero()) state.ShotQuality *= 0.8f; float nearnessBoost = 0; if (ShotQualityEvaluation.OptimalDistance > 0) { var distance = Vector3.Magnitude(state.ReferenceLookAt - state.GetFinalPosition()); if (distance <= ShotQualityEvaluation.OptimalDistance) { if (distance >= ShotQualityEvaluation.NearLimit) nearnessBoost = ShotQualityEvaluation.MaxQualityBoost * (distance - ShotQualityEvaluation.NearLimit) / (ShotQualityEvaluation.OptimalDistance - ShotQualityEvaluation.NearLimit); } else { distance -= ShotQualityEvaluation.OptimalDistance; if (distance < ShotQualityEvaluation.FarLimit) nearnessBoost = ShotQualityEvaluation.MaxQualityBoost * (1f - (distance / ShotQualityEvaluation.FarLimit)); } state.ShotQuality *= (1f + nearnessBoost); } } } bool GetAvoidanceResolutionTargetPoint( CinemachineVirtualCameraBase vcam, ref CameraState state, out Vector3 resolutuionTargetPoint) { var hasResolutionPoint = state.HasLookAt(); resolutuionTargetPoint = hasResolutionPoint ? state.ReferenceLookAt : state.GetCorrectedPosition(); if (AvoidObstacles.UseFollowTarget.Enabled) { var target = vcam.Follow; if (target != null) { hasResolutionPoint = true; resolutuionTargetPoint = TargetPositionCache.GetTargetPosition(target) + TargetPositionCache.GetTargetRotation(target) * Vector3.up * AvoidObstacles.UseFollowTarget.YOffset; } } return hasResolutionPoint; } Vector3 PreserveLineOfSight(ref CameraState state, ref VcamExtraState extra, Vector3 lookAtPoint) { if (CollideAgainst != 0 && CollideAgainst != TransparentLayers) { var cameraPos = state.GetCorrectedPosition(); var hitInfo = new RaycastHit(); var newPos = PullCameraInFrontOfNearestObstacle( cameraPos, lookAtPoint, CollideAgainst & ~TransparentLayers, ref hitInfo); if (hitInfo.collider != null) { extra.AddPointToDebugPath(newPos, hitInfo.collider); if (AvoidObstacles.Strategy != ObstacleAvoidance.ResolutionStrategy.PullCameraForward) { Vector3 targetToCamera = cameraPos - lookAtPoint; newPos = PushCameraBack( newPos, targetToCamera, hitInfo, lookAtPoint, new Plane(state.ReferenceUp, cameraPos), targetToCamera.magnitude, AvoidObstacles.MaximumEffort, ref extra); } } return newPos - cameraPos; } return Vector3.zero; } Vector3 PullCameraInFrontOfNearestObstacle( Vector3 cameraPos, Vector3 lookAtPos, int layerMask, ref RaycastHit hitInfo) { var newPos = cameraPos; var dir = cameraPos - lookAtPos; var targetDistance = dir.magnitude; if (targetDistance > Epsilon) { dir /= targetDistance; var minDistance = MinimumDistanceFromTarget + AvoidObstacles.CameraRadius + k_PrecisionSlush; if (targetDistance > minDistance) { // Make a ray that looks towards the camera, to get the obstacle closest to target var rayLength = Mathf.Max(targetDistance - minDistance - AvoidObstacles.CameraRadius, k_PrecisionSlush); if (AvoidObstacles.DistanceLimit > Epsilon) rayLength = Mathf.Min(AvoidObstacles.DistanceLimit, rayLength); if (RuntimeUtility.SphereCastIgnoreTag( new Ray(lookAtPos + dir * minDistance, dir), AvoidObstacles.CameraRadius, out hitInfo, rayLength, layerMask, IgnoreTag)) { newPos = hitInfo.point + hitInfo.normal * (AvoidObstacles.CameraRadius + k_PrecisionSlush); } // Respect the minimum distance from target - push camera back if we have to if ((lookAtPos - newPos).sqrMagnitude < minDistance * minDistance) newPos = lookAtPos + dir * minDistance; } } return newPos; } 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. var pos = currentPos; var 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.SphereCastIgnoreTag( ray, AvoidObstacles.CameraRadius, out var hitInfo, distance, CollideAgainst & ~TransparentLayers, IgnoreTag)) { // We hit something. Stop there and take a step along that wall. var adjustment = hitInfo.distance - k_PrecisionSlush; pos = ray.GetPoint(adjustment); extra.AddPointToDebugPath(pos, hitInfo.collider); 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; var d = dir.magnitude; if (d < Epsilon || RuntimeUtility.SphereCastIgnoreTag( new Ray(lookAtPos, dir), AvoidObstacles.CameraRadius, out _, d - k_PrecisionSlush, CollideAgainst & ~TransparentLayers, IgnoreTag)) return currentPos; // All clear ray = new Ray(pos, dir); extra.AddPointToDebugPath(pos, null); distance = GetPushBackDistance(ray, startPlane, targetDistance, lookAtPos); if (distance > Epsilon) { if (!RuntimeUtility.SphereCastIgnoreTag( ray, AvoidObstacles.CameraRadius, out hitInfo, distance, CollideAgainst & ~TransparentLayers, IgnoreTag)) { pos = ray.GetPoint(distance); // no obstacles - all good extra.AddPointToDebugPath(pos, null); } 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, hitInfo.collider); 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) { var normal2 = obstacle.normal; // Check for nearby obstacles. Are we in a corner? var nearbyDistance = k_PrecisionSlush * 5; int numFound = Physics.SphereCastNonAlloc( pos, nearbyDistance, pushDir.normalized, m_CornerBuffer, 0, CollideAgainst & ~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 (IgnoreTag.Length > 0 && m_CornerBuffer[i].collider.CompareTag(IgnoreTag)) continue; Type type = m_CornerBuffer[i].collider.GetType(); if (type == typeof(BoxCollider) || type == typeof(SphereCollider) || type == typeof(CapsuleCollider)) { var p = m_CornerBuffer[i].collider.ClosestPoint(pos); var 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 var dir = Vector3.Cross(obstacle.normal, normal2); if (dir.AlmostZero()) dir = Vector3.ProjectOnPlane(pushDir, obstacle.normal); else { var 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) { var maxDistance = targetDistance - (ray.origin - lookAtPos).magnitude; if (maxDistance < Epsilon) return 0; if (AvoidObstacles.Strategy == ObstacleAvoidance.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 var angle = Mathf.Abs(UnityVectorExtensions.Angle(startPlane.normal, ray.direction) - 90); 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) { var result = Vector3.zero; if (AvoidObstacles.CameraRadius < Epsilon || CollideAgainst == 0) return result; var dir = cameraPos - lookAtPos; var distance = dir.magnitude; if (distance > Epsilon) dir /= distance; // Pull it out of any intersecting obstacles RaycastHit hitInfo; int numObstacles = Physics.OverlapSphereNonAlloc( cameraPos, AvoidObstacles.CameraRadius, s_ColliderBuffer, CollideAgainst, QueryTriggerInteraction.Ignore); if (numObstacles == 0 && TransparentLayers != 0 && distance > MinimumDistanceFromTarget + Epsilon) { // Make sure the camera position isn't completely inside an obstacle. // OverlapSphereNonAlloc won't catch those. float d = distance - MinimumDistanceFromTarget; Vector3 targetPos = lookAtPos + dir * MinimumDistanceFromTarget; if (RuntimeUtility.SphereCastIgnoreTag( new Ray(targetPos, dir), AvoidObstacles.CameraRadius, out hitInfo, d, CollideAgainst, 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 > MinimumDistanceFromTarget) { var scratchCollider = RuntimeUtility.GetScratchCollider(); scratchCollider.radius = AvoidObstacles.CameraRadius; var newCamPos = cameraPos; for (int i = 0; i < numObstacles; ++i) { var c = s_ColliderBuffer[i]; if (IgnoreTag.Length > 0 && c.CompareTag(IgnoreTag)) continue; // If we have a lookAt target, move the camera to the nearest edge of obstacle if (distance > MinimumDistanceFromTarget) { dir = newCamPos - lookAtPos; var d = dir.magnitude; if (d > Epsilon) { dir /= d; var ray = new Ray(lookAtPos, dir); if (c.Raycast(ray, out hitInfo, d + AvoidObstacles.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 && MinimumDistanceFromTarget > Epsilon) { var minDistance = Mathf.Max(MinimumDistanceFromTarget, AvoidObstacles.CameraRadius) + k_PrecisionSlush; var newOffset = cameraPos + result - lookAtPos; if (newOffset.magnitude < minDistance) result = lookAtPos - cameraPos + dir * minDistance; } return result; } bool IsTargetObscured(CameraState state) { if (state.HasLookAt()) { var lookAtPos = state.ReferenceLookAt; var pos = state.GetCorrectedPosition(); var dir = lookAtPos - pos; var distance = dir.magnitude; if (distance < Mathf.Max(MinimumDistanceFromTarget, Epsilon)) return true; var ray = new Ray(pos, dir.normalized); if (RuntimeUtility.SphereCastIgnoreTag( ray, AvoidObstacles.CameraRadius, out _, distance - MinimumDistanceFromTarget, CollideAgainst & ~TransparentLayers, IgnoreTag)) return true; } return false; } } } #endif