using UnityEngine; using System.Collections.Generic; using Unity.Mathematics; namespace Pathfinding.Util { /// Interpolates along a sequence of points public class PathInterpolator { /// /// Represents a single point on the polyline represented by the . /// The cursor is a lightweight structure which can be used to move backwards and forwards along a . /// /// If the changes (e.g. has its path swapped out), then this cursor is invalidated and cannot be used anymore. /// public struct Cursor { private PathInterpolator interpolator; private int version; private float currentDistance; private float distanceToSegmentStart; private float currentSegmentLength; /// /// Current segment. /// The start and end points of the segment are path[value] and path[value+1]. /// int segmentIndex { get; set; } public int segmentCount { get { AssertValid(); return interpolator.path.Count - 1; } } /// Last point in the path public Vector3 endPoint { get { AssertValid(); return interpolator.path[interpolator.path.Count-1]; } } /// /// Fraction of the way along the current segment. /// 0 is at the start of the segment, 1 is at the end of the segment. /// public float fractionAlongCurrentSegment { get { return currentSegmentLength > 0 ? (currentDistance - distanceToSegmentStart) / currentSegmentLength : 1f; } set { currentDistance = distanceToSegmentStart + Mathf.Clamp01(value) * currentSegmentLength; } } /// A cursor at the start of the polyline represented by the interpolator public static Cursor StartOfPath (PathInterpolator interpolator) { if (!interpolator.valid) throw new System.InvalidOperationException("PathInterpolator has no path set"); return new Cursor { interpolator = interpolator, version = interpolator.version, segmentIndex = 0, currentDistance = 0, distanceToSegmentStart = 0, currentSegmentLength = (interpolator.path[1] - interpolator.path[0]).magnitude, }; } /// /// True if this instance has a path set. /// See: SetPath /// public bool valid { get { return interpolator != null && interpolator.version == version; } } /// /// Tangent of the curve at the current position. /// Not necessarily normalized. /// public Vector3 tangent { get { AssertValid(); return interpolator.path[segmentIndex+1] - interpolator.path[segmentIndex]; } } /// Remaining distance until the end of the path public float remainingDistance { get { AssertValid(); return interpolator.totalDistance - distance; } set { AssertValid(); distance = interpolator.totalDistance - value; } } /// Traversed distance from the start of the path public float distance { get { return currentDistance; } set { AssertValid(); currentDistance = value; while (currentDistance < distanceToSegmentStart && segmentIndex > 0) PrevSegment(); while (currentDistance > distanceToSegmentStart + currentSegmentLength && segmentIndex < interpolator.path.Count - 2) NextSegment(); } } /// Current position public Vector3 position { get { AssertValid(); float t = currentSegmentLength > 0.0001f ? (currentDistance - distanceToSegmentStart) / currentSegmentLength : 0f; return Vector3.Lerp(interpolator.path[segmentIndex], interpolator.path[segmentIndex+1], t); } } /// Appends the remaining path between and to buffer public void GetRemainingPath (List buffer) { AssertValid(); buffer.Add(position); for (int i = segmentIndex+1; i < interpolator.path.Count; i++) { buffer.Add(interpolator.path[i]); } } void AssertValid () { if (!this.valid) throw new System.InvalidOperationException("The cursor has been invalidated because SetPath has been called on the interpolator. Please create a new cursor."); } /// /// The tangent(s) of the curve at the current position. /// Not necessarily normalized. /// /// Will output t1=, t2= if on a straight line segment. /// Will output the previous and next tangents for the adjacent line segments when on a corner. /// /// This is similar to but can output two tangents instead of one when on a corner. /// public void GetTangents (out Vector3 t1, out Vector3 t2) { AssertValid(); var nearStart = currentDistance <= distanceToSegmentStart + 0.001f; var nearEnd = currentDistance >= distanceToSegmentStart + currentSegmentLength - 0.001f; if (nearStart || nearEnd) { int s1, s2; if (nearStart) { s1 = segmentIndex > 0 ? segmentIndex - 1 : segmentIndex; s2 = segmentIndex; } else { s1 = segmentIndex; s2 = segmentIndex < interpolator.path.Count - 2 ? segmentIndex + 1 : segmentIndex; } t1 = interpolator.path[s1+1] - interpolator.path[s1]; t2 = interpolator.path[s2+1] - interpolator.path[s2]; } else { t1 = tangent; t2 = t1; } } /// /// A vector parallel to the local curvature. /// /// This will be zero on straight line segments, and in the same direction as the rotation axis when on a corner. /// /// Since this interpolator follows a polyline, the curvature is always either 0 or infinite. /// Therefore the magnitude of this vector has no meaning when non-zero. Only the direction matters. /// public Vector3 curvatureDirection { get { GetTangents(out var t1, out var t2); var up = Vector3.Cross(t1, t2); return up.sqrMagnitude <= 0.000001f ? Vector3.zero : up; } } /// /// Moves the cursor to the next geometric corner in the path. /// /// This is the next geometric corner. /// If the original path contained any zero-length segments, they will be skipped over. /// public void MoveToNextCorner () { AssertValid(); var path = interpolator.path; // Skip zero-length segments while (currentDistance >= this.distanceToSegmentStart + this.currentSegmentLength && segmentIndex < path.Count - 2) NextSegment(); // Skip parallel segements while (segmentIndex < path.Count - 2 && VectorMath.IsColinear(path[segmentIndex], path[segmentIndex+1], path[segmentIndex+2])) NextSegment(); // Move to end of current segment currentDistance = distanceToSegmentStart + currentSegmentLength; } /// /// Moves to the closest intersection of the line segment (origin + direction*range.x, origin + direction*range.y). /// The closest intersection as measured by the distance along the path is returned. /// /// If no intersection is found, false will be returned and the cursor remains unchanged. /// /// The intersection is calculated in XZ space. /// /// A point on the line /// The direction of the line. Need not be normalized. /// The range of the line segment along the line. The segment is (origin + direction*range.x, origin + direction*range.y). May be (-inf, +inf) to consider an infinite line. public bool MoveToClosestIntersectionWithLineSegment (Vector3 origin, Vector3 direction, Vector2 range) { AssertValid(); var closestIntersection = float.PositiveInfinity; var closestDist = float.PositiveInfinity; var d = 0f; for (int i = 0; i < interpolator.path.Count - 1; i++) { var p1 = interpolator.path[i]; var p2 = interpolator.path[i+1]; var segmentLength = (p2 - p1).magnitude; if ( VectorMath.LineLineIntersectionFactors(((float3)p1).xz, ((float3)(p2 - p1)).xz, ((float3)origin).xz, ((float3)direction).xz, out float t1, out float t2) && t1 >= 0.0f && t1 <= 1.0f && t2 >= range.x && t2 <= range.y ) { var intersection = d + t1 * segmentLength; var dist = Mathf.Abs(intersection - this.currentDistance); if (dist < closestDist) { closestIntersection = intersection; closestDist = dist; } } d += segmentLength; } if (closestDist != float.PositiveInfinity) { this.distance = closestIntersection; return true; } return false; } /// Move to the specified segment and move a fraction of the way to the next segment void MoveToSegment (int index, float fractionAlongSegment) { AssertValid(); if (index < 0 || index >= interpolator.path.Count - 1) throw new System.ArgumentOutOfRangeException("index"); while (segmentIndex > index) PrevSegment(); while (segmentIndex < index) NextSegment(); currentDistance = distanceToSegmentStart + Mathf.Clamp01(fractionAlongSegment) * currentSegmentLength; } /// Move as close as possible to the specified point public void MoveToClosestPoint (Vector3 point) { AssertValid(); float bestDist = float.PositiveInfinity; float bestFactor = 0f; int bestIndex = 0; var path = interpolator.path; for (int i = 0; i < path.Count-1; i++) { float factor = VectorMath.ClosestPointOnLineFactor(path[i], path[i+1], point); Vector3 closest = Vector3.Lerp(path[i], path[i+1], factor); float dist = (point - closest).sqrMagnitude; if (dist < bestDist) { bestDist = dist; bestFactor = factor; bestIndex = i; } } MoveToSegment(bestIndex, bestFactor); } public void MoveToLocallyClosestPoint (Vector3 point, bool allowForwards = true, bool allowBackwards = true) { AssertValid(); var path = interpolator.path; segmentIndex = Mathf.Min(segmentIndex, path.Count - 2); while (true) { int currentSegment = segmentIndex; var factor = VectorMath.ClosestPointOnLineFactor(path[currentSegment], path[currentSegment+1], point); if (factor > 1.0f && allowForwards && segmentIndex < path.Count - 2) { NextSegment(); allowBackwards = false; } else if (factor < 0.0f && allowBackwards && segmentIndex > 0) { PrevSegment(); allowForwards = false; } else { if (factor > 0.5f && segmentIndex < path.Count - 2) { NextSegment(); } break; } } // Check the distances to the two segments extending from the vertex path[segmentIndex] // and pick the position on those segments that is closest to the #point parameter. float factor1 = 0, d1 = float.PositiveInfinity; if (segmentIndex > 0) { var s1 = segmentIndex - 1; factor1 = VectorMath.ClosestPointOnLineFactor(path[s1], path[s1+1], point); d1 = (Vector3.Lerp(path[s1], path[s1+1], factor1) - point).sqrMagnitude; } var factor2 = VectorMath.ClosestPointOnLineFactor(path[segmentIndex], path[segmentIndex+1], point); var d2 = (Vector3.Lerp(path[segmentIndex], path[segmentIndex+1], factor2) - point).sqrMagnitude; if (d1 < d2) MoveToSegment(segmentIndex - 1, factor1); else MoveToSegment(segmentIndex, factor2); } public void MoveToCircleIntersection2D(Vector3 circleCenter3D, float radius, T transform) where T : IMovementPlane { AssertValid(); var path = interpolator.path; // Move forwards as long as we are getting closer to circleCenter3D while (segmentIndex < path.Count - 2 && VectorMath.ClosestPointOnLineFactor(path[segmentIndex], path[segmentIndex+1], circleCenter3D) > 1) { NextSegment(); } var circleCenter = transform.ToPlane(circleCenter3D); // Move forwards as long as the current segment endpoint is within the circle while (segmentIndex < path.Count - 2 && (transform.ToPlane(path[segmentIndex+1]) - circleCenter).sqrMagnitude <= radius*radius) { NextSegment(); } // Calculate the intersection with the circle. This involves some math. var factor = VectorMath.LineCircleIntersectionFactor(circleCenter, transform.ToPlane(path[segmentIndex]), transform.ToPlane(path[segmentIndex+1]), radius); // Move to the intersection point MoveToSegment(segmentIndex, factor); } /// /// Integrates exp(-|x|/smoothingDistance)/(2*smoothingDistance) from a to b. /// The integral from -inf to +inf is 1. /// static float IntegrateSmoothingKernel (float a, float b, float smoothingDistance) { if (smoothingDistance <= 0) return a <= 0 && b > 0 ? 1 : 0; var iA = a < 0 ? Mathf.Exp(a / smoothingDistance) : 2.0f - Mathf.Exp(-a / smoothingDistance); var iB = b < 0 ? Mathf.Exp(b / smoothingDistance) : 2.0f - Mathf.Exp(-b / smoothingDistance); return 0.5f * (iB - iA); } /// Integrates (x - a)*exp(-x/smoothingDistance)/(2*smoothingDistance) from a to b. static float IntegrateSmoothingKernel2 (float a, float b, float smoothingDistance) { if (smoothingDistance <= 0) return 0f; var iA = -Mathf.Exp(-a / smoothingDistance) * (smoothingDistance); var iB = -Mathf.Exp(-b / smoothingDistance) * (smoothingDistance + b - a); return 0.5f * (iB - iA); } static Vector3 IntegrateSmoothTangent (Vector3 p1, Vector3 p2, ref Vector3 tangent, ref float distance, float expectedRadius, float smoothingDistance) { var segment = p2 - p1; var segmentLength = segment.magnitude; if (segmentLength <= 0.00001f) return Vector3.zero; var nextTangent = segment * (1.0f / segmentLength); var deltaAngle = Vector3.Angle(tangent, nextTangent) * Mathf.Deg2Rad; var arcLength = expectedRadius*Mathf.Abs(deltaAngle); // We try to approximate // integrate kernel(x) * tangent(x); where * denotes convolution var integratedTangent = Vector3.zero; if (arcLength > float.Epsilon) { // Arc // integrate kernel(x) * (tangent + (nextTangent - tangent) * x/arcLength) dx var convolution = tangent * IntegrateSmoothingKernel(distance, distance + arcLength, smoothingDistance) + (nextTangent - tangent) * IntegrateSmoothingKernel2(distance, distance + arcLength, smoothingDistance) / arcLength; integratedTangent += convolution; distance += arcLength; } // Straight line // integrate kernel(x) * nextTangent dx = nextTangent * integrate kernel(x) dx integratedTangent += nextTangent * IntegrateSmoothingKernel(distance, distance + segmentLength, smoothingDistance); tangent = nextTangent; distance += segmentLength; return integratedTangent; } public Vector3 EstimateSmoothTangent (Vector3 normalizedTangent, float smoothingDistance, float expectedRadius, Vector3 beforePathStartContribution, bool forward = true, bool backward = true) { AssertValid(); if (expectedRadius <= float.Epsilon || smoothingDistance <= 0f) return normalizedTangent; var path = interpolator.path; var estimatedTangent = Vector3.zero; // Avoid zero-length segments at the start while (currentDistance >= this.distanceToSegmentStart + this.currentSegmentLength && segmentIndex < interpolator.path.Count - 2) NextSegment(); if (forward) { var d = 0f; var prev = position; var prevTangent = normalizedTangent; for (int i = segmentIndex + 1; i < path.Count; i++) { estimatedTangent += IntegrateSmoothTangent(prev, path[i], ref prevTangent, ref d, expectedRadius, smoothingDistance); prev = path[i]; } } if (backward) { var d = 0f; var prevTangent = -normalizedTangent; var prev = position; for (int i = segmentIndex; i >= 0; i--) { estimatedTangent -= IntegrateSmoothTangent(prev, path[i], ref prevTangent, ref d, expectedRadius, smoothingDistance); prev = path[i]; } estimatedTangent += beforePathStartContribution * IntegrateSmoothingKernel(float.NegativeInfinity, -currentDistance, smoothingDistance); } return estimatedTangent; } public Vector3 EstimateSmoothCurvature (Vector3 tangent, float smoothingDistance, float expectedRadius) { AssertValid(); if (expectedRadius <= float.Epsilon) return Vector3.zero; var path = interpolator.path; tangent = tangent.normalized; var curvature = Vector3.zero; // Avoid zero-length segments at the start while (currentDistance >= this.distanceToSegmentStart + this.currentSegmentLength && segmentIndex < interpolator.path.Count - 2) NextSegment(); var d = 0f; var prev = position; var currentTangent = tangent.normalized; for (int i = segmentIndex + 1; i < path.Count; i++) { var segment = path[i] - prev; var t = segment.normalized; var deltaAngle = Vector3.Angle(currentTangent, t) * Mathf.Deg2Rad; var c = Vector3.Cross(currentTangent, t).normalized; var angleDerivative = 1.0f / expectedRadius; var arcLength = expectedRadius*Mathf.Abs(deltaAngle); // d/dx(f * angle(x)) = f * angle'(x); where * denotes convolution var convolutionDerivative = angleDerivative * IntegrateSmoothingKernel(d, d + arcLength, smoothingDistance); curvature -= convolutionDerivative * c; currentTangent = t; d += arcLength; d += segment.magnitude; prev = path[i]; } // Do another integral in the backwards direction. // Ensures that if smoothingDistance is 0, the smoothing kernel will only be sampled once at x=0. d = float.Epsilon; currentTangent = -tangent.normalized; prev = position; for (int i = segmentIndex; i >= 0; i--) { var segment = path[i] - prev; if (segment == Vector3.zero) continue; var t = segment.normalized; var deltaAngle = Vector3.Angle(currentTangent, t) * Mathf.Deg2Rad; var c = Vector3.Cross(currentTangent, t).normalized; var angleDerivative = 1.0f / expectedRadius; var arcLength = expectedRadius*Mathf.Abs(deltaAngle); // d/dx(f * angle(x)) = f * angle'(x); where * denotes convolution var convolutionDerivative = angleDerivative * IntegrateSmoothingKernel(d, d + arcLength, smoothingDistance); curvature += convolutionDerivative * c; currentTangent = t; d += arcLength; d += segment.magnitude; prev = path[i]; } return curvature; } /// /// Moves the agent along the path, stopping to rotate on the spot when the path changes direction. /// /// Note: The cursor state does not include the rotation of the agent. So if an agent stops in the middle of a rotation, the final state of this struct will be as if the agent completed its rotation. /// If you want to preserve the rotation state as well, keep track of the output tangent, and pass it along to the next call to this function. /// /// The number of seconds to move forwards or backwards (if negative). /// Speed in meters/second. /// Turning speed in radians/second. /// The current forwards direction of the agent. May be set to the #tangent property if you have no other needs. /// If set to something other than #tangent, the agent will start by rotating to face the #tangent direction. /// This will be replaced with the forwards direction of the agent after moving. /// It will be smoothly interpolated as the agent rotates from one segment to the next. /// It is more precise than the #tangent property after this call, which does not take rotation into account. /// This value is not necessarily normalized. public void MoveWithTurningSpeed (float time, float speed, float turningSpeed, ref Vector3 tangent) { if (turningSpeed <= 0) throw new System.ArgumentException("turningSpeed must be greater than zero"); if (speed <= 0) throw new System.ArgumentException("speed must be greater than zero"); AssertValid(); var radiansToMeters = speed / turningSpeed; var remainingOffset = time * speed; int its = 0; // Make sure we don't start by rotating unnecessarily while (remainingOffset > 0 && currentDistance >= this.distanceToSegmentStart + this.currentSegmentLength && segmentIndex < interpolator.path.Count - 2) NextSegment(); while (remainingOffset < 0 && currentDistance <= this.distanceToSegmentStart && segmentIndex > 0) PrevSegment(); while (remainingOffset != 0f) { its++; if (its > 100) throw new System.Exception("Infinite Loop " + remainingOffset + " " + time); var desiredTangent = this.tangent; if (tangent != desiredTangent && currentSegmentLength > 0) { // Rotate to face the desired tangent var angle = Vector3.Angle(tangent, desiredTangent) * Mathf.Deg2Rad; var arcLength = angle * radiansToMeters; if (Mathf.Abs(remainingOffset) > arcLength) { remainingOffset -= arcLength * Mathf.Sign(remainingOffset); tangent = desiredTangent; } else { tangent = Vector3.Slerp(tangent, desiredTangent, Mathf.Abs(remainingOffset) / arcLength); return; } } if (remainingOffset > 0) { // Move forward along the segment var remainingOnCurrentSegment = this.currentSegmentLength - (this.currentDistance - this.distanceToSegmentStart); if (remainingOffset >= remainingOnCurrentSegment) { remainingOffset -= remainingOnCurrentSegment; if (segmentIndex + 1 >= this.interpolator.path.Count - 1) { MoveToSegment(segmentIndex, 1.0f); return; } else { MoveToSegment(segmentIndex + 1, 0.0f); } } else { this.currentDistance += remainingOffset; return; } } else { // Move backward along the segment var remainingOnCurrentSegment = this.currentDistance - this.distanceToSegmentStart; if (-remainingOffset > remainingOnCurrentSegment) { remainingOffset += remainingOnCurrentSegment; if (segmentIndex - 1 < 0) { MoveToSegment(segmentIndex, 0.0f); return; } else { MoveToSegment(segmentIndex - 1, 1.0f); } } else { this.currentDistance += remainingOffset; return; } } } } void PrevSegment () { segmentIndex--; currentSegmentLength = (interpolator.path[segmentIndex+1] - interpolator.path[segmentIndex]).magnitude; distanceToSegmentStart -= currentSegmentLength; } void NextSegment () { segmentIndex++; distanceToSegmentStart += currentSegmentLength; currentSegmentLength = (interpolator.path[segmentIndex+1] - interpolator.path[segmentIndex]).magnitude; } } List path; int version = 1; float totalDistance; /// /// True if this instance has a path set. /// See: SetPath /// public bool valid { get { return path != null; } } public Cursor start { get { return Cursor.StartOfPath(this); } } public Cursor AtDistanceFromStart (float distance) { var cursor = start; cursor.distance = distance; return cursor; } /// /// Set the path to interpolate along. /// This will invalidate all existing cursors. /// public void SetPath (List path) { this.version++; if (this.path == null) this.path = new List(); this.path.Clear(); if (path == null) { totalDistance = float.PositiveInfinity; return; } if (path.Count < 2) throw new System.ArgumentException("Path must have a length of at least 2"); var prev = path[0]; totalDistance = 0; this.path.Capacity = Mathf.Max(this.path.Capacity, path.Count); this.path.Add(path[0]); for (int i = 1; i < path.Count; i++) { var current = path[i]; // Avoid degenerate segments if (current != prev) { totalDistance += (current - prev).magnitude; this.path.Add(current); prev = current; } } if (this.path.Count < 2) this.path.Add(path[0]); if (float.IsNaN(totalDistance)) throw new System.ArgumentException("Path contains NaN values"); } } }