skog, mjuka skuggor, jämnare övergång vatten på av avstånd

This commit is contained in:
2026-11-02 23:25:51 +01:00
parent cb5070dc1c
commit f447fe8e51
196 changed files with 5583 additions and 143 deletions

View File

@@ -7,10 +7,10 @@ namespace Universe.App.Graphics;
/// <summary>Builds tiles on worker threads. A parent remains visible until all four children are ready.</summary>
internal sealed class AdaptivePlanetMesh : IDisposable
{
private static readonly SemaphoreSlim Workers = new(System.Math.Clamp(Environment.ProcessorCount - 2, 1, 4));
private readonly GL _gl;
private readonly PlanetData _planet;
private readonly Dictionary<TerrainPatchKey, Tile> _tiles = [];
private readonly Queue<TerrainPatchKey> _retire = new();
private readonly CancellationTokenSource _cancel = new();
private int _frame;
private int _uploads;
@@ -28,6 +28,7 @@ internal sealed class AdaptivePlanetMesh : IDisposable
public int DrawnTiles { get; private set; }
public int DeepestLevel { get; private set; }
public int PendingTiles => _pending;
public int UploadedTiles=>_uploads;
public int NearestLevel { get; private set; }
public double NearestDistance { get; private set; }
@@ -69,11 +70,24 @@ internal sealed class AdaptivePlanetMesh : IDisposable
for (var face = 0; face < roots.Length; face++) roots[face] = new(face, 0, 0, 0);
SortNearest(roots, cameraLocal);
foreach (var root in roots) Visit(root, cameraLocal, screenHeight, draw);
if (_frame % 120 != 0) return;
foreach (var pair in _tiles.Where(pair => pair.Key.Level > 0 && pair.Value.Task is null && _frame - pair.Value.LastFrame > 360).ToArray())
// Frame-count expiry discarded nearby geometry during short orbital trips, especially at high FPS.
// Retain a minute of reuse, retire the oldest excess only, and spread driver deletes over frames.
if(_frame%120==0)
{
pair.Value.Mesh?.Dispose();
_tiles.Remove(pair.Key);
var excess=System.Math.Max(0,_tiles.Count-16384-_retire.Count);
foreach(var pair in _tiles.Where(p=>p.Key.Level>0&&p.Value.Task is null&&_frame-p.Value.LastFrame>2&&!p.Value.Retiring).OrderBy(p=>p.Value.LastUsedTicks))
{
if(System.Diagnostics.Stopwatch.GetElapsedTime(pair.Value.LastUsedTicks).TotalSeconds<=60&&excess<=0)break;
pair.Value.Retiring=true;_retire.Enqueue(pair.Key);excess--;
}
}
for(var i=0;i<16&&_retire.TryDequeue(out var key);i++)
{
if(!_tiles.TryGetValue(key,out var tile))continue;
tile.Retiring=false;
if(tile.Task is not null||_frame-tile.LastFrame<=2)continue;
if(_tiles.Count<=16384&&System.Diagnostics.Stopwatch.GetElapsedTime(tile.LastUsedTicks).TotalSeconds<=60)continue;
tile.Mesh?.Dispose();_tiles.Remove(key);
}
}
@@ -83,6 +97,7 @@ internal sealed class AdaptivePlanetMesh : IDisposable
var radius = _planet.RadiusMeters;
var tile = _tiles[key];
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)
@@ -159,14 +174,10 @@ internal sealed class AdaptivePlanetMesh : IDisposable
_tiles.Add(key, tile);
_pending++;
var token = _cancel.Token;
tile.Task = Task.Run(async () =>
{
await Workers.WaitAsync(token);
try { token.ThrowIfCancellationRequested(); return TerrainPatchBuilder.Build(_planet, key); }
finally { Workers.Release(); }
});
tile.Task = GenerationJobs.Run(()=>TerrainPatchBuilder.Build(_planet,key),token);
}
tile.LastFrame = _frame;
tile.LastUsedTicks=System.Diagnostics.Stopwatch.GetTimestamp();
if (tile.Mesh is not null) return true;
if (tile.Data is null || _uploads >= 6) return false;
tile.Mesh = new(_gl, 9, tile.Data.Vertices, tile.Data.Indices);
@@ -188,5 +199,7 @@ internal sealed class AdaptivePlanetMesh : IDisposable
public Task<TerrainPatchData>? Task;
public GpuMesh? Mesh;
public int LastFrame;
public long LastUsedTicks;
public bool Retiring;
}
}

View File

@@ -65,6 +65,7 @@ internal sealed class BenchmarkOverlay : IDisposable
Rect(x, y, 3 * scale, panelHeight, .13f, .55f, .8f);
var title=benchmark is GalaxyJourneyBenchmark journey
? journey.IsComplete ? journey.PerformancePassed ? "GALAXY JOURNEY PASS" : "GALAXY JOURNEY FAIL" : journey.IsRunning ? "GALAXY JOURNEY LIVE" : "GALAXY JOURNEY STOPPED"
: benchmark is ForestBenchmark forest?forest.IsComplete?forest.PerformancePassed?"FOREST PASS":"FOREST FAIL":forest.IsRunning?"FOREST LIVE":"FOREST STOPPED"
: benchmark.IsRunning ? "BENCHMARK LIVE" : benchmark.Phase == "STOPPED" ? "BENCHMARK STOPPED" : "BENCHMARK COMPLETE";
Text(title,
x + 16 * scale, y + 11 * scale, 1.75 * scale, .76f, .91f, 1f);

View File

@@ -0,0 +1,91 @@
using System.Diagnostics;
using System.Text.Json;
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
{
public static void RunLookaround(GL gl,UniverseRenderer renderer,FlyCamera camera,UniverseNeighborhood world,int width,int height)
{
var directory=Path.Combine(AppContext.BaseDirectory,"graphics-report","lookaround");Directory.CreateDirectory(directory);
var planet=world.ActiveSystem.Planets.Single(p=>p.Id=="earth");
var body=UniversePosition.AtCellCenter(world.ActiveSystem.Coordinate,camera.CellSizeMeters).Add(planet.PositionMeters,camera.CellSizeMeters);
var sun=(world.ActiveSystem.Stars[planet.HostStarIndex].PositionMeters-planet.PositionMeters).Normalized();
var direction=VegetationSiteFinder.FindForest(new(planet,renderer.Vegetation.Catalog),planet.Orientation.ToBody(sun));
var radial=planet.Orientation.ToWorld(direction);
var tangent=DVector3.Cross(planet.Orientation.ToWorld(new(0,1,0)),radial).Normalized();
var across=DVector3.Cross(radial,tangent).Normalized();
var position=body.Add(radial*(TerrainHeight.Sample(planet,radial)+1.8),camera.CellSizeMeters);
void Pose(double angle)
{
var forward=(tangent*System.Math.Cos(angle)+across*System.Math.Sin(angle)+radial*(-.08+.2*System.Math.Sin(angle*2))).Normalized();
camera.SetPlayerPose(position,forward,(radial-forward*DVector3.Dot(radial,forward)).Normalized());
}
Pose(0);renderer.SimulationTimeSeconds=12;renderer.BenchmarkProfiling=true;
var warmup=Stopwatch.StartNew();var stable=0;var warmupFrames=0;
while(warmup.Elapsed.TotalSeconds<90)
{
renderer.Render(camera,world,1d/60);gl.Finish();warmupFrames++;
var vegetation=renderer.Vegetation;
stable=vegetation.ReadyModels==vegetation.RequiredModels&&vegetation.Stream?.PendingTiles==0
&&renderer.TerrainPendingTiles==0&&renderer.TerrainUploads==0?stable+1:0;
if(stable>=30&&warmup.Elapsed.TotalSeconds>=10)break;
if(warmupFrames%300==0)Console.WriteLine($"Lookaround warmup {warmup.Elapsed.TotalSeconds:F1}s: models {vegetation.ReadyModels}/{vegetation.RequiredModels}, tiles {vegetation.Stream?.Tiles.Count()}, pending {vegetation.Stream?.PendingTiles}/{renderer.TerrainPendingTiles}.");
}
if(stable<30)throw new InvalidOperationException("Lookaround scene did not settle before measurement.");
warmup.Stop();
var profile=Environment.GetCommandLineArgs().Contains("--profile-passes");renderer.ProfileFrame=profile;
var frames=new List<LookaroundFrame>(840);
using var process=Process.GetCurrentProcess();var cpuStart=process.TotalProcessorTime;
var allocatedStart=GC.GetTotalAllocatedBytes();var collections=Enumerable.Range(0,3).Select(GC.CollectionCount).ToArray();
var run=Stopwatch.StartNew();
for(var frame=0;frame<840;frame++)
{
var phase=frame<120?"stationary":frame<480?"first-turn":"repeat-turn";
Pose(frame<120?0:(frame-120)*System.Math.PI/180);
renderer.SimulationTimeSeconds=12+frame/60d;
var start=Stopwatch.GetTimestamp();renderer.Render(camera,world,1d/60);gl.Finish();
var ms=Stopwatch.GetElapsedTime(start).TotalMilliseconds;
if(profile&&ms>50)Console.WriteLine($"Slow lookaround frame {frame}: "+JsonSerializer.Serialize(renderer.FrameTimings));
if(gl.GetError()!=GLEnum.NoError)throw new InvalidOperationException("Lookaround OpenGL error.");
var vegetation=renderer.Vegetation;
frames.Add(new(phase,ms,vegetation.CpuMilliseconds,vegetation.UploadMilliseconds,vegetation.Stream!.CpuMilliseconds,
vegetation.Stream.GenerationMilliseconds,vegetation.Stream.Revision,vegetation.Stream.PendingTiles,renderer.TerrainPendingTiles,renderer.TerrainUploads));
if(frame%120==119)Console.WriteLine($"Lookaround {phase}: {frame+1}/840 frames, {ms:F2} ms, vegetation CPU {vegetation.CpuMilliseconds:F2} ms.");
}
var elapsed=run.Elapsed.TotalSeconds;var cpu=(process.TotalProcessorTime-cpuStart).TotalSeconds;
renderer.ProfileFrame=false;
var allocated=GC.GetTotalAllocatedBytes()-allocatedStart;
var gc=Enumerable.Range(0,3).Select(i=>GC.CollectionCount(i)-collections[i]).ToArray();
using(var csv=new StreamWriter(Path.Combine(directory,"frames.csv")))
{
csv.WriteLine("phase,frame_ms,vegetation_cpu_ms,upload_ms,stream_cpu_ms,generation_worker_ms,revision,vegetation_pending,terrain_pending,terrain_uploads");
foreach(var f in frames)csv.WriteLine(FormattableString.Invariant($"{f.Phase},{f.Milliseconds},{f.VegetationCpuMs},{f.UploadMs},{f.StreamCpuMs},{f.GenerationMs},{f.Revision},{f.VegetationPending},{f.TerrainPending},{f.TerrainUploads}"));
}
var summary=new
{
Width=width,Height=height,Gpu=renderer.HardwareName,SyncGpuEachFrame=true,ProfilePasses=profile,WarmupSeconds=warmup.Elapsed.TotalSeconds,
CpuSeconds=cpu,CpuPercentOfAllProcessors=cpu/elapsed/Environment.ProcessorCount*100,AllocatedBytes=allocated,Collections=gc,
Phases=frames.GroupBy(f=>f.Phase).Select(g=>new
{
Phase=g.Key,AverageMs=g.Average(f=>f.Milliseconds),P99Ms=Percentile(g.Select(f=>f.Milliseconds),.99),MaxMs=g.Max(f=>f.Milliseconds),
VegetationCpuAverageMs=g.Average(f=>f.VegetationCpuMs),VegetationCpuP99Ms=Percentile(g.Select(f=>f.VegetationCpuMs),.99),
GenerationWorkerMs=g.Sum(f=>f.GenerationMs),VegetationRevisions=g.Last().Revision-g.First().Revision,
TerrainUploads=g.Sum(f=>f.TerrainUploads),FramesOver50Ms=g.Count(f=>f.Milliseconds>50)
}).ToArray()
};
var json=JsonSerializer.Serialize(summary,new JsonSerializerOptions{WriteIndented=true});
File.WriteAllText(Path.Combine(directory,"summary.json"),json);Console.WriteLine(json);
WritePng(Path.Combine(directory,"final.png"),width,height,Read(gl,width,height),false);
Console.WriteLine("Lookaround report: "+directory);
}
private static double Percentile(IEnumerable<double> values,double percentile)
{var sorted=values.Order().ToArray();return sorted[(int)((sorted.Length-1)*percentile)];}
private sealed record LookaroundFrame(string Phase,double Milliseconds,double VegetationCpuMs,double UploadMs,double StreamCpuMs,
double GenerationMs,long Revision,int VegetationPending,int TerrainPending,int TerrainUploads);
}

View File

@@ -0,0 +1,81 @@
using System.Text.Json;
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 unsafe void ValidateOceanDetail(GL gl, UniverseRenderer renderer, FlyCamera camera,
UniverseNeighborhood world, PlanetData planet, UniversePosition body, DVector3 radial,
DVector3 heading, int width, int height, string directory)
{
void Pose(double altitude, DVector3 forward, DVector3 up) => camera.SetPlayerPose(
body.Add(radial * (planet.RadiusMeters + planet.Ocean!.SeaLevelMeters + altitude), camera.CellSizeMeters),
forward, up, resetMotionHistory: true);
double Roughness(double altitude)
{
Pose(altitude, -radial, heading);
renderer.Render(camera, world, 0); gl.Finish();
var framebuffer = gl.GenFramebuffer();
try
{
gl.BindFramebuffer(GLEnum.ReadFramebuffer, framebuffer);
gl.FramebufferTexture2D(GLEnum.ReadFramebuffer, GLEnum.ColorAttachment0, GLEnum.Texture2D, renderer.OceanMaterialTexture, 0);
gl.ReadBuffer(GLEnum.ColorAttachment0);
if (gl.CheckFramebufferStatus(GLEnum.ReadFramebuffer) != GLEnum.FramebufferComplete)
throw new InvalidOperationException("Incomplete ocean material diagnostic framebuffer.");
var pixels = new float[16 * 16 * 4];
fixed (float* p = pixels) gl.ReadPixels(width / 2 - 8, height / 2 - 8, 16, 16, PixelFormat.Rgba, PixelType.Float, p);
return Enumerable.Range(0, 256).Average(i => (double)pixels[i * 4 + 3]);
}
finally { gl.BindFramebuffer(GLEnum.ReadFramebuffer, 0); gl.DeleteFramebuffer(framebuffer); }
}
// Read the production material buffer: lost normal detail must become optical roughness.
var near = Roughness(8);
var far = Roughness(8000);
if (!double.IsFinite(near + far) || near < .04 || far > 1 || far <= near + .015)
throw new InvalidOperationException($"Subpixel ocean slopes lost their roughness: near {near}, far {far}.");
// Freeze the environment and cross the old and current geometric detail ranges in small steps.
var stableWorld = world with { ActiveSystem = world.ActiveSystem with
{
Planets = world.ActiveSystem.Planets.Select(p => p with { Clouds = null }).ToArray()
} };
var maxDelta = 0d;
foreach (var range in new[] { 800d, 2000d, planet.Ocean!.WaveGeometryDistanceMeters }.Distinct())
{
float[]? previous = null;
for (var step = -3; step <= 3; step++)
{
Pose((range + step * .5) / System.Math.Sqrt(2), (heading - radial).Normalized(), radial);
renderer.Render(camera, stableWorld, 0); gl.Finish();
var pixels = Read(gl, width, height);
if (pixels.Any(v => !float.IsFinite(v))) throw new InvalidOperationException("Ocean LOD produced non-finite pixels.");
if (previous is not null)
{
double sum = 0;
for (var i = 0; i < width * height / 2; i++)
for (var c = 0; c < 3; c++) sum += System.Math.Abs(pixels[i * 4 + c] - previous[i * 4 + c]);
maxDelta = System.Math.Max(maxDelta, sum / (width * height / 2 * 3));
}
previous = pixels;
}
}
if (maxDelta > .02 || gl.GetError() != GLEnum.NoError)
throw new InvalidOperationException($"Ocean detail transition popped: mean RGB step {maxDelta}.");
File.WriteAllText(Path.Combine(directory, "ocean-detail.json"), JsonSerializer.Serialize(new
{
Gpu = renderer.HardwareName, Width = width, Height = height,
NearRoughness = near, FarRoughness = far, MaximumTransitionRgbDelta = maxDelta,
TransitionStepMeters = .5, TransitionDeltaLimit = .02, WaveComponents = OceanSurface.WaveCount,
planet.Ocean.WaveGeometryDistanceMeters
}, new JsonSerializerOptions { WriteIndented = true }));
Console.WriteLine($"Ocean LOD passed: roughness {near:F3} -> {far:F3}, maximum transition RGB step {maxDelta:G4}.");
}
}

View File

@@ -0,0 +1,86 @@
using System.Numerics;
using Silk.NET.OpenGL;
namespace Universe.App.Graphics;
internal static partial class GraphicsReport
{
public static unsafe void RunShadows(GL gl)
{
const int size=32,width=512,height=256;
var directory=Path.Combine(AppContext.BaseDirectory,"graphics-report","shadows");Directory.CreateDirectory(directory);
var depths=new float[size*size*4];
for(var layer=0;layer<4;layer++)for(var y=0;y<size;y++)for(var x=0;x<size;x++)
{
var shadow=x<size/2+(int)System.Math.Floor((y-size/2)*.31);
if(layer>=2)shadow=!shadow;
depths[(layer*size+y)*size+x]=shadow?.3f:.7f;
}
var texture=gl.GenTexture();
gl.ActiveTexture(GLEnum.Texture2);gl.BindTexture(GLEnum.Texture2DArray,texture);
fixed(float* p=depths)gl.TexImage3D(GLEnum.Texture2DArray,0,InternalFormat.DepthComponent24,size,size,4,0,PixelFormat.DepthComponent,PixelType.Float,p);
gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureMinFilter,(int)GLEnum.Linear);
gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureMagFilter,(int)GLEnum.Linear);
gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureCompareMode,(int)GLEnum.CompareRefToTexture);
gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureCompareFunc,(int)GLEnum.Lequal);
gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureWrapS,(int)GLEnum.ClampToEdge);
gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureWrapT,(int)GLEnum.ClampToEdge);
try
{
using var target=new RenderTarget(gl,width,height,fullPrecision:true);
using var quad=new GpuMesh(gl,3,[-1,-1,0,3,-1,0,-1,3,0]);
ShaderProgram Program(bool foliage)=>ShaderProgram.Create(gl,ShaderSources.Load("fullscreen.vert"),
"#version 330 core\n"+(foliage?"#define VEGETATION_FOLIAGE\n":"")+
"in vec2 vUv;\nuniform int uFixtureMode;\nuniform int uFixtureLight;\nout vec4 fragColor;\n"+
ShaderSources.Load("vegetation-shadow")+"\nvoid main(){vec3 p=uFixtureMode==0?vec3(vUv*2.0-1.0,0):vec3(70.0+vUv.x*40.0,0,0);fragColor=vec4(vec3(vegetationShadow(p,uFixtureLight)),1);}");
using var ground=Program(false);using var foliage=Program(true);
float[] Draw(ShaderProgram program,float softness=1,int light=0,bool cascade=false,bool enabled=true)
{
target.Bind();gl.Disable(GLEnum.DepthTest);gl.Disable(GLEnum.Blend);program.Use();
program.Set("uVegetationShadowMap",2);program.Set("uVegetationShadows",enabled?1:0);
program.Set("uVegetationShadowDistances",new Vector3(100,400,0));
program.Set("uVegetationShadowSettings",new Vector3(softness,.2f,.035f));
program.Set("uFixtureMode",cascade?1:0);program.Set("uFixtureLight",light);
for(var i=0;i<4;i++)program.SetMatrix($"uVegetationShadowMatrices[{i}]",cascade?Matrix4x4.CreateScale(.005f):Matrix4x4.Identity);
gl.ActiveTexture(GLEnum.Texture2);gl.BindTexture(GLEnum.Texture2DArray,texture);
quad.Draw();gl.Finish();return Read(gl,width,height);
}
double Sample(double u,double v,int layer)
{
var px=u*size-.5;var py=v*size-.5;var x=(int)System.Math.Floor(px);var y=(int)System.Math.Floor(py);
double Tap(int xx,int yy)=>depths[(layer*size+System.Math.Clamp(yy,0,size-1))*size+System.Math.Clamp(xx,0,size-1)]>.5?1:0;
var fx=px-x;var fy=py-y;
return (Tap(x,y)*(1-fx)+Tap(x+1,y)*fx)*(1-fy)+(Tap(x,y+1)*(1-fx)+Tap(x+1,y+1)*fx)*fy;
}
double Validate(float[] pixels,int radius,int light)
{
double error=0;var denominator=radius==2?81d:16d;
for(var y=3;y<height-3;y+=3)for(var x=3;x<width-3;x+=3)
{
double expected=0;
for(var j=-radius;j<=radius;j++)for(var i=-radius;i<=radius;i++)
expected+=(radius+1-System.Math.Abs(i))*(radius+1-System.Math.Abs(j))*Sample((x+.5)/width+(double)i/size,(y+.5)/height+(double)j/size,light*2);
error=System.Math.Max(error,System.Math.Abs(pixels[(y*width+x)*4]-(.18+.82*expected/denominator)));
}
if(error>.003)throw new InvalidOperationException($"Shadow tent filter mismatch: {error}.");
return error;
}
var soft=Draw(ground);var hard=Draw(ground,0);
var groundError=Validate(soft,2,0);var leafError=Validate(Draw(foliage),1,0);var secondSunError=Validate(Draw(ground,light:1),2,1);
double EdgeStep(float[] pixels)=>Enumerable.Range(1,width-1).Max(x=>System.Math.Abs(pixels[(height/2*width+x)*4]-pixels[(height/2*width+x-1)*4]));
var hardStep=EdgeStep(hard);var softStep=EdgeStep(soft);
if(softStep>=hardStep*.6)throw new InvalidOperationException("Soft shadows did not smooth the edge.");
WritePng(Path.Combine(directory,"soft.png"),width,height,soft,false);WritePng(Path.Combine(directory,"hard.png"),width,height,hard,false);
Array.Fill(depths,.7f);Array.Fill(depths,.3f,0,size*size);
fixed(float* p=depths)gl.TexSubImage3D(GLEnum.Texture2DArray,0,0,0,0,size,size,4,PixelFormat.DepthComponent,PixelType.Float,p);
var blended=Draw(ground,cascade:true);var blendStep=EdgeStep(blended);
if(blendStep>.006||blended[0]>.181||blended[(width-1)*4]<.999)
throw new InvalidOperationException($"Shadow cascade transition is discontinuous: {blendStep}.");
if(Draw(ground,enabled:false).Any(v=>System.Math.Abs(v-1)>.0001)||gl.GetError()!=GLEnum.NoError)
throw new InvalidOperationException("Disabled shadows or shadow sampler state is invalid.");
WritePng(Path.Combine(directory,"cascades.png"),width,height,blended,false);
Console.WriteLine($"Shadow GPU test passed: ground/foliage/two-sun reference errors {groundError:G3}/{leafError:G3}/{secondSunError:G3}; edge step {hardStep:G3} -> {softStep:G3}; cascade step {blendStep:G3}.");
}
finally {gl.DeleteTexture(texture);gl.BindFramebuffer(GLEnum.Framebuffer,0);gl.Enable(GLEnum.DepthTest);gl.DepthMask(true);}
}
}

View File

@@ -11,8 +11,107 @@ using Universe.App.Runtime;
namespace Universe.App.Graphics;
/// <summary>Offscreen tests of the production shaders; no desktop capture or mouse control.</summary>
internal static class GraphicsReport
internal static partial class GraphicsReport
{
public static void RunVegetation(GL gl,UniverseRenderer renderer,FlyCamera camera,UniverseNeighborhood world,int width,int height)
{
var dir=Path.Combine(AppContext.BaseDirectory,"graphics-report","vegetation");Directory.CreateDirectory(dir);
var planet=world.ActiveSystem.Planets.Single(p=>p.Id=="earth");
var origin=UniversePosition.AtCellCenter(world.ActiveSystem.Coordinate,camera.CellSizeMeters);
var body=origin.Add(planet.PositionMeters,camera.CellSizeMeters);
var field=new BiomeField(planet,renderer.Vegetation.Catalog);
var sun=(world.ActiveSystem.Stars[planet.HostStarIndex].PositionMeters-planet.PositionMeters).Normalized();
var direction=VegetationSiteFinder.FindForest(field,planet.Orientation.ToBody(sun));
var radial=planet.Orientation.ToWorld(direction);var tangent=DVector3.Cross(planet.Orientation.ToWorld(new(0,1,0)),radial).Normalized();
var ground=TerrainHeight.Sample(planet,radial);var home=camera.Position;var homeForward=camera.Forward;var homeUp=camera.Up;
void Pose(double height,double pitch=0)
{camera.SetPlayerPose(body.Add(radial*(ground+height),camera.CellSizeMeters),(tangent+radial*pitch).Normalized(),radial,true);}
Pose(1.8,-.03);renderer.SimulationTimeSeconds=12;renderer.BenchmarkProfiling=true;
var load=System.Diagnostics.Stopwatch.StartNew();var frames=0;var settledAt=0d;
while(load.Elapsed.TotalSeconds<120)
{
renderer.Render(camera,world,1d/60);frames++;
if(frames%180==0)Console.WriteLine($"Vegetation warmup {load.Elapsed.TotalSeconds:F1}s: models {renderer.Vegetation.ReadyModels}, instances {renderer.Vegetation.Stream?.InstanceCount}, tiles {renderer.Vegetation.Stream?.Tiles.Count()}, pending {renderer.Vegetation.Stream?.PendingTiles}.");
if(renderer.Vegetation.ReadyModels>=renderer.Vegetation.RequiredModels
&&renderer.Vegetation.DistantInstances>1000&&renderer.TerrainPendingTiles==0&&renderer.TerrainUploads==0&&renderer.Vegetation.Stream?.PendingTiles==0)
{if(settledAt==0)settledAt=load.Elapsed.TotalSeconds;else if(load.Elapsed.TotalSeconds-settledAt>.5&&load.Elapsed.TotalSeconds>12)break;}
else settledAt=0;
Thread.Sleep(1);
}
if(renderer.Vegetation.ReadyModels<renderer.Vegetation.RequiredModels||renderer.Vegetation.Stream?.InstanceCount<100)
throw new InvalidOperationException("Vegetation did not finish streaming valid models/instances.");
// Compile/warm the unoccluded terrain variant as part of initial resource loading too.
// Cold/warm below refers to vegetation tiles, not a first-use driver shader compilation.
renderer.Vegetation.Enabled=false;
camera.SetPlayerPose(body.Add(radial*(planet.RadiusMeters*4.8),camera.CellSizeMeters),-radial,DVector3.Cross(radial,tangent),true);
for(var i=0;i<48;i++){renderer.Render(camera,world,1d/60);gl.Finish();}
Pose(1.8,-.03);var stable=0;
for(var i=0;i<1200&&stable<60;i++){renderer.Render(camera,world,1d/60);stable=renderer.TerrainPendingTiles==0&&renderer.TerrainUploads==0?stable+1:0;}
gl.Finish();renderer.Vegetation.Enabled=true;
var sourceLoadSeconds=load.Elapsed.TotalSeconds;
void Capture(string name)
{
renderer.Render(camera,world,1d/60);gl.Finish();
if(gl.GetError()!=GLEnum.NoError)throw new InvalidOperationException("Vegetation OpenGL error.");
var pixels=Read(gl,width,height);if(pixels.Any(p=>!float.IsFinite(p)))throw new InvalidOperationException("Vegetation produced non-finite output.");
WritePng(Path.Combine(dir,name),width,height,pixels,false);
}
Capture("forest-ground.png");
Pose(1.8,-.65);for(var i=0;i<120;i++)renderer.Render(camera,world,1d/60);gl.Finish();
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.Take(width*height/4).Count(d=>float.IsFinite(d)&&d<threshold)/(double)(width*height/4);
Console.WriteLine($"Ground coverage: {covered:P2} within 100 m.");
if(covered<.99)throw new InvalidOperationException("Missing terrain near the player's feet after an orbital trip.");
Pose(1.8,-.03);
var secondSun=world.ActiveSystem.Stars[0] with {Index=1,Id="forest-light-test",PositionMeters=planet.PositionMeters+(radial+tangent*.6).Normalized()*UniverseScale.AstronomicalUnitMeters,Color=new(.7f,.8f,1),LuminositySolar=.45};
renderer.Render(camera,world with {ActiveSystem=world.ActiveSystem with {Stars=[world.ActiveSystem.Stars[0],secondSun]}},1d/60);gl.Finish();
if(gl.GetError()!=GLEnum.NoError)throw new InvalidOperationException("Two-sun vegetation shadow GL error.");
WritePng(Path.Combine(dir,"forest-two-suns.png"),width,height,Read(gl,width,height),false);
renderer.Render(camera,world,1d/60);
renderer.DebugMode=true;renderer.DrawDebugOverlay(60);gl.Finish();WritePng(Path.Combine(dir,"forest-debug.png"),width,height,Read(gl,width,height),false);renderer.DebugMode=false;
Pose(80,-.35);for(var i=0;i<30;i++)renderer.Render(camera,world,1d/60);Capture("forest-canopy.png");
Pose(1.8,-.03);
renderer.ProfileFrame=true;renderer.Render(camera,world,0);renderer.ProfileFrame=false;
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(renderer.FrameTimings));
if(Environment.GetCommandLineArgs().Contains("--preview-only"))return;
var samples=new List<object>();var allPass=true;
foreach(var enabled in new[]{false,true})foreach(var cold in new[]{true,false})
{
renderer.Vegetation.Enabled=enabled;if(cold)renderer.Vegetation.ClearTiles();
// Identical camera poses in all A/B runs; trunk collision is verified separately.
var benchmark=new ForestBenchmark(world.ActiveSystem,planet,renderer.Vegetation.Catalog,camera,home,homeForward,homeUp);
var runName=$"{(enabled?"on":"off")}-{(cold?"cold":"warm")}";
var measured=new List<double>();var vegetation=new List<double>();var shadows=new List<double>();
var watch=System.Diagnostics.Stopwatch.StartNew();
while(benchmark.IsRunning)
{
benchmark.Update(1d/30,camera);
renderer.SimulationTimeSeconds=12+benchmark.ElapsedSeconds;
var start=System.Diagnostics.Stopwatch.GetTimestamp();renderer.Render(camera,world,1d/30);gl.Finish();
var seconds=System.Diagnostics.Stopwatch.GetElapsedTime(start).TotalSeconds;
benchmark.Metrics(renderer.Vegetation.CpuMilliseconds,renderer.Vegetation.UploadMilliseconds,renderer.VegetationGpuMilliseconds,renderer.VegetationShadowMilliseconds,
renderer.Vegetation.Instances,renderer.Vegetation.Triangles,renderer.Vegetation.Stream?.Tiles.Count()??0,renderer.Vegetation.Stream?.PendingTiles??0,renderer.Vegetation.ReadyModels,renderer.Vegetation.LodCounts,renderer.Vegetation.Stream?.GenerationMilliseconds??0);
benchmark.RecordFrame(seconds);measured.Add(seconds*1000);
vegetation.Add(renderer.VegetationGpuMilliseconds);shadows.Add(renderer.VegetationShadowMilliseconds);
if(measured.Count%720==0)Console.WriteLine($"Forest {runName}: {benchmark.ElapsedSeconds:F0}/{benchmark.DurationSeconds:F0}s, {benchmark.AverageFps:F1} FPS, {renderer.Vegetation.Instances} instances.");
}
benchmark.SaveReport(Path.Combine(dir,runName));
renderer.DrawBenchmarkOverlay(benchmark);gl.Finish();
WritePng(Path.Combine(dir,runName,"fps-graph.png"),width,height,Read(gl,width,height),false);
var slowest=measured.Max();var gpu=vegetation.Zip(shadows,(a,b)=>a+b).Order().ToArray();var gpu99=gpu[(int)((gpu.Length-1)*.99)];
var pass=benchmark.AverageFps>=60&&benchmark.OnePercentLowFps>=55&&slowest<=50&&(!enabled||gpu99<=5);allPass&=pass;
samples.Add(new{Name=runName,benchmark.AverageFps,benchmark.OnePercentLowFps,MaximumFrameMilliseconds=slowest,VegetationGpuP99Milliseconds=gpu99,Passed=pass,Seconds=watch.Elapsed.TotalSeconds});
Console.WriteLine($"Forest {runName}: {benchmark.AverageFps:F1} FPS, 1% {benchmark.OnePercentLowFps:F1}, max {slowest:F2} ms, vegetation GPU p99 {gpu99:F2} ms; {(pass?"PASS":"FAIL")}.");
}
renderer.Vegetation.Enabled=true;renderer.ProfileFrame=true;Pose(1.8,-.03);renderer.Render(camera,world,0);renderer.ProfileFrame=false;
File.WriteAllText(Path.Combine(dir,"summary.json"),System.Text.Json.JsonSerializer.Serialize(new{Width=width,Height=height,Gpu=renderer.HardwareName,SourceLoadSeconds=sourceLoadSeconds,Passed=allPass,Runs=samples,FrameTimings=renderer.FrameTimings},new System.Text.Json.JsonSerializerOptions{WriteIndented=true}));
Capture("forest-final.png");Console.WriteLine("Vegetation report: "+dir);
if(!allPass)Environment.ExitCode=1;
}
public static void RunSimulation(GL gl, UniverseRenderer renderer, UniverseNeighborhood initial, double cellSize, int width, int height)
{
var directory = Path.Combine(AppContext.BaseDirectory, "graphics-report"); Directory.CreateDirectory(directory);
@@ -227,7 +326,7 @@ internal static class GraphicsReport
var dir = Path.Combine(AppContext.BaseDirectory, "graphics-report"); Directory.CreateDirectory(dir);
WritePng(Path.Combine(dir, "pause-menu.png"), width, height, pixels, false);
var scale = System.Math.Min(width / 1440d, height / 900d);
if (Enumerable.Range(0,7).Any(i => renderer.MenuHit(width*.5,height*.5+(-182+i*57)*scale,width,height)!=i)
if (Enumerable.Range(0,PauseMenu.Count).Any(i => renderer.MenuHit(width*.5,height*.5+(-182+i*57)*scale,width,height)!=i)
|| renderer.MenuHit(0, 0, width, height) != -1)
throw new InvalidOperationException("Menu hit testing failed.");
Console.WriteLine($"Pause menu rendered and hit testing passed. Image: {dir}");
@@ -394,8 +493,10 @@ internal static class GraphicsReport
var withoutReflections=Snapshot(WithoutSsr(neighborhood),"earth-ocean-reflections-off.png");
var reflectionDifference=withReflections.Zip(withoutReflections,(a,b)=>System.Math.Abs(a-b)).Average();
if(reflectionDifference<.0001)throw new InvalidOperationException($"Water reflections did not change the production image: {reflectionDifference}.");
renderer.ProfileFrame=true;Snapshot(neighborhood,"earth-ocean-reflections-on.png");renderer.ProfileFrame=false;
renderer.ProfileFrame=true;
for(var i=0;i<8;i++)renderer.Render(camera,neighborhood,0);
var reflectionMs=renderer.FrameTimings.GetValueOrDefault("water reflections");
renderer.ProfileFrame=false;
var earth=neighborhood.ActiveSystem.Planets.Single(p=>p.Id=="earth");
var up=(camera.Position.RelativeTo(origin,cellSize)-earth.PositionMeters).Normalized();
var night=neighborhood with { ActiveSystem=neighborhood.ActiveSystem with
@@ -405,6 +506,20 @@ internal static class GraphicsReport
var nightOff=Snapshot(WithoutSsr(night),"earth-ocean-night-reflections-off.png");
var nightDifference=nightOn.Zip(nightOff,(a,b)=>System.Math.Abs(a-b)).Average();
if(nightDifference<.00001)throw new InvalidOperationException("The night sky did not reflect in the water.");
var oceanPosition=camera.Position; var oceanForward=camera.Forward; var oceanUp=camera.Up;
var seaBody=origin.Add(earth.PositionMeters,cellSize);
var tangent=(oceanForward-up*DVector3.Dot(oceanForward,up)).Normalized();
var side=DVector3.Cross(up,tangent).Normalized();
var surveyHeading=(tangent*.35+side*.94).Normalized();
foreach(var altitude in new[]{10d,100d,1000d,10000d})
{
var pitch=altitude<100?.12:altitude<1000?.18:altitude<10000?.28:.5;
camera.SetPlayerPose(seaBody.Add(up*(earth.RadiusMeters+earth.Ocean!.SeaLevelMeters+altitude),cellSize),
(surveyHeading-up*pitch).Normalized(),up,resetMotionHistory:true);
Snapshot(neighborhood,$"earth-ocean-{altitude:0}m.png");
}
ValidateOceanDetail(gl,renderer,camera,neighborhood,earth,seaBody,up,surveyHeading,width,height,directory);
camera.SetPlayerPose(oceanPosition,oceanForward,oceanUp,resetMotionHistory:true);
renderer.SimulationTimeSeconds = null;
for (var i = 0; i < 18; i++) camera.Update(.1, -camera.Up, neighborhood);
Capture("earth-underwater.png");
@@ -436,7 +551,7 @@ internal static class GraphicsReport
float t=object?wall:1e8;
color=vec4(.08,.2,.4,1);normal=vec4(0);fallback=vec4(0);
if(object)color=vec4(2,.04,.02,1);
if(water<t){t=water;color=vec4(.03,.07,.12,1);normal=vec4(0,.7,0,1);fallback=vec4(.03,.07,.12,1);}
if(water<t){t=water;color=vec4(.03,.07,.12,1);normal=vec4(0,.7,0,1);fallback=vec4(.03,.07,.12,.08);}
gl_FragDepth=t<1e7?log2(1.0-t*ray.z)/log2(100001.0):1.0;
}
""");
@@ -448,6 +563,7 @@ internal static class GraphicsReport
void Bind(int unit,uint texture){gl.ActiveTexture((GLEnum)((int)GLEnum.Texture0+unit));gl.BindTexture(GLEnum.Texture2D,texture);}
reflection.Bind();gl.Disable(GLEnum.DepthTest);gl.DepthMask(false);trace.Use();
Bind(0,source.Color);Bind(1,source.Depth);Bind(2,source.AuxiliaryColors[0]);
Bind(3,source.AuxiliaryColors[1]);trace.Set("uWaterFallback",3);
trace.Set("uScene",0);trace.Set("uDepth",1);trace.Set("uWaterNormals",2);trace.Set("uFar",far);
trace.Set("uForward",new Vector3(0,0,-1));trace.Set("uRight",Vector3.UnitX);trace.Set("uUp",Vector3.UnitY);
trace.Set("uAspect",(float)width/height);trace.Set("uTanHalfFov",.67f);

View File

@@ -8,7 +8,8 @@ internal sealed class PauseMenu : IDisposable
private readonly GpuMesh _mesh;
public int Selected { get; set; }
public PauseMenu(GL gl) => _mesh = new(gl, 6, [], dynamic: true);
private static readonly string[] Labels = ["PLAY", "FREE CAMERA", "DEBUG OFF", "BENCHMARK EARTH", "BENCHMARK UNKNOWN", "GALAXY JOURNEY", "QUIT"];
private static readonly string[] Labels = ["PLAY", "FREE CAMERA", "DEBUG OFF", "BENCHMARK EARTH", "BENCHMARK UNKNOWN", "GALAXY JOURNEY", "BENCHMARK FOREST", "QUIT"];
public static int Count=>Labels.Length;
internal static readonly Dictionary<char, string[]> Font = new()
{
['A']=["01110","10001","10001","11111","10001","10001","10001"], ['C']=["01111","10000","10000","10000","10000","10000","01111"],

View File

@@ -11,7 +11,7 @@ internal sealed class PerformanceOverlay : IDisposable
public void Draw(int width, int height, IReadOnlyList<KeyValuePair<string, double>> samples,
double cpuMilliseconds, double fps, int terrainTiles, int terrainLod, int pendingTiles, string? probePlanetId,
double timeScale = 1, DateTimeOffset? simulationUtc = null)
double timeScale = 1, DateTimeOffset? simulationUtc = null,VegetationRenderer? vegetation=null)
{
var values = samples.ToDictionary(p => p.Key, p => p.Value, StringComparer.Ordinal);
var rows = new List<(string Label, string Value, bool Emphasis)>();
@@ -19,6 +19,9 @@ internal sealed class PerformanceOverlay : IDisposable
AddMetric("SKY / STARFIELD", "SKY / STARFIELD");
AddMetric("GALAXIES / NEBULAE", "GALAXIES / NEBULAE");
AddMetric("PLANETS TOTAL", "PLANETS TOTAL");
AddMetric("VEGETATION", "VEGETATION");
AddMetric("VEGETATION DEPTH", "VEGETATION DEPTH");
AddMetric("VEGETATION SHADOWS", "VEGETATION SHADOWS");
foreach (var key in values.Keys.Where(k => k.StartsWith("PLANET ", StringComparison.Ordinal) && k != "PLANETS TOTAL" && k != "GPU FRAME")
.OrderBy(k => k, StringComparer.Ordinal))
AddMetric(key, key);
@@ -46,6 +49,16 @@ internal sealed class PerformanceOverlay : IDisposable
rows.Add(("FPS", $"{Number(fps, "F1")}", false));
rows.Add(("TERRAIN TILES", terrainTiles.ToString(CultureInfo.InvariantCulture), false));
rows.Add(("LOD / PENDING", $"{terrainLod} / {pendingTiles}", false));
if(vegetation is {} veg)
{
rows.Add(("VEG CPU / UPLOAD",$"{Number(veg.CpuMilliseconds)} / {Number(veg.UploadMilliseconds)} MS",false));
rows.Add(("VEG SUBMITTED / TRIS",$"{veg.Instances} / {veg.Triangles}",false));
rows.Add(("VEG WORKER RESULTS",$"{Number(veg.Stream?.GenerationMilliseconds??0)} MS",false));
rows.Add(("VEG LOD 0 / 1",$"{veg.LodCounts[0]} / {veg.LodCounts[1]}",false));
rows.Add(("VEG LOD 2 / BILLBOARDS",$"{veg.LodCounts[2]} / {veg.LodCounts[3]}",false));
rows.Add(("VEG TILES / PENDING",$"{veg.Stream?.Tiles.Count()??0} / {veg.Stream?.PendingTiles??0}",false));
rows.Add(("VEG MODELS / DRAWS",$"{veg.ReadyModels} / {veg.DrawCalls}",false));
}
rows.Add(("TIME SCALE", $"{Number(timeScale, "F1")} X", true));
if (simulationUtc is { } date) rows.Add(("WORLD UTC", date.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), false));

View File

@@ -10,6 +10,7 @@ internal sealed unsafe class PlanetTextures : IDisposable
public uint Height { get; }
public uint OceanMask { get; }
public uint RegionalHeight { get; }
public uint Biome {get;}
public PlanetTextures(GL gl, PlanetData planet)
{
@@ -22,6 +23,11 @@ internal sealed unsafe class PlanetTextures : IDisposable
gl.TexImage2D(GLEnum.Texture2D, 0, InternalFormat.Srgb8Alpha8, (uint)(image?.Width ?? 1), (uint)(image?.Height ?? 1), 0, PixelFormat.Rgba, PixelType.UnsignedByte, data);
SetParameters(gl, true);
gl.GenerateMipmap(GLEnum.Texture2D);
Biome=gl.GenTexture();gl.BindTexture(GLEnum.Texture2D,Biome);
var biome=planet.VegetationBiomeTexture??planet.SurfaceTexture;
fixed(byte* data=biome?.Rgba??[45,80,35,255])
gl.TexImage2D(GLEnum.Texture2D,0,InternalFormat.Rgba8,(uint)(biome?.Width??1),(uint)(biome?.Height??1),0,PixelFormat.Rgba,PixelType.UnsignedByte,data);
SetParameters(gl,true);gl.GenerateMipmap(GLEnum.Texture2D);
Height = gl.GenTexture();
gl.BindTexture(GLEnum.Texture2D, Height);
var map = planet.Terrain.HeightMap;
@@ -54,5 +60,5 @@ internal sealed unsafe class PlanetTextures : IDisposable
gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureWrapT, (int)GLEnum.ClampToEdge);
}
public void Dispose() { _gl.DeleteTexture(Color); _gl.DeleteTexture(Height); _gl.DeleteTexture(OceanMask); _gl.DeleteTexture(RegionalHeight); }
public void Dispose() { _gl.DeleteTexture(Color); _gl.DeleteTexture(Biome); _gl.DeleteTexture(Height); _gl.DeleteTexture(OceanMask); _gl.DeleteTexture(RegionalHeight); }
}

View File

@@ -0,0 +1,61 @@
using System.Numerics;
using Silk.NET.OpenGL;
namespace Universe.App.Graphics;
internal sealed class PlantGpuModel : IDisposable
{
public VegetationModelData Data {get;}
private readonly PlantTexture[][] _textures;
private readonly VegetationMesh[][][] _meshes;
public PlantGpuModel(VegetationModelData data,bool singleLod=false)
{
Data=data;_textures=new PlantTexture[data.Materials.Length][];
_meshes=data.Lods.Take(singleLod?1:3).Select(l=>l.Select(v=>new VegetationMesh[v.Primitives.Length]).ToArray()).ToArray();
}
public IEnumerable<Action> Upload(GL gl)
{
for(var i=0;i<_textures.Length;i++)
{
var index=i;var material=Data.Materials[i];
_textures[i]=new PlantTexture[3];
_textures[index][0]=new(gl,material.Color,true,material.Foliage,true);
foreach(var step in _textures[index][0].UploadSteps())yield return step;
_textures[index][1]=new(gl,material.Normal,false,deferred:true);
foreach(var step in _textures[index][1].UploadSteps())yield return step;
_textures[index][2]=new(gl,material.Arm,false,deferred:true);
foreach(var step in _textures[index][2].UploadSteps())yield return step;
}
for(var l=0;l<_meshes.Length;l++)for(var v=0;v<_meshes[l].Length;v++)for(var p=0;p<_meshes[l][v].Length;p++)
{
var lod=l;var variant=v;var primitive=p;
yield return ()=>_meshes[lod][variant][primitive]=new(gl,Data.Lods[lod][variant].Primitives[primitive]);
}
}
public int Draw(GL gl,ShaderProgram program,int lod,int variant,uint instances,int count,bool shadow=false,bool canopyOnly=false)
{
var primitives=Data.Lods[lod][variant].Primitives;var triangles=0;
program.Set("uHeight",System.Math.Max(.1f,Data.Lods[lod][variant].Maximum.Y));
program.Set("uDetailedLighting",lod==0?1:0);
for(var i=0;i<primitives.Length;i++)
{
var material=Data.Materials[primitives[i].Material];var textures=_textures[primitives[i].Material];
if(canopyOnly&&!material.Name.Contains("twig")&&!material.Name.Contains("leaves"))continue;
gl.ActiveTexture(GLEnum.Texture0);gl.BindTexture(GLEnum.Texture2D,textures[0].Handle);
program.Set("uColor",0);program.Set("uAlphaCutoff",material.AlphaCutoff);
if(!shadow)
{
gl.ActiveTexture(GLEnum.Texture1);gl.BindTexture(GLEnum.Texture2D,textures[1].Handle);
gl.ActiveTexture(GLEnum.Texture2);gl.BindTexture(GLEnum.Texture2D,textures[2].Handle);
program.Set("uNormal",1);program.Set("uArm",2);program.Set("uFactor",material.Factor);program.Set("uFoliage",material.Foliage?1:0);
}
_meshes[lod][variant][i].Draw(instances,count);triangles+=_meshes[lod][variant][i].IndexCount/3*count;
}
return triangles;
}
public void Dispose()
{
foreach(var textures in _textures)if(textures is not null)foreach(var t in textures)t?.Dispose();
foreach(var lod in _meshes)foreach(var variant in lod)foreach(var p in variant)p?.Dispose();
}
}

View File

@@ -0,0 +1,27 @@
using System.Buffers.Binary;
using System.IO.Compression;
using System.Text;
namespace Universe.App.Graphics;
internal static class PngOutput
{
public static void Rgba(string path,int width,int height,float[] pixels)
{
using var output=File.Create(path);output.Write(new byte[]{137,80,78,71,13,10,26,10});
var header=new byte[13];BinaryPrimitives.WriteInt32BigEndian(header,width);BinaryPrimitives.WriteInt32BigEndian(header.AsSpan(4),height);header[8]=8;header[9]=6;Chunk(output,"IHDR",header);
using var compressed=new MemoryStream();
using(var z=new ZLibStream(compressed,CompressionLevel.Fastest,true))
for(var y=height-1;y>=0;y--)
{
z.WriteByte(0);for(var x=0;x<width;x++)for(var c=0;c<4;c++)z.WriteByte((byte)(System.Math.Clamp(pixels[(y*width+x)*4+c],0,1)*255+.5));
}
Chunk(output,"IDAT",compressed.ToArray());Chunk(output,"IEND",[]);
}
private static void Chunk(Stream output,string type,byte[] data)
{
var name=Encoding.ASCII.GetBytes(type);var length=new byte[4];BinaryPrimitives.WriteInt32BigEndian(length,data.Length);output.Write(length);output.Write(name);output.Write(data);
uint crc=0xffffffff;foreach(var b in name.Concat(data)){crc^=b;for(var i=0;i<8;i++)crc=(crc&1)!=0?(crc>>1)^0xedb88320:crc>>1;}
BinaryPrimitives.WriteUInt32BigEndian(length,~crc);output.Write(length);
}
}

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" })
foreach (var include in new[] { "noise", "planet-height", "camera-ray", "ring-optics", "rock-common", "asteroid-shape", "cloud-common", "screen-space-reflection", "vegetation-shadow", "biomes" })
{
var directive = $"#include \"{include}.glsl\"";
if (source.Contains(directive, StringComparison.Ordinal))

View File

@@ -0,0 +1,31 @@
uniform int uBiomesEnabled;
uniform int uBiomeHasTexture;
uniform sampler2D uBiomeTexture;
uniform sampler2DArray uBiomeColors;
uniform vec3 uBiomeTargets[4];
uniform float uBiomeTolerances[4];
uniform float uForestScale;
vec4 biomeWeights(vec3 direction){
vec3 color;
if(uBiomeHasTexture!=0)color=texture(uBiomeTexture,planetUv(direction)).rgb;
else {
float n=noise3(direction*5.0+uNoiseOffset);
color=abs(direction.y)>.91?vec3(235,238,240)/255.0:n<-.2&&abs(direction.y)<.6?vec3(205,181,120)/255.0:n<.1?vec3(145,150,80)/255.0:vec3(45,80,35)/255.0;
}
vec4 weights=vec4(0);
for(int i=0;i<4;i++){
float d=length(color-uBiomeTargets[i])*57.7350269;
float t=uBiomeTolerances[i];weights[i]=t<=0.0?(d<.00001?1.0:0.0):1.0-smoothstep(t*.8,t,d);
}
return weights/max(1.0,dot(weights,vec4(1)));
}
vec4 biomeTriColor(float layer,vec3 p,vec3 weights){
vec4 color=texture(uBiomeColors,vec3(p.yz,layer))*weights.x+texture(uBiomeColors,vec3(p.xz,layer))*weights.y+texture(uBiomeColors,vec3(p.xy,layer))*weights.z;
color.rgb=pow(color.rgb,vec3(2.2));return color;
}
vec3 biomeTriNormal(float layer,vec3 p,vec3 n,vec3 weights){
vec3 x=texture(uBiomeColors,vec3(p.yz,layer+4.0)).xyz*2.0-1.0;
vec3 y=texture(uBiomeColors,vec3(p.xz,layer+4.0)).xyz*2.0-1.0;
vec3 z=texture(uBiomeColors,vec3(p.xy,layer+4.0)).xyz*2.0-1.0;
return normalize(vec3(sign(n.x)*x.z,x.x,x.y)*weights.x+vec3(y.x,sign(n.y)*y.z,y.y)*weights.y+vec3(z.x,z.y,sign(n.z)*z.z)*weights.z);
}

View File

@@ -92,7 +92,7 @@ float cloudShadow(vec3 worldPoint,vec3 worldLight) {
for(int i=0;i<6;i++)optical+=cloudDensity(ro+rd*(start+(float(i)+.5)*ds),lod,false)*ds;
return mix(1.0,exp(-min(20.0,optical*uCloudOptics.y)),uCloudOptics.z);
}
vec3 cloudReflection(vec3 worldPoint,vec3 worldDirection,vec3 sky) {
vec3 cloudReflection(vec3 worldPoint,vec3 worldDirection,vec3 sky,float coneAngle) {
if(uCloudEnabled==0)return sky;
vec3 ro=uCloudCamera+cloudToLocal(worldPoint),rd=cloudToLocal(worldDirection);
vec2 outer=cloudSphere(ro,rd,uCloudLayer.x+uCloudLayer.y),inner=cloudSphere(ro,rd,uCloudLayer.x);
@@ -104,7 +104,9 @@ vec3 cloudReflection(vec3 worldPoint,vec3 worldDirection,vec3 sky) {
vec3 light=vec3(0.0);
for(int i=0;i<10;i++) {
vec3 p=ro+rd*(start+(float(i)+.5)*ds);
float lod=clamp(log2(max(1.0,(length(worldPoint)+start+(float(i)+.5)*ds)*uCloudPixelAngle*128.0/uCloudLayer.z)),1.0,7.0);
float travel=start+(float(i)+.5)*ds;
float footprint=max((length(worldPoint)+travel)*uCloudPixelAngle,travel*coneAngle);
float lod=clamp(log2(max(1.0,footprint*128.0/uCloudLayer.z)),1.0,7.0);
float a=1.0-exp(-cloudDensity(p,lod,false)*ds*uCloudOptics.y);
float day=smoothstep(-.05,.12,dot(normalize(p+vec3(0,uCloudPlanetRadius,0)),uCloudPrimarySun));
vec3 color=(vec3(.15,.2,.3)*.2+uCloudPrimaryColor*cloudSunTint(p,uCloudPrimarySun)*.8)*day+vec3(.001);

View File

@@ -2,6 +2,7 @@
#include "camera-ray.glsl"
#include "screen-space-reflection.glsl"
in vec2 vUv;
uniform sampler2D uWaterFallback; // alpha contains the pixel's integrated water roughness
out vec4 fragColor;
void main() {
fragColor=vec4(0);
@@ -10,7 +11,8 @@ void main() {
vec3 ray=cameraRay(vUv);
float distance=linearDepth(vUv)/max(.001,dot(ray,uForward));
if(distance<=.03 || distance>uMaxSurfaceDistance)return;
vec4 hit=traceReflection(ray*distance,normalize(surface.rgb));
float roughness=max(uReflectionSettings.z,textureLod(uWaterFallback,vUv,0).a);
vec4 hit=traceReflection(ray*distance,normalize(surface.rgb),roughness);
float fade=1.0-smoothstep(uMaxSurfaceDistance*.6,uMaxSurfaceDistance,distance);
fade*=1.0-smoothstep(.6,1.0,uReflectionSettings.z);
fragColor=hit*(fade*uReflectionStrength);

View File

@@ -12,11 +12,12 @@ uniform float uCameraHeight;
uniform vec3 uAxisX;
uniform vec3 uAxisY;
uniform vec3 uAxisZ;
uniform vec3 uWaveVectors[12];
uniform float uWavePhases[12];
uniform float uWaveAmplitudes[12];
uniform vec3 uDetailOrigins[2];
uniform vec3 uWaveVectors[32];
uniform float uWavePhases[32];
uniform float uWaveAmplitudes[32];
uniform vec3 uDetailOrigins[3];
uniform vec3 uWaveSettings; // maximum displacement, choppiness, pixel angular size
uniform vec3 uSurfaceDetail; // ripple strength, geometry range (metres), whitecap threshold
uniform vec3 uWaterOptics; // roughness, reflection, refraction
uniform vec3 uDeepColor;
uniform vec3 uShallowColor;
@@ -49,43 +50,64 @@ vec2 oceanSphere(vec3 ro,vec3 rd,float expansion) {
float radialHeight(vec3 p) {
return (dot(p,p)+2.0*uSeaRadius*p.y)/(length(p+vec3(0,uSeaRadius,0))+uSeaRadius);
}
float wavePhase(int i,vec3 p,out vec3 phaseGradient) {
int partner=(i/4)*4+(i+1)%4;
float crossing=dot(p,uWaveVectors[partner])+uWavePhases[partner];
// Crossing waves bend and break up the crests without a tiled height texture.
phaseGradient=uWaveVectors[i]+uWaveVectors[partner]*(.55*cos(crossing));
return dot(p,uWaveVectors[i])+uWavePhases[i]+.55*sin(crossing);
}
float waveHeight(vec3 p,float footprint) {
float height=0.0;
for(int i=0;i<8;i++) {
float weight=1.0-smoothstep(.35,1.0,length(uWaveVectors[i])*footprint);
float base=dot(p,uWaveVectors[i])+uWavePhases[i];
for(int i=0;i<16;i++) {
float width=length(uWaveVectors[i])*footprint;
float weight=exp(-.5*width*width);
vec3 phaseGradient;
float base=wavePhase(i,p,phaseGradient);
float phase=base;
for(int j=0;j<2;j++)phase=base-uWaveSettings.y*.48*cos(phase);
height+=sin(phase)*uWaveAmplitudes[i]*weight;
}
return height;
}
vec3 waveNormal(vec3 p,float footprint,out float compression) {
vec3 waveNormal(vec3 p,vec3 dx,vec3 dy,float footprint,out float compression,out float variance) {
vec3 gradient=vec3(0); compression=0.0;
float total=0.0;
for(int i=0;i<12;i++) {
float weight=1.0-smoothstep(.35,1.0,length(uWaveVectors[i])*footprint);
float base=dot(p,uWaveVectors[i])+uWavePhases[i], phase=base;
// Waves below the smallest normal band still contribute microscopic surface roughness.
variance=.004*uSurfaceDetail.x*uSurfaceDetail.x;
vec3 up=normalize(p+vec3(0,uSeaRadius,0));
for(int i=0;i<32;i++) {
// Integrate each direction over the actual pixel footprint, including grazing views.
vec3 phaseGradient;
float base=wavePhase(i,p,phaseGradient);
float width=length(vec2(dot(phaseGradient,dx),dot(phaseGradient,dy)));
float weight=exp(-.5*width*width);
float phase=base;
float q=uWaveSettings.y*.48;
for(int j=0;j<3;j++)phase=base-q*cos(phase);
float a=uWaveAmplitudes[i]*weight;
gradient+=uWaveVectors[i]*(a*cos(phase)/max(.15,1.0-q*sin(phase)));
compression+=max(0.0,sin(phase))*a;
total+=a;
gradient+=phaseGradient*(a*cos(phase)/max(.15,1.0-q*sin(phase)));
vec3 tangent=uWaveVectors[i]-up*dot(up,uWaveVectors[i]);
float slope=uWaveAmplitudes[i]*length(tangent);
// Unresolved waves become BRDF roughness instead of disappearing into a flat mirror.
variance+=.5*slope*slope*(1.0-weight*weight);
compression+=sin(phase)*slope*weight*uWaveSettings.y;
}
compression/=max(.001,total);
vec3 up=normalize(p+vec3(0,uSeaRadius,0));
gradient-=up*dot(gradient,up);
// Capillary ripples use a planet-anchored noise field and disappear below the pixel footprint.
vec3 bodyPoint=mat3(uWorldToBody)*toWorld(p);
vec3 bodyX=mat3(uWorldToBody)*uAxisX,bodyZ=mat3(uWorldToBody)*uAxisZ;
for(int detail=0;detail<2;detail++) {
float frequency=detail==0?.35:1.4;
float weight=1.0-smoothstep(.25,.8,footprint*frequency);
vec3 q=bodyPoint*frequency+uDetailOrigins[detail];
for(int detail=0;detail<3;detail++) {
float frequency=detail==0?.045:(detail==1?.35:1.4);
float width=footprint*frequency*2.0;
float weight=exp(-.5*width*width);
// Modulo origins on the CPU preserve centimetre detail without planet-sized floats.
vec3 drift=vec3(uTime*.018,0,uTime*.011)*sqrt(frequency);
vec3 q=bodyPoint*frequency+uDetailOrigins[detail]+drift;
float gx=(noise3(q+bodyX*.04)-noise3(q-bodyX*.04))/.08;
float gz=(noise3(q+bodyZ*.04)-noise3(q-bodyZ*.04))/.08;
gradient+=vec3(gx,0,gz)*weight*(detail==0?.13:.075);
float strength=(detail==0?.055:(detail==1?.14:.08))*uSurfaceDetail.x;
gradient+=vec3(gx,0,gz)*weight*strength;
variance+=strength*strength*.35*(1.0-weight*weight);
}
return normalize(up-gradient);
}
@@ -101,7 +123,7 @@ vec3 skyReflection(vec3 direction,vec3 up) {
return sky*(.006+daylight*1.35);
}
void main() {
// Premultiplied-alpha MRT blending preserves earlier ocean planets outside this one's silhouette.
// Misses discard, preserving earlier oceans and their reflection metadata.
reflectionNormal=vec4(0); reflectionFallback=vec4(0);
vec3 scene=texture(uScene,vUv).rgb;
float originalDepth=texture(uDepth,vUv).r;
@@ -114,7 +136,8 @@ void main() {
vec2 base=oceanSphere(ro,rd,0.0);
float t=base.x>0.0?base.x:base.y;
bool hit=envelope.y>0.0 && t>0.0;
if(hit && abs(uCameraHeight)<2500.0 && t<2000.0) {
float geometryRange=uSurfaceDetail.y;
if(hit && abs(uCameraHeight)<geometryRange && t<geometryRange) {
vec2 inner=oceanSphere(ro,rd,-displacement);
float low=max(.015,envelope.x),high=inner.x>0.0?inner.x:envelope.y;
if(underwater) {low=max(.015,inner.y);high=envelope.y;}
@@ -128,10 +151,14 @@ void main() {
if(height*reference>0.0)low=mid;else high=mid;
}
float solved=(low+high)*.5;
t=mix(solved,t,smoothstep(800.0,2000.0,t));
float geometry=1.0-smoothstep(geometryRange*.35,geometryRange,t);
geometry*=1.0-smoothstep(geometryRange*.5,geometryRange,abs(uCameraHeight));
t=mix(t,solved,geometry);
}
}
vec3 p=ro+rd*max(0.0,t);
// Evaluate derivatives before any divergent discard at the shoreline/horizon.
vec3 dx=dFdx(p),dy=dFdy(p);
vec3 radial=normalize(uAxisY+toWorld(p)/uSeaRadius);
float waterMask=uHasOceanMask!=0?oceanMaskValue(bodyDirection(radial)):1.0;
hit=hit && waterMask>.01 && t<opaque+.15;
@@ -140,25 +167,33 @@ void main() {
if(underwater && maskHere>.5) {
vec3 transmission=exp(-uAbsorption*min(opaque,400.0));
scene=scene*transmission+uShallowColor*(1.0-transmission)*.5;
fragColor=vec4(scene,1);return;
}
fragColor=vec4(scene,1);return;
discard;
}
float footprint=max(.02,t*uWaveSettings.z/max(.12,abs(dot(rd,normalize(p+vec3(0,uSeaRadius,0))))));
float compression;
vec3 n=waveNormal(p,footprint,compression), up=normalize(p+vec3(0,uSeaRadius,0));
float footprint=max(.02,max(length(dx),length(dy)));
float compression,variance;
vec3 n=waveNormal(p,dx,dy,footprint,compression,variance), up=normalize(p+vec3(0,uSeaRadius,0));
float alpha=clamp(sqrt(pow(uWaterOptics.x,4.0)+variance*.7),.001,1.0);
float roughness=clamp(sqrt(alpha),.04,1.0);
vec3 view=-rd;
if(dot(n,view)<0.0)n=-n;
float ndv=max(.02,dot(n,view));
float fresnel=.0204+.9796*pow(1.0-ndv,5.0);
// Average Fresnel over unresolved slopes, rather than applying the flat-surface grazing spike.
float environmentNdv=min(1.0,sqrt(ndv*ndv+2.0*variance));
float fresnel=.0204+(max(.0204,1.0-roughness*.65)-.0204)*pow(1.0-environmentNdv,5.0);
vec3 reflectionDirection=reflect(rd,n);
vec3 reflected=skyReflection(reflectionDirection,up);
reflected=cloudReflection(toWorld(p-ro),toWorld(reflectionDirection),reflected);
vec3 reflected=skyReflection(normalize(mix(reflectionDirection,up,roughness*.3)),up);
reflected=cloudReflection(toWorld(p-ro),toWorld(reflectionDirection),reflected,roughness*roughness);
vec2 refractUv=clamp(vUv+vec2(dot(toWorld(n),uRight),dot(toWorld(n),uUp))*uWaterOptics.z/(1.0+t*.012),vec2(.001),vec2(.999));
float floorDistance=sceneDistance(refractUv,cameraRay(refractUv));
float thickness=max(.0,min(400.0,floorDistance-t));
vec3 transmission=exp(-uAbsorption*thickness);
vec3 scatter=mix(uShallowColor,uDeepColor,1.0-exp(-thickness*.075));
vec3 lighting=vec3(.04), highlights=vec3(0);
float skyDaylight=0.0;
for(int l=0;l<2;l++){if(l>=uLightCount)break;skyDaylight+=sqrt(uLightFlux[l])*smoothstep(-.08,.16,dot(up,uLightDirections[l]));}
// Diffuse skylight keeps deep water blue even when a cloud blocks the direct sun.
vec3 lighting=vec3(.004)+vec3(.12,.2,.3)*skyDaylight, highlights=vec3(0);
for(int l=0;l<2;l++) {
if(l>=uLightCount)break;
vec3 light=uLightDirections[l];
@@ -168,21 +203,22 @@ void main() {
lighting+=uLightColors[l]*uLightFlux[l]*visible*(.25+.75*max(0.0,sunHeight));
vec3 halfVector=normalize(view+light);
float ndh=max(0.0,dot(n,halfVector));
float alpha=max(.045,uWaterOptics.x*uWaterOptics.x+min(.14,footprint*.001));
float a2=alpha*alpha, den=ndh*ndh*(a2-1.0)+1.0;
float brdfAlpha=max(.025,alpha);
float a2=brdfAlpha*brdfAlpha, den=ndh*ndh*(a2-1.0)+1.0;
float distribution=a2/(PI*den*den);
float k=(alpha+1.0)*(alpha+1.0)/8.0;
float k=(brdfAlpha+1.0)*(brdfAlpha+1.0)/8.0;
float geometry=ndv/(ndv*(1.0-k)+k)*ndl/(ndl*(1.0-k)+k);
float f=.0204+.9796*pow(1.0-max(0.0,dot(view,halfVector)),5.0);
highlights+=uLightColors[l]*uLightFlux[l]*visible*min(25.0,distribution*geometry*f/(4.0*ndv))*1.8;
scatter+=uShallowColor*pow(max(0.0,dot(view,-light)),4.0)*compression*.28*visible;
highlights+=uLightColors[l]*uLightFlux[l]*visible*min(12.0,distribution*geometry*f/(4.0*ndv));
scatter+=uShallowColor*pow(max(0.0,dot(view,-light)),4.0)*max(0.0,compression)*.28*visible;
}
vec3 refracted=texture(uScene,refractUv).rgb*transmission+scatter*(1.0-transmission)*lighting;
vec3 color=mix(refracted,reflected*uWaterOptics.y,fresnel)+highlights;
float shore=(1.0-smoothstep(.2,3.5,thickness))*(.5+.5*sin(uTime*1.3+t*.03));
vec3 bodyDetail=mat3(uWorldToBody)*toWorld(p)*.35+uDetailOrigins[0];
float breakup=noise3(bodyDetail+vec3(uTime*.09,0,uTime*.06))*.5+.5;
float foam=clamp((smoothstep(.54,.78,compression)+shore*.7)*uFoamStrength*smoothstep(.22,.72,breakup),0.0,.9);
vec3 bodyDetail=mat3(uWorldToBody)*toWorld(p)*.045+uDetailOrigins[0];
float breakup=noise3(bodyDetail+vec3(uTime*.025,0,uTime*.016))*.5+.5;
float breaking=smoothstep(uSurfaceDetail.z,uSurfaceDetail.z+.12,compression);
float foam=clamp((breaking*smoothstep(.25,.75,breakup)+shore*.7)*uFoamStrength,0.0,.9);
color=mix(color,vec3(.72,.86,.88)*lighting,foam);
if(underwater) {
vec3 through=exp(-uAbsorption*min(t,400.0));
@@ -191,6 +227,6 @@ void main() {
fragColor=vec4(max(color,vec3(0)),1);
float reflectance=underwater?0.0:fresnel*uWaterOptics.y*(1.0-foam);
reflectionNormal=vec4(toWorld(n)*reflectance,1);
reflectionFallback=vec4(reflected*reflectance,1);
reflectionFallback=vec4(reflected*reflectance,roughness);
gl_FragDepth=clamp(log2(1.0+t*max(.001,dot(worldRay,uForward)))/log2(uFar+1.0),0.0,1.0);
}

View File

@@ -32,9 +32,8 @@ float rasterValue(sampler2D raster,vec2 uv) {
}
float heightMapValue(vec3 localDirection) { return rasterValue(uHeightMap,planetUv(localDirection)); }
float oceanMaskValue(vec3 localDirection) { return rasterValue(uOceanMask,planetUv(localDirection)); }
float surfaceElevation(vec3 worldDirection) {
vec3 d=bodyDirection(worldDirection);
float mapValue=uHasHeightMap!=0 ? heightMapValue(d) : 0.0;
float mappedElevation(vec3 d,out float mapValue) {
mapValue=uHasHeightMap!=0 ? heightMapValue(d) : 0.0;
float mapped=uHasHeightMap!=0 ? mix(uHeightRange.x,uHeightRange.y,pow(mapValue,uHeightRange.z)) : 0.0;
if(uHasRegionalHeight!=0) {
// Avoid subtracting nearby global UVs: the regional tile is only one degree wide.
@@ -46,6 +45,18 @@ float surfaceElevation(vec3 worldDirection) {
mapped=mix(mapped,texture(uRegionalHeight,clamp(pos,0.0,1.0)).r/uRadius,smoothstep(0.0,uRegionOther.y,edge));
}
}
return mapped;
}
float surfaceElevationUpperBound(vec3 worldDirection){
vec3 d=bodyDirection(worldDirection);float mapValue;
float mapped=mappedElevation(d,mapValue);
float water=uHasOceanMask!=0?oceanMaskValue(d):0.0;
// The ridged terrain is bounded by 1.2 amplitudes, including its land mask.
return mix(mapped+1.2*uAmplitude,uOceanTerrain.x-uOceanTerrain.y,water*uOceanTerrain.z);
}
float surfaceElevation(vec3 worldDirection) {
vec3 d=bodyDirection(worldDirection);float mapValue;
float mapped=mappedElevation(d,mapValue);
float mask=(uHasHeightMap!=0 && uLandMask.x>0.5) ? smoothstep(uLandMask.y,uLandMask.y+uLandMask.z,mapValue) : 1.0;
float detail=uAmplitude>0.0 ? terrainHeight(d*uNoiseFrequency,uNoiseOffset,9)*uAmplitude*mask : 0.0;
float water=uHasOceanMask!=0 ? oceanMaskValue(d) : 0.0;

View File

@@ -18,15 +18,14 @@ float edgeConfidence(vec2 uv) {
float edge=min(min(uv.x,uv.y),min(1.0-uv.x,1.0-uv.y));
return smoothstep(0.0,uReflectionSettings.x,edge);
}
vec4 traceReflection(vec3 world,vec3 normal) {
vec4 traceReflection(vec3 world,vec3 normal,float roughness) {
vec3 reflection=reflect(normalize(world),normal);
vec3 origin=viewPoint(world+normal*uTraceSettings.z),direction=viewPoint(reflection);
if(origin.z<=.02)return vec4(0);
float distanceLimit=uTraceSettings.x;
if(direction.z<0.0)distanceLimit=min(distanceLimit,(.05-origin.z)/direction.z);
vec4 result=vec4(0);
float roughness=uReflectionSettings.z;
float mip=roughness*5.0;
float mip=clamp(log2(max(1.0,roughness*roughness*float(textureSize(uScene,0).y)*.25)),0.0,7.0);
// Infinite/distant sky lookup includes the actual stars, atmospheric colour and cloud texture.
// Opaque foreground pixels are never treated as sky or used to fill missing geometry.
if(uReflectSky!=0 && direction.z>.001) {

View File

@@ -2,6 +2,8 @@
#include "noise.glsl"
#include "planet-height.glsl"
#include "cloud-common.glsl"
#include "vegetation-shadow.glsl"
#include "biomes.glsl"
in vec3 vWorldPosition;
in vec3 vPlanetLocal;
in vec3 vNormal;
@@ -14,6 +16,8 @@ uniform sampler2D uSlopeColor;
uniform sampler2D uSlopeNormal;
uniform vec3 uMaterialSettings;
uniform float uFar;
uniform sampler2D uVegetationDepth;
uniform int uVegetationDepthCullEnabled;
uniform sampler2D uSurfaceTexture;
uniform int uHasSurfaceTexture;
uniform int uSurfaceType;
@@ -30,6 +34,7 @@ uniform int uOccluderCount;
uniform vec3 uOccluderCenters[16];
uniform float uOccluderRadii[16];
uniform int uTerrainShadows;
uniform float uMaximumTerrainRadius;
uniform int uDebugSkipSurfaceNoise;
uniform int uDebugDisableBodyShadows;
out vec4 fragColor;
@@ -52,6 +57,11 @@ float mountainShadow(vec3 p, vec3 lightDir, float sunHeight) {
float t=0.000055;
for(int i=0;i<6;i++) {
vec3 q=p+lightDir*t;
// Once an outward ray leaves the conservative terrain sphere it cannot hit terrain again.
if(length(q)>uMaximumTerrainRadius&&dot(q,lightDir)>0.0)break;
// Skip procedural octaves only when even the upper height bound yields full visibility.
float clearanceLower=length(q)-(1.0+surfaceElevationUpperBound(normalize(q)))+.000055;
if(18.0*clearanceLower>=t){t*=2.75;continue;}
float ground=1.0+surfaceElevation(normalize(q));
float clearance=length(q)-ground+0.000055;
visibility=min(visibility,clamp(18.0*clearance/t,0.0,1.0));
@@ -82,6 +92,7 @@ vec3 cloudMaterial(vec3 d) {
void main() {
if(uVegetationDepthCullEnabled!=0&&clamp(log2(max(.0001,vFragDepth))/log2(uFar+1.0),0.0,1.0)>texelFetch(uVegetationDepth,ivec2(gl_FragCoord.xy),0).r+.0000002)discard;
vec3 radial=normalize(vPlanetLocal);
vec3 localDirection=bodyDirection(radial);
vec3 textureColor=uHasSurfaceTexture!=0 ? texture(uSurfaceTexture,planetUv(localDirection)).rgb : vec3(0.0);
@@ -94,11 +105,13 @@ void main() {
// Small rock relief is filtered by the pixel footprint to avoid distant sparkling.
vec3 rockPosition=radial*18000.0+uNoiseOffset;
float grainFootprint=length(fwidth(rockPosition));
float materialCoverage=uMaterialsEnabled!=0?uMaterialSettings.z*(1.0-smoothstep(uMaterialSettings.y*.35,uMaterialSettings.y,length(vWorldPosition))):0.0;
float grain=0.0,erosion=0.0;
if(uDebugSkipSurfaceNoise==0) {
grain=noise3(rockPosition)*(1.0-smoothstep(0.55,1.0,grainFootprint))
+0.25*noise3(rockPosition*3.1)*(1.0-smoothstep(0.55,1.0,grainFootprint*3.1));
erosion=fbm(radial*2400.0+uNoiseOffset)*0.8;
// Detailed PBR ground already supplies small relief; avoid a second rock-noise layer over it.
if(materialCoverage<.5)erosion=fbm(radial*2400.0+uNoiseOffset)*0.8;
}
float detail=grain*0.10+erosion;
vec3 sigmaX=dFdx(vWorldPosition), sigmaY=dFdy(vWorldPosition);
@@ -111,7 +124,7 @@ void main() {
if(uSurfaceType>=2 || ocean>0.5)normal=radial;
float slope=clamp(1.0-dot(normal,radial),0.0,1.0);
float geology=0.0,deposits=0.5,strata=0.5;
if(uDebugSkipSurfaceNoise==0) {
if(uDebugSkipSurfaceNoise==0&&uHasSurfaceTexture==0) {
geology=fbm(radial*31.0+uNoiseOffset);
deposits=noise3(radial*170.0+uNoiseOffset);
strata=sin(height*150.0+noise3(radial*76.0)*2.2)*0.5+0.5;
@@ -127,17 +140,43 @@ void main() {
if(uHasSurfaceTexture!=0) { mineral=textureColor; ao=1.0; snow=0.0; }
if(uSurfaceType>=2) { mineral=cloudMaterial(localDirection); ao=1.0; snow=0.0; }
float materialRoughness=.8;
vec4 environment=uBiomesEnabled!=0?biomeWeights(localDirection):vec4(0);
if(uBiomesEnabled!=0&&uBiomeHasTexture==0){
vec3 biomeColor=vec3(0);for(int b=0;b<4;b++)biomeColor+=pow(uBiomeTargets[b],vec3(2.2))*environment[b];
float total=dot(environment,vec4(1));if(total>.001)mineral=mix(mineral,biomeColor/total,min(1.0,total));
}
if(uBiomesEnabled!=0&&ocean<.5){
vec3 q=localDirection*uRadius/uForestScale+uNoiseOffset;
float n=noise3(q)*.7+noise3(q*2.17)*.3;
float cover=smoothstep(-.26,.39,n);
mineral*=1.0-environment.x*cover*.16*smoothstep(350.0,1600.0,length(vWorldPosition));
}
if(uMaterialsEnabled!=0 && ocean<.5) {
float strength=uMaterialSettings.z*(1.0-smoothstep(uMaterialSettings.y*.35,uMaterialSettings.y,length(vWorldPosition)));
float strength=materialCoverage;
if(strength>.001) {
vec3 bodyNormal=normalize(mat3(uWorldToBody)*normal);
vec3 weights=pow(abs(bodyNormal),vec3(4.0)); weights/=max(.0001,weights.x+weights.y+weights.z);
vec3 position=vMaterialPosition/uMaterialSettings.x;
float rock=smoothstep(.04,.23,slope);
vec4 material=mix(triColor(uFlatColor,position,weights),triColor(uSlopeColor,position,weights),rock);
vec4 flatMaterial;
vec3 flatDetail;
if(uBiomesEnabled!=0&&dot(environment,vec4(1))>.01){
int dominant=0;for(int b=1;b<4;b++)if(environment[b]>environment[dominant])dominant=b;
vec4 mixed=biomeTriColor(float(dominant),position,weights)*environment[dominant];
vec3 mixedNormal=biomeTriNormal(float(dominant),position,bodyNormal,weights)*environment[dominant];
float sum=environment[dominant];
for(int b=0;b<4;b++)if(b!=dominant&&environment[b]>.05){
mixed+=biomeTriColor(float(b),position,weights)*environment[b];
mixedNormal+=biomeTriNormal(float(b),position,bodyNormal,weights)*environment[b];sum+=environment[b];
}
flatMaterial=mixed/max(.0001,sum);flatDetail=normalize(mixedNormal);
}
else{flatMaterial=triColor(uFlatColor,position,weights);flatDetail=triNormal(uFlatNormal,position,bodyNormal,weights);}
vec4 material=flatMaterial;vec3 detail=flatDetail;
// Do not fetch the legacy flat material beneath a biome or rock maps on flat ground.
if(rock>.001){material=mix(flatMaterial,triColor(uSlopeColor,position,weights),rock);detail=mix(flatDetail,triNormal(uSlopeNormal,position,bodyNormal,weights),rock);}
mineral=mix(mineral,material.rgb,strength);
materialRoughness=mix(.8,material.a,strength);
vec3 detail=mix(triNormal(uFlatNormal,position,bodyNormal,weights),triNormal(uSlopeNormal,position,bodyNormal,weights),rock);
normal=normalize(mix(normal,transpose(mat3(uWorldToBody))*detail,strength*.4));
}
}
@@ -149,7 +188,8 @@ void main() {
float distanceToLight=length(toLight);
vec3 ld=toLight/max(distanceToLight,1.0);
float sunHeight=dot(radial,ld);
float visibility=smoothstep(-0.025,0.018,sunHeight);
float vegetationVisibility=vegetationShadow(vWorldPosition,l);
float visibility=smoothstep(-0.025,0.018,sunHeight)*vegetationVisibility;
if(visibility>0.001) {
visibility*=cloudShadow(vWorldPosition,ld);
if(uDebugDisableBodyShadows==0) {

View File

@@ -0,0 +1,5 @@
#version 330 core
in vec2 vUv;
uniform sampler2D uColor;
uniform float uAlphaCutoff;
void main(){if(uAlphaCutoff>0.0&&texture(uColor,vUv).a<uAlphaCutoff)discard;}

View File

@@ -0,0 +1,47 @@
#version 330 core
#define VEGETATION_FOLIAGE
#include "vegetation-shadow.glsl"
in vec3 vWorldPosition;
in vec3 vStablePosition;
in vec2 vUv;
in float vFragDepth;
flat in vec4 vVariation;
flat in vec3 vView;
flat in mat3 vBasis;
uniform sampler2D uColor;
uniform sampler2D uNormal;
uniform float uFar;
uniform int uDepthOnly;
uniform sampler2D uNearestDepth;
uniform int uLightCount;
uniform vec3 uLightDirections[2];
uniform vec3 uLightColors[2];
uniform float uLightFlux[2];
uniform vec3 uPlanetUp;
out vec4 fragColor;
vec2 atlas(float column,float row){return vec2((mod(column,16.0)+vUv.x)/16.0,1.0-(row+vUv.y)/3.0);}
void main(){
gl_FragDepth=clamp(log2(max(.0001,vFragDepth))/log2(uFar+1.0),0.0,1.0);
if(uDepthOnly==0&&gl_FragDepth>texelFetch(uNearestDepth,ivec2(gl_FragCoord.xy),0).r+0.0000002)discard;
float angle=atan(vView.x,vView.z)*16.0/6.28318530718;float first=floor(angle),blend=fract(angle);
float row=clamp(round(asin(clamp(vView.y,0.0,1.0))*6.0/3.14159265359),0.0,2.0);
vec4 a=texture(uColor,atlas(first,row)),b=texture(uColor,atlas(first+1.0,row));
vec4 tex=mix(a,b,blend);
tex.rgb=mix(a.rgb*a.a,b.rgb*b.a,blend)/max(tex.a,.0001);
if(tex.a<.35)discard;
if(vVariation.z<.9999){
float dither=1.0-fract(sin(dot(floor((vStablePosition+vVariation.w*71.0)*47.0),vec3(12.9898,78.233,37.719)))*43758.5453);
if(dither>vVariation.z)discard;
}
if(uDepthOnly>0){fragColor=vec4(0);return;}
vec3 na=texture(uNormal,atlas(first,row)).rgb*2.0-1.0,nb=texture(uNormal,atlas(first+1.0,row)).rgb*2.0-1.0;
vec3 normal=normalize(vBasis*mix(na*a.a,nb*b.a,blend));
vec3 light=vec3(.04,.055,.07);
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);
}
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

@@ -0,0 +1,42 @@
#version 330 core
layout(location=0)in vec3 aPosition;
layout(location=2)in vec2 aUv;
layout(location=3)in vec4 aOffsetScale;
layout(location=4)in vec4 aUpYaw;
layout(location=5)in vec4 aVariation;
uniform mat4 uViewProjection;
uniform mat4 uBodyToWorld;
uniform vec3 uAnchor;
uniform vec3 uCameraBody;
uniform vec3 uCameraOffset;
uniform vec3 uBatchBodyOrigin;
uniform vec3 uModelCenter;
uniform float uBillboardSize;
uniform float uFar;
uniform float uTime;
uniform vec3 uImpostorRange;
out vec3 vWorldPosition;
out vec3 vStablePosition;
out vec2 vUv;
out float vFragDepth;
flat out vec4 vVariation;
flat out vec3 vView;
flat out mat3 vBasis;
void main(){
vec3 up=normalize(aUpYaw.xyz);
vec3 right=normalize(cross(abs(up.y)<.95?vec3(0,1,0):vec3(1,0,0),up)),forward=cross(right,up);
float c=cos(aUpYaw.w),s=sin(aUpYaw.w);mat3 basis=mat3(right*c+forward*s,up,forward*c-right*s);
vec3 center=aOffsetScale.xyz+basis*uModelCenter*aOffsetScale.w;
vec3 eye=uCameraOffset-center;
float distance=length(eye);
vec3 view=normalize(eye),bx=normalize(cross(up,view)),by=cross(view,bx);
vec3 p=center+(bx*aPosition.x+by*aPosition.y)*uBillboardSize*aOffsetScale.w;
p+=right*sin(uTime*1.3+aVariation.x*28.0)*aVariation.y*max(0.0,aPosition.y);
vWorldPosition=uAnchor+mat3(uBodyToWorld)*p;vUv=aUv;
vStablePosition=transpose(basis)*(p-aOffsetScale.xyz)/aOffsetScale.w;
vView=transpose(basis)*view;vBasis=mat3(uBodyToWorld)*basis;vVariation=aVariation;
vVariation.z*=smoothstep(uImpostorRange.x,uImpostorRange.y,distance)*(1.0-smoothstep(uImpostorRange.z*.9,uImpostorRange.z,distance));
vec4 clip=uViewProjection*vec4(vWorldPosition,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;
if(distance>uImpostorRange.z||vVariation.z<=0.0)gl_Position=vec4(2,2,2,1);
}

View File

@@ -0,0 +1,56 @@
uniform sampler2DArrayShadow uVegetationShadowMap;
uniform mat4 uVegetationShadowMatrices[4];
uniform int uVegetationShadows;
uniform vec3 uVegetationShadowDistances;
uniform vec3 uVegetationShadowSettings; // softness [0,1], cascade blend fraction, bias in metres
float vegetationCascadeShadow(vec3 world,int light,int cascade) {
int layer=light*2+cascade;
vec4 p=uVegetationShadowMatrices[layer]*vec4(world,1);
vec3 q=p.xyz*.5+.5;
if(any(lessThan(q,vec3(0)))||any(greaterThan(q,vec3(1))))return 1.0;
// Convert metres using the actual orthographic depth gradient, including the GL depth remap.
mat4 matrix=uVegetationShadowMatrices[layer];
float depthPerMeter=.5*length(vec3(matrix[0].z,matrix[1].z,matrix[2].z));
float reference=q.z-uVegetationShadowSettings.z*depthPerMeter;
vec2 size=vec2(textureSize(uVegetationShadowMap,0).xy);
vec2 pixel=q.xy*size-.5,f=fract(pixel),anchor=(floor(pixel)+.5)/size;
float softness=uVegetationShadowSettings.x;
if(softness<.001)return texture(uVegetationShadowMap,vec4(q.xy,float(layer),reference));
float sum=0.0;
#ifdef VEGETATION_FOLIAGE
// Separable 3x3 tent: four bilinear comparison fetches instead of nine point fetches.
vec2 wx=vec2(3.0-2.0*f.x,1.0+2.0*f.x),wy=vec2(3.0-2.0*f.y,1.0+2.0*f.y);
vec2 ox=vec2(-1.0+(2.0-f.x)/wx.x,1.0+f.x/wx.y);
vec2 oy=vec2(-1.0+(2.0-f.y)/wy.x,1.0+f.y/wy.y);
for(int y=0;y<2;y++)for(int x=0;x<2;x++)
sum+=wx[x]*wy[y]*texture(uVegetationShadowMap,vec4(anchor+vec2(ox[x],oy[y])/size,float(layer),reference));
sum/=16.0;
#else
// Exact, subtexel-continuous 5x5 tent, paired into nine hardware PCF fetches.
// The weights sum to 9 on each axis, including between shadow-map texels.
vec3 wx=vec3(3.0-2.0*f.x,5.0,1.0+2.0*f.x),wy=vec3(3.0-2.0*f.y,5.0,1.0+2.0*f.y);
vec3 ox=vec3(-2.0+(2.0-f.x)/wx.x,(2.0+f.x)/5.0,2.0+f.x/wx.z);
vec3 oy=vec3(-2.0+(2.0-f.y)/wy.x,(2.0+f.y)/5.0,2.0+f.y/wy.z);
for(int y=0;y<3;y++)for(int x=0;x<3;x++)
sum+=wx[x]*wy[y]*texture(uVegetationShadowMap,vec4(anchor+vec2(ox[x],oy[y])/size,float(layer),reference));
sum/=81.0;
#endif
if(softness<.999)
sum=mix(texture(uVegetationShadowMap,vec4(q.xy,float(layer),reference)),sum,softness);
return sum;
}
float vegetationShadow(vec3 world,int light) {
if(uVegetationShadows==0)return 1.0;
float distance=length(world);
if(distance>uVegetationShadowDistances.y)return 1.0;
int cascade=distance<uVegetationShadowDistances.x?0:1;
float visibility=vegetationCascadeShadow(world,light,cascade);
float blend=uVegetationShadowSettings.y;
float start=uVegetationShadowDistances.x*(1.0-blend);
if(cascade==0&&blend>0.0&&distance>start)
visibility=mix(visibility,vegetationCascadeShadow(world,light,1),smoothstep(start,uVegetationShadowDistances.x,distance));
float fade=1.0-smoothstep(uVegetationShadowDistances.y*.8,uVegetationShadowDistances.y,distance);
return mix(1.0,.18+.82*visibility,fade);
}

View File

@@ -0,0 +1,80 @@
#version 330 core
#define VEGETATION_FOLIAGE
#include "vegetation-shadow.glsl"
in vec3 vWorldPosition;
in vec3 vNormal;
in vec3 vStablePosition;
in vec2 vUv;
in float vFragDepth;
flat in vec4 vVariation;
uniform sampler2D uColor;
uniform sampler2D uNormal;
uniform sampler2D uArm;
uniform vec3 uFactor;
uniform float uAlphaCutoff;
uniform int uFoliage;
uniform float uFar;
uniform int uBake;
uniform int uDitherInvert;
uniform int uDepthOnly;
uniform sampler2D uNearestDepth;
uniform int uBakeCluster;
uniform vec3 uBakeMinimum;
uniform vec3 uBakeMaximum;
uniform mat4 uBakeNormalBasis;
uniform int uLightCount;
uniform vec3 uLightDirections[2];
uniform vec3 uLightColors[2];
uniform float uLightFlux[2];
uniform vec3 uPlanetUp;
uniform int uDetailedLighting;
out vec4 fragColor;
float hash(vec3 p){return fract(sin(dot(floor(p*47.0),vec3(12.9898,78.233,37.719)))*43758.5453);}
void main(){
gl_FragDepth=uBake!=0?gl_FragCoord.z:clamp(log2(max(.0001,vFragDepth))/log2(uFar+1.0),0.0,1.0);
if(uBake==0&&uDepthOnly==0&&gl_FragDepth>texelFetch(uNearestDepth,ivec2(gl_FragCoord.xy),0).r+0.0000002)discard;
if(uBakeCluster!=0&&(any(lessThan(vWorldPosition,uBakeMinimum))||any(greaterThan(vWorldPosition,uBakeMaximum))))discard;
vec4 tex=texture(uColor,vUv);
if(tex.a<uAlphaCutoff)discard;
if(vVariation.z<.9999){
float coverage=hash(vStablePosition+vVariation.w*71.0);
if(uDitherInvert!=0)coverage=1.0-coverage;
if(coverage>vVariation.z)discard;
}
if(uDepthOnly>0){fragColor=vec4(0);return;}
vec3 normal=normalize(vNormal);if(!gl_FrontFacing)normal=-normal;
vec3 dp1=dFdx(vWorldPosition),dp2=dFdy(vWorldPosition);vec2 du1=dFdx(vUv),du2=dFdy(vUv);
vec3 tangent=dp1*du2.y-dp2*du1.y;vec3 bitangent=-dp1*du2.x+dp2*du1.x;
float inv= inversesqrt(max(dot(tangent,tangent),dot(bitangent,bitangent))+.00001);
vec3 detail=texture(uNormal,vUv).xyz*2.0-1.0;
normal=normalize(mat3(tangent*inv,bitangent*inv,normal)*detail);
vec3 color=tex.rgb*uFactor*(.9+vVariation.x*.2);
if(uBake!=0){
if(uBake==2)fragColor=vec4((uBakeCluster!=0?mat3(uBakeNormalBasis)*normal:normal)*.5+.5,1);
else fragColor=vec4(pow(max(color,vec3(0)),vec3(1.0/2.2)),tex.a);
return;
}
vec3 arm=texture(uArm,vUv).rgb;
vec3 light=vec3(.035,.05,.065)*(.35+.65*arm.r);
vec3 highlights=vec3(0);
vec3 view=normalize(-vWorldPosition);
for(int i=0;i<2;i++){
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 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);
light+=sunlight*(ndl+back);
if(uDetailedLighting!=0){
vec3 halfway=normalize(view+ld);float gloss=1.0-clamp(arm.g,.35,1.0);
float nh=max(0.0,dot(normal,halfway));float nh2=nh*nh,nh4=nh2*nh2,nh8=nh4*nh4,nh16=nh8*nh8,nh32=nh16*nh16;
float f=1.0-max(0.0,dot(view,halfway));float f2=f*f;
float fresnel=.04+.96*f2*f2*f;
highlights+=sunlight*ndl*fresnel*mix(nh8*10.0,nh32*34.0,gloss*gloss)/25.1327412;
}else highlights+=sunlight*ndl*.003;
}
fragColor=vec4(color*light+highlights,1);
gl_FragDepth=clamp(log2(max(.0001,vFragDepth))/log2(uFar+1.0),0.0,1.0);
}

View File

@@ -0,0 +1,46 @@
#version 330 core
layout(location=0)in vec3 aPosition;
layout(location=1)in vec3 aNormal;
layout(location=2)in vec2 aUv;
layout(location=3)in vec4 aOffsetScale;
layout(location=4)in vec4 aUpYaw;
layout(location=5)in vec4 aVariation;
uniform mat4 uViewProjection;
uniform mat4 uBodyToWorld;
uniform vec3 uAnchor;
uniform float uFar;
uniform float uTime;
uniform float uHeight;
uniform int uShadowPass;
uniform vec3 uLodNearFar;
out vec3 vWorldPosition;
out vec3 vNormal;
out vec3 vStablePosition;
out vec2 vUv;
out float vFragDepth;
flat out vec4 vVariation;
void main(){
vec3 up=normalize(aUpYaw.xyz);
vec3 right=normalize(cross(abs(up.y)<.95?vec3(0,1,0):vec3(1,0,0),up));
vec3 forward=cross(right,up);
float c=cos(aUpYaw.w),s=sin(aUpYaw.w);
mat3 basis=mat3(right*c+forward*s,up,forward*c-right*s);
vec3 local=aPosition;
float bend=pow(clamp(aPosition.y/max(.1,uHeight),0.0,1.0),2.0);
local.x+=sin(uTime*1.3+aVariation.x*28.0+aPosition.y*.5)*aVariation.y*bend;
local.z+=cos(uTime*.93+aVariation.x*19.0)*aVariation.y*bend*.6;
vWorldPosition=uAnchor+mat3(uBodyToWorld)*(aOffsetScale.xyz+basis*local*aOffsetScale.w);
vNormal=mat3(uBodyToWorld)*basis*aNormal;vUv=aUv;vVariation=aVariation;
if(uShadowPass==0){
float distance=length(uAnchor+mat3(uBodyToWorld)*aOffsetScale.xyz);
float fade=uLodNearFar.x<=0.0?1.0:smoothstep(uLodNearFar.x*.9,uLodNearFar.x*1.1,distance);
fade*=1.0-smoothstep(uLodNearFar.y*.9,uLodNearFar.y*1.1,distance);
float range=abs(uLodNearFar.z);
fade*=1.0-smoothstep(range*(uLodNearFar.z<0.0?.8:.9),range,distance);
vVariation.z*=fade;
}
vStablePosition=aPosition;
vec4 clip=uViewProjection*vec4(vWorldPosition,1);
vFragDepth=clip.w+1.0;gl_Position=clip;
if(uShadowPass==0)gl_Position.z=(2.0*log2(max(.0001,vFragDepth))/log2(uFar+1.0)-1.0)*clip.w;
}

View File

@@ -14,6 +14,7 @@ internal sealed unsafe class SurfaceMaterials : IDisposable
private readonly Catalog _catalog;
private readonly Dictionary<string, Entry> _entries;
private readonly Dictionary<string, (uint Color, uint Normal)> _textures = new(StringComparer.OrdinalIgnoreCase);
private uint _biomeColor,_biomeNormal;
public SurfaceMaterials(GL gl, string directory)
{
@@ -45,6 +46,37 @@ internal sealed unsafe class SurfaceMaterials : IDisposable
return full;
}
public (uint Color,uint Normal) Biomes(VegetationCatalog catalog)
{
if(_biomeColor!=0)return(_biomeColor,_biomeNormal);
const int size=2048;
_biomeColor=_gl.GenTexture();_biomeNormal=_biomeColor;
_gl.BindTexture(GLEnum.Texture2DArray,_biomeColor);
_gl.TexImage3D(GLEnum.Texture2DArray,0,InternalFormat.Rgba8,size,size,8,0,PixelFormat.Rgba,PixelType.UnsignedByte,null);
for(var kind=0;kind<2;kind++)
{
for(var layer=0;layer<4;layer++)
{
var entry=_entries[catalog.Biomes[layer].Material];
var image=RasterImage.Load(Resolve(kind==0?entry.Color:entry.Normal));
var rough=kind==0?RasterImage.Load(Resolve(entry.Roughness)):null;
var data=new byte[size*size*4];
for(var y=0;y<size;y++)for(var x=0;x<size;x++)
{
var source=((y*image.Height/size)*image.Width+x*image.Width/size)*4;var target=(y*size+x)*4;
data[target]=image.Rgba[source];data[target+1]=image.Rgba[source+1];data[target+2]=image.Rgba[source+2];
data[target+3]=rough is null?(byte)255:rough.Rgba[((y*rough.Height/size)*rough.Width+x*rough.Width/size)*4];
}
fixed(byte* p=data)_gl.TexSubImage3D(GLEnum.Texture2DArray,0,0,0,layer+kind*4,size,size,1,PixelFormat.Rgba,PixelType.UnsignedByte,p);
}
}
_gl.GenerateMipmap(GLEnum.Texture2DArray);
_gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureMinFilter,(int)GLEnum.LinearMipmapLinear);
_gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureMagFilter,(int)GLEnum.Linear);
_gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureWrapS,(int)GLEnum.Repeat);_gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureWrapT,(int)GLEnum.Repeat);
return(_biomeColor,_biomeNormal);
}
private (uint Color, uint Normal) Get(string id)
{
if (_textures.TryGetValue(id, out var existing)) return existing;
@@ -76,5 +108,6 @@ internal sealed unsafe class SurfaceMaterials : IDisposable
public void Dispose()
{
foreach (var value in _textures.Values) { _gl.DeleteTexture(value.Color); _gl.DeleteTexture(value.Normal); }
if(_biomeColor!=0){_gl.DeleteTexture(_biomeColor);}
}
}

View File

@@ -53,6 +53,7 @@ internal sealed class UniverseRenderer : IDisposable
private readonly NoiseTexture _noise;
private readonly CloudNoiseTexture _cloudNoise;
private readonly SurfaceMaterials _materials;
internal VegetationRenderer Vegetation {get;}
private RenderTarget? _cloudRaw;
private RenderTarget? _waterReflectionRaw;
private RenderTarget _waterReflectionTarget = null!;
@@ -84,6 +85,12 @@ internal sealed class UniverseRenderer : IDisposable
public double TerrainNearestDistance { get; private set; }
public int RingRockCount { get; private set; }
public bool DebugMode { get; set; }
public bool BenchmarkProfiling {get;set;}
internal uint SceneDepthTexture=>_scene.Depth;
internal uint OceanMaterialTexture=>_scene.AuxiliaryColors[1];
internal int TerrainUploads {get;private set;}
public double VegetationGpuMilliseconds=>_gpuProfiler.Results().Where(p=>p.Key is "VEGETATION" or "VEGETATION DEPTH").Sum(p=>p.Value);
public double VegetationShadowMilliseconds=>_gpuProfiler.Results().FirstOrDefault(p=>p.Key=="VEGETATION SHADOWS").Value;
public bool DebugProbeEnabled { get; set; }
public double CpuRenderMilliseconds { get; private set; }
public float ExposureCompensation { get; set; } = 1f;
@@ -98,6 +105,8 @@ internal sealed class UniverseRenderer : IDisposable
_cloudNoise = new(gl);
_emptyCloudDensity = new(gl, null);
_materials = new(gl, Path.Combine(AppContext.BaseDirectory, "Content"));
Vegetation=new(gl,Path.Combine(AppContext.BaseDirectory,"Content","Vegitations"));
_materials.Biomes(Vegetation.Catalog);
_emptyRing = new(gl, null);
var (vertices, indices) = PrimitiveMeshes.UnitSphere(96, 144);
_sphere = new(gl, 6, vertices, indices);
@@ -190,6 +199,7 @@ internal sealed class UniverseRenderer : IDisposable
("fullscreen.vert","ocean-reflection.frag"),("fullscreen.vert","ocean-reflection-composite.frag") })
replacements.Add(Program(pair.Item1, pair.Item2));
_starTrails.ReloadShaders();
Vegetation.ReloadShaders();
}
catch (Exception error)
{
@@ -266,7 +276,7 @@ internal sealed class UniverseRenderer : IDisposable
{
if (_disposed) return;
_starTrails.BeginFrame(camera,_width,_height,deltaSeconds);
var cpuTimer = DebugMode ? System.Diagnostics.Stopwatch.StartNew() : null;
var cpuTimer = DebugMode||BenchmarkProfiling ? System.Diagnostics.Stopwatch.StartNew() : null;
var timer = ProfileFrame ? System.Diagnostics.Stopwatch.StartNew() : null;
if (timer is not null) { _gl.Finish(); timer.Restart(); FrameTimings.Clear(); }
void Mark(string name)
@@ -275,7 +285,7 @@ internal sealed class UniverseRenderer : IDisposable
_gl.Finish(); FrameTimings[name] = timer.Elapsed.TotalMilliseconds; timer.Restart();
}
_time = SimulationTimeSeconds ?? (_time + System.Math.Min(deltaSeconds, 0.1));
TerrainTiles = 0;
TerrainTiles = 0;TerrainUploads=0;
TerrainDeepestLevel = 0; TerrainPendingTiles = 0;
TerrainNearestLevel = 0; TerrainNearestDistance = double.PositiveInfinity;
SetActiveSystem(neighborhood.ActiveSystem.Coordinate);
@@ -284,11 +294,17 @@ internal sealed class UniverseRenderer : IDisposable
var projection = Matrix4x4.CreatePerspectiveFieldOfView((float)camera.FieldOfViewRadians, (float)_width / _height, 0.02f, far);
var vp = view * projection;
var system = neighborhood.ActiveSystem;
Vegetation.Aspect=(double)_width/_height;Vegetation.Update(camera,system,_time);
Mark("vegetation streaming");
RetainSystem(system);
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
var starPositions = system.Stars.Select(s => center.Add(s.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters)).ToArray();
BindNoise();
_gpuProfiler.BeginFrame(DebugMode);
_gpuProfiler.BeginFrame(DebugMode||BenchmarkProfiling);
_gpuProfiler.BeginPass("VEGETATION SHADOWS");
Vegetation.DrawShadows(camera,system);
_gpuProfiler.EndPass();
Mark("vegetation shadows");
_oceanRendered = false; _cloudLayerPlanetId = null;
_scene.Bind(allColors: true);
_gl.DepthMask(true);
@@ -318,6 +334,14 @@ internal sealed class UniverseRenderer : IDisposable
Mark("galaxies and nebulae");
_gl.DepthMask(true);
_gl.Disable(GLEnum.Blend);
_gpuProfiler.BeginPass("VEGETATION DEPTH");
Vegetation.PrepareDepth(camera,system,vp,far,_width,_height);
_gpuProfiler.EndPass();
Mark("vegetation depth");
_gpuProfiler.BeginPass("VEGETATION");
Vegetation.Draw(camera,system,vp,far,_width,_height);
_gpuProfiler.EndPass();
Mark("vegetation");
_gpuProfiler.BeginPass("PLANETS TOTAL");
DrawPlanets(camera, system, vp, far, starPositions);
_gpuProfiler.EndPass();
@@ -389,7 +413,7 @@ internal sealed class UniverseRenderer : IDisposable
_gl.Disable(GLEnum.DepthTest); _gl.Disable(GLEnum.Blend); _gl.DepthMask(false);
_overlay.Use();
_performanceOverlay.Draw(_width, _height, DebugSamples(), CpuRenderMilliseconds, fps,
TerrainTiles, TerrainDeepestLevel, TerrainPendingTiles, _debugProbePlanetId, timeScale, simulationUtc);
TerrainTiles, TerrainDeepestLevel, TerrainPendingTiles, _debugProbePlanetId, timeScale, simulationUtc,Vegetation);
_gl.DepthMask(true); _gl.Enable(GLEnum.DepthTest);
}
@@ -468,6 +492,16 @@ internal sealed class UniverseRenderer : IDisposable
if (onlyPlanetId is not null && planet.Id != onlyPlanetId) continue;
if (planet.IsGasPlanet) 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=>
{
var light=star.PositionMeters-planet.PositionMeters;var distance=light.Length;var direction=light/distance;
var offset=b.Body.PositionMeters-planet.PositionMeters;var along=DVector3.Dot(offset,direction);
return along>0&&along<distance&&(offset-direction*along).Length<=b.Body.MaximumTerrainRadiusMeters+planet.MaximumTerrainRadiusMeters+star.RadiusMeters*along/distance;
})).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.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;
if (!_planetMeshes.TryGetValue(planet.Id, out var mesh))
@@ -485,20 +519,39 @@ internal sealed class UniverseRenderer : IDisposable
}
}
BindPlanetTerrain(_surface, planet, textures);
Vegetation.BindDepthCulling(_surface,onlyPlanetId is null);
Vegetation.BindShadows(_surface,planet.Id,2);
var biomesEnabled=planet.Vegetation is {Enabled:true,AffectGroundMaterials:true};
_surface.Set("uBiomesEnabled",biomesEnabled?1:0);_surface.Set("uBiomeColors",0);_surface.Set("uBiomeTexture",1);
_surface.Set("uFlatColor",11);_surface.Set("uFlatNormal",12);_surface.Set("uSlopeColor",13);_surface.Set("uSlopeNormal",14);
if(biomesEnabled)
{
var biomeTextures=_materials.Biomes(Vegetation.Catalog);
_gl.ActiveTexture(GLEnum.Texture0);_gl.BindTexture(GLEnum.Texture2DArray,biomeTextures.Color);
BindTexture(1,textures.Biome);
_surface.Set("uBiomeHasTexture",planet.VegetationBiomeTexture is not null||planet.SurfaceTexture is not null?1:0);
_surface.Set("uForestScale",(float)planet.Vegetation!.ForestScaleMeters);
_surface.SetVectorArray("uBiomeTargets",Vegetation.Catalog.Biomes.Select(b=>new Vector3(b.ColorRgb[0],b.ColorRgb[1],b.ColorRgb[2])/255).ToArray());
_surface.SetFloatArray("uBiomeTolerances",Vegetation.Catalog.Biomes.Select(b=>(float)b.TolerancePercent).ToArray());
}
BindCloudSettings(_surface, camera, system, planet, relative);
var materialSettings = planet.Materials;
_surface.Set("uMaterialsEnabled", materialSettings.Enabled && planet.SurfaceType != PlanetSurfaceType.CloudCovered ? 1 : 0);
// The shader's detail contribution is zero beyond this range. In particular,
// first seeing the Moon from Earth must not decode/upload its ground maps.
var materialsEnabled=materialSettings.Enabled&&materialSettings.Strength>0&&planet.SurfaceType!=PlanetSurfaceType.CloudCovered
&&relative.Length-planet.MaximumTerrainRadiusMeters<materialSettings.FadeDistanceMeters;
_surface.Set("uMaterialsEnabled", materialsEnabled ? 1 : 0);
if (onlyPlanetId is not null && probeEffect == PlanetProbeEffect.Materials)
_surface.Set("uMaterialsEnabled", 0);
_surface.Set("uMaterialSettings", new Vector3((float)materialSettings.ScaleMeters, (float)materialSettings.FadeDistanceMeters, (float)materialSettings.Strength));
if (materialSettings.Enabled && planet.SurfaceType != PlanetSurfaceType.CloudCovered)
if (materialsEnabled)
{
var material = _materials.ForPlanet(planet);
BindTexture(11, material.FlatColor); BindTexture(12, material.FlatNormal);
BindTexture(13, material.SlopeColor); BindTexture(14, material.SlopeNormal);
_surface.Set("uFlatColor", 11); _surface.Set("uFlatNormal", 12); _surface.Set("uSlopeColor", 13); _surface.Set("uSlopeNormal", 14);
}
_surface.Set("uPlanetIndex", i);
_surface.Set("uPlanetIndex", -1); // The planet itself was excluded from the compact occluder list.
_surface.Set("uSurfaceColor", planet.SurfaceColor);
_surface.Set("uAtmosphereColor", planet.AtmosphereHeightMeters > 0 ? planet.AtmosphereColor : Vector3.Zero);
_surface.Set("uSurfaceTexture", 5);
@@ -527,6 +580,7 @@ internal sealed class UniverseRenderer : IDisposable
{
_gpuProfiler.EndPass();
TerrainTiles += mesh.DrawnTiles;
TerrainUploads+=mesh.UploadedTiles;
TerrainDeepestLevel = System.Math.Max(TerrainDeepestLevel, mesh.DeepestLevel);
TerrainPendingTiles += mesh.PendingTiles;
if (mesh.NearestDistance < TerrainNearestDistance)
@@ -694,7 +748,7 @@ internal sealed class UniverseRenderer : IDisposable
_gl.BlitFramebuffer(0, 0, _width, _height, 0, 0, _width, _height,
ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit, BlitFramebufferFilter.Nearest);
_scene.Bind(allColors: true); _gl.Enable(GLEnum.DepthTest); _gl.DepthFunc(GLEnum.Always); _gl.DepthMask(true);
_gl.Enable(GLEnum.Blend); _gl.BlendFunc(GLEnum.One, GLEnum.OneMinusSrcAlpha);
_gl.Disable(GLEnum.Blend);
_oceanRendered = true;
_ocean.Use(); SetCameraRay(_ocean, camera); BindPlanetTerrain(_ocean, planet, textures);
BindCloudSettings(_ocean, camera, system, planet, relative);
@@ -710,9 +764,9 @@ internal sealed class UniverseRenderer : IDisposable
_ocean.Set("uAxisX", east.ToVector3()); _ocean.Set("uAxisY", up.ToVector3()); _ocean.Set("uAxisZ", north.ToVector3());
_ocean.Set("uSeaRadius", (float)radius); _ocean.Set("uCameraHeight", (float)(relative.Length - radius));
var bodyAnchor = planet.Orientation.ToBody(up * radius);
for (var detail = 0; detail < 2; detail++)
for (var detail = 0; detail < OceanSurface.DetailFrequencies.Length; detail++)
{
var q = bodyAnchor * (detail == 0 ? .35 : 1.4);
var q = bodyAnchor * OceanSurface.DetailFrequencies[detail];
_ocean.Set($"uDetailOrigins[{detail}]", new Vector3((float)System.Math.IEEERemainder(q.X, 64),
(float)System.Math.IEEERemainder(q.Y, 64), (float)System.Math.IEEERemainder(q.Z, 64)));
}
@@ -721,11 +775,12 @@ internal sealed class UniverseRenderer : IDisposable
{
var wave = waves[i]; var direction = planet.Orientation.ToWorld(wave.Direction);
_ocean.Set($"uWaveVectors[{i}]", (Local(direction) * wave.WaveNumber).ToVector3());
_ocean.Set($"uWavePhases[{i}]", (float)System.Math.IEEERemainder(DVector3.Dot(up * radius, direction) * wave.WaveNumber - _time * wave.AngularFrequency, System.Math.Tau));
_ocean.Set($"uWavePhases[{i}]", (float)System.Math.IEEERemainder(DVector3.Dot(up * radius, direction) * wave.WaveNumber - _time * wave.AngularFrequency + wave.Phase, System.Math.Tau));
_ocean.Set($"uWaveAmplitudes[{i}]", (float)wave.Amplitude);
}
_ocean.Set("uWaveSettings", new Vector3((float)(waves.Take(8).Sum(w => w.Amplitude) + .1), (float)settings.Choppiness,
_ocean.Set("uWaveSettings", new Vector3((float)(waves.Take(OceanSurface.GeometryWaveCount).Sum(w => w.Amplitude) + .1), (float)settings.Choppiness,
(float)(2 * System.Math.Tan(camera.FieldOfViewRadians * .5) / _height)));
_ocean.Set("uSurfaceDetail", new Vector3((float)settings.RippleStrength, (float)settings.WaveGeometryDistanceMeters, (float)settings.WhitecapThreshold));
_ocean.Set("uWaterOptics", new Vector3((float)settings.Roughness, (float)settings.ReflectionStrength, (float)settings.RefractionStrength));
_ocean.Set("uDeepColor", new Vector3(settings.DeepColor[0], settings.DeepColor[1], settings.DeepColor[2]));
_ocean.Set("uShallowColor", new Vector3(settings.ShallowColor[0], settings.ShallowColor[1], settings.ShallowColor[2]));
@@ -761,7 +816,9 @@ internal sealed class UniverseRenderer : IDisposable
_gl.Disable(GLEnum.Blend); _gl.Disable(GLEnum.DepthTest); _gl.DepthMask(false);
_waterReflectionRaw.Bind(); _oceanReflection.Use(); SetCameraRay(_oceanReflection, camera);
BindTexture(0, source.Color); BindTexture(1, _scene.Depth); BindTexture(2, _scene.AuxiliaryColors[0]);
BindTexture(3, _scene.AuxiliaryColors[1]);
_oceanReflection.Set("uScene", 0); _oceanReflection.Set("uDepth", 1); _oceanReflection.Set("uWaterNormals", 2);
_oceanReflection.Set("uWaterFallback", 3);
_oceanReflection.Set("uFar", far); _oceanReflection.Set("uMaxSteps", r.MaxSteps);
_oceanReflection.Set("uTraceSettings", new Vector3((float)r.MaxDistanceMeters, (float)r.ThicknessMeters, (float)r.NormalBiasMeters));
_oceanReflection.Set("uReflectionSettings", new Vector3((float)r.EdgeFade, (float)r.StridePixels, (float)planet.Ocean.Roughness));
@@ -1183,6 +1240,7 @@ internal sealed class UniverseRenderer : IDisposable
_ocean.Dispose();
_oceanReflection.Dispose(); _oceanReflectionComposite.Dispose(); _waterReflectionRaw?.Dispose(); _waterReflectionTarget.Dispose();
_clouds.Dispose(); _cloudComposite.Dispose(); _cloudNoise.Dispose(); _emptyCloudDensity.Dispose(); _materials.Dispose(); _cloudRaw?.Dispose();
Vegetation.Dispose();
_scene.Dispose(); _scatterA.Dispose(); _scatterB.Dispose(); _bloomA.Dispose(); _bloomB.Dispose(); _noise.Dispose();
}
}

View File

@@ -0,0 +1,126 @@
using Silk.NET.OpenGL;
namespace Universe.App.Graphics;
internal sealed unsafe class VegetationMesh : IDisposable
{
private readonly GL _gl;
private readonly uint _vao,_vertices,_indices;
private readonly Dictionary<uint,uint> _instanceVaos=[];
public int IndexCount {get;}
public VegetationMesh(GL gl,PlantPrimitive mesh)
{
_gl=gl;IndexCount=mesh.Indices.Length;_vao=gl.GenVertexArray();_vertices=gl.GenBuffer();_indices=gl.GenBuffer();
gl.BindVertexArray(_vao);gl.BindBuffer(GLEnum.ArrayBuffer,_vertices);
fixed(float* p=mesh.Vertices)gl.BufferData(GLEnum.ArrayBuffer,(nuint)(mesh.Vertices.Length*4),p,GLEnum.StaticDraw);
Attribute(0,3,32,0);Attribute(1,3,32,12);Attribute(2,2,32,24);
gl.BindBuffer(GLEnum.ElementArrayBuffer,_indices);
fixed(uint* p=mesh.Indices)gl.BufferData(GLEnum.ElementArrayBuffer,(nuint)(mesh.Indices.Length*4),p,GLEnum.StaticDraw);
gl.BindVertexArray(0);
}
private void Attribute(uint id,int count,uint stride,int offset)
{_gl.EnableVertexAttribArray(id);_gl.VertexAttribPointer(id,count,VertexAttribPointerType.Float,false,stride,(void*)offset);}
public void Draw(uint instances,int count)
{
if(count==0)return;
if(!_instanceVaos.TryGetValue(instances,out var vao))
{
vao=_gl.GenVertexArray();_instanceVaos.Add(instances,vao);_gl.BindVertexArray(vao);
_gl.BindBuffer(GLEnum.ArrayBuffer,_vertices);Attribute(0,3,32,0);Attribute(1,3,32,12);Attribute(2,2,32,24);
_gl.BindBuffer(GLEnum.ElementArrayBuffer,_indices);_gl.BindBuffer(GLEnum.ArrayBuffer,instances);
for(uint i=3;i<=5;i++){Attribute(i,4,48,(int)(i-3)*16);_gl.VertexAttribDivisor(i,1);}
}
else _gl.BindVertexArray(vao);
_gl.DrawElementsInstanced(GLEnum.Triangles,(uint)IndexCount,DrawElementsType.UnsignedInt,null,(uint)count);
_gl.BindVertexArray(0);
}
public void ForgetInstances(uint buffer){if(_instanceVaos.Remove(buffer,out var vao))_gl.DeleteVertexArray(vao);}
public void Dispose(){foreach(var vao in _instanceVaos.Values)_gl.DeleteVertexArray(vao);_gl.DeleteVertexArray(_vao);_gl.DeleteBuffer(_vertices);_gl.DeleteBuffer(_indices);}
}
internal sealed unsafe class PlantTexture : IDisposable
{
private readonly GL _gl;
private readonly bool _srgb;
private readonly Universe.Core.Generation.RasterImage[] _mips;
public uint Handle {get;}
public PlantTexture(GL gl,Universe.Core.Generation.RasterImage image,bool srgb,bool preserveCoverage=false,bool deferred=false)
{
_gl=gl;_srgb=srgb;_mips=PlantMipmaps.Get(image,preserveCoverage);Handle=gl.GenTexture();
gl.BindTexture(GLEnum.Texture2D,Handle);
gl.TexParameter(GLEnum.Texture2D,GLEnum.TextureMinFilter,(int)GLEnum.LinearMipmapLinear);
gl.TexParameter(GLEnum.Texture2D,GLEnum.TextureMagFilter,(int)GLEnum.Linear);
gl.TexParameter(GLEnum.Texture2D,GLEnum.TextureWrapS,(int)GLEnum.Repeat);gl.TexParameter(GLEnum.Texture2D,GLEnum.TextureWrapT,(int)GLEnum.Repeat);
if(!deferred)foreach(var step in UploadSteps())step();
}
public IEnumerable<Action> UploadSteps()
{
for(var level=0;level<_mips.Length;level++)
{
var mip=_mips[level];var index=level;
yield return ()=>Allocate(index,mip);
var rows=System.Math.Max(1,262144/(mip.Width*4));
for(var y=0;y<mip.Height;y+=rows)
{
var row=y;var count=System.Math.Min(rows,mip.Height-y);
yield return ()=>Upload(index,mip,row,count);
}
}
}
private void Allocate(int level,Universe.Core.Generation.RasterImage mip)
{
_gl.BindTexture(GLEnum.Texture2D,Handle);
_gl.TexImage2D(GLEnum.Texture2D,level,_srgb?InternalFormat.Srgb8Alpha8:InternalFormat.Rgba8,(uint)mip.Width,(uint)mip.Height,0,PixelFormat.Rgba,PixelType.UnsignedByte,null);
}
private void Upload(int level,Universe.Core.Generation.RasterImage mip,int row,int count)
{
_gl.BindTexture(GLEnum.Texture2D,Handle);
fixed(byte* p=mip.Rgba)_gl.TexSubImage2D(GLEnum.Texture2D,level,0,row,(uint)mip.Width,(uint)count,PixelFormat.Rgba,PixelType.UnsignedByte,p+row*mip.Width*4);
}
public void Dispose()=>_gl.DeleteTexture(Handle);
}
internal static class PlantMipmaps
{
private sealed class Cache {public Universe.Core.Generation.RasterImage[]? Plain,Alpha;}
private static readonly System.Runtime.CompilerServices.ConditionalWeakTable<Universe.Core.Generation.RasterImage,Cache> Caches=new();
public static Universe.Core.Generation.RasterImage[] Get(Universe.Core.Generation.RasterImage image,bool preserveCoverage)
{
var cache=Caches.GetOrCreateValue(image);
lock(cache)
{
var existing=preserveCoverage?cache.Alpha:cache.Plain;if(existing is not null)return existing;
var result=new List<Universe.Core.Generation.RasterImage>{image};
var width=image.Width;var height=image.Height;var pixels=image.Rgba;
var target=preserveCoverage?(double)Enumerable.Range(0,width*height).Count(i=>pixels[i*4+3]>=102)/(width*height):0;
while(width>1||height>1)
{
var w=System.Math.Max(1,width/2);var h=System.Math.Max(1,height/2);var next=new byte[w*h*4];
for(var y=0;y<h;y++)for(var x=0;x<w;x++)
{
var alpha=0;var red=0;var green=0;var blue=0;
for(var dy=0;dy<2;dy++)for(var dx=0;dx<2;dx++)
{
var source=(System.Math.Min(height-1,y*2+dy)*width+System.Math.Min(width-1,x*2+dx))*4;var a=pixels[source+3];
alpha+=a;red+=pixels[source]*a;green+=pixels[source+1]*a;blue+=pixels[source+2]*a;
}
var targetPixel=(y*w+x)*4;
next[targetPixel]=(byte)(alpha==0?0:red/alpha);next[targetPixel+1]=(byte)(alpha==0?0:green/alpha);next[targetPixel+2]=(byte)(alpha==0?0:blue/alpha);next[targetPixel+3]=(byte)(alpha/4);
}
if(preserveCoverage&&target>0&&target<1)
{
var lo=.25;var hi=4d;
for(var i=0;i<10;i++)
{
var scale=(lo+hi)*.5;var covered=0;for(var j=3;j<next.Length;j+=4)if(next[j]*scale>=102)covered++;
if((double)covered/(w*h)<target)lo=scale;else hi=scale;
}
var scaleFinal=(lo+hi)*.5;for(var j=3;j<next.Length;j+=4)next[j]=(byte)System.Math.Min(255,next[j]*scaleFinal);
}
result.Add(new(w,h,next));pixels=next;width=w;height=h;
}
if(preserveCoverage)cache.Alpha=result.ToArray();else cache.Plain=result.ToArray();
return (preserveCoverage?cache.Alpha:cache.Plain)!;
}
}
}

View File

@@ -0,0 +1,66 @@
using System.Numerics;
using SharpGLTF.Schema2;
using StbImageSharp;
using Universe.Core.Generation;
namespace Universe.App.Graphics;
internal sealed record PlantMaterial(RasterImage Color,RasterImage Normal,RasterImage Arm,Vector3 Factor,float AlphaCutoff,bool Foliage,string Name);
internal sealed record PlantPrimitive(float[] Vertices,uint[] Indices,int Material);
internal sealed record PlantVariant(PlantPrimitive[] Primitives,Vector3 Minimum,Vector3 Maximum);
internal sealed record VegetationModelData(PlantVariant[][] Lods,PlantMaterial[] Materials)
{
public static VegetationModelData Load(string path)
{
var root=ModelRoot.Load(path);
var images=new Dictionary<int,RasterImage>();
RasterImage Image(Texture? texture,byte[] fallback)
{
if(texture is null)return new(1,1,fallback);
var source=texture.PrimaryImage;
if(images.TryGetValue(source.LogicalIndex,out var cached))return cached;
var decoded=ImageResult.FromMemory(source.Content.Content.ToArray(),ColorComponents.RedGreenBlueAlpha);
var image=new RasterImage(decoded.Width,decoded.Height,decoded.Data);images[source.LogicalIndex]=image;return image;
}
var materials=root.LogicalMaterials.Select(m=>
{
var color=m.FindChannel("BaseColor");var normal=m.FindChannel("Normal");var arm=m.FindChannel("MetallicRoughness");
var foliage=m.DoubleSided||m.Alpha!=AlphaMode.OPAQUE;
return new PlantMaterial(Image(color?.Texture,[255,255,255,255]),Image(normal?.Texture,[128,128,255,255]),
Image(arm?.Texture,[255,190,0,255]),color is {} c?new(c.Color.X,c.Color.Y,c.Color.Z):Vector3.One,
foliage?System.Math.Max(.25f,m.AlphaCutoff):0,foliage,m.Name??"");
}).ToArray();
foreach(var m in materials){PlantMipmaps.Get(m.Color,m.Foliage);PlantMipmaps.Get(m.Normal,false);PlantMipmaps.Get(m.Arm,false);}
var lods=new PlantVariant[3][];
for(var lod=0;lod<3;lod++)
{
if(lod>0&&!root.LogicalScenes.Any(s=>s.Name=="LOD"+lod)){lods[lod]=lods[0];continue;}
var scene=root.LogicalScenes.FirstOrDefault(s=>s.Name=="LOD"+lod)??root.DefaultScene;
lods[lod]=scene.VisualChildren.Select(node=>
{
var primitives=new List<PlantPrimitive>();var minimum=new Vector3(float.PositiveInfinity);var maximum=new Vector3(float.NegativeInfinity);
void Visit(Node n)
{
if(n.Mesh is {} mesh)foreach(var p in mesh.Primitives)
{
var positions=p.GetVertexAccessor("POSITION").AsVector3Array();
var normals=p.GetVertexAccessor("NORMAL")?.AsVector3Array();
var uv=p.GetVertexAccessor("TEXCOORD_0")?.AsVector2Array();
var data=new float[positions.Count*8];var matrix=n.WorldMatrix;
Matrix4x4.Invert(matrix,out var inverse);var normalMatrix=Matrix4x4.Transpose(inverse);
for(var i=0;i<positions.Count;i++)
{
var v=Vector3.Transform(positions[i],matrix);var normal=Vector3.Normalize(Vector3.TransformNormal(normals?[i]??Vector3.UnitY,normalMatrix));
var t=uv?[i]??Vector2.Zero;minimum=Vector3.Min(minimum,v);maximum=Vector3.Max(maximum,v);
var j=i*8;data[j]=v.X;data[j+1]=v.Y;data[j+2]=v.Z;data[j+3]=normal.X;data[j+4]=normal.Y;data[j+5]=normal.Z;data[j+6]=t.X;data[j+7]=t.Y;
}
primitives.Add(new(data,p.GetIndices().Select(i=>(uint)i).ToArray(),p.Material.LogicalIndex));
}
foreach(var child in n.VisualChildren)Visit(child);
}
Visit(node);return new PlantVariant(primitives.ToArray(),minimum,maximum);
}).ToArray();
}
return new(lods,materials);
}
}

View File

@@ -0,0 +1,120 @@
using System.Numerics;
using System.Text.Json;
using Silk.NET.OpenGL;
using Universe.Core.Generation;
namespace Universe.App.Graphics;
internal static unsafe class VegetationPreparation
{
private sealed record Card(float[] Center,float[] Right,float[] Up,float[] Normal,float Size,float[] Uv);
private static void BakeFoliage(GL gl,string directory,VegetationCatalog catalog,ShaderProgram program,uint instance)
{
const int columns=8,rows=9,cell=192;
using var target=new RenderTarget(gl,columns*cell,rows*cell,true);
foreach(var definition in catalog.Plants.Where(p=>p.Type==VegetationType.Tree))
{
var prepared=Path.GetDirectoryName(Path.Combine(directory,definition.Model))!;
var full=Path.Combine(prepared,"../source/bake-source.glb");
if(!File.Exists(full)||File.Exists(Path.Combine(prepared,"cards.json")))continue;
Console.WriteLine("Baking clustered full-detail foliage: "+definition.Id);
var source=VegetationModelData.Load(full);using var model=new PlantGpuModel(source,true);
foreach(var upload in model.Upload(gl))upload();
var variants=new List<Card[]>();
for(var variant=0;variant<source.Lods[0].Length;variant++)
{
var bounds=source.Lods[0][variant];var extent=bounds.Maximum-bounds.Minimum;
var cards=new List<Card>();var minima=new List<Vector3>();var maxima=new List<Vector3>();
for(var y=0;y<6;y++)for(var z=0;z<2;z++)for(var x=0;x<2;x++)
{
var min=bounds.Minimum+extent*new Vector3(x*.5f,y/6f,z*.5f);
var max=min+extent*new Vector3(.5f,1/6f,.5f);var center=(min+max)*.5f;
foreach(var direction in new[]{Vector3.UnitZ,Vector3.UnitX,Vector3.UnitY})
{
var right=Vector3.Normalize(Vector3.Cross(direction==Vector3.UnitY?Vector3.UnitZ:Vector3.UnitY,direction));
var up=Vector3.Cross(direction,right);var size=Vector3.Distance(min,max)*1.03f;var n=cards.Count;
cards.Add(new([center.X,center.Y,center.Z],[right.X,right.Y,right.Z],[up.X,up.Y,up.Z],[direction.X,direction.Y,direction.Z],size,
[(n%columns)/(float)columns,1-(n/columns+1)/(float)rows,1f/columns,1f/rows]));
minima.Add(min);maxima.Add(max);
}
}
variants.Add(cards.ToArray());
for(var mode=1;mode<=2;mode++)
{
target.Bind();gl.ClearColor(0,0,0,0);gl.ClearDepth(1);gl.Clear(ClearBufferMask.ColorBufferBit|ClearBufferMask.DepthBufferBit);
gl.Disable(GLEnum.Blend);gl.Disable(GLEnum.CullFace);gl.Enable(GLEnum.DepthTest);gl.DepthMask(true);gl.DepthFunc(GLEnum.Lequal);
program.Use();program.SetMatrix("uBodyToWorld",Matrix4x4.Identity);program.Set("uAnchor",Vector3.Zero);
program.Set("uTime",0f);program.Set("uFar",1000f);program.Set("uShadowPass",1);program.Set("uBake",mode);program.Set("uBakeCluster",1);
program.Set("uVegetationShadows",0);program.Set("uVegetationShadowMap",15);
for(var i=0;i<cards.Count;i++)
{
var c=cards[i];var center=new Vector3(c.Center[0],c.Center[1],c.Center[2]);var direction=new Vector3(c.Normal[0],c.Normal[1],c.Normal[2]);
var up=new Vector3(c.Up[0],c.Up[1],c.Up[2]);var view=Matrix4x4.CreateLookAt(center+direction*c.Size*2,center,up);
var rotation=view;rotation.Translation=Vector3.Zero;
program.SetMatrix("uBakeNormalBasis",rotation);program.Set("uBakeMinimum",minima[i]);program.Set("uBakeMaximum",maxima[i]);
program.SetMatrix("uViewProjection",view*Matrix4x4.CreateOrthographic(c.Size,c.Size,.01f,c.Size*5));
gl.Viewport(i%columns*cell,i/columns*cell,cell,cell);model.Draw(gl,program,0,variant,instance,1,canopyOnly:true);
}
gl.Finish();var pixels=new float[target.Width*target.Height*4];fixed(float* p=pixels)gl.ReadPixels(0,0,(uint)target.Width,(uint)target.Height,PixelFormat.Rgba,PixelType.Float,p);
PngOutput.Rgba(Path.Combine(prepared,$"cards-{variant}-{(mode==1?"color":"normal")}.png"),target.Width,target.Height,pixels);
}
Console.WriteLine($"Baked {definition.Id} variant {variant}: {cards.Count} canopy cards.");
}
File.WriteAllText(Path.Combine(prepared,"cards.json"),JsonSerializer.Serialize(new{Variants=variants}));
if(gl.GetError()!=GLEnum.NoError)throw new InvalidOperationException("Foliage baking OpenGL error.");
}
gl.BindFramebuffer(GLEnum.Framebuffer,0);
}
public static void Run(GL gl,string directory)
{
var catalog=VegetationCatalog.Load(Path.Combine(directory,"vegetation.json"));
using var program=ShaderProgram.Create(gl,ShaderSources.Load("vegetation.vert"),ShaderSources.Load("vegetation.frag"));
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);
if(Environment.GetCommandLineArgs().Contains("--bake-foliage"))
{BakeFoliage(gl,directory,catalog,program,instance);gl.DeleteBuffer(instance);return;}
const int views=16,elevations=3,cell=256;
using var target=new RenderTarget(gl,views*cell,elevations*cell,true);
foreach(var definition in catalog.Plants)
{
var modelPath=Path.GetFullPath(Path.Combine(directory,definition.Model));
var prepared=Path.GetDirectoryName(modelPath)!;
var prefix=Path.GetFileNameWithoutExtension(modelPath)=="dry"?"dry-":"";
if(File.Exists(Path.Combine(prepared,prefix+"impostors.json"))&&!Environment.GetCommandLineArgs().Contains("--force"))continue;
Console.WriteLine("Baking vegetation impostors: "+definition.Id);
var source=VegetationModelData.Load(modelPath);using var model=new PlantGpuModel(source);
foreach(var upload in model.Upload(gl))upload();
var bounds=new List<object>();
for(var variant=0;variant<source.Lods[0].Length;variant++)
{
var v=source.Lods[0][variant];var center=(v.Minimum+v.Maximum)*.5f;
var size=System.Math.Max(v.Maximum.Y-v.Minimum.Y,Vector2.Distance(new(v.Minimum.X,v.Minimum.Z),new(v.Maximum.X,v.Maximum.Z)))*1.12f;
bounds.Add(new{Center=new[]{center.X,center.Y,center.Z},Size=size});
for(var mode=1;mode<=2;mode++)
{
target.Bind();gl.ClearColor(0,0,0,0);gl.ClearDepth(1);gl.Clear(ClearBufferMask.ColorBufferBit|ClearBufferMask.DepthBufferBit);
gl.Disable(GLEnum.Blend);gl.Disable(GLEnum.CullFace);gl.Enable(GLEnum.DepthTest);gl.DepthMask(true);gl.DepthFunc(GLEnum.Lequal);
program.Use();program.SetMatrix("uBodyToWorld",Matrix4x4.Identity);program.Set("uAnchor",Vector3.Zero);
program.Set("uTime",0f);program.Set("uFar",1000f);program.Set("uShadowPass",1);program.Set("uBake",mode);
program.Set("uVegetationShadows",0);program.Set("uVegetationShadowMap",15);
for(var row=0;row<elevations;row++)for(var col=0;col<views;col++)
{
var azimuth=col*System.Math.Tau/views;var elevation=row*System.Math.PI/6;
var direction=new Vector3((float)(System.Math.Sin(azimuth)*System.Math.Cos(elevation)),(float)System.Math.Sin(elevation),(float)(System.Math.Cos(azimuth)*System.Math.Cos(elevation)));
var view=Matrix4x4.CreateLookAt(center+direction*size*2,center,Vector3.UnitY);
var projection=Matrix4x4.CreateOrthographic(size,size,.01f,size*5);
program.SetMatrix("uViewProjection",view*projection);
gl.Viewport(col*cell,row*cell,cell,cell);model.Draw(gl,program,0,variant,instance,1);
}
gl.Finish();var pixels=new float[target.Width*target.Height*4];fixed(float* p=pixels)gl.ReadPixels(0,0,(uint)target.Width,(uint)target.Height,PixelFormat.Rgba,PixelType.Float,p);
PngOutput.Rgba(Path.Combine(prepared,$"{prefix}impostor-{variant}-{(mode==1?"color":"normal")}.png"),target.Width,target.Height,pixels);
}
}
File.WriteAllText(Path.Combine(prepared,prefix+"impostors.json"),JsonSerializer.Serialize(new{Views=views,Elevations=elevations,Variants=bounds}));
if(gl.GetError()!=GLEnum.NoError)throw new InvalidOperationException("Vegetation impostor baking GL error.");
}
gl.DeleteBuffer(instance);gl.BindFramebuffer(GLEnum.Framebuffer,0);
Console.WriteLine("Vegetation impostors prepared successfully.");
}
}

View File

@@ -0,0 +1,468 @@
using System.Diagnostics;
using System.Numerics;
using System.Text.Json;
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 sealed unsafe class VegetationRenderer : IDisposable
{
private sealed record AtlasBounds(float[] Center,float Size);
private sealed record AtlasMetadata(int Views,int Elevations,AtlasBounds[] Variants);
private sealed record ModelSource(VegetationModelData Data,AtlasMetadata Metadata,RasterImage[] Colors,RasterImage[] Normals);
private sealed class Model
{
public required Task<ModelSource> Task;
public PlantGpuModel? Gpu;
public IEnumerator<Action>? Upload;
public PlantTexture[] Color=[],Normal=[];
public bool Ready;
public double ReadyAt;
public double ReadyFade;
}
private sealed class Batch {public readonly List<float> Data=[];public uint Buffer;public int CapacityBytes;public int Count=>Data.Count/12;}
private readonly GL _gl;private readonly string _root;
public VegetationCatalog Catalog {get;}
public VegetationStream? Stream {get;private set;}
public string? PlanetId {get;private set;}
private PlanetData? _planet;
private string? _systemId;
private DVector3 _cameraBody,_origin;
private readonly Dictionary<string,Model> _models=[];
private readonly HashSet<string> _requiredPaths=[];
private readonly Dictionary<(int Plant,int Lod,int Variant),Batch> _near=[];
private readonly Dictionary<(int Plant,int Variant),Batch> _far=[];
private readonly Dictionary<(int Plant,int Cascade,int Variant),Batch> _shadowBatches=[];
private ShaderProgram _surface,_impostor,_depth;
private readonly VegetationMesh _quad;
private uint _shadowMap,_shadowFramebuffer;private int _shadowSize;
private readonly Matrix4x4[] _shadowMatrices=new Matrix4x4[4];
private int _shadowLights;
private uint _nearestDepth;private int _depthWidth,_depthHeight;
private double _time;
private double _surfaceAltitude;
private long _farRevision=-1;private DVector3 _farOrigin;
private long _nearRevision=-1;private int _nearModels;private DVector3 _nearCamera;
private VegetationView? _nearView;
private readonly List<NearInstance> _nearCandidates=[];
private readonly record struct NearInstance(VegetationInstance Instance,int Variant,double Radius,double Distance,double Fade);
private sealed record FarSource(Dictionary<(int Plant,int Variant),Batch> Batches,DVector3 Origin,long Revision);
private Task<FarSource>? _farTask;private FarSource? _farBack;private IEnumerator<Action>? _farUpload;
private Dictionary<(int Plant,int Variant),Batch>? _farSpare;private uint[] _farBuildingBuffers=[];
private CancellationTokenSource _farCancellation=new();private long _lastFarBuild;
public bool Enabled {get;set;}=true;
public bool DepthReady {get;private set;}
public double Aspect {get;set;}=1.8;
public int Instances {get;private set;}
public long Triangles {get;private set;}
public int DrawCalls {get;private set;}
public int[] LodCounts {get;}=new int[4];
public double UploadMilliseconds {get;private set;}
public double CpuMilliseconds {get;private set;}
public int ReadyModels=>_requiredPaths.Count(path=>_models.TryGetValue(path,out var model)&&model.Ready);
public int RequiredModels=>_requiredPaths.Count;
public int DistantInstances=>_far.Values.Sum(b=>b.Count);
public VegetationRenderer(GL gl,string root)
{
_gl=gl;_root=root;Catalog=VegetationCatalog.Load(Path.Combine(root,"vegetation.json"));
_surface=Program("vegetation.vert","vegetation.frag");_impostor=Program("vegetation-impostor.vert","vegetation-impostor.frag");_depth=Program("vegetation.vert","vegetation-depth.frag");
_quad=new(gl,new([- .5f,-.5f,0,0,0,1,0,0,.5f,-.5f,0,0,0,1,1,0,.5f,.5f,0,0,0,1,1,1,-.5f,.5f,0,0,0,1,0,1],[0,1,2,0,2,3],0));
}
private ShaderProgram Program(string v,string f)=>ShaderProgram.Create(_gl,ShaderSources.Load(v),ShaderSources.Load(f));
public void ReloadShaders()
{
var surface=Program("vegetation.vert","vegetation.frag");var impostor=Program("vegetation-impostor.vert","vegetation-impostor.frag");var depth=Program("vegetation.vert","vegetation-depth.frag");
_surface.Dispose();_impostor.Dispose();_depth.Dispose();_surface=surface;_impostor=impostor;_depth=depth;
}
public void Update(FlyCamera camera,StarSystemData system,double time)
{
var watch=Stopwatch.StartNew();_time=time;Instances=0;Triangles=0;DrawCalls=0;DepthReady=false;CpuMilliseconds=0;UploadMilliseconds=0;Array.Clear(LodCounts);
Stream?.ResetFrameDiagnostics();
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);
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)
{
Stream?.Dispose();Stream=new(new(planet,Catalog));PlanetId=planet.Id;_farRevision=-1;
ResetFar();_systemId=system.Id;
_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());
if(_surfaceAltitude>10000){CpuMilliseconds=watch.Elapsed.TotalMilliseconds;return;}
Stream!.Update(_cameraBody);
var biome=Stream.Generator.Field.Weights(Stream.Generator.Field.Color(_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))
{ _models[definition.Model]=new(){Task=GenerationJobs.Run(()=>Load(definition.Model),CancellationToken.None)};break; }
PumpUploads(planet.Vegetation!.UploadBudgetMilliseconds*.45);
PumpFar(planet.Vegetation.UploadBudgetMilliseconds*.75);
foreach(var model in _models.Values)if(model.Ready)model.ReadyFade=System.Math.Clamp(Stopwatch.GetElapsedTime((long)model.ReadyAt).TotalSeconds/.5,0,1);
var ready=ReadyModels;
var view=new VegetationView(planet.Orientation.ToBody(camera.Forward),planet.Orientation.ToBody(camera.Right),
planet.Orientation.ToBody(camera.Up),System.Math.Tan(camera.FieldOfViewRadians*.5),Aspect);
if(_surfaceAltitude<planet.Vegetation.TreeDistanceMeters*1.2&&(_nearRevision!=Stream.Revision||_nearModels!=ready
||(_nearCamera-_cameraBody).Length>.5||_models.Values.Any(m=>m.Ready&&m.ReadyFade<1)))
{
BuildSpatialBatches();_nearRevision=Stream.Revision;_nearModels=ready;_nearCamera=_cameraBody;_nearView=null;
}
if(_nearView is null||!_nearView.Value.CanReuse(view))BuildViewBatches(view);
CpuMilliseconds=watch.Elapsed.TotalMilliseconds;
}
private ModelSource Load(string relative)
{
var path=Path.GetFullPath(Path.Combine(_root,relative));
if(!path.StartsWith(Path.GetFullPath(_root)+Path.DirectorySeparatorChar,StringComparison.OrdinalIgnoreCase))throw new InvalidDataException("Vegetation model must stay inside its catalog folder.");
var data=VegetationModelData.Load(path);var directory=Path.GetDirectoryName(path)!;var prefix=Path.GetFileNameWithoutExtension(path)=="dry"?"dry-":"";
var metadata=JsonSerializer.Deserialize<AtlasMetadata>(File.ReadAllText(Path.Combine(directory,prefix+"impostors.json")))!;
var colors=Enumerable.Range(0,metadata.Variants.Length).Select(i=>RasterImage.Load(Path.Combine(directory,$"{prefix}impostor-{i}-color.png"))).ToArray();
var normals=Enumerable.Range(0,metadata.Variants.Length).Select(i=>RasterImage.Load(Path.Combine(directory,$"{prefix}impostor-{i}-normal.png"))).ToArray();
foreach(var image in colors)PlantMipmaps.Get(image,true);foreach(var image in normals)PlantMipmaps.Get(image,false);
return new(data,metadata,colors,normals);
}
private IEnumerable<Action> UploadModel(Model model)
{
var source=model.Task.Result;model.Gpu=new(source.Data);
foreach(var action in model.Gpu.Upload(_gl))yield return action;
model.Color=new PlantTexture[source.Colors.Length];model.Normal=new PlantTexture[source.Normals.Length];
for(var i=0;i<model.Color.Length;i++)
{
model.Color[i]=new(_gl,source.Colors[i],true,true,true);foreach(var action in model.Color[i].UploadSteps())yield return action;
model.Normal[i]=new(_gl,source.Normals[i],false,deferred:true);foreach(var action in model.Normal[i].UploadSteps())yield return action;
}
model.Ready=true;model.ReadyAt=Stopwatch.GetTimestamp();_farRevision=-1;
}
private void PumpUploads(double budget)
{
var watch=Stopwatch.StartNew();
foreach(var model in _models.Values)
{
if(model.Ready)continue;
if(model.Task.IsFaulted)throw new InvalidOperationException("Vegetation model preparation failed.",model.Task.Exception);
if(!model.Task.IsCompletedSuccessfully)continue;
model.Upload??=UploadModel(model).GetEnumerator();
while(watch.Elapsed.TotalMilliseconds<budget)
{
if(!model.Upload.MoveNext()){model.Upload.Dispose();model.Upload=null;break;}
model.Upload.Current();
}
if(watch.Elapsed.TotalMilliseconds>=budget)break;
}
UploadMilliseconds=watch.Elapsed.TotalMilliseconds;
}
private static void Add(Batch batch,VegetationInstance i,DVector3 origin,VegetationDefinition definition,double fade)
{
var p=i.BodyPosition-origin;
var data=batch.Data;var start=data.Count;
System.Runtime.InteropServices.CollectionsMarshal.SetCount(data,start+12);
var span=System.Runtime.InteropServices.CollectionsMarshal.AsSpan(data)[start..];
span[0]=(float)p.X;span[1]=(float)p.Y;span[2]=(float)p.Z;span[3]=(float)i.Scale;
span[4]=(float)i.Up.X;span[5]=(float)i.Up.Y;span[6]=(float)i.Up.Z;span[7]=(float)i.Yaw;
span[8]=(float)i.Variation;span[9]=(float)definition.WindStrength;span[10]=(float)fade;span[11]=(float)(i.Id%65536)/65536;
}
private Batch Get<TKey>(Dictionary<TKey,Batch> dictionary,TKey key) where TKey:notnull
{if(!dictionary.TryGetValue(key,out var b)){b=new(){Buffer=_gl.GenBuffer()};dictionary[key]=b;}return b;}
private void UploadBatch(Batch batch)
{
var started=Stopwatch.GetTimestamp();
_gl.BindBuffer(GLEnum.ArrayBuffer,batch.Buffer);
var span=System.Runtime.InteropServices.CollectionsMarshal.AsSpan(batch.Data);
fixed(float* p=span)_gl.BufferData(GLEnum.ArrayBuffer,(nuint)(span.Length*4),p,GLEnum.StreamDraw);
UploadMilliseconds+=Stopwatch.GetElapsedTime(started).TotalMilliseconds;
}
private void PumpFar(double budget)
{
var watch=Stopwatch.StartNew();
if(_farTask is {IsCompleted:true} task&&_farBack is null)
{
_farTask=null;_farBuildingBuffers=[];
if(task.IsFaulted)throw new InvalidOperationException("Distant forest generation failed.",task.Exception);
if(task.IsCompletedSuccessfully){_farBack=task.Result;_farUpload=UploadFar(_farBack).GetEnumerator();}
}
while(_farUpload is not null&&watch.Elapsed.TotalMilliseconds+UploadMilliseconds<budget)
{
if(_farUpload.MoveNext())_farUpload.Current();
else
{
_farUpload.Dispose();_farUpload=null;
_farSpare=_far.ToDictionary(p=>p.Key,p=>p.Value);_far.Clear();
foreach(var pair in _farBack!.Batches)_far.Add(pair.Key,pair.Value);
_farOrigin=_farBack.Origin;_farRevision=_farBack.Revision;_farBack=null;
}
}
UploadMilliseconds+=watch.Elapsed.TotalMilliseconds;
var stream=Stream!;var revision=stream.TreeRevision*256+ReadyModels;
var origin=_cameraBody.Normalized()*_planet!.RadiusMeters;
if(_farTask is null&&_farBack is null&&(_farRevision!=revision||(_farOrigin-origin).Length>150)
&&Stopwatch.GetElapsedTime(_lastFarBuild).TotalSeconds>.12)
{
_lastFarBuild=Stopwatch.GetTimestamp();
var tiles=stream.Tiles.Where(t=>t.Key.Layer==0).ToArray();var cameraBody=_cameraBody;
var range=_planet.Vegetation!.TreeDistanceMeters+200;var token=_farCancellation.Token;
var variants=Catalog.Plants.Select(p=>_models.TryGetValue(p.Model,out var m)&&m.Ready?m.Task.Result.Data.Lods[0].Length:0).ToArray();
var batches=_farSpare??new Dictionary<(int Plant,int Variant),Batch>();_farSpare=null;
_farBuildingBuffers=batches.Values.Where(b=>b.Buffer!=0).Select(b=>b.Buffer).ToArray();
_farTask=GenerationJobs.Run(()=>
{
foreach(var batch in batches.Values)batch.Data.Clear();
foreach(var tile in tiles)
{
token.ThrowIfCancellationRequested();
foreach(var instance in tile.Instances)
{
var count=variants[instance.PlantIndex];if(count==0||(instance.BodyPosition-cameraBody).Length>range)continue;
var key=(instance.PlantIndex,(int)(instance.Id%(uint)count));
if(!batches.TryGetValue(key,out var batch)){batch=new();batches.Add(key,batch);}
Add(batch,instance,origin,Catalog.Plants[instance.PlantIndex],1);
}
}
return new FarSource(batches,origin,revision);
},token);
}
}
private IEnumerable<Action> UploadFar(FarSource source)
{
foreach(var batch in source.Batches.Values)
{
if(batch.Count==0)continue;
yield return ()=>{unsafe{
if(batch.Buffer==0)batch.Buffer=_gl.GenBuffer();_gl.BindBuffer(GLEnum.ArrayBuffer,batch.Buffer);
if(batch.Data.Count*4>batch.CapacityBytes){batch.CapacityBytes=(int)System.Numerics.BitOperations.RoundUpToPowerOf2((uint)(batch.Data.Count*4));_gl.BufferData(GLEnum.ArrayBuffer,(nuint)batch.CapacityBytes,null,GLEnum.StaticDraw);}
}};
for(var offset=0;offset<batch.Data.Count;offset+=65536)
{
var start=offset;var count=System.Math.Min(65536,batch.Data.Count-start);
yield return ()=>
{
_gl.BindBuffer(GLEnum.ArrayBuffer,batch.Buffer);
var span=System.Runtime.InteropServices.CollectionsMarshal.AsSpan(batch.Data).Slice(start,count);
unsafe{fixed(float* p=span)_gl.BufferSubData(GLEnum.ArrayBuffer,(nint)(start*4),(nuint)(count*4),p);}
};
}
}
}
private void ResetFar()
{
_farCancellation.Cancel();_farCancellation.Dispose();_farCancellation=new();_farTask=null;
_farUpload?.Dispose();_farUpload=null;
var buffers=_far.Values.Concat(_farBack?.Batches.Values.AsEnumerable()??Enumerable.Empty<Batch>()).Concat(_farSpare?.Values.AsEnumerable()??Enumerable.Empty<Batch>()).Select(b=>b.Buffer).Concat(_farBuildingBuffers).Where(b=>b!=0).Distinct();
foreach(var buffer in buffers){_quad.ForgetInstances(buffer);_gl.DeleteBuffer(buffer);}
_farBack=null;_farSpare=null;_farBuildingBuffers=[];_far.Clear();_farRevision=-1;
_nearRevision=-1;
_nearCandidates.Clear();_nearView=null;
foreach(var b in _near.Values)b.Data.Clear();foreach(var b in _shadowBatches.Values)b.Data.Clear();
}
private void BuildSpatialBatches()
{
var stream=Stream!;var settings=_planet!.Vegetation!;
_nearCandidates.Clear();foreach(var batch in _shadowBatches.Values)batch.Data.Clear();
_origin=_cameraBody.Normalized()*_planet.RadiusMeters;
var nearRange=System.Math.Max(settings.ShadowDistancesMeters[1]+60,Catalog.Plants.Where(p=>p.Type==VegetationType.Tree)
.Select(p=>(p.LodMeters??settings.TreeLodMeters)[2]*1.1).DefaultIfEmpty(0).Max());
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;
if(tileDistance>nearRange+256)continue;
foreach(var instance in tile.Instances)
{
var definition=Catalog.Plants[instance.PlantIndex];
if(!_models.TryGetValue(definition.Model,out var model)||!model.Ready)continue;
var source=model.Task.Result;var variant=(int)(instance.Id%(uint)source.Data.Lods[0].Length);
var delta=instance.BodyPosition-_cameraBody;var distance=delta.Length;
if(distance>System.Math.Min(stream.Generator.Range(tile.Key.Layer),definition.DistanceMeters??double.PositiveInfinity)+20)continue;
if(distance>nearRange&&definition.Type==VegetationType.Tree)continue;
if(distance>stream.Generator.Range(tile.Key.Layer)+20)continue;
for(var cascade=0;cascade<2;cascade++)
if(distance<settings.ShadowDistancesMeters[cascade]+30&&(tile.Key.Layer<2||distance<30))
Add(Get(_shadowBatches,(instance.PlantIndex,cascade,variant)),instance,_origin,definition,1);
_nearCandidates.Add(new(instance,variant,source.Metadata.Variants[variant].Size*instance.Scale+.75,distance,model.ReadyFade));
}
}
// Shadow casters depend on position and loaded tiles, never on head rotation.
foreach(var b in _shadowBatches.Values)if(b.Count>0)UploadBatch(b);
}
private void BuildViewBatches(VegetationView view)
{
var stream=Stream!;var settings=_planet!.Vegetation!;
foreach(var batch in _near.Values)batch.Data.Clear();
foreach(var candidate in _nearCandidates)
{
var instance=candidate.Instance;var definition=Catalog.Plants[instance.PlantIndex];
if(!view.Intersects(instance.BodyPosition-_cameraBody,candidate.Radius))continue;
var distance=candidate.Distance;var variant=candidate.Variant;var readyFade=candidate.Fade;
if(definition.Type==VegetationType.Tree)
{
var thresholds=definition.LodMeters??settings.TreeLodMeters;
for(var lod=0;lod<3;lod++)
{
var begin=lod==0?0:thresholds[lod-1];var end=thresholds[lod];
if((lod==0||distance>begin*.9-1)&&distance<end*1.1+1)
Add(Get(_near,(instance.PlantIndex,lod,variant)),instance,_origin,definition,readyFade);
}
}
else
{
var max=System.Math.Min(stream.Generator.Range(VegetationTileGenerator.Layer(definition.Type)),definition.DistanceMeters??double.PositiveInfinity);var lod=distance<max*.25?0:distance<max*.6?1:2;
if(distance<max+1)Add(Get(_near,(instance.PlantIndex,lod,variant)),instance,_origin,definition,readyFade);
}
}
foreach(var b in _near.Values)if(b.Count>0)UploadBatch(b);
_nearView=view;
}
private static double Smooth(double a,double b,double x){var t=System.Math.Clamp((x-a)/(b-a),0,1);return t*t*(3-2*t);}
public void DrawShadows(FlyCamera camera,StarSystemData system)
{
_shadowLights=0;if(!Enabled||_planet is null||Stream is null||_surfaceAltitude>1000||!_shadowBatches.Values.Any(b=>b.Count>0))return;
var settings=_planet.Vegetation!;
if(_shadowSize!=settings.ShadowResolution)
{
if(_shadowMap!=0){_gl.DeleteTexture(_shadowMap);_gl.DeleteFramebuffer(_shadowFramebuffer);}
_shadowSize=settings.ShadowResolution;_shadowMap=_gl.GenTexture();_shadowFramebuffer=_gl.GenFramebuffer();
_gl.BindTexture(GLEnum.Texture2DArray,_shadowMap);
_gl.TexImage3D(GLEnum.Texture2DArray,0,InternalFormat.DepthComponent24,(uint)_shadowSize,(uint)_shadowSize,4,0,PixelFormat.DepthComponent,PixelType.Float,null);
_gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureMinFilter,(int)GLEnum.Linear);_gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureMagFilter,(int)GLEnum.Linear);
_gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureCompareMode,(int)GLEnum.CompareRefToTexture);
_gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureCompareFunc,(int)GLEnum.Lequal);
_gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureWrapS,(int)GLEnum.ClampToEdge);_gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureWrapT,(int)GLEnum.ClampToEdge);
}
_gl.BindFramebuffer(GLEnum.Framebuffer,_shadowFramebuffer);_gl.DrawBuffer(GLEnum.None);_gl.ReadBuffer(GLEnum.None);
_gl.Enable(GLEnum.DepthTest);_gl.DepthMask(true);_gl.DepthFunc(GLEnum.Lequal);_gl.Disable(GLEnum.Blend);_gl.Disable(GLEnum.CullFace);
_gl.Viewport(0,0,(uint)_shadowSize,(uint)_shadowSize);
_depth.Use();_depth.Set("uTime",(float)_time);_depth.Set("uShadowPass",1);_depth.Set("uAnchor",_planet.Orientation.ToWorld(_origin-_cameraBody).ToVector3());_depth.SetMatrix("uBodyToWorld",_planet.Orientation.BodyToWorld);
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 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;
var texel=radius*2/_shadowSize;
var snapped=axis*(System.Math.Floor(DVector3.Dot(target,axis)/texel)*texel)
+vertical*(System.Math.Floor(DVector3.Dot(target,vertical)/texel)*texel)+lightBody*DVector3.Dot(target,lightBody);
var relative=_planet.Orientation.ToWorld(snapped-_cameraBody);
var view=Matrix4x4.CreateLookAt((relative+ld*radius*2).ToVector3(),relative.ToVector3(),_planet.Orientation.ToWorld(vertical).ToVector3());
var projection=Matrix4x4.CreateOrthographic((float)radius*2,(float)radius*2,.1f,(float)radius*4);
var index=light*2+cascade;_shadowMatrices[index]=view*projection;
_gl.FramebufferTextureLayer(GLEnum.Framebuffer,GLEnum.DepthAttachment,_shadowMap,0,index);
if(_gl.CheckFramebufferStatus(GLEnum.Framebuffer)!=GLEnum.FramebufferComplete)throw new InvalidOperationException("Vegetation shadow framebuffer incomplete.");
_gl.ClearDepth(1);_gl.Clear(ClearBufferMask.DepthBufferBit);_depth.SetMatrix("uViewProjection",_shadowMatrices[index]);
foreach(var (key,batch) in _shadowBatches)
if(key.Cascade==cascade&&batch.Count>0)_models[Catalog.Plants[key.Plant].Model].Gpu!.Draw(_gl,_depth,2,key.Variant,batch.Buffer,batch.Count,true);
}
_shadowLights=system.Stars.Count;
}
public void BindShadows(ShaderProgram program,string? planetId,int textureUnit=15)
{
program.Set("uVegetationShadowMap",textureUnit);program.Set("uVegetationShadows",Enabled&&planetId==PlanetId&&_shadowLights>0?1:0);
_gl.ActiveTexture((GLEnum)((int)GLEnum.Texture0+textureUnit));_gl.BindTexture(GLEnum.Texture2DArray,_shadowMap);
var distances=_planet?.Vegetation?.ShadowDistancesMeters??[100d,400d];
program.Set("uVegetationShadowDistances",new Vector3((float)distances[0],(float)distances[1],0));
var settings=_planet?.Vegetation??VegetationSettings.Default;
program.Set("uVegetationShadowSettings",new Vector3((float)settings.ShadowSoftness,(float)settings.ShadowCascadeBlend,(float)settings.ShadowBiasMeters));
for(var i=0;i<4;i++)program.SetMatrix($"uVegetationShadowMatrices[{i}]",_shadowMatrices[i]);
}
private void Lighting(ShaderProgram program,StarSystemData system)
{
program.Set("uPlanetUp",_planet!.Orientation.ToWorld(_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());
program.SetFloatArray("uLightFlux",system.Stars.Select(s=>(float)System.Math.Clamp(s.LuminositySolar/System.Math.Pow((s.PositionMeters-_planet!.PositionMeters).Length/UniverseScale.AstronomicalUnitMeters,2),.02,7)).ToArray());
BindShadows(program,PlanetId);
}
public void PrepareDepth(FlyCamera camera,StarSystemData system,Matrix4x4 vp,float far,int width,int height)
{
if(!Enabled||_planet is null||Stream is null||_surfaceAltitude>_planet.Vegetation!.TreeDistanceMeters*1.2)return;
if(!_planet.Vegetation.DepthPrepass)return;
_gl.ColorMask(false,false,false,false);DrawPass(camera,system,vp,far,true);_gl.ColorMask(true,true,true,true);
CaptureDepth(width,height);
}
private void CaptureDepth(int width,int height)
{
_gl.ActiveTexture(GLEnum.Texture3);
if(_nearestDepth==0)_nearestDepth=_gl.GenTexture();_gl.BindTexture(GLEnum.Texture2D,_nearestDepth);
if(width!=_depthWidth||height!=_depthHeight)
{
_depthWidth=width;_depthHeight=height;
_gl.TexImage2D(GLEnum.Texture2D,0,InternalFormat.DepthComponent32f,(uint)width,(uint)height,0,PixelFormat.DepthComponent,PixelType.Float,null);
_gl.TexParameter(GLEnum.Texture2D,GLEnum.TextureMinFilter,(int)GLEnum.Nearest);_gl.TexParameter(GLEnum.Texture2D,GLEnum.TextureMagFilter,(int)GLEnum.Nearest);
}
_gl.CopyTexSubImage2D(GLEnum.Texture2D,0,0,0,0,0,(uint)width,(uint)height);
DepthReady=true;
}
public void BindDepthCulling(ShaderProgram program,bool enabled)
{
program.Set("uVegetationDepthCullEnabled",DepthReady&&enabled?1:0);program.Set("uVegetationDepth",3);
_gl.ActiveTexture(GLEnum.Texture3);_gl.BindTexture(GLEnum.Texture2D,_nearestDepth);
}
public void Draw(FlyCamera camera,StarSystemData system,Matrix4x4 vp,float far,int width,int height)
{
if(!Enabled||_planet is null||Stream is null||_surfaceAltitude>_planet.Vegetation!.TreeDistanceMeters*1.2)return;
if(_planet.Vegetation.DepthPrepass&&!DepthReady)PrepareDepth(camera,system,vp,far,width,height);
DrawPass(camera,system,vp,far,false);
// This pass runs before terrain: reuse its exact alpha/log-depth coverage to reject hidden ground.
CaptureDepth(width,height);
}
private void DrawPass(FlyCamera camera,StarSystemData system,Matrix4x4 vp,float far,bool depthOnly)
{
if(!Enabled||_planet is null||Stream is null||_surfaceAltitude>_planet.Vegetation!.TreeDistanceMeters*1.2)return;
_gl.Enable(GLEnum.DepthTest);_gl.DepthMask(true);_gl.Disable(GLEnum.Blend);_gl.Disable(GLEnum.CullFace);
_surface.Use();_surface.SetMatrix("uViewProjection",vp);_surface.SetMatrix("uBodyToWorld",_planet.Orientation.BodyToWorld);
_surface.Set("uDepthOnly",depthOnly?1:_planet.Vegetation!.DepthPrepass?0:-1);_surface.Set("uNearestDepth",3);
_surface.Set("uAnchor",_planet.Orientation.ToWorld(_origin-_cameraBody).ToVector3());_surface.Set("uFar",far);_surface.Set("uTime",(float)_time);_surface.Set("uShadowPass",0);_surface.Set("uBake",0);Lighting(_surface,system);
foreach(var (key,batch) in _near)
{
if(batch.Count==0)continue;var definition=Catalog.Plants[key.Plant];
_surface.Set("uDitherInvert",key.Lod%2);
var range=System.Math.Min(Stream!.Generator.Range(VegetationTileGenerator.Layer(definition.Type)),definition.DistanceMeters??double.PositiveInfinity);
var thresholds=definition.LodMeters??_planet.Vegetation!.TreeLodMeters;
_surface.Set("uLodNearFar",definition.Type==VegetationType.Tree?new Vector3(key.Lod==0?0:(float)thresholds[key.Lod-1],(float)thresholds[key.Lod],(float)range):new Vector3(0,(float)(range*4),(float)-range));
var triangles=_models[definition.Model].Gpu!.Draw(_gl,_surface,key.Lod,key.Variant,batch.Buffer,batch.Count,shadow:depthOnly);
if(!depthOnly){Triangles+=triangles;Instances+=batch.Count;LodCounts[key.Lod]+=batch.Count;}DrawCalls+=_models[definition.Model].Gpu!.Data.Lods[key.Lod][key.Variant].Primitives.Length;
}
if(depthOnly)return; // Nearby canopy is enough for terrain occlusion; distant impostors are drawn once.
_impostor.Use();_impostor.SetMatrix("uViewProjection",vp);_impostor.SetMatrix("uBodyToWorld",_planet.Orientation.BodyToWorld);
_impostor.Set("uDepthOnly",depthOnly?1:_planet.Vegetation!.DepthPrepass?0:-1);_impostor.Set("uNearestDepth",3);
_impostor.Set("uAnchor",_planet.Orientation.ToWorld(_farOrigin-_cameraBody).ToVector3());_impostor.Set("uBatchBodyOrigin",_farOrigin.ToVector3());_impostor.Set("uCameraBody",_cameraBody.ToVector3());
_impostor.Set("uCameraOffset",(_cameraBody-_farOrigin).ToVector3());
_impostor.Set("uFar",far);_impostor.Set("uTime",(float)_time);Lighting(_impostor,system);
var lod=_planet.Vegetation.TreeLodMeters[2];_impostor.Set("uImpostorRange",new Vector3((float)(lod*.9),(float)(lod*1.1),(float)_planet.Vegetation.TreeDistanceMeters));
foreach(var (key,batch) in _far)
{
if(batch.Count==0)continue;var model=_models[Catalog.Plants[key.Plant].Model];if(!model.Ready)continue;
var definition=Catalog.Plants[key.Plant];var threshold=(definition.LodMeters??_planet.Vegetation.TreeLodMeters)[2];
_impostor.Set("uImpostorRange",new Vector3((float)(threshold*.9),(float)(threshold*1.1),(float)System.Math.Min(_planet.Vegetation.TreeDistanceMeters,definition.DistanceMeters??double.PositiveInfinity)));
var bounds=model.Task.Result.Metadata.Variants[key.Variant];_impostor.Set("uModelCenter",new Vector3(bounds.Center[0],bounds.Center[1],bounds.Center[2]));_impostor.Set("uBillboardSize",bounds.Size);
_gl.ActiveTexture(GLEnum.Texture0);_gl.BindTexture(GLEnum.Texture2D,model.Color[key.Variant].Handle);_impostor.Set("uColor",0);
_gl.ActiveTexture(GLEnum.Texture1);_gl.BindTexture(GLEnum.Texture2D,model.Normal[key.Variant].Handle);_impostor.Set("uNormal",1);
_quad.Draw(batch.Buffer,batch.Count);if(!depthOnly){Instances+=batch.Count;LodCounts[3]+=batch.Count;Triangles+=batch.Count*2;}DrawCalls++;
}
}
public TrunkCapsule[] Trunks(string planetId,DVector3 bodyPosition)
{
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)
.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();
}
public void ClearTiles(){Stream?.Clear();ResetFar();}
public void Dispose()
{
ResetFar();_farCancellation.Cancel();_farCancellation.Dispose();Stream?.Dispose();_surface.Dispose();_impostor.Dispose();_depth.Dispose();_quad.Dispose();
foreach(var m in _models.Values){m.Upload?.Dispose();m.Gpu?.Dispose();foreach(var t in m.Color)t?.Dispose();foreach(var t in m.Normal)t?.Dispose();}
foreach(var b in _near.Values)_gl.DeleteBuffer(b.Buffer);foreach(var b in _far.Values)_gl.DeleteBuffer(b.Buffer);foreach(var b in _shadowBatches.Values)_gl.DeleteBuffer(b.Buffer);
if(_shadowMap!=0){_gl.DeleteTexture(_shadowMap);_gl.DeleteFramebuffer(_shadowFramebuffer);}
if(_nearestDepth!=0)_gl.DeleteTexture(_nearestDepth);
}
}