using UnityEngine; using Cinemachine.Utility; using System; namespace Cinemachine { /// Abstract base class for a world-space path, /// suitable for a camera dolly track. public abstract class CinemachinePathBase : MonoBehaviour { /// Path samples per waypoint [Tooltip("Path samples per waypoint. This is used for calculating path distances.")] [Range(1, 100)] public int m_Resolution = 20; /// This class holds the settings that control how the path /// will appear in the editor scene view. The path is not visible in the game view [DocumentationSorting(DocumentationSortingAttribute.Level.UserRef)] [Serializable] public class Appearance { /// The color of the path itself when it is active in the editor [Tooltip("The color of the path itself when it is active in the editor")] public Color pathColor = Color.green; /// The color of the path itself when it is inactive in the editor [Tooltip("The color of the path itself when it is inactive in the editor")] public Color inactivePathColor = Color.gray; /// The width of the railroad-tracks that are drawn to represent the path [Tooltip("The width of the railroad-tracks that are drawn to represent the path")] [Range(0f, 10f)] public float width = 0.2f; } /// The settings that control how the path /// will appear in the editor scene view. [Tooltip("The settings that control how the path will appear in the editor scene view.")] public Appearance m_Appearance = new Appearance(); /// The minimum value for the path position public abstract float MinPos { get; } /// The maximum value for the path position public abstract float MaxPos { get; } /// True if the path ends are joined to form a continuous loop public abstract bool Looped { get; } /// Get a standardized path position, taking spins into account if looped /// Position along the path /// Standardized position, between MinPos and MaxPos public virtual float StandardizePos(float pos) { if (Looped && MaxPos > 0) { pos = pos % MaxPos; if (pos < 0) pos += MaxPos; return pos; } return Mathf.Clamp(pos, 0, MaxPos); } /// Get a worldspace position of a point along the path /// Postion along the path. Need not be standardized. /// World-space position of the point along at path at pos public virtual Vector3 EvaluatePosition(float pos) => transform.TransformPoint(EvaluateLocalPosition(pos)); /// Get the world-space tangent of the curve at a point along the path. /// Postion along the path. Need not be standardized. /// World-space direction of the path tangent. /// Length of the vector represents the tangent strength public virtual Vector3 EvaluateTangent(float pos) => transform.TransformDirection(EvaluateLocalTangent(pos)); /// Get the world-space orientation the curve at a point along the path. /// Postion along the path. Need not be standardized. /// World-space orientation of the path public virtual Quaternion EvaluateOrientation(float pos) => transform.rotation * EvaluateLocalOrientation(pos); /// Get a worldspace position of a point along the path /// Postion along the path. Need not be standardized. /// Local-space position of the point along at path at pos public abstract Vector3 EvaluateLocalPosition(float pos); /// Get the tangent of the curve at a point along the path. /// Postion along the path. Need not be standardized. /// Local-space direction of the path tangent. /// Length of the vector represents the tangent strength public abstract Vector3 EvaluateLocalTangent(float pos); /// Get the orientation the curve at a point along the path. /// Postion along the path. Need not be standardized. /// Local-space orientation of the path public abstract Quaternion EvaluateLocalOrientation(float pos); /// Find the closest point on the path to a given worldspace target point. /// Performance could be improved by checking the bounding polygon of each segment, /// and only entering the best segment(s) /// Worldspace target that we want to approach /// In what segment of the path to start the search. /// A Segment is a section of path between 2 waypoints. /// How many segments on either side of the startSegment /// to search. -1 means no limit, i.e. search the entire path /// We search a segment by dividing it into this many /// straight pieces. The higher the number, the more accurate the result, but performance /// is proportionally slower for higher numbers /// The position along the path that is closest to the target point. /// The value is in Path Units, not Distance units. public virtual float FindClosestPoint( Vector3 p, int startSegment, int searchRadius, int stepsPerSegment) { float start = MinPos; float end = MaxPos; if (searchRadius >= 0) { if (Looped) { var r = Mathf.Min(searchRadius, Mathf.FloorToInt((end - start) / 2f)); start = startSegment - r; end = startSegment + r + 1; } else { start = Mathf.Max(startSegment - searchRadius, MinPos); end = Mathf.Min(startSegment + searchRadius + 1, MaxPos); } } stepsPerSegment = Mathf.RoundToInt(Mathf.Clamp(stepsPerSegment, 1f, 100f)); float stepSize = 1f / stepsPerSegment; float bestPos = startSegment; float bestDistance = float.MaxValue; int iterations = (stepsPerSegment == 1) ? 1 : 3; for (int i = 0; i < iterations; ++i) { Vector3 v0 = EvaluatePosition(start); for (float f = start + stepSize; f <= end; f += stepSize) { Vector3 v = EvaluatePosition(f); float t = p.ClosestPointOnSegment(v0, v); float d = Vector3.SqrMagnitude(p - Vector3.Lerp(v0, v, t)); if (d < bestDistance) { bestDistance = d; bestPos = f - (1 - t) * stepSize; } v0 = v; } start = bestPos - stepSize; end = bestPos + stepSize; stepSize /= stepsPerSegment; } return bestPos; } /// How to interpret the Path Position public enum PositionUnits { /// Use PathPosition units, where 0 is first waypoint, 1 is second waypoint, etc PathUnits, /// Use Distance Along Path. Path will be sampled according to its Resolution /// setting, and a distance lookup table will be cached internally Distance, /// Normalized units, where 0 is the start of the path, and 1 is the end. /// Path will be sampled according to its Resolution /// setting, and a distance lookup table will be cached internally Normalized } /// Get the minimum value, for the given unit type /// The unit type /// The minimum allowable value for this path public float MinUnit(PositionUnits units) { if (units == PositionUnits.Normalized) return 0; return units == PositionUnits.Distance ? 0 : MinPos; } /// Get the maximum value, for the given unit type /// The unit type /// The maximum allowable value for this path public float MaxUnit(PositionUnits units) { if (units == PositionUnits.Normalized) return 1; return units == PositionUnits.Distance ? PathLength : MaxPos; } /// Standardize the unit, so that it lies between MinUmit and MaxUnit /// The value to be standardized /// The unit type /// The standardized value of pos, between MinUnit and MaxUnit public virtual float StandardizeUnit(float pos, PositionUnits units) { if (units == PositionUnits.PathUnits) return StandardizePos(pos); if (units == PositionUnits.Distance) return StandardizePathDistance(pos); float len = PathLength; if (len < UnityVectorExtensions.Epsilon) return 0; return StandardizePathDistance(pos * len) / len; } /// Get a worldspace position of a point along the path /// Postion along the path. Need not be normalized. /// The unit to use when interpreting the value of pos. /// World-space position of the point along at path at pos public Vector3 EvaluatePositionAtUnit(float pos, PositionUnits units) { return EvaluatePosition(ToNativePathUnits(pos, units)); } /// Get the tangent of the curve at a point along the path. /// Postion along the path. Need not be normalized. /// The unit to use when interpreting the value of pos. /// World-space direction of the path tangent. /// Length of the vector represents the tangent strength public Vector3 EvaluateTangentAtUnit(float pos, PositionUnits units) { return EvaluateTangent(ToNativePathUnits(pos, units)); } /// Get the orientation the curve at a point along the path. /// Postion along the path. Need not be normalized. /// The unit to use when interpreting the value of pos. /// World-space orientation of the path public Quaternion EvaluateOrientationAtUnit(float pos, PositionUnits units) { return EvaluateOrientation(ToNativePathUnits(pos, units)); } /// When calculating the distance cache, sample the path this many /// times between points public abstract int DistanceCacheSampleStepsPerSegment { get; } /// Call this if the path changes in such a way as to affect distances /// or other cached path elements public virtual void InvalidateDistanceCache() { m_DistanceToPos = null; m_PosToDistance = null; m_CachedSampleSteps = 0; m_PathLength = 0; } /// See whether the distance cache is valid. If it's not valid, /// then any call to GetPathLength() or ToNativePathUnits() will /// trigger a potentially costly regeneration of the path distance cache /// Whether the cache is valid public bool DistanceCacheIsValid() { return (MaxPos == MinPos) || (m_DistanceToPos != null && m_PosToDistance != null && m_CachedSampleSteps == DistanceCacheSampleStepsPerSegment && m_CachedSampleSteps > 0); } /// Get the length of the path in distance units. /// If the distance cache is not valid, then calling this will /// trigger a potentially costly regeneration of the path distance cache /// The length of the path in distance units, when sampled at this rate public float PathLength { get { if (DistanceCacheSampleStepsPerSegment < 1) return 0; if (!DistanceCacheIsValid()) ResamplePath(DistanceCacheSampleStepsPerSegment); return m_PathLength; } } /// Standardize a distance along the path based on the path length. /// If the distance cache is not valid, then calling this will /// trigger a potentially costly regeneration of the path distance cache /// The distance to standardize /// The standardized distance, ranging from 0 to path length public float StandardizePathDistance(float distance) { float length = PathLength; if (length < Vector3.kEpsilon) return 0; if (Looped) { distance = distance % length; if (distance < 0) distance += length; } return Mathf.Clamp(distance, 0, length); } /// Get the path position to native path units. /// If the distance cache is not valid, then calling this will /// trigger a potentially costly regeneration of the path distance cache /// The value to convert from /// The units in which pos is expressed /// The path position, in native units public float ToNativePathUnits(float pos, PositionUnits units) { if (units == PositionUnits.PathUnits) return pos; if (DistanceCacheSampleStepsPerSegment < 1 || PathLength < UnityVectorExtensions.Epsilon) return MinPos; if (units == PositionUnits.Normalized) pos *= PathLength; pos = StandardizePathDistance(pos); float d = pos / m_cachedDistanceStepSize; int i = Mathf.FloorToInt(d); if (i >= m_DistanceToPos.Length-1) return MaxPos; float t = d - (float)i; return MinPos + Mathf.Lerp(m_DistanceToPos[i], m_DistanceToPos[i+1], t); } /// Convert a path position from native path units to the desired units. /// If the distance cache is not valid, then calling this will /// trigger a potentially costly regeneration of the path distance cache /// The value to convert from, in native units /// The units to convert to /// Tha path position, in the requested units public float FromPathNativeUnits(float pos, PositionUnits units) { if (units == PositionUnits.PathUnits) return pos; float length = PathLength; if (DistanceCacheSampleStepsPerSegment < 1 || length < UnityVectorExtensions.Epsilon) return 0; pos = StandardizePos(pos); float d = pos / m_cachedPosStepSize; int i = Mathf.FloorToInt(d); if (i >= m_PosToDistance.Length-1) pos = m_PathLength; else { float t = d - (float)i; pos = Mathf.Lerp(m_PosToDistance[i], m_PosToDistance[i+1], t); } if (units == PositionUnits.Normalized) pos /= length; return pos; } private float[] m_DistanceToPos; private float[] m_PosToDistance; private int m_CachedSampleSteps; private float m_PathLength; private float m_cachedPosStepSize; private float m_cachedDistanceStepSize; private void ResamplePath(int stepsPerSegment) { InvalidateDistanceCache(); float minPos = MinPos; float maxPos = MaxPos; float stepSize = 1f / Mathf.Max(1, stepsPerSegment); // Sample the positions int numKeys = Mathf.RoundToInt((maxPos - minPos) / stepSize) + 1; m_PosToDistance = new float[numKeys]; m_CachedSampleSteps = stepsPerSegment; m_cachedPosStepSize = stepSize; Vector3 p0 = EvaluatePosition(0); m_PosToDistance[0] = 0; float pos = minPos; for (int i = 1; i < numKeys; ++i) { pos += stepSize; Vector3 p = EvaluatePosition(pos); float d = Vector3.Distance(p0, p); m_PathLength += d; p0 = p; m_PosToDistance[i] = m_PathLength; } // Resample the distances m_DistanceToPos = new float[numKeys]; m_DistanceToPos[0] = 0; if (numKeys > 1) { stepSize = m_PathLength / (numKeys - 1); m_cachedDistanceStepSize = stepSize; float distance = 0; int posIndex = 1; for (int i = 1; i < numKeys; ++i) { distance += stepSize; float d = m_PosToDistance[posIndex]; while (d < distance && posIndex < numKeys-1) d = m_PosToDistance[++posIndex]; float d0 = m_PosToDistance[posIndex-1]; float delta = d - d0; float t = (distance - d0) / delta; m_DistanceToPos[i] = m_cachedPosStepSize * (t + posIndex - 1); } } } } }