bättre ringworld

This commit is contained in:
2026-11-03 20:32:59 +01:00
parent f447fe8e51
commit 6ea397a9e5
39 changed files with 2141 additions and 54 deletions

View File

@@ -50,7 +50,7 @@ internal sealed class AdaptivePlanetMesh : IDisposable
_forward = forward; _right = right; _up = up; _aspect = aspect; _tanHalfFov = tanHalfFov;
_cameraDirection = cameraLocal.Normalized();
_localSurfaceRadius = TerrainHeight.Sample(_planet, _cameraDirection);
var altitude = System.Math.Abs(cameraLocal.Length - _localSurfaceRadius);
var altitude = _planet.IsRingWorld ? System.Math.Abs(RingWorldGeometry.Clearance(_planet, cameraLocal)) : System.Math.Abs(cameraLocal.Length - _localSurfaceRadius);
_nearSurfaceDistance = altitude < 1000 ? altitude + 20 : 0;
_innerRadius = TerrainVisibility.InnerRadius(_planet);
_horizonAngle = TerrainVisibility.HorizonAngle(cameraLocal, _innerRadius, _planet.MaximumTerrainRadiusMeters);
@@ -99,8 +99,14 @@ internal sealed class AdaptivePlanetMesh : IDisposable
tile.LastFrame = _frame;
tile.LastUsedTicks=System.Diagnostics.Stopwatch.GetTimestamp();
var distance = Distance(key, camera);
var spacingPixels = radius * key.Width / TerrainPatchBuilder.Segments / System.Math.Max(distance, 1) * screenHeight / (2 * _tanHalfFov);
if (spacingPixels > TerrainVisibility.PixelTarget(distance) && key.Level < 19)
var patchSpan = _planet.IsRingWorld
? System.Math.Max(radius * System.Math.Tau / 6, _planet.RingWorld!.WidthMeters) / (1 << key.Level)
: radius * key.Width;
var spacingPixels = patchSpan / TerrainPatchBuilder.Segments / System.Math.Max(distance, 1) * screenHeight / (2 * _tanHalfFov);
// The concave band exposes its entire far side. A five-pixel near-field target keeps
// the finest LOD underfoot without refining thousands of distant quads as densely.
var pixelTarget = TerrainVisibility.PixelTarget(distance) * (_planet.IsRingWorld ? 2 : 1);
if (spacingPixels > pixelTarget && key.Level < 19)
{
Span<TerrainPatchKey> buffer = stackalloc TerrainPatchKey[4];
var childCount = 0;
@@ -128,7 +134,39 @@ internal sealed class AdaptivePlanetMesh : IDisposable
}
}
private double Distance(TerrainPatchKey key, DVector3 camera) => TerrainVisibility.DistanceToPatch(key, camera, _localSurfaceRadius);
private double Distance(TerrainPatchKey key, DVector3 camera)
{
if (!_planet.IsRingWorld) return TerrainVisibility.DistanceToPatch(key, camera, _localSurfaceRadius);
_tiles.TryGetValue(key, out var tile);
if (tile is not null)
{
if (tile.DistanceFrame == _frame) return tile.FrameDistance;
var moved = (camera - tile.DistanceCamera).Length;
if (moved <= .5)
{
// Subtract camera travel to bias LOD toward more detail while reusing the
// expensive sampled surface point for sub-metre motion or standing still.
tile.DistanceFrame = _frame;
return tile.FrameDistance = System.Math.Max(1, tile.DistanceAtCamera - moved);
}
}
var angle = System.Math.Atan2(camera.Z, camera.X);
if (angle < 0) angle += System.Math.Tau;
var start = (key.Face + key.X / (double)(1 << key.Level)) * System.Math.Tau / 6;
var width = System.Math.Tau / 6 / (1 << key.Level);
var middle = start + width * .5;
var delta = System.Math.Atan2(System.Math.Sin(angle - middle), System.Math.Cos(angle - middle));
var nearestAngle = middle + System.Math.Clamp(delta, -width * .5, width * .5);
var y0 = (key.Y / (double)(1 << key.Level) - .5) * _planet.RingWorld!.WidthMeters;
var y1 = y0 + _planet.RingWorld.WidthMeters / (1 << key.Level);
var distance = System.Math.Max(1, (RingWorldGeometry.Point(_planet, nearestAngle, System.Math.Clamp(camera.Y, y0, y1)) - camera).Length);
if (tile is not null)
{
tile.DistanceCamera = camera; tile.DistanceAtCamera = distance;
tile.DistanceFrame = _frame; tile.FrameDistance = distance;
}
return distance;
}
private bool Needed(TerrainPatchKey key, DVector3 camera)
=> Visible(key, camera) || (_nearSurfaceDistance > 0 && Distance(key, camera) <= _nearSurfaceDistance);
@@ -144,6 +182,16 @@ internal sealed class AdaptivePlanetMesh : IDisposable
private bool Visible(TerrainPatchKey key, DVector3 camera)
{
if (_planet.IsRingWorld)
{
// Cull by the view only: the opposite side of a concave habitat is visible above the player.
if (_tiles.TryGetValue(key, out var ringTile) && ringTile.Data is { } data) return GeometryVisible(data, camera);
var center = RingWorldGeometry.PatchPoint(_planet, key);
var bound = System.Math.Sqrt(System.Math.Pow(_planet.RadiusMeters * System.Math.PI / 6 / (1 << key.Level), 2)
+ System.Math.Pow(_planet.RingWorld!.WidthMeters * .5 / (1 << key.Level), 2))
+ _planet.TerrainAmplitudeMeters * 2 + System.Math.Max(0, _planet.Terrain.HeightmapMaxMeters);
return GeometryVisible(new(center, [], []) { BoundsCenter = center, BoundsRadius = bound }, camera);
}
var radius = _planet.RadiusMeters;
var outer = _planet.MaximumTerrainRadiusMeters;
// Include the recessed skirt, but keep its uncertainty radial instead of expanding
@@ -201,5 +249,8 @@ internal sealed class AdaptivePlanetMesh : IDisposable
public int LastFrame;
public long LastUsedTicks;
public bool Retiring;
public DVector3 DistanceCamera = new(double.PositiveInfinity, 0, 0);
public double DistanceAtCamera, FrameDistance;
public int DistanceFrame = -1;
}
}

View File

@@ -0,0 +1,124 @@
using Silk.NET.OpenGL;
using Universe.App.Runtime;
using Universe.Core.Generation;
using Universe.Core.Math;
using Universe.Core.Physics;
using Universe.Core.World;
namespace Universe.App.Graphics;
internal static partial class GraphicsReport
{
public static void RunRingWorld(GL gl, UniverseRenderer renderer, FlyCamera camera, UniverseNeighborhood world, int width, int height)
{
var planet = world.ActiveSystem.Planets.Single(p => p.Id == "ring-world");
var dir = Path.Combine(AppContext.BaseDirectory, "graphics-report", "ring-world"); Directory.CreateDirectory(dir);
if (planet.SurfaceTexture is { } map)
{
Console.WriteLine($"Ring World surface: {map.Width}x{map.Height}, {System.Math.Tau * planet.RadiusMeters / map.Width:F2} x {planet.RingWorld!.WidthMeters / map.Height:F2} metres/texel.");
// Four consecutive strips show the full circumference without distorting physical proportions.
const int strips = 4, gap = 6;
var scale = System.Math.Min(1, 1300d * strips / map.Width);
var mapWidth = (int)(map.Width * scale / strips); var stripHeight = System.Math.Max(1, (int)(map.Height * scale));
var mapHeight = stripHeight * strips + gap * (strips - 1);
var preview = new float[mapWidth * mapHeight * 4];
for (var strip = 0; strip < strips; strip++) for (var y = 0; y < stripHeight; y++) for (var x = 0; x < mapWidth; x++)
{
var sx = System.Math.Min(map.Width - 1, (int)((strip + (x + .5) / mapWidth) * map.Width / strips));
var sy = System.Math.Min(map.Height - 1, (int)((y + .5) * map.Height / stripHeight));
var source = (sy * map.Width + sx) * 4;
var target = ((mapHeight - 1 - strip * (stripHeight + gap) - y) * mapWidth + x) * 4;
for (var c = 0; c < 3; c++) preview[target + c] = map.Rgba[source + c] / 255f;
}
WritePng(Path.Combine(dir, "surface-map.png"), mapWidth, mapHeight, preview, false);
}
var body = UniversePosition.AtCellCenter(world.ActiveSystem.Coordinate, camera.CellSizeMeters).Add(planet.PositionMeters, camera.CellSizeMeters);
renderer.SimulationTimeSeconds = 0;
void Pose(DVector3 local, DVector3 forward, DVector3 up) => camera.SetPlayerPose(body.Add(planet.Orientation.ToWorld(local), camera.CellSizeMeters),
planet.Orientation.ToWorld(forward), planet.Orientation.ToWorld(up), true);
void Capture(string name, double seconds = 12)
{
var watch = System.Diagnostics.Stopwatch.StartNew(); var frames = 0; var stable = 0;
do
{
renderer.Render(camera, world, 1d / 60); gl.Finish(); frames++;
stable = renderer.TerrainPendingTiles == 0 && renderer.TerrainUploads == 0 ? stable + 1 : 0;
} while (watch.Elapsed.TotalSeconds < seconds && (stable < 8 || frames < 40));
if (gl.GetError() != GLEnum.NoError) throw new InvalidOperationException("Ring World OpenGL error.");
var pixels = Read(gl, width, height);
if (pixels.Any(p => !float.IsFinite(p))) throw new InvalidOperationException("Non-finite Ring World output.");
WritePng(Path.Combine(dir, name + ".png"), width, height, pixels, false);
Console.WriteLine($"Ring World {name}: {frames} frames, {watch.Elapsed.TotalSeconds:F2}s, {renderer.TerrainTiles} terrain tiles, LOD {renderer.TerrainDeepestLevel}, vegetation {renderer.Vegetation.Instances}.");
}
var overview = new DVector3(1, 1.35, .65).Normalized() * planet.RadiusMeters * 3;
Pose(overview, -overview.Normalized(), new(0, 1, 0)); Capture("overview");
RunRingWorldShadowFixtures(gl, renderer, planet, dir);
var local = FlyCamera.RingInteriorPoint(world.ActiveSystem, planet);
var up = -RingWorldGeometry.Radial(local); var tangent = DVector3.Cross(new(0, 1, 0), up);
Pose(local, (tangent + up * .1).Normalized(), up); Capture("interior");
var ground = RingWorldGeometry.Point(planet, System.Math.Atan2(local.Z, local.X), local.Y, 1.7);
Pose(ground, (tangent + up * .06).Normalized(), up); Capture("ground", 35);
var samples = new List<double>();
for (var i = 0; i < 120; i++)
{
var timer = System.Diagnostics.Stopwatch.StartNew();
renderer.Render(camera, world, 1d / 60); gl.Finish();
samples.Add(timer.Elapsed.TotalMilliseconds);
}
samples.Sort();
var measurements = new List<string> { $"Ground {width}x{height}: median {samples[60]:F2} ms ({1000 / samples[60]:F1} FPS), p95 {samples[114]:F2} ms; {renderer.TerrainTiles} terrain tiles." };
renderer.ProfileFrame = true;
var passes = new Dictionary<string, double>();
for (var i = 0; i < 12; i++)
{
renderer.Render(camera, world, 1d / 60);
foreach (var pass in renderer.FrameTimings) passes[pass.Key] = passes.GetValueOrDefault(pass.Key) + pass.Value / 12;
}
renderer.ProfileFrame = false;
measurements.AddRange(passes.OrderByDescending(p => p.Value).Select(p => $"{p.Key}: {p.Value:F3} ms"));
// Include the real controller, tree collision, orbital translation and body rotation.
// Preserve the report's starting lighting/pose while advancing the actual orbit by its delta.
var player = new PlayerController(camera.Position, camera.Forward, camera.Up, camera.CellSizeMeters, world)
{ TrunksProvider = renderer.Vegetation.Trunks };
for (var i = 0; i < 60; i++) player.Update(1d / 60, 0, 0, false, false, world);
var anchored = planet.Orientation.ToBody(player.Position.RelativeTo(body, camera.CellSizeMeters));
var current = world; var maximumDrift = 0d;
samples.Clear();
for (var i = 1; i <= 180; i++)
{
var timer = System.Diagnostics.Stopwatch.StartNew(); var seconds = i / 60d;
var moved = planet with
{
PositionMeters = planet.PositionMeters + (planet.Orbit is { } orbit ? orbit.Position(seconds) - orbit.Position(0) : DVector3.Zero),
Orientation = new(planet.Orientation.AxialTiltDegrees,
planet.Orientation.RotationDegrees - 360 * seconds / (planet.Rotation?.PeriodSeconds ?? 86400), planet.Orientation.PoleLongitudeDegrees)
};
var planets = world.ActiveSystem.Planets.Select(p => p.Id == planet.Id ? moved : p).ToArray();
var next = world with { ActiveSystem = world.ActiveSystem with { Planets = planets } };
player.ApplyCelestialMotion(current, next); player.Update(1d / 60, 0, 0, false, false, next);
camera.SetPlayerPose(player.Position, player.Forward, player.Up);
renderer.Render(camera, next, 1d / 60); gl.Finish();
if (i > 60) samples.Add(timer.Elapsed.TotalMilliseconds);
var movingBody = UniversePosition.AtCellCenter(next.ActiveSystem.Coordinate, camera.CellSizeMeters).Add(moved.PositionMeters, camera.CellSizeMeters);
maximumDrift = System.Math.Max(maximumDrift, (moved.Orientation.ToBody(player.Position.RelativeTo(movingBody, camera.CellSizeMeters)) - anchored).Length);
current = next;
}
samples.Sort();
measurements.Add($"Playing on moving ring: median {samples[60]:F2} ms ({1000 / samples[60]:F1} FPS), p95 {samples[114]:F2} ms; idle drift {maximumDrift:F6} m.");
if (maximumDrift > .005) throw new InvalidOperationException("Player drifts relative to the moving Ring World.");
foreach (var line in measurements) Console.WriteLine(line);
File.WriteAllLines(Path.Combine(dir, "performance.txt"), measurements);
if (renderer.Vegetation.Stream?.InstanceCount is not > 0) throw new InvalidOperationException("Ring World vegetation did not stream.");
RunRingWorldShadowViews(gl, renderer, camera, world, planet, ground, width, height, dir);
Pose(ground, (tangent * .2 - up).Normalized(), tangent); Capture("feet", 8);
var depth = new float[width * height]; gl.BindTexture(GLEnum.Texture2D, renderer.SceneDepthTexture);
unsafe { fixed (float* p = depth) gl.GetTexImage(GLEnum.Texture2D, 0, PixelFormat.DepthComponent, PixelType.Float, p); }
var threshold = System.Math.Log2(101) / System.Math.Log2(camera.CellSizeMeters * 4.5 + 1);
var covered = depth.Count(d => float.IsFinite(d) && d < threshold) / (double)depth.Length;
Console.WriteLine($"Ring World ground depth coverage within 100m: {covered:P2}.");
if (covered < .98) throw new InvalidOperationException("Ring World ground is missing beneath the player.");
var water = FlyCamera.RingInteriorPoint(world.ActiveSystem, planet, 1); up = -RingWorldGeometry.Radial(water); tangent = DVector3.Cross(new(0, 1, 0), up);
Pose(water, (tangent - up * .15).Normalized(), up); Capture("ocean");
Console.WriteLine("Ring World graphics report PASS: " + dir);
}
}

View File

@@ -0,0 +1,134 @@
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 static partial class GraphicsReport
{
private static void RunRingWorldShadowFixtures(GL gl, UniverseRenderer renderer, PlanetData planet, string directory)
{
const int size = 128;
using var target = new RenderTarget(gl, size, size, fullPrecision: true);
using var quad = new GpuMesh(gl, 3, [-1, -1, 0, 3, -1, 0, -1, 3, 0]);
using var shader = ShaderProgram.Create(gl, ShaderSources.Load("fullscreen.vert"),
"#version 330 core\n" + ShaderSources.Load("ring-world-shadow") + """
in vec2 vUv;
uniform vec3 uFixtureOrigin;
uniform vec3 uFixtureLight;
uniform float uFixtureExtent;
out vec4 fragColor;
void main() {
vec3 p=uFixtureOrigin+vec3(0,(vUv.x-.5)*uFixtureExtent,(vUv.y-.5)*uFixtureExtent);
if(uFixtureExtent>0.0)p.x=sqrt(max(0.0,uFixtureOrigin.x*uFixtureOrigin.x-p.z*p.z));
vec3 ld=normalize(uFixtureLight);
vec3 world=transpose(mat3(uRingWorldToBody))*(p-uRingCameraBody)*uRingRadius;
vec3 light=transpose(mat3(uRingWorldToBody))*ld;
fragColor=vec4(ringHullVisibility(p,ld),ringCloudTransmission(p,ld),ringEnvironmentShadowWorld(world,light),1);
}
""");
float[] Draw(PlanetData body, Vector3 origin, Vector3 light, float extent = 0, float time = 0)
{
target.Bind(); gl.Disable(GLEnum.DepthTest); gl.Disable(GLEnum.Blend); shader.Use();
renderer.BindRingShadowSettings(shader, body, new DVector3(origin.X,origin.Y,origin.Z) * body.RadiusMeters);
shader.Set("uFixtureOrigin", origin); shader.Set("uFixtureLight", light); shader.Set("uFixtureExtent", extent);
shader.Set("uRingTime", time); quad.Draw(); gl.Finish();
var pixels = Read(gl, size, size);
if (pixels.Any(p => !float.IsFinite(p)) || gl.GetError() != GLEnum.NoError)
throw new InvalidOperationException("Non-finite or invalid Ring World shadow shader output.");
return pixels;
}
var clear = planet with { Clouds = planet.Clouds! with { Coverage = 0 }, CloudDensityMap = null };
var cases = new (Vector3 Origin, Vector3 Light, float Expected, string Name)[]
{
(new(.9996f,0,0),new(-1,.8f,0),1,"sun above positive rim"),
(new(.9996f,0,0),new(-1,-.8f,0),1,"sun above negative rim"),
(new(.9996f,0,0),new(-.2f,1,0),0,"positive wall blocks low sun"),
(new(.9996f,0,0),new(-.2f,-1,0),0,"negative wall blocks low sun"),
(new(.9996f,0,0),new(-1,0,0),0,"opposite deck blocks sun"),
(new(.88f,0,0),new(0,1,0),1,"ray above rim"),
(Vector3.Zero,new(0,1,0),1,"open axis"),
(new(.95f,.09999f,0),new(0,1,0),0,"inner wall face"),
(new(.95f,.10201f,0),new(0,1,0),1,"outer wall face"),
(new(1.02001f,0,0),new(1,0,0),1,"outer hull facing sun"),
(new(1.02001f,0,0),new(-1,0,0),0,"outer hull facing away")
};
foreach (var test in cases)
{
var pixels = Draw(clear, test.Origin, test.Light);
if (System.Math.Abs(pixels[0] - test.Expected) > .001 || System.Math.Abs(pixels[2] - test.Expected) > .001)
throw new InvalidOperationException($"Ring World shadow mismatch ({test.Name}): {pixels[0]}/{pixels[2]}, expected {test.Expected}.");
}
var cloudy = clear with { Clouds = clear.Clouds! with { Coverage = 1, ShadowStrength = 1, Density = 1 } };
var overcast = Draw(cloudy, new(.9996f,0,0), new(-1,.8f,0));
if (overcast[1] >= .6 || overcast[2] >= .6)
throw new InvalidOperationException($"Overcast ring clouds did not shadow the ground: {overcast[1]}.");
var disabled = Draw(cloudy with { Clouds = cloudy.Clouds! with { ShadowStrength = 0 } }, new(.9996f,0,0), new(-1,.8f,0));
if (disabled[1] < .999) throw new InvalidOperationException("Ring cloud shadow strength zero still casts shadows.");
var weather = Draw(planet, new(.9996f,0,0), new(-1,.8f,0), .12f);
var later = Draw(planet, new(.9996f,0,0), new(-1,.8f,0), .12f, 900);
double windDelta = 0; var low = 1f; var high = 0f; var transformError = 0f;
for (var i = 1; i < weather.Length; i += 4)
{
windDelta += System.Math.Abs(weather[i] - later[i]) / (size * size);
low = System.Math.Min(low, weather[i]); high = System.Math.Max(high, weather[i]);
transformError = System.Math.Max(transformError, System.Math.Abs(weather[i - 1] * weather[i] - weather[i + 1]));
}
// Ray fixtures above are binary/exact. Allow one percent for filtered density:
// camera-relative float roundtrips can cross a hardware interpolation step.
if (transformError > .01) throw new InvalidOperationException($"Body/world cloud shadow error: {transformError}.");
if (high - low < .1 || windDelta < .01) throw new InvalidOperationException("Ring cloud shadows do not vary spatially or follow wind.");
WritePng(Path.Combine(directory, "shadow-field.png"), size, size, weather, false);
Console.WriteLine($"Ring shadow GPU fixtures PASS: {cases.Length} hull/wall rays, overcast transmission {overcast[1]:F3}, cloud range {low:F3}–{high:F3}, wind delta {windDelta:F3}, transform error {transformError:F4}.");
}
private static void RunRingWorldShadowViews(GL gl, UniverseRenderer renderer, FlyCamera camera, UniverseNeighborhood world,
PlanetData planet, DVector3 ground, int width, int height, string directory)
{
var trees = renderer.Vegetation.Stream!.Tiles.SelectMany(t => t.Instances)
.Where(i => renderer.Vegetation.Catalog.Plants[i.PlantIndex].Type == VegetationType.Tree)
.OrderBy(i => (i.BodyPosition - ground).LengthSquared).ToArray();
if (trees.Length == 0) throw new InvalidOperationException("Missing tree shadow test casters.");
var tree = trees[0].BodyPosition; var up = -RingWorldGeometry.Radial(tree);
var tangent = DVector3.Cross(new(0,1,0), up).Normalized();
var local = tree + up * 18 + tangent * 24;
var body = UniversePosition.AtCellCenter(world.ActiveSystem.Coordinate, camera.CellSizeMeters).Add(planet.PositionMeters, camera.CellSizeMeters);
camera.SetPlayerPose(body.Add(planet.Orientation.ToWorld(local), camera.CellSizeMeters),
planet.Orientation.ToWorld((tree + up * 2 - local).Normalized()), planet.Orientation.ToWorld(up), true);
for (var i = 0; i < 120; i++) { renderer.Render(camera, world, 0); gl.Finish(); }
float[] Snapshot(UniverseNeighborhood scene, string name)
{
renderer.Render(camera, scene, 0); gl.Finish();
var pixels = Read(gl, width, height); WritePng(Path.Combine(directory, name + ".png"), width, height, pixels, false);
return pixels;
}
double Darkened(float[] on, float[] off)
{
double delta = 0;
for (var i = 0; i < on.Length; i += 4)
delta += System.Math.Max(0, (off[i] + off[i+1] + off[i+2] - on[i] - on[i+1] - on[i+2]) / 3) / (width * height);
return delta;
}
try
{
renderer.RingCloudShadows = false;
renderer.RingVegetationShadows = false; var noTrees = Snapshot(world, "tree-shadows-off");
renderer.RingVegetationShadows = true; var withTrees = Snapshot(world, "tree-shadows-on");
var treeDelta = Darkened(withTrees, noTrees);
var overcastPlanet = planet with { Clouds = planet.Clouds! with { Coverage = 1, ShadowStrength = 1 }, CloudDensityMap = null };
var overcast = world with { ActiveSystem = world.ActiveSystem with
{ Planets = world.ActiveSystem.Planets.Select(p => p.Id == planet.Id ? overcastPlanet : p).ToArray() } };
var noClouds = Snapshot(overcast, "cloud-shadows-off");
renderer.RingCloudShadows = true; var withClouds = Snapshot(overcast, "cloud-shadows-on");
var cloudDelta = Darkened(withClouds, noClouds);
if (treeDelta < .0001 || cloudDelta < .001 || gl.GetError() != GLEnum.NoError)
throw new InvalidOperationException($"Ring shadow image comparison failed: tree {treeDelta:G5}, cloud {cloudDelta:G5}.");
Console.WriteLine($"Ring shadow scene PASS: tree shadow darkening {treeDelta:F4}, cloud shadow darkening {cloudDelta:F4}.");
}
finally { renderer.RingVegetationShadows = true; renderer.RingCloudShadows = true; }
}
}

View File

@@ -15,6 +15,9 @@ internal sealed unsafe class PlanetTextures : IDisposable
public PlanetTextures(GL gl, PlanetData planet)
{
_gl = gl;
if (planet.RingWorld?.ProceduralSurface is not null && planet.SurfaceTexture is { } generated
&& System.Math.Max(generated.Width, generated.Height) > gl.GetInteger(GLEnum.MaxTextureSize))
throw new InvalidOperationException("Ring World texture exceeds this GPU's limit. Lower ringWorld.proceduralSurface.maxTextureSize in its JSON configuration.");
Color = gl.GenTexture();
gl.BindTexture(GLEnum.Texture2D, Color);
var image = planet.SurfaceTexture;

View File

@@ -17,7 +17,7 @@ internal static class ShaderSources
}
private static string Expand(string name, string source)
{
foreach (var include in new[] { "noise", "planet-height", "camera-ray", "ring-optics", "rock-common", "asteroid-shape", "cloud-common", "screen-space-reflection", "vegetation-shadow", "biomes" })
foreach (var include in new[] { "noise", "planet-height", "camera-ray", "ring-optics", "rock-common", "asteroid-shape", "cloud-common", "screen-space-reflection", "vegetation-shadow", "biomes", "ring-world-shadow", "ring-world-common" })
{
var directive = $"#include \"{include}.glsl\"";
if (source.Contains(directive, StringComparison.Ordinal))

View File

@@ -0,0 +1,57 @@
#version 330 core
#include "ring-world-common.glsl"
#include "camera-ray.glsl"
in vec2 vUv;
uniform sampler2D uScene;
uniform sampler2D uDepth;
uniform float uFar;
out vec4 fragColor;
vec3 integrate(vec3 source,vec3 o,vec3 d,vec2 interval,bool clouds) {
float a=max(0.0,interval.x),b=interval.y;
if(b<=a)return source;
vec3 scatter=vec3(0);vec3 transmission=vec3(1);
// Integrate front-to-back, then composite this interval over the intervals behind it.
for(int i=0;i<48;i++) {
float x=float(i)/48.0,y=float(i+1)/48.0;
float t=a+(b-a)*(x*x+y*y)*.5,ds=(b-a)*(y*y-x*x)*uRadius;
vec3 p=o+d*t;
float height=max(0.0,(1.0-length(p.xz))*uRadius);
vec3 n=-normalize(vec3(p.x,0,p.z));
vec3 light=vec3(.14);
for(int j=0;j<2;j++) {
if(j>=uLightCount)break;
float sun=max(.03,.25+.75*dot(n,uLightDirections[j]));
float mu=dot(d,uLightDirections[j]);
light+=uLightColors[j]*uLightFlux[j]*sun*(.7+.3*mu*mu)*ringHullVisibility(p,uLightDirections[j]);
}
vec3 extinction;
vec3 tint;
if(clouds) {
float density=ringCloudDensity(p);
extinction=vec3(density*uRingCloudShape.z);
tint=vec3(.92,.96,1.0)*light;
} else {
float density=exp(-height/max(100.0,uAirHeight*uRadius*.055))*uAirDensity;
extinction=(vec3(.45,.55,.7)+uAirColor)*density*.000012;
tint=(vec3(.18,.28,.45)+uAirColor*.4)*light;
}
vec3 stepTransmission=exp(-extinction*ds);
scatter+=transmission*(1.0-stepTransmission)*tint;
transmission*=stepTransmission;
if(max(transmission.r,max(transmission.g,transmission.b))<.001)break;
}
return source*transmission+scatter;
}
void main() {
vec3 worldRay=cameraRay(vUv),d=mat3(uWorldToBody)*worldRay,o=uCameraBody;
float depth=texture(uDepth,vUv).r;
float stop=depth>=.999999?1e8:(exp2(depth*log2(uFar+1.0))-1.0)/(uRadius*max(.0001,dot(worldRay,uForward)));
vec3 color=texture(uScene,vUv).rgb;
vec4 air=shellSegments(o,d,1.0-uAirHeight,1.0,uRingSize.x,stop);
color=integrate(color,o,d,air.zw,false);color=integrate(color,o,d,air.xy,false);
if(uRingCloudEnabled!=0) {
vec4 cloud=shellSegments(o,d,1.0-uRingCloudLayer.x-uRingCloudLayer.y,1.0-uRingCloudLayer.x,uRingSize.x,stop);
color=integrate(color,o,d,cloud.zw,true);color=integrate(color,o,d,cloud.xy,true);
}
fragColor=vec4(color,1);
}

View File

@@ -0,0 +1,35 @@
#include "ring-world-shadow.glsl"
#include "vegetation-shadow.glsl"
#include "planet-height.glsl"
uniform float uAirHeight;
uniform float uAirDensity;
uniform vec3 uAirColor;
uniform vec3 uCameraBody;
uniform int uLightCount;
uniform vec3 uLightDirections[2];
uniform vec3 uLightColors[2];
uniform float uLightFlux[2];
uniform float uTime;
vec2 ringUv(vec3 p) {
return vec2(.5-atan(p.z,p.x)/6.28318530718,clamp(.5-p.y/(uRingSize.x*2.0),0.0,1.0));
}
vec2 ringUvDerivative(vec3 p,vec3 dp) {
return vec2((p.z*dp.x-p.x*dp.z)/(max(1e-12,dot(p.xz,p.xz))*6.28318530718),-dp.y/(uRingSize.x*2.0));
}
vec3 ringDirection(vec3 p) {
// Avoid negative cos(pi/2) at a rim due to float roundoff, which flips longitude by half a turn.
float latitude=clamp(p.y/(uRingSize.x*2.0),-.4999999,.4999999)*3.14159265359;
return vec3(normalize(p.xz).x*cos(latitude),sin(latitude),normalize(p.xz).y*cos(latitude));
}
vec3 ringLight(vec3 p,vec3 n,vec3 world) {
vec3 light=vec3(.09);
for(int i=0;i<2;i++) {
if(i>=uLightCount)break;
vec3 ld=uLightDirections[i];
float ndl=max(0.0,dot(n,ld));
if(ndl<=0.0)continue;
float visibility=ringEnvironmentShadow(p,ld)*vegetationShadow(world,i);
light+=uLightColors[i]*uLightFlux[i]*ndl*visibility;
}
return light;
}

View File

@@ -0,0 +1,66 @@
#version 330 core
#include "ring-world-common.glsl"
#include "camera-ray.glsl"
in vec2 vUv;
uniform float uFar;
uniform int uWaterEnabled;
uniform float uWaterLevel;
uniform vec3 uWaterDeep;
uniform vec3 uWaterShallow;
uniform vec3 uWaves;
out vec4 fragColor;
float best=1e10;
vec3 normal;
int material=0;
void candidate(float t,vec3 n,int m) {if(t>.00000001&&t<best){best=t;normal=n;material=m;}}
void tube(vec3 o,vec3 d,float radius,float y0,float y1,float signNormal,int m) {
vec2 times=cylinder(o,d,radius);
for(int i=0;i<2;i++) {
float t=i==0?times.x:times.y;
vec3 p=o+d*t;
if(p.y>=y0&&p.y<=y1)candidate(t,normalize(vec3(p.x,0,p.z))*signNormal,m);
}
}
void disk(vec3 o,vec3 d,float y,float inner,float outer,float signNormal) {
if(abs(d.y)<1e-12)return;
float t=(y-o.y)/d.y;float r=length((o+d*t).xz);
if(r>=inner&&r<=outer)candidate(t,vec3(0,signNormal,0),0);
}
void main() {
vec3 worldRay=cameraRay(vUv),d=mat3(uWorldToBody)*worldRay,o=uCameraBody;
float w=uRingSize.x,ow=w+uRingSize.z,outer=1.0+uRingSize.y,rim=1.0-uAirHeight;
tube(o,d,outer,-ow,ow,1.0,0);
tube(o,d,rim,w,ow,-1.0,0);tube(o,d,rim,-ow,-w,-1.0,0);
disk(o,d,w,rim,outer,-1.0);disk(o,d,-w,rim,outer,1.0);
disk(o,d,ow,rim,outer,1.0);disk(o,d,-ow,rim,outer,-1.0);
if(uWaterEnabled!=0)tube(o,d,1.0-uWaterLevel,-w,w,-1.0,1);
if(best>1e9)discard;
vec3 p=o+d*best;
vec3 world=worldRay*(best*uRadius);
vec3 color;
if(material==1) {
vec3 direction=ringDirection(p);float map;
float h=mappedElevation(direction,map);
float wet=uHasOceanMask!=0?oceanMaskValue(direction):float(h<uWaterLevel);
// Keep shallow generated coastlines covered too; the terrain depth test clips water on dry land.
if(wet<=.0001&&h>=uWaterLevel)discard;
float a=atan(p.z,p.x)*uRadius,across=p.y*uRadius;
float k=6.2831853/max(1.0,uWaves.y),time=uTime*uWaves.z;
vec3 tangent=normalize(vec3(-p.z,0,p.x));
float waveFilter=1.0-smoothstep(.5,3.0,max(fwidth(a*k),fwidth(across*k)));
normal=normalize(normal+(tangent*cos(a*k+time)*uWaves.x*k*.5+vec3(0,1,0)*sin(across*k*.73-time)*uWaves.x*k*.4)*waveFilter);
float fresnel=.02+.98*pow(1.0-max(0.0,dot(normal,-d)),5.0);
color=mix(uWaterShallow,uWaterDeep,smoothstep(.05,.8,wet))*ringLight(p,normal,world);
color=mix(color,vec3(.16,.32,.50),fresnel*.8);
for(int i=0;i<2;i++){if(i>=uLightCount)break;color+=uLightColors[i]*pow(max(0.0,dot(reflect(d,normal),uLightDirections[i])),160.0)*uLightFlux[i]*1.7*ringEnvironmentShadow(p,uLightDirections[i])*vegetationShadow(world,i);}
color+=vec3(.5)*pow(1.0-wet,8.0)*.2;
} else {
float longitude=atan(p.z,p.x)/6.2831853;
float panels=step(.04,fract(longitude*600.0))*step(.04,fract(p.y*uRadius/8000.0));
color=mix(vec3(.055,.07,.085),vec3(.16,.20,.23),panels)*ringLight(p,normal,world);
color+=vec3(.015,.07,.09)*(1.0-panels)*.25;
}
fragColor=vec4(color,1);
float forwardDistance=best*uRadius*max(.0001,dot(worldRay,uForward));
gl_FragDepth=log2(1.0+forwardDistance)/log2(uFar+1.0);
}

View File

@@ -0,0 +1,88 @@
#include "noise.glsl"
uniform int uRingShadowEnabled;
uniform vec3 uRingSize; // half width, hull thickness, wall thickness / radius
uniform float uRingRadius;
uniform float uRingAirHeight;
uniform vec3 uRingCameraBody; // body position / radius
uniform mat4 uRingWorldToBody;
uniform vec3 uRingNoiseOffset;
uniform float uRingTime;
uniform int uRingCloudEnabled;
uniform int uRingCloudHasMap;
uniform sampler2D uRingCloudMap;
uniform float uRingCloudMapStrength;
uniform vec3 uRingCloudLayer; // base/radius, thickness/radius, density
uniform vec3 uRingCloudShape; // shape scale in metres, coverage, extinction per metre
uniform vec3 uRingCloudWind;
uniform float uRingCloudShadowStrength;
vec2 cylinder(vec3 o,vec3 d,float radius) {
float a=dot(d.xz,d.xz);
if(a<1e-12)return length(o.xz)<radius?vec2(-1e10,1e10):vec2(1e10,-1e10);
float middle=-dot(o.xz,d.xz)/a;
vec2 closest=o.xz+d.xz*middle;
float h=radius*radius-dot(closest,closest);
if(h<0.0)return vec2(1e10,-1e10);
h=sqrt(h/a);return vec2(middle-h,middle+h);
}
vec2 slab(vec3 o,vec3 d,float halfWidth) {
if(abs(d.y)<1e-10)return abs(o.y)<=halfWidth?vec2(-1e10,1e10):vec2(1e10,-1e10);
vec2 t=(vec2(-halfWidth,halfWidth)-o.y)/d.y;return vec2(min(t.x,t.y),max(t.x,t.y));
}
vec4 shellSegments(vec3 o,vec3 d,float inner,float outer,float halfWidth,float stop) {
vec2 c=cylinder(o,d,outer),s=slab(o,d,halfWidth),hole=cylinder(o,d,inner);
float a=max(0.0,max(c.x,s.x)),b=min(stop,min(c.y,s.y));
if(hole.y<=a||hole.x>=b||hole.x>hole.y)return vec4(a,b,1.0,-1.0);
return vec4(a,min(b,hole.x),max(a,hole.y),b);
}
bool ringSolidHit(vec3 o,vec3 d,float inner,float outer,float halfWidth) {
vec4 segments=shellSegments(o,d,inner,outer,halfWidth,1e6);
return segments.y>segments.x||segments.w>segments.z;
}
float ringHullVisibility(vec3 p,vec3 ld) {
// Bias in metres, not a fixed fraction of a planet. The deck and both annular
// walls are solid volumes: their caps and inner/outer faces all block sunlight.
vec3 o=p+ld*(2.0/uRingRadius);
float outer=1.0+uRingSize.y,wallCenter=uRingSize.x+uRingSize.z*.5;
if(ringSolidHit(o,ld,1.0,outer,uRingSize.x))return 0.0;
if(ringSolidHit(o-vec3(0,wallCenter,0),ld,1.0-uRingAirHeight,outer,uRingSize.z*.5))return 0.0;
if(ringSolidHit(o+vec3(0,wallCenter,0),ld,1.0-uRingAirHeight,outer,uRingSize.z*.5))return 0.0;
return 1.0;
}
float ringCloudDensity(vec3 p) {
if(uRingCloudEnabled==0||abs(p.y)>uRingSize.x)return 0.0;
float altitude=1.0-length(p.xz);
float h=(altitude-uRingCloudLayer.x)/uRingCloudLayer.y;
if(h<=0.0||h>=1.0)return 0.0;
vec3 wind=uRingCloudWind*uRingTime/uRingRadius;
vec3 q=p-wind;
vec2 uv=vec2(.5-atan(q.z,q.x)/6.28318530718,clamp(.5-q.y/(uRingSize.x*2.0),0.0,1.0));
float map=uRingCloudHasMap!=0?textureLod(uRingCloudMap,uv,0.0).r:1.0;
float shape=noise3((p*uRingRadius-uRingCloudWind*uRingTime)/uRingCloudShape.x+uRingNoiseOffset)*.5+.5;
float coverage=mix(uRingCloudShape.y,map,uRingCloudMapStrength*float(uRingCloudHasMap));
float density=smoothstep(1.0-coverage,1.16-coverage,shape);
return density*smoothstep(0.0,.2,h)*(1.0-smoothstep(.65,1.0,h))*uRingCloudLayer.z;
}
float ringCloudOpticalDepth(vec3 p,vec3 ld,vec2 interval) {
float length=max(0.0,interval.y-interval.x);
if(length<=0.0)return 0.0;
float density=0.0;
for(int i=0;i<6;i++)density+=ringCloudDensity(p+ld*(interval.x+length*(float(i)+.5)/6.0));
return density*length*uRingRadius*uRingCloudShape.z/6.0;
}
float ringCloudTransmission(vec3 p,vec3 ld) {
if(uRingCloudEnabled==0||uRingCloudShadowStrength<=0.0)return 1.0;
vec4 segments=shellSegments(p,ld,1.0-uRingCloudLayer.x-uRingCloudLayer.y,
1.0-uRingCloudLayer.x,uRingSize.x,1e6);
float optical=ringCloudOpticalDepth(p,ld,segments.xy)+ringCloudOpticalDepth(p,ld,segments.zw);
return mix(1.0,exp(-min(30.0,optical)),uRingCloudShadowStrength);
}
float ringEnvironmentShadow(vec3 p,vec3 ld) {
if(ringHullVisibility(p,ld)<.5)return 0.0;
return ringCloudTransmission(p,ld);
}
float ringEnvironmentShadowWorld(vec3 world,vec3 worldLight) {
if(uRingShadowEnabled==0)return 1.0;
vec3 p=uRingCameraBody+mat3(uRingWorldToBody)*world/uRingRadius;
return ringEnvironmentShadow(p,mat3(uRingWorldToBody)*worldLight);
}

View File

@@ -0,0 +1,44 @@
#version 330 core
#include "ring-world-common.glsl"
in vec3 vBody;
in vec3 vDirection;
in vec3 vNormal;
in vec3 vWorld;
in float vFragDepth;
in vec3 vMaterial;
uniform int uMaterialsEnabled;
uniform vec3 uMaterialSettings;
uniform sampler2D uFlatColor;
uniform sampler2D uSlopeColor;
uniform sampler2D uSurfaceTexture;
uniform int uHasSurfaceTexture;
uniform vec3 uSurfaceColor;
uniform float uFar;
out vec4 fragColor;
vec3 triColor(sampler2D raster,vec3 p,vec3 w) {
return texture(raster,p.yz).rgb*w.x+texture(raster,p.xz).rgb*w.y+texture(raster,p.xy).rgb*w.z;
}
void main() {
vec3 n=normalize(vNormal);
// Cylindrical UVs have constant physical scale across the band, including at the rims.
// Analytic derivatives keep the longitude seam from selecting the coarsest mip level.
vec2 uv=ringUv(vBody),dx=ringUvDerivative(vBody,dFdx(vBody)),dy=ringUvDerivative(vBody,dFdy(vBody));
vec3 color=uHasSurfaceTexture!=0?textureGrad(uSurfaceTexture,uv,dx,dy).rgb:uSurfaceColor;
// Metre-scale variation supplements the geographic albedo at walking distance.
float detail=noise3(vBody*uRadius*.13)*.12+noise3(vBody*uRadius*.7)*.045;
color*=1.0+detail*(1.0-smoothstep(100.0,3000.0,length(vWorld)));
float slope=1.0-max(0.0,dot(n,-normalize(vec3(vBody.x,0,vBody.z))));
color=mix(color,vec3(.22,.20,.17),smoothstep(.16,.65,slope)*.7);
if(uMaterialsEnabled!=0) {
vec3 weights=pow(abs(n),vec3(4));weights/=max(.0001,weights.x+weights.y+weights.z);
vec3 materialUv=vMaterial/uMaterialSettings.x;
vec3 flatColor=triColor(uFlatColor,materialUv,weights);
// Retain the generated biome hue when adding the close-range ground texture.
float grain=dot(flatColor,vec3(.2126,.7152,.0722));
vec3 material=mix(color*clamp(grain*4.0,.45,1.7),triColor(uSlopeColor,materialUv,weights),smoothstep(.06,.3,slope));
float strength=uMaterialSettings.z*(1.0-smoothstep(uMaterialSettings.y*.35,uMaterialSettings.y,length(vWorld)));
color=mix(color,material,strength);
}
fragColor=vec4(color*ringLight(vBody,n,vWorld),1);
gl_FragDepth=log2(max(.0001,vFragDepth))/log2(uFar+1.0);
}

View File

@@ -0,0 +1,27 @@
#version 330 core
layout(location=0) in vec3 aPosition;
layout(location=1) in vec3 aNormal;
layout(location=2) in vec3 aDirection;
uniform vec3 uChunkPosition;
uniform vec3 uBodyOrigin;
uniform float uRadius;
uniform mat4 uViewProjection;
uniform mat4 uBodyToWorld;
uniform float uFar;
uniform vec3 uMaterialAnchor;
out vec3 vMaterial;
out vec3 vBody;
out vec3 vDirection;
out vec3 vNormal;
out vec3 vWorld;
out float vFragDepth;
void main() {
vWorld=uChunkPosition+mat3(uBodyToWorld)*aPosition;
vMaterial=aPosition+uMaterialAnchor;
vBody=uBodyOrigin+aPosition/uRadius;
vDirection=aDirection; vNormal=aNormal;
vec4 clip=uViewProjection*vec4(vWorld,1);
vFragDepth=clip.w+1.0;
gl_Position=clip;
gl_Position.z=(2.0*log2(max(.0001,vFragDepth))/log2(uFar+1.0)-1.0)*clip.w;
}

View File

@@ -1,6 +1,7 @@
#version 330 core
#define VEGETATION_FOLIAGE
#include "vegetation-shadow.glsl"
#include "ring-world-shadow.glsl"
in vec3 vWorldPosition;
in vec3 vStablePosition;
in vec2 vUv;
@@ -40,7 +41,7 @@ void main(){
for(int i=0;i<2;i++){
if(i>=uLightCount)break;float sunHeight=dot(uPlanetUp,uLightDirections[i]);
vec3 extinction=exp(-vec3(.035,.07,.16)/(max(sunHeight,.04)+.08));
light+=uLightColors[i]*extinction*uLightFlux[i]*smoothstep(-.025,.018,sunHeight)*max(.12,dot(normal,uLightDirections[i]))*vegetationShadow(vWorldPosition,i);
light+=uLightColors[i]*extinction*uLightFlux[i]*smoothstep(-.025,.018,sunHeight)*max(.12,dot(normal,uLightDirections[i]))*vegetationShadow(vWorldPosition,i)*ringEnvironmentShadowWorld(vWorldPosition,uLightDirections[i]);
}
fragColor=vec4(tex.rgb*light*(.9+vVariation.x*.2),1);
gl_FragDepth=clamp(log2(max(.0001,vFragDepth))/log2(uFar+1.0),0.0,1.0);

View File

@@ -1,6 +1,7 @@
#version 330 core
#define VEGETATION_FOLIAGE
#include "vegetation-shadow.glsl"
#include "ring-world-shadow.glsl"
in vec3 vWorldPosition;
in vec3 vNormal;
in vec3 vStablePosition;
@@ -62,7 +63,7 @@ void main(){
if(i>=uLightCount)break;
vec3 ld=uLightDirections[i];float ndl=max(0.0,dot(normal,ld));
float back=uFoliage!=0?pow(max(0.0,dot(view,-ld)),3.0)*.32:0.0;
float visibility=vegetationShadow(vWorldPosition,i);
float visibility=vegetationShadow(vWorldPosition,i)*ringEnvironmentShadowWorld(vWorldPosition,uLightDirections[i]);
float sunHeight=dot(uPlanetUp,ld);
vec3 extinction=exp(-vec3(.035,.07,.16)/(max(sunHeight,.04)+.08));
vec3 sunlight=uLightColors[i]*extinction*uLightFlux[i]*visibility*smoothstep(-.025,.018,sunHeight);

View File

@@ -0,0 +1,145 @@
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 partial class UniverseRenderer
{
private ShaderProgram _ringWorldTerrain = null!, _ringWorldHull = null!, _ringWorldAir = null!;
internal bool RingVegetationShadows { get; set; } = true;
internal bool RingCloudShadows { get; set; } = true;
private void InitializeRingWorld()
{
Vegetation.BindEnvironmentShadows = BindRingShadowSettings;
_ringWorldTerrain = Program("ring-world.vert", "ring-world-terrain.frag");
_ringWorldHull = Program("fullscreen.vert", "ring-world-hull.frag");
_ringWorldAir = Program("fullscreen.vert", "ring-world-air.frag");
}
private void ReloadRingWorldShaders()
{
var replacements = new List<ShaderProgram>();
try
{
replacements.Add(Program("ring-world.vert", "ring-world-terrain.frag"));
replacements.Add(Program("fullscreen.vert", "ring-world-hull.frag"));
replacements.Add(Program("fullscreen.vert", "ring-world-air.frag"));
}
catch { foreach (var p in replacements) p.Dispose(); throw; }
DisposeRingWorld();
_ringWorldTerrain = replacements[0]; _ringWorldHull = replacements[1]; _ringWorldAir = replacements[2];
}
private void DisposeRingWorld() { _ringWorldTerrain.Dispose(); _ringWorldHull.Dispose(); _ringWorldAir.Dispose(); }
internal void BindRingShadowSettings(ShaderProgram shader, PlanetData planet, DVector3 bodyCamera)
{
// Set sampler units even on spherical bodies; GLSL forbids active samplers of
// different types sharing the default unit, including behind a disabled branch.
shader.Set("uNoise", 4); shader.Set("uRingCloudMap", 10);
shader.Set("uRingShadowEnabled", planet.IsRingWorld ? 1 : 0);
if (planet.RingWorld is not { } ring) return;
shader.Set("uRingRadius", (float)ring.RadiusMeters);
shader.Set("uRingSize", new Vector3((float)(ring.WidthMeters * .5 / ring.RadiusMeters),
(float)(ring.ThicknessMeters / ring.RadiusMeters), (float)(ring.WallThicknessMeters / ring.RadiusMeters)));
shader.Set("uRingAirHeight", (float)(planet.AtmosphereHeightMeters / ring.RadiusMeters));
shader.Set("uRingCameraBody", (bodyCamera / ring.RadiusMeters).ToVector3());
shader.SetMatrix("uRingWorldToBody", planet.Orientation.WorldToBody);
shader.Set("uRingTime", (float)(_time % 100000));
var cloud = planet.Clouds;
shader.Set("uRingNoiseOffset", ProceduralNoise.SeedOffset(cloud?.Seed ?? planet.Seed).ToVector3());
shader.Set("uRingCloudEnabled", cloud is { Enabled: true } ? 1 : 0);
shader.Set("uRingCloudLayer", cloud is null ? Vector3.Zero : new((float)(cloud.BaseAltitudeMeters / ring.RadiusMeters),
(float)(cloud.ThicknessMeters / ring.RadiusMeters), (float)cloud.Density));
shader.Set("uRingCloudShape", cloud is null ? Vector3.One : new((float)cloud.ShapeScaleMeters, (float)cloud.Coverage, (float)cloud.ExtinctionPerMeter));
shader.Set("uRingCloudWind", cloud is null ? Vector3.Zero : CloudLighting.Wind(cloud).ToVector3());
shader.Set("uRingCloudHasMap", planet.CloudDensityMap is null ? 0 : 1);
shader.Set("uRingCloudMapStrength", (float)(cloud?.DensityTextureStrength ?? 0));
shader.Set("uRingCloudShadowStrength", RingCloudShadows ? (float)(cloud?.ShadowStrength ?? 0) : 0);
BindTexture(10, planet.CloudDensityMap is { } map ? GetCloudDensityTexture(planet, map) : _emptyCloudDensity.Handle);
}
private void BindRingWorld(ShaderProgram shader, PlanetData planet, StarSystemData system, FlyCamera camera, DVector3 relative, float far)
{
if (!_planetTextures.TryGetValue(planet.Id, out var textures)) _planetTextures.Add(planet.Id, textures = new(_gl, planet));
shader.Use(); BindPlanetTerrain(shader, planet, textures); SetCameraRay(shader, camera);
BindRingShadowSettings(shader, planet, planet.Orientation.ToBody(-relative));
Vegetation.BindShadows(shader, RingVegetationShadows ? planet.Id : null, 2);
shader.Set("uSurfaceTexture", 5); shader.Set("uHasSurfaceTexture", planet.SurfaceTexture is null ? 0 : 1);
shader.Set("uSurfaceColor", planet.SurfaceColor);
shader.SetMatrix("uBodyToWorld", planet.Orientation.BodyToWorld);
shader.Set("uCameraBody", planet.Orientation.ToBody(-relative / planet.RadiusMeters).ToVector3());
var r = planet.RingWorld!;
shader.Set("uRingSize", new Vector3((float)(r.WidthMeters * .5 / r.RadiusMeters), (float)(r.ThicknessMeters / r.RadiusMeters), (float)(r.WallThicknessMeters / r.RadiusMeters)));
shader.Set("uAirHeight", (float)(planet.AtmosphereHeightMeters / r.RadiusMeters));
shader.Set("uAirDensity", (float)planet.AtmosphereDensity);
shader.Set("uAirColor", planet.AtmosphereColor);
shader.Set("uFar", far); shader.Set("uTime", (float)(_time % 100000));
BindBodyLighting(shader, system, planet);
var water = planet.Ocean;
shader.Set("uWaterEnabled", water is { Enabled: true } ? 1 : 0);
shader.Set("uWaterLevel", (float)((water?.SeaLevelMeters ?? 0) / r.RadiusMeters));
shader.Set("uWaterDeep", water is null ? new Vector3(.008f, .07f, .14f) : new((float)water.DeepColor[0], (float)water.DeepColor[1], (float)water.DeepColor[2]));
shader.Set("uWaterShallow", water is null ? new Vector3(.025f, .28f, .24f) : new((float)water.ShallowColor[0], (float)water.ShallowColor[1], (float)water.ShallowColor[2]));
shader.Set("uWaves", water is null ? Vector3.Zero : new((float)water.WaveAmplitudeMeters, (float)water.WaveLengthMeters, (float)water.WaveSpeed));
}
private void DrawRingWorlds(FlyCamera camera, StarSystemData system, Matrix4x4 vp, float far)
{
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
foreach (var planet in system.Planets.Where(p => p.IsRingWorld))
{
var relative = center.Add(planet.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters);
if (DVector3.Dot(relative, camera.Forward) < -planet.MaximumTerrainRadiusMeters || relative.Length > planet.RadiusMeters * 2000) continue;
if (!_planetMeshes.TryGetValue(planet.Id, out var mesh)) _planetMeshes.Add(planet.Id, mesh = new(_gl, planet));
BindRingWorld(_ringWorldTerrain, planet, system, camera, relative, far);
_ringWorldTerrain.SetMatrix("uViewProjection", vp);
var materialSettings = planet.Materials;
var materialsEnabled = materialSettings.Enabled && materialSettings.Strength > 0
&& PlanetSurface.Distance(planet, -relative) < materialSettings.FadeDistanceMeters;
_ringWorldTerrain.Set("uMaterialsEnabled", materialsEnabled ? 1 : 0);
_ringWorldTerrain.Set("uMaterialSettings", new Vector3((float)materialSettings.ScaleMeters, (float)materialSettings.FadeDistanceMeters, (float)materialSettings.Strength));
_ringWorldTerrain.Set("uFlatColor", 11); _ringWorldTerrain.Set("uSlopeColor", 13);
if (materialsEnabled)
{
var materials = _materials.ForPlanet(planet);
BindTexture(11, materials.FlatColor); BindTexture(13, materials.SlopeColor);
}
var bodyCamera = planet.Orientation.ToBody(-relative);
mesh.Draw(bodyCamera, planet.Orientation.ToBody(camera.Forward), planet.Orientation.ToBody(camera.Right), planet.Orientation.ToBody(camera.Up),
_height, (double)_width / _height, System.Math.Tan(camera.FieldOfViewRadians * .5), (origin, gpu) =>
{
_ringWorldTerrain.Set("uChunkPosition", (relative + planet.Orientation.ToWorld(origin)).ToVector3());
_ringWorldTerrain.Set("uBodyOrigin", (origin / planet.RadiusMeters).ToVector3());
var scale = materialSettings.ScaleMeters;
static double Mod(double v, double s) => v - System.Math.Floor(v / s) * s;
_ringWorldTerrain.Set("uMaterialAnchor", new Vector3((float)Mod(origin.X, scale), (float)Mod(origin.Y, scale), (float)Mod(origin.Z, scale)));
gpu.Draw();
});
TerrainTiles += mesh.DrawnTiles; TerrainPendingTiles += mesh.PendingTiles; TerrainUploads += mesh.UploadedTiles;
TerrainDeepestLevel = System.Math.Max(TerrainDeepestLevel, mesh.DeepestLevel);
if (mesh.NearestDistance < TerrainNearestDistance) { TerrainNearestDistance = mesh.NearestDistance; TerrainNearestLevel = mesh.NearestLevel; }
BindRingWorld(_ringWorldHull, planet, system, camera, relative, far);
_gl.Enable(GLEnum.DepthTest); _gl.DepthMask(true); _fullscreen.Draw();
}
}
private RenderTarget DrawRingWorldAtmospheres(FlyCamera camera, StarSystemData system, float far, RenderTarget source)
{
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
foreach (var planet in system.Planets.Where(p => p.IsRingWorld).OrderByDescending(p => center.Add(p.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters).Length))
{
var relative = center.Add(planet.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters);
if (relative.Length > planet.RadiusMeters * 100 || DVector3.Dot(relative, camera.Forward) < -planet.MaximumTerrainRadiusMeters) continue;
var target = source == _scatterA ? _scatterB : _scatterA;
target.Bind(); _gl.Disable(GLEnum.DepthTest); _gl.DepthMask(false);
BindRingWorld(_ringWorldAir, planet, system, camera, relative, far);
BindTexture(0, source.Color); BindTexture(1, _scene.Depth);
_ringWorldAir.Set("uScene", 0); _ringWorldAir.Set("uDepth", 1);
_fullscreen.Draw(); source = target;
}
return source;
}
}

View File

@@ -7,7 +7,7 @@ using Universe.Core.World;
namespace Universe.App.Graphics;
internal sealed class UniverseRenderer : IDisposable
internal sealed partial class UniverseRenderer : IDisposable
{
private enum PlanetProbeEffect { Materials, CloudShadows, MountainShadows, BodyShadows, SurfaceNoise }
private static readonly PlanetProbeEffect[] PlanetProbeEffects = Enum.GetValues<PlanetProbeEffect>();
@@ -119,6 +119,7 @@ internal sealed class UniverseRenderer : IDisposable
_starTrails = new(gl, starMotionBlur ?? new());
if (galaxies is not null) _cosmos = new(gl, galaxies, _starTrails);
_surface = Program("terrain.vert", "terrain.frag");
InitializeRingWorld();
_star = Program("star.vert", "star.frag");
_points = Program("point.vert", "point.frag");
_sky = Program("fullscreen.vert", "sky.frag");
@@ -200,6 +201,7 @@ internal sealed class UniverseRenderer : IDisposable
replacements.Add(Program(pair.Item1, pair.Item2));
_starTrails.ReloadShaders();
Vegetation.ReloadShaders();
ReloadRingWorldShaders();
}
catch (Exception error)
{
@@ -344,6 +346,7 @@ internal sealed class UniverseRenderer : IDisposable
Mark("vegetation");
_gpuProfiler.BeginPass("PLANETS TOTAL");
DrawPlanets(camera, system, vp, far, starPositions);
DrawRingWorlds(camera, system, vp, far);
_gpuProfiler.EndPass();
Mark("terrain");
_gpuProfiler.BeginPass("STARS");
@@ -363,6 +366,7 @@ internal sealed class UniverseRenderer : IDisposable
Mark("bodies and ocean");
_gpuProfiler.BeginPass("ATMOSPHERE");
var composed = DrawAtmospheres(camera, system, far);
composed = DrawRingWorldAtmospheres(camera, system, far, composed);
_gpuProfiler.EndPass();
Mark("atmosphere");
_gpuProfiler.BeginPass("CLOUDS / COMPOSITE");
@@ -485,12 +489,12 @@ internal sealed class UniverseRenderer : IDisposable
_surface.SetFloatArray("uLightLuminosities", system.Stars.Select(s => (float)s.LuminositySolar).ToArray());
_surface.Set("uOccluderCount", system.Planets.Count);
_surface.SetVectorArray("uOccluderCenters", relativePlanets.Select(p => p.ToVector3()).ToArray());
_surface.SetFloatArray("uOccluderRadii", system.Planets.Select(p => (float)p.RadiusMeters).ToArray());
_surface.SetFloatArray("uOccluderRadii", system.Planets.Select(p => (float)(p.IsRingWorld ? 0 : p.RadiusMeters)).ToArray());
for (var i = 0; i < system.Planets.Count; i++)
{
var planet = system.Planets[i];
if (onlyPlanetId is not null && planet.Id != onlyPlanetId) continue;
if (planet.IsGasPlanet) continue;
if (planet.IsGasPlanet || planet.IsRingWorld) continue;
var relative = relativePlanets[i];
var blockers=system.Planets.Select((body,index)=>(Body:body,Index:index)).Where(b=>b.Index!=i&&system.Stars.Any(star=>
{
@@ -500,7 +504,7 @@ internal sealed class UniverseRenderer : IDisposable
})).ToArray();
_surface.Set("uOccluderCount",blockers.Length);
_surface.SetVectorArray("uOccluderCenters",blockers.Select(b=>relativePlanets[b.Index].ToVector3()).ToArray());
_surface.SetFloatArray("uOccluderRadii",blockers.Select(b=>(float)b.Body.RadiusMeters).ToArray());
_surface.SetFloatArray("uOccluderRadii",blockers.Select(b=>(float)(b.Body.IsRingWorld ? 0 : b.Body.RadiusMeters)).ToArray());
_surface.Set("uMaximumTerrainRadius",(float)(planet.MaximumTerrainRadiusMeters/planet.RadiusMeters));
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;
@@ -724,7 +728,7 @@ internal sealed class UniverseRenderer : IDisposable
_gas.Set("uBandFrequency", (float)planet.CloudBandFrequency); _gas.Set("uStormStrength", (float)planet.StormStrength);
_gas.Set("uPlanetIndex", planet.Index); _gas.Set("uOccluderCount", system.Planets.Count);
_gas.SetVectorArray("uOccluderCenters", system.Planets.Select(p => planet.Orientation.ToBody((p.PositionMeters - planet.PositionMeters) / planet.RadiusMeters).ToVector3()).ToArray());
_gas.SetFloatArray("uOccluderRadii", system.Planets.Select(p => (float)(p.RadiusMeters / planet.RadiusMeters)).ToArray());
_gas.SetFloatArray("uOccluderRadii", system.Planets.Select(p => (float)(p.IsRingWorld ? 0 : p.RadiusMeters / planet.RadiusMeters)).ToArray());
BindBodyLighting(_gas, system, planet); BindRingProfile(_gas, planet);
_fullscreen.Draw();
}
@@ -734,7 +738,7 @@ internal sealed class UniverseRenderer : IDisposable
private void DrawOceans(FlyCamera camera, StarSystemData system, float far)
{
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
var candidates = system.Planets.Where(p => p.Ocean is { Enabled: true }).Select(p => (Planet: p,
var candidates = system.Planets.Where(p => !p.IsRingWorld && p.Ocean is { Enabled: true }).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.MaximumTerrainRadiusMeters)
.OrderByDescending(p => p.Relative.Length);
@@ -802,7 +806,7 @@ internal sealed class UniverseRenderer : IDisposable
if (!_oceanRendered) return source;
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
// Only a nearby ocean needs detailed SSR; orbital views retain the cheaper analytic reflection.
var planet = system.Planets.Where(p => p.Ocean is { Enabled: true, ScreenSpaceReflections: { Enabled: true, Strength: > 0 } })
var planet = system.Planets.Where(p => !p.IsRingWorld && p.Ocean is { Enabled: true, ScreenSpaceReflections: { Enabled: true, Strength: > 0 } })
.Where(p => System.Math.Abs(center.Add(p.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters).Length
- p.RadiusMeters - p.Ocean!.SeaLevelMeters) < p.Ocean.ScreenSpaceReflections.MaxSurfaceDistanceMeters)
.MinBy(p => center.Add(p.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters).Length - p.RadiusMeters);
@@ -949,7 +953,7 @@ internal sealed class UniverseRenderer : IDisposable
{
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
var candidates = system.Planets.Select(p => (Planet: p, Relative: center.Add(p.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters)))
.Where(p => !p.Planet.IsGasPlanet && p.Planet.AtmosphereHeightMeters > 0 && p.Relative.Length < p.Planet.RadiusMeters * 35 && DVector3.Dot(p.Relative, camera.Forward) > -p.Planet.RadiusMeters * 1.04)
.Where(p => !p.Planet.IsGasPlanet && !p.Planet.IsRingWorld && p.Planet.AtmosphereHeightMeters > 0 && p.Relative.Length < p.Planet.RadiusMeters * 35 && DVector3.Dot(p.Relative, camera.Forward) > -p.Planet.RadiusMeters * 1.04)
.OrderBy(p => p.Relative.Length / p.Planet.RadiusMeters).Take(3).OrderByDescending(p => p.Relative.Length).ToArray();
var source = _scene;
_gl.Disable(GLEnum.DepthTest); _gl.DepthMask(false);
@@ -1048,7 +1052,7 @@ internal sealed class UniverseRenderer : IDisposable
private RenderTarget DrawClouds(FlyCamera camera, StarSystemData system, float far, RenderTarget source)
{
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
var candidates = system.Planets.Where(p => p.Clouds is { Enabled: true, Coverage: > 0, Density: > 0 }).Select(p => (Planet: p,
var candidates = system.Planets.Where(p => !p.IsRingWorld && p.Clouds is { Enabled: true, Coverage: > 0, Density: > 0 }).Select(p => (Planet: p,
Relative: center.Add(p.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters)))
.Where(p => p.Relative.Length < p.Planet.RadiusMeters * 35
&& DVector3.Dot(p.Relative, camera.Forward) > -p.Planet.RadiusMeters - p.Planet.Clouds!.BaseAltitudeMeters - p.Planet.Clouds.ThicknessMeters)
@@ -1112,7 +1116,7 @@ internal sealed class UniverseRenderer : IDisposable
var visibility = 1d;
foreach (var planet in system.Planets)
{
if (planet.AtmosphereHeightMeters <= 0) continue;
if (planet.AtmosphereHeightMeters <= 0 || planet.IsRingWorld) continue;
var radial = local - planet.PositionMeters;
var altitude = radial.Length - planet.RadiusMeters;
if (altitude > planet.AtmosphereHeightMeters) continue;
@@ -1221,6 +1225,7 @@ internal sealed class UniverseRenderer : IDisposable
{
if (_disposed) return;
_disposed = true;
DisposeRingWorld();
foreach (var mesh in _planetMeshes.Values) mesh.Dispose();
_planetMeshes.Clear();
foreach (var textures in _planetTextures.Values) textures.Dispose();

View File

@@ -69,6 +69,7 @@ internal static unsafe class VegetationPreparation
{
var catalog=VegetationCatalog.Load(Path.Combine(directory,"vegetation.json"));
using var program=ShaderProgram.Create(gl,ShaderSources.Load("vegetation.vert"),ShaderSources.Load("vegetation.frag"));
program.Use();program.Set("uNoise",4);program.Set("uRingCloudMap",10);program.Set("uRingShadowEnabled",0);
var instance=gl.GenBuffer();gl.BindBuffer(GLEnum.ArrayBuffer,instance);
float[] data=[0,0,0,1,0,1,0,-System.MathF.PI/2,.5f,0,1,.5f];
fixed(float* p=data)gl.BufferData(GLEnum.ArrayBuffer,(nuint)(data.Length*4),p,GLEnum.StaticDraw);

View File

@@ -56,6 +56,7 @@ internal sealed unsafe class VegetationRenderer : IDisposable
private Dictionary<(int Plant,int Variant),Batch>? _farSpare;private uint[] _farBuildingBuffers=[];
private CancellationTokenSource _farCancellation=new();private long _lastFarBuild;
public bool Enabled {get;set;}=true;
internal Action<ShaderProgram,PlanetData,DVector3>? BindEnvironmentShadows { get; set; }
public bool DepthReady {get;private set;}
public double Aspect {get;set;}=1.8;
public int Instances {get;private set;}
@@ -86,7 +87,7 @@ internal sealed unsafe class VegetationRenderer : IDisposable
if(!Enabled){CpuMilliseconds=0;UploadMilliseconds=0;return;}
var center=UniversePosition.AtCellCenter(system.Coordinate,camera.CellSizeMeters);
var planet=system.Planets.Where(p=>p.Vegetation is {Enabled:true}&&!p.IsGasPlanet)
.MinBy(p=>center.Add(p.PositionMeters,camera.CellSizeMeters).RelativeTo(camera.Position,camera.CellSizeMeters).Length-p.RadiusMeters);
.MinBy(p=>PlanetSurface.Distance(p,-center.Add(p.PositionMeters,camera.CellSizeMeters).RelativeTo(camera.Position,camera.CellSizeMeters)));
if(planet is null){if(Stream is not null){Stream.Dispose();ResetFar();}Stream=null;PlanetId=null;_planet=null;_requiredPaths.Clear();return;}
if(PlanetId!=planet.Id||_planet?.Seed!=planet.Seed||_systemId!=system.Id)
{
@@ -95,10 +96,10 @@ internal sealed unsafe class VegetationRenderer : IDisposable
_requiredPaths.Clear();foreach(var p in Catalog.Plants)if(planet.Vegetation!.AllowedPlants is null||planet.Vegetation.AllowedPlants.Contains(p.Id,StringComparer.Ordinal))_requiredPaths.Add(p.Model);
}
_planet=planet;_cameraBody=planet.Orientation.ToBody(-center.Add(planet.PositionMeters,camera.CellSizeMeters).RelativeTo(camera.Position,camera.CellSizeMeters));
_surfaceAltitude=_cameraBody.Length-TerrainHeight.Sample(Stream!.Generator.Field.Planet,_cameraBody.Normalized());
_surfaceAltitude=PlanetSurface.Distance(Stream!.Generator.Field.Planet,_cameraBody);
if(_surfaceAltitude>10000){CpuMilliseconds=watch.Elapsed.TotalMilliseconds;return;}
Stream!.Update(_cameraBody);
var biome=Stream.Generator.Field.Weights(Stream.Generator.Field.Color(_cameraBody.Normalized()));
var biome=Stream.Generator.Field.Weights(Stream.Generator.Field.Color(planet.IsRingWorld?RingWorldGeometry.TextureDirection(planet,_cameraBody):_cameraBody.Normalized()));
if(Stream.InstanceCount>0||biome.X+biome.Y>.1)
foreach(var definition in Catalog.Plants)
if(_requiredPaths.Contains(definition.Model)&&!_models.ContainsKey(definition.Model)&&!_models.Values.Any(m=>!m.Task.IsCompleted))
@@ -270,7 +271,7 @@ internal sealed unsafe class VegetationRenderer : IDisposable
foreach(var tile in stream.Tiles)
{
// Tile anchors are on the reference sphere; mountains can be kilometres above it.
var tileDistance=(tile.Origin.Normalized()*_cameraBody.Length-_cameraBody).Length;
var tileDistance=(_planet.IsRingWorld?tile.Origin-_cameraBody:tile.Origin.Normalized()*_cameraBody.Length-_cameraBody).Length;
if(tileDistance>nearRange+256)continue;
foreach(var instance in tile.Instances)
{
@@ -341,7 +342,7 @@ internal sealed unsafe class VegetationRenderer : IDisposable
for(var light=0;light<system.Stars.Count;light++)for(var cascade=0;cascade<2;cascade++)
{
var radius=settings.ShadowDistancesMeters[cascade];var ld=(system.Stars[light].PositionMeters-_planet.PositionMeters).Normalized();
var lightBody=_planet.Orientation.ToBody(ld);var localUp=_cameraBody.Normalized();
var lightBody=_planet.Orientation.ToBody(ld);var localUp=_planet.IsRingWorld?-RingWorldGeometry.Radial(_cameraBody):_cameraBody.Normalized();
var axis=DVector3.Cross(localUp,lightBody).Normalized();if(axis.LengthSquared<.1)axis=DVector3.Cross(System.Math.Abs(lightBody.Y)<.9?new(0,1,0):new DVector3(1,0,0),lightBody).Normalized();
// Keep the light-space grid independent of head rotation; snap in double precision.
var vertical=DVector3.Cross(lightBody,axis).Normalized();var target=_cameraBody;
@@ -372,7 +373,8 @@ internal sealed unsafe class VegetationRenderer : IDisposable
}
private void Lighting(ShaderProgram program,StarSystemData system)
{
program.Set("uPlanetUp",_planet!.Orientation.ToWorld(_cameraBody.Normalized()).ToVector3());
BindEnvironmentShadows?.Invoke(program,_planet!,_cameraBody);
program.Set("uPlanetUp",_planet!.Orientation.ToWorld(_planet.IsRingWorld?-RingWorldGeometry.Radial(_cameraBody):_cameraBody.Normalized()).ToVector3());
program.Set("uLightCount",system.Stars.Count);
program.SetVectorArray("uLightDirections",system.Stars.Select(s=>(s.PositionMeters-_planet!.PositionMeters).Normalized().ToVector3()).ToArray());
program.SetVectorArray("uLightColors",system.Stars.Select(s=>s.Color).ToArray());
@@ -451,7 +453,7 @@ internal sealed unsafe class VegetationRenderer : IDisposable
{
if(!Enabled||Stream is null||PlanetId!=planetId)return [];
var range=_planet!.Vegetation!.CollisionDistanceMeters;
return Stream.Tiles.Where(t=>t.Key.Layer==0&&(t.Origin.Normalized()*bodyPosition.Length-bodyPosition).Length<range+400).SelectMany(t=>t.Instances)
return Stream.Tiles.Where(t=>t.Key.Layer==0&&(_planet!.IsRingWorld?t.Origin-bodyPosition:t.Origin.Normalized()*bodyPosition.Length-bodyPosition).Length<range+400).SelectMany(t=>t.Instances)
.Where(i=>(i.BodyPosition-bodyPosition).Length<range&&_models.TryGetValue(Catalog.Plants[i.PlantIndex].Model,out var m)&&m.Ready)
.Select(i=>(i,definition:Catalog.Plants[i.PlantIndex])).Where(x=>x.definition.TrunkCollision is not null)
.Select(x=>new TrunkCapsule(x.i.BodyPosition,x.i.Up,x.definition.TrunkCollision!.RadiusMeters*x.i.Scale,x.definition.TrunkCollision.HeightMeters*x.i.Scale)).ToArray();

View File

@@ -97,6 +97,7 @@ public sealed class FlyCamera
.MinBy(p => World(p.System, p.Planet.PositionMeters).RelativeTo(Position, CellSizeMeters).Length);
if (target.Planet is null) return;
var planet = target.Planet; var cloud = planet.Clouds!;
if (planet.IsRingWorld) { RequestRingInterior(target.System, planet, 2); return; }
var body = World(target.System, planet.PositionMeters);
var sun = (target.System.Stars[planet.HostStarIndex].PositionMeters - planet.PositionMeters).Normalized();
var side = Perpendicular(sun); var across = DVector3.Cross(sun, side);
@@ -132,6 +133,7 @@ public sealed class FlyCamera
.MinBy(p => World(p.System, p.Planet.PositionMeters).RelativeTo(Position, CellSizeMeters).Length);
if (target.Planet is null) return;
var planet = target.Planet; var ocean = planet.Ocean!;
if (planet.IsRingWorld) { RequestRingInterior(target.System, planet, 1); return; }
var body = World(target.System, planet.PositionMeters);
var sun = (target.System.Stars[planet.HostStarIndex].PositionMeters - planet.PositionMeters).Normalized();
var side = Perpendicular(sun); var bitangent = DVector3.Cross(sun, side);
@@ -203,6 +205,7 @@ public sealed class FlyCamera
var nearest = candidates
.MinBy(body => World(body.System, body.Planet.PositionMeters).RelativeTo(Position, CellSizeMeters).Length - body.Planet.RadiusMeters);
var planet = nearest.Planet;
if (planet.IsRingWorld && surface) { RequestRingInterior(nearest.System, planet, 0); return; }
var bodyPosition = World(nearest.System, planet.PositionMeters);
var sun = (nearest.System.Stars[planet.HostStarIndex].PositionMeters - planet.PositionMeters).Normalized();
var side = Perpendicular(sun);
@@ -275,8 +278,19 @@ public sealed class FlyCamera
up = RotateAround(pole, forward, v.RollDegrees * System.Math.PI / 180);
}
speed = 250_000;
if (planet.IsRingWorld)
{
radial = planet.Orientation.ToWorld(new DVector3(1, 1.35, .65).Normalized());
forward = -radial; up = planet.Orientation.ToWorld(new(0, 1, 0));
}
}
BeginFlight(bodyPosition, radial, finalRadius, System.Math.Max(planet.MaximumTerrainRadiusMeters * 5, finalRadius * 1.4), forward, up, speed);
if (planet.IsRingWorld)
{
var startBody = planet.Orientation.ToBody(Position.RelativeTo(bodyPosition, CellSizeMeters));
if (new DVector3(startBody.X, 0, startBody.Z).Length < planet.RadiusMeters && System.Math.Abs(startBody.Y) < planet.RingWorld!.WidthMeters * .5)
_flight = _flight! with { RingExit = true, StartRadial = planet.Orientation.ToWorld(new(0, 1, 0)), ApproachRadius = planet.RadiusMeters * 1.5 };
}
}
public void RequestStarView(IEnumerable<StarSystemData> systems)
@@ -329,6 +343,48 @@ public sealed class FlyCamera
private UniversePosition World(StarSystemData system, DVector3 offset) => UniversePosition.AtCellCenter(system.Coordinate, CellSizeMeters).Add(offset, CellSizeMeters);
public static DVector3 RingInteriorPoint(StarSystemData system, PlanetData planet, int view = 0)
{
var sun = planet.Orientation.ToBody((system.Stars[planet.HostStarIndex].PositionMeters - planet.PositionMeters).Normalized());
var best = double.NegativeInfinity; var selected = RingWorldGeometry.Point(planet, 0, 0, 1000);
for (var row = -4; row <= 4; row++) for (var i = 0; i < 144; i++)
{
var angle = i * System.Math.Tau / 144; var across = row * planet.RingWorld!.WidthMeters / 12;
var point = RingWorldGeometry.Point(planet, angle, across);
var d = RingWorldGeometry.TextureDirection(planet, point);
var wet = planet.OceanMask?.Sample(d) ?? 0;
if (view == 1 ? wet < .95 : wet > .05) continue;
if (view != 1 && planet.Ocean is { Enabled: true } ocean
&& RingWorldGeometry.Elevation(planet, point) <= ocean.SeaLevelMeters + 1) continue;
var flatness = DVector3.Dot(RingWorldGeometry.Contact(planet, point).Up, -RingWorldGeometry.Radial(point));
if (view == 0 && flatness < .98) continue;
var color = planet.SurfaceTexture?.SampleColor(d) ?? new System.Numerics.Vector3(45, 80, 35);
var score = DVector3.Dot(-RingWorldGeometry.Radial(point), sun) * 2
+ (color.Y - color.X) / 80 - System.Math.Abs(across) / planet.RingWorld.WidthMeters * .2 + flatness * 2;
if (score <= best) continue;
best = score;
var up = -RingWorldGeometry.Radial(point);
selected = view == 1
? RingWorldGeometry.Radial(point) * (planet.RadiusMeters - planet.Ocean!.SeaLevelMeters - 8) + new DVector3(0, across, 0)
: point + up * (view == 2 ? (planet.Clouds!.BaseAltitudeMeters + planet.Clouds.ThicknessMeters + 1000) : 1000);
}
return selected;
}
private void RequestRingInterior(StarSystemData system, PlanetData planet, int view)
{
var local = RingInteriorPoint(system, planet, view);
var up = planet.Orientation.ToWorld(-RingWorldGeometry.Radial(local));
var tangent = planet.Orientation.ToWorld(DVector3.Cross(new(0, 1, 0), -RingWorldGeometry.Radial(local)));
var point = planet.Orientation.ToWorld(local);
var body = World(system, planet.PositionMeters);
var from = planet.Orientation.ToBody(Position.RelativeTo(body, CellSizeMeters));
var inside = new DVector3(from.X, 0, from.Z).Length < planet.RadiusMeters && System.Math.Abs(from.Y) < planet.RingWorld!.WidthMeters * .5;
BeginFlight(body, point.Normalized(), point.Length, planet.RadiusMeters * 1.5,
(tangent - up * .08).Normalized(), up, view == 0 ? 100 : 10, duration: 6);
_flight = _flight! with { RingInterior = true, RingDirect = inside, StartRadial = planet.Orientation.ToWorld(new(0, 1, 0)) };
}
private void BeginFlight(UniversePosition body, DVector3 radial, double finalRadius, double approachRadius, DVector3 forward, DVector3 up, double speed,
double duration = 4.5, double? fieldOfView = null, Departure? departure = null)
{
@@ -384,7 +440,7 @@ public sealed class FlyCamera
var system = current.NearbySystems.Append(current.ActiveSystem).FirstOrDefault(s => s.Coordinate == nearest.System.Coordinate);
var planet = system?.Planets.FirstOrDefault(p => p.Id == nearest.Planet.Id);
if (planet is null) return;
if (offset.Length - TerrainHeight.Sample(nearest.Planet, offset.Normalized()) < 100)
if (PlanetSurface.Distance(nearest.Planet, offset) < 100)
{
DVector3 Turn(DVector3 v) => planet.Orientation.ToWorld(nearest.Planet.Orientation.ToBody(v));
offset = Turn(offset); _forward = Turn(_forward); _up = Turn(_up);
@@ -402,7 +458,29 @@ public sealed class FlyCamera
var t = System.Math.Clamp(_flightElapsed / f.Duration, 0, 1);
var travelT = f.Departure is null ? t : System.Math.Clamp((t - .28) / .72, 0, 1);
UniversePosition proposed;
if (f.Departure is { } departure && t < .28)
if (f.RingExit)
{
if (t < .3) proposed = f.Start.Add(f.Body.RelativeTo(f.Start, CellSizeMeters) * Smooth(t / .3), CellSizeMeters);
else if (t < .55) proposed = f.Body.Add(f.StartRadial * f.ApproachRadius * Smooth((t - .3) / .25), CellSizeMeters);
else
{
var portal = f.StartRadial * f.ApproachRadius;
proposed = f.Body.Add(DVector3.Lerp(portal, f.TargetRadial * f.FinalRadius, Smooth((t - .55) / .45)), CellSizeMeters);
}
}
else if (f.RingInterior)
{
var destination = f.Body.Add(f.TargetRadial * f.FinalRadius, CellSizeMeters);
if (f.RingDirect) proposed = f.Start.Add(destination.RelativeTo(f.Start, CellSizeMeters) * Smooth(t), CellSizeMeters);
else if (t < .3)
{
var portal = f.Body.Add(f.StartRadial * f.ApproachRadius, CellSizeMeters);
proposed = f.Start.Add(portal.RelativeTo(f.Start, CellSizeMeters) * Smooth(t / .3), CellSizeMeters);
}
else if (t < .55) proposed = f.Body.Add(f.StartRadial * f.ApproachRadius * (1 - Smooth((t - .3) / .25)), CellSizeMeters);
else proposed = f.Body.Add(f.TargetRadial * f.FinalRadius * Smooth((t - .55) / .45), CellSizeMeters);
}
else if (f.Departure is { } departure && t < .28)
{
var direction = t < .12 ? departure.StartRadial : Slerp(departure.StartRadial, departure.TargetRadial, Smooth((t - .12) / .16));
var radius = t < .12 ? departure.StartRadius + (departure.ClearanceRadius - departure.StartRadius) * Smooth(t / .12) : departure.ClearanceRadius;
@@ -474,6 +552,11 @@ public sealed class FlyCamera
private sealed record FocusFlight(UniversePosition Start, UniversePosition Body, DVector3 StartRadial, DVector3 TargetRadial,
double ApproachRadius, double FinalRadius, DVector3 StartForward, DVector3 StartUp, DVector3 TargetForward, DVector3 TargetUp, double FinalSpeed,
double Duration, double StartFov, double TargetFov, Departure? Departure);
double Duration, double StartFov, double TargetFov, Departure? Departure)
{
public bool RingInterior { get; init; }
public bool RingDirect { get; init; }
public bool RingExit { get; init; }
}
private sealed record Departure(UniversePosition Body, DVector3 StartRadial, DVector3 TargetRadial, double StartRadius, double ClearanceRadius);
}

View File

@@ -13,6 +13,9 @@ public sealed class PlayerController
private DVector3 _heading, _velocity;
private double _pitch;
private bool _jumpHeld;
private string? _ringId;
private SectorCoordinate _ringSystem;
private DVector3 _ringLocalPosition;
public UniversePosition Position { get; private set; }
public bool Grounded { get; private set; }
public bool Crouching { get; private set; }
@@ -26,7 +29,7 @@ public sealed class PlayerController
{
Position = position; _cellSize = cellSize;
var target = Nearest(neighborhood);
Up = target is { } t ? position.RelativeTo(t.Body, cellSize).Normalized() : fallbackUp.Normalized();
Up = target is { } t ? PlanetSurface.GravityUp(t.Planet, position.RelativeTo(t.Body, cellSize)) : fallbackUp.Normalized();
if (Up == DVector3.Zero) Up = new(0, 1, 0);
var projected = forward - Up * DVector3.Dot(forward, Up);
_heading = projected.LengthSquared > .0001 ? projected.Normalized() : Tangent(Up);
@@ -48,9 +51,11 @@ public sealed class PlayerController
if (planet is null) return;
var body = UniversePosition.AtCellCenter(system!.Coordinate, _cellSize).Add(planet.PositionMeters, _cellSize);
var relative = Position.RelativeTo(old.Body, _cellSize);
if (old.Planet.IsRingWorld && _ringId == old.Planet.Id && _ringSystem == old.System.Coordinate)
relative = old.Planet.Orientation.ToWorld(_ringLocalPosition);
// Use the rotating local frame near the surface, including jumps: the player inherits the ground's
// rotational motion instead of the planet sliding hundreds of metres sideways during a jump.
if (Grounded || relative.Length - TerrainHeight.Sample(old.Planet, relative.Normalized()) <= System.Math.Max(2000, old.Planet.AtmosphereHeightMeters))
if (Grounded || PlanetSurface.Distance(old.Planet, relative) <= System.Math.Max(2000, old.Planet.AtmosphereHeightMeters))
{
DVector3 Rotate(DVector3 v) => planet.Orientation.ToWorld(old.Planet.Orientation.ToBody(v));
relative = Rotate(relative); _heading = Rotate(_heading); Up = Rotate(Up); _velocity = Rotate(_velocity);
@@ -65,6 +70,13 @@ public sealed class PlayerController
var target = Nearest(neighborhood);
if (target is not { } t) { _jumpHeld = jump; return; }
var relative = Position.RelativeTo(t.Body, _cellSize);
if (t.Planet.IsRingWorld)
{
if (_ringId == t.Planet.Id && _ringSystem == t.System.Coordinate)
relative = t.Planet.Orientation.ToWorld(_ringLocalPosition);
UpdateRing(dt, forward, side, jump, crouch, t.System, t.Planet, t.Body, relative); return;
}
_ringId = null;
var radial = relative.Normalized();
if (radial == DVector3.Zero) radial = Up;
Align(radial);
@@ -139,6 +151,81 @@ public sealed class PlayerController
_heading = heading.LengthSquared > .0001 ? heading.Normalized() : Tangent(up);
Up = up;
}
private void UpdateRing(double dt, double forward, double side, bool jump, bool crouch,
StarSystemData system, PlanetData planet, UniversePosition body, DVector3 relative)
{
var contact = PlanetSurface.Contact(planet, relative);
Align(PlanetSurface.GravityUp(planet, relative));
var previousEye = EyeHeight;
Grounded = contact.Clearance <= previousEye + .08 && DVector3.Dot(_velocity, Up) <= .05
&& CanStandOnRing(contact, Up);
Crouching = crouch;
// Correct eye height along gravity only. Re-projecting onto the normal each frame
// creeps sideways on curved terrain, even with no input and no celestial motion.
if (Grounded) relative = RingEyePosition(planet, relative, EyeHeight);
var right = DVector3.Cross(_heading, Up).Normalized();
var input = _heading * System.Math.Clamp(forward, -1, 1) + right * System.Math.Clamp(side, -1, 1);
if (input.LengthSquared > 1) input = input.Normalized();
var horizontal = input * (Crouching ? 1.5 : 4.5);
var vertical = DVector3.Dot(_velocity, Up);
if (Grounded && jump && !_jumpHeld) { vertical = 5.2; Grounded = false; }
_jumpHeld = jump;
if (Grounded)
{
var next = MoveThroughTrees(planet, relative, relative + horizontal * dt, Up);
var nextContact = PlanetSurface.Contact(planet, next);
// Follow the curved surface while retaining the local tangent heading around the seam.
var rise = DVector3.Dot(nextContact.Point - contact.Point, Up);
if (CanStandOnRing(nextContact, PlanetSurface.GravityUp(planet, next))
&& rise <= .45 && (horizontal.Length * dt < .001 || rise <= horizontal.Length * dt * .85))
relative = RingEyePosition(planet, next, EyeHeight);
SetRingPosition(system, planet, body, relative);
Align(PlanetSurface.GravityUp(planet, relative));
_velocity = horizontal - Up * DVector3.Dot(horizontal, Up);
return;
}
var altitude = System.Math.Max(0, contact.Clearance - EyeHeight);
vertical = altitude > 2000 && vertical <= 0
? -System.Math.Max(-vertical, System.Math.Min(altitude / 5, _cellSize * .02))
: System.Math.Max(-180, vertical - planet.RingWorld!.GravityMetersPerSecondSquared * dt);
_velocity = horizontal + Up * vertical;
var destination = MoveThroughTrees(planet, relative, relative + _velocity * dt, Up);
var collision = PlanetCollisionSolver.Sweep(body.Add(relative, _cellSize), body.Add(destination, _cellSize),
UniversePosition.AtCellCenter(system.Coordinate, _cellSize), [planet], _cellSize, EyeHeight);
Position = collision.Position;
var resolved = Position.RelativeTo(body, _cellSize);
SetRingPosition(system, planet, body, resolved);
Align(PlanetSurface.GravityUp(planet, resolved));
if (collision.Collided)
{
var hit = PlanetSurface.Contact(planet, resolved);
Grounded = CanStandOnRing(hit, Up) && DVector3.Dot(_velocity, Up) <= .05;
var intoSurface = DVector3.Dot(_velocity, hit.Up);
if (intoSurface < 0) _velocity -= hit.Up * intoSurface;
}
}
private void SetRingPosition(StarSystemData system, PlanetData planet, UniversePosition body, DVector3 relative)
{
// World coordinates are quantised at millimetres in a 100 AU cell. Do not feed that
// rounding back into the next frame's local position as the habitat orbits/spins.
_ringId = planet.Id; _ringSystem = system.Coordinate;
_ringLocalPosition = planet.Orientation.ToBody(relative);
Position = body.Add(relative, _cellSize);
}
private static DVector3 RingEyePosition(PlanetData planet, DVector3 relative, double eye)
{
var up = PlanetSurface.GravityUp(planet, relative);
for (var i = 0; i < 3; i++)
{
var contact = PlanetSurface.Contact(planet, relative);
var error = eye - contact.Clearance;
if (System.Math.Abs(error) < .001) break;
relative += up * (error / System.Math.Max(.2, DVector3.Dot(contact.Up, up)));
}
return relative;
}
private static bool CanStandOnRing(SurfaceContact contact, DVector3 up)
=> DVector3.Dot(contact.Up, up) >= 1 / System.Math.Sqrt(1 + .85 * .85);
private DVector3 MoveThroughTrees(PlanetData planet,DVector3 start,DVector3 end,DVector3 up)
{
if(TrunksProvider is null)return end;
@@ -156,7 +243,7 @@ public sealed class PlayerController
{
if (planet.IsGasPlanet) continue;
var body = UniversePosition.AtCellCenter(system.Coordinate, _cellSize).Add(planet.PositionMeters, _cellSize);
var distance = Position.RelativeTo(body, _cellSize).Length - planet.RadiusMeters;
var distance = PlanetSurface.Distance(planet, Position.RelativeTo(body, _cellSize));
if (distance >= closest) continue;
closest = distance; best = (system, planet, body);
}

View File

@@ -84,7 +84,7 @@ public sealed class UniverseApplication : IDisposable
var options = WindowOptions.Default with
{
Title = "Universe | loading",
IsVisible = !Environment.GetCommandLineArgs().Any(a => a is "--shadow-report" or "--lookaround-report" or "--prepare-vegetation" or "--vegetation-report" or "--simulation-report" or "--graphics-report" or "--scene-report" or "--ocean-report" or "--cloud-report" or "--terrain-report" or "--menu-report" or "--terrain-perf-report" or "--black-hole-report" or "--galaxy-journey-report" or "--star-trails-report"),
IsVisible = !Environment.GetCommandLineArgs().Any(a => a is "--ring-world-report" or "--shadow-report" or "--lookaround-report" or "--prepare-vegetation" or "--vegetation-report" or "--simulation-report" or "--graphics-report" or "--scene-report" or "--ocean-report" or "--cloud-report" or "--terrain-report" or "--menu-report" or "--terrain-perf-report" or "--black-hole-report" or "--galaxy-journey-report" or "--star-trails-report"),
Size = Environment.GetCommandLineArgs().Contains("--vegetation-report")?new Vector2D<int>(2560,1440):new Vector2D<int>(1440, 900),
WindowBorder = Environment.GetCommandLineArgs().Contains("--vegetation-report")?WindowBorder.Hidden:WindowBorder.Resizable,
VSync = true,
@@ -108,6 +108,8 @@ public sealed class UniverseApplication : IDisposable
{VegetationPreparation.Run(gl,Path.Combine(AppContext.BaseDirectory,"../../../../..","Content","Vegitations"));_window.Close();return;}
_input = _window.CreateInput();
_renderer = new(gl, _window.FramebufferSize.X, _window.FramebufferSize.Y, _galaxies, _starMotionBlur);
if(Environment.GetCommandLineArgs().Contains("--ring-world-report"))
{GraphicsReport.RunRingWorld(gl,_renderer,_camera,_neighborhood,_window.FramebufferSize.X,_window.FramebufferSize.Y);_window.Close();return;}
if(Environment.GetCommandLineArgs().Contains("--lookaround-report"))
{GraphicsReport.RunLookaround(gl,_renderer,_camera,_neighborhood,_window.FramebufferSize.X,_window.FramebufferSize.Y);_window.Close();return;}
if(Environment.GetCommandLineArgs().Contains("--vegetation-report"))