tidssystem

This commit is contained in:
2026-11-01 16:52:32 +01:00
parent 9d44d14cc9
commit a7207086b2
24 changed files with 849 additions and 174 deletions

View File

@@ -34,11 +34,12 @@ internal sealed class AdaptivePlanetMesh : IDisposable
public AdaptivePlanetMesh(GL gl, PlanetData planet)
{
_gl = gl;
_planet = planet;
// Meshes live in a stable body frame. Spin is applied on the GPU, never by rebuilding terrain.
_planet = planet with { Orientation = PlanetOrientation.Identity };
for (var face = 0; face < 6; face++)
{
var key = new TerrainPatchKey(face, 0, 0, 0);
var data = TerrainPatchBuilder.Build(planet, key);
var data = TerrainPatchBuilder.Build(_planet, key);
_tiles[key] = new() { Data = data, Mesh = new(gl, 9, data.Vertices, data.Indices) };
}
}

View File

@@ -13,6 +13,50 @@ namespace Universe.App.Graphics;
/// <summary>Offscreen tests of the production shaders; no desktop capture or mouse control.</summary>
internal static class GraphicsReport
{
public static void RunSimulation(GL gl, UniverseRenderer renderer, UniverseNeighborhood initial, double cellSize, int width, int height)
{
var directory = Path.Combine(AppContext.BaseDirectory, "graphics-report"); Directory.CreateDirectory(directory);
var center = UniversePosition.AtCellCenter(initial.ActiveSystem.Coordinate, cellSize);
var camera = new FlyCamera(center, cellSize);
var basis = initial.ActiveSystem;
camera.SetInitialView(basis, new StartupConfiguration { PlanetId = "earth", DistanceInRadii = 3, LongitudeDegrees = 20, LatitudeDegrees = 20 });
var earth = basis.Planets.Single(p => p.Id == "earth");
var offset = camera.Position.RelativeTo(center.Add(earth.PositionMeters, cellSize), cellSize);
var forward = camera.Forward; var up = camera.Up;
float[] Frame(double seconds, string file)
{
var system = Universe.Core.Physics.CelestialSimulation.Evaluate(basis, basis.EpochSecondsSinceJ2000 + seconds);
var p = system.Planets.Single(p => p.Id == "earth");
camera.SetPlayerPose(center.Add(p.PositionMeters + offset, cellSize), forward, up, resetMotionHistory: true);
renderer.SimulationTimeSeconds = seconds;
for (var i = 0; i < 24; i++) renderer.Render(camera, initial with { ActiveSystem = system }, 0);
gl.Finish(); var pixels = Read(gl, width, height);
if (gl.GetError() != GLEnum.NoError || pixels.Any(v => !float.IsFinite(v)))
throw new InvalidOperationException("Simulation produced invalid OpenGL output.");
WritePng(Path.Combine(directory, file), width, height, pixels, false);
return pixels;
}
var first = Frame(0, "earth-time-0h.png");
var rotated = Frame(21600, "earth-time-6h.png");
var difference = first.Zip(rotated, (a,b) => System.Math.Abs(a-b)).Average();
if (difference < .002) throw new InvalidOperationException($"Earth spin did not visibly change the production rendering: {difference}.");
renderer.DrawMenu(2, true, 100, Universe.Core.Physics.SimulationClock.J2000.AddHours(6));
gl.Finish(); WritePng(Path.Combine(directory, "time-debug-menu.png"), width, height, Read(gl, width, height), false);
// Sample a detailed surface after spin: mesh generation, culling and displacement must use the body frame.
var after = Universe.Core.Physics.CelestialSimulation.Evaluate(basis, basis.EpochSecondsSinceJ2000 + 21600);
var planet = after.Planets.Single(p => p.Id == "earth");
var lat = 46.5 * System.Math.PI / 180; var lon = -7.5 * System.Math.PI / 180;
var radial = planet.Orientation.ToWorld(new(System.Math.Cos(lat)*System.Math.Cos(lon), System.Math.Sin(lat), System.Math.Cos(lat)*System.Math.Sin(lon)));
var tangent = DVector3.Cross(radial, planet.Orientation.ToWorld(new(0,1,0))).Normalized();
var radius = TerrainHeight.Sample(planet, radial);
camera.SetPlayerPose(center.Add(planet.PositionMeters + radial * (radius + 400), cellSize), (tangent - radial * .3).Normalized(), radial, true);
for (var i = 0; i < 72; i++) renderer.Render(camera, initial with { ActiveSystem = after }, 0);
gl.Finish(); var ground = Read(gl, width, height);
if (gl.GetError() != GLEnum.NoError) throw new InvalidOperationException("Rotating terrain GL error.");
WritePng(Path.Combine(directory, "earth-rotated-terrain.png"), width, height, ground, false);
Console.WriteLine($"Simulation GPU report passed: Earth 0/6h visual difference {difference:F5}, rotated terrain {renderer.TerrainTiles} tiles, LOD {renderer.TerrainDeepestLevel}; debug time menu. Images: {directory}");
}
public static void RunStarTrails(GL gl,StarMotionBlurSettings settings,int width,int height)
{
using var trails=new StarTrailRenderer(gl,settings with {Enabled=true});

View File

@@ -49,7 +49,18 @@ internal sealed class PauseMenu : IDisposable
return -1;
}
public void Draw(int width, int height, bool debugEnabled)
public static double? TimeScaleHit(double x, double y, int width, int height)
{
var (s, cx, cy) = Layout(width, height);
x = (x - cx) / s; y = (y - cy) / s;
if (x >= 290 && x <= 610 && y >= -80 && y <= -40) return System.Math.Round((x - 290) / 320 * 100);
if (y >= 0 && y <= 38)
for (var i = 0; i < 4; i++)
if (x >= 290 + i * 82 && x <= 364 + i * 82) return new double[] { 0, 1, 10, 100 }[i];
return null;
}
public void Draw(int width, int height, bool debugEnabled, double timeScale = 1, DateTimeOffset? simulationUtc = null)
{
var (s, cx, cy) = Layout(width, height);
var data = new List<float>();
@@ -84,6 +95,26 @@ internal sealed class PauseMenu : IDisposable
Text(label, cx, y + 12*s, (i >= 3 ? 3.35 : 4) * s, .92f, .95f, 1);
}
Text("P OR ESC TO RESUME", cx, cy + 250*s, 2*s, .6f, .7f, .8f);
if (debugEnabled)
{
Rect(cx + 250*s, cy - 180*s, 400*s, 340*s, .025f, .04f, .075f);
Text("WORLD TIME", cx + 450*s, cy - 150*s, 3.5*s, .9f, .95f, 1);
Text($"SPEED {timeScale:0.##} X", cx + 450*s, cy - 108*s, 2.8*s, .4f, .85f, 1);
Rect(cx + 290*s, cy - 64*s, 320*s, 8*s, .13f, .24f, .34f);
Rect(cx + 290*s, cy - 64*s, timeScale / 100 * 320*s, 8*s, .3f, .7f, .95f);
Rect(cx + (286 + timeScale / 100 * 320)*s, cy - 74*s, 8*s, 28*s, .8f, .95f, 1);
Text("0 X 100 X", cx + 450*s, cy - 30*s, 1.8*s, .7f, .8f, .9f);
var speeds = new[] { "0 X", "1 X", "10 X", "100 X" };
for (var i = 0; i < speeds.Length; i++)
{
Rect(cx + (290 + i * 82)*s, cy, 74*s, 38*s, .08f, .18f, .28f);
Text(speeds[i], cx + (327 + i * 82)*s, cy + 12*s, 2*s, .9f, .95f, 1);
}
Text("LEFT / RIGHT TO ADJUST", cx + 450*s, cy + 60*s, 2*s, .65f, .8f, .9f);
Text("PAUSED IN MENU", cx + 450*s, cy + 88*s, 2*s, .65f, .8f, .9f);
Text((simulationUtc ?? Universe.Core.Physics.SimulationClock.J2000).ToString("yyyy-MM-dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture),
cx + 450*s, cy + 121*s, 2*s, .7f, .85f, .95f);
}
_mesh.ReplaceVertices(data.ToArray()); _mesh.Draw(GLEnum.Triangles);
}
public void Dispose() => _mesh.Dispose();

View File

@@ -10,7 +10,8 @@ internal sealed class PerformanceOverlay : IDisposable
public PerformanceOverlay(GL gl) => _mesh = new(gl, 6, [], dynamic: true);
public void Draw(int width, int height, IReadOnlyList<KeyValuePair<string, double>> samples,
double cpuMilliseconds, double fps, int terrainTiles, int terrainLod, int pendingTiles, string? probePlanetId)
double cpuMilliseconds, double fps, int terrainTiles, int terrainLod, int pendingTiles, string? probePlanetId,
double timeScale = 1, DateTimeOffset? simulationUtc = null)
{
var values = samples.ToDictionary(p => p.Key, p => p.Value, StringComparer.Ordinal);
var rows = new List<(string Label, string Value, bool Emphasis)>();
@@ -44,6 +45,8 @@ internal sealed class PerformanceOverlay : IDisposable
rows.Add(("FPS", $"{Number(fps, "F1")}", false));
rows.Add(("TERRAIN TILES", terrainTiles.ToString(CultureInfo.InvariantCulture), false));
rows.Add(("LOD / PENDING", $"{terrainLod} / {pendingTiles}", false));
rows.Add(("TIME SCALE", $"{Number(timeScale, "F1")} X", true));
if (simulationUtc is { } date) rows.Add(("WORLD UTC", date.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), false));
var scale = System.Math.Clamp(System.Math.Min(width / 1440d, height / 900d), .58, 1.25);
var x = 14 * scale;

View File

@@ -8,6 +8,7 @@ uniform float uRadius;
uniform mat4 uViewProjection;
uniform float uFar;
uniform mat4 uWorldToBody;
uniform mat4 uBodyToWorld;
uniform vec3 uMaterialAnchor;
out vec3 vWorldPosition;
out vec3 vPlanetLocal;
@@ -15,10 +16,11 @@ out vec3 vNormal;
out float vFragDepth;
out vec3 vMaterialPosition;
void main() {
vWorldPosition=uChunkPosition+aPosition;
vPlanetLocal=uChunkOrigin+aPosition/uRadius;
vNormal=aNormal;
vMaterialPosition=mat3(uWorldToBody)*aPosition+uMaterialAnchor;
vec3 worldOffset=mat3(uBodyToWorld)*aPosition;
vWorldPosition=uChunkPosition+worldOffset;
vPlanetLocal=uChunkOrigin+worldOffset/uRadius;
vNormal=mat3(uBodyToWorld)*aNormal;
vMaterialPosition=aPosition+uMaterialAnchor;
vec4 clip=uViewProjection*vec4(vWorldPosition,1.0);
vFragDepth=clip.w+1.0;
gl_Position=clip;

View File

@@ -66,6 +66,7 @@ internal sealed class UniverseRenderer : IDisposable
private int _width;
private int _height;
private double _time;
public double? SimulationTimeSeconds { get; set; }
private int _probeFrameCounter;
private int _probeEffectIndex;
private string? _debugProbePlanetId;
@@ -260,7 +261,7 @@ internal sealed class UniverseRenderer : IDisposable
if (timer is null) return;
_gl.Finish(); FrameTimings[name] = timer.Elapsed.TotalMilliseconds; timer.Restart();
}
_time += System.Math.Min(deltaSeconds, 0.1);
_time = SimulationTimeSeconds ?? (_time + System.Math.Min(deltaSeconds, 0.1));
TerrainTiles = 0;
TerrainDeepestLevel = 0; TerrainPendingTiles = 0;
TerrainNearestLevel = 0; TerrainNearestDistance = double.PositiveInfinity;
@@ -355,20 +356,21 @@ internal sealed class UniverseRenderer : IDisposable
}
public int MenuHit(double x, double y, int width, int height) => _pauseMenu.Hit(x, y, width, height);
public void DrawMenu(int selection, bool debugEnabled)
public double? TimeScaleHit(double x, double y, int width, int height) => PauseMenu.TimeScaleHit(x, y, width, height);
public void DrawMenu(int selection, bool debugEnabled, double timeScale = 1, DateTimeOffset? simulationUtc = null)
{
_gl.Disable(GLEnum.DepthTest); _gl.Disable(GLEnum.Blend);
_pauseMenu.Selected = selection; _overlay.Use(); _pauseMenu.Draw(_width, _height, debugEnabled);
_pauseMenu.Selected = selection; _overlay.Use(); _pauseMenu.Draw(_width, _height, debugEnabled, timeScale, simulationUtc);
_gl.Enable(GLEnum.DepthTest);
}
public void DrawDebugOverlay(double fps)
public void DrawDebugOverlay(double fps, double timeScale = 1, DateTimeOffset? simulationUtc = null)
{
if (!DebugMode) return;
_gl.Disable(GLEnum.DepthTest); _gl.Disable(GLEnum.Blend); _gl.DepthMask(false);
_overlay.Use();
_performanceOverlay.Draw(_width, _height, DebugSamples(), CpuRenderMilliseconds, fps,
TerrainTiles, TerrainDeepestLevel, TerrainPendingTiles, _debugProbePlanetId);
TerrainTiles, TerrainDeepestLevel, TerrainPendingTiles, _debugProbePlanetId, timeScale, simulationUtc);
_gl.DepthMask(true); _gl.Enable(GLEnum.DepthTest);
}
@@ -490,14 +492,16 @@ internal sealed class UniverseRenderer : IDisposable
BindRingProfile(_surface, planet);
_surface.Set("uTerrainShadows", relative.Length < planet.RadiusMeters * 1.18 && probeEffect != PlanetProbeEffect.MountainShadows ? 1 : 0);
if (onlyPlanetId is null) _gpuProfiler.BeginPass($"PLANET {planet.Id.ToUpperInvariant()}");
mesh.Draw(-relative, camera.Forward, camera.Right, camera.Up, _height, (double)_width / _height, System.Math.Tan(camera.FieldOfViewRadians * 0.5), (origin, gpu) =>
_surface.SetMatrix("uBodyToWorld", planet.Orientation.BodyToWorld);
mesh.Draw(planet.Orientation.ToBody(-relative), planet.Orientation.ToBody(camera.Forward), planet.Orientation.ToBody(camera.Right), planet.Orientation.ToBody(camera.Up), _height, (double)_width / _height, System.Math.Tan(camera.FieldOfViewRadians * 0.5), (origin, gpu) =>
{
var anchor = planet.Orientation.ToBody(origin);
var anchor = origin;
var scale = materialSettings.ScaleMeters;
static double Mod(double value, double divisor) => value - System.Math.Floor(value / divisor) * divisor;
_surface.Set("uMaterialAnchor", new Vector3((float)Mod(anchor.X, scale), (float)Mod(anchor.Y, scale), (float)Mod(anchor.Z, scale)));
_surface.Set("uChunkPosition", (relative + origin).ToVector3());
_surface.Set("uChunkOrigin", (origin / planet.RadiusMeters).ToVector3());
var worldOrigin = planet.Orientation.ToWorld(origin);
_surface.Set("uChunkPosition", (relative + worldOrigin).ToVector3());
_surface.Set("uChunkOrigin", (worldOrigin / planet.RadiusMeters).ToVector3());
gpu.Draw();
});
if (onlyPlanetId is null)

View File

@@ -12,6 +12,8 @@ public sealed class FlyCamera
private DVector3 _up = new(0, 1, 0);
private FocusFlight? _flight;
private double _flightElapsed;
private StarSystemData? _flightSystem;
private PlanetData? _flightPlanet;
private double _defaultFieldOfView = 2 * System.Math.Atan(.67);
public double FieldOfViewRadians { get; private set; } = 2 * System.Math.Atan(.67);
public FlyCamera(UniversePosition initialPosition, double cellSizeMeters)
@@ -336,6 +338,58 @@ public sealed class FlyCamera
_flight = new(travelStart, body, startRadial, radial, approachRadius, finalRadius, Forward, Up, forward, up, speed,
duration, FieldOfViewRadians, fieldOfView ?? _defaultFieldOfView, departure);
_flightElapsed = 0;
_flightSystem = null; _flightPlanet = null;
}
public void ApplyCelestialMotion(UniverseNeighborhood previous, UniverseNeighborhood current, double secondsSinceJ2000)
{
var bodies = previous.NearbySystems.Append(previous.ActiveSystem)
.SelectMany(s => s.Planets.Select(p => (System: s, Planet: p, Body: World(s, p.PositionMeters)))).ToArray();
if (_flight is { } flight)
{
if (flight.Departure is { } departure)
{
var source = bodies.FirstOrDefault(b => b.Body.RelativeTo(departure.Body, CellSizeMeters).Length < 1);
if (source.Planet is { } sourcePlanet)
{
var movedSystem = CelestialSimulation.Evaluate(source.System, secondsSinceJ2000);
var movedPlanet = movedSystem.Planets.First(p => p.Id == sourcePlanet.Id);
DVector3 TurnDeparture(DVector3 v) => movedPlanet.Orientation.ToWorld(sourcePlanet.Orientation.ToBody(v));
var movedBody = World(movedSystem, movedPlanet.PositionMeters);
var movedDeparture = departure with { Body = movedBody, StartRadial = TurnDeparture(departure.StartRadial), TargetRadial = TurnDeparture(departure.TargetRadial) };
flight = flight with { Departure = movedDeparture,
Start = movedBody.Add(movedDeparture.TargetRadial * departure.ClearanceRadius, CellSizeMeters) };
}
}
if (_flightPlanet is null)
{
var target = bodies.FirstOrDefault(b => b.Body.RelativeTo(flight.Body, CellSizeMeters).Length < 1);
_flightSystem = target.System; _flightPlanet = target.Planet;
}
if (_flightSystem is not null && _flightPlanet is { } old)
{
var updated = CelestialSimulation.Evaluate(_flightSystem, secondsSinceJ2000).Planets.First(p => p.Id == old.Id);
DVector3 Turn(DVector3 v) => updated.Orientation.ToWorld(old.Orientation.ToBody(v));
_flight = flight with { Body = World(_flightSystem, updated.PositionMeters), TargetRadial = Turn(flight.TargetRadial),
TargetForward = Turn(flight.TargetForward), TargetUp = Turn(flight.TargetUp) };
_flightPlanet = updated;
}
else _flight = flight;
return;
}
var nearest = bodies.MinBy(b => b.Body.RelativeTo(Position, CellSizeMeters).Length / b.Planet.RadiusMeters);
if (nearest.Planet is null) return;
var offset = Position.RelativeTo(nearest.Body, CellSizeMeters);
if (offset.Length > nearest.Planet.RadiusMeters * 12) return;
var system = current.NearbySystems.Append(current.ActiveSystem).FirstOrDefault(s => s.Coordinate == nearest.System.Coordinate);
var planet = system?.Planets.FirstOrDefault(p => p.Id == nearest.Planet.Id);
if (planet is null) return;
if (offset.Length - TerrainHeight.Sample(nearest.Planet, offset.Normalized()) < 100)
{
DVector3 Turn(DVector3 v) => planet.Orientation.ToWorld(nearest.Planet.Orientation.ToBody(v));
offset = Turn(offset); _forward = Turn(_forward); _up = Turn(_up);
}
Position = World(system!, planet.PositionMeters).Add(offset, CellSizeMeters);
}
public void Update(double deltaSeconds, DVector3 movement, UniverseNeighborhood neighborhood)

View File

@@ -28,7 +28,7 @@ internal sealed class GalaxyJourneyBenchmark : IBenchmarkSession
public object? RenderEnvironment { get; set; }
public GalaxyJourneyBenchmark(GalaxyUniverse galaxies, UniverseGenerator generator, StarSystemData homeSystem, PlanetData earth,
UniversePosition home, DVector3 homeForward, DVector3 homeUp)
UniversePosition home, DVector3 homeForward, DVector3 homeUp, double simulationSeconds = 0)
{
_galaxies = galaxies; _home = home; _homeForward = homeForward; _homeUp = homeUp;
_homeBody = UniversePosition.AtCellCenter(homeSystem.Coordinate, galaxies.CellSize).Add(earth.PositionMeters, galaxies.CellSize);
@@ -37,7 +37,7 @@ internal sealed class GalaxyJourneyBenchmark : IBenchmarkSession
HomeGalaxy = galaxies.GalaxyAt(home);
var region = HomeGalaxy.Region;
TargetGalaxy = galaxies.Get(new(checked(region.X + 1), region.Y, region.Z));
TargetSystem = generator.Generate(galaxies.SelectSystem(TargetGalaxy));
TargetSystem = Universe.Core.Physics.CelestialSimulation.Evaluate(generator.Generate(galaxies.SelectSystem(TargetGalaxy)), simulationSeconds);
TargetPlanet = TargetSystem.Planets.Where(p => !p.IsGasPlanet && p.ParentPlanetIndex is null).OrderByDescending(p => p.RadiusMeters).First();
_targetBody = UniversePosition.AtCellCenter(TargetSystem.Coordinate, galaxies.CellSize).Add(TargetPlanet.PositionMeters, galaxies.CellSize);
// Exercise the illuminated terrain/shadow path, rather than accidentally

View File

@@ -39,6 +39,24 @@ public sealed class PlayerController
_pitch = System.Math.Clamp(_pitch - dy * .0022, -1.5, 1.5);
}
public void ApplyCelestialMotion(UniverseNeighborhood previous, UniverseNeighborhood current)
{
if (Nearest(previous) is not { } old) return;
var system = current.NearbySystems.Append(current.ActiveSystem).FirstOrDefault(s => s.Coordinate == old.System.Coordinate);
var planet = system?.Planets.FirstOrDefault(p => p.Id == old.Planet.Id);
if (planet is null) return;
var body = UniversePosition.AtCellCenter(system!.Coordinate, _cellSize).Add(planet.PositionMeters, _cellSize);
var relative = Position.RelativeTo(old.Body, _cellSize);
// Use the rotating local frame near the surface, including jumps: the player inherits the ground's
// rotational motion instead of the planet sliding hundreds of metres sideways during a jump.
if (Grounded || relative.Length - TerrainHeight.Sample(old.Planet, relative.Normalized()) <= System.Math.Max(2000, old.Planet.AtmosphereHeightMeters))
{
DVector3 Rotate(DVector3 v) => planet.Orientation.ToWorld(old.Planet.Orientation.ToBody(v));
relative = Rotate(relative); _heading = Rotate(_heading); Up = Rotate(Up); _velocity = Rotate(_velocity);
}
Position = body.Add(relative, _cellSize);
}
public void Update(double dt, double forward, double side, bool jump, bool crouch, UniverseNeighborhood neighborhood)
{
if (!double.IsFinite(dt) || dt <= 0) return;

View File

@@ -10,6 +10,7 @@ using Universe.Core.Generation;
using Universe.Core.Math;
using Universe.Core.World;
using Universe.Core.Configuration;
using Universe.Core.Physics;
namespace Universe.App;
@@ -20,6 +21,7 @@ public sealed class UniverseApplication : IDisposable
private readonly UniverseStreamer _streamer;
private readonly GalaxyUniverse _galaxies;
private readonly StarMotionBlurSettings _starMotionBlur;
private readonly SimulationClock _clock;
private readonly IReadOnlyList<string> _planetHotkeys;
private readonly StarSystemData _homeSystem;
private readonly PlanetData _homeEarth;
@@ -52,6 +54,7 @@ public sealed class UniverseApplication : IDisposable
{
var configPath = Path.Combine(AppContext.BaseDirectory, "config.json");
var configuration = EngineConfiguration.Load(configPath);
_clock = new(configuration.Simulation);
_starMotionBlur = configuration.StarMotionBlur;
_planetHotkeys = configuration.Startup.PlanetHotkeys;
var catalogPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(configPath)!, configuration.KnownSolarSystemsFile));
@@ -60,8 +63,9 @@ public sealed class UniverseApplication : IDisposable
Path.GetFullPath(Path.Combine(Path.GetDirectoryName(configPath)!, configuration.Galaxies.KnownGalaxiesFile)));
_generator = new(configuration.Seed, configuration.CellSizeMeters, catalog, configuration.BlackHoles, _galaxies);
_streamer = new(_generator);
_streamer.SimulationSeconds = _clock.SecondsSinceJ2000;
var initialSystem = configuration.ResolveStartup(catalog).System;
_homeSystem = _generator.Generate(catalog.GetById("sol").Coordinate);
_homeSystem = _streamer.AtCurrentTime(_generator.Generate(catalog.GetById("sol").Coordinate));
_homeEarth = _homeSystem.Planets.FirstOrDefault(p => p.Id.Equals("earth", StringComparison.OrdinalIgnoreCase))
?? throw new InvalidDataException("The known solar system must define an Earth planet with id 'earth'.");
var startCoordinate = initialSystem.Coordinate;
@@ -79,7 +83,7 @@ public sealed class UniverseApplication : IDisposable
var options = WindowOptions.Default with
{
Title = "Universe | loading",
IsVisible = !Environment.GetCommandLineArgs().Any(a => a is "--graphics-report" or "--scene-report" or "--ocean-report" or "--cloud-report" or "--terrain-report" or "--menu-report" or "--terrain-perf-report" or "--black-hole-report" or "--galaxy-journey-report" or "--star-trails-report"),
IsVisible = !Environment.GetCommandLineArgs().Any(a => a is "--simulation-report" or "--graphics-report" or "--scene-report" or "--ocean-report" or "--cloud-report" or "--terrain-report" or "--menu-report" or "--terrain-perf-report" or "--black-hole-report" or "--galaxy-journey-report" or "--star-trails-report"),
Size = new Vector2D<int>(1440, 900),
VSync = true,
PreferredDepthBufferBits = 24,
@@ -100,6 +104,11 @@ public sealed class UniverseApplication : IDisposable
var gl = _window.CreateOpenGL();
_input = _window.CreateInput();
_renderer = new(gl, _window.FramebufferSize.X, _window.FramebufferSize.Y, _galaxies, _starMotionBlur);
if (Environment.GetCommandLineArgs().Contains("--simulation-report"))
{
GraphicsReport.RunSimulation(gl, _renderer, _neighborhood, _generator.CellSizeMeters, _window.FramebufferSize.X, _window.FramebufferSize.Y);
_window.Close(); return;
}
if (Environment.GetCommandLineArgs().Contains("--star-trails-report"))
{
GraphicsReport.RunStarTrails(gl,_starMotionBlur,_window.FramebufferSize.X,_window.FramebufferSize.Y);
@@ -164,6 +173,16 @@ public sealed class UniverseApplication : IDisposable
if (_menuOpen) { _mouseDelta = Vector2.Zero; return; }
var previous = _neighborhood;
_clock.Advance(deltaSeconds);
_streamer.SimulationSeconds = _clock.SecondsSinceJ2000;
if (_clock.TimeScale > 0)
{
_neighborhood = _streamer.Refresh(previous);
if (_player is { } movingPlayer) movingPlayer.ApplyCelestialMotion(previous, _neighborhood);
else _camera.ApplyCelestialMotion(previous, _neighborhood, _clock.SecondsSinceJ2000);
}
if (_mouseCaptured)
{
if (_player is null) _camera.Rotate(_mouseDelta.X, _mouseDelta.Y);
@@ -216,12 +235,13 @@ public sealed class UniverseApplication : IDisposable
if (_renderer is not null)
{
_renderer.DebugMode = _debugMode;
_renderer.SimulationTimeSeconds = _clock.ElapsedSeconds;
_renderer.DebugProbeEnabled = _debugMode && !_menuOpen;
}
_renderer?.Render(_camera, _neighborhood, _menuOpen ? 0 : deltaSeconds);
_renderer?.DrawDebugOverlay(1 / _frameTime);
_renderer?.DrawDebugOverlay(1 / _frameTime, _clock.TimeScale, _clock.Utc);
if (_benchmark is { HasRun: true } benchmark) _renderer?.DrawBenchmarkOverlay(benchmark);
if (_menuOpen) _renderer?.DrawMenu(_menuSelection, _debugMode);
if (_menuOpen) _renderer?.DrawMenu(_menuSelection, _debugMode, _clock.TimeScale, _clock.Utc);
_benchmarkNeedsInitialFrame = false;
}
@@ -252,6 +272,8 @@ public sealed class UniverseApplication : IDisposable
if (_menuOpen)
{
if (key == Key.Escape) SetMenu(false);
else if (_debugMode && key is Key.Left or Key.Right)
_clock.TimeScale += (key == Key.Right ? 1 : -1) * (IsDown(Key.ShiftLeft) || IsDown(Key.ShiftRight) ? 10 : 1);
else if (key == Key.Up) _menuSelection = (_menuSelection + 6) % 7;
else if (key == Key.Down) _menuSelection = (_menuSelection + 1) % 7;
else if (key is Key.Enter or Key.KeypadEnter) ActivateMenu();
@@ -337,6 +359,9 @@ public sealed class UniverseApplication : IDisposable
{
if (_menuOpen)
{
if (_debugMode && mouse.IsButtonPressed(MouseButton.Left)
&& _renderer?.TimeScaleHit(position.X, position.Y, _window.Size.X, _window.Size.Y) is { } rate)
_clock.TimeScale = rate;
var hit = _renderer?.MenuHit(position.X, position.Y, _window.Size.X, _window.Size.Y) ?? -1;
if (hit >= 0) _menuSelection = hit;
return;
@@ -363,6 +388,8 @@ public sealed class UniverseApplication : IDisposable
{
if (!_menuOpen || button != MouseButton.Left) return;
var position = mouse.Position;
if (_debugMode && _renderer?.TimeScaleHit(position.X, position.Y, _window.Size.X, _window.Size.Y) is { } rate)
{ _clock.TimeScale = rate; return; }
var hit = _renderer?.MenuHit(position.X, position.Y, _window.Size.X, _window.Size.Y) ?? -1;
if (hit >= 0) { _menuSelection = hit; ActivateMenu(); }
}
@@ -390,7 +417,8 @@ public sealed class UniverseApplication : IDisposable
_debugMode = false; _player = null;
_benchmark = CreateGalaxyJourney();
_benchmarkNeedsInitialFrame = true;
_camera.SetPlayerPose(_homeCameraPosition, _homeCameraForward, _homeCameraUp, resetMotionHistory: true);
var home = CurrentHomeView();
_camera.SetPlayerPose(home.Position, home.Forward, home.Up, resetMotionHistory: true);
UpdateNeighborhood(); SetMenu(false); return;
}
if (_menuSelection is 3 or 4)
@@ -417,8 +445,9 @@ public sealed class UniverseApplication : IDisposable
private void StartBenchmark(bool unknownPlanet)
{
_camera.RestoreDefaultFieldOfView();
var system = _homeSystem;
var planet = _homeEarth;
var home = CurrentHomeView();
var system = home.System;
var planet = home.Earth;
if (unknownPlanet)
{
var candidate = _neighborhood.NearbySystems.Append(_neighborhood.ActiveSystem)
@@ -441,7 +470,7 @@ public sealed class UniverseApplication : IDisposable
_debugMode = false;
_player = null;
_benchmark = new BenchmarkRunner(system, planet, _generator.CellSizeMeters, _camera,
_homeCameraPosition, _homeCameraForward, _homeCameraUp);
home.Position, home.Forward, home.Up);
_benchmarkNeedsInitialFrame = true;
UpdateNeighborhood();
SetMenu(false);
@@ -450,14 +479,27 @@ public sealed class UniverseApplication : IDisposable
private GalaxyJourneyBenchmark CreateGalaxyJourney()
{
_camera.RestoreDefaultFieldOfView();
var journey = new GalaxyJourneyBenchmark(_galaxies, _generator, _homeSystem, _homeEarth,
_homeCameraPosition, _homeCameraForward, _homeCameraUp);
var home = CurrentHomeView();
var journey = new GalaxyJourneyBenchmark(_galaxies, _generator, home.System, home.Earth,
home.Position, home.Forward, home.Up, _clock.SecondsSinceJ2000);
journey.RenderEnvironment = new { Width = _window.FramebufferSize.X, Height = _window.FramebufferSize.Y, Gpu = _renderer?.HardwareName, Mode = "interactive-vsync", FieldOfViewDegrees = _camera.FieldOfViewRadians * 180 / System.Math.PI };
foreach (var planet in _homeSystem.Planets) _renderer?.PreparePlanet(_homeSystem, planet);
foreach (var planet in journey.TargetSystem.Planets) _renderer?.PreparePlanet(journey.TargetSystem, planet);
return journey;
}
private (StarSystemData System, PlanetData Earth, UniversePosition Position, DVector3 Forward, DVector3 Up) CurrentHomeView()
{
var system = _streamer.AtCurrentTime(_homeSystem);
var earth = system.Planets.First(p => p.Id == _homeEarth.Id);
var center = UniversePosition.AtCellCenter(system.Coordinate, _generator.CellSizeMeters);
var oldBody = center.Add(_homeEarth.PositionMeters, _generator.CellSizeMeters);
var body = center.Add(earth.PositionMeters, _generator.CellSizeMeters);
DVector3 Turn(DVector3 v) => earth.Orientation.ToWorld(_homeEarth.Orientation.ToBody(v));
return (system, earth, body.Add(Turn(_homeCameraPosition.RelativeTo(oldBody, _generator.CellSizeMeters)), _generator.CellSizeMeters),
Turn(_homeCameraForward), Turn(_homeCameraUp));
}
private void OnClosing()
{
_renderer?.Dispose();

View File

@@ -26,6 +26,7 @@ public sealed record EngineConfiguration
public BlackHoleGenerationSettings BlackHoles { get; init; } = new();
public GalaxySettings Galaxies { get; init; } = new();
public StarMotionBlurSettings StarMotionBlur { get; init; } = new();
public Physics.SimulationSettings Simulation { get; init; } = new();
public double CellSizeMeters => CellSizeAu * UniverseScale.AstronomicalUnitMeters;
public ulong Seed => UniverseSeed.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
? ulong.Parse(UniverseSeed.AsSpan(2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture)
@@ -45,6 +46,7 @@ public sealed record EngineConfiguration
public static EngineConfiguration Load(string path)
{
var settings = JsonSettings.Read<EngineConfiguration>(path);
(settings.Simulation ?? throw new InvalidDataException("config.json: simulation cannot be null.")).Validate();
(settings.BlackHoles ?? throw new InvalidDataException("config.json: blackHoles cannot be null.")).Validate();
(settings.Galaxies ?? throw new InvalidDataException("config.json: galaxies cannot be null.")).Validate();
(settings.StarMotionBlur ?? throw new InvalidDataException("config.json: starMotionBlur cannot be null.")).Validate();

View File

@@ -55,7 +55,8 @@ public sealed class KnownSolarSystemCatalog
VisualSettingsValidator.StarGlow(star.Glow, star.Id);
stars.Add(new(stars.Count, star.Name, offset + Vector(star.PositionMeters), star.RadiusMeters, star.LuminositySolar,
Color(star.Color), star.Seed ?? StableSeed(definition.Id + "/" + star.Id))
{ Id = star.Id, Glow = star.Glow });
{ Id = star.Id, Glow = star.Glow, MassKg = star.MassKg });
Positive(star.MassKg, "star mass");
}
var planets = new List<PlanetData>();
foreach (var planet in definition.Planets) planets.Add(BuildPlanet(planet, definition.Id, stars, planets.Count, null, ids));
@@ -76,10 +77,21 @@ public sealed class KnownSolarSystemCatalog
foreach (var planet in planets)
{
CheckBounds(planet.PositionMeters, System.Math.Max(planet.MaximumTerrainRadiusMeters, planet.Rings?.OuterRadiusMeters ?? 0), cellSize, planet.Id);
if (planet.Orbit is { } orbit)
{
var host = planet.ParentPlanetIndex is { } parent ? planets[parent] : null;
var star = stars[planet.HostStarIndex];
var extent = orbit.SemiMajorAxisMeters * (1 + orbit.Eccentricity) + planet.MaximumTerrainRadiusMeters;
if (host?.Orbit is { } parentOrbit) extent += parentOrbit.SemiMajorAxisMeters * (1 + parentOrbit.Eccentricity);
CheckBounds(star.PositionMeters, extent, cellSize, planet.Id + " orbit");
if (orbit.SemiMajorAxisMeters * (1 - orbit.Eccentricity) <= (host?.MaximumTerrainRadiusMeters ?? star.RadiusMeters) + planet.MaximumTerrainRadiusMeters)
throw new InvalidDataException($"'{planet.Id}': periapsis intersects its host body.");
}
if (stars.Any(s => (s.PositionMeters - planet.PositionMeters).Length <= s.RadiusMeters + planet.MaximumTerrainRadiusMeters))
throw new InvalidDataException($"Planet '{planet.Id}' overlaps a star.");
}
return new(coordinate, definition.Seed ?? StableSeed(definition.Id), stars, planets) { Id = definition.Id, Name = definition.Name };
return new(coordinate, definition.Seed ?? StableSeed(definition.Id), stars, planets)
{ Id = definition.Id, Name = definition.Name, EpochSecondsSinceJ2000 = (definition.EpochUtc - Physics.SimulationClock.J2000).TotalSeconds };
}
private PlanetData BuildPlanet(PlanetDefinition p, string systemId, List<StarData> stars, int index, PlanetData? parent, HashSet<string> ids)
@@ -110,7 +122,11 @@ public sealed class KnownSolarSystemCatalog
throw new InvalidDataException($"'{p.Id}': gas planets use gas volume settings, not a terrain heightmap or terrain amplitude.");
if (terrain.NoiseMask == TerrainNoiseMask.HeightmapLand && heightmap is null)
throw new InvalidDataException($"'{p.Id}': heightmapLand noise mask requires a heightmap.");
var position = (parent?.PositionMeters ?? stars[host].PositionMeters) + OrbitPosition(p.Orbit);
Positive(p.MassKg, "planet mass");
if (!double.IsFinite(p.RotationPeriodHours * 3600) || p.RotationPeriodHours == 0 || !double.IsFinite(p.PoleLongitudeDegrees))
throw new InvalidDataException($"'{p.Id}': rotationPeriodHours must be finite and nonzero; pole longitude must be finite.");
var orbit = BuildOrbit(p.Orbit, (parent?.MassKg ?? stars[host].MassKg) + p.MassKg);
var position = (parent?.PositionMeters ?? stars[host].PositionMeters) + orbit.Position(0);
var surfaceTexture = p.SurfaceTexture is null ? null : LoadColor(p.SurfaceTexture);
var planetSeed = p.Seed ?? StableSeed(systemId + "/" + p.Id);
var clouds = p.Clouds ?? CloudSettings.ForAtmosphere(planetSeed, air.HeightMeters, air.Density, p.SurfaceType);
@@ -131,6 +147,9 @@ public sealed class KnownSolarSystemCatalog
p.Orbit.PhaseDegrees * System.Math.PI / 180, host, p.Seed ?? StableSeed(systemId + "/" + p.Id))
{
Id = p.Id,
MassKg = p.MassKg,
Orbit = orbit,
Rotation = new(p.RotationPeriodHours * 3600, p.AxialTiltDegrees, p.RotationDegrees, p.PoleLongitudeDegrees),
Description = p.Description,
ParentPlanetIndex = parent?.Index,
SurfaceType = p.SurfaceType,
@@ -145,7 +164,7 @@ public sealed class KnownSolarSystemCatalog
OceanMask = p.Ocean is { Enabled: true, MaskFromSurfaceTexture: true } ? OceanSurface.CreateMask(surfaceTexture!) : null,
AtmosphereDensity = air.Density,
AtmosphereComposition = air.Composition,
Orientation = new(p.AxialTiltDegrees, p.RotationDegrees),
Orientation = new(p.AxialTiltDegrees, p.RotationDegrees, p.PoleLongitudeDegrees),
Terrain = new()
{
HeightMap = heightmap,
@@ -185,22 +204,27 @@ public sealed class KnownSolarSystemCatalog
return result;
}
private static DVector3 OrbitPosition(OrbitDefinition orbit)
private static Physics.KeplerOrbit BuildOrbit(OrbitDefinition orbit, double massKg)
{
Positive(orbit.SemiMajorAxisAu, "semi-major axis");
if (!Finite(orbit.Eccentricity, orbit.PhaseDegrees, orbit.InclinationDegrees, orbit.AscendingNodeDegrees, orbit.PeriapsisDegrees)
|| orbit.Eccentricity is < 0 or >= 1) throw new InvalidDataException("Only finite elliptical orbits with eccentricity in [0,1) are supported.");
var phase = orbit.PhaseDegrees * System.Math.PI / 180;
var distance = orbit.SemiMajorAxisAu * UniverseScale.AstronomicalUnitMeters * (1 - orbit.Eccentricity * orbit.Eccentricity)
/ (1 + orbit.Eccentricity * System.Math.Cos(phase));
var angle = phase + orbit.PeriapsisDegrees * System.Math.PI / 180;
var axis = orbit.SemiMajorAxisAu * UniverseScale.AstronomicalUnitMeters;
var period = orbit.PeriodDays is { } days ? days * 86400 : Physics.KeplerOrbit.Period(axis, massKg);
Positive(period, "orbit period");
if (orbit.MeanAnomalyDegrees is { } mean && !double.IsFinite(mean)) throw new InvalidDataException("meanAnomalyDegrees must be finite.");
var anomaly = orbit.MeanAnomalyDegrees is { } m ? m * System.Math.PI / 180 : Physics.KeplerOrbit.MeanAnomalyFromTrue(phase, orbit.Eccentricity);
var angle = orbit.PeriapsisDegrees * System.Math.PI / 180;
var inclination = orbit.InclinationDegrees * System.Math.PI / 180;
var node = orbit.AscendingNodeDegrees * System.Math.PI / 180;
var x = System.Math.Cos(angle) * distance;
var z = System.Math.Sin(angle) * distance;
var y = z * System.Math.Sin(inclination);
z *= System.Math.Cos(inclination);
return new(x * System.Math.Cos(node) - z * System.Math.Sin(node), y, x * System.Math.Sin(node) + z * System.Math.Cos(node));
DVector3 Basis(double a)
{
var x = System.Math.Cos(a); var z = System.Math.Sin(a) * System.Math.Cos(inclination);
return new(x * System.Math.Cos(node) - z * System.Math.Sin(node), System.Math.Sin(a) * System.Math.Sin(inclination),
x * System.Math.Sin(node) + z * System.Math.Cos(node));
}
return new(axis, orbit.Eccentricity, anomaly, period, Basis(angle), Basis(angle + System.Math.PI * .5));
}
private string AssetPath(string relative)

View File

@@ -18,6 +18,7 @@ public sealed record SolarSystemDefinition
public required string Name { get; init; }
public required SectorDefinition Sector { get; init; }
public ulong? Seed { get; init; }
public DateTimeOffset EpochUtc { get; init; } = Physics.SimulationClock.J2000;
public double[] CenterOffsetMeters { get; init; } = [0, 0, 0];
public required List<StarDefinition> Stars { get; init; }
public required List<PlanetDefinition> Planets { get; init; }
@@ -31,6 +32,7 @@ public sealed record StarDefinition
public double[] PositionMeters { get; init; } = [0, 0, 0];
public required double RadiusMeters { get; init; }
public double LuminositySolar { get; init; } = 1;
public double MassKg { get; init; } = 1.98847e30;
public float[] Color { get; init; } = [1, 0.94f, 0.85f];
public ulong? Seed { get; init; }
}
@@ -40,6 +42,8 @@ public sealed record OrbitDefinition
public required double SemiMajorAxisAu { get; init; }
public double Eccentricity { get; init; }
public double PhaseDegrees { get; init; }
public double? MeanAnomalyDegrees { get; init; }
public double? PeriodDays { get; init; }
public double InclinationDegrees { get; init; }
public double AscendingNodeDegrees { get; init; }
public double PeriapsisDegrees { get; init; }
@@ -107,6 +111,9 @@ public sealed record PlanetDefinition
public ulong? Seed { get; init; }
public double AxialTiltDegrees { get; init; }
public double RotationDegrees { get; init; }
public double MassKg { get; init; } = 5.97217e24;
public double RotationPeriodHours { get; init; } = 24;
public double PoleLongitudeDegrees { get; init; }
public PlanetSurfaceType SurfaceType { get; init; }
public GasVolumeSettings Gas { get; init; } = new();
public double CloudBandFrequency { get; init; } = 28;

View File

@@ -1,5 +1,6 @@
using Universe.Core.Math;
using Universe.Core.World;
using Universe.Core.Physics;
namespace Universe.Core.Generation;
@@ -13,6 +14,7 @@ public sealed record StarData(
ulong Seed)
{
public string Id { get; init; } = $"star-{Index}";
public double MassKg { get; init; } = 1.98847e30;
public StarGlowSettings Glow { get; init; } = new();
public BlackHoleData? BlackHole { get; init; }
public bool IsBlackHole => BlackHole is not null;
@@ -33,6 +35,9 @@ public sealed record PlanetData(
ulong Seed)
{
public string Id { get; init; } = $"planet-{Index}";
public double MassKg { get; init; } = 5.97217e24;
public KeplerOrbit? Orbit { get; init; }
public AxialRotation? Rotation { get; init; }
public string Description { get; init; } = string.Empty;
public PlanetSurfaceType SurfaceType { get; init; }
public bool IsGasPlanet => SurfaceType is PlanetSurfaceType.GasGiant or PlanetSurfaceType.IceGiant;
@@ -65,6 +70,7 @@ public sealed record StarSystemData(
{
public string Id { get; init; } = $"procedural:{Coordinate.X},{Coordinate.Y},{Coordinate.Z}";
public string Name { get; init; } = "Proceduriellt solsystem";
public double EpochSecondsSinceJ2000 { get; init; }
public string? GalaxyId { get; init; }
public string? NebulaId { get; init; }
}

View File

@@ -32,30 +32,36 @@ public sealed record PlanetRings(double InnerRadiusMeters, double OuterRadiusMet
public RingPreviewSettings Preview { get; init; } = new();
}
/// <summary>Fixed body orientation. Terrain, texture UVs, ring planes and collision use this same transform.</summary>
/// <summary>Body orientation shared by terrain, texture UVs, ring planes and collision.</summary>
public sealed class PlanetOrientation
{
public static readonly PlanetOrientation Identity = new(0, 0);
private readonly double _sinTilt, _cosTilt, _sinSpin, _cosSpin;
private readonly double _sinPole, _cosPole;
public double AxialTiltDegrees { get; }
public double RotationDegrees { get; }
public double PoleLongitudeDegrees { get; }
public Matrix4x4 BodyToWorld { get; }
public Matrix4x4 WorldToBody { get; }
public PlanetOrientation(double axialTiltDegrees, double rotationDegrees)
public PlanetOrientation(double axialTiltDegrees, double rotationDegrees, double poleLongitudeDegrees = 0)
{
AxialTiltDegrees = axialTiltDegrees;
RotationDegrees = rotationDegrees;
PoleLongitudeDegrees = poleLongitudeDegrees;
var tilt = axialTiltDegrees * System.Math.PI / 180;
var spin = rotationDegrees * System.Math.PI / 180;
(_sinTilt, _cosTilt) = System.Math.SinCos(tilt);
(_sinSpin, _cosSpin) = System.Math.SinCos(spin);
BodyToWorld = Matrix4x4.CreateRotationY((float)spin) * Matrix4x4.CreateRotationZ((float)tilt);
var pole = poleLongitudeDegrees * System.Math.PI / 180;
(_sinPole, _cosPole) = System.Math.SinCos(pole);
BodyToWorld = Matrix4x4.CreateRotationY((float)spin) * Matrix4x4.CreateRotationZ((float)tilt) * Matrix4x4.CreateRotationY((float)pole);
WorldToBody = Matrix4x4.Transpose(BodyToWorld);
}
public DVector3 ToBody(DVector3 d)
{
d = new(_cosPole * d.X - _sinPole * d.Z, d.Y, _sinPole * d.X + _cosPole * d.Z);
var x = _cosTilt * d.X + _sinTilt * d.Y;
var y = -_sinTilt * d.X + _cosTilt * d.Y;
return new(_cosSpin * x - _sinSpin * d.Z, y, _sinSpin * x + _cosSpin * d.Z);
@@ -65,6 +71,7 @@ public sealed class PlanetOrientation
{
var x = _cosSpin * d.X + _sinSpin * d.Z;
var z = -_sinSpin * d.X + _cosSpin * d.Z;
return new(_cosTilt * x - _sinTilt * d.Y, _sinTilt * x + _cosTilt * d.Y, z);
var tx = _cosTilt * x - _sinTilt * d.Y;
return new(_cosPole * tx + _sinPole * z, _sinTilt * x + _cosTilt * d.Y, -_sinPole * tx + _cosPole * z);
}
}

View File

@@ -1,6 +1,7 @@
using System.Numerics;
using Universe.Core.Math;
using Universe.Core.World;
using Universe.Core.Physics;
using Universe.Core.Configuration;
namespace Universe.Core.Generation;
@@ -68,7 +69,7 @@ public sealed class UniverseGenerator
stars.Add(new(i, $"{coordinate.X}:{coordinate.Y}:{coordinate.Z} Star {i + 1}", position,
UniverseScale.SolarRadiusMeters * scale, System.Math.Pow(scale, 3.5),
StarColors[random.NextInt(0, StarColors.Length)], DeterministicRandom.Mix(systemSeed + (ulong)i + 1))
{ Id = $"procedural:{coordinate.X},{coordinate.Y},{coordinate.Z}:star:{i}" });
{ Id = $"procedural:{coordinate.X},{coordinate.Y},{coordinate.Z}:star:{i}", MassKg = 1.98847e30 * scale });
}
if (hasBlackHole)
@@ -77,7 +78,7 @@ public sealed class UniverseGenerator
var hole = new BlackHoleData(mass, UnitVector(ref holeRandom), BlackHoles.DiskOuterRadiusInHorizons,
BlackHoles.DiskBrightness, BlackHoles.RaySteps);
stars[0] = stars[0] with { Name = $"{coordinate.X}:{coordinate.Y}:{coordinate.Z} Black Hole", RadiusMeters = hole.HorizonRadiusMeters,
LuminositySolar = .8, Color = new(1f, .73f, .44f), BlackHole = hole };
LuminositySolar = .8, Color = new(1f, .73f, .44f), BlackHole = hole, MassKg = mass * 1.98847e30 };
}
var planetCount = random.NextInt(5, 10);
@@ -113,6 +114,9 @@ public sealed class UniverseGenerator
orbitRadius, phase, 0, seed)
{
Id = $"procedural:{coordinate.X},{coordinate.Y},{coordinate.Z}:planet:{i}",
MassKg = 5.97217e24 * System.Math.Pow(radius / UniverseScale.EarthRadiusMeters, 3),
Orbit = MovingOrbit(position - stars[0].PositionMeters, orbitNormal, eccentricity, phase, stars[0].MassKg),
Rotation = new((10 + weather.NextDouble() * 40) * 3600, weather.NextDouble() * 60, weather.NextDouble() * 360),
AtmosphereDensity = atmosphereDensity,
Clouds = CloudSettings.ForAtmosphere(seed, radius * .018, atmosphereDensity, PlanetSurfaceType.Rocky)
});
@@ -132,6 +136,9 @@ public sealed class UniverseGenerator
moonOrbit, random.NextDouble() * System.Math.Tau, parent.HostStarIndex, moonSeed)
{
ParentPlanetIndex = parent.Index, Id = parent.Id + ":moon", AtmosphereDensity = .18,
MassKg = parent.MassKg * System.Math.Pow(moonRadius / parent.RadiusMeters, 3) * .6,
Orbit = MovingOrbit(moonDirection * moonOrbit, planeNormal, 0, 0, parent.MassKg),
Rotation = new(KeplerOrbit.Period(moonOrbit, parent.MassKg), 6, 0),
Clouds = CloudSettings.ForAtmosphere(moonSeed, atmosphereHeight, .18, PlanetSurfaceType.Rocky)
});
moonCount++;
@@ -165,6 +172,17 @@ public sealed class UniverseGenerator
return new(r * System.Math.Cos(phi), y, r * System.Math.Sin(phi));
}
private static KeplerOrbit MovingOrbit(DVector3 initial, DVector3 normal, double eccentricity, double phase, double mass)
{
var direction = initial.Normalized();
var tangent = DVector3.Cross(normal, direction).Normalized();
if (tangent.LengthSquared < .1) tangent = DVector3.Cross(new(1, 0, 0), direction).Normalized();
var axis = initial.Length * (1 + eccentricity * System.Math.Cos(phase)) / (1 - eccentricity * eccentricity);
return new(axis, eccentricity, KeplerOrbit.MeanAnomalyFromTrue(phase, eccentricity), KeplerOrbit.Period(axis, mass),
direction * System.Math.Cos(phase) - tangent * System.Math.Sin(phase),
direction * System.Math.Sin(phase) + tangent * System.Math.Cos(phase));
}
private static string RomanNumeral(int number) => number switch
{
1 => "I",

View File

@@ -0,0 +1,71 @@
using Universe.Core.Generation;
using Universe.Core.Math;
namespace Universe.Core.Physics;
/// <summary>Analytic two-body orbit, referenced to its system epoch. U/V span the orbital plane.</summary>
public sealed record KeplerOrbit(double SemiMajorAxisMeters, double Eccentricity, double MeanAnomalyAtEpochRadians,
double PeriodSeconds, DVector3 U, DVector3 V)
{
public const double GravitationalConstant = 6.67430e-11;
public static double Period(double semiMajorAxisMeters, double massKg) =>
System.Math.Tau * System.Math.Sqrt(semiMajorAxisMeters * semiMajorAxisMeters * semiMajorAxisMeters / (GravitationalConstant * massKg));
public DVector3 Position(double seconds)
{
var m = System.Math.IEEERemainder(MeanAnomalyAtEpochRadians + System.Math.Tau * (seconds % PeriodSeconds) / PeriodSeconds, System.Math.Tau);
// Bisection is bounded and robust even for e very close to 1; Newton accelerates ordinary orbits.
var lo = -System.Math.PI; var hi = System.Math.PI; var e = m;
for (var i = 0; i < 64; i++)
{
var error = e - Eccentricity * System.Math.Sin(e) - m;
if (System.Math.Abs(error) < 1e-14) break;
if (error > 0) hi = e; else lo = e;
var next = e - error / (1 - Eccentricity * System.Math.Cos(e));
e = next > lo && next < hi ? next : (lo + hi) * .5;
}
return U * (SemiMajorAxisMeters * (System.Math.Cos(e) - Eccentricity))
+ V * (SemiMajorAxisMeters * System.Math.Sqrt(1 - Eccentricity * Eccentricity) * System.Math.Sin(e));
}
public static double MeanAnomalyFromTrue(double trueAnomaly, double eccentricity)
{
var e = System.Math.Atan2(System.Math.Sqrt(1 - eccentricity * eccentricity) * System.Math.Sin(trueAnomaly),
eccentricity + System.Math.Cos(trueAnomaly));
return e - eccentricity * System.Math.Sin(e);
}
}
/// <summary>Positive period turns about the angular-momentum pole; tilt > 90 degrees gives retrograde spin.</summary>
public sealed record AxialRotation(double PeriodSeconds, double AxialTiltDegrees, double PhaseDegrees, double PoleLongitudeDegrees = 0)
{
public PlanetOrientation At(double seconds) => new(AxialTiltDegrees,
PhaseDegrees - 360 * (seconds % PeriodSeconds) / PeriodSeconds, PoleLongitudeDegrees);
}
public static class CelestialSimulation
{
public static StarSystemData Evaluate(StarSystemData system, double secondsSinceJ2000)
{
if (system.Planets.Count == 0) return system;
var time = secondsSinceJ2000 - system.EpochSecondsSinceJ2000;
var planets = new PlanetData[system.Planets.Count];
// Catalog/generator place parents before their satellites.
for (var i = 0; i < planets.Length; i++)
{
var p = system.Planets[i];
var position = p.PositionMeters;
if (p.Orbit is { } orbit)
{
var center = p.ParentPlanetIndex is { } parent ? planets[parent].PositionMeters : system.Stars[p.HostStarIndex].PositionMeters;
position = center + orbit.Position(time);
}
planets[i] = p with
{
PositionMeters = position,
Orientation = p.Rotation?.At(time) ?? p.Orientation
};
}
return system with { Planets = planets };
}
}

View File

@@ -0,0 +1,45 @@
namespace Universe.Core.Physics;
public sealed record SimulationSettings
{
public DateTimeOffset StartUtc { get; init; } = SimulationClock.J2000;
public double TimeScale { get; init; } = 1;
public void Validate()
{
if (!double.IsFinite(TimeScale) || TimeScale is < 0 or > 100)
throw new InvalidDataException("simulation.timeScale must be between 0 and 100.");
}
}
/// <summary>World time only: camera input, player movement and UI use unscaled time.</summary>
public sealed class SimulationClock
{
public static readonly DateTimeOffset J2000 = new(2000, 1, 1, 12, 0, 0, TimeSpan.Zero);
private double _timeScale;
public double SecondsSinceJ2000 { get; private set; }
public double ElapsedSeconds { get; private set; }
public DateTimeOffset Utc => J2000.AddSeconds(SecondsSinceJ2000);
public double TimeScale
{
get => _timeScale;
set => _timeScale = double.IsFinite(value) ? System.Math.Clamp(value, 0, 100)
: throw new ArgumentOutOfRangeException(nameof(value));
}
public SimulationClock(SimulationSettings settings)
{
settings.Validate();
SecondsSinceJ2000 = (settings.StartUtc - J2000).TotalSeconds;
TimeScale = settings.TimeScale;
}
public void Advance(double realSeconds)
{
if (!double.IsFinite(realSeconds) || realSeconds < 0) throw new ArgumentOutOfRangeException(nameof(realSeconds));
// Do not clamp frame time: a slow frame must not slow the astronomical clock.
var step = realSeconds * TimeScale;
SecondsSinceJ2000 += step;
ElapsedSeconds += step;
}
}

View File

@@ -1,4 +1,5 @@
using Universe.Core.Generation;
using Universe.Core.Physics;
namespace Universe.Core.World;
@@ -10,6 +11,10 @@ public sealed class UniverseStreamer(UniverseGenerator generator)
private readonly Dictionary<SectorCoordinate, StarSystemData> _cache = [];
private SectorCoordinate? _nearBlock;
private IReadOnlyList<StarSystemData> _nearSystems = [];
public double SimulationSeconds { get; set; }
public StarSystemData AtCurrentTime(StarSystemData system) => CelestialSimulation.Evaluate(system, SimulationSeconds);
public UniverseNeighborhood Refresh(UniverseNeighborhood neighborhood) => new(
AtCurrentTime(neighborhood.ActiveSystem), neighborhood.NearbySystems.Select(AtCurrentTime).ToArray());
public (StarSystemData System, StarData Hole)? FindNearestBlackHole(UniversePosition position)
{
@@ -67,7 +72,7 @@ public sealed class UniverseStreamer(UniverseGenerator generator)
var keep = _nearSystems.Select(s => s.Coordinate).Append(activeCoordinate).ToHashSet();
foreach (var key in _cache.Keys.ToArray()) if (!keep.Contains(key)) _cache.Remove(key);
}
return new(active, _nearSystems.Where(s => s.Coordinate != activeCoordinate).ToArray());
return Refresh(new(active, _nearSystems.Where(s => s.Coordinate != activeCoordinate).ToArray()));
}
var nearby = new List<StarSystemData>(26);
for (var z = -1; z <= 1; z++)
@@ -88,7 +93,7 @@ public sealed class UniverseStreamer(UniverseGenerator generator)
_cache.Remove(key);
}
return new(active, nearby);
return Refresh(new(active, nearby));
}
private StarSystemData GetOrCreate(SectorCoordinate coordinate)