galaxer och motion blur
This commit is contained in:
@@ -13,6 +13,8 @@ internal sealed class BenchmarkOverlay : IDisposable
|
||||
private long _lastRevision = -1;
|
||||
private int _lastTextTick = -1;
|
||||
private string _lastPhase = string.Empty;
|
||||
private IBenchmarkSession? _session;
|
||||
private (int Width, int Height) _size;
|
||||
|
||||
public BenchmarkOverlay(GL gl)
|
||||
{
|
||||
@@ -21,8 +23,10 @@ internal sealed class BenchmarkOverlay : IDisposable
|
||||
_curve = new(gl, 6, [], dynamic: true);
|
||||
}
|
||||
|
||||
public void Draw(int width, int height, BenchmarkRunner benchmark)
|
||||
public void Draw(int width, int height, IBenchmarkSession benchmark)
|
||||
{
|
||||
if (_session != benchmark || _size != (width, height))
|
||||
{ _session = benchmark; _size = (width, height); _lastRevision = -1; _lastTextTick = -1; _lastPhase = string.Empty; }
|
||||
var scale = System.Math.Clamp(System.Math.Min(width / 1440d, height / 900d), .58, 1.2);
|
||||
var x = width - 402 * scale;
|
||||
var y = 14 * scale;
|
||||
@@ -53,13 +57,16 @@ internal sealed class BenchmarkOverlay : IDisposable
|
||||
_curve.Draw(GLEnum.LineStrip);
|
||||
}
|
||||
|
||||
private void UpdatePanel(int width, int height, BenchmarkRunner benchmark, double x, double y,
|
||||
private void UpdatePanel(int width, int height, IBenchmarkSession benchmark, double x, double y,
|
||||
double panelWidth, double panelHeight, double scale)
|
||||
{
|
||||
var vertices = new List<float>(20_000);
|
||||
Rect(x, y, panelWidth, panelHeight, .018f, .028f, .05f);
|
||||
Rect(x, y, 3 * scale, panelHeight, .13f, .55f, .8f);
|
||||
Text(benchmark.IsRunning ? "BENCHMARK LIVE" : benchmark.Phase == "STOPPED" ? "BENCHMARK STOPPED" : "BENCHMARK COMPLETE",
|
||||
var title=benchmark is GalaxyJourneyBenchmark journey
|
||||
? journey.IsComplete ? journey.PerformancePassed ? "GALAXY JOURNEY PASS" : "GALAXY JOURNEY FAIL" : journey.IsRunning ? "GALAXY JOURNEY LIVE" : "GALAXY JOURNEY STOPPED"
|
||||
: benchmark.IsRunning ? "BENCHMARK LIVE" : benchmark.Phase == "STOPPED" ? "BENCHMARK STOPPED" : "BENCHMARK COMPLETE";
|
||||
Text(title,
|
||||
x + 16 * scale, y + 11 * scale, 1.75 * scale, .76f, .91f, 1f);
|
||||
Text($"PHASE {benchmark.Phase}", x + 16 * scale, y + 29 * scale, 1.32 * scale, .52f, .7f, .82f);
|
||||
Text($"TARGET {benchmark.TargetLabel}", x + 16 * scale, y + 47 * scale,
|
||||
@@ -74,6 +81,11 @@ internal sealed class BenchmarkOverlay : IDisposable
|
||||
1.20 * scale, .7f, .83f, .91f);
|
||||
Text($"MIN {Number(benchmark.MinimumFps)} 1 PCT LOW {Number(benchmark.OnePercentLowFps)}",
|
||||
x + 16 * scale, y + 132 * scale, 1.02 * scale, .7f, .83f, .91f);
|
||||
var maximum=benchmark.GraphSamples.Count==0?60:System.Math.Max(60,System.Math.Ceiling(benchmark.GraphSamples.Max()/30)*30);
|
||||
Text($"{maximum:0} FPS",x+21*scale,y+154*scale,.95*scale,.4f,.6f,.7f);
|
||||
Text("0 S",x+17*scale,y+219*scale,.95*scale,.4f,.6f,.7f);
|
||||
var end=$"{benchmark.ElapsedSeconds:0} S";
|
||||
Text(end,x+panelWidth-(end.Length*6*.95+17)*scale,y+219*scale,.95*scale,.4f,.6f,.7f);
|
||||
|
||||
_panel.ReplaceVertices(vertices.ToArray());
|
||||
|
||||
@@ -108,7 +120,6 @@ internal sealed class BenchmarkOverlay : IDisposable
|
||||
var grid = new List<float>(1800);
|
||||
var curve = new List<float>(samples.Count * 6);
|
||||
var maximum = samples.Count == 0 ? 60 : System.Math.Max(60, System.Math.Ceiling(samples.Max() / 30) * 30);
|
||||
maximum = System.Math.Min(maximum, 240);
|
||||
for (var i = 0; i <= 4; i++)
|
||||
{
|
||||
var py = y + graphHeight * i / 4;
|
||||
|
||||
187
src/Universe.App/Graphics/CosmicRenderer.cs
Normal file
187
src/Universe.App/Graphics/CosmicRenderer.cs
Normal file
@@ -0,0 +1,187 @@
|
||||
using System.Numerics;
|
||||
using Silk.NET.OpenGL;
|
||||
using Universe.App.Runtime;
|
||||
using Universe.Core.Generation;
|
||||
using Universe.Core.Math;
|
||||
using Universe.Core.World;
|
||||
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
internal sealed class CosmicRenderer : IDisposable
|
||||
{
|
||||
private sealed class Cloud(UniversePosition center, double scale, PlanetOrientation orientation)
|
||||
{
|
||||
public UniversePosition Center = center;
|
||||
public double Scale = scale;
|
||||
public PlanetOrientation Orientation = orientation;
|
||||
public Task<float[]>? Pending;
|
||||
public GpuMesh? Mesh;
|
||||
public int LastFrame;
|
||||
}
|
||||
private readonly GL _gl;
|
||||
private readonly GalaxyUniverse _universe;
|
||||
private readonly StarTrailRenderer _trails;
|
||||
private readonly ShaderProgram _volume, _stars;
|
||||
private readonly GpuMesh _fullscreen;
|
||||
private readonly GpuMesh _markers;
|
||||
private readonly Dictionary<string, Cloud> _clouds = [];
|
||||
private readonly Dictionary<string, uint> _textures = [];
|
||||
private readonly uint _white;
|
||||
private int _frame;
|
||||
private int _uploads;
|
||||
private Cloud? _localCurrent, _localPrevious;
|
||||
private double _localBlend = 1;
|
||||
private string? _localGalaxyId;
|
||||
private long _localCutVersion = -1;
|
||||
public int StarCount { get; private set; }
|
||||
public CosmicRenderer(GL gl, GalaxyUniverse universe, StarTrailRenderer trails)
|
||||
{
|
||||
_gl = gl; _universe = universe; _trails = trails;
|
||||
_volume = ShaderProgram.Create(gl, ShaderSources.Load("fullscreen.vert"), ShaderSources.Load("galaxy.frag"));
|
||||
_stars = ShaderProgram.Create(gl, ShaderSources.Load("cosmic.vert"), ShaderSources.Load("cosmic.frag"));
|
||||
_fullscreen = new(gl, 3, [-1,-1,0,3,-1,0,-1,3,0]);
|
||||
_markers = new(gl,7,[],dynamic:true);
|
||||
_white = Texture(new RasterImage(2,2,Enumerable.Repeat((byte)255,16).ToArray()));
|
||||
}
|
||||
public void Draw(FlyCamera camera, int width, int height, double frameSeconds)
|
||||
{
|
||||
_frame++; _uploads = 0; StarCount = 0;
|
||||
_gl.Disable(GLEnum.DepthTest); _gl.DepthMask(false); _gl.Enable(GLEnum.Blend); _gl.BlendFunc(GLEnum.One, GLEnum.One);
|
||||
var galaxies = _universe.VisibleGalaxies(camera.Position).OrderBy(g => camera.Position.RelativeTo(g.Center, camera.CellSizeMeters).LengthSquared).ToArray();
|
||||
foreach (var galaxy in galaxies.Take(10))
|
||||
{
|
||||
DrawVolume(camera, width, height, galaxy.Id, galaxy.Center, galaxy.RadiusMeters, galaxy.ThicknessMeters / galaxy.RadiusMeters,
|
||||
galaxy.Orientation, galaxy.SpiralArms, galaxy.DensityGamma, galaxy.Seed, galaxy.Texture, 0);
|
||||
if(camera.Position.RelativeTo(galaxy.Center,camera.CellSizeMeters).Length < galaxy.RadiusMeters*6)
|
||||
{
|
||||
var cloud = GetCloud(galaxy.Id, galaxy.Center, galaxy.RadiusMeters, galaxy.Orientation,
|
||||
() => BuildStars(galaxy, null, _universe.Settings.GalaxyStarBudget));
|
||||
DrawCloud(camera, width, height, cloud, _universe.SpacingCells * _universe.CellSize * 24 / galaxy.RadiusMeters);
|
||||
}
|
||||
}
|
||||
var current = _universe.GalaxyAt(camera.Position);
|
||||
if (_localGalaxyId != current.Id || _localCutVersion != camera.MotionHistoryVersion)
|
||||
{
|
||||
_localCurrent = null; _localPrevious = null; _localBlend = 1;
|
||||
_localGalaxyId = current.Id; _localCutVersion = camera.MotionHistoryVersion;
|
||||
}
|
||||
var markerScale=_universe.Settings.RegionSizeLightYears*UniverseScale.LightYearMeters;
|
||||
var markers=new List<float>(256);
|
||||
void Marker(UniversePosition position,double radius,Vector3 color,bool forcePoint=false)
|
||||
{
|
||||
var relative=position.RelativeTo(camera.Position,camera.CellSizeMeters);
|
||||
var angular=radius/System.Math.Max(radius,relative.Length);
|
||||
var t=System.Math.Clamp((angular-.004)/.021,0,1);var fade=forcePoint?1:1-t*t*(3-2*t);
|
||||
if(fade<.01)return;
|
||||
var p=relative/markerScale;
|
||||
markers.AddRange([(float)p.X,(float)p.Y,(float)p.Z,color.X*(float)fade,color.Y*(float)fade,color.Z*(float)fade,2.5f]);
|
||||
}
|
||||
var volumeIds=galaxies.Take(10).Select(g=>g.Id).ToHashSet();
|
||||
foreach(var galaxy in galaxies)Marker(galaxy.Center,galaxy.RadiusMeters,new(.3f,.3f,.5f),!volumeIds.Contains(galaxy.Id));
|
||||
var nearNebulae=current.Nebulae.OrderBy(n=>camera.Position.RelativeTo(n.Center,camera.CellSizeMeters).LengthSquared).Take(5).Select(n=>n.Id).ToHashSet();
|
||||
foreach(var nebula in current.Nebulae)Marker(nebula.Center,nebula.RadiusMeters,new(.2f,.12f,.26f),!nearNebulae.Contains(nebula.Id));
|
||||
_markers.ReplaceVertices(markers.ToArray());
|
||||
Camera(_stars,camera,width,height,camera.Position,markerScale,PlanetOrientation.Identity);
|
||||
_stars.Set("uNearFade",1e-10f);_markers.Draw(GLEnum.Points);
|
||||
if (camera.Position.RelativeTo(current.Center, camera.CellSizeMeters).Length < current.RadiusMeters * 1.8)
|
||||
{
|
||||
var at = _universe.StellarBlock(camera.Position.Sector);
|
||||
// A stable sampling anchor spans 32 stellar blocks. Crossing every
|
||||
// individual system grid boundary must not replace the whole sky.
|
||||
var block = new SectorCoordinate(GalaxyUniverse.FloorDiv(at.X,32)*32,
|
||||
GalaxyUniverse.FloorDiv(at.Y,32)*32,GalaxyUniverse.FloorDiv(at.Z,32)*32);
|
||||
var origin = UniversePosition.AtCellCenter(new(block.X * _universe.SpacingCells, block.Y * _universe.SpacingCells, block.Z * _universe.SpacingCells), camera.CellSizeMeters);
|
||||
var scale = _universe.SpacingCells * _universe.CellSize * 64;
|
||||
var cloud = GetCloud($"local:{block.X}:{block.Y}:{block.Z}", origin, scale, PlanetOrientation.Identity,
|
||||
() => BuildStars(current, (origin, scale, block), _universe.Settings.LocalStarBudget));
|
||||
if (cloud.Mesh is not null && cloud != _localCurrent)
|
||||
{
|
||||
_localPrevious = _localCurrent; _localCurrent = cloud;
|
||||
_localBlend = 0;
|
||||
}
|
||||
_localBlend = System.Math.Min(1,_localBlend+System.Math.Clamp(frameSeconds,0,.1)/_trails.Settings.StarFadeSeconds);
|
||||
var fadeDistance = _universe.SpacingCells * _universe.CellSize * .15 / scale;
|
||||
if (_localPrevious is not null && _localBlend < 1) DrawCloud(camera,width,height,_localPrevious,fadeDistance,(float)(1-_localBlend));
|
||||
if (_localCurrent is not null) DrawCloud(camera,width,height,_localCurrent,fadeDistance,(float)_localBlend);
|
||||
if (_localBlend >= 1) _localPrevious = null;
|
||||
foreach (var n in current.Nebulae.OrderBy(n => camera.Position.RelativeTo(n.Center, camera.CellSizeMeters).LengthSquared).Take(5))
|
||||
DrawVolume(camera, width, height, n.Id, n.Center, n.RadiusMeters, 1, n.Orientation, 0, 1, n.Seed, n.Texture, 1);
|
||||
}
|
||||
if (_clouds.Count > 48)
|
||||
foreach (var key in _clouds.Where(p => p.Value != _localCurrent && p.Value != _localPrevious && p.Value.LastFrame < _frame - 2 && p.Value.Pending?.IsCompleted != false).Select(p => p.Key).Take(_clouds.Count - 48).ToArray())
|
||||
{ _clouds[key].Mesh?.Dispose(); _clouds.Remove(key); }
|
||||
_gl.Disable(GLEnum.Blend); _gl.Enable(GLEnum.DepthTest);
|
||||
}
|
||||
private Cloud GetCloud(string key, UniversePosition center, double scale, PlanetOrientation orientation, Func<float[]> build)
|
||||
{
|
||||
if (!_clouds.TryGetValue(key, out var cloud))
|
||||
{ cloud = new(center, scale, orientation); _clouds.Add(key, cloud); }
|
||||
cloud.LastFrame = _frame;
|
||||
if (cloud.Pending is null && cloud.Mesh is null && _clouds.Values.Count(c => c.Pending?.IsCompleted == false) < 2)
|
||||
cloud.Pending = Task.Run(build);
|
||||
if (cloud.Pending is { IsCompletedSuccessfully: true } ready && _uploads < 1)
|
||||
{ cloud.Mesh = new(_gl, 7, ready.Result); cloud.Pending = null; _uploads++; }
|
||||
return cloud;
|
||||
}
|
||||
private float[] BuildStars(GalaxyData galaxy, (UniversePosition Center, double Scale, SectorCoordinate Block)? local, int budget)
|
||||
{
|
||||
var random = new DeterministicRandom(galaxy.Seed ^ (local.HasValue ? GalaxyUniverse.Hash(local.Value.Block, 55) : 77));
|
||||
var vertices = new List<float>(budget * 7); var used = new HashSet<SectorCoordinate>();
|
||||
for (var i = 0; i < budget * 24 && used.Count < budget; i++)
|
||||
{
|
||||
SectorCoordinate block;
|
||||
if (local is { } l) block = new(l.Block.X + random.NextInt(-64,65), l.Block.Y + random.NextInt(-64,65), l.Block.Z + random.NextInt(-64,65));
|
||||
else
|
||||
{
|
||||
var p = new DVector3(random.NextDouble()*2-1,(random.NextDouble()*6-3)*galaxy.ThicknessMeters/galaxy.RadiusMeters,random.NextDouble()*2-1) * galaxy.RadiusMeters;
|
||||
block = _universe.StellarBlock(galaxy.Center.Add(galaxy.Orientation.ToWorld(p), _universe.CellSize).Sector);
|
||||
}
|
||||
if (!_universe.Candidate(galaxy, block, out var sector) || !used.Add(sector)) continue;
|
||||
var position = UniversePosition.AtCellCenter(sector, _universe.CellSize);
|
||||
var delta = local is { } ll ? position.RelativeTo(ll.Center, _universe.CellSize) / ll.Scale
|
||||
: galaxy.Orientation.ToBody(position.RelativeTo(galaxy.Center, _universe.CellSize)) / galaxy.RadiusMeters;
|
||||
var tint = random.NextDouble(); var brightness = .14 + random.NextDouble() * .9;
|
||||
vertices.AddRange([(float)delta.X,(float)delta.Y,(float)delta.Z,(float)(brightness*(.7+tint*.3)),(float)(brightness*.82),(float)(brightness*(1.0-tint*.4)),(float)(1.2+random.NextDouble()*2.3)]);
|
||||
}
|
||||
return vertices.ToArray();
|
||||
}
|
||||
private void Camera(ShaderProgram shader, FlyCamera camera, int width, int height, UniversePosition center, double scale, PlanetOrientation orientation)
|
||||
{
|
||||
shader.Use(); shader.Set("uOrigin", (camera.Position.RelativeTo(center, camera.CellSizeMeters)/scale).ToVector3());
|
||||
shader.Set("uAxisX", orientation.ToWorld(new(1,0,0)).ToVector3()); shader.Set("uAxisY", orientation.ToWorld(new(0,1,0)).ToVector3()); shader.Set("uAxisZ", orientation.ToWorld(new(0,0,1)).ToVector3());
|
||||
shader.Set("uForward",camera.Forward.ToVector3()); shader.Set("uRight",camera.Right.ToVector3()); shader.Set("uUp",camera.Up.ToVector3());
|
||||
shader.Set("uAspect",(float)width/height); shader.Set("uTanHalfFov",(float)System.Math.Tan(camera.FieldOfViewRadians*.5));
|
||||
}
|
||||
private void DrawCloud(FlyCamera camera,int width,int height,Cloud cloud,double nearFade,float visibility=1)
|
||||
{
|
||||
if (cloud.Mesh is null) return;
|
||||
cloud.LastFrame = _frame;
|
||||
_trails.Draw(cloud.Mesh,cloud.Center,cloud.Scale,cloud.Orientation,nearFade,visibility);
|
||||
}
|
||||
private void DrawVolume(FlyCamera camera,int width,int height,string id,UniversePosition center,double radius,double thickness,
|
||||
PlanetOrientation orientation,int arms,double gamma,ulong seed,RasterImage? texture,int kind)
|
||||
{
|
||||
var relative = center.RelativeTo(camera.Position,camera.CellSizeMeters);
|
||||
if (DVector3.Dot(relative,camera.Forward)<-radius*1.2 || radius / System.Math.Max(radius,relative.Length) < .0003) return;
|
||||
var angular=radius/System.Math.Max(radius,relative.Length);
|
||||
var opacity=System.Math.Clamp((angular-.004)/.021,0,1);opacity=opacity*opacity*(3-2*opacity);
|
||||
if(opacity<=0)return;
|
||||
Camera(_volume,camera,width,height,center,radius,orientation);
|
||||
_volume.Set("uOpacity",(float)opacity);
|
||||
_volume.Set("uThickness",(float)thickness); _volume.Set("uArms",(float)arms); _volume.Set("uGamma",(float)gamma);
|
||||
_volume.Set("uSeed",(float)(seed&65535)*.001f); _volume.Set("uKind",kind); _volume.Set("uSteps",_universe.Settings.VolumeSteps);
|
||||
_volume.Set("uHasTexture",texture is null?0:1); _volume.Set("uDensity",0);
|
||||
_gl.ActiveTexture(GLEnum.Texture0);
|
||||
if (texture is not null && !_textures.ContainsKey(id)) _textures[id]=Texture(texture);
|
||||
_gl.BindTexture(GLEnum.Texture2D,texture is null?_white:_textures[id]); _fullscreen.Draw();
|
||||
}
|
||||
private unsafe uint Texture(RasterImage image)
|
||||
{
|
||||
var handle=_gl.GenTexture();_gl.BindTexture(GLEnum.Texture2D,handle);
|
||||
fixed(byte* pixels=image.Rgba) _gl.TexImage2D(GLEnum.Texture2D,0,InternalFormat.Rgba8,(uint)image.Width,(uint)image.Height,0,PixelFormat.Rgba,PixelType.UnsignedByte,pixels);
|
||||
_gl.TexParameter(GLEnum.Texture2D,GLEnum.TextureMinFilter,(int)GLEnum.Linear);_gl.TexParameter(GLEnum.Texture2D,GLEnum.TextureMagFilter,(int)GLEnum.Linear);
|
||||
_gl.TexParameter(GLEnum.Texture2D,GLEnum.TextureWrapS,(int)GLEnum.ClampToEdge);_gl.TexParameter(GLEnum.Texture2D,GLEnum.TextureWrapT,(int)GLEnum.ClampToEdge);
|
||||
return handle;
|
||||
}
|
||||
public void Dispose(){foreach(var cloud in _clouds.Values)cloud.Mesh?.Dispose();foreach(var texture in _textures.Values)_gl.DeleteTexture(texture);_gl.DeleteTexture(_white);_volume.Dispose();_stars.Dispose();_fullscreen.Dispose();_markers.Dispose();}
|
||||
}
|
||||
@@ -13,6 +13,100 @@ 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 RunStarTrails(GL gl,StarMotionBlurSettings settings,int width,int height)
|
||||
{
|
||||
using var trails=new StarTrailRenderer(gl,settings with {Enabled=true});
|
||||
using var target=new RenderTarget(gl,width,height);
|
||||
var cell=UniverseScale.DefaultCellSizeMeters;const double scale=1e12;
|
||||
var center=UniversePosition.AtCellCenter(new(600_000_000_000_000_000,0,0),cell);
|
||||
var camera=new FlyCamera(center,cell);
|
||||
var random=new DeterministicRandom(0x5354415254524149UL);var data=new List<float>();
|
||||
for(var i=0;i<900;i++)
|
||||
{
|
||||
var z=8+random.NextDouble()*52;
|
||||
data.AddRange([(float)((random.NextDouble()*2-1)*z*.7),(float)((random.NextDouble()*2-1)*z*.43),-(float)z,
|
||||
1.1f,(float)(.75+random.NextDouble()*.35),.8f,(float)(1.5+random.NextDouble()*1.5)]);
|
||||
}
|
||||
using var stars=new GpuMesh(gl,7,data.ToArray());
|
||||
float[] Frame(GpuMesh mesh)
|
||||
{
|
||||
target.Bind();gl.Disable(GLEnum.DepthTest);gl.DepthMask(false);
|
||||
gl.ClearColor(0,0,0,1);gl.Clear(ClearBufferMask.ColorBufferBit);
|
||||
gl.Enable(GLEnum.Blend);gl.BlendFunc(GLEnum.One,GLEnum.One);
|
||||
trails.BeginFrame(camera,width,height,1d/60);
|
||||
trails.Draw(mesh,center,scale,PlanetOrientation.Identity);
|
||||
gl.Finish();return Read(gl,width,height);
|
||||
}
|
||||
Frame(stars);
|
||||
camera.SetPlayerPose(center.Add(new(0,0,-12*scale),cell),camera.Forward,camera.Up);
|
||||
var moving=Frame(stars);
|
||||
var stopped=Frame(stars);
|
||||
int Coverage(float[] pixels)=>Enumerable.Range(0,width*height).Count(i=>pixels[i*4]>.035);
|
||||
var movingCoverage=Coverage(moving);var stillCoverage=Coverage(stopped);
|
||||
if(moving.Any(p=>!float.IsFinite(p))||movingCoverage<stillCoverage*1.5)
|
||||
throw new InvalidOperationException($"Star trails did not stretch the moving stars: moving {movingCoverage}, stopped {stillCoverage} pixels.");
|
||||
trails.ReloadShaders();
|
||||
camera.SetPlayerPose(camera.Position,camera.Forward,camera.Up,resetMotionHistory:true);
|
||||
var cut=Frame(stars);
|
||||
if(trails.History.Valid||cut.Zip(stopped,(a,b)=>System.Math.Abs(a-b)).Max()>.0001)
|
||||
throw new InvalidOperationException("Star trails retained an afterimage after stopping or a camera cut.");
|
||||
using var single=new GpuMesh(gl,7,[0,0,-10,1,1,1,2]);
|
||||
camera.SetPlayerPose(center,camera.Forward,camera.Up,resetMotionHistory:true);Frame(single);
|
||||
camera.SetPlayerPose(center.Add(new(-6*scale,0,0),cell),camera.Forward,camera.Up);
|
||||
var bounded=Frame(single);
|
||||
var columns=Enumerable.Range(0,width).Where(x=>Enumerable.Range(0,height).Any(y=>bounded[(y*width+x)*4]>.035)).ToArray();
|
||||
var span=columns.Length==0?0:columns[^1]-columns[0]+1;
|
||||
if(span<settings.MaximumTrailPixels*.5||span>settings.MaximumTrailPixels+14)
|
||||
throw new InvalidOperationException($"Star trail pixel limit failed: span {span}, limit {settings.MaximumTrailPixels}.");
|
||||
if(gl.GetError() is var error&&error!=GLEnum.NoError)throw new InvalidOperationException($"Star trails GL error: {error}");
|
||||
var directory=Path.Combine(AppContext.BaseDirectory,"graphics-report");Directory.CreateDirectory(directory);
|
||||
WritePng(Path.Combine(directory,"star-motion-blur.png"),width,height,moving);
|
||||
WritePng(Path.Combine(directory,"star-motion-stopped.png"),width,height,stopped);
|
||||
Console.WriteLine($"Star trails passed: {movingCoverage} moving vs {stillCoverage} stationary pixels; capped trail {span} px; camera cut/stop clean; OpenGL {gl.GetStringS(GLEnum.Renderer)}.");
|
||||
gl.BindFramebuffer(GLEnum.Framebuffer,0);gl.Disable(GLEnum.Blend);gl.Enable(GLEnum.DepthTest);gl.DepthMask(true);
|
||||
}
|
||||
|
||||
public static void RunGalaxyJourney(GL gl, UniverseRenderer renderer, FlyCamera camera, UniverseStreamer streamer,
|
||||
GalaxyJourneyBenchmark journey, int width, int height)
|
||||
{
|
||||
var neighborhood = streamer.Load(camera.Position.Sector);
|
||||
journey.RenderEnvironment = new { Width = width, Height = height, Gpu = renderer.HardwareName, Mode = "synchronized-accelerated", SimulationStepSeconds = 1d/12, FieldOfViewDegrees = camera.FieldOfViewRadians * 180 / System.Math.PI };
|
||||
var startPosition=camera.Position;var startForward=camera.Forward;var startUp=camera.Up;
|
||||
var nebula=journey.HomeGalaxy.Nebulae[0];
|
||||
var radial=nebula.Orientation.ToWorld(new DVector3(.1,.05,1).Normalized());
|
||||
camera.SetPlayerPose(nebula.Center.Add(radial*nebula.RadiusMeters*2.8,camera.CellSizeMeters),-radial,nebula.Orientation.ToWorld(new(0,1,0)),resetMotionHistory:true);
|
||||
for(var i=0;i<6;i++){renderer.Render(camera,streamer.Load(camera.Position.Sector),1d/60);gl.Finish();}
|
||||
var nebulaDirectory=Path.Combine(AppContext.BaseDirectory,"graphics-report");Directory.CreateDirectory(nebulaDirectory);
|
||||
WritePng(Path.Combine(nebulaDirectory,"galaxy-nebula.png"),width,height,Read(gl,width,height),false);
|
||||
camera.SetPlayerPose(startPosition,startForward,startUp,resetMotionHistory:true);
|
||||
neighborhood=streamer.Load(camera.Position.Sector);
|
||||
for (var i=0;i<8;i++) {renderer.Render(camera,neighborhood,1d/60);gl.Finish();}
|
||||
var directory = Path.Combine(AppContext.BaseDirectory,"graphics-report");Directory.CreateDirectory(directory);
|
||||
var captured = new HashSet<string>();
|
||||
var timer = System.Diagnostics.Stopwatch.StartNew();
|
||||
while(journey.IsRunning)
|
||||
{
|
||||
journey.Update(1d/12,camera);
|
||||
neighborhood=streamer.Load(camera.Position.Sector);
|
||||
renderer.Render(camera,neighborhood,1d/12);renderer.DrawBenchmarkOverlay(journey);gl.Finish();
|
||||
var seconds=timer.Elapsed.TotalSeconds;timer.Restart();
|
||||
journey.RecordFrame(seconds);
|
||||
if(captured.Add(journey.Phase))Console.WriteLine($"Galaxy journey: {journey.Phase}; sector {camera.Position.Sector}; galaxy {neighborhood.ActiveSystem.GalaxyId}");
|
||||
// Capture late in each phase, after the point-cloud workers have had time to publish.
|
||||
if(journey.ElapsedSeconds is >23.8 and <24 || journey.ElapsedSeconds is >44.8 and <45 || journey.ElapsedSeconds is >53.8 and <54 || journey.ElapsedSeconds is >89.8 and <90)
|
||||
{
|
||||
renderer.DrawBenchmarkOverlay(journey);
|
||||
WritePng(Path.Combine(directory,$"galaxy-journey-{(int)journey.ElapsedSeconds}.png"),width,height,Read(gl,width,height),false);
|
||||
timer.Restart();
|
||||
}
|
||||
}
|
||||
renderer.DrawBenchmarkOverlay(journey);
|
||||
WritePng(Path.Combine(directory,"galaxy-journey-results.png"),width,height,Read(gl,width,height),false);
|
||||
if(gl.GetError() is var error && error!=GLEnum.NoError)throw new InvalidOperationException($"Galaxy journey GL error: {error}");
|
||||
var report=journey.SaveReport();
|
||||
Console.WriteLine($"Galaxy journey completed; target {journey.TargetGalaxy.Id} / {journey.TargetPlanet.Id}; average {journey.AverageFps:F1} FPS; 1% low {journey.OnePercentLowFps:F1} FPS; performance passed {journey.PerformancePassed}; report {report}");
|
||||
if(!journey.PerformancePassed)Environment.ExitCode=2;
|
||||
}
|
||||
public static void RunBlackHole(GL gl, UniverseRenderer renderer, UniverseNeighborhood neighborhood, StarData hole,
|
||||
double cellSize, int width, int height)
|
||||
{
|
||||
@@ -74,12 +168,7 @@ internal static class GraphicsReport
|
||||
var dir = Path.Combine(AppContext.BaseDirectory, "graphics-report"); Directory.CreateDirectory(dir);
|
||||
WritePng(Path.Combine(dir, "pause-menu.png"), width, height, pixels, false);
|
||||
var scale = System.Math.Min(width / 1440d, height / 900d);
|
||||
if (renderer.MenuHit(width * .5, height * .5 - 152 * scale, width, height) != 0
|
||||
|| renderer.MenuHit(width * .5, height * .5 - 95 * scale, width, height) != 1
|
||||
|| renderer.MenuHit(width * .5, height * .5 - 38 * scale, width, height) != 2
|
||||
|| renderer.MenuHit(width * .5, height * .5 + 19 * scale, width, height) != 3
|
||||
|| renderer.MenuHit(width * .5, height * .5 + 76 * scale, width, height) != 4
|
||||
|| renderer.MenuHit(width * .5, height * .5 + 133 * scale, width, height) != 5
|
||||
if (Enumerable.Range(0,7).Any(i => renderer.MenuHit(width*.5,height*.5+(-182+i*57)*scale,width,height)!=i)
|
||||
|| renderer.MenuHit(0, 0, width, height) != -1)
|
||||
throw new InvalidOperationException("Menu hit testing failed.");
|
||||
Console.WriteLine($"Pause menu rendered and hit testing passed. Image: {dir}");
|
||||
|
||||
@@ -8,7 +8,7 @@ internal sealed class PauseMenu : IDisposable
|
||||
private readonly GpuMesh _mesh;
|
||||
public int Selected { get; set; }
|
||||
public PauseMenu(GL gl) => _mesh = new(gl, 6, [], dynamic: true);
|
||||
private static readonly string[] Labels = ["PLAY", "FREE CAMERA", "DEBUG OFF", "BENCHMARK EARTH", "BENCHMARK UNKNOWN", "QUIT"];
|
||||
private static readonly string[] Labels = ["PLAY", "FREE CAMERA", "DEBUG OFF", "BENCHMARK EARTH", "BENCHMARK UNKNOWN", "GALAXY JOURNEY", "QUIT"];
|
||||
internal static readonly Dictionary<char, string[]> Font = new()
|
||||
{
|
||||
['A']=["01110","10001","10001","11111","10001","10001","10001"], ['C']=["01111","10000","10000","10000","10000","10000","01111"],
|
||||
@@ -18,6 +18,7 @@ internal sealed class PauseMenu : IDisposable
|
||||
['H']=["10001","10001","10001","11111","10001","10001","10001"], ['N']=["10001","11001","10101","10011","10001","10001","10001"],
|
||||
['V']=["10001","10001","10001","10001","10001","01010","00100"], ['W']=["10001","10001","10001","10101","10101","10101","01010"],
|
||||
['K']=["10001","10010","10100","11000","10100","10010","10001"],
|
||||
['J']=["00111","00010","00010","00010","10010","10010","01100"], ['X']=["10001","10001","01010","00100","01010","10001","10001"],
|
||||
['L']=["10000","10000","10000","10000","10000","10000","11111"], ['M']=["10001","11011","10101","10101","10001","10001","10001"],
|
||||
['O']=["01110","10001","10001","10001","10001","10001","01110"], ['P']=["11110","10001","10001","11110","10000","10000","10000"],
|
||||
['Q']=["01110","10001","10001","10001","10101","10010","01101"], ['R']=["11110","10001","10001","11110","10100","10010","10001"],
|
||||
@@ -44,7 +45,7 @@ internal sealed class PauseMenu : IDisposable
|
||||
var (s, cx, cy) = Layout(width, height);
|
||||
if (s <= 0 || x < cx - 190 * s || x > cx + 190 * s) return -1;
|
||||
for (var i = 0; i < Labels.Length; i++)
|
||||
if (y >= cy + (-174 + i * 57) * s && y <= cy + (-130 + i * 57) * s) return i;
|
||||
if (y >= cy + (-204 + i * 57) * s && y <= cy + (-160 + i * 57) * s) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -70,19 +71,19 @@ internal sealed class PauseMenu : IDisposable
|
||||
x += 6 * unit;
|
||||
}
|
||||
}
|
||||
Rect(cx - 230*s, cy - 260*s, 460*s, 520*s, .025f, .04f, .075f);
|
||||
Rect(cx - 230*s, cy - 260*s, 460*s, 4*s, .25f, .65f, .9f);
|
||||
Text("PAUSED", cx, cy - 226*s, 5*s, .9f, .95f, 1);
|
||||
Rect(cx - 230*s, cy - 290*s, 460*s, 580*s, .025f, .04f, .075f);
|
||||
Rect(cx - 230*s, cy - 290*s, 460*s, 4*s, .25f, .65f, .9f);
|
||||
Text("PAUSED", cx, cy - 256*s, 5*s, .9f, .95f, 1);
|
||||
for (var i = 0; i < Labels.Length; i++)
|
||||
{
|
||||
var selected = i == Selected;
|
||||
var y = cy + (-174 + i * 57) * s;
|
||||
var y = cy + (-204 + i * 57) * s;
|
||||
Rect(cx - 190*s, y, 380*s, 44*s, selected ? .12f : .055f, selected ? .25f : .10f, selected ? .36f : .16f);
|
||||
if (selected) Rect(cx - 190*s, y, 4*s, 44*s, .35f, .8f, 1);
|
||||
var label = i == 2 ? debugEnabled ? "DEBUG ON" : "DEBUG OFF" : Labels[i];
|
||||
Text(label, cx, y + 12*s, (i >= 3 ? 3.35 : 4) * s, .92f, .95f, 1);
|
||||
}
|
||||
Text("P OR ESC TO RESUME", cx, cy + 220*s, 2*s, .6f, .7f, .8f);
|
||||
Text("P OR ESC TO RESUME", cx, cy + 250*s, 2*s, .6f, .7f, .8f);
|
||||
_mesh.ReplaceVertices(data.ToArray()); _mesh.Draw(GLEnum.Triangles);
|
||||
}
|
||||
public void Dispose() => _mesh.Dispose();
|
||||
|
||||
@@ -16,6 +16,7 @@ internal sealed class PerformanceOverlay : IDisposable
|
||||
var rows = new List<(string Label, string Value, bool Emphasis)>();
|
||||
AddMetric("GPU FRAME", "GPU FRAME", true);
|
||||
AddMetric("SKY / STARFIELD", "SKY / STARFIELD");
|
||||
AddMetric("GALAXIES / NEBULAE", "GALAXIES / NEBULAE");
|
||||
AddMetric("PLANETS TOTAL", "PLANETS TOTAL");
|
||||
foreach (var key in values.Keys.Where(k => k.StartsWith("PLANET ", StringComparison.Ordinal) && k != "PLANETS TOTAL" && k != "GPU FRAME")
|
||||
.OrderBy(k => k, StringComparer.Ordinal))
|
||||
|
||||
@@ -17,24 +17,29 @@ internal sealed unsafe class ShaderProgram : IDisposable
|
||||
|
||||
public uint Handle { get; }
|
||||
|
||||
public static ShaderProgram Create(GL gl, string vertexSource, string fragmentSource)
|
||||
public static ShaderProgram Create(GL gl, string vertexSource, string fragmentSource, string? geometrySource = null)
|
||||
{
|
||||
var vertex = Compile(gl, ShaderType.VertexShader, vertexSource);
|
||||
var fragment = Compile(gl, ShaderType.FragmentShader, fragmentSource);
|
||||
var program = gl.CreateProgram();
|
||||
gl.AttachShader(program, vertex);
|
||||
gl.AttachShader(program, fragment);
|
||||
gl.LinkProgram(program);
|
||||
gl.GetProgram(program, GLEnum.LinkStatus, out var linked);
|
||||
gl.DeleteShader(vertex);
|
||||
gl.DeleteShader(fragment);
|
||||
if (linked == 0)
|
||||
uint vertex = 0, fragment = 0, geometry = 0, program = 0;
|
||||
try
|
||||
{
|
||||
var log = gl.GetProgramInfoLog(program);
|
||||
gl.DeleteProgram(program);
|
||||
throw new InvalidOperationException($"OpenGL shader link failed: {log}");
|
||||
vertex = Compile(gl, ShaderType.VertexShader, vertexSource);
|
||||
fragment = Compile(gl, ShaderType.FragmentShader, fragmentSource);
|
||||
if (geometrySource is not null) geometry = Compile(gl, ShaderType.GeometryShader, geometrySource);
|
||||
program = gl.CreateProgram();
|
||||
gl.AttachShader(program, vertex); gl.AttachShader(program, fragment);
|
||||
if (geometry != 0) gl.AttachShader(program, geometry);
|
||||
gl.LinkProgram(program);
|
||||
gl.GetProgram(program, GLEnum.LinkStatus, out var linked);
|
||||
if (linked == 0) throw new InvalidOperationException($"OpenGL shader link failed: {gl.GetProgramInfoLog(program)}");
|
||||
return new(gl, program);
|
||||
}
|
||||
catch { if (program != 0) gl.DeleteProgram(program); throw; }
|
||||
finally
|
||||
{
|
||||
if (vertex != 0) gl.DeleteShader(vertex);
|
||||
if (fragment != 0) gl.DeleteShader(fragment);
|
||||
if (geometry != 0) gl.DeleteShader(geometry);
|
||||
}
|
||||
return new(gl, program);
|
||||
}
|
||||
|
||||
public void Use() => _gl.UseProgram(Handle);
|
||||
|
||||
4
src/Universe.App/Graphics/Shaders/cosmic.frag.glsl
Normal file
4
src/Universe.App/Graphics/Shaders/cosmic.frag.glsl
Normal file
@@ -0,0 +1,4 @@
|
||||
#version 330 core
|
||||
in vec3 vColor;
|
||||
out vec4 fragColor;
|
||||
void main(){vec2 p=gl_PointCoord*2.0-1.0;float r=dot(p,p);if(r>1.0)discard;fragColor=vec4(vColor*exp(-r*3.5),0);}
|
||||
23
src/Universe.App/Graphics/Shaders/cosmic.vert.glsl
Normal file
23
src/Universe.App/Graphics/Shaders/cosmic.vert.glsl
Normal file
@@ -0,0 +1,23 @@
|
||||
#version 330 core
|
||||
layout(location=0) in vec3 aPosition;
|
||||
layout(location=1) in vec3 aColor;
|
||||
layout(location=2) in float aSize;
|
||||
uniform vec3 uOrigin;
|
||||
uniform vec3 uAxisX;
|
||||
uniform vec3 uAxisY;
|
||||
uniform vec3 uAxisZ;
|
||||
uniform vec3 uForward;
|
||||
uniform vec3 uRight;
|
||||
uniform vec3 uUp;
|
||||
uniform float uAspect;
|
||||
uniform float uTanHalfFov;
|
||||
uniform float uNearFade;
|
||||
out vec3 vColor;
|
||||
void main(){
|
||||
vec3 delta=uAxisX*aPosition.x+uAxisY*aPosition.y+uAxisZ*aPosition.z-uOrigin;
|
||||
float d=length(delta),z=dot(delta,uForward);
|
||||
vec2 xy=vec2(dot(delta,uRight)/uAspect,dot(delta,uUp))/uTanHalfFov;
|
||||
gl_Position=z>0.0?vec4(xy,z*.999999,z):vec4(2,2,2,1);
|
||||
gl_PointSize=clamp(aSize,1.0,4.5);
|
||||
vColor=aColor*smoothstep(uNearFade,uNearFade*2.0,d);
|
||||
}
|
||||
62
src/Universe.App/Graphics/Shaders/galaxy.frag.glsl
Normal file
62
src/Universe.App/Graphics/Shaders/galaxy.frag.glsl
Normal file
@@ -0,0 +1,62 @@
|
||||
#version 330 core
|
||||
#include "camera-ray.glsl"
|
||||
in vec2 vUv;
|
||||
uniform vec3 uOrigin;
|
||||
uniform vec3 uAxisX;
|
||||
uniform vec3 uAxisY;
|
||||
uniform vec3 uAxisZ;
|
||||
uniform float uThickness;
|
||||
uniform float uArms;
|
||||
uniform float uGamma;
|
||||
uniform float uSeed;
|
||||
uniform int uKind;
|
||||
uniform int uSteps;
|
||||
uniform int uHasTexture;
|
||||
uniform sampler2D uDensity;
|
||||
uniform float uOpacity;
|
||||
out vec4 fragColor;
|
||||
float turbulence(vec3 p){return .5+.23*sin(p.x*17.0+sin(p.y*11.0))*sin(p.z*19.0+p.y*7.0)+.12*sin(dot(p,vec3(61,37,83)));}
|
||||
vec3 local(vec3 p){return vec3(dot(p,uAxisX),dot(p,uAxisY),dot(p,uAxisZ));}
|
||||
void main(){
|
||||
vec3 rd=local(cameraRay(vUv)),ro=local(uOrigin);
|
||||
vec3 bounds=uKind==0?vec3(1.05,max(.24,uThickness*5.0),1.05):vec3(1.1);
|
||||
vec3 o=ro/bounds,d=rd/bounds;
|
||||
float a=dot(d,d),b=dot(o,d),c=dot(o,o)-1.0,disc=b*b-a*c;
|
||||
if(disc<0.0)discard;
|
||||
float entry=max(0.0,(-b-sqrt(disc))/a),exit=(-b+sqrt(disc))/a;
|
||||
if(exit<entry)discard;
|
||||
float ds=(exit-entry)/float(uSteps);
|
||||
vec3 emission=vec3(0);float trans=1.0;
|
||||
for(int i=0;i<96;i++){
|
||||
if(i>=uSteps)break;
|
||||
vec3 p=ro+rd*(entry+(float(i)+.5)*ds);
|
||||
float n=turbulence(p*3.0+uSeed),density=0.0;
|
||||
vec3 color;
|
||||
if(uKind==0){
|
||||
float radius=length(p.xz);
|
||||
vec2 uv=p.xz*vec2(.5,-.5)+.5;
|
||||
vec4 map=texture(uDensity,clamp(uv,vec2(0),vec2(1)));
|
||||
float spiral=atan(p.z,p.x)*uArms-log(radius+.05)*5.8;
|
||||
float pattern=uHasTexture==1?dot(map.rgb,vec3(.2126,.7152,.0722))*map.a:
|
||||
(.05+pow(.5+.5*cos(spiral),5.0)*.8)*pow(max(0.0,1.0-radius),.7)+exp(-radius*radius*65.0)*.8;
|
||||
float disk=pow(clamp(pattern,0.0,1.0),uGamma)*exp(-abs(p.y)/uThickness)*(1.0-smoothstep(.94,1.0,radius));
|
||||
float bulge=exp(-dot(p,p)*100.0)*2.0;
|
||||
density=disk*(.55+.65*n)+bulge;
|
||||
color=mix(vec3(.24,.24,.65),vec3(1.6,1.35,.95),clamp(bulge+pattern*.7,0.0,1.0));
|
||||
if(uHasTexture==1)color=mix(color,map.rgb*2.1,.55);
|
||||
emission+=trans*color*density*ds*1.7;
|
||||
trans*=exp(-disk*(1.0-n)*ds*.7);
|
||||
}else{
|
||||
float shell1=exp(-pow((length((p-vec3(0,.36,0))*vec3(1,1.25,1))-.43)/.055,2.0));
|
||||
float shell2=exp(-pow((length((p+vec3(0,.36,0))*vec3(1,1.25,1))-.43)/.055,2.0));
|
||||
float torus=exp(-pow((length(p.xz)-.21)/.055,2.0)-p.y*p.y*240.0);
|
||||
density=(shell1+shell2+torus*2.5)*(.25+n*n);
|
||||
color=mix(vec3(1.2,.16,.025),vec3(.05,1.1,.68),exp(-dot(p,p)*18.0));
|
||||
if(uHasTexture==1){vec4 map=texture(uDensity,clamp(p.xy*vec2(.5,-.5)+.5,vec2(0),vec2(1)));density*=.35+dot(map.rgb,vec3(.33))*map.a;color=mix(color,map.rgb*2.5,.5);}
|
||||
emission+=trans*color*density*ds*.85;
|
||||
trans*=exp(-density*ds*.5);
|
||||
}
|
||||
}
|
||||
float localExposure=uKind==0&&length(ro)<1.1?.14:1.0;
|
||||
fragColor=vec4(emission*uOpacity*localExposure,0);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ uniform vec3 uStarDirections[2];
|
||||
uniform vec3 uStarColors[2];
|
||||
uniform float uStarAngles[2];
|
||||
uniform float uTime;
|
||||
uniform float uBackgroundStrength;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec2 xy=(vUv*2.0-1.0)*uTanHalfFov;
|
||||
@@ -18,6 +19,7 @@ void main() {
|
||||
float band=pow(max(0.0,1.0-abs(dot(rd,normalize(vec3(0.32,0.72,0.48))))),17.0);
|
||||
float dust=smoothstep(-0.26,0.36,fbm(rd*8.0+vec3(4.1,9.2,3.4)));
|
||||
vec3 color=vec3(0.00013,0.00020,0.00042)+vec3(0.0013,0.0016,0.0026)*band*dust;
|
||||
color*=uBackgroundStrength;
|
||||
for(int i=0;i<2;i++) {
|
||||
if(i>=uLightCount) break;
|
||||
float angle=acos(clamp(dot(rd,uStarDirections[i]),-1.0,1.0));
|
||||
|
||||
17
src/Universe.App/Graphics/Shaders/star-trail.frag.glsl
Normal file
17
src/Universe.App/Graphics/Shaders/star-trail.frag.glsl
Normal file
@@ -0,0 +1,17 @@
|
||||
#version 330 core
|
||||
noperspective in vec2 gPixel;
|
||||
flat in vec3 gColor;
|
||||
flat in float gLength;
|
||||
flat in float gSigma;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
float closest=clamp(gPixel.x,0.0,gLength);
|
||||
vec2 delta=vec2(gPixel.x-closest,gPixel.y);
|
||||
float sigma2=gSigma*gSigma+1.0/12.0;
|
||||
float profile=exp(-dot(delta,delta)/(2.0*sigma2));
|
||||
float along=gLength>.5?mix(.32,1.0,smoothstep(0.0,1.0,closest/gLength)):1.0;
|
||||
float head=exp(-((gPixel.x-gLength)*(gPixel.x-gLength)+gPixel.y*gPixel.y)/(2.0*sigma2));
|
||||
float brightness=profile*along+head*.15;
|
||||
if(brightness<.003)discard;
|
||||
fragColor=vec4(gColor*brightness,0);
|
||||
}
|
||||
58
src/Universe.App/Graphics/Shaders/star-trail.geom.glsl
Normal file
58
src/Universe.App/Graphics/Shaders/star-trail.geom.glsl
Normal file
@@ -0,0 +1,58 @@
|
||||
#version 330 core
|
||||
layout(points) in;
|
||||
layout(triangle_strip,max_vertices=4) out;
|
||||
in VS_OUT { vec4 previousClip; vec3 color; float size; } star[];
|
||||
uniform vec3 uViewport;
|
||||
uniform float uShutter;
|
||||
uniform float uMaximumTrailPixels;
|
||||
uniform float uMinimumWidthPixels;
|
||||
uniform float uMovingBrightness;
|
||||
noperspective out vec2 gPixel;
|
||||
flat out vec3 gColor;
|
||||
flat out float gLength;
|
||||
flat out float gSigma;
|
||||
|
||||
// Clip the swept segment to the viewport before bounding its length. A star
|
||||
// crossing an edge still leaves a visible trail without a full-screen triangle.
|
||||
bool clipAxis(float origin,float delta,float bound,inout float lo,inout float hi) {
|
||||
if(abs(delta)<1e-7)return abs(origin)<=bound;
|
||||
float a=(-bound-origin)/delta,b=(bound-origin)/delta;
|
||||
lo=max(lo,min(a,b));hi=min(hi,max(a,b));return lo<=hi;
|
||||
}
|
||||
void vertex(vec2 pixel,vec2 uv) {
|
||||
gl_Position=vec4(pixel/uViewport.xy*2.0,.999999,1);
|
||||
gPixel=uv;EmitVertex();
|
||||
}
|
||||
void main() {
|
||||
if(max(max(star[0].color.r,star[0].color.g),star[0].color.b)<.00001)return;
|
||||
vec4 head=gl_in[0].gl_Position,tail=star[0].previousClip;
|
||||
if(uShutter<=0.0)tail=head;
|
||||
const float nearPlane=.00001;
|
||||
if(head.w<=nearPlane&&tail.w<=nearPlane)return;
|
||||
if(head.w<nearPlane)head=mix(tail,head,(tail.w-nearPlane)/(tail.w-head.w));
|
||||
if(tail.w<nearPlane)tail=mix(head,tail,(head.w-nearPlane)/(head.w-tail.w));
|
||||
vec2 h=head.xy/head.w*uViewport.xy*.5;
|
||||
vec2 t=mix(h,tail.xy/tail.w*uViewport.xy*.5,uShutter);
|
||||
vec2 delta=h-t;
|
||||
float lo=0.0,hi=1.0;
|
||||
vec2 bounds=uViewport.xy*.5+vec2(8.0);
|
||||
if(!clipAxis(t.x,delta.x,bounds.x,lo,hi)||!clipAxis(t.y,delta.y,bounds.y,lo,hi))return;
|
||||
h=t+delta*hi;t+=delta*lo;
|
||||
float distancePixels=length(h-t);
|
||||
if(distancePixels>uMaximumTrailPixels)t=h-(h-t)*(uMaximumTrailPixels/distancePixels);
|
||||
gLength=length(h-t);
|
||||
vec2 direction=gLength>.001?(h-t)/gLength:vec2(1,0);
|
||||
vec2 perpendicular=vec2(-direction.y,direction.x);
|
||||
// A minimum Gaussian footprint averages subpixel motion instead of toggling
|
||||
// a one-pixel point on and off as the camera passes it.
|
||||
gSigma=max(uMinimumWidthPixels/2.0,star[0].size*.3);
|
||||
float radius=gSigma*3.0;
|
||||
float moving=1.0-exp(-gLength/14.0);
|
||||
float boost=mix(1.0,uMovingBrightness,moving);
|
||||
gColor=star[0].color*.7*boost;
|
||||
vertex(t-direction*radius-perpendicular*radius,vec2(-radius,-radius));
|
||||
vertex(t-direction*radius+perpendicular*radius,vec2(-radius,radius));
|
||||
vertex(h+direction*radius-perpendicular*radius,vec2(gLength+radius,-radius));
|
||||
vertex(h+direction*radius+perpendicular*radius,vec2(gLength+radius,radius));
|
||||
EndPrimitive();
|
||||
}
|
||||
35
src/Universe.App/Graphics/Shaders/star-trail.vert.glsl
Normal file
35
src/Universe.App/Graphics/Shaders/star-trail.vert.glsl
Normal file
@@ -0,0 +1,35 @@
|
||||
#version 330 core
|
||||
layout(location=0) in vec3 aPosition;
|
||||
layout(location=1) in vec3 aColor;
|
||||
layout(location=2) in float aSize;
|
||||
uniform vec3 uOrigin;
|
||||
uniform vec3 uPreviousOrigin;
|
||||
uniform vec3 uAxisX;
|
||||
uniform vec3 uAxisY;
|
||||
uniform vec3 uAxisZ;
|
||||
uniform vec3 uForward;
|
||||
uniform vec3 uRight;
|
||||
uniform vec3 uUp;
|
||||
uniform vec3 uPreviousForward;
|
||||
uniform vec3 uPreviousRight;
|
||||
uniform vec3 uPreviousUp;
|
||||
uniform float uAspect;
|
||||
uniform float uTanHalfFov;
|
||||
uniform float uPreviousAspect;
|
||||
uniform float uPreviousTanHalfFov;
|
||||
uniform float uNearFade;
|
||||
uniform float uVisibility;
|
||||
out VS_OUT { vec4 previousClip; vec3 color; float size; } star;
|
||||
vec4 projectStar(vec3 p,vec3 forward,vec3 right,vec3 up,float aspect,float fov) {
|
||||
float z=dot(p,forward);
|
||||
return vec4(dot(p,right)/(aspect*fov),dot(p,up)/fov,z*.999999,z);
|
||||
}
|
||||
void main() {
|
||||
vec3 world=uAxisX*aPosition.x+uAxisY*aPosition.y+uAxisZ*aPosition.z;
|
||||
vec3 current=world-uOrigin;
|
||||
gl_Position=projectStar(current,uForward,uRight,uUp,uAspect,uTanHalfFov);
|
||||
star.previousClip=projectStar(world-uPreviousOrigin,uPreviousForward,uPreviousRight,uPreviousUp,uPreviousAspect,uPreviousTanHalfFov);
|
||||
float visibility=uNearFade>0.0?smoothstep(uNearFade,uNearFade*2.0,length(current)):1.0;
|
||||
star.color=aColor*visibility*uVisibility;
|
||||
star.size=aSize;
|
||||
}
|
||||
57
src/Universe.App/Graphics/StarTrailRenderer.cs
Normal file
57
src/Universe.App/Graphics/StarTrailRenderer.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using Silk.NET.OpenGL;
|
||||
using Universe.App.Runtime;
|
||||
using Universe.Core.Generation;
|
||||
using Universe.Core.Math;
|
||||
using Universe.Core.World;
|
||||
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
/// <summary>Draws only stellar sprites as swept Gaussian capsules. No scene-color accumulation.</summary>
|
||||
internal sealed class StarTrailRenderer : IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private ShaderProgram _program;
|
||||
public StarMotionBlurSettings Settings { get; }
|
||||
public StarMotionHistory History { get; } = new();
|
||||
public float Brightness { get; set; } = 1;
|
||||
private double _cellSize;
|
||||
|
||||
public StarTrailRenderer(GL gl, StarMotionBlurSettings settings)
|
||||
{
|
||||
settings.Validate(); _gl = gl; Settings = settings; _program = CreateProgram();
|
||||
}
|
||||
private ShaderProgram CreateProgram() => ShaderProgram.Create(_gl, ShaderSources.Load("star-trail.vert"),
|
||||
ShaderSources.Load("star-trail.frag"), ShaderSources.Load("star-trail.geom"));
|
||||
public void ReloadShaders()
|
||||
{
|
||||
var replacement = CreateProgram(); _program.Dispose(); _program = replacement;
|
||||
}
|
||||
public void BeginFrame(FlyCamera camera, int width, int height, double seconds)
|
||||
{
|
||||
_cellSize = camera.CellSizeMeters;
|
||||
History.Advance(camera, width, height, seconds, Settings);
|
||||
}
|
||||
public void Draw(GpuMesh mesh, UniversePosition center, double scale, PlanetOrientation orientation,
|
||||
double nearFade = 0, float visibility = 1, bool directional = false)
|
||||
{
|
||||
var current = History.Current; var previous = History.Previous;
|
||||
_program.Use();
|
||||
_program.Set("uOrigin", directional ? System.Numerics.Vector3.Zero : (current.Position.RelativeTo(center, _cellSize) / scale).ToVector3());
|
||||
_program.Set("uPreviousOrigin", directional ? System.Numerics.Vector3.Zero : (previous.Position.RelativeTo(center, _cellSize) / scale).ToVector3());
|
||||
_program.Set("uAxisX", orientation.ToWorld(new(1,0,0)).ToVector3());
|
||||
_program.Set("uAxisY", orientation.ToWorld(new(0,1,0)).ToVector3());
|
||||
_program.Set("uAxisZ", orientation.ToWorld(new(0,0,1)).ToVector3());
|
||||
_program.Set("uForward", current.Forward.ToVector3()); _program.Set("uRight", current.Right.ToVector3()); _program.Set("uUp", current.Up.ToVector3());
|
||||
_program.Set("uPreviousForward", previous.Forward.ToVector3()); _program.Set("uPreviousRight", previous.Right.ToVector3()); _program.Set("uPreviousUp", previous.Up.ToVector3());
|
||||
_program.Set("uAspect", (float)current.Width/current.Height); _program.Set("uTanHalfFov", (float)current.TanHalfFov);
|
||||
_program.Set("uPreviousAspect", (float)previous.Width/previous.Height); _program.Set("uPreviousTanHalfFov", (float)previous.TanHalfFov);
|
||||
_program.Set("uNearFade", (float)nearFade); _program.Set("uVisibility", visibility*Brightness);
|
||||
_program.Set("uViewport", new System.Numerics.Vector3(current.Width,current.Height,0));
|
||||
_program.Set("uShutter", History.Valid ? (float)Settings.ShutterFraction : 0);
|
||||
_program.Set("uMaximumTrailPixels", (float)Settings.MaximumTrailPixels);
|
||||
_program.Set("uMinimumWidthPixels", (float)Settings.MinimumWidthPixels);
|
||||
_program.Set("uMovingBrightness", (float)Settings.MovingBrightness);
|
||||
mesh.Draw(GLEnum.Points);
|
||||
}
|
||||
public void Dispose() => _program.Dispose();
|
||||
}
|
||||
@@ -27,6 +27,9 @@ internal sealed class UniverseRenderer : IDisposable
|
||||
private readonly PerformanceOverlay _performanceOverlay;
|
||||
private readonly BenchmarkOverlay _benchmarkOverlay;
|
||||
private readonly GpuPassProfiler _gpuProfiler;
|
||||
private readonly CosmicRenderer? _cosmos;
|
||||
private readonly StarTrailRenderer _starTrails;
|
||||
private readonly Dictionary<SectorCoordinate, StarSystemData> _residentSystems = [];
|
||||
private ShaderProgram _surface;
|
||||
private ShaderProgram _star;
|
||||
private ShaderProgram _sky;
|
||||
@@ -77,10 +80,11 @@ internal sealed class UniverseRenderer : IDisposable
|
||||
public bool DebugProbeEnabled { get; set; }
|
||||
public double CpuRenderMilliseconds { get; private set; }
|
||||
public float ExposureCompensation { get; set; } = 1f;
|
||||
public string HardwareName => _gl.GetStringS(GLEnum.Renderer);
|
||||
internal bool ProfileFrame { get; set; }
|
||||
internal Dictionary<string, double> FrameTimings { get; } = [];
|
||||
|
||||
public UniverseRenderer(GL gl, int width, int height)
|
||||
public UniverseRenderer(GL gl, int width, int height, GalaxyUniverse? galaxies = null, StarMotionBlurSettings? starMotionBlur = null)
|
||||
{
|
||||
_gl = gl;
|
||||
_noise = new(gl);
|
||||
@@ -96,6 +100,8 @@ internal sealed class UniverseRenderer : IDisposable
|
||||
_performanceOverlay = new(gl);
|
||||
_benchmarkOverlay = new(gl);
|
||||
_gpuProfiler = new(gl);
|
||||
_starTrails = new(gl, starMotionBlur ?? new());
|
||||
if (galaxies is not null) _cosmos = new(gl, galaxies, _starTrails);
|
||||
_surface = Program("terrain.vert", "terrain.frag");
|
||||
_star = Program("star.vert", "star.frag");
|
||||
_points = Program("point.vert", "point.frag");
|
||||
@@ -173,6 +179,7 @@ internal sealed class UniverseRenderer : IDisposable
|
||||
("fullscreen.vert","gas.frag"),("rock.vert","rock.frag"),("rock-point.vert","rock-point.frag"),("fullscreen.vert","sun-glow.frag"),("fullscreen.vert","ocean.frag"),
|
||||
("fullscreen.vert","cloud.frag"),("fullscreen.vert","cloud-composite.frag"),("fullscreen.vert","black-hole.frag") })
|
||||
replacements.Add(Program(pair.Item1, pair.Item2));
|
||||
_starTrails.ReloadShaders();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
@@ -196,19 +203,33 @@ internal sealed class UniverseRenderer : IDisposable
|
||||
{
|
||||
if (coordinate == _activeSystem) return;
|
||||
_activeSystem = coordinate;
|
||||
foreach (var mesh in _planetMeshes.Values) mesh.Dispose();
|
||||
_planetMeshes.Clear();
|
||||
foreach (var textures in _planetTextures.Values) textures.Dispose();
|
||||
foreach (var rings in _ringProfiles.Values) rings.Dispose();
|
||||
foreach (var rocks in _ringParticles.Values) rocks.Dispose();
|
||||
_planetTextures.Clear(); _ringProfiles.Clear(); _ringParticles.Clear();
|
||||
foreach (var texture in _cloudDensityTextures.Values) texture.Dispose();
|
||||
_cloudDensityTextures.Clear();
|
||||
_gpuProfiler.Clear();
|
||||
_probeFrameCounter = 0;
|
||||
_debugProbePlanetId = null;
|
||||
}
|
||||
|
||||
private void RetainSystem(StarSystemData system)
|
||||
{
|
||||
if (system.Stars.Count == 0) return;
|
||||
_residentSystems.Remove(system.Coordinate); _residentSystems.Add(system.Coordinate, system);
|
||||
if (_residentSystems.Count <= 3) return;
|
||||
var victim = _residentSystems.First(); _residentSystems.Remove(victim.Key);
|
||||
foreach (var planet in victim.Value.Planets)
|
||||
{
|
||||
if (_planetMeshes.Remove(planet.Id, out var mesh)) mesh.Dispose();
|
||||
if (_planetTextures.Remove(planet.Id, out var textures)) textures.Dispose();
|
||||
if (_ringProfiles.Remove(planet.Id, out var rings)) rings.Dispose();
|
||||
if (_ringParticles.Remove(planet.Id, out var rocks)) rocks.Dispose();
|
||||
if (_cloudDensityTextures.Remove(planet.Id, out var clouds)) clouds.Dispose();
|
||||
}
|
||||
}
|
||||
public void PreparePlanet(StarSystemData system, PlanetData planet)
|
||||
{
|
||||
RetainSystem(system);
|
||||
if (!planet.IsGasPlanet && !_planetMeshes.ContainsKey(planet.Id)) _planetMeshes[planet.Id] = new(_gl, planet);
|
||||
if (!_planetTextures.ContainsKey(planet.Id)) _planetTextures[planet.Id] = new(_gl, planet);
|
||||
}
|
||||
|
||||
public void Resize(int width, int height)
|
||||
{
|
||||
_width = System.Math.Max(width, 1);
|
||||
@@ -230,6 +251,7 @@ internal sealed class UniverseRenderer : IDisposable
|
||||
public void Render(FlyCamera camera, UniverseNeighborhood neighborhood, double deltaSeconds)
|
||||
{
|
||||
if (_disposed) return;
|
||||
_starTrails.BeginFrame(camera,_width,_height,deltaSeconds);
|
||||
var cpuTimer = DebugMode ? System.Diagnostics.Stopwatch.StartNew() : null;
|
||||
var timer = ProfileFrame ? System.Diagnostics.Stopwatch.StartNew() : null;
|
||||
if (timer is not null) { _gl.Finish(); timer.Restart(); FrameTimings.Clear(); }
|
||||
@@ -248,6 +270,7 @@ internal sealed class UniverseRenderer : IDisposable
|
||||
var projection = Matrix4x4.CreatePerspectiveFieldOfView((float)camera.FieldOfViewRadians, (float)_width / _height, 0.02f, far);
|
||||
var vp = view * projection;
|
||||
var system = neighborhood.ActiveSystem;
|
||||
RetainSystem(system);
|
||||
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
|
||||
var starPositions = system.Stars.Select(s => center.Add(s.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters)).ToArray();
|
||||
BindNoise();
|
||||
@@ -262,6 +285,7 @@ internal sealed class UniverseRenderer : IDisposable
|
||||
_sky.Use();
|
||||
SetCameraRay(_sky, camera);
|
||||
_sky.Set("uNoise", 4); _sky.Set("uTime", (float)_time);
|
||||
_sky.Set("uBackgroundStrength", _cosmos is null ? 1f : 0f);
|
||||
_sky.Set("uLightCount", system.Stars.Count);
|
||||
_sky.SetVectorArray("uStarDirections", starPositions.Select(p => p.Normalized().ToVector3()).ToArray());
|
||||
_sky.SetVectorArray("uStarColors", system.Stars.Select(s => s.IsBlackHole ? Vector3.Zero : s.Color).ToArray());
|
||||
@@ -272,6 +296,10 @@ internal sealed class UniverseRenderer : IDisposable
|
||||
DrawStarfield(camera, neighborhood, vp, far);
|
||||
_gpuProfiler.EndPass();
|
||||
Mark("sky");
|
||||
_gpuProfiler.BeginPass("GALAXIES / NEBULAE");
|
||||
_cosmos?.Draw(camera, _width, _height, deltaSeconds);
|
||||
_gpuProfiler.EndPass();
|
||||
Mark("galaxies and nebulae");
|
||||
_gl.DepthMask(true);
|
||||
_gl.Disable(GLEnum.Blend);
|
||||
_gpuProfiler.BeginPass("PLANETS TOTAL");
|
||||
@@ -344,7 +372,7 @@ internal sealed class UniverseRenderer : IDisposable
|
||||
_gl.DepthMask(true); _gl.Enable(GLEnum.DepthTest);
|
||||
}
|
||||
|
||||
public void DrawBenchmarkOverlay(BenchmarkRunner benchmark)
|
||||
public void DrawBenchmarkOverlay(IBenchmarkSession benchmark)
|
||||
{
|
||||
_gl.Disable(GLEnum.DepthTest); _gl.Disable(GLEnum.Blend); _gl.DepthMask(false);
|
||||
_overlay.Use();
|
||||
@@ -381,9 +409,9 @@ internal sealed class UniverseRenderer : IDisposable
|
||||
{
|
||||
_gl.Enable(GLEnum.Blend);
|
||||
_gl.BlendFunc(GLEnum.One, GLEnum.One);
|
||||
_points.Use(); _points.SetMatrix("uViewProjection", vp); _points.Set("uFar", far); _points.Set("uScale", far * 0.55f);
|
||||
_points.Set("uBrightness", StarVisibility(camera, neighborhood.ActiveSystem));
|
||||
_backgroundStars.Draw(GLEnum.Points);
|
||||
_starTrails.Brightness = StarVisibility(camera, neighborhood.ActiveSystem);
|
||||
if (_cosmos is null) _starTrails.Draw(_backgroundStars,camera.Position,1,PlanetOrientation.Identity,directional:true);
|
||||
var scale = _cosmos is null ? (double)far : UniverseScale.LightYearMeters;
|
||||
var points = new List<float>(26 * 14);
|
||||
foreach (var system in neighborhood.NearbySystems)
|
||||
{
|
||||
@@ -392,12 +420,11 @@ internal sealed class UniverseRenderer : IDisposable
|
||||
{
|
||||
if (star.IsBlackHole) continue;
|
||||
var rel = origin.Add(star.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters);
|
||||
AddPoint(points, rel, star.Color * 1.3f, 3.2f);
|
||||
AddPoint(points, rel/scale, star.Color * 1.3f, 3.2f);
|
||||
}
|
||||
}
|
||||
_navigationStars.ReplaceVertices(points.ToArray());
|
||||
_points.Set("uScale", 1f);
|
||||
_navigationStars.Draw(GLEnum.Points);
|
||||
_starTrails.Draw(_navigationStars,camera.Position,scale,PlanetOrientation.Identity);
|
||||
_gl.Disable(GLEnum.Blend);
|
||||
}
|
||||
|
||||
@@ -421,6 +448,7 @@ internal sealed class UniverseRenderer : IDisposable
|
||||
if (planet.IsGasPlanet) continue;
|
||||
var relative = relativePlanets[i];
|
||||
if (DVector3.Dot(relative, camera.Forward) < -planet.MaximumTerrainRadiusMeters) continue;
|
||||
if (planet.RadiusMeters / System.Math.Max(1,relative.Length) * _height / (2 * System.Math.Tan(camera.FieldOfViewRadians*.5)) < .5) continue;
|
||||
if (!_planetMeshes.TryGetValue(planet.Id, out var mesh))
|
||||
{
|
||||
mesh = new(_gl, planet); _planetMeshes.Add(planet.Id, mesh);
|
||||
@@ -584,7 +612,8 @@ internal sealed class UniverseRenderer : IDisposable
|
||||
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
|
||||
var candidates = system.Planets.Where(p => p.IsGasPlanet).Select(p => (Planet: p,
|
||||
Relative: center.Add(p.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters)))
|
||||
.Where(p => DVector3.Dot(p.Relative, camera.Forward) > -p.Planet.RadiusMeters * 1.3)
|
||||
.Where(p => DVector3.Dot(p.Relative, camera.Forward) > -p.Planet.RadiusMeters * 1.3
|
||||
&& p.Planet.RadiusMeters / System.Math.Max(1,p.Relative.Length) * _height / (2 * System.Math.Tan(camera.FieldOfViewRadians*.5)) >= .5)
|
||||
.OrderByDescending(p => p.Relative.Length);
|
||||
foreach (var (planet, relative) in candidates)
|
||||
{
|
||||
@@ -1078,6 +1107,8 @@ internal sealed class UniverseRenderer : IDisposable
|
||||
_planetTextures.Clear(); _cloudDensityTextures.Clear(); _ringProfiles.Clear(); _ringParticles.Clear();
|
||||
_sphere.Dispose(); _fullscreen.Dispose(); _backgroundStars.Dispose(); _navigationStars.Dispose(); _crosshair.Dispose(); _pauseMenu.Dispose();
|
||||
_performanceOverlay.Dispose(); _gpuProfiler.Dispose();
|
||||
_cosmos?.Dispose();
|
||||
_starTrails.Dispose();
|
||||
_surface.Dispose(); _star.Dispose(); _sky.Dispose(); _points.Dispose(); _atmosphere.Dispose(); _bright.Dispose(); _blur.Dispose(); _final.Dispose(); _overlay.Dispose(); _ring.Dispose();
|
||||
_benchmarkOverlay.Dispose();
|
||||
_gas.Dispose(); _rock.Dispose(); _rockPoints.Dispose(); _emptyRing.Dispose(); _gasInput.Dispose();
|
||||
|
||||
2
src/Universe.App/Properties/AssemblyInfo.cs
Normal file
2
src/Universe.App/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,2 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
[assembly: InternalsVisibleTo("Universe.Tests")]
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
188
src/Universe.App/Runtime/GalaxyJourneyBenchmark.cs
Normal file
188
src/Universe.App/Runtime/GalaxyJourneyBenchmark.cs
Normal 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);
|
||||
}
|
||||
24
src/Universe.App/Runtime/IBenchmarkSession.cs
Normal file
24
src/Universe.App/Runtime/IBenchmarkSession.cs
Normal 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);
|
||||
}
|
||||
29
src/Universe.App/Runtime/StarMotionHistory.cs
Normal file
29
src/Universe.App/Runtime/StarMotionHistory.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
<ProjectReference Include="..\Universe.Core\Universe.Core.csproj" />
|
||||
<EmbeddedResource Include="Graphics\Shaders\*.glsl" />
|
||||
<Content Include="..\..\config.json" Link="config.json" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content Include="..\..\KnownGalaxies.json" Link="KnownGalaxies.json" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content Include="..\..\KnownSolarSystem\**\*" Link="KnownSolarSystem\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content Include="..\..\Content\**\*.json;..\..\Content\**\*.jpg;..\..\Content\**\*.png;..\..\Content\**\*.f32" Link="Content\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -18,6 +18,8 @@ public sealed class UniverseApplication : IDisposable
|
||||
private readonly IWindow _window;
|
||||
private readonly UniverseGenerator _generator;
|
||||
private readonly UniverseStreamer _streamer;
|
||||
private readonly GalaxyUniverse _galaxies;
|
||||
private readonly StarMotionBlurSettings _starMotionBlur;
|
||||
private readonly IReadOnlyList<string> _planetHotkeys;
|
||||
private readonly StarSystemData _homeSystem;
|
||||
private readonly PlanetData _homeEarth;
|
||||
@@ -26,7 +28,8 @@ public sealed class UniverseApplication : IDisposable
|
||||
private readonly DVector3 _homeCameraUp;
|
||||
private FlyCamera _camera;
|
||||
private PlayerController? _player;
|
||||
private BenchmarkRunner? _benchmark;
|
||||
private IBenchmarkSession? _benchmark;
|
||||
private bool _benchmarkNeedsInitialFrame;
|
||||
private bool _menuOpen;
|
||||
private bool _debugMode;
|
||||
private int _menuSelection;
|
||||
@@ -49,19 +52,23 @@ public sealed class UniverseApplication : IDisposable
|
||||
{
|
||||
var configPath = Path.Combine(AppContext.BaseDirectory, "config.json");
|
||||
var configuration = EngineConfiguration.Load(configPath);
|
||||
_starMotionBlur = configuration.StarMotionBlur;
|
||||
_planetHotkeys = configuration.Startup.PlanetHotkeys;
|
||||
var catalogPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(configPath)!, configuration.KnownSolarSystemsFile));
|
||||
var catalog = KnownSolarSystemCatalog.Load(catalogPath, configuration.CellSizeMeters);
|
||||
_generator = new(configuration.Seed, configuration.CellSizeMeters, catalog, configuration.BlackHoles);
|
||||
_galaxies = new(configuration.Seed, configuration.CellSizeMeters, configuration.Galaxies,
|
||||
Path.GetFullPath(Path.Combine(Path.GetDirectoryName(configPath)!, configuration.Galaxies.KnownGalaxiesFile)));
|
||||
_generator = new(configuration.Seed, configuration.CellSizeMeters, catalog, configuration.BlackHoles, _galaxies);
|
||||
_streamer = new(_generator);
|
||||
var initialSystem = configuration.ResolveStartup(catalog).System;
|
||||
_homeSystem = catalog.GetById("sol");
|
||||
_homeSystem = _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;
|
||||
var cellCenter = UniversePosition.AtCellCenter(startCoordinate, _generator.CellSizeMeters);
|
||||
var startPosition = cellCenter.Add(new DVector3(0, 0, 4 * UniverseScale.AstronomicalUnitMeters), _generator.CellSizeMeters);
|
||||
_camera = new(startPosition, _generator.CellSizeMeters);
|
||||
_camera.TravelLimitMeters = _galaxies.Settings.RegionSizeLightYears * UniverseScale.LightYearMeters * .25;
|
||||
_neighborhood = _streamer.Load(startCoordinate);
|
||||
_camera.SetInitialView(_neighborhood.ActiveSystem, configuration.Startup);
|
||||
_homeCameraPosition = _camera.Position;
|
||||
@@ -72,7 +79,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"),
|
||||
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"),
|
||||
Size = new Vector2D<int>(1440, 900),
|
||||
VSync = true,
|
||||
PreferredDepthBufferBits = 24,
|
||||
@@ -92,7 +99,18 @@ public sealed class UniverseApplication : IDisposable
|
||||
{
|
||||
var gl = _window.CreateOpenGL();
|
||||
_input = _window.CreateInput();
|
||||
_renderer = new(gl, _window.FramebufferSize.X, _window.FramebufferSize.Y);
|
||||
_renderer = new(gl, _window.FramebufferSize.X, _window.FramebufferSize.Y, _galaxies, _starMotionBlur);
|
||||
if (Environment.GetCommandLineArgs().Contains("--star-trails-report"))
|
||||
{
|
||||
GraphicsReport.RunStarTrails(gl,_starMotionBlur,_window.FramebufferSize.X,_window.FramebufferSize.Y);
|
||||
_window.Close(); return;
|
||||
}
|
||||
if (Environment.GetCommandLineArgs().Contains("--galaxy-journey-report"))
|
||||
{
|
||||
var journey = CreateGalaxyJourney();
|
||||
GraphicsReport.RunGalaxyJourney(gl, _renderer, _camera, _streamer, journey, _window.FramebufferSize.X, _window.FramebufferSize.Y);
|
||||
_window.Close(); return;
|
||||
}
|
||||
if (Environment.GetCommandLineArgs().Contains("--black-hole-report"))
|
||||
{
|
||||
var target = _streamer.FindNearestBlackHole(_camera.Position)
|
||||
@@ -138,6 +156,7 @@ public sealed class UniverseApplication : IDisposable
|
||||
|
||||
if (_benchmark?.IsRunning == true)
|
||||
{
|
||||
if (_benchmarkNeedsInitialFrame) return;
|
||||
_benchmark.Update(deltaSeconds, _camera);
|
||||
UpdateNeighborhood();
|
||||
return;
|
||||
@@ -190,7 +209,10 @@ public sealed class UniverseApplication : IDisposable
|
||||
private void OnRender(double deltaSeconds)
|
||||
{
|
||||
_frameTime += (System.Math.Clamp(deltaSeconds, 0.001, 1) - _frameTime) * 0.08;
|
||||
_benchmark?.RecordFrame(deltaSeconds);
|
||||
// The first interval still includes menu preparation and GPU preloading.
|
||||
// Render one initial frame before starting the measurement clock.
|
||||
if (!_benchmarkNeedsInitialFrame) _benchmark?.RecordFrame(deltaSeconds);
|
||||
if (_benchmark is GalaxyJourneyBenchmark { IsComplete: true, ReportDirectory: null } journey) journey.SaveReport();
|
||||
if (_renderer is not null)
|
||||
{
|
||||
_renderer.DebugMode = _debugMode;
|
||||
@@ -200,6 +222,7 @@ public sealed class UniverseApplication : IDisposable
|
||||
_renderer?.DrawDebugOverlay(1 / _frameTime);
|
||||
if (_benchmark is { HasRun: true } benchmark) _renderer?.DrawBenchmarkOverlay(benchmark);
|
||||
if (_menuOpen) _renderer?.DrawMenu(_menuSelection, _debugMode);
|
||||
_benchmarkNeedsInitialFrame = false;
|
||||
}
|
||||
|
||||
private void UpdateNeighborhood()
|
||||
@@ -229,8 +252,8 @@ public sealed class UniverseApplication : IDisposable
|
||||
if (_menuOpen)
|
||||
{
|
||||
if (key == Key.Escape) SetMenu(false);
|
||||
else if (key == Key.Up) _menuSelection = (_menuSelection + 5) % 6;
|
||||
else if (key == Key.Down) _menuSelection = (_menuSelection + 1) % 6;
|
||||
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();
|
||||
return;
|
||||
}
|
||||
@@ -273,6 +296,16 @@ public sealed class UniverseApplication : IDisposable
|
||||
_camera.RequestOceanView(_neighborhood.NearbySystems.Append(_neighborhood.ActiveSystem));
|
||||
else if (key == Key.F7)
|
||||
_camera.RequestCloudView(_neighborhood.NearbySystems.Append(_neighborhood.ActiveSystem));
|
||||
else if(key==Key.F8)
|
||||
{
|
||||
var galaxy=_galaxies.GalaxyAt(_camera.Position);
|
||||
_camera.RequestCosmicView(galaxy.Center,galaxy.RadiusMeters,galaxy.Orientation,false,_neighborhood.NearbySystems.Append(_neighborhood.ActiveSystem));
|
||||
}
|
||||
else if(key==Key.F9)
|
||||
{
|
||||
var nebula=_galaxies.GalaxyAt(_camera.Position).Nebulae.MinBy(n=>n.Center.RelativeTo(_camera.Position,_generator.CellSizeMeters).LengthSquared)!;
|
||||
_camera.RequestCosmicView(nebula.Center,nebula.RadiusMeters,nebula.Orientation,true,_neighborhood.NearbySystems.Append(_neighborhood.ActiveSystem));
|
||||
}
|
||||
else if (key == Key.F1 && _mouse is not null)
|
||||
{
|
||||
_mouseCaptured = !_mouseCaptured;
|
||||
@@ -350,8 +383,16 @@ public sealed class UniverseApplication : IDisposable
|
||||
|
||||
private void ActivateMenu()
|
||||
{
|
||||
if (_menuSelection == 5) { _window.Close(); return; }
|
||||
if (_menuSelection == 6) { _window.Close(); return; }
|
||||
if (_menuSelection == 2) { _debugMode = !_debugMode; return; }
|
||||
if (_menuSelection == 5)
|
||||
{
|
||||
_debugMode = false; _player = null;
|
||||
_benchmark = CreateGalaxyJourney();
|
||||
_benchmarkNeedsInitialFrame = true;
|
||||
_camera.SetPlayerPose(_homeCameraPosition, _homeCameraForward, _homeCameraUp, resetMotionHistory: true);
|
||||
UpdateNeighborhood(); SetMenu(false); return;
|
||||
}
|
||||
if (_menuSelection is 3 or 4)
|
||||
{
|
||||
try
|
||||
@@ -375,6 +416,7 @@ public sealed class UniverseApplication : IDisposable
|
||||
|
||||
private void StartBenchmark(bool unknownPlanet)
|
||||
{
|
||||
_camera.RestoreDefaultFieldOfView();
|
||||
var system = _homeSystem;
|
||||
var planet = _homeEarth;
|
||||
if (unknownPlanet)
|
||||
@@ -398,12 +440,24 @@ public sealed class UniverseApplication : IDisposable
|
||||
|
||||
_debugMode = false;
|
||||
_player = null;
|
||||
_benchmark = new(system, planet, _generator.CellSizeMeters, _camera,
|
||||
_benchmark = new BenchmarkRunner(system, planet, _generator.CellSizeMeters, _camera,
|
||||
_homeCameraPosition, _homeCameraForward, _homeCameraUp);
|
||||
_benchmarkNeedsInitialFrame = true;
|
||||
UpdateNeighborhood();
|
||||
SetMenu(false);
|
||||
}
|
||||
|
||||
private GalaxyJourneyBenchmark CreateGalaxyJourney()
|
||||
{
|
||||
_camera.RestoreDefaultFieldOfView();
|
||||
var journey = new GalaxyJourneyBenchmark(_galaxies, _generator, _homeSystem, _homeEarth,
|
||||
_homeCameraPosition, _homeCameraForward, _homeCameraUp);
|
||||
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 void OnClosing()
|
||||
{
|
||||
_renderer?.Dispose();
|
||||
|
||||
@@ -24,6 +24,8 @@ public sealed record EngineConfiguration
|
||||
public string KnownSolarSystemsFile { get; init; } = "KnownSolarSystem/solar-systems.json";
|
||||
public StartupConfiguration Startup { get; init; } = new();
|
||||
public BlackHoleGenerationSettings BlackHoles { get; init; } = new();
|
||||
public GalaxySettings Galaxies { get; init; } = new();
|
||||
public StarMotionBlurSettings StarMotionBlur { 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)
|
||||
@@ -44,6 +46,8 @@ public sealed record EngineConfiguration
|
||||
{
|
||||
var settings = JsonSettings.Read<EngineConfiguration>(path);
|
||||
(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();
|
||||
if (string.IsNullOrWhiteSpace(settings.UniverseSeed)) throw new InvalidDataException("config.json: universeSeed cannot be empty.");
|
||||
try { _ = settings.Seed; }
|
||||
catch (Exception error) when (error is FormatException or OverflowException or ArgumentNullException)
|
||||
@@ -57,7 +61,7 @@ public sealed record EngineConfiguration
|
||||
|| !double.IsFinite(startup.DistanceInRadii) || startup.DistanceInRadii < 1.05
|
||||
|| !double.IsFinite(startup.LongitudeDegrees) || !double.IsFinite(startup.LatitudeDegrees) || System.Math.Abs(startup.LatitudeDegrees) > 90
|
||||
|| !double.IsFinite(startup.SpeedMetersPerSecond) || startup.SpeedMetersPerSecond < 0.1
|
||||
|| startup.SpeedMetersPerSecond > UniverseScale.LightSpeedMetersPerSecond * 1_000_000
|
||||
|| startup.SpeedMetersPerSecond > UniverseScale.LightSpeedMetersPerSecond * 1e15
|
||||
|| !double.IsFinite(startup.FieldOfViewDegrees) || startup.FieldOfViewDegrees is < 30 or > 120)
|
||||
throw new InvalidDataException("config.json: invalid startup target, planet hotkeys, view angles, distance or flight speed. Planet hotkeys must contain at most nine non-empty planet ids.");
|
||||
if (string.IsNullOrWhiteSpace(settings.KnownSolarSystemsFile)) throw new InvalidDataException("config.json: knownSolarSystemsFile is required.");
|
||||
|
||||
@@ -65,4 +65,6 @@ public sealed record StarSystemData(
|
||||
{
|
||||
public string Id { get; init; } = $"procedural:{Coordinate.X},{Coordinate.Y},{Coordinate.Z}";
|
||||
public string Name { get; init; } = "Proceduriellt solsystem";
|
||||
public string? GalaxyId { get; init; }
|
||||
public string? NebulaId { get; init; }
|
||||
}
|
||||
|
||||
225
src/Universe.Core/Generation/GalaxyUniverse.cs
Normal file
225
src/Universe.Core/Generation/GalaxyUniverse.cs
Normal file
@@ -0,0 +1,225 @@
|
||||
using System.Numerics;
|
||||
using Universe.Core.Configuration;
|
||||
using Universe.Core.Math;
|
||||
using Universe.Core.World;
|
||||
|
||||
namespace Universe.Core.Generation;
|
||||
|
||||
public sealed record GalaxySettings
|
||||
{
|
||||
public string KnownGalaxiesFile { get; init; } = "KnownGalaxies.json";
|
||||
public double RegionSizeLightYears { get; init; } = 1_000_000;
|
||||
public double SystemSpacingLightYears { get; init; } = 4;
|
||||
public int GalaxyStarBudget { get; init; } = 6000;
|
||||
public int LocalStarBudget { get; init; } = 2600;
|
||||
public int VolumeSteps { get; init; } = 40;
|
||||
public double MinimumBenchmarkFps { get; init; } = 45;
|
||||
public double MaximumBenchmarkFrameMilliseconds { get; init; } = 50;
|
||||
public void Validate()
|
||||
{
|
||||
if (!double.IsFinite(RegionSizeLightYears) || RegionSizeLightYears is < 300_000 or > 100_000_000
|
||||
|| !double.IsFinite(SystemSpacingLightYears) || SystemSpacingLightYears is < .01 or > 100
|
||||
|| GalaxyStarBudget is < 256 or > 30000 || LocalStarBudget is < 128 or > 10000
|
||||
|| VolumeSteps is < 16 or > 96 || !double.IsFinite(MinimumBenchmarkFps) || MinimumBenchmarkFps is < 1 or > 1000
|
||||
|| !double.IsFinite(MaximumBenchmarkFrameMilliseconds) || MaximumBenchmarkFrameMilliseconds is < 1 or > 1000)
|
||||
throw new InvalidDataException("Invalid galaxies settings in config.json.");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record NebulaDefinition
|
||||
{
|
||||
public string Id { get; init; } = "nebula";
|
||||
public double[] PositionLightYears { get; init; } = [26000, 0, 300];
|
||||
public double RadiusLightYears { get; init; } = 80;
|
||||
public string? Texture { get; init; }
|
||||
}
|
||||
public sealed record GalaxyDefinition
|
||||
{
|
||||
public string Id { get; init; } = "milky-way";
|
||||
public string Name { get; init; } = "Vintergatan";
|
||||
public long[] Region { get; init; } = [0, 0, 0];
|
||||
public double[] CenterOffsetLightYears { get; init; } = [-26000, 0, 0];
|
||||
public double RadiusLightYears { get; init; } = 50000;
|
||||
public double ThicknessLightYears { get; init; } = 1800;
|
||||
public double AxialTiltDegrees { get; init; }
|
||||
public double RotationDegrees { get; init; }
|
||||
public int SpiralArms { get; init; } = 4;
|
||||
public string? Texture { get; init; }
|
||||
public double DensityGamma { get; init; } = .7;
|
||||
public NebulaDefinition[] Nebulae { get; init; } = [];
|
||||
}
|
||||
public sealed record KnownGalaxyDefinitions
|
||||
{
|
||||
public GalaxyDefinition[] Galaxies { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed record NebulaData(string Id, UniversePosition Center, double RadiusMeters, PlanetOrientation Orientation, ulong Seed, RasterImage? Texture);
|
||||
|
||||
public sealed record GalaxyData(string Id, string Name, GalaxyCoordinate Region, UniversePosition Center, double RadiusMeters,
|
||||
double ThicknessMeters, PlanetOrientation Orientation, int SpiralArms, ulong Seed, double DensityGamma, RasterImage? Texture,
|
||||
IReadOnlyList<NebulaData> Nebulae)
|
||||
{
|
||||
public double PlanarDensity(double x, double z)
|
||||
{
|
||||
var r = System.Math.Sqrt(x * x + z * z);
|
||||
if (r >= 1) return 0;
|
||||
double density;
|
||||
if (Texture is { } map)
|
||||
{
|
||||
var u = System.Math.Clamp((x * .5 + .5) * (map.Width - 1), 0, map.Width - 1);
|
||||
var v = System.Math.Clamp((.5 - z * .5) * (map.Height - 1), 0, map.Height - 1);
|
||||
var ix = (int)u; var iy = (int)v;
|
||||
double At(int px, int py)
|
||||
{
|
||||
var p = (py * map.Width + px) * 4;
|
||||
return (map.Rgba[p] * .2126 + map.Rgba[p + 1] * .7152 + map.Rgba[p + 2] * .0722) / 255 * map.Rgba[p + 3] / 255;
|
||||
}
|
||||
var tx = u - ix; var ty = v - iy;
|
||||
var a = At(ix, iy) * (1 - tx) + At(System.Math.Min(ix + 1, map.Width - 1), iy) * tx;
|
||||
var b = At(ix, System.Math.Min(iy + 1, map.Height - 1)) * (1 - tx)
|
||||
+ At(System.Math.Min(ix + 1, map.Width - 1), System.Math.Min(iy + 1, map.Height - 1)) * tx;
|
||||
density = a * (1 - ty) + b * ty;
|
||||
}
|
||||
else
|
||||
{
|
||||
var spiral = System.Math.Atan2(z, x) * SpiralArms - System.Math.Log(r + .05) * 5.8;
|
||||
var arms = System.Math.Pow(.5 + .5 * System.Math.Cos(spiral), 5);
|
||||
density = (.05 + arms * .8) * System.Math.Pow(1 - r, .7) + System.Math.Exp(-r * r * 65) * .8;
|
||||
}
|
||||
return System.Math.Pow(System.Math.Clamp(density, 0, 1), DensityGamma);
|
||||
}
|
||||
|
||||
public double Density(UniversePosition position, double cellSize)
|
||||
{
|
||||
var p = Orientation.ToBody(position.RelativeTo(Center, cellSize));
|
||||
var vertical = System.Math.Exp(-System.Math.Abs(p.Y) / ThicknessMeters);
|
||||
return vertical < .001 ? 0 : PlanarDensity(p.X / RadiusMeters, p.Z / RadiusMeters) * vertical;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Procedural macrocells and sparse deterministic stellar candidates; textures control real occupancy.</summary>
|
||||
public sealed class GalaxyUniverse
|
||||
{
|
||||
private readonly Dictionary<GalaxyCoordinate, GalaxyData> _cache = [];
|
||||
private readonly Dictionary<GalaxyCoordinate, GalaxyDefinition> _known;
|
||||
private readonly string _root;
|
||||
public ulong Seed { get; }
|
||||
public double CellSize { get; }
|
||||
public GalaxySettings Settings { get; }
|
||||
public long RegionCells { get; }
|
||||
public long SpacingCells { get; }
|
||||
|
||||
public GalaxyUniverse(ulong seed, double cellSize, GalaxySettings settings, string catalogPath)
|
||||
{
|
||||
settings.Validate(); Seed = seed; CellSize = cellSize; Settings = settings;
|
||||
RegionCells = checked((long)System.Math.Round(settings.RegionSizeLightYears * UniverseScale.LightYearMeters / cellSize));
|
||||
SpacingCells = System.Math.Max(2, checked((long)System.Math.Round(settings.SystemSpacingLightYears * UniverseScale.LightYearMeters / cellSize)));
|
||||
_root = Path.GetDirectoryName(Path.GetFullPath(catalogPath))!;
|
||||
_known = new();
|
||||
foreach (var definition in JsonSettings.Read<KnownGalaxyDefinitions>(catalogPath).Galaxies)
|
||||
{
|
||||
if (definition.Region.Length != 3 || definition.CenterOffsetLightYears.Length != 3 || string.IsNullOrWhiteSpace(definition.Id)
|
||||
|| !double.IsFinite(definition.RadiusLightYears) || definition.RadiusLightYears <= 0 || definition.RadiusLightYears > settings.RegionSizeLightYears * .2
|
||||
|| !double.IsFinite(definition.ThicknessLightYears) || definition.ThicknessLightYears <= 0
|
||||
|| !double.IsFinite(definition.DensityGamma) || definition.DensityGamma is < .1 or > 8
|
||||
|| definition.SpiralArms is < 1 or > 8 || definition.CenterOffsetLightYears.Any(v => !double.IsFinite(v)))
|
||||
throw new InvalidDataException("Invalid galaxy in KnownGalaxies.json.");
|
||||
_known.Add(new(definition.Region[0], definition.Region[1], definition.Region[2]), definition);
|
||||
}
|
||||
}
|
||||
|
||||
public GalaxyCoordinate RegionAt(UniversePosition position) => GalaxyAddress.FromPosition(position, RegionCells).Region;
|
||||
public GalaxyData GalaxyAt(UniversePosition position) => Get(RegionAt(position));
|
||||
|
||||
public GalaxyData Get(GalaxyCoordinate coordinate)
|
||||
{
|
||||
if (_cache.TryGetValue(coordinate, out var cached)) return cached;
|
||||
var seed = Hash(new(coordinate.X, coordinate.Y, coordinate.Z), Seed ^ 0x47414C415859UL);
|
||||
var random = new DeterministicRandom(seed);
|
||||
_known.TryGetValue(coordinate, out var known);
|
||||
var regionOrigin = new GalaxyAddress(coordinate, new(0, 0, 0), DVector3.Zero).ToPosition(RegionCells);
|
||||
var offset = known is null ? new DVector3(random.NextDouble() - .5, random.NextDouble() - .5, random.NextDouble() - .5) * (Settings.RegionSizeLightYears * .15)
|
||||
: new(known.CenterOffsetLightYears[0], known.CenterOffsetLightYears[1], known.CenterOffsetLightYears[2]);
|
||||
var center = regionOrigin.Add(offset * UniverseScale.LightYearMeters, CellSize);
|
||||
var radius = (known?.RadiusLightYears ?? (25000 + random.NextDouble() * 45000)) * UniverseScale.LightYearMeters;
|
||||
var thickness = (known?.ThicknessLightYears ?? (800 + random.NextDouble() * 1800)) * UniverseScale.LightYearMeters;
|
||||
var orientation = new PlanetOrientation(known?.AxialTiltDegrees ?? random.NextDouble() * 130 - 65, known?.RotationDegrees ?? random.NextDouble() * 360);
|
||||
var id = known?.Id ?? $"galaxy:{coordinate.X}:{coordinate.Y}:{coordinate.Z}";
|
||||
var nebulae = new List<NebulaData>();
|
||||
foreach (var n in known?.Nebulae ?? [])
|
||||
{
|
||||
if (n.PositionLightYears.Length != 3 || n.PositionLightYears.Any(v => !double.IsFinite(v)) || !double.IsFinite(n.RadiusLightYears) || n.RadiusLightYears <= 0)
|
||||
throw new InvalidDataException("Invalid nebula in KnownGalaxies.json.");
|
||||
var local = new DVector3(n.PositionLightYears[0], n.PositionLightYears[1], n.PositionLightYears[2]) * UniverseScale.LightYearMeters;
|
||||
nebulae.Add(new(n.Id, center.Add(orientation.ToWorld(local), CellSize), n.RadiusLightYears * UniverseScale.LightYearMeters,
|
||||
orientation, seed ^ (ulong)nebulae.Count, Load(n.Texture)));
|
||||
}
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
var r = .18 + random.NextDouble() * .6; var phi = random.NextDouble() * System.Math.Tau;
|
||||
var local = new DVector3(System.Math.Cos(phi) * r * radius, (random.NextDouble() - .5) * thickness, System.Math.Sin(phi) * r * radius);
|
||||
nebulae.Add(new($"{id}:nebula:{i}", center.Add(orientation.ToWorld(local), CellSize), (150 + random.NextDouble() * 700) * UniverseScale.LightYearMeters,
|
||||
new(random.NextDouble() * 180, random.NextDouble() * 360), random.NextUInt64(), null));
|
||||
}
|
||||
var galaxy = new GalaxyData(id, known?.Name ?? $"Galax {coordinate.X}:{coordinate.Y}:{coordinate.Z}", coordinate, center, radius, thickness, orientation,
|
||||
known?.SpiralArms ?? random.NextInt(2, 6), seed, known?.DensityGamma ?? .7, Load(known?.Texture), nebulae);
|
||||
if (_cache.Count >= 96)
|
||||
{
|
||||
var victim = _cache.Keys.FirstOrDefault(k => !_known.ContainsKey(k));
|
||||
_cache.Remove(victim);
|
||||
}
|
||||
_cache[coordinate] = galaxy;
|
||||
return galaxy;
|
||||
}
|
||||
|
||||
private RasterImage? Load(string? path) => path is null ? null : RasterImage.Load(Path.GetFullPath(Path.Combine(_root, path)));
|
||||
public IReadOnlyList<GalaxyData> VisibleGalaxies(UniversePosition position)
|
||||
{
|
||||
var r = RegionAt(position); var result = new List<GalaxyData>(27);
|
||||
for (var z = -1; z <= 1; z++) for (var y = -1; y <= 1; y++) for (var x = -1; x <= 1; x++)
|
||||
{
|
||||
try { result.Add(Get(new(checked(r.X + x), checked(r.Y + y), checked(r.Z + z)))); }
|
||||
catch (OverflowException) { }
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static long FloorDiv(long a, long b) { var q = a / b; return a < 0 && a % b != 0 ? q - 1 : q; }
|
||||
public SectorCoordinate StellarBlock(SectorCoordinate sector) => new(FloorDiv(sector.X, SpacingCells), FloorDiv(sector.Y, SpacingCells), FloorDiv(sector.Z, SpacingCells));
|
||||
public bool Candidate(GalaxyData galaxy, SectorCoordinate block, out SectorCoordinate sector)
|
||||
{
|
||||
var random = new DeterministicRandom(Hash(block, Seed ^ 0x5354415253UL));
|
||||
long Offset() => (long)((.12 + random.NextDouble() * .76) * SpacingCells);
|
||||
sector = new(checked(block.X * SpacingCells + Offset()), checked(block.Y * SpacingCells + Offset()), checked(block.Z * SpacingCells + Offset()));
|
||||
return random.NextDouble() < galaxy.Density(UniversePosition.AtCellCenter(sector, CellSize), CellSize);
|
||||
}
|
||||
public bool HasSystem(SectorCoordinate sector)
|
||||
{
|
||||
var position = UniversePosition.AtCellCenter(sector, CellSize);
|
||||
return Candidate(GalaxyAt(position), StellarBlock(sector), out var candidate) && candidate == sector;
|
||||
}
|
||||
public IReadOnlyList<SectorCoordinate> NearbySystemCells(SectorCoordinate sector, int radius = 3, int maximum = 16)
|
||||
{
|
||||
var center = UniversePosition.AtCellCenter(sector, CellSize); var galaxy = GalaxyAt(center); var block = StellarBlock(sector);
|
||||
var found = new List<SectorCoordinate>();
|
||||
for (var z = -radius; z <= radius; z++) for (var y = -radius; y <= radius; y++) for (var x = -radius; x <= radius; x++)
|
||||
if (Candidate(galaxy, new(block.X + x, block.Y + y, block.Z + z), out var c)) found.Add(c);
|
||||
// Sol is pinned and never rejected by the density map.
|
||||
if (galaxy.Id == "milky-way" && center.RelativeTo(UniversePosition.AtCellCenter(new(), CellSize), CellSize).Length < SpacingCells * CellSize * (radius + 1)) found.Add(new());
|
||||
return found.Distinct().OrderBy(c => UniversePosition.AtCellCenter(c, CellSize).RelativeTo(center, CellSize).LengthSquared).Take(maximum).ToArray();
|
||||
}
|
||||
public SectorCoordinate SelectSystem(GalaxyData galaxy)
|
||||
{
|
||||
var random = new DeterministicRandom(galaxy.Seed ^ 0x544152474554UL);
|
||||
for (var i = 0; i < 4096; i++)
|
||||
{
|
||||
var local = new DVector3(random.NextDouble() * 1.4 - .7, (random.NextDouble() - .5) * galaxy.ThicknessMeters / galaxy.RadiusMeters,
|
||||
random.NextDouble() * 1.4 - .7) * galaxy.RadiusMeters;
|
||||
var block = StellarBlock(galaxy.Center.Add(galaxy.Orientation.ToWorld(local), CellSize).Sector);
|
||||
if (Candidate(galaxy, block, out var sector)) return sector;
|
||||
}
|
||||
throw new InvalidOperationException("Galaxy density contains no selectable system. Check the texture brightness and alpha.");
|
||||
}
|
||||
public string NebulaFor(GalaxyData galaxy, UniversePosition position) => galaxy.Nebulae.MinBy(n => position.RelativeTo(n.Center, CellSize).LengthSquared)!.Id;
|
||||
public static ulong Hash(SectorCoordinate p, ulong seed) => DeterministicRandom.Mix(DeterministicRandom.Mix(DeterministicRandom.Mix(seed ^ unchecked((ulong)p.X)) ^ unchecked((ulong)p.Y)) ^ unchecked((ulong)p.Z));
|
||||
}
|
||||
23
src/Universe.Core/Generation/StarMotionBlurSettings.cs
Normal file
23
src/Universe.Core/Generation/StarMotionBlurSettings.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace Universe.Core.Generation;
|
||||
|
||||
public sealed record StarMotionBlurSettings
|
||||
{
|
||||
public bool Enabled { get; init; } = true;
|
||||
public double ShutterFraction { get; init; } = .9;
|
||||
public double MaximumTrailPixels { get; init; } = 180;
|
||||
public double MinimumWidthPixels { get; init; } = 1.4;
|
||||
public double MovingBrightness { get; init; } = 1.8;
|
||||
public double HistoryResetSeconds { get; init; } = .25;
|
||||
public double StarFadeSeconds { get; init; } = .3;
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (!double.IsFinite(ShutterFraction) || ShutterFraction is < 0 or > 2
|
||||
|| !double.IsFinite(MaximumTrailPixels) || MaximumTrailPixels is < 1 or > 1024
|
||||
|| !double.IsFinite(MinimumWidthPixels) || MinimumWidthPixels is < .5 or > 6
|
||||
|| !double.IsFinite(MovingBrightness) || MovingBrightness is < 1 or > 4
|
||||
|| !double.IsFinite(HistoryResetSeconds) || HistoryResetSeconds is < .05 or > 2
|
||||
|| !double.IsFinite(StarFadeSeconds) || StarFadeSeconds is < .05 or > 2)
|
||||
throw new InvalidDataException("config.json: invalid starMotionBlur settings.");
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ public sealed class UniverseGenerator
|
||||
[new(0.13f, 0.32f, 1f), new(0.16f, 0.42f, 1f), new(0.09f, 0.22f, 0.88f), new(0.80f, 0.48f, 0.25f)];
|
||||
|
||||
public UniverseGenerator(ulong universeSeed, double cellSizeMeters = UniverseScale.DefaultCellSizeMeters, KnownSolarSystemCatalog? knownSystems = null,
|
||||
BlackHoleGenerationSettings? blackHoles = null)
|
||||
BlackHoleGenerationSettings? blackHoles = null, GalaxyUniverse? galaxies = null)
|
||||
{
|
||||
if (!double.IsFinite(cellSizeMeters) || cellSizeMeters <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(cellSizeMeters));
|
||||
@@ -25,15 +25,26 @@ public sealed class UniverseGenerator
|
||||
_knownSystems = knownSystems;
|
||||
BlackHoles = blackHoles ?? new();
|
||||
BlackHoles.Validate();
|
||||
Galaxies = galaxies;
|
||||
}
|
||||
|
||||
public ulong UniverseSeed { get; }
|
||||
public double CellSizeMeters { get; }
|
||||
public BlackHoleGenerationSettings BlackHoles { get; }
|
||||
public GalaxyUniverse? Galaxies { get; }
|
||||
|
||||
public StarSystemData Generate(SectorCoordinate coordinate)
|
||||
{
|
||||
if (_knownSystems is not null && _knownSystems.TryGet(coordinate, out var known)) return known;
|
||||
StarSystemData Assign(StarSystemData s)
|
||||
{
|
||||
if (Galaxies is null) return s;
|
||||
var position = UniversePosition.AtCellCenter(coordinate, CellSizeMeters);
|
||||
var galaxy = Galaxies.GalaxyAt(position);
|
||||
return s with { GalaxyId = galaxy.Id, NebulaId = Galaxies.NebulaFor(galaxy, position) };
|
||||
}
|
||||
if (_knownSystems is not null && _knownSystems.TryGet(coordinate, out var known)) return Assign(known);
|
||||
if (Galaxies is not null && !Galaxies.HasSystem(coordinate))
|
||||
return new(coordinate, SeedFor(coordinate), [], []) { Name = "Interstellar space" };
|
||||
var systemSeed = SeedFor(coordinate);
|
||||
var random = new DeterministicRandom(systemSeed);
|
||||
var noiseOffset = ProceduralNoise.SeedOffset(systemSeed);
|
||||
@@ -125,7 +136,7 @@ public sealed class UniverseGenerator
|
||||
});
|
||||
moonCount++;
|
||||
}
|
||||
return new(coordinate, systemSeed, stars, planets);
|
||||
return Assign(new(coordinate, systemSeed, stars, planets));
|
||||
}
|
||||
|
||||
public IEnumerable<StarSystemData> GenerateNeighborhood(SectorCoordinate center, int radius = 1)
|
||||
|
||||
28
src/Universe.Core/World/GalaxyAddress.cs
Normal file
28
src/Universe.Core/World/GalaxyAddress.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Universe.Core.Math;
|
||||
|
||||
namespace Universe.Core.World;
|
||||
|
||||
public readonly record struct GalaxyCoordinate(long X, long Y, long Z);
|
||||
|
||||
/// <summary>Integer macrocell, integer solar-system cell, then local metres. No huge global float position.</summary>
|
||||
public readonly record struct GalaxyAddress(GalaxyCoordinate Region, SectorCoordinate SystemCell, DVector3 LocalMeters)
|
||||
{
|
||||
public static GalaxyAddress FromPosition(UniversePosition position, long regionCells)
|
||||
{
|
||||
static (long Region, long Local) Split(long value, long size)
|
||||
{
|
||||
var wide = (Int128)value + size / 2;
|
||||
var region = wide / size;
|
||||
if (wide < 0 && wide % size != 0) region--;
|
||||
return (checked((long)region), checked((long)((Int128)value - region * size)));
|
||||
}
|
||||
if (regionCells < 2) throw new ArgumentOutOfRangeException(nameof(regionCells));
|
||||
var x = Split(position.Sector.X, regionCells); var y = Split(position.Sector.Y, regionCells); var z = Split(position.Sector.Z, regionCells);
|
||||
return new(new(x.Region, y.Region, z.Region), new(x.Local, y.Local, z.Local), position.LocalOffset);
|
||||
}
|
||||
|
||||
public UniversePosition ToPosition(long regionCells) => new(new(
|
||||
checked((long)((Int128)Region.X * regionCells + SystemCell.X)),
|
||||
checked((long)((Int128)Region.Y * regionCells + SystemCell.Y)),
|
||||
checked((long)((Int128)Region.Z * regionCells + SystemCell.Z))), LocalMeters);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ public static class UniverseScale
|
||||
{
|
||||
public const double AstronomicalUnitMeters = 149_597_870_700d;
|
||||
public const double LightSpeedMetersPerSecond = 299_792_458d;
|
||||
public const double LightYearMeters = 9_460_730_472_580_800d;
|
||||
public const double SolarRadiusMeters = 695_700_000d;
|
||||
public const double EarthRadiusMeters = 6_371_000d;
|
||||
public const double DefaultCellSizeMeters = 100d * AstronomicalUnitMeters;
|
||||
|
||||
@@ -8,10 +8,20 @@ public sealed record UniverseNeighborhood(StarSystemData ActiveSystem, IReadOnly
|
||||
public sealed class UniverseStreamer(UniverseGenerator generator)
|
||||
{
|
||||
private readonly Dictionary<SectorCoordinate, StarSystemData> _cache = [];
|
||||
private SectorCoordinate? _nearBlock;
|
||||
private IReadOnlyList<StarSystemData> _nearSystems = [];
|
||||
|
||||
public (StarSystemData System, StarData Hole)? FindNearestBlackHole(UniversePosition position)
|
||||
{
|
||||
if (generator.BlackHoles.OccurrenceChance <= 0) return null;
|
||||
if (generator.Galaxies is { } galaxies)
|
||||
{
|
||||
return galaxies.NearbySystemCells(position.Sector, 8, 2048).Select(GetOrCreate)
|
||||
.SelectMany(s => s.Stars.Where(h => h.IsBlackHole).Select(h => ((StarSystemData System, StarData Hole)?)(s, h)))
|
||||
.OrderBy(b => UniversePosition.AtCellCenter(b!.Value.System.Coordinate, generator.CellSizeMeters)
|
||||
.Add(b.Value.Hole.PositionMeters, generator.CellSizeMeters).RelativeTo(position, generator.CellSizeMeters).LengthSquared)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
(StarSystemData System, StarData Hole)? nearest = null;
|
||||
var bestDistance = double.PositiveInfinity;
|
||||
var cell = generator.CellSizeMeters;
|
||||
@@ -44,6 +54,21 @@ public sealed class UniverseStreamer(UniverseGenerator generator)
|
||||
public UniverseNeighborhood Load(SectorCoordinate activeCoordinate)
|
||||
{
|
||||
var active = GetOrCreate(activeCoordinate);
|
||||
if (generator.Galaxies is { } galaxies)
|
||||
{
|
||||
var block = galaxies.StellarBlock(activeCoordinate);
|
||||
if (_nearBlock != block)
|
||||
{
|
||||
_nearSystems = galaxies.NearbySystemCells(activeCoordinate).Select(GetOrCreate).ToArray();
|
||||
_nearBlock = block;
|
||||
}
|
||||
if (_cache.Count > 96)
|
||||
{
|
||||
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());
|
||||
}
|
||||
var nearby = new List<StarSystemData>(26);
|
||||
for (var z = -1; z <= 1; z++)
|
||||
for (var y = -1; y <= 1; y++)
|
||||
@@ -71,6 +96,7 @@ public sealed class UniverseStreamer(UniverseGenerator generator)
|
||||
if (!_cache.TryGetValue(coordinate, out var system))
|
||||
{
|
||||
system = generator.Generate(coordinate);
|
||||
if (system.Stars.Count == 0) return system;
|
||||
_cache.Add(coordinate, system);
|
||||
}
|
||||
return system;
|
||||
|
||||
Reference in New Issue
Block a user