The previous post sets the foundations in which part 2 and 3 are built upon. Part 1 was mandatory as there have been many changes since the original Physics-based Solar System Lessons post.
Gravity Part 2: Orbit Predictions (One-Body problem)
Once we have a system affected only by gravitational forces, the next logical step was to predict orbits of objects. This is not a trivial task, especially when there are multiple bodies in play. I focused on implementing object trajectory predictions for One-Body problems.
Orbital mechanics sketch (image credit: wikipedia [4])
Background
All the math and concepts behind the One/N-Body problems have been obtained from the books by Keeton, C. (2014) [1] and Walter, U. (2018) [2].
What is a One-Body problem?
A system in which the source of gravity is stationary while another object is in motion (and therefore affected by the stationary object’s gravity).
What is a Two-Body Problem?
A system in which two objects interact with each other through their force of gravity.
Can we predict orbits?
There are 3 different problems to predicting trajectories:
One-Body problem: Solving the prediction is not complicated. The visual representation is simple as the trajectory will be fixed in time.
Two-Body problem: Things become tricky as both objects move in time. Any visualization would be complicated as the forces applied will change as objects move in time.
N-Body problem: This is a generalization of the Two-body problem to a larger scale. As mentioned before, visualization of the orbits would be complicated.
There are two possibilities to visualize the trajectories:
Given a system, let it run for a certain amount of time and we can visualize the position and velocity of the objects. This can be used to predict N-Body problems and see the future trajectories, but it will not give you any past trajectories.
Given a function obtain the past and future trajectory of an object. This is simple and doable for a One-Body problem.
Disclaimer: There are ways to solve N-Body problems numerically, but I have not tried to apply the math in Unity.
The first option can be used by setting up a system with the information provided in Gravity Part 1: An update. The second option is what I am going to explain in this post.
Predictions
As we are focused on One-Body problems, we will always have one of these trajectories:
Elliptical/Circular: object “captured” by the gravity of the massive object.
Parabolic: Object that is passing just fast enough to escape gravitation pull.
Hyperbolic: Object that is passing too fast and escapes gravitation pull.
Calculations for elliptical trajectories are easy, but those for parabolic and hyperbolic are a bit more complex. All these trajectories come from conic sections. The parabolic trajectory is a fringe case that I will not implement, but it can be added following the same logic as it is for elliptic/hyperbolic.
To create these conics I will be using the following data:
A Massive object that is the primary force attraction:
Given its position.
Given its gravity force.
Another object that we want to check its trajectory:
Given its position.
Given its velocity and direction.
The Code
From this point onwards I will be explaining the math as well as the code I have used. The code can be summarized into 2 parts:
Conics: Conic shape trajectories.
Orbit: Calculation of the trajectory using conic shapes.
Conics
As can be observed by the following picture the conic family can create the desired trajectories: Circular, Elliptical, parabolic, and hyperbolic.
Conic sections and orbits (image credit: braeunig [3])
All conics have the same basic structure, as such I created an interface IConic that affects all.
It is worth mentioning that both ellipse and hyperbola will get all equation variables in the constructor from the eccentricity and semi major axis. Here is an explanation of the public functions:
GetPointsInWorldCoordinates(...):
Given a transform (rotation and traslation) obtain the trajectory points in world coordinates
SetConicSectionTransform(...):
Modify the Orbit Object transform component to be aligned with the conic section. We need to know whether the velocity is increasing to obtain the correct angle of the object in the conic segment.
Ellipse trajectory
This also considers the Circular trajectory as it is a fringe case of the ellipse. We need to solve the basic (no translation nor rotation) ellipse equation:
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Collections.ObjectModel;
usingSystem.Linq;
usingUnityEngine;
publicclassEllipse : IConicSection
{
#region Propertiespublicdouble SemiMajorAxis { get; privateset; }
publicdouble SemiMinorAxis { get; privateset; }
publicdouble Eccentricity { get; privateset; }
publicdouble Distance { get; privateset; } //Foci Distancepublic ReadOnlyCollection<Vector3> Points { get; privateset; }
#endregion#region Public Methods/// <summary>/// Basic ellipse in Unity default units, centered in the origin and no rotation applied/// x^2/a^2 + y^2/b^2 = 1/// /// a = SemiMajorAxis/// b = SemiMinorAxis/// </summary>/// <param name="eccentricity"> Ellipse eccentricity [0,1] </param>/// <param name="semiMajorAxis"> Length of the semi major axis </param>publicEllipse(double eccentricity, double semiMajorAxis, int numberOfPoints = 10)
{
Eccentricity = eccentricity;
SemiMajorAxis = semiMajorAxis;
SemiMinorAxis = CalculateSemiMinorAxis();
Distance = CalculateFociDistance();
CreateEllipseLocalPoints(numberOfPoints);
}
/// <summary>/// Get all the local points in world coordinates, applying:/// - A specific transform (position, normal and rotation around local Y)/// </summary>/// <param name="objectTransform"> An object's transform </param>/// <returns> Ellipse points in world coordinates </returns>public List<Vector3> GetPointsInWorldCoordinates(Transform objectTransform)
{
return Points.Select(p => objectTransform.TransformPoint(p)).ToList();
}
/// <summary>/// Given the orbit plane, an ellipse focus point, an object's position in the /// ellipse (and if it is accelerating), adjust the orbit plane position and its /// rotation so a projection of the ellipse is correct in world coordinates./// </summary>/// <param name="orbitPlane"> where the Ellipse will be projected </param>/// <param name="focusPoint"> ellipse focus </param>/// <param name="pointInTrajectory"> point in the ellipse edge </param>/// <param name="isVelocityIncreasing"> is point in trajectory increasing speed? used to get the correct angle </param>publicvoidSetConicSectionTransform(Transform orbitPlane, Vector3 focusPoint, Vector3 pointInTrajectory, bool isVelocityIncreasing)
{
//Set positionvar distanceFromObject = Vector3.Distance(pointInTrajectory, focusPoint);
var angleRadians = GetAngleBetweenPointInEllipseAndSemiMajorAxis(distanceFromObject, isVelocityIncreasing);
var rot = Quaternion.AngleAxis(-(float)angleRadians * Mathf.Rad2Deg, orbitPlane.up);
var semiMajorAxisDirection = (rot * (focusPoint - pointInTrajectory)).normalized;
orbitPlane.position = focusPoint + semiMajorAxisDirection * (float)Distance;
//Set rotationvar semiMajorAxisDirectionLocal = orbitPlane.InverseTransformDirection(semiMajorAxisDirection);
var angle = Vector3.Angle(semiMajorAxisDirectionLocal, Vector3.right);
orbitPlane.Rotate(Vector3.up, angle, Space.Self);
var localPos = orbitPlane.InverseTransformPoint(pointInTrajectory);
if (!IsPointInEdge(localPos))
{
orbitPlane.Rotate(Vector3.up, -angle * 2, Space.Self);
}
}
#endregion#region Private MethodsprivatedoubleCalculateSemiMinorAxis()
{
return SemiMajorAxis * Math.Sqrt(1 - Math.Pow(Eccentricity, 2));
}
privatedoubleCalculateFociDistance()
{
return Math.Sqrt(Math.Pow(SemiMajorAxis, 2) - Math.Pow(SemiMinorAxis, 2));
}
privatevoidCreateEllipseLocalPoints(int numberOfPoints)
{
var points = new List<Vector3>();
var gap = 360.0f / numberOfPoints;
for (int i = 0; i < numberOfPoints; i++)
{
var theta = (i * gap) * Mathf.Deg2Rad;
var x = SemiMajorAxis * Mathf.Cos(theta);
var z = SemiMinorAxis * Mathf.Sin(theta);
points.Add(new Vector3((float)x, 0, (float)z));
}
points.Add(points[0]);
Points = points.AsReadOnly();
}
//Get the angle using the orbit equationprivatedoubleGetAngleBetweenPointInEllipseAndSemiMajorAxis(double distance, bool isIncreasingVelocity)
{
var cosE = GetCenterCosAngleFromOrbitEquation(distance);
var angle = Math.Acos((cosE - Eccentricity) / (1 - Eccentricity * cosE));
return isIncreasingVelocity ? angle : -angle;
}
privatedoubleGetCenterCosAngleFromOrbitEquation(double distance)
{
return -(distance / SemiMajorAxis - 1) / Eccentricity;
}
/// <summary>/// Verify if the given point is in the ellipse/// </summary>/// <param name="point"> a point in 3D, note the Y-axis is not used here </param>/// <returns></returns>privateboolIsPointInEdge(Vector3 point)
{
return Math.Abs(Math.Pow(point.x, 2) / Math.Pow(SemiMajorAxis, 2) + Math.Pow(point.z, 2) / Math.Pow(SemiMinorAxis, 2) - 1) < Constants.EPSILON;
}
#endregion
}
Note: to obtain the rotation with respect to the focus we use the orbit equation and its derivation. To follow the math I followed the formulas provided by Walter (equations 7:4:14) [2]
Hyperbolic trajectory:
We need to solve the basic (no translation nor rotation) hyperbolic equation:
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Collections.ObjectModel;
usingSystem.Linq;
usingUnityEngine;
publicclassHyperbola : IConicSection
{
#region Propertiespublicdouble SemiMajorAxis { get; privateset; }
publicdouble SemiMinorAxis { get; privateset; }
publicdouble Eccentricity { get; privateset; }
publicdouble Distance { get; privateset; } //Minimum distance from foci to edgepublicdouble SemiLatusRectum { get; privateset; }
public ReadOnlyCollection<Vector3> Points { get; privateset; }
#endregion#region Public Methods/// <summary>/// Basic hyperbola in Unity default units, centered in the origin and no rotation applied/// x^2/a^2 - y^2/b^2 = 1/// /// a = SemiMajorAxis/// b = SemiMinorAxis/// </summary>/// <param name="eccentricity"> Hyperbola eccentricity > 1 </param>/// <param name="semiMajorAxis"> Length of the semi major axis </param>publicHyperbola(double eccentricity, double semiMajorAxis, int numberOfPoints = 10)
{
Eccentricity = eccentricity;
SemiMajorAxis = semiMajorAxis;
SemiMinorAxis = CalculateSemiMinorAxis();
Distance = CalculateMinimumDistance();
SemiLatusRectum = SemiMajorAxis * (1 - Math.Pow(Eccentricity, 2));
CreateHyperbolaLocalPoints(numberOfPoints);
}
/// <summary>/// Get all the local points in world coordinates, applying:/// - A specific transform (position, normal and rotation around local Y)/// </summary>/// <param name="objectTransform"> An object's transform </param>/// <returns> Hyperbola points in world coordinates </returns>public List<Vector3> GetPointsInWorldCoordinates(Transform objectTransform)
{
return Points.Select(p => objectTransform.TransformPoint(p)).ToList();
}
/// <summary>/// Given the orbit plane, a hyperbola focus point, an object's position in the /// hyperbola (and if it is accelerating), adjust the orbit plane position and its /// rotation so a projection of the hyperbola is correct in world coordinates./// </summary>/// <param name="orbitPlane"> where the Hyperbola will be projected </param>/// <param name="focusPoint"> hyperbola focus </param>/// <param name="pointInTrajectory"> point in the hyperbola edge </param>/// <param name="isVelocityIncreasing"> is point in trajectory increasing speed? </param>publicvoidSetConicSectionTransform(Transform orbitPlane, Vector3 focusPoint, Vector3 pointInTrajectory, bool isVelocityIncreasing)
{
//Set position
orbitPlane.position = focusPoint;
//Set rotationfloat angle = GetAngle(orbitPlane, focusPoint, pointInTrajectory, isVelocityIncreasing);
orbitPlane.Rotate(Vector3.up, angle, Space.Self);
var localPos = orbitPlane.InverseTransformPoint(pointInTrajectory);
if (!IsPointInEdge(localPos))
{
orbitPlane.Rotate(Vector3.up, -angle * 2, Space.Self);
}
}
#endregion#region Private MethodsprivatedoubleCalculateSemiMinorAxis()
{
return -SemiMajorAxis * Math.Sqrt(Math.Pow(Eccentricity, 2) - 1);
}
privatedoubleCalculateMinimumDistance()
{
return SemiMajorAxis * (1 - Eccentricity);
}
privatevoidCreateHyperbolaLocalPoints(int numberOfPoints)
{
var pointsPositive = new List<Vector3>();
var pointsNegative = new List<Vector3>();
var offset = -SemiMajorAxis + Distance;
for (int i = 0; i < numberOfPoints / 2; ++i)
{
var z = i * i / 5;
var x = (float)(SemiMajorAxis * Math.Sqrt(1 + Math.Pow(z, 2) / Math.Pow(SemiMinorAxis, 2)) + offset);
pointsPositive.Add(new Vector3(x, 0, z));
if (i != 0)
{
pointsNegative.Add(new Vector3(x, 0, -z));
}
}
pointsPositive.Reverse();
pointsPositive.AddRange(pointsNegative);
Points = pointsPositive.AsReadOnly();
}
privatefloatGetAngle(Transform orbitPlane, Vector3 focusPoint, Vector3 pointInTrajectory, bool isVelocityIncreasing)
{
var distanceFromObject = Vector3.Distance(focusPoint, pointInTrajectory);
var angleRadians = Math.Acos((SemiLatusRectum / distanceFromObject - 1) / Eccentricity);
angleRadians = (isVelocityIncreasing) ? -angleRadians : angleRadians;
var rot = Quaternion.AngleAxis((float)angleRadians * Mathf.Rad2Deg, orbitPlane.up);
var semiMajorAxisDirection = (rot * (pointInTrajectory - focusPoint)).normalized;
var semiMajorAxisDirectionLocal = orbitPlane.InverseTransformDirection(semiMajorAxisDirection);
var angleLocal = Vector3.Angle(semiMajorAxisDirectionLocal, Vector3.right);
return angleLocal;
}
/// <summary>/// Verify if the given point is in the hyperbola. Offset applied as the points are not/// centered in the "center" of the hyperbola, but 1 of the focus points./// </summary>/// <param name="point"> a point in 3D, not the Y-axis is not used here </param>/// <returns></returns>privateboolIsPointInEdge(Vector3 point)
{
var offset = new Vector3((float)(Distance - SemiMajorAxis), 0, 0);
point -= offset;
var distanceToEdge = Math.Abs(Math.Pow(point.x, 2) / Math.Pow(SemiMajorAxis, 2) - Math.Pow(point.z, 2) / Math.Pow(SemiMinorAxis, 2) - 1);
return distanceToEdge < Constants.EPSILON;
}
#endregion
}
Note: The function GetAngle(...) returns the angle from Vector3.Angle which outputs a value [0, 180]. As it is missing the [180, 360] range we check if the object is in the trajectory.
Orbit
The Orbit code basically consists of the functionality that will trigger the orbit creation and visualization.
usingSystem;
usingUnityEngine;
usingstatic Constants;
publicclassOrbit : MonoBehaviour
{
public GameObject OrbitingObject { get; privateset; }
public GameObject OrbitAroundObject { get; privateset; }
public OrbitTrajectory OrbitTrajectory { get; privateset; } = OrbitTrajectory.Undefined;
private GameManager _gameManager;
private LineRenderer _lineRenderer;
private IConicSection _conicSection;
publicvoidCreateOrbit(GameObject orbitAroundObject, GameObject orbitingObject)
{
_gameManager = GameObject.Find("GameManager").GetComponent<GameManager>();
OrbitingObject = orbitingObject;
OrbitAroundObject = orbitAroundObject;
var isVelocityIncreasing = OrbitingObject.GetComponent<SpaceObject>().IsVelocityIncreasing;
_conicSection = CreateConicSection();
_conicSection.SetConicSectionTransform(transform, OrbitAroundObject.transform.position, OrbitingObject.transform.position, isVelocityIncreasing);
var realWorlTrajectoryPoints = _conicSection.GetPointsInWorldCoordinates(transform);
_lineRenderer = GetComponent<LineRenderer>();
_lineRenderer.positionCount = realWorlTrajectoryPoints.Count;
_lineRenderer.SetPositions(realWorlTrajectoryPoints.ToArray());
}
private IConicSection CreateConicSection()
{
var focusPoint = OrbitAroundObject.transform.position;
var point = OrbitingObject.transform.position;
var distance = Vector3.Distance(point, focusPoint);
var velocity = OrbitingObject.GetComponent<Rigidbody>().velocity;
var mu = _gameManager.GravitationalConstantUnityScaled * OrbitAroundObject.GetComponent<SpaceObject>().Mass;
var semiMajorAxis = Math.Pow(2 / (double)distance - Math.Pow(velocity.magnitude, 2) / mu, -1);
var orbitNormal = -Vector3.Cross(velocity, focusPoint - point);
transform.up = orbitNormal.normalized;
var angularMomentum = orbitNormal.magnitude;
var semiLatusRectum = Math.Pow(angularMomentum, 2) / mu;
var eccentricity = Math.Sqrt(1 - semiLatusRectum / semiMajorAxis);
IConicSection conicSection;
if (IsObjectInEllipticalOrbit())
{
conicSection = new Ellipse(eccentricity, semiMajorAxis, 50);
OrbitTrajectory = OrbitTrajectory.Elliptical;
}
else
{
conicSection = new Hyperbola(eccentricity, semiMajorAxis, 50);
OrbitTrajectory = OrbitTrajectory.Escape;
}
return conicSection;
}
privateboolIsObjectInEllipticalOrbit()
{
var ellipsePointVelocity = OrbitingObject.GetComponent<Rigidbody>().velocity;
var distance = (OrbitAroundObject.transform.position - OrbitingObject.transform.position).magnitude;
var escapeVelocity = OrbitAroundObject.GetComponent<SpaceObject>().GetEscapeVelocity(distance);
return ellipsePointVelocity.magnitude < escapeVelocity;
}
}
It is worth mentioning that all the conics need the eccentricity and semimajor axis to be calculated. These are obtained through simple formulas:
Semi major axis:
\[ a = \frac{1}{\sqrt{\frac{2}{d} - \frac{v^{2}}{\mu} }}\]
\[ \mu = G m\]
where d is the distance between objects, v is the velocity of the orbiting object, G is the gravitational constant and m is the mass of the gravity source.
Eccentricity:
\[ e = \sqrt{1 - \frac{L}{a}}\]
\[ L = \frac{p^{2}}{\mu}\]
where L is the Semi latus Rectum and p is the angular momentum.
One last thing worth mentioning is how I obtain the orbit normal vector. The code is a simplification of the actual operation. For better visibility I will put here the logic used:
usingSystem;
usingSystem.Linq;
usingUnityEngine;
usingstatic Constants;
publicclassSpaceObject : Gravity
{
#region variables [Header("Orbit related")] [SerializeField]private GameObject _orbit;
[Header("Velocity settings")] [SerializeField]private Vector3 _initialDirection = new Vector3(0, 0, 1);
[SerializeField]private InitialVelocity _velocityType = InitialVelocity.Free;
[SerializeField]privatedouble _initialVelocity = 0; // In standard km/s [Header("SpaceObject Properties")] [SerializeField] [Tooltip("external gravity forces do not affect this object")]privatebool _disableGravityAffected = false;
[SerializeField] [Tooltip("Kilograms")]privatedouble _mass = 0; //Kg [SerializeField] [Tooltip("Kilometers")]privatedouble _radius = 0; //km [SerializeField] [Tooltip("Kilometers. Distance at which entry to atmosphere happens")]privatedouble _entryInterfaceDistance = 0; //Km. distance at which entry to atmosphere happensprivate Rigidbody _rigidbody;
private GameObject _orbitAround;
privatebool initializationComplete = false;
[Header("Debug")] [SerializeField]privatedouble _currentVelocity = 0; // In standard km/s#endregion#region Propertiespublicdouble Mass => _mass;
publicdouble CollisionDistance { get => (_radius + _entryInterfaceDistance) / _gameManager.SpaceScaleKm; }
publicdouble CurrentVelocity
{
get { return _currentVelocity * _gameManager.TimeScale / _gameManager.SpaceScaleKm; }
set { _currentVelocity = value * _gameManager.SpaceScaleKm / _gameManager.TimeScale; }
}
publicbool IsVelocityIncreasing { get; privateset; } = true;
#endregionvoidAwake()
{
var spaceObjects = GameObject.FindGameObjectsWithTag("SpaceObject").Where(o => o.gameObject != gameObject).ToArray();
_rigidbody = GetComponent<Rigidbody>();
InitializeGravity(spaceObjects, _mass);
}
voidUpdate()
{
//This code would go on a game controller and affect desired objects.if (Input.GetKeyDown(KeyCode.Space))
{
if (!_disableGravityAffected)
{
SetOrbit();
}
}
}
voidFixedUpdate()
{
try
{
if (!initializationComplete)
{
Initialization();
}
if (!_disableGravityAffected)
{
_rigidbody.velocity += GetExternalNetGravityVector();
}
UpdateVelocityStatus();
}
catch (Exception e)
{
thrownewSystemException("Problem obtaining gravity: " + e);
}
}
privatevoidUpdateVelocityStatus()
{
if (CurrentVelocity != _rigidbody.velocity.magnitude)
{
IsVelocityIncreasing = (CurrentVelocity < _rigidbody.velocity.magnitude) ? true : false;
}
CurrentVelocity = _rigidbody.velocity.magnitude;
}
privatevoidInitialization()
{
_initialVelocity = GetInitialVelocity(_velocityType);
_rigidbody.velocity = _initialDirection.normalized * (float)_initialVelocity;
_initialVelocity = _initialVelocity * _gameManager.SpaceScaleKm / _gameManager.TimeScale;
initializationComplete = true;
}
privatedoubleGetInitialVelocity(InitialVelocity initialVelocityType)
{
if (_spaceObjects.Length == 0)
{
thrownewSystemException("No SpaceObjects found. Need at least 1 to get velocity");
}
_orbitAround = _spaceObjects
.OrderByDescending(o => o.GetComponent<SpaceObject>().GetGravitationalPullForce(
(o.transform.position - transform.position).magnitude))
.First();
var distance = (_orbitAround.transform.position - transform.position).magnitude;
var velocity = 0.0;
switch (initialVelocityType)
{
case InitialVelocity.CircularOrbit:
velocity = _orbitAround.GetComponent<SpaceObject>().GetVelocityForCircularOrbit(distance) + _orbitAround.GetComponent<Rigidbody>().velocity.magnitude;
break;
case InitialVelocity.EscapeOrbit:
velocity = _orbitAround.GetComponent<SpaceObject>().GetEscapeVelocity(distance);
break;
default: //Free velocity
velocity = _initialVelocity * _gameManager.TimeScale / _gameManager.SpaceScaleKm;
break;
}
return velocity;
}
privatevoidSetOrbit()
{
var target = _spaceObjects.First().GetComponent<SpaceObject>();
target.GetOrbitFromCurrentVelocity(gameObject, ref _orbit);
}
}
The code added here only affects the user input to create the orbit. This could easily be in some type of game controller that only affects the desired objects. How this is triggered would be completely project-depended. I decided to keep it here for the sake of simplicity, though it should be somewhere else. As this is global to every object with an Orbit, triggering it will display the orbits of every single object.
Unity setup
The setup to have the orbit working is very simple. We need a child Orbit GameObject to the orbiting object
Add Orbit to SpaceObject script
The orbit object just needs 2 components: A line renderer and the Orbit script:
Components required for the Orbit GameObject
Now that the setup is finished let's view a few GIFs with elliptical and hyperbolic trajectory predictions. The blue line is the object's trail. The brown line is the predicted trajectory.
Circular trajectory prediction
Elliptic trajectory prediction
Hyperbolic trajectory prediction
All these examples are in the XZ plane, to keep it simple, but they work in any other plane.
Conclusions
This is an upgrade from the previous blog entry to add One-Body problem solutions for any single object. The implementation for N-Body problems is more complicated and maybe one day I will look into it.
As with the previous post a sample project can be found on this download link with all the relevant code.
Unity version: 2020.2.7f1
The next post will look into painting a force map using CPU, Computational and Vertex shader.
No comments:
Post a Comment