bättre ringworld
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
124
src/Universe.App/Graphics/GraphicsReport.RingWorld.cs
Normal file
124
src/Universe.App/Graphics/GraphicsReport.RingWorld.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
134
src/Universe.App/Graphics/GraphicsReport.RingWorldShadows.cs
Normal file
134
src/Universe.App/Graphics/GraphicsReport.RingWorldShadows.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
57
src/Universe.App/Graphics/Shaders/ring-world-air.frag.glsl
Normal file
57
src/Universe.App/Graphics/Shaders/ring-world-air.frag.glsl
Normal 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);
|
||||
}
|
||||
35
src/Universe.App/Graphics/Shaders/ring-world-common.glsl
Normal file
35
src/Universe.App/Graphics/Shaders/ring-world-common.glsl
Normal 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;
|
||||
}
|
||||
66
src/Universe.App/Graphics/Shaders/ring-world-hull.frag.glsl
Normal file
66
src/Universe.App/Graphics/Shaders/ring-world-hull.frag.glsl
Normal 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);
|
||||
}
|
||||
88
src/Universe.App/Graphics/Shaders/ring-world-shadow.glsl
Normal file
88
src/Universe.App/Graphics/Shaders/ring-world-shadow.glsl
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
27
src/Universe.App/Graphics/Shaders/ring-world.vert.glsl
Normal file
27
src/Universe.App/Graphics/Shaders/ring-world.vert.glsl
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
145
src/Universe.App/Graphics/UniverseRenderer.RingWorld.cs
Normal file
145
src/Universe.App/Graphics/UniverseRenderer.RingWorld.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user