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);
}
}
}
}
}