galaxer och motion blur

This commit is contained in:
2026-11-01 15:46:28 +01:00
parent 8274a20001
commit 9d44d14cc9
39 changed files with 1502 additions and 72 deletions

View File

@@ -6,7 +6,7 @@ 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
internal sealed class BenchmarkRunner : IBenchmarkSession
{
private const double SpaceSeconds = 8;
private const double DescentSeconds = 40;
@@ -77,7 +77,7 @@ internal sealed class BenchmarkRunner
_orbitBitangent = DVector3.Cross(_targetRadial, _orbitTangent).Normalized();
var initialRadius = _planet.RadiusMeters * 4.8;
camera.SetPlayerPose(_body.Add(_spaceRadial * initialRadius, cellSizeMeters), -_spaceRadial, _spaceUp);
camera.SetPlayerPose(_body.Add(_spaceRadial * initialRadius, cellSizeMeters), -_spaceRadial, _spaceUp, resetMotionHistory: true);
}
public bool IsRunning => !_stopped && _elapsed < TotalSeconds;
@@ -169,7 +169,7 @@ internal sealed class BenchmarkRunner
_cellSize, cameraRadiusMeters: 1.5, samples: 32);
if (_elapsed >= TotalSeconds)
{
camera.SetPlayerPose(_returnPosition, _returnForward, _returnUp);
camera.SetPlayerPose(_returnPosition, _returnForward, _returnUp, resetMotionHistory: true);
return;
}
var finalPosition = proposed;

View File

@@ -18,11 +18,15 @@ public sealed class FlyCamera
{
Position = initialPosition;
CellSizeMeters = cellSizeMeters;
TravelLimitMeters = cellSizeMeters * .45;
SpeedMetersPerSecond = 250_000;
}
public UniversePosition Position { get; private set; }
public double CellSizeMeters { get; }
public double SpeedMetersPerSecond { get; private set; }
public double TravelLimitMeters { get; set; }
public long MotionHistoryVersion { get; private set; }
public void RestoreDefaultFieldOfView() => FieldOfViewRadians = _defaultFieldOfView;
public bool IsFocusing => _flight is not null;
public DVector3 Forward => _forward;
public DVector3 Right => DVector3.Cross(_forward, _up).Normalized();
@@ -37,16 +41,18 @@ public sealed class FlyCamera
_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 AdjustSpeed(double delta) => SpeedMetersPerSecond = System.Math.Clamp(SpeedMetersPerSecond * System.Math.Pow(1.7, delta), 0.1, UniverseScale.LightSpeedMetersPerSecond * 1e15);
public void SetPlayerPose(UniversePosition position, DVector3 forward, DVector3 up)
public void SetPlayerPose(UniversePosition position, DVector3 forward, DVector3 up, bool resetMotionHistory = false)
{
if (resetMotionHistory) MotionHistoryVersion++;
_flight = null;
Position = position; _forward = forward.Normalized(); _up = up.Normalized();
}
public void SetInitialView(StarSystemData system)
{
MotionHistoryVersion++;
var planet = system.Planets[0];
var center = UniversePosition.AtCellCenter(system.Coordinate, CellSizeMeters);
var sun = (system.Stars[0].PositionMeters - planet.PositionMeters).Normalized();
@@ -59,6 +65,7 @@ public sealed class FlyCamera
public void SetInitialView(StarSystemData system, StartupConfiguration startup)
{
MotionHistoryVersion++;
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;
@@ -302,6 +309,22 @@ public sealed class FlyCamera
star.RadiusMeters * .08, duration: 9, fieldOfView: 32 * System.Math.PI / 180, departure: departure);
}
public void RequestCosmicView(UniversePosition center,double radius,PlanetOrientation orientation,bool nebula,IEnumerable<StarSystemData> departureSystems)
{
var radial=orientation.ToWorld((nebula?new DVector3(.1,.05,1):new DVector3(.1,1,.3)).Normalized());
var up=orientation.ToWorld(nebula?new(0,1,0):new DVector3(0,0,-1));
Departure? departure=null;
var escape=departureSystems.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.1)
departure=new(origin,start.Normalized(),center.RelativeTo(origin,CellSizeMeters).Normalized(),start.Length,solid.MaximumTerrainRadiusMeters*1.12);
}
BeginFlight(center,radial,radius*2.5,radius*3.5,-radial,up,radius*.2,duration:8,departure:departure);
}
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,
@@ -361,7 +384,8 @@ public sealed class FlyCamera
return;
}
if (movement.LengthSquared < 1e-12) return;
var distance = System.Math.Min(SpeedMetersPerSecond * System.Math.Min(deltaSeconds, 0.1), CellSizeMeters * 0.45);
var nearSurface = systems.Any(s => s.Planets.Any(p => Position.RelativeTo(World(s, p.PositionMeters), CellSizeMeters).Length < p.RadiusMeters * 10));
var distance = System.Math.Min(SpeedMetersPerSecond * System.Math.Min(deltaSeconds, 0.1), nearSurface ? CellSizeMeters * .45 : TravelLimitMeters);
var end = Position.Add(movement.Normalized() * distance, CellSizeMeters);
Position = SweepNeighborhood(Position, end, systems).Position;
}

View File

@@ -0,0 +1,188 @@
using System.Globalization;
using System.Text;
using System.Text.Json;
using Universe.Core.Generation;
using Universe.Core.Math;
using Universe.Core.World;
namespace Universe.App.Runtime;
internal sealed class GalaxyJourneyBenchmark : IBenchmarkSession
{
private readonly GalaxyUniverse _galaxies;
private readonly UniversePosition _home, _homeBody, _milkyView, _otherView, _targetBody;
private readonly DVector3 _homeForward, _homeUp, _homeRadial, _targetRadial, _targetHeading;
private readonly double _homeRadius;
private readonly List<(double Time, double Milliseconds, string Phase)> _frames = [];
private readonly List<double> _graph = [];
private readonly HashSet<string> _visited = [];
private double _elapsed, _sampleTime, _totalFrameTime;
private double _lastSampleSequenceTime;
private int _sampleFrames;
private bool _stopped, _finalRecorded;
public GalaxyData HomeGalaxy { get; }
public GalaxyData TargetGalaxy { get; }
public StarSystemData TargetSystem { get; }
public PlanetData TargetPlanet { get; }
public string? ReportDirectory { get; private set; }
public object? RenderEnvironment { get; set; }
public GalaxyJourneyBenchmark(GalaxyUniverse galaxies, UniverseGenerator generator, StarSystemData homeSystem, PlanetData earth,
UniversePosition home, DVector3 homeForward, DVector3 homeUp)
{
_galaxies = galaxies; _home = home; _homeForward = homeForward; _homeUp = homeUp;
_homeBody = UniversePosition.AtCellCenter(homeSystem.Coordinate, galaxies.CellSize).Add(earth.PositionMeters, galaxies.CellSize);
_homeRadial = home.RelativeTo(_homeBody, galaxies.CellSize).Normalized();
_homeRadius = home.RelativeTo(_homeBody, galaxies.CellSize).Length;
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));
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
// benchmarking the cheap night-side shading branch.
var sun=(TargetSystem.Stars[TargetPlanet.HostStarIndex].PositionMeters-TargetPlanet.PositionMeters).Normalized();
var side=DVector3.Cross(sun,System.Math.Abs(sun.Y)<.9?new(0,1,0):new DVector3(1,0,0)).Normalized();
_targetRadial=(sun*.6+side*.8).Normalized();
_targetHeading=DVector3.Cross(_targetRadial,sun).Normalized();
_milkyView = HomeGalaxy.Center.Add(HomeGalaxy.Orientation.ToWorld(new DVector3(.1,1,.3).Normalized()) * HomeGalaxy.RadiusMeters * 2.5, galaxies.CellSize);
_otherView = TargetGalaxy.Center.Add(TargetGalaxy.Orientation.ToWorld(new DVector3(.3,.8,1).Normalized()) * TargetGalaxy.RadiusMeters * 2.5, galaxies.CellSize);
}
public bool IsRunning => !_stopped && _elapsed < DurationSeconds;
public bool IsComplete => !_stopped && _elapsed >= DurationSeconds;
public bool HasRun => true;
public double ElapsedSeconds => _elapsed;
public double DurationSeconds => 94;
public long GraphRevision { get; private set; }
public IReadOnlyList<double> GraphSamples => _graph;
public string TargetLabel => TargetPlanet.Name.ToUpperInvariant();
public double TargetEarthRadii => TargetPlanet.RadiusMeters / UniverseScale.EarthRadiusMeters;
public double CurrentFps { get; private set; }
public double AverageFps => _totalFrameTime <= 0 ? 0 : _frames.Count / _totalFrameTime;
public double MinimumFps => _frames.Count == 0 ? 0 : 1000 / _frames.Max(f => f.Milliseconds);
public double OnePercentLowFps => Low(_frames.Select(f => f.Milliseconds).ToArray());
public string Phase => _stopped ? "STOPPED" : IsComplete ? "COMPLETE" : PhaseAt(_elapsed);
public IReadOnlyCollection<string> VisitedPhases => _visited;
private static string PhaseAt(double t) => t < 18 ? "LEAVE EARTH" : t < 25 ? "MILKY WAY" : t < 40 ? "BETWEEN GALAXIES"
: t < 55 ? "ENTER GALAXY" : t < 65 ? "TARGET SYSTEM" : t < 85 ? "LANDING" : "GROUND WALK";
public void Stop() => _stopped = true;
public void Update(double seconds, FlyCamera camera)
{
if (!IsRunning || !double.IsFinite(seconds) || seconds <= 0) return;
_elapsed = System.Math.Min(DurationSeconds, _elapsed + seconds);
_visited.Add(PhaseAt(System.Math.Min(_elapsed, DurationSeconds - .001)));
var cell = _galaxies.CellSize;
UniversePosition position; DVector3 forward; DVector3 up = _homeUp;
if (_elapsed < 18)
{
var t = Smooth(_elapsed / 18);
var viewRadial = _milkyView.RelativeTo(_homeBody,cell).Normalized();
var radius = LogLerp(_homeRadius, _milkyView.RelativeTo(_homeBody,cell).Length, t);
var direction = DVector3.Lerp(_homeRadial,viewRadial,Smooth(System.Math.Clamp((t-.6)/.4,0,1))).Normalized();
position = _homeBody.Add(direction * radius, cell);
var focus = System.Math.Clamp((System.Math.Log(System.Math.Max(radius,1)) - System.Math.Log(UniverseScale.LightYearMeters)) / 12, 0, 1);
forward = DVector3.Lerp(_homeForward, HomeGalaxy.Center.RelativeTo(position, cell).Normalized(), Smooth(focus)).Normalized();
}
else if (_elapsed < 25)
{
position = _milkyView;
forward = HomeGalaxy.Center.RelativeTo(position, cell).Normalized();
}
else if (_elapsed < 40)
{
var t = Smooth((_elapsed - 25) / 15);
position = Interpolate(_milkyView, _otherView, t, cell);
var from = HomeGalaxy.Center.RelativeTo(_milkyView, cell).Normalized();
var to = _targetBody.RelativeTo(_otherView, cell).Normalized();
forward = DVector3.Lerp(from, to, t).Normalized();
}
else if (_elapsed < 55)
{
var t = Smooth((_elapsed - 40) / 15);
var radial = _otherView.RelativeTo(_targetBody, cell).Normalized();
var radius = LogLerp(_otherView.RelativeTo(_targetBody, cell).Length, TargetPlanet.RadiusMeters * 4.8, t);
// Rotate at galaxy distances; keep the final solar approach radial and precise.
var direction = DVector3.Lerp(radial, _targetRadial, Smooth(System.Math.Min(t * 3, 1))).Normalized();
position = _targetBody.Add(direction * radius, cell);
forward = _targetBody.RelativeTo(position, cell).Normalized();
up = new(0,1,0);
}
else if (_elapsed < 65)
{
position = _targetBody.Add(_targetRadial * TargetPlanet.RadiusMeters * 4.8, cell);
forward = -_targetRadial; up = new(0,1,0);
}
else
{
var t = System.Math.Clamp((_elapsed - 65) / 20, 0, 1);
var surface = TerrainHeight.Sample(TargetPlanet, _targetRadial);
var radius = t < .5 ? TargetPlanet.RadiusMeters * 4.8 + (surface + 10001.8 - TargetPlanet.RadiusMeters * 4.8) * Smooth(t * 2)
: surface + 1.8 + 10000 * (1 - Smooth((t - .5) * 2));
var radial = _targetRadial;
if (_elapsed >= 85)
{
var walk = (_elapsed - 85) / 9;
radial = (_targetRadial + _targetHeading * (walk * 12 / TargetPlanet.RadiusMeters)).Normalized();
radius = TerrainHeight.Sample(TargetPlanet, radial) + 1.8;
}
position = _targetBody.Add(radial * radius, cell);
var landingLook = (_targetHeading * System.Math.Cos(.2) - radial * System.Math.Sin(.2)).Normalized();
forward = DVector3.Lerp(-radial, landingLook, Smooth(System.Math.Clamp((t - .4) / .6,0,1))).Normalized();
if (_elapsed >= 85)
{
var yaw = System.Math.Sin((_elapsed - 85) * .7) * .6;
var right = DVector3.Cross(_targetHeading, radial).Normalized();
forward = (_targetHeading * System.Math.Cos(yaw) + right * System.Math.Sin(yaw) - radial * .15).Normalized();
}
up = radial;
}
if (_elapsed >= DurationSeconds) { position = _home; forward = _homeForward; up = _homeUp; }
up = (up - forward * DVector3.Dot(up,forward)).Normalized();
if (up.LengthSquared < .1) up = DVector3.Cross(forward,new(1,0,0)).Normalized();
camera.SetPlayerPose(position, forward, up, resetMotionHistory: IsComplete);
}
public void RecordFrame(double seconds)
{
if (_stopped || _finalRecorded || !double.IsFinite(seconds) || seconds <= 0) return;
CurrentFps = 1 / seconds; _totalFrameTime += seconds;
_frames.Add((_elapsed, seconds * 1000, PhaseAt(System.Math.Min(_elapsed,DurationSeconds - .001))));
_sampleTime += seconds; _sampleFrames++;
if (_elapsed - _lastSampleSequenceTime >= .2 || IsComplete)
{
_graph.Add(_sampleFrames / _sampleTime); _sampleTime = 0; _sampleFrames = 0; _lastSampleSequenceTime = _elapsed; GraphRevision++;
}
if (IsComplete) _finalRecorded = true;
}
public object[] PhaseResults() => _frames.GroupBy(f => f.Phase).Select(group =>
{
var values = group.Select(f => f.Milliseconds).Order().ToArray();
var p99 = values[System.Math.Min(values.Length - 1,(int)System.Math.Ceiling(values.Length * .99) - 1)];
return (object)new { Phase = group.Key, Frames = values.Length, AverageFps = 1000 / values.Average(), LowOnePercentFps = Low(values),
P99Milliseconds = p99, MaximumMilliseconds = values.Max(),
Passed = Low(values) >= _galaxies.Settings.MinimumBenchmarkFps && values.Max() <= _galaxies.Settings.MaximumBenchmarkFrameMilliseconds };
}).ToArray();
public bool PerformancePassed => IsComplete && _frames.Count > 0 && _frames.GroupBy(f => f.Phase).All(g =>
Low(g.Select(f => f.Milliseconds).ToArray()) >= _galaxies.Settings.MinimumBenchmarkFps
&& g.Max(f => f.Milliseconds) <= _galaxies.Settings.MaximumBenchmarkFrameMilliseconds);
public string SaveReport(string? root = null)
{
if (ReportDirectory is not null) return ReportDirectory;
var directory = Path.Combine(root ?? Path.Combine(AppContext.BaseDirectory,"benchmark-results"),"galaxy-journey-"+DateTime.UtcNow.ToString("yyyyMMdd-HHmmss"));
Directory.CreateDirectory(directory);
var csv = new StringBuilder("timeSeconds,phase,frameMilliseconds,fps\n");
foreach (var f in _frames) csv.AppendLine(FormattableString.Invariant($"{f.Time:F4},{f.Phase},{f.Milliseconds:F4},{1000/f.Milliseconds:F4}"));
File.WriteAllText(Path.Combine(directory,"frames.csv"),csv.ToString());
File.WriteAllText(Path.Combine(directory,"summary.json"),JsonSerializer.Serialize(new { Completed = IsComplete, PerformancePassed,
UniverseSeed = _galaxies.Seed.ToString("X16"), RenderEnvironment,
Quality = new { _galaxies.Settings.GalaxyStarBudget, _galaxies.Settings.LocalStarBudget, _galaxies.Settings.VolumeSteps, _galaxies.Settings.SystemSpacingLightYears },
TargetGalaxy = TargetGalaxy.Id, TargetSystem = TargetSystem.Id, TargetPlanet = TargetPlanet.Id,
MinimumFps = _galaxies.Settings.MinimumBenchmarkFps, MaximumFrameMilliseconds = _galaxies.Settings.MaximumBenchmarkFrameMilliseconds,
Visited = _visited, Phases = PhaseResults() },new JsonSerializerOptions { WriteIndented = true }));
ReportDirectory = directory; return directory;
}
private static double Low(double[] milliseconds) => milliseconds.Length == 0 ? 0 : 1000 / milliseconds.OrderDescending().Take(System.Math.Max(1,(int)System.Math.Ceiling(milliseconds.Length * .01))).Average();
private static double Smooth(double t) { t=System.Math.Clamp(t,0,1);return t*t*t*(t*(t*6-15)+10); }
private static double LogLerp(double a,double b,double t) => System.Math.Exp(System.Math.Log(a)*(1-t)+System.Math.Log(b)*t);
private static UniversePosition Interpolate(UniversePosition a,UniversePosition b,double t,double cell) => t<.5?a.Add(b.RelativeTo(a,cell)*t,cell):b.Add(a.RelativeTo(b,cell)*(1-t),cell);
}

View File

@@ -0,0 +1,24 @@
using Universe.Core.Generation;
namespace Universe.App.Runtime;
internal interface IBenchmarkSession
{
bool IsRunning { get; }
bool IsComplete { get; }
bool HasRun { get; }
double ElapsedSeconds { get; }
double DurationSeconds { get; }
long GraphRevision { get; }
IReadOnlyList<double> GraphSamples { get; }
string TargetLabel { get; }
double TargetEarthRadii { get; }
double CurrentFps { get; }
double AverageFps { get; }
double MinimumFps { get; }
double OnePercentLowFps { get; }
string Phase { get; }
void Stop();
void Update(double seconds, FlyCamera camera);
void RecordFrame(double seconds);
}

View File

@@ -0,0 +1,29 @@
using Universe.Core.Generation;
using Universe.Core.Math;
using Universe.Core.World;
namespace Universe.App.Runtime;
internal readonly record struct StarCameraPose(UniversePosition Position, DVector3 Forward, DVector3 Right, DVector3 Up,
double TanHalfFov, int Width, int Height, long CutVersion);
internal sealed class StarMotionHistory
{
private bool _initialized;
public StarCameraPose Current { get; private set; }
public StarCameraPose Previous { get; private set; }
public bool Valid { get; private set; }
public void Advance(FlyCamera camera, int width, int height, double frameSeconds, StarMotionBlurSettings settings)
{
var pose = new StarCameraPose(camera.Position, camera.Forward, camera.Right, camera.Up,
System.Math.Tan(camera.FieldOfViewRadians * .5), width, height, camera.MotionHistoryVersion);
Previous = Current;
Valid = _initialized && settings.Enabled && double.IsFinite(frameSeconds) && frameSeconds > 0
&& frameSeconds <= settings.HistoryResetSeconds && Previous.Width == width && Previous.Height == height
&& Previous.CutVersion == pose.CutVersion && System.Math.Abs(Previous.TanHalfFov / pose.TanHalfFov - 1) < .15;
Current = pose;
if (!Valid) Previous = Current;
_initialized = true;
}
}