Skip to content

Instantly share code, notes, and snippets.

@ditzel
Last active February 19, 2024 02:07
Show Gist options
  • Star 98 You must be signed in to star a gist
  • Fork 27 You must be signed in to fork a gist
  • Save ditzel/68be36987d8e7c83d48f497294c66e08 to your computer and use it in GitHub Desktop.
Save ditzel/68be36987d8e7c83d48f497294c66e08 to your computer and use it in GitHub Desktop.
A simple Math class to calculate a point in 2D or 3D space lying on a parabola. And a more complex parabola controller that you can put on an object.
using UnityEngine;
using System;
public class MathParabola
{
public static Vector3 Parabola(Vector3 start, Vector3 end, float height, float t)
{
Func<float, float> f = x => -4 * height * x * x + 4 * height * x;
var mid = Vector3.Lerp(start, end, t);
return new Vector3(mid.x, f(t) + Mathf.Lerp(start.y, end.y, t), mid.z);
}
public static Vector2 Parabola(Vector2 start, Vector2 end, float height, float t)
{
Func<float, float> f = x => -4 * height * x * x + 4 * height * x;
var mid = Vector2.Lerp(start, end, t);
return new Vector2(mid.x, f(t) + Mathf.Lerp(start.y, end.y, t));
}
}
using UnityEngine;
using System.Collections.Generic;
public class ParabolaController : MonoBehaviour
{
/// <summary>
/// Animation Speed
/// </summary>
public float Speed = 1;
/// <summary>
/// Start of Parabola
/// </summary>
public GameObject ParabolaRoot;
/// <summary>
/// Autostart Animation
/// </summary>
public bool Autostart = true;
/// <summary>
/// Animate
/// </summary>
public bool Animation = true;
//next parabola event
internal bool nextParbola = false;
//animation time
protected float animationTime = float.MaxValue;
//gizmo
protected ParabolaFly gizmo;
//draw
protected ParabolaFly parabolaFly;
void OnDrawGizmos()
{
if (gizmo == null)
{
gizmo = new ParabolaFly(ParabolaRoot.transform);
}
gizmo.RefreshTransforms(1f);
if ((gizmo.Points.Length - 1) % 2 != 0)
return;
int accur = 50;
Vector3 prevPos = gizmo.Points[0].position;
for (int c = 1; c <= accur; c++)
{
float currTime = c * gizmo.GetDuration() / accur;
Vector3 currPos = gizmo.GetPositionAtTime(currTime);
float mag = (currPos - prevPos).magnitude * 2;
Gizmos.color = new Color(mag, 0, 0, 1);
Gizmos.DrawLine(prevPos, currPos);
Gizmos.DrawSphere(currPos, 0.01f);
prevPos = currPos;
}
}
// Use this for initialization
void Start()
{
parabolaFly = new ParabolaFly(ParabolaRoot.transform);
if (Autostart)
{
RefreshTransforms(Speed);
FollowParabola();
}
}
// Update is called once per frame
void Update()
{
nextParbola = false;
if (Animation && parabolaFly != null && animationTime < parabolaFly.GetDuration())
{
int parabolaIndexBefore;
int parabolaIndexAfter;
parabolaFly.GetParabolaIndexAtTime(animationTime, out parabolaIndexBefore);
animationTime += Time.deltaTime;
parabolaFly.GetParabolaIndexAtTime(animationTime, out parabolaIndexAfter);
transform.position = parabolaFly.GetPositionAtTime(animationTime);
if (parabolaIndexBefore != parabolaIndexAfter)
nextParbola = true;
//if (transform.position.y > HighestPoint.y)
//HighestPoint = transform.position;
}
else if (Animation && parabolaFly != null && animationTime > parabolaFly.GetDuration())
{
animationTime = float.MaxValue;
Animation = false;
}
}
public void FollowParabola()
{
RefreshTransforms(Speed);
animationTime = 0f;
transform.position = parabolaFly.Points[0].position;
Animation = true;
//HighestPoint = points[0].position;
}
public Vector3 getHighestPoint(int parabolaIndex)
{
return parabolaFly.getHighestPoint(parabolaIndex);
}
public Transform[] getPoints()
{
return parabolaFly.Points;
}
public Vector3 GetPositionAtTime(float time)
{
return parabolaFly.GetPositionAtTime(time);
}
public float GetDuration()
{
return parabolaFly.GetDuration();
}
public void StopFollow()
{
animationTime = float.MaxValue;
}
/// <summary>
/// Returns children transforms, sorted by name.
/// </summary>
public void RefreshTransforms(float speed)
{
parabolaFly.RefreshTransforms(speed);
}
public static float DistanceToLine(Ray ray, Vector3 point)
{
//see:http://answers.unity3d.com/questions/62644/distance-between-a-ray-and-a-point.html
return Vector3.Cross(ray.direction, point - ray.origin).magnitude;
}
public static Vector3 ClosestPointInLine(Ray ray, Vector3 point)
{
return ray.origin + ray.direction * Vector3.Dot(ray.direction, point - ray.origin);
}
public class ParabolaFly
{
public Transform[] Points;
protected Parabola3D[] parabolas;
protected float[] partDuration;
protected float completeDuration;
public ParabolaFly(Transform ParabolaRoot)
{
List<Component> components = new List<Component>(ParabolaRoot.GetComponentsInChildren(typeof(Transform)));
List<Transform> transforms = components.ConvertAll(c => (Transform)c);
transforms.Remove(ParabolaRoot.transform);
transforms.Sort(delegate (Transform a, Transform b)
{
return a.name.CompareTo(b.name);
});
Points = transforms.ToArray();
//check if odd
if ((Points.Length - 1) % 2 != 0)
throw new UnityException("ParabolaRoot needs odd number of points");
//check if larger is needed
if (parabolas == null || parabolas.Length < (Points.Length - 1) / 2)
{
parabolas = new Parabola3D[(Points.Length - 1) / 2];
partDuration = new float[parabolas.Length];
}
}
public Vector3 GetPositionAtTime(float time)
{
int parabolaIndex;
float timeInParabola;
GetParabolaIndexAtTime(time, out parabolaIndex, out timeInParabola);
var percent = timeInParabola / partDuration[parabolaIndex];
return parabolas[parabolaIndex].GetPositionAtLength(percent * parabolas[parabolaIndex].Length);
}
public void GetParabolaIndexAtTime(float time, out int parabolaIndex)
{
float timeInParabola;
GetParabolaIndexAtTime(time, out parabolaIndex, out timeInParabola);
}
public void GetParabolaIndexAtTime(float time, out int parabolaIndex, out float timeInParabola)
{
//f(x) = ax² + bx + c
timeInParabola = time;
parabolaIndex = 0;
//determine parabola
while (parabolaIndex < parabolas.Length - 1 && partDuration[parabolaIndex] < timeInParabola)
{
timeInParabola -= partDuration[parabolaIndex];
parabolaIndex++;
}
}
public float GetDuration()
{
return completeDuration;
}
public Vector3 getHighestPoint(int parabolaIndex)
{
return parabolas[parabolaIndex].getHighestPoint();
}
/// <summary>
/// Returns children transforms, sorted by name.
/// </summary>
public void RefreshTransforms(float speed)
{
if (speed <= 0f)
speed = 1f;
if (Points != null)
{
completeDuration = 0;
//create parabolas
for (int i = 0; i < parabolas.Length; i++)
{
if (parabolas[i] == null)
parabolas[i] = new Parabola3D();
parabolas[i].Set(Points[i * 2].position, Points[i * 2 + 1].position, Points[i * 2 + 2].position);
partDuration[i] = parabolas[i].Length / speed;
completeDuration += partDuration[i];
}
}
}
}
public class Parabola3D
{
public float Length { get; private set; }
public Vector3 A;
public Vector3 B;
public Vector3 C;
protected Parabola2D parabola2D;
protected Vector3 h;
protected bool tooClose;
public Parabola3D()
{
}
public Parabola3D(Vector3 A, Vector3 B, Vector3 C)
{
Set(A, B, C);
}
public void Set(Vector3 A, Vector3 B, Vector3 C)
{
this.A = A;
this.B = B;
this.C = C;
refreshCurve();
}
public Vector3 getHighestPoint()
{
var d = (C.y - A.y) / parabola2D.Length;
var e = A.y - C.y;
var parabolaCompl = new Parabola2D(parabola2D.a, parabola2D.b + d, parabola2D.c + e, parabola2D.Length);
Vector3 E = new Vector3();
E.y = parabolaCompl.E.y;
E.x = A.x + (C.x - A.x) * (parabolaCompl.E.x / parabolaCompl.Length);
E.z = A.z + (C.z - A.z) * (parabolaCompl.E.x / parabolaCompl.Length);
return E;
}
public Vector3 GetPositionAtLength(float length)
{
//f(x) = ax² + bx + c
var percent = length / Length;
var x = percent * (C - A).magnitude;
if (tooClose)
x = percent * 2f;
Vector3 pos;
pos = A * (1f - percent) + C * percent + h.normalized * parabola2D.f(x);
if (tooClose)
pos.Set(A.x, pos.y, A.z);
return pos;
}
private void refreshCurve()
{
if (Vector2.Distance(new Vector2(A.x, A.z), new Vector2(B.x, B.z)) < 0.1f &&
Vector2.Distance(new Vector2(B.x, B.z), new Vector2(C.x, C.z)) < 0.1f)
tooClose = true;
else
tooClose = false;
Length = Vector3.Distance(A, B) + Vector3.Distance(B, C);
if (!tooClose)
{
refreshCurveNormal();
}
else
{
refreshCurveClose();
}
}
private void refreshCurveNormal()
{
// . E .
// . | point[1]
// . |h | .
// . | ___v1------point[2]
// . ______--vl------
// point[0]---------
//
//lower v1
Ray rl = new Ray(A, C - A);
var v1 = ClosestPointInLine(rl, B);
//get A=(x1,y1) B=(x2,y2) C=(x3,y3)
Vector2 A2d, B2d, C2d;
A2d.x = 0f;
A2d.y = 0f;
B2d.x = Vector3.Distance(A, v1);
B2d.y = Vector3.Distance(B, v1);
C2d.x = Vector3.Distance(A, C);
C2d.y = 0f;
parabola2D = new Parabola2D(A2d, B2d, C2d);
//lower v
//var p = parabola.E.x / parabola.Length;
//Vector3 vl = points[0].position * (1f - p) + points[2].position * p;
//h
h = (B - v1) / Vector3.Distance(v1, B) * parabola2D.E.y;
}
private void refreshCurveClose()
{
//distance to x0 - x2 line = |(x1-x0)x(x1-x2)|/|x2-x0|
var fac01 = (A.y <= B.y) ? 1f : -1f;
var fac02 = (A.y <= C.y) ? 1f : -1f;
Vector2 A2d, B2d, C2d;
//get A=(x1,y1) B=(x2,y2) C=(x3,y3)
A2d.x = 0f;
A2d.y = 0f;
//b = sqrt(c²-a²)
B2d.x = 1f;
B2d.y = Vector3.Distance((A + C) / 2f, B) * fac01;
C2d.x = 2f;
C2d.y = Vector3.Distance(A, C) * fac02;
parabola2D = new Parabola2D(A2d, B2d, C2d);
h = Vector3.up;
}
}
public class Parabola2D
{
public float a { get; private set; }
public float b { get; private set; }
public float c { get; private set; }
public Vector2 E { get; private set; }
public float Length { get; private set; }
public Parabola2D(float a, float b, float c, float length)
{
this.a = a;
this.b = b;
this.c = c;
setMetadata();
this.Length = length;
}
public Parabola2D(Vector2 A, Vector2 B, Vector2 C)
{
//f(x) = ax² + bx + c
//a = (x1(y2 - y3) + x2(y3 - y1) + x3(y1 - y2)) / ((x1 - x2)(x1 - x3)(x3 - x2))
//b = (x1²(y2 - y3) + x2²(y3 - y1) + x3²(y1 - y2))/ ((x1 - x2)(x1 - x3)(x2 - x3))
//c = (x1²(x2y3 - x3y2) + x1(x3²y2 - x2²y3) + x2x3y1(x2 - x3))/ ((x1 - x2)(x1 - x3)(x2 - x3))
var divisor = ((A.x - B.x) * (A.x - C.x) * (C.x - B.x));
if (divisor == 0f)
{
A.x += 0.00001f;
B.x += 0.00002f;
C.x += 0.00003f;
divisor = ((A.x - B.x) * (A.x - C.x) * (C.x - B.x));
}
a = (A.x * (B.y - C.y) + B.x * (C.y - A.y) + C.x * (A.y - B.y)) / divisor;
b = (A.x * A.x * (B.y - C.y) + B.x * B.x * (C.y - A.y) + C.x * C.x * (A.y - B.y)) / divisor;
c = (A.x * A.x * (B.x * C.y - C.x * B.y) + A.x * (C.x * C.x * B.y - B.x * B.x * C.y) + B.x * C.x * A.y * (B.x - C.x)) / divisor;
b = b * -1f;//hack
setMetadata();
Length = Vector2.Distance(A, C);
}
public float f(float x)
{
return a * x * x + b * x + c;
}
private void setMetadata()
{
//derive
//a*x²+b*x+c = 0
//2ax+b=0
//x = -b/2a
var x = -b / (2 * a);
E = new Vector2(x, f(x));
}
}
}
@Dangerface
Copy link

Hi @ditzel
Thanks for providing this useful code!
I am trying to use it to drop items in my game, so I know exactly where the items land. But I can't seem to get the object to follow a downward parabola.
It seem that there is something in the code that looks for the lowest point in the parabola root and sets that as a starting point. Is this true, and if so can it be reversed so the highest point is the starting point?

Cheers and thanks again :)

@MayAnKjajam
Copy link

Thanks for code!
I am trying to use it in WebGL but seems its not working, could you please let me know if anything need to be changed to make it work?

Thanks!

@opticfibre
Copy link

Thanks, @ditzel, Is there a way for the cube following the parabola to align to the parabola whilst following?

Many Thanks.

@shasvat13
Copy link

Hello!

Thank you so much for sharing this useful code.

can you please let me know from where have you derived this equation - ( -4 * height * x * x + 4 * height * x;)

Thanks in advance :)

@ICEYNDERP
Copy link

Thanks for the code!

How would I be able to rotate the object to align with the direction of movement on the parabola?

Thanks in advance :D

@Carmitus
Copy link

Thank you so much, it was very usefull <3

@Ti8an
Copy link

Ti8an commented Jan 10, 2024

Thanks a lot of work

@corpvs2
Copy link

corpvs2 commented Jan 12, 2024

thanks a lot for the code!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment