Real-Time Orbits: Building a Starlink Simulation in Unity
This project started as a simple curiosity. I wanted to see if I could simulate satellites in orbit using real physics inside Unity. I decided to avoid any pre-baked animations or simplified approximations, focusing instead on direct orbital math. The result is a working orbital simulation that runs entirely in real time. It models the Starlink constellation orbiting a rotating Earth, complete with accurate lighting, timing, and telemetry.
“Every dot on screen represents an actual satellite following a computed Keplerian orbit. Nothing in this scene is animated by hand.”

A real-time view of Starlink satellites orbiting a physically rotating Earth with accurate sunlight direction.
Simulating Orbits with Real Physics
The foundation of this simulation is built on Kepler’s laws of motion. Each satellite’s position is calculated in real time using orbital elements such as altitude, inclination, eccentricity, right ascension, and mean anomaly. Nothing is cached or precomputed. Every frame, Unity reevaluates each orbit using these parameters, ensuring true physical accuracy.
To determine a satellite’s position, the code solves Kepler’s equation iteratively. This equation relates mean anomaly (M), eccentric anomaly (E), and eccentricity (e) through a nonlinear relationship:
M = E - e * sin(E)
E_{n+1} = E_n - (E_n - e * sin(E_n) - M) / (1 - e * cos(E_n))Once the true anomaly is computed, the satellite’s position is rotated into the Earth-centered inertial frame, then converted into the Earth-fixed frame for rendering. Unity handles the rendering, orbit trails, and motion interpolation on the GPU.
Building Starlink’s Constellation
The Starlink network is composed of several structured orbital shells, each with its own altitude, inclination, and number of satellites per plane. This structure ensures global coverage and collision-free spacing. I defined each shell in code as follows:
new Shell(altitudeKm: 550f, inclination: 53f, planes: 24, satsPerPlane: 22); new Shell(altitudeKm: 570f, inclination: 70f, planes: 12, satsPerPlane: 20); new Shell(altitudeKm: 1200f, inclination: 97.6f, planes: 6, satsPerPlane: 18);
Each shell generates a full ring of orbits distributed around the planet. The simulation offsets each plane’s right ascension so satellites never overlap. When all shells are combined, they create a realistic multi-layered constellation that visually matches Starlink’s real orbital design.
Earth, Sunlight, and Sidereal Time
The Earth’s rotation is synchronized with real sidereal time, which completes one rotation every 23 hours and 56 minutes. Unlike solar time, this rate is based on Earth’s rotation relative to the stars rather than the Sun. It ensures that satellites appear to move naturally relative to the surface, without long-term drift.
The sunlight direction is calculated from the Sun’s apparent position using UTC time. This allows the day-night terminator to move correctly across the globe. The system even supports offsets for texture alignment and lighting validation so that the subsolar point matches real-world data.
The cloud layer rotates independently of the surface, creating a subtle parallax motion that gives the planet a more dynamic appearance, especially during slow cinematic camera passes.
Time Control and Synchronization
The entire system runs on a shared simulation clock that drives all major components. The Earth’s rotation, Sun orientation, and orbital propagation all use the same time reference. This ensures that lighting, motion, and telemetry remain perfectly synchronized regardless of playback speed.
The simulation can be accelerated, slowed, or paused entirely. The TimeHUD displays both real UTC time and the simulation offset, giving a clear indication of how far ahead or behind the simulation is from the current real-world time.
Telemetry and Interaction
Each satellite can be clicked to view real-time telemetry values such as altitude, velocity, and phase angle. These numbers come directly from the live orbital calculations each frame, not from placeholders or approximations. It is a great way to confirm the physics are working as intended.
The camera can smoothly focus on any satellite, transitioning between global and local perspectives with ease. It adds a layer of immersion, allowing you to zoom in and watch orbital motion close up or pull back to see the full constellation geometry.
What Comes Next
The system is flexible enough to visualize any orbital dataset. Adding constellations like GPS, Galileo, or the ISS only requires loading new orbital parameters. Future plans include debris tracking, launch trajectory visualization, and simulated ground-station communication links.
My goal is to make orbital mechanics something that anyone can explore visually. The math becomes easier to understand when you can see it moving in front of you, orbit by orbit.
Development Notes
I built most of this over two days, learning orbital mechanics as I went. The hardest part was making Earth’s rotation and the sunlight direction agree on what “real time” means. Tiny differences between UTC, solar time, and sidereal time created visible drift in the day-night line. Debugging that required visualizing the subsolar point and comparing it with online ephemeris data.
Another challenge was figuring out coordinate transforms. Converting from orbital elements to Earth-fixed coordinates requires several rotations, and it is easy to mix up ascending nodes or rotation orders. I ended up plotting debug vectors in Unity and watching them move over time until the orbits finally behaved correctly.
On the performance side, I learned that GPU instancing is not just an optimization, it is essential. Without it, hundreds of satellites would choke the CPU. The compute shader approach for orbit trails kept things smooth even at high simulation speeds.
The most satisfying moment came when everything lined up. The Earth rotated at the right rate, the light matched the real position of the Sun, and the orbits looked alive. For me, this project was as much about understanding spaceflight as it was about writing code.
Source Code
These are the main scripts that power the simulation. Expand any file to view it.
EarthRotation.csSidereal spin with GMST orientation
using System;
using UnityEngine;
/// Rotates Earth at the correct sidereal rate and optionally matches real world
/// orientation using GMST computed from the simulation UTC clock.
public class EarthRotation : MonoBehaviour
{
public enum Mode { SiderealRateFromSim, RealWorldUTC_GMST }
[Header("Rotation Mode")]
public Mode rotationMode = Mode.RealWorldUTC_GMST;
[Header("Axis & Offsets")]
public Vector3 rotationAxis = Vector3.up;
[Tooltip("Rotate so your texture's prime meridian (0°E) faces +Z at angle=0. Adjust for your UVs.")]
[Range(-180f, 180f)] public float primeMeridianLocalOffsetDeg = 0f;
const double SIDEREAL_DAY_SEC = 86164.0905;
void Update()
{
bool hasClock = OrbitSimulationManager.Instance != null;
if (rotationMode == Mode.SiderealRateFromSim || !hasClock)
{
float dt = hasClock ? OrbitSimulationManager.Instance.ScaledDeltaTime : Time.deltaTime;
float degPerSec = (float)(360.0 / SIDEREAL_DAY_SEC);
transform.Rotate(rotationAxis, degPerSec * dt, Space.Self);
return;
}
DateTime utc = OrbitSimulationManager.Instance.NowUtc;
double gmstDeg = GMSTDegrees(utc);
float angle = (float)(gmstDeg + primeMeridianLocalOffsetDeg);
transform.localRotation = Quaternion.AngleAxis(angle, rotationAxis.normalized);
}
static double GMSTDegrees(DateTime utc)
{
double jd = ToJulianDate(utc);
double T = (jd - 2451545.0) / 36525.0;
double GMST_sec = 24110.54841
+ 8640184.812866 * T
+ 0.093104 * T * T
- 6.2e-6 * T * T * T;
DateTime utc0 = new DateTime(utc.Year, utc.Month, utc.Day, 0, 0, 0, DateTimeKind.Utc);
double secondsSince0h = (utc - utc0).TotalSeconds;
GMST_sec += 1.00273790935 * secondsSince0h;
GMST_sec %= 86400.0; if (GMST_sec < 0) GMST_sec += 86400.0;
return (GMST_sec / 86400.0) * 360.0;
}
static double ToJulianDate(DateTime dtUtc)
{
int Y = dtUtc.Year, M = dtUtc.Month;
double D = dtUtc.Day + (dtUtc.Hour + (dtUtc.Minute + dtUtc.Second / 60.0) / 60.0) / 24.0;
if (M <= 2) { Y -= 1; M += 12; }
int A = Y / 100; int B = 2 - A + A / 4;
return Math.Floor(365.25 * (Y + 4716))
+ Math.Floor(30.6001 * (M + 1))
+ D + B - 1524.5;
}
}SunDirectionalLight.csAccurate Sun direction from UTC
using System;
using UnityEngine;
/// Aim the Directional Light from the Sun toward Earth for the sim UTC.
/// Set invertLight = true if your scene shows day/night flipped.
public class SunDirectionalLight : MonoBehaviour
{
public Light sunLight;
public Transform earth;
[Tooltip("Flip this if day/night looks inverted in your pipeline.")]
public bool invertLight = true;
public bool showDebug = false;
void Update()
{
if (!sunLight || !earth) return;
DateTime utc = OrbitSimulationManager.Instance
? OrbitSimulationManager.Instance.NowUtc
: DateTime.UtcNow;
double jd = ToJD(utc);
double T = (jd - 2451545.0) / 36525.0;
double L0 = 280.466 + 36000.770 * T;
double M = 357.528 + 35999.050 * T;
double lambda = L0 + 1.915 * Math.Sin(M * Mathf.Deg2Rad) + 0.020 * Math.Sin(2 * M * Mathf.Deg2Rad);
double eps = 23.4393 - 0.0000004 * T;
double ra = Math.Atan2(Math.Cos(eps * Mathf.Deg2Rad) * Math.Sin(lambda * Mathf.Deg2Rad),
Math.Cos(lambda * Mathf.Deg2Rad));
if (ra < 0) ra += 2 * Math.PI;
double dec = Math.Asin(Math.Sin(eps * Mathf.Deg2Rad) * Math.Sin(lambda * Mathf.Deg2Rad));
double D = jd - 2451545.0;
double GMSTdeg = (280.46061837 + 360.98564736629 * D) % 360.0;
double theta = GMSTdeg * Mathf.Deg2Rad;
double H = theta - ra;
Vector3 sunECEF = new(
(float)(Math.Cos(dec) * Math.Cos(H)),
(float)(Math.Sin(dec)),
(float)(Math.Cos(dec) * Math.Sin(H))
);
Vector3 mapped = new Vector3(sunECEF.z, sunECEF.y, sunECEF.x).normalized; // ECEF to Unity mapping
Vector3 world = earth.TransformDirection(mapped);
Vector3 look = invertLight ? world : -world; // flip if your scene needs it
sunLight.transform.rotation = Quaternion.LookRotation(look, Vector3.up);
if (showDebug)
{
double subLat = dec * Mathf.Rad2Deg;
double subLon = (ra - theta) * Mathf.Rad2Deg;
if (subLon > 180) subLon -= 360;
Debug.Log($"Subsolar lat {subLat:F2}°, lon {subLon:F2}° UTC {utc:yyyy-MM-dd HH:mm:ss}Z");
}
}
static double ToJD(DateTime utc)
{
int Y = utc.Year, M = utc.Month;
double D = utc.Day + (utc.Hour + (utc.Minute + utc.Second / 60.0) / 60.0) / 24.0;
if (M <= 2) { Y--; M += 12; }
int A = Y / 100; int B = 2 - A + A / 4;
return Math.Floor(365.25 * (Y + 4716)) + Math.Floor(30.6001 * (M + 1)) + D + B - 1524.5;
}
}KeplerOrbit3D.csPropagates satellite positions in world space
using UnityEngine;
/// Realistic 3D orbit simulation using 6 Keplerian elements.
/// Works in world space. 1 unit = 1000 km by default.
public class KeplerOrbit3D : MonoBehaviour
{
[Header("References")]
public Transform planet;
public Transform solarSystemRoot;
[Header("Orbital Elements (units, deg, seconds)")]
public float semiMajorAxis = 6.921f;
[Range(0f, 0.99f)] public float eccentricity = 0.001f;
public float inclination = 53f;
public float raan = 0f;
public float argPerigee = 0f;
public float period = 5730f;
public float meanAnomaly0 = 0f;
[Header("Scaling")]
public bool respectRootScale = true;
bool warned;
void Start()
{
if (planet != null && ValidOrbit())
{
Vector3 worldPos = ComputeWorldPosition(GetSimulationTime());
transform.position = planet.position + worldPos;
}
}
void Update()
{
if (planet == null) return;
if (!ValidOrbit())
{
if (!warned)
{
Debug.LogError($"[{name}] Invalid orbit: a={semiMajorAxis}, T={period}. Check spawner and inputs.");
warned = true;
}
return;
}
Vector3 worldPos = ComputeWorldPosition(GetSimulationTime());
transform.position = planet.position + worldPos;
}
float GetSimulationTime()
{
return (OrbitSimulationManager.Instance != null)
? OrbitSimulationManager.Instance.SimulationTime
: Time.time;
}
bool ValidOrbit()
{
return semiMajorAxis > 0f && period > 0f &&
float.IsFinite(semiMajorAxis) && float.IsFinite(period);
}
Vector3 ComputeWorldPosition(float simTime)
{
float n = 2f * Mathf.PI / Mathf.Max(period, 0.0001f);
float M = Mathf.Deg2Rad * meanAnomaly0 + n * simTime;
M = Mathf.Repeat(M, 2f * Mathf.PI);
float E = SolveEccentricAnomaly(M, eccentricity);
float nu = 2f * Mathf.Atan2(
Mathf.Sqrt(1 + eccentricity) * Mathf.Sin(E / 2f),
Mathf.Sqrt(1 - eccentricity) * Mathf.Cos(E / 2f)
);
float r = semiMajorAxis * (1 - eccentricity * Mathf.Cos(E));
Vector3 posOrbital = new Vector3(r * Mathf.Cos(nu), 0f, r * Mathf.Sin(nu));
float scaleFactor = 1f;
if (respectRootScale)
{
if (solarSystemRoot != null)
scaleFactor = solarSystemRoot.lossyScale.x;
else if (planet != null && planet.parent != null)
scaleFactor = planet.parent.lossyScale.x;
}
posOrbital *= scaleFactor;
Quaternion rotation =
Quaternion.Euler(0f, raan, 0f) *
Quaternion.Euler(inclination, 0f, 0f) *
Quaternion.Euler(0f, argPerigee, 0f);
return rotation * posOrbital;
}
public float SolveEccentricAnomaly(float M, float e, int maxIter = 12)
{
float E = M;
for (int i = 0; i < maxIter; i++)
{
float f = E - e * Mathf.Sin(E) - M;
float fp = 1 - e * Mathf.Cos(E);
E -= f / Mathf.Max(1e-6f, fp);
}
return E;
}
}OrbitalMath.csUnits and orbital helpers
using UnityEngine;
public static class OrbitalMath
{
public const float EarthRadiusKm = 6371f;
public const float MuEarthKm = 398600.4418f;
public const float UnitToKm = 1000f;
public static float SemiMajorAxisUnitsFromAltitudeKm(float altitudeKm)
{
float aKm = EarthRadiusKm + altitudeKm;
return aKm / UnitToKm;
}
public static float PeriodSecondsFromAltitudeKm(float altitudeKm)
{
float aKm = EarthRadiusKm + altitudeKm;
return 2f * Mathf.PI * Mathf.Sqrt((aKm * aKm * aKm) / MuEarthKm);
}
}OrbitSimulationManager.csGlobal simulation clock
using System;
using UnityEngine;
/// Centralized simulation clock with a UTC epoch.
public class OrbitSimulationManager : MonoBehaviour
{
public static OrbitSimulationManager Instance { get; private set; }
[Header("Time Controls")]
public float timeScale = 1f;
public bool startPaused = false;
[Header("UTC Epoch")]
public bool startFromSystemUtc = true;
public DateTime customEpochUtc = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public float SimulationTime { get; private set; }
public float ScaledDeltaTime { get; private set; }
public bool IsPaused { get; private set; }
public DateTime EpochUtc { get; private set; }
void Awake()
{
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
Instance = this;
IsPaused = startPaused;
SimulationTime = 0f;
ScaledDeltaTime = 0f;
EpochUtc = startFromSystemUtc ? DateTime.UtcNow : DateTime.SpecifyKind(customEpochUtc, DateTimeKind.Utc);
}
void Update()
{
if (IsPaused) { ScaledDeltaTime = 0f; return; }
ScaledDeltaTime = Mathf.Max(0f, Time.deltaTime * timeScale);
SimulationTime += ScaledDeltaTime;
}
public void SetTimeScale(float newScale) => timeScale = Mathf.Max(0f, newScale);
public void TogglePause() => IsPaused = !IsPaused;
public void ResetTime(float toSeconds = 0f) => SimulationTime = Mathf.Max(0f, toSeconds);
public DateTime NowUtc => EpochUtc.AddSeconds(SimulationTime);
}ConstellationSpawner.csStarlink shells and satellite instancing
using UnityEngine;
public class ConstellationSpawner : MonoBehaviour
{
public enum ConstellationType { Custom, Starlink, GPS, Galileo }
[Header("Mode")]
public ConstellationType constellationType = ConstellationType.Starlink;
[Header("References")]
public Transform planet;
public Transform solarSystemRoot;
public GameObject satellitePrefab;
[Header("Custom Constellation Settings (km)")]
public int orbitCount = 3;
public int planesPerShell = 6;
public int satellitesPerPlane = 8;
public float baseAltitudeKm = 550f;
public float altitudeSpacingKm = 200f;
public float baseInclination = 50f;
public float inclinationStep = 10f;
[Header("Satellite Visuals")]
public float baseSatelliteScale = 1f;
public bool addGlowLight = true;
public Color glowColor = Color.yellow;
[Header("Performance")]
public int maxOrbitPaths = 50;
void Start()
{
if (planet == null || satellitePrefab == null)
{
Debug.LogError("ConstellationSpawner: Missing planet or satellite prefab");
return;
}
if (solarSystemRoot == null && planet.parent != null)
solarSystemRoot = planet.parent;
ConstellationPreset preset = GetPreset();
int satelliteCount = 0;
for (int shell = 0; shell < preset.shells.Length; shell++)
{
Shell shellData = preset.shells[shell];
for (int plane = 0; plane < shellData.planes; plane++)
{
float raan = (360f / shellData.planes) * plane;
for (int satIndex = 0; satIndex < shellData.satsPerPlane; satIndex++)
{
float meanAnomaly = (360f / shellData.satsPerPlane) * satIndex + Random.Range(-5f, 5f);
GameObject sat = Instantiate(satellitePrefab, transform);
sat.name = $"{preset.name}-S{shell:D2}P{plane:D2}-{satIndex:D4}";
KeplerOrbit3D orbit = sat.GetComponent<KeplerOrbit3D>();
orbit.planet = planet;
orbit.solarSystemRoot = solarSystemRoot;
orbit.semiMajorAxis = OrbitalMath.SemiMajorAxisUnitsFromAltitudeKm(shellData.altitudeKm);
orbit.eccentricity = 0.001f;
orbit.inclination = shellData.inclination;
orbit.raan = raan;
orbit.argPerigee = 0f;
orbit.period = OrbitalMath.PeriodSecondsFromAltitudeKm(shellData.altitudeKm);
orbit.meanAnomaly0 = meanAnomaly;
if (satelliteCount < maxOrbitPaths)
{
var path = sat.AddComponent<OrbitPathRenderer>();
path.orbit = orbit;
path.segments = 180;
}
if (addGlowLight)
{
Light glow = sat.AddComponent<Light>();
glow.type = LightType.Point;
glow.color = glowColor;
glow.intensity = 1.2f;
glow.range = 2f * (solarSystemRoot ? solarSystemRoot.lossyScale.x : 1f);
glow.shadows = LightShadows.None;
}
var telemetry = sat.GetComponent<SatelliteTelemetry>();
if (telemetry != null)
telemetry.satelliteName = sat.name;
satelliteCount++;
}
}
}
Debug.Log($"Spawned {satelliteCount} satellites for {preset.name} constellation.");
}
ConstellationPreset GetPreset()
{
switch (constellationType)
{
case ConstellationType.GPS:
return new ConstellationPreset("GPS", new[] {
new Shell(20200f, 55f, 6, 4)
});
case ConstellationType.Galileo:
return new ConstellationPreset("GALILEO", new[] {
new Shell(23222f, 56f, 3, 10)
});
case ConstellationType.Starlink:
return new ConstellationPreset("STARLINK", new[] {
new Shell(550f, 53f, 24, 22),
new Shell(570f, 70f, 12, 20),
new Shell(1200f, 97.6f, 6, 18)
});
default:
Shell[] shells = new Shell[orbitCount];
for (int i = 0; i < orbitCount; i++)
{
shells[i] = new Shell(
baseAltitudeKm + i * altitudeSpacingKm,
baseInclination + i * inclinationStep,
planesPerShell,
satellitesPerPlane
);
}
return new ConstellationPreset("CUSTOM", shells);
}
}
struct ConstellationPreset
{
public string name;
public Shell[] shells;
public ConstellationPreset(string name, Shell[] shells) { this.name = name; this.shells = shells; }
}
struct Shell
{
public float altitudeKm;
public float inclination;
public int planes;
public int satsPerPlane;
public Shell(float altitudeKm, float inclination, int planes, int satsPerPlane)
{
this.altitudeKm = altitudeKm;
this.inclination = inclination;
this.planes = planes;
this.satsPerPlane = satsPerPlane;
}
}
}OrbitPathRenderer.csDraws orbit polylines
using UnityEngine;
[RequireComponent(typeof(LineRenderer))]
public class OrbitPathRenderer : MonoBehaviour
{
[Header("References")]
public KeplerOrbit3D orbit;
[Header("Appearance")]
public int segments = 180;
public float baseWidth = 0.1f;
public Color pathColor = Color.cyan;
[Header("Scaling")]
public bool respectRootScale = true;
static Material sharedOrbitMat;
LineRenderer lr;
void Start()
{
lr = GetComponent<LineRenderer>();
lr.useWorldSpace = true;
lr.loop = false;
lr.positionCount = Mathf.Clamp(segments, 16, 2048) + 1;
if (sharedOrbitMat == null)
sharedOrbitMat = new Material(Shader.Find("Unlit/Color"));
lr.sharedMaterial = sharedOrbitMat;
lr.startColor = pathColor;
lr.endColor = pathColor;
if (orbit == null || orbit.planet == null)
{
Debug.LogWarning("[OrbitPathRenderer] Missing orbit or planet reference.");
return;
}
RebuildPath();
}
void Update()
{
if (Camera.main == null || lr == null || orbit == null || orbit.planet == null) return;
float camDist = Vector3.Distance(Camera.main.transform.position, orbit.planet.position);
lr.widthMultiplier = baseWidth * Mathf.Clamp(camDist * 0.002f, 0.5f, 100f);
}
public void RebuildPath()
{
if (orbit == null || orbit.planet == null || lr == null) return;
float scaleFactor = 1f;
if (respectRootScale)
{
if (orbit.solarSystemRoot != null)
scaleFactor = orbit.solarSystemRoot.lossyScale.x;
else if (orbit.planet != null && orbit.planet.parent != null)
scaleFactor = orbit.planet.parent.lossyScale.x;
}
Vector3[] pts = new Vector3[segments + 1];
for (int i = 0; i <= segments; i++)
{
float t = i / (float)segments;
float M = t * 2f * Mathf.PI;
float E = orbit.SolveEccentricAnomaly(M, orbit.eccentricity);
float nu = 2f * Mathf.Atan2(
Mathf.Sqrt(1 + orbit.eccentricity) * Mathf.Sin(E / 2f),
Mathf.Sqrt(1 - orbit.eccentricity) * Mathf.Cos(E / 2f)
);
float r = orbit.semiMajorAxis * (1 - orbit.eccentricity * Mathf.Cos(E));
Vector3 posOrbital = new Vector3(r * Mathf.Cos(nu), 0f, r * Mathf.Sin(nu)) * scaleFactor;
Quaternion rotation =
Quaternion.Euler(0f, orbit.raan, 0f) *
Quaternion.Euler(orbit.inclination, 0f, 0f) *
Quaternion.Euler(0f, orbit.argPerigee, 0f);
pts[i] = orbit.planet.position + rotation * posOrbital;
}
pts[segments] = pts[0];
lr.SetPositions(pts);
}
}SatelliteTelemetry.csLive readouts per satellite
using UnityEngine;
[RequireComponent(typeof(KeplerOrbit3D))]
public class SatelliteTelemetry : MonoBehaviour
{
KeplerOrbit3D orbit;
float orbitalSpeed_unitsPerSec;
float altitude_units;
float meanAnomaly;
float eccentricAnomaly;
float trueAnomaly;
[Header("Display")]
public string satelliteName = "Satellite";
public Color satelliteColor = Color.yellow;
[Header("Units")]
public float unitToKm = OrbitalMath.UnitToKm;
void Start()
{
orbit = GetComponent<KeplerOrbit3D>();
}
void Update()
{
if (orbit == null || orbit.planet == null) return;
Vector3 planetPos = orbit.planet.position;
Vector3 satPos = transform.position;
altitude_units = Vector3.Distance(planetPos, satPos) - orbit.planet.localScale.x * 0.5f;
float simTime = (OrbitSimulationManager.Instance != null)
? OrbitSimulationManager.Instance.SimulationTime
: Time.time;
float n = 2f * Mathf.PI / orbit.period;
meanAnomaly = Mathf.Deg2Rad * orbit.meanAnomaly0 + n * simTime;
meanAnomaly = Mathf.Repeat(meanAnomaly, 2f * Mathf.PI);
eccentricAnomaly = orbit.SolveEccentricAnomaly(meanAnomaly, orbit.eccentricity);
trueAnomaly = 2f * Mathf.Atan2(
Mathf.Sqrt(1 + orbit.eccentricity) * Mathf.Sin(eccentricAnomaly / 2f),
Mathf.Sqrt(1 - orbit.eccentricity) * Mathf.Cos(eccentricAnomaly / 2f)
);
float a = orbit.semiMajorAxis;
float e = orbit.eccentricity;
orbitalSpeed_unitsPerSec =
Mathf.Sqrt(1.0f - e * e) * n * a / (1 - e * Mathf.Cos(eccentricAnomaly));
}
public string GetTelemetry()
{
float altitudeKm = altitude_units * unitToKm;
float semiMajorAxisKm = orbit.semiMajorAxis * unitToKm;
float orbitalSpeedKmS = orbitalSpeed_unitsPerSec * unitToKm;
return
$"<b><color=#{ColorUtility.ToHtmlStringRGB(satelliteColor)}>{satelliteName}</color></b>\n\n" +
$"a (Semi-major axis): {semiMajorAxisKm:F0} km\n" +
$"e (Eccentricity): {orbit.eccentricity:F4}\n" +
$"i (Inclination): {orbit.inclination:F2}°\n" +
$"Ω (RAAN): {orbit.raan:F2}°\n" +
$"ω (Arg. of Perigee): {orbit.argPerigee:F2}°\n" +
$"T (Period): {orbit.period:F0}s\n\n" +
$"Mean anomaly (M): {Mathf.Rad2Deg * meanAnomaly:F2}°\n" +
$"Eccentric anomaly (E): {Mathf.Rad2Deg * eccentricAnomaly:F2}°\n" +
$"True anomaly (ν): {Mathf.Rad2Deg * trueAnomaly:F2}°\n\n" +
$"Altitude: {altitudeKm:F0} km\n" +
$"Speed: {orbitalSpeedKmS:F2} km/s\n" +
$"Orbit Phase: {(meanAnomaly / (2f * Mathf.PI) * 100f):F1}%";
}
}OrbitCameraController.csMouse, zoom, and focus controls
using UnityEngine;
public class OrbitCameraController : MonoBehaviour
{
[Header("Targets")]
public Transform defaultTarget;
private Transform target;
[Header("Movement")]
public float distance = 300f;
public float minDistance = 1f;
public float maxDistance = 5000f;
public float zoomSpeed = 800f;
[Header("Rotation")]
public float rotationSpeed = 50f;
private float yaw = 0f;
private float pitch = 20f;
[Header("Smoothing")]
public float zoomSmoothTime = 0.2f;
public float rotationSmoothTime = 0.1f;
private float zoomVelocity;
private float targetDistance;
private float targetYaw;
private float targetPitch;
private float yawVelocity;
private float pitchVelocity;
[Header("Scaling")]
public Transform solarSystemRoot;
void Start()
{
target = defaultTarget;
Vector3 angles = transform.eulerAngles;
yaw = targetYaw = angles.y;
pitch = targetPitch = angles.x;
targetDistance = distance;
if (solarSystemRoot == null && defaultTarget != null && defaultTarget.parent != null)
solarSystemRoot = defaultTarget.parent;
if (solarSystemRoot != null)
{
float rootScale = solarSystemRoot.lossyScale.x;
distance *= rootScale;
targetDistance = distance;
minDistance *= rootScale;
maxDistance *= rootScale;
zoomSpeed *= rootScale;
}
if (Camera.main != null)
Camera.main.farClipPlane = Mathf.Max(Camera.main.farClipPlane, maxDistance * 5f);
}
void Update()
{
if (target == null) return;
HandleKeyboardInput();
HandleMouseZoom();
HandleClickFocus();
yaw = Mathf.SmoothDampAngle(yaw, targetYaw, ref yawVelocity, rotationSmoothTime);
pitch = Mathf.SmoothDampAngle(pitch, targetPitch, ref pitchVelocity, rotationSmoothTime);
distance = Mathf.SmoothDamp(distance, targetDistance, ref zoomVelocity, zoomSmoothTime);
Quaternion rotation = Quaternion.Euler(pitch, yaw, 0);
Vector3 offset = rotation * new Vector3(0, 0, -distance);
transform.position = target.position + offset;
transform.LookAt(target.position);
}
void HandleKeyboardInput()
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
targetYaw -= h * rotationSpeed * Time.deltaTime;
targetPitch += v * rotationSpeed * Time.deltaTime;
targetPitch = Mathf.Clamp(targetPitch, -89f, 89f);
if (Input.GetKey(KeyCode.Equals) || Input.GetKey(KeyCode.KeypadPlus))
targetDistance -= zoomSpeed * Time.deltaTime;
if (Input.GetKey(KeyCode.Minus) || Input.GetKey(KeyCode.KeypadMinus))
targetDistance += zoomSpeed * Time.deltaTime;
targetDistance = Mathf.Clamp(targetDistance, minDistance, maxDistance);
}
void HandleMouseZoom()
{
float scroll = Input.GetAxis("Mouse ScrollWheel");
if (Mathf.Abs(scroll) > 0.01f)
{
targetDistance *= 1f - scroll * 2f;
targetDistance = Mathf.Clamp(targetDistance, minDistance, maxDistance);
}
}
void HandleClickFocus()
{
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit))
{
if (hit.collider.CompareTag("Planet") ||
hit.collider.CompareTag("Satellite") ||
hit.collider.CompareTag("GroundStation"))
{
target = hit.collider.transform;
var telemetry = hit.collider.GetComponent<SatelliteTelemetry>();
if (telemetry != null)
{
FindObjectOfType<TelemetryHUD>()?.ShowTelemetry(telemetry);
}
else
{
FindObjectOfType<TelemetryHUD>()?.HideTelemetry();
}
}
}
}
if (Input.GetKeyDown(KeyCode.Escape))
{
target = defaultTarget;
FindObjectOfType<TelemetryHUD>()?.HideTelemetry();
}
}
public void AdoptFromCurrentCamera(Transform focusTarget = null)
{
target = focusTarget != null ? focusTarget
: (defaultTarget != null ? defaultTarget : target);
if (target == null) return;
Vector3 offset = transform.position - target.position;
float d = offset.magnitude;
if (d < 1e-4f) d = Mathf.Max(1e-2f, minDistance);
float newYaw = Mathf.Atan2(offset.x, -offset.z) * Mathf.Rad2Deg;
float s = Mathf.Clamp(offset.y / d, -1f, 1f);
float newPitch = Mathf.Asin(s) * Mathf.Rad2Deg;
newPitch = Mathf.Clamp(newPitch, -89f, 89f);
distance = Mathf.Clamp(d, minDistance, maxDistance);
targetDistance = distance;
yaw = targetYaw = newYaw;
pitch = targetPitch = newPitch;
yawVelocity = 0f;
pitchVelocity = 0f;
zoomVelocity = 0f;
transform.LookAt(target.position);
}
}