Created
March 20, 2019 17:02
-
-
Save dropmeaword/1ee56ec337f634884b7745e59baee49f to your computer and use it in GitHub Desktop.
ballistic calculations for Unity objects
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// LICENSE | |
// | |
// This software is dual-licensed to the public domain and under the following | |
// license: you are granted a perpetual, irrevocable license to copy, modify, | |
// publish, and distribute this file as you see fit. | |
// | |
// VERSION | |
// 0.1.0 (2016-06-01) Initial release | |
// | |
// AUTHOR | |
// Forrest Smith | |
// | |
// ADDITIONAL READING | |
// https://medium.com/@ForrestTheWoods/solving-ballistic-trajectories-b0165523348c | |
// | |
// API | |
// int solve_ballistic_arc(Vector3 proj_pos, float proj_speed, Vector3 target, float gravity, out Vector3 low, out Vector3 high); | |
// int solve_ballistic_arc(Vector3 proj_pos, float proj_speed, Vector3 target, Vector3 target_velocity, float gravity, out Vector3 s0, out Vector3 s1, out Vector3 s2, out Vector3 s3); | |
// bool solve_ballistic_arc_lateral(Vector3 proj_pos, float lateral_speed, Vector3 target, float max_height, out float vertical_speed, out float gravity); | |
// bool solve_ballistic_arc_lateral(Vector3 proj_pos, float lateral_speed, Vector3 target, Vector3 target_velocity, float max_height_offset, out Vector3 fire_velocity, out float gravity, out Vector3 impact_point); | |
// | |
// float ballistic_range(float speed, float gravity, float initial_height); | |
// | |
// bool IsZero(double d); | |
// int SolveQuadric(double c0, double c1, double c2, out double s0, out double s1); | |
// int SolveCubic(double c0, double c1, double c2, double c3, out double s0, out double s1, out double s2); | |
// int SolveQuartic(double c0, double c1, double c2, double c3, double c4, out double s0, out double s1, out double s2, out double s3); | |
using UnityEngine; | |
using System.Collections; | |
using System.Collections.Generic; | |
public class Ballistics { | |
// SolveQuadric, SolveCubic, and SolveQuartic were ported from C as written for Graphics Gems I | |
// Original Author: Jochen Schwarze ([email protected]) | |
// https://github.com/erich666/GraphicsGems/blob/240a34f2ad3fa577ef57be74920db6c4b00605e4/gems/Roots3And4.c | |
// Utility function used by SolveQuadratic, SolveCubic, and SolveQuartic | |
public static bool IsZero(double d) { | |
const double eps = 1e-9; | |
return d > -eps && d < eps; | |
} | |
// Solve quadratic equation: c0*x^2 + c1*x + c2. | |
// Returns number of solutions. | |
public static int SolveQuadric(double c0, double c1, double c2, out double s0, out double s1) { | |
s0 = double.NaN; | |
s1 = double.NaN; | |
double p, q, D; | |
/* normal form: x^2 + px + q = 0 */ | |
p = c1 / (2 * c0); | |
q = c2 / c0; | |
D = p * p - q; | |
if (IsZero(D)) { | |
s0 = -p; | |
return 1; | |
} | |
else if (D < 0) { | |
return 0; | |
} | |
else /* if (D > 0) */ { | |
double sqrt_D = System.Math.Sqrt(D); | |
s0 = sqrt_D - p; | |
s1 = -sqrt_D - p; | |
return 2; | |
} | |
} | |
// Solve cubic equation: c0*x^3 + c1*x^2 + c2*x + c3. | |
// Returns number of solutions. | |
public static int SolveCubic(double c0, double c1, double c2, double c3, out double s0, out double s1, out double s2) | |
{ | |
s0 = double.NaN; | |
s1 = double.NaN; | |
s2 = double.NaN; | |
int num; | |
double sub; | |
double A, B, C; | |
double sq_A, p, q; | |
double cb_p, D; | |
/* normal form: x^3 + Ax^2 + Bx + C = 0 */ | |
A = c1 / c0; | |
B = c2 / c0; | |
C = c3 / c0; | |
/* substitute x = y - A/3 to eliminate quadric term: x^3 +px + q = 0 */ | |
sq_A = A * A; | |
p = 1.0/3 * (- 1.0/3 * sq_A + B); | |
q = 1.0/2 * (2.0/27 * A * sq_A - 1.0/3 * A * B + C); | |
/* use Cardano's formula */ | |
cb_p = p * p * p; | |
D = q * q + cb_p; | |
if (IsZero(D)) { | |
if (IsZero(q)) /* one triple solution */ { | |
s0 = 0; | |
num = 1; | |
} | |
else /* one single and one double solution */ { | |
double u = System.Math.Pow(-q, 1.0/3.0); | |
s0 = 2 * u; | |
s1 = - u; | |
num = 2; | |
} | |
} | |
else if (D < 0) /* Casus irreducibilis: three real solutions */ { | |
double phi = 1.0/3 * System.Math.Acos(-q / System.Math.Sqrt(-cb_p)); | |
double t = 2 * System.Math.Sqrt(-p); | |
s0 = t * System.Math.Cos(phi); | |
s1 = - t * System.Math.Cos(phi + System.Math.PI / 3); | |
s2 = - t * System.Math.Cos(phi - System.Math.PI / 3); | |
num = 3; | |
} | |
else /* one real solution */ { | |
double sqrt_D = System.Math.Sqrt(D); | |
double u = System.Math.Pow(sqrt_D - q, 1.0/3.0); | |
double v = - System.Math.Pow(sqrt_D + q, 1.0/3.0); | |
s0 = u + v; | |
num = 1; | |
} | |
/* resubstitute */ | |
sub = 1.0/3 * A; | |
if (num > 0) s0 -= sub; | |
if (num > 1) s1 -= sub; | |
if (num > 2) s2 -= sub; | |
return num; | |
} | |
// Solve quartic function: c0*x^4 + c1*x^3 + c2*x^2 + c3*x + c4. | |
// Returns number of solutions. | |
public static int SolveQuartic(double c0, double c1, double c2, double c3, double c4, out double s0, out double s1, out double s2, out double s3) { | |
s0 = double.NaN; | |
s1 = double.NaN; | |
s2 = double.NaN; | |
s3 = double.NaN; | |
double[] coeffs = new double[4]; | |
double z, u, v, sub; | |
double A, B, C, D; | |
double sq_A, p, q, r; | |
int num; | |
/* normal form: x^4 + Ax^3 + Bx^2 + Cx + D = 0 */ | |
A = c1 / c0; | |
B = c2 / c0; | |
C = c3 / c0; | |
D = c4 / c0; | |
/* substitute x = y - A/4 to eliminate cubic term: x^4 + px^2 + qx + r = 0 */ | |
sq_A = A * A; | |
p = - 3.0/8 * sq_A + B; | |
q = 1.0/8 * sq_A * A - 1.0/2 * A * B + C; | |
r = - 3.0/256*sq_A*sq_A + 1.0/16*sq_A*B - 1.0/4*A*C + D; | |
if (IsZero(r)) { | |
/* no absolute term: y(y^3 + py + q) = 0 */ | |
coeffs[ 3 ] = q; | |
coeffs[ 2 ] = p; | |
coeffs[ 1 ] = 0; | |
coeffs[ 0 ] = 1; | |
num = Ballistics.SolveCubic(coeffs[0], coeffs[1], coeffs[2], coeffs[3], out s0, out s1, out s2); | |
} | |
else { | |
/* solve the resolvent cubic ... */ | |
coeffs[ 3 ] = 1.0/2 * r * p - 1.0/8 * q * q; | |
coeffs[ 2 ] = - r; | |
coeffs[ 1 ] = - 1.0/2 * p; | |
coeffs[ 0 ] = 1; | |
SolveCubic(coeffs[0], coeffs[1], coeffs[2], coeffs[3], out s0, out s1, out s2); | |
/* ... and take the one real solution ... */ | |
z = s0; | |
/* ... to build two quadric equations */ | |
u = z * z - r; | |
v = 2 * z - p; | |
if (IsZero(u)) | |
u = 0; | |
else if (u > 0) | |
u = System.Math.Sqrt(u); | |
else | |
return 0; | |
if (IsZero(v)) | |
v = 0; | |
else if (v > 0) | |
v = System.Math.Sqrt(v); | |
else | |
return 0; | |
coeffs[ 2 ] = z - u; | |
coeffs[ 1 ] = q < 0 ? -v : v; | |
coeffs[ 0 ] = 1; | |
num = Ballistics.SolveQuadric(coeffs[0], coeffs[1], coeffs[2], out s0, out s1); | |
coeffs[ 2 ]= z + u; | |
coeffs[ 1 ] = q < 0 ? v : -v; | |
coeffs[ 0 ] = 1; | |
if (num == 0) num += Ballistics.SolveQuadric(coeffs[0], coeffs[1], coeffs[2], out s0, out s1); | |
if (num == 1) num += Ballistics.SolveQuadric(coeffs[0], coeffs[1], coeffs[2], out s1, out s2); | |
if (num == 2) num += Ballistics.SolveQuadric(coeffs[0], coeffs[1], coeffs[2], out s2, out s3); | |
} | |
/* resubstitute */ | |
sub = 1.0/4 * A; | |
if (num > 0) s0 -= sub; | |
if (num > 1) s1 -= sub; | |
if (num > 2) s2 -= sub; | |
if (num > 3) s3 -= sub; | |
return num; | |
} | |
// Calculate the maximum range that a ballistic projectile can be fired on given speed and gravity. | |
// | |
// speed (float): projectile velocity | |
// gravity (float): force of gravity, positive is down | |
// initial_height (float): distance above flat terrain | |
// | |
// return (float): maximum range | |
public static float ballistic_range(float speed, float gravity, float initial_height) { | |
// Handling these cases is up to your project's coding standards | |
Debug.Assert(speed > 0 && gravity > 0 && initial_height >= 0, "Ballistics.ballistic_range called with invalid data"); | |
// Derivation | |
// (1) x = speed * time * cos O | |
// (2) y = initial_height + (speed * time * sin O) - (.5 * gravity*time*time) | |
// (3) via quadratic: t = (speed*sin O)/gravity + sqrt(speed*speed*sin O + 2*gravity*initial_height)/gravity [ignore smaller root] | |
// (4) solution: range = x = (speed*cos O)/gravity * sqrt(speed*speed*sin O + 2*gravity*initial_height) [plug t back into x=speed*time*cos O] | |
float angle = 45 * Mathf.Deg2Rad; // no air resistence, so 45 degrees provides maximum range | |
float cos = Mathf.Cos(angle); | |
float sin = Mathf.Sin(angle); | |
float range = (speed*cos/gravity) * (speed*sin + Mathf.Sqrt(speed*speed*sin*sin + 2*gravity*initial_height)); | |
return range; | |
} | |
// Solve firing angles for a ballistic projectile with speed and gravity to hit a fixed position. | |
// | |
// proj_pos (Vector3): point projectile will fire from | |
// proj_speed (float): scalar speed of projectile | |
// target (Vector3): point projectile is trying to hit | |
// gravity (float): force of gravity, positive down | |
// | |
// s0 (out Vector3): firing solution (low angle) | |
// s1 (out Vector3): firing solution (high angle) | |
// | |
// return (int): number of unique solutions found: 0, 1, or 2. | |
public static int solve_ballistic_arc(Vector3 proj_pos, float proj_speed, Vector3 target, float gravity, out Vector3 s0, out Vector3 s1) { | |
// Handling these cases is up to your project's coding standards | |
Debug.Assert(proj_pos != target && proj_speed > 0 && gravity > 0, "Ballistics.solve_ballistic_arc called with invalid data"); | |
// C# requires out variables be set | |
s0 = Vector3.zero; | |
s1 = Vector3.zero; | |
// Derivation | |
// (1) x = v*t*cos O | |
// (2) y = v*t*sin O - .5*g*t^2 | |
// | |
// (3) t = x/(cos O*v) [solve t from (1)] | |
// (4) y = v*x*sin O/(cos O * v) - .5*g*x^2/(cos^2 O*v^2) [plug t into y=...] | |
// (5) y = x*tan O - g*x^2/(2*v^2*cos^2 O) [reduce; cos/sin = tan] | |
// (6) y = x*tan O - (g*x^2/(2*v^2))*(1+tan^2 O) [reduce; 1+tan O = 1/cos^2 O] | |
// (7) 0 = ((-g*x^2)/(2*v^2))*tan^2 O + x*tan O - (g*x^2)/(2*v^2) - y [re-arrange] | |
// Quadratic! a*p^2 + b*p + c where p = tan O | |
// | |
// (8) let gxv = -g*x*x/(2*v*v) | |
// (9) p = (-x +- sqrt(x*x - 4gxv*(gxv - y)))/2*gxv [quadratic formula] | |
// (10) p = (v^2 +- sqrt(v^4 - g(g*x^2 + 2*y*v^2)))/gx [multiply top/bottom by -2*v*v/x; move 4*v^4/x^2 into root] | |
// (11) O = atan(p) | |
Vector3 diff = target - proj_pos; | |
Vector3 diffXZ = new Vector3(diff.x, 0f, diff.z); | |
float groundDist = diffXZ.magnitude; | |
float speed2 = proj_speed*proj_speed; | |
float speed4 = proj_speed*proj_speed*proj_speed*proj_speed; | |
float y = diff.y; | |
float x = groundDist; | |
float gx = gravity*x; | |
float root = speed4 - gravity*(gravity*x*x + 2*y*speed2); | |
// No solution | |
if (root < 0) | |
return 0; | |
root = Mathf.Sqrt(root); | |
float lowAng = Mathf.Atan2(speed2 - root, gx); | |
float highAng = Mathf.Atan2(speed2 + root, gx); | |
int numSolutions = lowAng != highAng ? 2 : 1; | |
Vector3 groundDir = diffXZ.normalized; | |
s0 = groundDir*Mathf.Cos(lowAng)*proj_speed + Vector3.up*Mathf.Sin(lowAng)*proj_speed; | |
if (numSolutions > 1) | |
s1 = groundDir*Mathf.Cos(highAng)*proj_speed + Vector3.up*Mathf.Sin(highAng)*proj_speed; | |
return numSolutions; | |
} | |
// Solve firing angles for a ballistic projectile with speed and gravity to hit a target moving with constant, linear velocity. | |
// | |
// proj_pos (Vector3): point projectile will fire from | |
// proj_speed (float): scalar speed of projectile | |
// target (Vector3): point projectile is trying to hit | |
// target_velocity (Vector3): velocity of target | |
// gravity (float): force of gravity, positive down | |
// | |
// s0 (out Vector3): firing solution (fastest time impact) | |
// s1 (out Vector3): firing solution (next impact) | |
// s2 (out Vector3): firing solution (next impact) | |
// s3 (out Vector3): firing solution (next impact) | |
// | |
// return (int): number of unique solutions found: 0, 1, 2, 3, or 4. | |
public static int solve_ballistic_arc(Vector3 proj_pos, float proj_speed, Vector3 target_pos, Vector3 target_velocity, float gravity, out Vector3 s0, out Vector3 s1) { | |
// Initialize output parameters | |
s0 = Vector3.zero; | |
s1 = Vector3.zero; | |
// Derivation | |
// | |
// For full derivation see: blog.forrestthewoods.com | |
// Here is an abbreviated version. | |
// | |
// Four equations, four unknowns (solution.x, solution.y, solution.z, time): | |
// | |
// (1) proj_pos.x + solution.x*time = target_pos.x + target_vel.x*time | |
// (2) proj_pos.y + solution.y*time + .5*G*t = target_pos.y + target_vel.y*time | |
// (3) proj_pos.z + solution.z*time = target_pos.z + target_vel.z*time | |
// (4) proj_speed^2 = solution.x^2 + solution.y^2 + solution.z^2 | |
// | |
// (5) Solve for solution.x and solution.z in equations (1) and (3) | |
// (6) Square solution.x and solution.z from (5) | |
// (7) Solve solution.y^2 by plugging (6) into (4) | |
// (8) Solve solution.y by rearranging (2) | |
// (9) Square (8) | |
// (10) Set (8) = (7). All solution.xyz terms should be gone. Only time remains. | |
// (11) Rearrange 10. It will be of the form a*^4 + b*t^3 + c*t^2 + d*t * e. This is a quartic. | |
// (12) Solve the quartic using SolveQuartic. | |
// (13) If there are no positive, real roots there is no solution. | |
// (14) Each positive, real root is one valid solution | |
// (15) Plug each time value into (1) (2) and (3) to calculate solution.xyz | |
// (16) The end. | |
double G = gravity; | |
double A = proj_pos.x; | |
double B = proj_pos.y; | |
double C = proj_pos.z; | |
double M = target_pos.x; | |
double N = target_pos.y; | |
double O = target_pos.z; | |
double P = target_velocity.x; | |
double Q = target_velocity.y; | |
double R = target_velocity.z; | |
double S = proj_speed; | |
double H = M - A; | |
double J = O - C; | |
double K = N - B; | |
double L = -.5f * G; | |
// Quartic Coeffecients | |
double c0 = L*L; | |
double c1 = 2*Q*L; | |
double c2 = Q*Q + 2*K*L - S*S + P*P + R*R; | |
double c3 = 2*K*Q + 2*H*P + 2*J*R; | |
double c4 = K*K + H*H + J*J; | |
// Solve quartic | |
double[] times = new double[4]; | |
int numTimes = SolveQuartic(c0, c1, c2, c3, c4, out times[0], out times[1], out times[2], out times[3]); | |
// Sort so faster collision is found first | |
System.Array.Sort(times); | |
// Plug quartic solutions into base equations | |
// There should never be more than 2 positive, real roots. | |
Vector3[] solutions = new Vector3[2]; | |
int numSolutions = 0; | |
for (int i = 0; i < numTimes && numSolutions < 2; ++i) { | |
double t = times[i]; | |
if ( t <= 0) | |
continue; | |
solutions[numSolutions].x = (float)((H+P*t)/t); | |
solutions[numSolutions].y = (float)((K+Q*t-L*t*t)/ t); | |
solutions[numSolutions].z = (float)((J+R*t)/t); | |
++numSolutions; | |
} | |
// Write out solutions | |
if (numSolutions > 0) s0 = solutions[0]; | |
if (numSolutions > 1) s1 = solutions[1]; | |
return numSolutions; | |
} | |
// Solve the firing arc with a fixed lateral speed. Vertical speed and gravity varies. | |
// This enables a visually pleasing arc. | |
// | |
// proj_pos (Vector3): point projectile will fire from | |
// lateral_speed (float): scalar speed of projectile along XZ plane | |
// target_pos (Vector3): point projectile is trying to hit | |
// max_height (float): height above Max(proj_pos, impact_pos) for projectile to peak at | |
// | |
// fire_velocity (out Vector3): firing velocity | |
// gravity (out float): gravity necessary to projectile to hit precisely max_height | |
// | |
// return (bool): true if a valid solution was found | |
public static bool solve_ballistic_arc_lateral(Vector3 proj_pos, float lateral_speed, Vector3 target_pos, float max_height, out Vector3 fire_velocity, out float gravity) { | |
// Handling these cases is up to your project's coding standards | |
Debug.Assert(proj_pos != target_pos && lateral_speed > 0 && max_height > proj_pos.y, "Ballistics.solve_ballistic_arc called with invalid data"); | |
fire_velocity = Vector3.zero; | |
gravity = float.NaN; | |
Vector3 diff = target_pos - proj_pos; | |
Vector3 diffXZ = new Vector3(diff.x, 0f, diff.z); | |
float lateralDist = diffXZ.magnitude; | |
if (lateralDist == 0) | |
return false; | |
float time = lateralDist / lateral_speed; | |
fire_velocity = diffXZ.normalized * lateral_speed; | |
// System of equations. Hit max_height at t=.5*time. Hit target at t=time. | |
// | |
// peak = y0 + vertical_speed*halfTime + .5*gravity*halfTime^2 | |
// end = y0 + vertical_speed*time + .5*gravity*time^s | |
// Wolfram Alpha: solve b = a + .5*v*t + .5*g*(.5*t)^2, c = a + vt + .5*g*t^2 for g, v | |
float a = proj_pos.y; // initial | |
float b = max_height; // peak | |
float c = target_pos.y; // final | |
gravity = -4*(a - 2*b + c) / (time* time); | |
fire_velocity.y = -(3*a - 4*b + c) / time; | |
return true; | |
} | |
// Solve the firing arc with a fixed lateral speed. Vertical speed and gravity varies. | |
// This enables a visually pleasing arc. | |
// | |
// proj_pos (Vector3): point projectile will fire from | |
// lateral_speed (float): scalar speed of projectile along XZ plane | |
// target_pos (Vector3): point projectile is trying to hit | |
// max_height (float): height above Max(proj_pos, impact_pos) for projectile to peak at | |
// | |
// fire_velocity (out Vector3): firing velocity | |
// gravity (out float): gravity necessary to projectile to hit precisely max_height | |
// impact_point (out Vector3): point where moving target will be hit | |
// | |
// return (bool): true if a valid solution was found | |
public static bool solve_ballistic_arc_lateral(Vector3 proj_pos, float lateral_speed, Vector3 target, Vector3 target_velocity, float max_height_offset, out Vector3 fire_velocity, out float gravity, out Vector3 impact_point) { | |
// Handling these cases is up to your project's coding standards | |
Debug.Assert(proj_pos != target && lateral_speed > 0, "Ballistics.solve_ballistic_arc_lateral called with invalid data"); | |
// Initialize output variables | |
fire_velocity = Vector3.zero; | |
gravity = 0f; | |
impact_point = Vector3.zero; | |
// Ground plane terms | |
Vector3 targetVelXZ = new Vector3(target_velocity.x, 0f, target_velocity.z); | |
Vector3 diffXZ = target - proj_pos; | |
diffXZ.y = 0; | |
// Derivation | |
// (1) Base formula: |P + V*t| = S*t | |
// (2) Substitute variables: |diffXZ + targetVelXZ*t| = S*t | |
// (3) Square both sides: Dot(diffXZ,diffXZ) + 2*Dot(diffXZ, targetVelXZ)*t + Dot(targetVelXZ, targetVelXZ)*t^2 = S^2 * t^2 | |
// (4) Quadratic: (Dot(targetVelXZ,targetVelXZ) - S^2)t^2 + (2*Dot(diffXZ, targetVelXZ))*t + Dot(diffXZ, diffXZ) = 0 | |
float c0 = Vector3.Dot(targetVelXZ, targetVelXZ) - lateral_speed*lateral_speed; | |
float c1 = 2f * Vector3.Dot(diffXZ, targetVelXZ); | |
float c2 = Vector3.Dot(diffXZ, diffXZ); | |
double t0, t1; | |
int n = Ballistics.SolveQuadric(c0, c1, c2, out t0, out t1); | |
// pick smallest, positive time | |
bool valid0 = n > 0 && t0 > 0; | |
bool valid1 = n > 1 && t1 > 0; | |
float t; | |
if (!valid0 && !valid1) | |
return false; | |
else if (valid0 && valid1) | |
t = Mathf.Min((float)t0, (float)t1); | |
else | |
t = valid0 ? (float)t0 : (float)t1; | |
// Calculate impact point | |
impact_point = target + (target_velocity*t); | |
// Calculate fire velocity along XZ plane | |
Vector3 dir = impact_point - proj_pos; | |
fire_velocity = new Vector3(dir.x, 0f, dir.z).normalized * lateral_speed; | |
// Solve system of equations. Hit max_height at t=.5*time. Hit target at t=time. | |
// | |
// peak = y0 + vertical_speed*halfTime + .5*gravity*halfTime^2 | |
// end = y0 + vertical_speed*time + .5*gravity*time^s | |
// Wolfram Alpha: solve b = a + .5*v*t + .5*g*(.5*t)^2, c = a + vt + .5*g*t^2 for g, v | |
float a = proj_pos.y; // initial | |
float b = Mathf.Max(proj_pos.y, impact_point.y) + max_height_offset; // peak | |
float c = impact_point.y; // final | |
gravity = -4*(a - 2*b + c) / (t* t); | |
fire_velocity.y = -(3*a - 4*b + c) / t; | |
return true; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment