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();