init
This commit is contained in:
224
src/Universe.App/Runtime/BenchmarkRunner.cs
Normal file
224
src/Universe.App/Runtime/BenchmarkRunner.cs
Normal file
@@ -0,0 +1,224 @@
|
||||
using Universe.Core.Generation;
|
||||
using Universe.Core.Math;
|
||||
using Universe.Core.Physics;
|
||||
using Universe.Core.World;
|
||||
|
||||
namespace Universe.App.Runtime;
|
||||
|
||||
/// <summary>Repeatable camera path and frame-time capture for a selected planetary terrain scene.</summary>
|
||||
internal sealed class BenchmarkRunner
|
||||
{
|
||||
private const double SpaceSeconds = 8;
|
||||
private const double DescentSeconds = 40;
|
||||
private const double SurfaceSeconds = 24;
|
||||
private const double TotalSeconds = SpaceSeconds + DescentSeconds + SurfaceSeconds;
|
||||
private const double GraphSampleSeconds = 0.2;
|
||||
private const double BrakingAltitudeMeters = 10_000;
|
||||
|
||||
private readonly StarSystemData _system;
|
||||
private readonly PlanetData _planet;
|
||||
private readonly UniversePosition _systemCenter;
|
||||
private readonly UniversePosition _body;
|
||||
private readonly UniversePosition _returnPosition;
|
||||
private readonly DVector3 _returnForward;
|
||||
private readonly DVector3 _returnUp;
|
||||
private readonly double _cellSize;
|
||||
private readonly DVector3 _targetRadial;
|
||||
private readonly DVector3 _spaceRadial;
|
||||
private readonly DVector3 _spaceUp;
|
||||
private readonly DVector3 _orbitTangent;
|
||||
private readonly DVector3 _orbitBitangent;
|
||||
private readonly List<double> _frameRates = [];
|
||||
private readonly List<double> _graph = [];
|
||||
private double _elapsed;
|
||||
private double _renderElapsed;
|
||||
private double _sampleElapsed;
|
||||
private double _sampleFpsSum;
|
||||
private int _sampleFrameCount;
|
||||
private bool _completionFrameRecorded;
|
||||
private bool _stopped;
|
||||
|
||||
public BenchmarkRunner(StarSystemData system, PlanetData planet, double cellSizeMeters, FlyCamera camera,
|
||||
UniversePosition returnPosition, DVector3 returnForward, DVector3 returnUp)
|
||||
{
|
||||
_system = system;
|
||||
_planet = planet;
|
||||
_cellSize = cellSizeMeters;
|
||||
_systemCenter = UniversePosition.AtCellCenter(system.Coordinate, cellSizeMeters);
|
||||
_body = _systemCenter.Add(_planet.PositionMeters, cellSizeMeters);
|
||||
_returnPosition = returnPosition;
|
||||
_returnForward = returnForward.Normalized();
|
||||
_returnUp = returnUp.Normalized();
|
||||
|
||||
DVector3 bodyDirection;
|
||||
if (_planet.Id.Equals("earth", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// The installed Copernicus tile covers the Alps. Its longitude is mirrored
|
||||
// into the planet's local direction frame by the shared heightmap mapping.
|
||||
const double latitude = 46.5;
|
||||
const double longitude = -7.5;
|
||||
var lat = latitude * System.Math.PI / 180;
|
||||
var lon = longitude * System.Math.PI / 180;
|
||||
bodyDirection = new(System.Math.Cos(lat) * System.Math.Cos(lon), System.Math.Sin(lat),
|
||||
System.Math.Cos(lat) * System.Math.Sin(lon));
|
||||
}
|
||||
else
|
||||
{
|
||||
var random = new DeterministicRandom(DeterministicRandom.Mix(_planet.Seed ^ 0x42454E43484D4152UL));
|
||||
var y = random.NextDouble() * 2 - 1;
|
||||
var longitude = random.NextDouble() * System.Math.Tau;
|
||||
var latitudeRadius = System.Math.Sqrt(1 - y * y);
|
||||
bodyDirection = new(latitudeRadius * System.Math.Cos(longitude), y, latitudeRadius * System.Math.Sin(longitude));
|
||||
}
|
||||
_targetRadial = _planet.Orientation.ToWorld(bodyDirection).Normalized();
|
||||
_spaceRadial = _targetRadial;
|
||||
_spaceUp = Perpendicular(-_spaceRadial);
|
||||
_orbitTangent = Perpendicular(_targetRadial);
|
||||
_orbitBitangent = DVector3.Cross(_targetRadial, _orbitTangent).Normalized();
|
||||
|
||||
var initialRadius = _planet.RadiusMeters * 4.8;
|
||||
camera.SetPlayerPose(_body.Add(_spaceRadial * initialRadius, cellSizeMeters), -_spaceRadial, _spaceUp);
|
||||
}
|
||||
|
||||
public bool IsRunning => !_stopped && _elapsed < TotalSeconds;
|
||||
public bool IsComplete => !_stopped && _elapsed >= TotalSeconds;
|
||||
public bool HasRun => true;
|
||||
public double ElapsedSeconds => System.Math.Min(_elapsed, TotalSeconds);
|
||||
public double DurationSeconds => TotalSeconds;
|
||||
public long GraphRevision { get; private set; }
|
||||
public IReadOnlyList<double> GraphSamples => _graph;
|
||||
public string TargetLabel => _planet.Id.Equals("earth", StringComparison.OrdinalIgnoreCase) ? "EARTH" : _planet.Name;
|
||||
public double TargetEarthRadii => _planet.RadiusMeters / UniverseScale.EarthRadiusMeters;
|
||||
public double CurrentFps { get; private set; }
|
||||
public double AverageFps => _renderElapsed <= 0 ? 0 : _frameRates.Count / _renderElapsed;
|
||||
public double MinimumFps => _frameRates.Count == 0 ? 0 : _frameRates.Min();
|
||||
public double OnePercentLowFps
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_frameRates.Count == 0) return 0;
|
||||
var count = System.Math.Max(1, (int)System.Math.Ceiling(_frameRates.Count * .01));
|
||||
return _frameRates.Order().Take(count).Average();
|
||||
}
|
||||
}
|
||||
|
||||
public string Phase => _stopped ? "STOPPED"
|
||||
: IsComplete ? "COMPLETE"
|
||||
: _elapsed < SpaceSeconds ? "SPACE VIEW"
|
||||
: _elapsed < SpaceSeconds + DescentSeconds ? "DESCENT"
|
||||
: "GROUND WALK";
|
||||
|
||||
public void Stop() => _stopped = true;
|
||||
|
||||
public void Update(double deltaSeconds, FlyCamera camera)
|
||||
{
|
||||
if (!IsRunning || !double.IsFinite(deltaSeconds) || deltaSeconds <= 0) return;
|
||||
_elapsed = System.Math.Min(TotalSeconds, _elapsed + System.Math.Min(deltaSeconds, .1));
|
||||
|
||||
UniversePosition proposed;
|
||||
DVector3 radial;
|
||||
DVector3 forward;
|
||||
DVector3 up;
|
||||
var finalRadius = TerrainHeight.Sample(_planet, _targetRadial) + 1.8;
|
||||
|
||||
if (_elapsed <= SpaceSeconds)
|
||||
{
|
||||
radial = _spaceRadial;
|
||||
proposed = _body.Add(radial * (_planet.RadiusMeters * 4.8), _cellSize);
|
||||
forward = -radial;
|
||||
up = _spaceUp;
|
||||
}
|
||||
else if (_elapsed < SpaceSeconds + DescentSeconds)
|
||||
{
|
||||
var t = System.Math.Clamp((_elapsed - SpaceSeconds) / DescentSeconds, 0, 1);
|
||||
// Keep the long, high-speed part of the approach far above the surface.
|
||||
// The final 10 km get their own slow curve so collision resolution never
|
||||
// has to snap a fast-moving camera down onto the ground.
|
||||
var startRadius = _planet.RadiusMeters * 4.8;
|
||||
var brakingRadius = finalRadius + BrakingAltitudeMeters;
|
||||
var radius = t < .5
|
||||
? startRadius + (brakingRadius - startRadius) * Smooth(t * 2)
|
||||
: finalRadius + BrakingAltitudeMeters * (1 - SmootherStep((t - .5) * 2));
|
||||
radial = _targetRadial;
|
||||
proposed = _body.Add(radial * radius, _cellSize);
|
||||
var landingLook = (_orbitBitangent * System.Math.Cos(.25) - radial * System.Math.Sin(.25)).Normalized();
|
||||
forward = Slerp(-radial, landingLook, SmootherStep(System.Math.Clamp((t - .28) / .72, 0, 1)));
|
||||
up = (radial - forward * DVector3.Dot(radial, forward)).Normalized();
|
||||
}
|
||||
else
|
||||
{
|
||||
var t = System.Math.Clamp((_elapsed - SpaceSeconds - DescentSeconds) / SurfaceSeconds, 0, 1);
|
||||
var angle = t * System.Math.Tau;
|
||||
var walkRadius = 16 * Smooth(t); // Starts at the exact landing point without a lateral jump.
|
||||
var orbitAngle = walkRadius / _planet.RadiusMeters;
|
||||
radial = (_targetRadial * System.Math.Cos(orbitAngle)
|
||||
+ (_orbitTangent * System.Math.Cos(angle) + _orbitBitangent * System.Math.Sin(angle)) * System.Math.Sin(orbitAngle)).Normalized();
|
||||
var terrainRadius = TerrainHeight.Sample(_planet, radial);
|
||||
var radius = System.Math.Max(terrainRadius + 1.8, _planet.RadiusMeters + 1.8);
|
||||
proposed = _body.Add(radial * radius, _cellSize);
|
||||
var walkingDirection = (_orbitTangent * -System.Math.Sin(angle) + _orbitBitangent * System.Math.Cos(angle)).Normalized();
|
||||
var headTurn = .48 * System.Math.Sin(t * System.Math.Tau * 1.5);
|
||||
var pitch = -.25 + .18 * System.Math.Sin(t * System.Math.Tau * 2);
|
||||
var lookingDirection = RotateAround(walkingDirection, radial, headTurn);
|
||||
forward = (lookingDirection * System.Math.Cos(pitch) + radial * System.Math.Sin(pitch)).Normalized();
|
||||
up = (radial - forward * DVector3.Dot(radial, forward)).Normalized();
|
||||
}
|
||||
|
||||
if (up.LengthSquared < .1) up = Perpendicular(forward);
|
||||
var collision = PlanetCollisionSolver.Sweep(camera.Position, proposed, _systemCenter, _system.Planets,
|
||||
_cellSize, cameraRadiusMeters: 1.5, samples: 32);
|
||||
if (_elapsed >= TotalSeconds)
|
||||
{
|
||||
camera.SetPlayerPose(_returnPosition, _returnForward, _returnUp);
|
||||
return;
|
||||
}
|
||||
var finalPosition = proposed;
|
||||
if (collision.Collided)
|
||||
{
|
||||
var contactRadial = collision.Position.RelativeTo(_body, _cellSize).Normalized();
|
||||
finalPosition = _body.Add(contactRadial * (TerrainHeight.Sample(_planet, contactRadial) + 1.8), _cellSize);
|
||||
}
|
||||
camera.SetPlayerPose(finalPosition, forward, up);
|
||||
}
|
||||
|
||||
public void RecordFrame(double deltaSeconds)
|
||||
{
|
||||
if (_stopped || _elapsed >= TotalSeconds && _completionFrameRecorded || !double.IsFinite(deltaSeconds) || deltaSeconds <= 0)
|
||||
return;
|
||||
var frameSeconds = System.Math.Clamp(deltaSeconds, .0001, 1);
|
||||
CurrentFps = 1 / frameSeconds;
|
||||
_frameRates.Add(CurrentFps);
|
||||
_renderElapsed += frameSeconds;
|
||||
_sampleElapsed += frameSeconds;
|
||||
_sampleFpsSum += CurrentFps;
|
||||
_sampleFrameCount++;
|
||||
if (_sampleElapsed >= GraphSampleSeconds || _elapsed >= TotalSeconds)
|
||||
{
|
||||
_graph.Add(_sampleFrameCount == 0 ? CurrentFps : _sampleFpsSum / _sampleFrameCount);
|
||||
_sampleElapsed = 0;
|
||||
_sampleFpsSum = 0;
|
||||
_sampleFrameCount = 0;
|
||||
GraphRevision++;
|
||||
}
|
||||
if (_elapsed >= TotalSeconds) _completionFrameRecorded = true;
|
||||
}
|
||||
|
||||
private static double Smooth(double t) => t * t * (3 - 2 * t);
|
||||
private static double SmootherStep(double t) => t * t * t * (t * (t * 6 - 15) + 10);
|
||||
private static DVector3 Perpendicular(DVector3 direction) =>
|
||||
DVector3.Cross(direction, System.Math.Abs(direction.Y) < .9 ? new(0, 1, 0) : new DVector3(1, 0, 0)).Normalized();
|
||||
|
||||
private static DVector3 RotateAround(DVector3 vector, DVector3 axis, double angle) =>
|
||||
vector * System.Math.Cos(angle) + DVector3.Cross(axis, vector) * System.Math.Sin(angle)
|
||||
+ axis * DVector3.Dot(axis, vector) * (1 - System.Math.Cos(angle));
|
||||
|
||||
private static DVector3 Slerp(DVector3 from, DVector3 to, double t)
|
||||
{
|
||||
var dot = System.Math.Clamp(DVector3.Dot(from, to), -1, 1);
|
||||
if (dot > .9995) return DVector3.Lerp(from, to, t).Normalized();
|
||||
var angle = System.Math.Acos(dot);
|
||||
var tangent = (to - from * dot).Normalized();
|
||||
if (tangent.LengthSquared < .1) tangent = Perpendicular(from);
|
||||
return (from * System.Math.Cos(angle * t) + tangent * System.Math.Sin(angle * t)).Normalized();
|
||||
}
|
||||
}
|
||||
379
src/Universe.App/Runtime/FlyCamera.cs
Normal file
379
src/Universe.App/Runtime/FlyCamera.cs
Normal file
@@ -0,0 +1,379 @@
|
||||
using Universe.Core.Generation;
|
||||
using Universe.Core.Math;
|
||||
using Universe.Core.Physics;
|
||||
using Universe.Core.World;
|
||||
using Universe.Core.Configuration;
|
||||
|
||||
namespace Universe.App.Runtime;
|
||||
|
||||
public sealed class FlyCamera
|
||||
{
|
||||
private DVector3 _forward = new(0, 0, -1);
|
||||
private DVector3 _up = new(0, 1, 0);
|
||||
private FocusFlight? _flight;
|
||||
private double _flightElapsed;
|
||||
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)
|
||||
{
|
||||
Position = initialPosition;
|
||||
CellSizeMeters = cellSizeMeters;
|
||||
SpeedMetersPerSecond = 250_000;
|
||||
}
|
||||
public UniversePosition Position { get; private set; }
|
||||
public double CellSizeMeters { get; }
|
||||
public double SpeedMetersPerSecond { get; private set; }
|
||||
public bool IsFocusing => _flight is not null;
|
||||
public DVector3 Forward => _forward;
|
||||
public DVector3 Right => DVector3.Cross(_forward, _up).Normalized();
|
||||
public DVector3 Up => DVector3.Cross(Right, _forward).Normalized();
|
||||
|
||||
public void Rotate(double mouseDeltaX, double mouseDeltaY)
|
||||
{
|
||||
if (IsFocusing) return;
|
||||
_forward = RotateAround(_forward, Up, -mouseDeltaX * 0.0022).Normalized();
|
||||
var axis = Right;
|
||||
var beforePitchUp = Up;
|
||||
_forward = RotateAround(_forward, axis, -mouseDeltaY * 0.0022).Normalized();
|
||||
_up = RotateAround(beforePitchUp, axis, -mouseDeltaY * 0.0022).Normalized();
|
||||
}
|
||||
public void AdjustSpeed(double delta) => SpeedMetersPerSecond = System.Math.Clamp(SpeedMetersPerSecond * System.Math.Pow(1.7, delta), 0.1, UniverseScale.LightSpeedMetersPerSecond * 1_000_000);
|
||||
|
||||
public void SetPlayerPose(UniversePosition position, DVector3 forward, DVector3 up)
|
||||
{
|
||||
_flight = null;
|
||||
Position = position; _forward = forward.Normalized(); _up = up.Normalized();
|
||||
}
|
||||
|
||||
public void SetInitialView(StarSystemData system)
|
||||
{
|
||||
var planet = system.Planets[0];
|
||||
var center = UniversePosition.AtCellCenter(system.Coordinate, CellSizeMeters);
|
||||
var sun = (system.Stars[0].PositionMeters - planet.PositionMeters).Normalized();
|
||||
var side = Perpendicular(sun);
|
||||
var radial = (sun * 0.42 + side * 0.907).Normalized();
|
||||
Position = center.Add(planet.PositionMeters + radial * (planet.RadiusMeters * 2.85), CellSizeMeters);
|
||||
_forward = -radial;
|
||||
_up = new(0, 1, 0);
|
||||
}
|
||||
|
||||
public void SetInitialView(StarSystemData system, StartupConfiguration startup)
|
||||
{
|
||||
var planet = system.Planets.FirstOrDefault(p => p.Id == startup.PlanetId)
|
||||
?? throw new InvalidDataException($"Startup planet '{startup.PlanetId}' does not exist in system '{system.Id}'.");
|
||||
var longitude = startup.LongitudeDegrees * System.Math.PI / 180;
|
||||
var latitude = startup.LatitudeDegrees * System.Math.PI / 180;
|
||||
var bodyDirection = new DVector3(System.Math.Cos(latitude) * System.Math.Cos(longitude),
|
||||
System.Math.Sin(latitude), System.Math.Cos(latitude) * System.Math.Sin(longitude));
|
||||
var radial = planet.Orientation.ToWorld(bodyDirection).Normalized();
|
||||
var distance = System.Math.Max(planet.RadiusMeters * startup.DistanceInRadii, planet.MaximumTerrainRadiusMeters + 10);
|
||||
var center = UniversePosition.AtCellCenter(system.Coordinate, CellSizeMeters);
|
||||
Position = center.Add(planet.PositionMeters + radial * distance, CellSizeMeters);
|
||||
_forward = -radial;
|
||||
_up = new(0, 1, 0);
|
||||
if (System.Math.Abs(DVector3.Dot(_forward, _up)) > 0.999) _up = Perpendicular(_forward);
|
||||
SpeedMetersPerSecond = startup.SpeedMetersPerSecond;
|
||||
_defaultFieldOfView = FieldOfViewRadians = startup.FieldOfViewDegrees * System.Math.PI / 180;
|
||||
_flight = null;
|
||||
}
|
||||
|
||||
public void RequestFocus(IEnumerable<StarSystemData> systems) => RequestPlanetView(systems, false);
|
||||
public void RequestFocus(IEnumerable<StarSystemData> systems, string planetId) => RequestPlanetView(systems, false, planetId);
|
||||
public void RequestSurfaceView(IEnumerable<StarSystemData> systems) => RequestPlanetView(systems, true);
|
||||
|
||||
public void RequestCloudView(IEnumerable<StarSystemData> systems)
|
||||
{
|
||||
var available = systems.ToArray();
|
||||
var target = available.SelectMany(s => s.Planets.Where(p => p.Clouds is { Enabled: true, Coverage: > 0 }).Select(p => (System: s, Planet: p)))
|
||||
.MinBy(p => World(p.System, p.Planet.PositionMeters).RelativeTo(Position, CellSizeMeters).Length);
|
||||
if (target.Planet is null) return;
|
||||
var planet = target.Planet; var cloud = planet.Clouds!;
|
||||
var body = World(target.System, planet.PositionMeters);
|
||||
var sun = (target.System.Stars[planet.HostStarIndex].PositionMeters - planet.PositionMeters).Normalized();
|
||||
var side = Perpendicular(sun); var across = DVector3.Cross(sun, side);
|
||||
var current = Position.RelativeTo(body, CellSizeMeters).Normalized();
|
||||
var radial = (sun * .4 + side * .9165).Normalized(); var best = double.NegativeInfinity;
|
||||
for (var i = 0; i < 120; i++)
|
||||
{
|
||||
var angle = i * System.Math.Tau / 120;
|
||||
var candidate = (sun * .4 + (side * System.Math.Cos(angle) + across * System.Math.Sin(angle)) * .9165).Normalized();
|
||||
var score = DVector3.Dot(candidate, current) + (planet.OceanMask?.Sample(planet.Orientation.ToBody(candidate)) ?? 0) * 2;
|
||||
if (score <= best) continue;
|
||||
radial = candidate; best = score;
|
||||
}
|
||||
var height = System.Math.Max(planet.RadiusMeters + cloud.BaseAltitudeMeters + cloud.ThicknessMeters + 500, TerrainHeight.Sample(planet, radial) + 500);
|
||||
var forward = ((sun - radial * DVector3.Dot(sun, radial)).Normalized() - radial * .22).Normalized();
|
||||
Departure? departure = null;
|
||||
var escape = available.SelectMany(s => s.Planets.Where(p => !p.IsGasPlanet).Select(p => (System: s, Planet: p)))
|
||||
.MinBy(p => World(p.System, p.Planet.PositionMeters).RelativeTo(Position, CellSizeMeters).Length / p.Planet.RadiusMeters);
|
||||
if (escape.Planet is { } solid)
|
||||
{
|
||||
var origin = World(escape.System, solid.PositionMeters);
|
||||
var start = Position.RelativeTo(origin, CellSizeMeters);
|
||||
if (start.Length < solid.MaximumTerrainRadiusMeters * 1.03)
|
||||
departure = new(origin, start.Normalized(), body.Add(radial * height, CellSizeMeters).RelativeTo(origin, CellSizeMeters).Normalized(), start.Length, solid.MaximumTerrainRadiusMeters * 1.04);
|
||||
}
|
||||
BeginFlight(body, radial, height, System.Math.Max(height * 1.04, planet.MaximumTerrainRadiusMeters * 1.04), forward, radial, 100, departure: departure);
|
||||
}
|
||||
|
||||
public void RequestOceanView(IEnumerable<StarSystemData> systems)
|
||||
{
|
||||
var available = systems.ToArray();
|
||||
var target = available.SelectMany(s => s.Planets.Where(p => p.Ocean is { Enabled: true }).Select(p => (System: s, Planet: p)))
|
||||
.MinBy(p => World(p.System, p.Planet.PositionMeters).RelativeTo(Position, CellSizeMeters).Length);
|
||||
if (target.Planet is null) return;
|
||||
var planet = target.Planet; var ocean = planet.Ocean!;
|
||||
var body = World(target.System, planet.PositionMeters);
|
||||
var sun = (target.System.Stars[planet.HostStarIndex].PositionMeters - planet.PositionMeters).Normalized();
|
||||
var side = Perpendicular(sun); var bitangent = DVector3.Cross(sun, side);
|
||||
var currentRadial = Position.RelativeTo(body, CellSizeMeters).Normalized();
|
||||
var radial = DVector3.Zero; var score = double.NegativeInfinity;
|
||||
for (var i = 0; i < 120; i++)
|
||||
{
|
||||
var angle = i * System.Math.Tau / 120;
|
||||
var candidate = ((side * System.Math.Cos(angle) + bitangent * System.Math.Sin(angle)) * .925 + sun * .38).Normalized();
|
||||
if (planet.OceanMask is { } mask && mask.Sample(planet.Orientation.ToBody(candidate)) < .99) continue;
|
||||
if (TerrainHeight.Sample(planet, candidate) > planet.RadiusMeters + ocean.SeaLevelMeters - 3) continue;
|
||||
var value = DVector3.Dot(candidate, currentRadial);
|
||||
if (value <= score) continue;
|
||||
radial = candidate; score = value;
|
||||
}
|
||||
if (radial == DVector3.Zero) return;
|
||||
var forward = ((sun - radial * DVector3.Dot(sun, radial)).Normalized() - radial * .085).Normalized();
|
||||
Departure? departure = null;
|
||||
var escape = available.SelectMany(s => s.Planets.Where(p => !p.IsGasPlanet).Select(p => (System: s, Planet: p)))
|
||||
.MinBy(p => World(p.System, p.Planet.PositionMeters).RelativeTo(Position, CellSizeMeters).Length / p.Planet.RadiusMeters);
|
||||
if (escape.Planet is { } solid)
|
||||
{
|
||||
var origin = World(escape.System, solid.PositionMeters);
|
||||
var start = Position.RelativeTo(origin, CellSizeMeters);
|
||||
if (start.Length < solid.MaximumTerrainRadiusMeters * 1.03)
|
||||
departure = new(origin, start.Normalized(), body.Add(radial * planet.RadiusMeters, CellSizeMeters).RelativeTo(origin, CellSizeMeters).Normalized(), start.Length, solid.MaximumTerrainRadiusMeters * 1.04);
|
||||
}
|
||||
BeginFlight(body, radial, planet.RadiusMeters + ocean.SeaLevelMeters + System.Math.Max(6, ocean.WaveAmplitudeMeters * 4),
|
||||
planet.MaximumTerrainRadiusMeters * 1.04, forward, radial, 10, 6, departure: departure);
|
||||
}
|
||||
|
||||
public void RequestRingView(IEnumerable<StarSystemData> systems)
|
||||
{
|
||||
var available = systems.ToArray();
|
||||
var nearest = available.SelectMany(s => s.Planets.Where(p => p.Rings is not null).Select(p => (System: s, Planet: p)))
|
||||
.MinBy(p => World(p.System, p.Planet.PositionMeters).RelativeTo(Position, CellSizeMeters).Length);
|
||||
if (nearest.Planet is null) return;
|
||||
var planet = nearest.Planet; var rings = planet.Rings!; var preview = rings.Preview;
|
||||
var azimuth = preview.AzimuthDegrees * System.Math.PI / 180;
|
||||
var radius = rings.InnerRadiusMeters + (rings.OuterRadiusMeters - rings.InnerRadiusMeters) * preview.RadiusFraction;
|
||||
var offset = new DVector3(radius * System.Math.Cos(azimuth), preview.HeightMeters, radius * System.Math.Sin(azimuth));
|
||||
var inward = new DVector3(-System.Math.Cos(azimuth), 0, -System.Math.Sin(azimuth));
|
||||
var yaw = RotateAround(inward, new(0, 1, 0), preview.YawOffsetDegrees * System.Math.PI / 180);
|
||||
var pitch = preview.PitchDegrees * System.Math.PI / 180;
|
||||
var forward = planet.Orientation.ToWorld(yaw * System.Math.Cos(pitch) + new DVector3(0, System.Math.Sin(pitch), 0));
|
||||
var escapeBody = available.SelectMany(s => s.Planets.Where(p => !p.IsGasPlanet).Select(p => (System: s, Planet: p)))
|
||||
.MinBy(p => World(p.System, p.Planet.PositionMeters).RelativeTo(Position, CellSizeMeters).Length / p.Planet.RadiusMeters);
|
||||
Departure? departure = null;
|
||||
var destination = World(nearest.System, planet.PositionMeters);
|
||||
if (escapeBody.Planet is { } solid)
|
||||
{
|
||||
var origin = World(escapeBody.System, solid.PositionMeters);
|
||||
var start = Position.RelativeTo(origin, CellSizeMeters);
|
||||
if (start.Length < solid.MaximumTerrainRadiusMeters * 6)
|
||||
departure = new(origin, start.Normalized(), destination.RelativeTo(origin, CellSizeMeters).Normalized(), start.Length, solid.MaximumTerrainRadiusMeters * 6);
|
||||
}
|
||||
var up = RotateAround(planet.Orientation.ToWorld(new(0, 1, 0)), forward, preview.RollDegrees * System.Math.PI / 180);
|
||||
BeginFlight(destination, planet.Orientation.ToWorld(offset.Normalized()), offset.Length, rings.OuterRadiusMeters * 2.6,
|
||||
forward, up, preview.SpeedMetersPerSecond,
|
||||
preview.FlightSeconds, preview.FieldOfViewDegrees * System.Math.PI / 180, departure);
|
||||
}
|
||||
|
||||
private void RequestPlanetView(IEnumerable<StarSystemData> systems, bool surface, string? targetPlanetId = null)
|
||||
{
|
||||
var candidates = systems.SelectMany(system => system.Planets.Select(planet => (System: system, Planet: planet)))
|
||||
.Where(body => targetPlanetId is null || string.Equals(body.Planet.Id, targetPlanetId, StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
if (candidates.Length == 0) return;
|
||||
var nearest = candidates
|
||||
.MinBy(body => World(body.System, body.Planet.PositionMeters).RelativeTo(Position, CellSizeMeters).Length - body.Planet.RadiusMeters);
|
||||
var planet = nearest.Planet;
|
||||
var bodyPosition = World(nearest.System, planet.PositionMeters);
|
||||
var sun = (nearest.System.Stars[planet.HostStarIndex].PositionMeters - planet.PositionMeters).Normalized();
|
||||
var side = Perpendicular(sun);
|
||||
DVector3 radial, forward, up;
|
||||
double finalRadius, speed;
|
||||
if (surface)
|
||||
{
|
||||
var moon = nearest.System.Planets.FirstOrDefault(p => p.ParentPlanetIndex == planet.Index);
|
||||
// A low sun reveals ridge relief. Choose a mountain shoulder instead of a featureless basin.
|
||||
radial = side;
|
||||
var score = double.NegativeInfinity;
|
||||
for (var i = 0; i < 40; i++)
|
||||
{
|
||||
var candidate = (RotateAround(side, sun, i * System.Math.Tau / 40) * 0.9755 + sun * 0.22).Normalized();
|
||||
if (planet.Terrain.HeightMap is { } map && map.Sample(planet.Orientation.ToBody(candidate)) < planet.Terrain.LandThreshold + planet.Terrain.LandBlend)
|
||||
continue;
|
||||
var normal = TerrainHeight.Normal(planet, candidate);
|
||||
var relief = (1 - DVector3.Dot(normal, candidate)) * 0.20
|
||||
+ (TerrainHeight.Sample(planet, candidate) - planet.RadiusMeters) / System.Math.Max(1, planet.TerrainAmplitudeMeters) * 0.80;
|
||||
if (moon is not null)
|
||||
{
|
||||
var moonDirection = (moon.PositionMeters - planet.PositionMeters - candidate * planet.RadiusMeters).Normalized();
|
||||
var elevation = DVector3.Dot(moonDirection, candidate);
|
||||
relief += 1.4 * System.Math.Max(0, 1 - System.Math.Abs(elevation - 0.14) / 0.13);
|
||||
}
|
||||
if (relief <= score) continue;
|
||||
score = relief;
|
||||
radial = candidate;
|
||||
}
|
||||
var terrainTop = TerrainHeight.Sample(planet, radial);
|
||||
var localTangent = Perpendicular(radial);
|
||||
var localBitangent = DVector3.Cross(radial, localTangent);
|
||||
// Clear neighboring ridges as well as the terrain directly under the camera.
|
||||
foreach (var ring in new[] { 0.003, 0.007, 0.012, 0.020 })
|
||||
for (var sample = 0; sample < 16; sample++)
|
||||
{
|
||||
var angle = sample * System.Math.Tau / 16;
|
||||
var d = (radial + (localTangent * System.Math.Cos(angle) + localBitangent * System.Math.Sin(angle)) * ring).Normalized();
|
||||
terrainTop = System.Math.Max(terrainTop, TerrainHeight.Sample(planet, d));
|
||||
}
|
||||
finalRadius = terrainTop + planet.RadiusMeters * 0.0015;
|
||||
var horizontal = (sun - radial * DVector3.Dot(sun, radial)).Normalized();
|
||||
var east = DVector3.Cross(radial, horizontal).Normalized();
|
||||
var look = (horizontal * 0.22 + east * 0.975).Normalized();
|
||||
if (moon is not null)
|
||||
{
|
||||
var toMoon = (moon.PositionMeters - planet.PositionMeters - radial * finalRadius).Normalized();
|
||||
if (DVector3.Dot(toMoon, radial) > 0.02) look = (toMoon - radial * DVector3.Dot(toMoon, radial)).Normalized();
|
||||
}
|
||||
forward = (look - radial * 0.32).Normalized();
|
||||
up = radial;
|
||||
speed = 1_200;
|
||||
}
|
||||
else
|
||||
{
|
||||
radial = (sun * 0.42 + side * 0.907).Normalized();
|
||||
finalRadius = System.Math.Max(planet.MaximumTerrainRadiusMeters * 2.85, (planet.Rings?.OuterRadiusMeters ?? 0) * 2.6);
|
||||
forward = -radial;
|
||||
up = new(0, 1, 0);
|
||||
if (planet.Rings is { } rings)
|
||||
{
|
||||
var v = rings.Preview;
|
||||
var pole = planet.Orientation.ToWorld(new(0, 1, 0));
|
||||
var equatorSun = (sun - pole * DVector3.Dot(sun, pole)).Normalized();
|
||||
var heading = RotateAround(equatorSun, pole, v.OverviewSunAngleDegrees * System.Math.PI / 180);
|
||||
var elevation = v.OverviewElevationDegrees * System.Math.PI / 180;
|
||||
radial = (heading * System.Math.Cos(elevation) + pole * System.Math.Sin(elevation)).Normalized();
|
||||
finalRadius = rings.OuterRadiusMeters * v.OverviewDistanceInOuterRadii;
|
||||
forward = -radial;
|
||||
up = RotateAround(pole, forward, v.RollDegrees * System.Math.PI / 180);
|
||||
}
|
||||
speed = 250_000;
|
||||
}
|
||||
BeginFlight(bodyPosition, radial, finalRadius, System.Math.Max(planet.MaximumTerrainRadiusMeters * 5, finalRadius * 1.4), forward, up, speed);
|
||||
}
|
||||
|
||||
public void RequestStarView(IEnumerable<StarSystemData> systems)
|
||||
{
|
||||
var nearest = systems.SelectMany(system => system.Stars.Select(star => (System: system, Star: star)))
|
||||
.MinBy(body => World(body.System, body.Star.PositionMeters).RelativeTo(Position, CellSizeMeters).LengthSquared);
|
||||
if (nearest.Star is null) return;
|
||||
var bodyPosition = World(nearest.System, nearest.Star.PositionMeters);
|
||||
var radial = Position.RelativeTo(bodyPosition, CellSizeMeters).Normalized();
|
||||
BeginFlight(bodyPosition, radial, nearest.Star.RadiusMeters * 2.55, nearest.Star.RadiusMeters * 5, -radial, new(0, 1, 0), 10_000_000);
|
||||
}
|
||||
|
||||
private UniversePosition World(StarSystemData system, DVector3 offset) => UniversePosition.AtCellCenter(system.Coordinate, CellSizeMeters).Add(offset, CellSizeMeters);
|
||||
|
||||
private void BeginFlight(UniversePosition body, DVector3 radial, double finalRadius, double approachRadius, DVector3 forward, DVector3 up, double speed,
|
||||
double duration = 4.5, double? fieldOfView = null, Departure? departure = null)
|
||||
{
|
||||
var travelStart = departure is null ? Position : departure.Body.Add(departure.TargetRadial * departure.ClearanceRadius, CellSizeMeters);
|
||||
var startRadial = travelStart.RelativeTo(body, CellSizeMeters).Normalized();
|
||||
if (startRadial == DVector3.Zero) startRadial = -Forward;
|
||||
_flight = new(travelStart, body, startRadial, radial, approachRadius, finalRadius, Forward, Up, forward, up, speed,
|
||||
duration, FieldOfViewRadians, fieldOfView ?? _defaultFieldOfView, departure);
|
||||
_flightElapsed = 0;
|
||||
}
|
||||
|
||||
public void Update(double deltaSeconds, DVector3 movement, UniverseNeighborhood neighborhood)
|
||||
{
|
||||
if (deltaSeconds <= 0 || !double.IsFinite(deltaSeconds)) return;
|
||||
var systems = neighborhood.NearbySystems.Append(neighborhood.ActiveSystem).ToArray();
|
||||
if (_flight is { } f)
|
||||
{
|
||||
_flightElapsed += System.Math.Min(deltaSeconds, 0.1);
|
||||
var t = System.Math.Clamp(_flightElapsed / f.Duration, 0, 1);
|
||||
var travelT = f.Departure is null ? t : System.Math.Clamp((t - .28) / .72, 0, 1);
|
||||
UniversePosition proposed;
|
||||
if (f.Departure is { } departure && t < .28)
|
||||
{
|
||||
var direction = t < .12 ? departure.StartRadial : Slerp(departure.StartRadial, departure.TargetRadial, Smooth((t - .12) / .16));
|
||||
var radius = t < .12 ? departure.StartRadius + (departure.ClearanceRadius - departure.StartRadius) * Smooth(t / .12) : departure.ClearanceRadius;
|
||||
proposed = departure.Body.Add(direction * radius, CellSizeMeters);
|
||||
}
|
||||
else if (travelT < 0.38)
|
||||
{
|
||||
var waypoint = f.Body.Add(f.StartRadial * f.ApproachRadius, CellSizeMeters);
|
||||
proposed = f.Start.Add(waypoint.RelativeTo(f.Start, CellSizeMeters) * Smooth(travelT / 0.38), CellSizeMeters);
|
||||
}
|
||||
else if (travelT < 0.7)
|
||||
{
|
||||
var direction = Slerp(f.StartRadial, f.TargetRadial, Smooth((travelT - 0.38) / 0.32));
|
||||
proposed = f.Body.Add(direction * f.ApproachRadius, CellSizeMeters);
|
||||
}
|
||||
else
|
||||
{
|
||||
var radius = f.ApproachRadius + (f.FinalRadius - f.ApproachRadius) * Smooth((travelT - 0.7) / 0.3);
|
||||
proposed = f.Body.Add(f.TargetRadial * radius, CellSizeMeters);
|
||||
}
|
||||
var result = SweepNeighborhood(Position, proposed, systems);
|
||||
Position = result.Position;
|
||||
_forward = Slerp(f.StartForward, f.TargetForward, Smooth(t));
|
||||
FieldOfViewRadians = f.StartFov + (f.TargetFov - f.StartFov) * Smooth(t);
|
||||
var up = Slerp(f.StartUp, f.TargetUp, Smooth(t));
|
||||
_up = (up - _forward * DVector3.Dot(up, _forward)).Normalized();
|
||||
if (_up.LengthSquared < 0.1) _up = Perpendicular(_forward);
|
||||
if (result.Collided || t >= 1)
|
||||
{
|
||||
_flight = null;
|
||||
SpeedMetersPerSecond = f.FinalSpeed;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (movement.LengthSquared < 1e-12) return;
|
||||
var distance = System.Math.Min(SpeedMetersPerSecond * System.Math.Min(deltaSeconds, 0.1), CellSizeMeters * 0.45);
|
||||
var end = Position.Add(movement.Normalized() * distance, CellSizeMeters);
|
||||
Position = SweepNeighborhood(Position, end, systems).Position;
|
||||
}
|
||||
|
||||
private CollisionResult SweepNeighborhood(UniversePosition start, UniversePosition end, IReadOnlyList<StarSystemData> systems)
|
||||
{
|
||||
var best = new CollisionResult(end, false, -1);
|
||||
var bestDistance = double.PositiveInfinity;
|
||||
foreach (var system in systems)
|
||||
{
|
||||
var result = PlanetCollisionSolver.Sweep(start, end, UniversePosition.AtCellCenter(system.Coordinate, CellSizeMeters), system.Planets, CellSizeMeters);
|
||||
if (!result.Collided) continue;
|
||||
var distance = result.Position.RelativeTo(start, CellSizeMeters).LengthSquared;
|
||||
if (distance >= bestDistance) continue;
|
||||
best = result; bestDistance = distance;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private static double Smooth(double t) => t * t * (3 - 2 * t);
|
||||
private static DVector3 Perpendicular(DVector3 d) => DVector3.Cross(d, System.Math.Abs(d.Y) < 0.9 ? new(0, 1, 0) : new DVector3(1, 0, 0)).Normalized();
|
||||
private static DVector3 RotateAround(DVector3 v, DVector3 axis, double angle) => v * System.Math.Cos(angle) + DVector3.Cross(axis, v) * System.Math.Sin(angle) + axis * DVector3.Dot(axis, v) * (1 - System.Math.Cos(angle));
|
||||
private static DVector3 Slerp(DVector3 a, DVector3 b, double t)
|
||||
{
|
||||
var dot = System.Math.Clamp(DVector3.Dot(a, b), -1, 1);
|
||||
if (dot > 0.9995) return DVector3.Lerp(a, b, t).Normalized();
|
||||
var angle = System.Math.Acos(dot);
|
||||
var tangent = (b - a * dot).Normalized();
|
||||
if (tangent.LengthSquared < 0.1) tangent = Perpendicular(a);
|
||||
return a * System.Math.Cos(angle * t) + tangent * System.Math.Sin(angle * t);
|
||||
}
|
||||
|
||||
private sealed record FocusFlight(UniversePosition Start, UniversePosition Body, DVector3 StartRadial, DVector3 TargetRadial,
|
||||
double ApproachRadius, double FinalRadius, DVector3 StartForward, DVector3 StartUp, DVector3 TargetForward, DVector3 TargetUp, double FinalSpeed,
|
||||
double Duration, double StartFov, double TargetFov, Departure? Departure);
|
||||
private sealed record Departure(UniversePosition Body, DVector3 StartRadial, DVector3 TargetRadial, double StartRadius, double ClearanceRadius);
|
||||
}
|
||||
136
src/Universe.App/Runtime/PlayerController.cs
Normal file
136
src/Universe.App/Runtime/PlayerController.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using Universe.Core.Generation;
|
||||
using Universe.Core.Math;
|
||||
using Universe.Core.Physics;
|
||||
using Universe.Core.World;
|
||||
|
||||
namespace Universe.App.Runtime;
|
||||
|
||||
/// <summary>First-person locomotion in a local frame aligned to the nearest solid planet.</summary>
|
||||
public sealed class PlayerController
|
||||
{
|
||||
private const double StandHeight = 1.7, CrouchHeight = 1.0;
|
||||
private readonly double _cellSize;
|
||||
private DVector3 _heading, _velocity;
|
||||
private double _pitch;
|
||||
private bool _jumpHeld;
|
||||
public UniversePosition Position { get; private set; }
|
||||
public bool Grounded { get; private set; }
|
||||
public bool Crouching { get; private set; }
|
||||
public DVector3 Up { get; private set; }
|
||||
public DVector3 Forward => (_heading * System.Math.Cos(_pitch) + Up * System.Math.Sin(_pitch)).Normalized();
|
||||
public double EyeHeight => Crouching ? CrouchHeight : StandHeight;
|
||||
public double Speed => _velocity.Length;
|
||||
|
||||
public PlayerController(UniversePosition position, DVector3 forward, DVector3 fallbackUp, double cellSize, UniverseNeighborhood neighborhood)
|
||||
{
|
||||
Position = position; _cellSize = cellSize;
|
||||
var target = Nearest(neighborhood);
|
||||
Up = target is { } t ? position.RelativeTo(t.Body, cellSize).Normalized() : fallbackUp.Normalized();
|
||||
if (Up == DVector3.Zero) Up = new(0, 1, 0);
|
||||
var projected = forward - Up * DVector3.Dot(forward, Up);
|
||||
_heading = projected.LengthSquared > .0001 ? projected.Normalized() : Tangent(Up);
|
||||
_pitch = System.Math.Clamp(System.Math.Asin(System.Math.Clamp(DVector3.Dot(forward.Normalized(), Up), -1, 1)), -1.5, 1.5);
|
||||
}
|
||||
|
||||
public void Look(double dx, double dy)
|
||||
{
|
||||
var angle = -dx * .0022;
|
||||
_heading = (_heading * System.Math.Cos(angle) + DVector3.Cross(Up, _heading) * System.Math.Sin(angle)).Normalized();
|
||||
_pitch = System.Math.Clamp(_pitch - dy * .0022, -1.5, 1.5);
|
||||
}
|
||||
|
||||
public void Update(double dt, double forward, double side, bool jump, bool crouch, UniverseNeighborhood neighborhood)
|
||||
{
|
||||
if (!double.IsFinite(dt) || dt <= 0) return;
|
||||
dt = System.Math.Min(dt, .1);
|
||||
var target = Nearest(neighborhood);
|
||||
if (target is not { } t) { _jumpHeld = jump; return; }
|
||||
var relative = Position.RelativeTo(t.Body, _cellSize);
|
||||
var radial = relative.Normalized();
|
||||
if (radial == DVector3.Zero) radial = Up;
|
||||
Align(radial);
|
||||
var surface = TerrainHeight.Sample(t.Planet, radial);
|
||||
var clearance = relative.Length - surface;
|
||||
var previousHeight = EyeHeight;
|
||||
Crouching = crouch;
|
||||
var eye = EyeHeight;
|
||||
// Tangential motion acquires a tiny outward component as the local radial frame turns.
|
||||
Grounded = clearance <= previousHeight + .08 && DVector3.Dot(_velocity, radial) <= .05;
|
||||
if (Grounded && System.Math.Abs(eye - previousHeight) > .001)
|
||||
{ Position = t.Body.Add(radial * (surface + eye), _cellSize); relative = Position.RelativeTo(t.Body, _cellSize); }
|
||||
|
||||
var right = DVector3.Cross(_heading, radial).Normalized();
|
||||
var input = _heading * System.Math.Clamp(forward, -1, 1) + right * System.Math.Clamp(side, -1, 1);
|
||||
if (input.LengthSquared > 1) input = input.Normalized();
|
||||
var walkSpeed = Crouching ? 1.5 : 4.5;
|
||||
var horizontal = input * walkSpeed;
|
||||
var vertical = DVector3.Dot(_velocity, radial);
|
||||
var gravity = 9.81 * System.Math.Clamp(t.Planet.RadiusMeters / UniverseScale.EarthRadiusMeters, .15, 2.5);
|
||||
if (Grounded && jump && !_jumpHeld) { vertical = 5.2; Grounded = false; }
|
||||
_jumpHeld = jump;
|
||||
|
||||
if (Grounded)
|
||||
{
|
||||
var proposed = relative + horizontal * dt;
|
||||
var nextRadial = proposed.Normalized();
|
||||
var nextSurface = TerrainHeight.Sample(t.Planet, nextRadial);
|
||||
var rise = nextSurface - surface;
|
||||
var move = horizontal.Length * dt;
|
||||
// Allow small steps but prevent walking up steep cliffs.
|
||||
if (rise > .45 || (move > .001 && rise / move > .85)) nextRadial = radial;
|
||||
else if (rise < -.45)
|
||||
{
|
||||
Position = t.Body.Add(proposed.Normalized() * (surface + eye), _cellSize);
|
||||
Grounded = false; vertical = 0;
|
||||
}
|
||||
if (Grounded)
|
||||
{
|
||||
Position = t.Body.Add(nextRadial * (TerrainHeight.Sample(t.Planet, nextRadial) + eye), _cellSize);
|
||||
_velocity = horizontal; Align(nextRadial); return;
|
||||
}
|
||||
}
|
||||
|
||||
// Far above a planet, a bounded descent assist avoids hours of empty free fall.
|
||||
var altitude = System.Math.Max(0, relative.Length - surface - eye);
|
||||
if (altitude > 2000 && vertical <= 0)
|
||||
vertical = -System.Math.Max(-vertical, System.Math.Min(altitude / 5, _cellSize * .02));
|
||||
else vertical = System.Math.Max(vertical - gravity * dt, -180);
|
||||
_velocity = horizontal + radial * vertical;
|
||||
var destination = Position.Add(_velocity * dt, _cellSize);
|
||||
var collision = PlanetCollisionSolver.Sweep(Position, destination,
|
||||
UniversePosition.AtCellCenter(t.System.Coordinate, _cellSize), [t.Planet], _cellSize, eye,
|
||||
_velocity.Length * dt < 100 ? 16 : 128);
|
||||
Position = collision.Position;
|
||||
if (collision.Collided)
|
||||
{
|
||||
var up = Position.RelativeTo(t.Body, _cellSize).Normalized();
|
||||
if (DVector3.Dot(_velocity, up) < 0) _velocity -= up * DVector3.Dot(_velocity, up);
|
||||
Grounded = true;
|
||||
}
|
||||
Align(Position.RelativeTo(t.Body, _cellSize).Normalized());
|
||||
}
|
||||
|
||||
private void Align(DVector3 up)
|
||||
{
|
||||
if (up == DVector3.Zero) return;
|
||||
var heading = _heading - up * DVector3.Dot(_heading, up);
|
||||
_heading = heading.LengthSquared > .0001 ? heading.Normalized() : Tangent(up);
|
||||
Up = up;
|
||||
}
|
||||
private static DVector3 Tangent(DVector3 up) => DVector3.Cross(System.Math.Abs(up.Y) < .9 ? new(0, 1, 0) : new DVector3(1, 0, 0), up).Normalized();
|
||||
private (StarSystemData System, PlanetData Planet, UniversePosition Body)? Nearest(UniverseNeighborhood neighborhood)
|
||||
{
|
||||
(StarSystemData System, PlanetData Planet, UniversePosition Body)? best = null;
|
||||
var closest = double.PositiveInfinity;
|
||||
foreach (var system in neighborhood.NearbySystems.Append(neighborhood.ActiveSystem))
|
||||
foreach (var planet in system.Planets)
|
||||
{
|
||||
if (planet.IsGasPlanet) continue;
|
||||
var body = UniversePosition.AtCellCenter(system.Coordinate, _cellSize).Add(planet.PositionMeters, _cellSize);
|
||||
var distance = Position.RelativeTo(body, _cellSize).Length - planet.RadiusMeters;
|
||||
if (distance >= closest) continue;
|
||||
closest = distance; best = (system, planet, body);
|
||||
}
|
||||
return best;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user