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