//
// (c) 2016 Digital Ruby, LLC
// http://www.digitalruby.com
// Code may not be redistributed in source form!
// Using this code in commercial games and apps is fine.
//
// if not using Unity, comment this out
#define UNITY
// if square root performance or accuracy is a problem, you may try one of the other defines here, and leave the other two commented out
#define USE_MATH_SQUARE_ROOT // most accurate
// #define USE_QUAKE_SQUARE_ROOT // pretty accuracte and faster than Math.Sqrt, but not perfect
// #define USE_FASTER_SQUARE_ROOT // pretty accurate and fastest, although less accurate than quake
// references for curves, splines and math that were invaluable in creating this code:
// http://stackoverflow.com/questions/9489736/catmull-rom-curve-with-no-cusps-and-no-self-intersections
// http://www.technologicalutopia.com/sourcecode/xnageometry/tableofcontents.htm
// http://blog.avangardo.com/2010/10/c-implementation-of-bezier-curvature-calculation/
// https://github.com/mono/sysdrawing-coregraphics/blob/master/System.Drawing.Drawing2D/GraphicsPath.cs
// https://github.com/retuxx/tinyspline
// http://www.codeproject.com/Articles/996281/NURBS-curve-made-easy
// http://catlikecoding.com/unity/tutorials/curves-and-splines/
// https://en.wikipedia.org/wiki/Fast_inverse_square_root
// http://blog.wouldbetheologian.com/2011/11/fast-approximate-sqrt-method-in-c.html
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
namespace DigitalRuby.ThunderAndLightning
{
#if UNITY
using UnityEngine;
#endif
///
/// Utility class to assist in generating curves, splines, etc.
///
public static class PathGenerator
{
#if !UNITY
public struct Vector3
{
public float x;
public float y;
public float z;
public Vector3(float x, float y, float z)
{
this.x = x;
this.y = y;
this.z = z;
}
}
public struct Vector4
{
public float x;
public float y;
public float z;
public float w;
public Vector4(float x, float y, float z, float w)
{
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
}
#endif
#if !USE_MATH_SQUARE_ROOT
[StructLayout(LayoutKind.Explicit)]
private struct FloatIntUnion
{
[FieldOffset(0)]
public float f;
[FieldOffset(0)]
public int tmp;
}
#endif
///
/// Minimum number of points to create a spline
///
public const int MinPointsForSpline = 4;
///
/// Whether paths are working in 2D or 3D space
///
public static bool Is2D;
///
/// Adjust how curves are calculated. This value should be positive. Generally leave as is unless you want some gnarly effects.
///
private const float curveMultiplier = 3.0f;
///
/// Adjusts how the spline curves. This value should be negative. Generally leave as is unless you want some gnarly effects.
///
private const float splineMultiplier1 = -3.0f;
///
/// Adjusts how the spline curves. This value should be positive. Generally leave as is unless you want some gnarly effects.
///
private const float splineMultiplier2 = 3.0f;
///
/// Adjusts how the spline curves. This value should be positive. Generally leave as is unless you want some gnarly effects.
///
private const float splineMultiplier3 = 2.0f;
///
/// In the event that the second distance of a spline is too small, force it to be this value
///
private const float splineDistanceClamp = 1.0f;
///
/// System.Math doesn't provide epsilon
///
private const float splineEpsilon = 0.0001f;
///
/// Optimized square root
///
/// Value
/// Square root of value
public static float SquareRoot(float x)
{
#if USE_MATH_SQUARE_ROOT
return (float)Math.Sqrt(x);
#elif USE_QUAKE_SQUARE_ROOT
// the famous Quake square root hack
FloatIntUnion u;
u.tmp = 0;
float xhalf = 0.5f * x;
u.f = x;
u.tmp = 0x5f375a86 - (u.tmp >> 1);
u.f = u.f * (1.5f - xhalf * u.f * u.f);
return u.f * x;
#else
// a slightly faster but less accurate method
FloatIntUnion u;
u.tmp = 0;
u.f = x;
u.tmp -= 1 << 23; /* Subtract 2^m. */
u.tmp >>= 1; /* Divide by 2. */
u.tmp += 1 << 29; /* Add ((b + 1) / 2) * 2^m. */
return u.f;
#endif
}
private static float Distance2D(ref Vector3 point1, ref Vector3 point2)
{
float deltaX = point2.x - point1.x;
float deltaY = point2.y - point1.y;
return SquareRoot((deltaX * deltaX) + (deltaY * deltaY));
}
private static float Distance3D(ref Vector3 point1, ref Vector3 point2)
{
float deltaX = point2.x - point1.x;
float deltaY = point2.y - point1.y;
float deltaZ = point2.z - point1.z;
return SquareRoot((deltaX * deltaX) + (deltaY * deltaY) + (deltaZ * deltaZ));
}
private static void GetCurvePoint2D(ref Vector3 start, ref Vector3 end, ref Vector3 ctr1, ref Vector3 ctr2, float t, out Vector3 point)
{
float tSquared = t * t;
float tCubed = tSquared * t;
float p1x = curveMultiplier * (ctr1.x - start.x);
float p1y = curveMultiplier * (ctr1.y - start.y);
float p2x = curveMultiplier * (ctr2.x - ctr1.x) - p1x;
float p2y = curveMultiplier * (ctr2.y - ctr1.y) - p1y;
float p3x = end.x - start.x - p1x - p2x;
float p3y = end.y - start.y - p1y - p2y;
float finalX = start.x + (p1x * t) + (p2x * tSquared) + (p3x * tCubed);
float finalY = start.y + (p1y * t) + (p2y * tSquared) + (p3y * tCubed);
point = new Vector3(finalX, finalY, 0.0f);
}
private static void GetCurvePoint3D(ref Vector3 start, ref Vector3 end, ref Vector3 ctr1, ref Vector3 ctr2, float t, out Vector3 point)
{
float tSquared = t * t;
float tCubed = tSquared * t;
float p1x = (ctr1.x - start.x) * curveMultiplier;
float p1y = (ctr1.y - start.y) * curveMultiplier;
float p1z = (ctr1.z - start.z) * curveMultiplier;
float p2x = ((ctr2.x - ctr1.x) * curveMultiplier) - p1x;
float p2y = ((ctr2.y - ctr1.y) * curveMultiplier) - p1y;
float p2z = ((ctr2.z - ctr1.z) * curveMultiplier) - p1z;
float p3x = end.x - start.x - p1x - p2x;
float p3y = end.y - start.y - p1y - p2y;
float p3z = end.z - start.z - p1z - p2z;
float finalX = start.x + (p1x * t) + (p2x * tSquared) + (p3x * tCubed);
float finalY = start.y + (p1y * t) + (p2y * tSquared) + (p3y * tCubed);
float finalZ = start.z + (p1z * t) + (p2z * tSquared) + (p3z * tCubed);
point = new Vector3(finalX, finalY, finalZ);
}
/*
private static Quaternion GetOrientation2D(Vector3[] pts, float t)
{
Vector3 tng = GetTangent(pts, t);
Vector3 nrm = GetNormal2D(pts, t);
return Quaternion.LookRotation(tng, nrm);
}
private static Quaternion GetOrientation3D(Vector3[] pts, float t, Vector3 up)
{
Vector3 tng = GetTangent(pts, t);
Vector3 nrm = GetNormal3D(pts, t, up);
return Quaternion.LookRotation(tng, nrm);
}
private static Vector3 GetNormal2D(Vector3[] pts, float t)
{
Vector3 tng = GetTangent(pts, t);
return new Vector3(-tng.y, tng.x, 0f);
}
private static Vector3 GetNormal3D(Vector3[] pts, float t, Vector3 up)
{
Vector3 tng = GetTangent(pts, t);
Vector3 binormal = Vector3.Cross(up, tng).normalized;
return Vector3.Cross(tng, binormal);
}
private static Vector3 GetTangent(Vector3[] pts, float t)
{
float omt = 1f - t;
float omt2 = omt * omt;
float t2 = t * t;
Vector3 tangent =
pts[0] * (-omt2) +
pts[1] * (3 * omt2 - 2 * omt) +
pts[2] * (-3 * t2 + 2 * t) +
pts[3] * (t2);
return tangent.normalized;
}
private static void CalculateSplinePoint3D(ref Vector3 p0, ref Vector3 p1, ref Vector3 p2, ref Vector3 p3, float t, out Vector3 p)
{
float omt = 1f - t;
float omt2 = omt * omt;
float t2 = t * t;
p = (p0 * omt2 * omt) +
(p1 * 3.0f * omt2 * t) +
(p2 * 3.0f * omt * t2) +
(p3 * t2 * t);
}
*/
private static void CalculateNonuniformCatmullRom(float p1, float p2, float p3, float p4, float distance1, float distance2, float distance3, out Vector4 point)
{
// http://stackoverflow.com/questions/9489736/catmull-rom-curve-with-no-cusps-and-no-self-intersections
// compute coefficients for a nonuniform Catmull-Rom spline
float t1 = ((p2 - p1) / distance1 - (p3 - p1) / (distance1 + distance2) + (p3 - p2) / distance2) * distance2;
float t2 = ((p3 - p2) / distance2 - (p4 - p2) / (distance2 + distance3) + (p4 - p3) / distance3) * distance2;
point = new Vector4
(
p2,
t1,
(splineMultiplier1 * p2) + (splineMultiplier2 * p3) - (t2 + (splineMultiplier3 * t1)),
(t1 + t2) + ((splineMultiplier3 * p2) - (splineMultiplier3 * p3))
);
}
private static float CalculatePolynomial(ref Vector4 point, float t)
{
// polynomial is w*t^3+z*t^2+y*t+x
float tSquared = t * t;
float tCubed = tSquared * t;
return (point.w * tCubed) + (point.z * tSquared) + (point.y * t) + point.x;
}
private static void ClampSplineDistances(ref float distance1, ref float distance2, ref float distance3)
{
// safety check for repeated points and 0 distance
if (distance2 < splineEpsilon)
{
distance2 = splineDistanceClamp;
}
if (distance1 < splineEpsilon)
{
distance1 = distance2;
}
if (distance3 < splineEpsilon)
{
distance3 = distance2;
}
}
private static void GetSplinePoint2D(ref Vector3 point1, ref Vector3 point2, ref Vector3 point3, ref Vector3 point4, float t, out Vector3 point)
{
float distance1 = Distance2D(ref point1, ref point2);
float distance2 = Distance2D(ref point2, ref point3);
float distance3 = Distance2D(ref point3, ref point4);
ClampSplineDistances(ref distance1, ref distance2, ref distance3);
// plot the spline on the x and y axis
Vector4 polynomialX;
CalculateNonuniformCatmullRom(point1.x, point2.x, point3.x, point4.x, distance1, distance2, distance3, out polynomialX);
Vector4 polynomialY;
CalculateNonuniformCatmullRom(point1.y, point2.y, point3.y, point4.y, distance1, distance2, distance3, out polynomialY);
point = new Vector3(CalculatePolynomial(ref polynomialX, t), CalculatePolynomial(ref polynomialY, t), 0.0f);
}
private static void GetSplinePoint3D(ref Vector3 point1, ref Vector3 point2, ref Vector3 point3, ref Vector3 point4, float t, out Vector3 point)
{
float distance1 = Distance3D(ref point1, ref point2);
float distance2 = Distance3D(ref point2, ref point3);
float distance3 = Distance3D(ref point3, ref point4);
ClampSplineDistances(ref distance1, ref distance2, ref distance3);
// plot the spline on the x, y and z axis
Vector4 polynomialX;
CalculateNonuniformCatmullRom(point1.x, point2.x, point3.x, point4.x, distance1, distance2, distance3, out polynomialX);
Vector4 polynomialY;
CalculateNonuniformCatmullRom(point1.y, point2.y, point3.y, point4.y, distance1, distance2, distance3, out polynomialY);
Vector4 polynomialZ;
CalculateNonuniformCatmullRom(point1.z, point2.z, point3.z, point4.z, distance1, distance2, distance3, out polynomialZ);
point = new Vector3(CalculatePolynomial(ref polynomialX, t), CalculatePolynomial(ref polynomialY, t), CalculatePolynomial(ref polynomialZ, t));
}
///
/// Create a quad/bezier curve that curves using two control points from start to end.
///
/// Receives the path points. Collection is not cleared.
/// Start point
/// End point
/// Control point 1
/// Control point 2
/// Number of segments. The higher this number, the more points are sampled.
/// Start t. This can be the return value of a previous call to maintain a smooth path.
/// Leftover t. This can be passed back in to startT to continue a path smoothly.
public static float CreateCurve(ICollection path, Vector3 start, Vector3 end, Vector3 ctr1, Vector3 ctr2, int numberOfSegments, float startT)
{
// make sure number of segments isn't too large or small
numberOfSegments = Math.Min(1024, Math.Max(numberOfSegments, MinPointsForSpline));
float t;
float tIncrement = 1.0f / (float)(numberOfSegments + 1);
Vector3 point;
if (Is2D)
{
for (t = startT; t <= 1.0f; t += tIncrement)
{
GetCurvePoint2D(ref start, ref end, ref ctr1, ref ctr2, t, out point);
path.Add(point);
}
}
else
{
for (t = startT; t <= 1.0f; t += tIncrement)
{
GetCurvePoint3D(ref start, ref end, ref ctr1, ref ctr2, t, out point);
path.Add(point);
}
}
return (t - 1.0f);
}
///
/// Creates a spline path that curves around points in a list. Each source segment will have the same number of spline points.
///
/// Receives the path points. Collection is not cleared.
/// Points for the spline to follow. Must contain at least 4 points.
/// Total number of line segments for the spline. The higher this number, the more points are sampled.
/// True to loop back to the start, false otherwise
/// True if success, false if points length is too small
public static bool CreateSpline(ICollection path, IList points, int numberOfSegments, bool closePath)
{
if (points.Count < MinPointsForSpline)
{
return false;
}
// make sure number of segments isn't too large or small
numberOfSegments = Math.Min(1024, Math.Max(numberOfSegments, 4));
int numberOfPoints = (closePath ? points.Count : points.Count - 1);
int previousPointIndex, currentPointIndex, nextPointIndex, nextNextPointIndex;
int closePathInt = (closePath ? 1 : 0);
float t;
float tIncrement = (1.0f / (float)numberOfSegments) * (float)numberOfPoints;
float start = 0.0f;
Vector3 point, previousPoint, currentPoint, nextPoint, nextNextPoint;
for (currentPointIndex = 0; currentPointIndex < numberOfPoints; currentPointIndex++)
{
// we will be using 4 points - the previous point, current point, next point and point after next
// the points can wrap around back to the start of the list
previousPointIndex = (currentPointIndex == 0 ? (numberOfPoints - closePathInt) : (currentPointIndex - 1));
nextPointIndex = currentPointIndex + 1;
nextNextPointIndex = currentPointIndex + 2;
if (closePath && nextPointIndex > numberOfPoints - 1)
{
nextPointIndex -= numberOfPoints;
}
if (nextNextPointIndex > numberOfPoints - 1)
{
nextNextPointIndex = (closePath ? nextNextPointIndex - numberOfPoints : numberOfPoints);
}
// assign the points - these will be passed by ref for performance
previousPoint = points[previousPointIndex];
currentPoint = points[currentPointIndex];
nextPoint = points[nextPointIndex];
nextNextPoint = points[nextNextPointIndex];
if (Is2D)
{
// loop through and spline the points
for (t = start; t <= 1.0f; t += tIncrement)
{
GetSplinePoint2D(ref previousPoint, ref currentPoint, ref nextPoint, ref nextNextPoint, t, out point);
path.Add(point);
}
}
else
{
// loop through and spline the points
for (t = start; t <= 1.0f; t += tIncrement)
{
GetSplinePoint3D(ref previousPoint, ref currentPoint, ref nextPoint, ref nextNextPoint, t, out point);
path.Add(point);
}
}
// reset start, keeping any additional amount over 1.0f to maintain a smooth curve
start = t - 1.0f;
}
return true;
}
///
/// Creates a spline path that curves around points in a list. This tries to maintain a similar distance between spline segments, regardless of distance between the initial points.
///
/// Receives the path points. Collection is not cleared.
/// Points for the spline to follow. Must contain at least 4 points.
/// The distance that each segment should be in the spline. This is approximate.
/// True to loop back to the start, false otherwise
/// True if success, false if points length is too small
public static bool CreateSplineWithSegmentDistance(ICollection path, IList points, float distancePerSegment, bool closePath)
{
if (points.Count < MinPointsForSpline || distancePerSegment <= 0.0f)
{
return false;
}
// make sure number of segments isn't too large or small
int numberOfPoints = (closePath ? points.Count : points.Count - 1);
int previousPointIndex, currentPointIndex, nextPointIndex, nextNextPointIndex;
int closePathInt = (closePath ? 1 : 0);
float t;
float tIncrement;
float start = 0.0f;
Vector3 point, previousPoint, currentPoint, nextPoint, nextNextPoint;
for (currentPointIndex = 0; currentPointIndex < numberOfPoints; currentPointIndex++)
{
// we will be using 4 points - the previous point, current point, next point and point after next
// the points can wrap around back to the start of the list
previousPointIndex = (currentPointIndex == 0 ? (numberOfPoints - closePathInt) : (currentPointIndex - 1));
nextPointIndex = currentPointIndex + 1;
nextNextPointIndex = currentPointIndex + 2;
if (closePath && nextPointIndex > numberOfPoints - 1)
{
nextPointIndex -= numberOfPoints;
}
if (nextNextPointIndex > numberOfPoints - 1)
{
nextNextPointIndex = (closePath ? nextNextPointIndex - numberOfPoints : numberOfPoints);
}
// assign the points - these will be passed by ref for performance
previousPoint = points[previousPointIndex];
currentPoint = points[currentPointIndex];
nextPoint = points[nextPointIndex];
nextNextPoint = points[nextNextPointIndex];
if (Is2D)
{
tIncrement = 1.0f / (Distance2D(ref nextPoint, ref currentPoint) / distancePerSegment);
tIncrement = Mathf.Clamp(tIncrement, 0.00390625f, 1.0f);
// loop through and spline the points
for (t = start; t <= 1.0f; t += tIncrement)
{
GetSplinePoint2D(ref previousPoint, ref currentPoint, ref nextPoint, ref nextNextPoint, t, out point);
path.Add(point);
}
}
else
{
tIncrement = 1.0f / (Distance3D(ref nextPoint, ref currentPoint) / distancePerSegment);
tIncrement = Mathf.Clamp(tIncrement, 0.00390625f, 1.0f);
// loop through and spline the points
for (t = start; t <= 1.0f; t += tIncrement)
{
GetSplinePoint3D(ref previousPoint, ref currentPoint, ref nextPoint, ref nextNextPoint, t, out point);
path.Add(point);
}
}
start = 0.0f;
}
return true;
}
}
}