1 // Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details
2 // Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
3 
4 #include "Orbit.h"
5 #include "gameconsts.h"
6 #include "libs.h"
7 
8 #ifdef _MSC_VER
9 #include "win32/WinMath.h"
10 #endif
11 
OrbitalPeriod(double semiMajorAxis,double centralMass)12 double Orbit::OrbitalPeriod(double semiMajorAxis, double centralMass)
13 {
14 	return 2.0 * M_PI * sqrt((semiMajorAxis * semiMajorAxis * semiMajorAxis) / (G * centralMass));
15 }
16 
OrbitalPeriodTwoBody(double semiMajorAxis,double totalMass,double bodyMass)17 double Orbit::OrbitalPeriodTwoBody(double semiMajorAxis, double totalMass, double bodyMass)
18 {
19 	// variable names according to the formula in:
20 	// http://en.wikipedia.org/wiki/Barycentric_coordinates_(astronomy)#Two-body_problem
21 	//
22 	// We have a 2-body orbital system, represented as a gravpoint (at the barycentre),
23 	// plus two bodies, each orbiting that gravpoint.
24 	// We need to compute the orbital period, given the semi-major axis of one body's orbit
25 	// around the gravpoint, the total mass of the system, and the mass of the body.
26 	//
27 	// According to Kepler, the orbital period P is defined by:
28 	//
29 	// P = 2*pi * sqrt( a**3 / G*(M1 + M2) )
30 	//
31 	// where a is the semi-major axis of the orbit, M1 is the mass of the primary and M2 is
32 	// the mass of the secondary. But we don't have that semi-major axis value, we have the
33 	// the semi-major axis for the orbit of the secondary around the gravpoint, instead.
34 	//
35 	// So, this first computes the semi-major axis of the secondary's orbit around the primary,
36 	// and then uses the above formula to compute the orbital period.
37 	const double r1 = semiMajorAxis;
38 	const double m2 = (totalMass - bodyMass);
39 	const double a = r1 * totalMass / m2;
40 	const double a3 = a * a * a;
41 	return 2.0 * M_PI * sqrt(a3 / (G * totalMass));
42 }
43 
calc_velocity_area_per_sec(double semiMajorAxis,double centralMass,double eccentricity)44 static double calc_velocity_area_per_sec(double semiMajorAxis, double centralMass, double eccentricity)
45 {
46 	const double a2 = semiMajorAxis * semiMajorAxis;
47 	const double e2 = eccentricity * eccentricity;
48 	return M_PI * a2 * sqrt((eccentricity < 1.0) ? (1 - e2) : (e2 - 1.0)) / Orbit::OrbitalPeriod(semiMajorAxis, centralMass);
49 }
50 
calc_velocity_area_per_sec_gravpoint(double semiMajorAxis,double totalMass,double bodyMass,double eccentricity)51 static double calc_velocity_area_per_sec_gravpoint(double semiMajorAxis, double totalMass, double bodyMass, double eccentricity)
52 {
53 	const double a2 = semiMajorAxis * semiMajorAxis;
54 	const double e2 = eccentricity * eccentricity;
55 	return M_PI * a2 * sqrt((eccentricity < 1.0) ? (1 - e2) : (e2 - 1.0)) / Orbit::OrbitalPeriodTwoBody(semiMajorAxis, totalMass, bodyMass);
56 }
57 
calc_position_from_mean_anomaly(const double M,const double e,const double a,double & cos_v,double & sin_v,double * r)58 static void calc_position_from_mean_anomaly(const double M, const double e, const double a, double &cos_v, double &sin_v, double *r)
59 {
60 	// M is mean anomaly
61 	// e is eccentricity
62 	// a is semi-major axis
63 
64 	cos_v = 0.0;
65 	sin_v = 0.0;
66 	if (r) {
67 		*r = 0.0;
68 	}
69 
70 	if (e < 1.0) { // elliptic orbit
71 		// eccentric anomaly
72 		// NR method to solve for E: M = E-e*sin(E)  {Kepler's equation}
73 		double E = M;
74 		int iter;
75 		for (iter = 0; iter < 10; iter++) {
76 			double dE = (E - e * (sin(E)) - M) / (1.0 - e * cos(E));
77 			E = E - dE;
78 			if (fabs(dE) < 0.0001) break;
79 		}
80 		// method above sometimes can't find the solution
81 		// especially when e approaches 1
82 		if (iter == 10) { // most likely no solution found
83 			//failsafe to bisection method
84 			//max(E - M) == 1, so safe interval is M+-1.1
85 			double Emin = M - 1.1;
86 			double Emax = M + 1.1;
87 			double Ymin = Emin - e * sin(Emin) - M;
88 			double Y;
89 			for (int i = 0; i < 14; i++) { // 14 iterations for precision 0.00006
90 				E = (Emin + Emax) / 2;
91 				Y = E - e * sin(E) - M;
92 				if ((Ymin * Y) < 0) {
93 					Emax = E;
94 				} else {
95 					Ymin = Y;
96 					Emin = E;
97 				}
98 			}
99 		}
100 
101 		// true anomaly (angle of orbit position)
102 		cos_v = (cos(E) - e) / (1.0 - e * cos(E));
103 		sin_v = (sqrt(1.0 - e * e) * sin(E)) / (1.0 - e * cos(E));
104 
105 		// heliocentric distance
106 		if (r) {
107 			*r = a * (1.0 - e * cos(E));
108 		}
109 
110 	} else { // parabolic or hyperbolic orbit
111 		// eccentric anomaly
112 		// NR method to solve for E: M = E-sinh(E)
113 		// sinh E and cosh E are solved directly, because of inherent numerical instability of tanh(k arctanh x)
114 		double sh = 2.0;
115 		for (int iter = 50; iter > 0; --iter) {
116 			double d_sh = (M + e * sh - asinh(sh)) / (e - 1 / sqrt(1 + (sh * sh)));
117 			sh = sh - d_sh;
118 			if (fabs(d_sh) < 0.0001) break;
119 		}
120 
121 		double ch = sqrt(1 + sh * sh);
122 
123 		// true anomaly (angle of orbit position)
124 		cos_v = (ch - e) / (1.0 - e * ch);
125 		sin_v = (sqrt(e * e - 1.0) * sh) / (e * ch - 1.0);
126 
127 		if (r) { // heliocentric distance
128 			*r = a * (e * ch - 1.0);
129 		}
130 	}
131 }
132 
TrueAnomalyFromMeanAnomaly(double MeanAnomaly) const133 double Orbit::TrueAnomalyFromMeanAnomaly(double MeanAnomaly) const
134 {
135 	double cos_v, sin_v;
136 	calc_position_from_mean_anomaly(MeanAnomaly, m_eccentricity, m_semiMajorAxis, cos_v, sin_v, 0);
137 	return atan2(sin_v, cos_v);
138 }
139 
MeanAnomalyFromTrueAnomaly(double trueAnomaly) const140 double Orbit::MeanAnomalyFromTrueAnomaly(double trueAnomaly) const
141 {
142 	double M_t0;
143 	const double e = m_eccentricity;
144 	if (e < 1.0) {
145 		M_t0 = 2.0 * atan(tan(trueAnomaly / 2.0) * sqrt((1.0 - e) / (1.0 + e)));
146 		M_t0 = M_t0 - e * sin(M_t0);
147 	} else {
148 		// For hyperbolic trajectories, mean anomaly has opposite sign to true anomaly, therefore trajectories which go forward
149 		// in time decrease their true anomaly. Yes, it is confusing.
150 		M_t0 = 2.0 * atanh(tan(trueAnomaly / 2.0) * sqrt((e - 1.0) / (1.0 + e)));
151 		M_t0 = M_t0 - e * sinh(M_t0);
152 	}
153 
154 	return M_t0;
155 }
156 
MeanAnomalyAtTime(double time) const157 double Orbit::MeanAnomalyAtTime(double time) const
158 {
159 	const double e = m_eccentricity;
160 	if (e < 1.0) { // elliptic orbit
161 		return 2.0 * M_PI * time / Period() + m_orbitalPhaseAtStart;
162 	} else {
163 		return -2.0 * time * m_velocityAreaPerSecond / (m_semiMajorAxis * m_semiMajorAxis * sqrt(e * e - 1)) + m_orbitalPhaseAtStart;
164 	}
165 }
166 
OrbitalPosAtTime(double t) const167 vector3d Orbit::OrbitalPosAtTime(double t) const
168 {
169 	if (is_zero_general(m_semiMajorAxis)) return m_positionForStaticBody;
170 	double cos_v, sin_v, r;
171 	calc_position_from_mean_anomaly(MeanAnomalyAtTime(t), m_eccentricity, m_semiMajorAxis, cos_v, sin_v, &r);
172 	return m_orient * vector3d(-cos_v * r, sin_v * r, 0);
173 }
174 
OrbitalTimeAtPos(const vector3d & pos,double centralMass) const175 double Orbit::OrbitalTimeAtPos(const vector3d &pos, double centralMass) const
176 {
177 	double c = m_eccentricity * m_semiMajorAxis;
178 	matrix3x3d matrixInv = m_orient.Inverse();
179 	vector3d approx3dPos = (matrixInv * pos - vector3d(c, 0., 0.)).Normalized();
180 
181 	double cos_v = -vector3d(1., 0., 0.).Dot(approx3dPos);
182 	double sin_v = std::copysign(vector3d(1., 0., 0.).Cross(approx3dPos).Length(), approx3dPos.y);
183 
184 	double cos_E = (cos_v + m_eccentricity) / (1. + m_eccentricity * cos_v);
185 	double E;
186 	double meanAnomaly;
187 	if (m_eccentricity <= 1.) {
188 		E = std::acos(cos_E);
189 		if (sin_v < 0)
190 			E *= -1.;
191 		meanAnomaly = E - m_eccentricity * std::sin(E);
192 	} else {
193 		E = std::acosh(cos_E);
194 		if (sin_v < 0)
195 			E *= -1.;
196 		meanAnomaly = E - m_eccentricity * std::sinh(E);
197 	}
198 
199 	if (m_eccentricity <= 1.) {
200 		meanAnomaly -= m_orbitalPhaseAtStart;
201 		while (meanAnomaly < 0)
202 			meanAnomaly += 2. * M_PI;
203 	} else if (meanAnomaly < 0.)
204 		meanAnomaly += m_orbitalPhaseAtStart;
205 
206 	if (m_eccentricity <= 1.)
207 		return meanAnomaly * Period() / (2. * M_PI);
208 	else if (meanAnomaly < 0.)
209 		return -meanAnomaly * std::sqrt(std::pow(m_semiMajorAxis, 3) / (G * centralMass));
210 	else
211 		return -std::fabs(meanAnomaly + m_orbitalPhaseAtStart) * std::sqrt(std::pow(m_semiMajorAxis, 3) / (G * centralMass));
212 }
213 
OrbitalVelocityAtTime(double totalMass,double t) const214 vector3d Orbit::OrbitalVelocityAtTime(double totalMass, double t) const
215 {
216 	double cos_v, sin_v, r;
217 	calc_position_from_mean_anomaly(MeanAnomalyAtTime(t), m_eccentricity, m_semiMajorAxis, cos_v, sin_v, &r);
218 
219 	double mi = G * totalMass;
220 	double p;
221 	if (m_eccentricity <= 1.)
222 		p = (1. - m_eccentricity * m_eccentricity) * m_semiMajorAxis;
223 	else
224 		p = (m_eccentricity * m_eccentricity - 1.) * m_semiMajorAxis;
225 
226 	double h = std::sqrt(mi / p);
227 
228 	return m_orient * vector3d(h * sin_v, h * (m_eccentricity + cos_v), 0);
229 }
230 
231 // used for stepping through the orbit in small fractions
232 // mean anomaly <-> true anomaly conversion doesn't have
233 // to be taken into account
EvenSpacedPosTrajectory(double t,double timeOffset) const234 vector3d Orbit::EvenSpacedPosTrajectory(double t, double timeOffset) const
235 {
236 	const double e = m_eccentricity;
237 	double v = 2 * M_PI * t + TrueAnomalyFromMeanAnomaly(MeanAnomalyAtTime(timeOffset));
238 	double r;
239 
240 	if (e < 1.0) {
241 		r = m_semiMajorAxis * (1 - e * e) / (1 + e * cos(v));
242 	} else {
243 		r = m_semiMajorAxis * (e * e - 1) / (1 + e * cos(v));
244 
245 		// planet is in infinity
246 		const double ac = acos(-1 / e);
247 		if (v <= -ac) {
248 			v = -ac + 0.0001;
249 			r = 100.0 * AU;
250 		}
251 		if (v >= ac) {
252 			v = ac - 0.0001;
253 			r = 100.0 * AU;
254 		}
255 	}
256 
257 	return m_orient * vector3d(-cos(v) * r, sin(v) * r, 0);
258 }
259 
Period() const260 double Orbit::Period() const
261 {
262 	if (m_eccentricity < 1 && m_eccentricity >= 0) {
263 		return M_PI * m_semiMajorAxis * m_semiMajorAxis * sqrt(1 - m_eccentricity * m_eccentricity) / m_velocityAreaPerSecond;
264 	} else { // hyperbola.. period makes no sense, should not be used
265 		assert(0);
266 		return 0;
267 	}
268 }
269 
Apogeum() const270 vector3d Orbit::Apogeum() const
271 {
272 	if (m_eccentricity < 1) {
273 		return m_semiMajorAxis * (1 + m_eccentricity) * (m_orient * vector3d(1, 0, 0));
274 	} else {
275 		return vector3d(0, 0, 0);
276 	}
277 }
278 
Perigeum() const279 vector3d Orbit::Perigeum() const
280 {
281 	if (m_eccentricity < 1) {
282 		return m_semiMajorAxis * (1 - m_eccentricity) * (m_orient * vector3d(-1, 0, 0));
283 	} else {
284 		return m_semiMajorAxis * (m_eccentricity - 1) * (m_orient * vector3d(-1, 0, 0));
285 	}
286 }
287 
SetShapeAroundBarycentre(double semiMajorAxis,double totalMass,double bodyMass,double eccentricity)288 void Orbit::SetShapeAroundBarycentre(double semiMajorAxis, double totalMass, double bodyMass, double eccentricity)
289 {
290 	m_semiMajorAxis = semiMajorAxis;
291 	m_eccentricity = eccentricity;
292 	m_velocityAreaPerSecond = calc_velocity_area_per_sec_gravpoint(semiMajorAxis, totalMass, bodyMass, eccentricity);
293 }
294 
SetShapeAroundPrimary(double semiMajorAxis,double centralMass,double eccentricity)295 void Orbit::SetShapeAroundPrimary(double semiMajorAxis, double centralMass, double eccentricity)
296 {
297 	m_semiMajorAxis = semiMajorAxis;
298 	m_eccentricity = eccentricity;
299 	m_velocityAreaPerSecond = calc_velocity_area_per_sec(semiMajorAxis, centralMass, eccentricity);
300 }
301 
ForStaticBody(const vector3d & position)302 Orbit Orbit::ForStaticBody(const vector3d &position)
303 {
304 	Orbit ret;
305 	// just remember the current position of the body, and we will return it, for any t
306 	ret.m_positionForStaticBody = position;
307 	return ret;
308 }
309 
FromBodyState(const vector3d & pos,const vector3d & vel_raw,double centralMass)310 Orbit Orbit::FromBodyState(const vector3d &pos, const vector3d &vel_raw, double centralMass)
311 {
312 	Orbit ret;
313 
314 	// standard gravitational parameter
315 	const double u = centralMass * G;
316 
317 	// maybe we will adjust the speed a little now
318 	vector3d vel = vel_raw;
319 	// angular momentum
320 	vector3d ang = pos.Cross(vel);
321 	// quite a rare case - the speed is directed strictly to the star or away from the star
322 	// let's make a small disturbance to the velocity, so as not to calculate the radial orbit
323 	if (is_zero_general(ang.LengthSqr()) && !is_zero_general(centralMass)) {
324 		if (is_zero_general(pos.x) && is_zero_general(pos.y)) // even rarer case, the body lies strictly on the z-axis
325 			vel.x += 0.001;
326 		else
327 			vel.z += 0.001;
328 		ang = pos.Cross(vel); // recalculate angular momentum
329 	}
330 
331 	const double r_now = pos.Length();
332 
333 	const double LLSqr = ang.LengthSqr();
334 
335 	// total energy
336 	const double EE = vel.LengthSqr() / 2.0 - u / r_now;
337 
338 	if (is_zero_general(centralMass) || is_zero_general(EE) || (ang.z * ang.z / LLSqr > 1.0))
339 		return Orbit::ForStaticBody(pos);
340 
341 	// http://en.wikipedia.org/wiki/Orbital_eccentricity
342 	ret.m_eccentricity = 1 + 2 * EE * LLSqr / (u * u);
343 	if (ret.m_eccentricity < 0.0) ret.m_eccentricity = 0.0;
344 	ret.m_eccentricity = sqrt(ret.m_eccentricity);
345 	//avoid parabola
346 	if (ret.m_eccentricity < 1.0001 && ret.m_eccentricity >= 1) ret.m_eccentricity = 1.0001;
347 	if (ret.m_eccentricity > 0.9999 && ret.m_eccentricity < 1) ret.m_eccentricity = 0.9999;
348 
349 	// lines represent these quantities:
350 	// 		(e M G)^2
351 	// 		M G (e - 1) / 2 EE, always positive (EE and (e-1) change sign
352 	// 		M G / 2 EE,
353 	// which is a (http://en.wikipedia.org/wiki/Semi-major_axis); a of hyperbola is taken as positive here
354 	ret.m_semiMajorAxis = 2 * EE * LLSqr + u * u;
355 	if (ret.m_semiMajorAxis < 0) ret.m_semiMajorAxis = 0;
356 	ret.m_semiMajorAxis = (sqrt(ret.m_semiMajorAxis) - u) / (2 * EE);
357 	ret.m_semiMajorAxis = ret.m_semiMajorAxis / fabs(1.0 - ret.m_eccentricity);
358 
359 	// clipping of the eccentricity leads to a strong decrease in the semimajor axis.
360 	// at low speed, since the ship is almost in the apocenter, semimajor axis should be
361 	// almost equal to half distance to the star (no less that's for sure)
362 	if (ret.m_eccentricity < 1 && ret.m_semiMajorAxis < r_now / 2) ret.m_semiMajorAxis = r_now / 2;
363 
364 	// The formulas for rotation matrix were derived based on following assumptions:
365 	//	1. Trajectory follows Kepler's law and vector {-r cos(v), -r sin(v), 0}, r(t) and v(t) are parameters.
366 	//	2. Correct transformation must transform {0,0,LL} to ang and {-r_now cos(orbitalPhaseAtStart), -r_now sin(orbitalPhaseAtStart), 0} to pos.
367 	//  3. orbitalPhaseAtStart (=offset) is calculated from r = a ((e^2 - 1)/(1 + e cos(v) ))
368 	double off = 0;
369 
370 	if (ret.m_eccentricity < 1) {
371 		off = ret.m_semiMajorAxis * (1 - ret.m_eccentricity * ret.m_eccentricity) - r_now;
372 	} else {
373 		off = ret.m_semiMajorAxis * (-1 + ret.m_eccentricity * ret.m_eccentricity) - r_now;
374 	}
375 
376 	// correct sign of offset is given by sign pos.Dot(vel) (heading towards apohelion or perihelion?]
377 	off = Clamp(off / (r_now * ret.m_eccentricity), -1.0, 1.0);
378 	off = -pos.Dot(vel) / fabs(pos.Dot(vel)) * acos(off);
379 
380 	//much simpler and satisfies the specified conditions
381 	//and does not have unstable places (almost almost)
382 	vector3d b1 = -pos.Normalized(); //x
383 	vector3d b2 = -ang.Normalized(); //z
384 	ret.m_orient = matrix3x3d::FromVectors(b1, b2.Cross(b1), b2) * matrix3x3d::RotateZ(-off).Transpose();
385 
386 	ret.m_velocityAreaPerSecond = calc_velocity_area_per_sec(ret.m_semiMajorAxis, centralMass, ret.m_eccentricity);
387 
388 	ret.m_orbitalPhaseAtStart = ret.MeanAnomalyFromTrueAnomaly(-off);
389 
390 	return ret;
391 }
392