skog, mjuka skuggor, jämnare övergång vatten på av avstånd
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
91
src/Universe.App/Graphics/GraphicsReport.Lookaround.cs
Normal file
91
src/Universe.App/Graphics/GraphicsReport.Lookaround.cs
Normal 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);
|
||||
}
|
||||
81
src/Universe.App/Graphics/GraphicsReport.Ocean.cs
Normal file
81
src/Universe.App/Graphics/GraphicsReport.Ocean.cs
Normal 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}.");
|
||||
}
|
||||
}
|
||||
86
src/Universe.App/Graphics/GraphicsReport.Shadows.cs
Normal file
86
src/Universe.App/Graphics/GraphicsReport.Shadows.cs
Normal 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);}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
61
src/Universe.App/Graphics/PlantGpuModel.cs
Normal file
61
src/Universe.App/Graphics/PlantGpuModel.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
27
src/Universe.App/Graphics/PngOutput.cs
Normal file
27
src/Universe.App/Graphics/PngOutput.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
31
src/Universe.App/Graphics/Shaders/biomes.glsl
Normal file
31
src/Universe.App/Graphics/Shaders/biomes.glsl
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
56
src/Universe.App/Graphics/Shaders/vegetation-shadow.glsl
Normal file
56
src/Universe.App/Graphics/Shaders/vegetation-shadow.glsl
Normal 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);
|
||||
}
|
||||
80
src/Universe.App/Graphics/Shaders/vegetation.frag.glsl
Normal file
80
src/Universe.App/Graphics/Shaders/vegetation.frag.glsl
Normal 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);
|
||||
}
|
||||
46
src/Universe.App/Graphics/Shaders/vegetation.vert.glsl
Normal file
46
src/Universe.App/Graphics/Shaders/vegetation.vert.glsl
Normal 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;
|
||||
}
|
||||
@@ -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);}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
126
src/Universe.App/Graphics/VegetationMesh.cs
Normal file
126
src/Universe.App/Graphics/VegetationMesh.cs
Normal 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)!;
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/Universe.App/Graphics/VegetationModel.cs
Normal file
66
src/Universe.App/Graphics/VegetationModel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
120
src/Universe.App/Graphics/VegetationPreparation.cs
Normal file
120
src/Universe.App/Graphics/VegetationPreparation.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
468
src/Universe.App/Graphics/VegetationRenderer.cs
Normal file
468
src/Universe.App/Graphics/VegetationRenderer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
113
src/Universe.App/Runtime/ForestBenchmark.cs
Normal file
113
src/Universe.App/Runtime/ForestBenchmark.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Universe.Core.Generation;
|
||||
using Universe.Core.Math;
|
||||
using Universe.Core.Physics;
|
||||
using Universe.Core.World;
|
||||
|
||||
namespace Universe.App.Runtime;
|
||||
|
||||
internal sealed class ForestBenchmark : IBenchmarkSession
|
||||
{
|
||||
private readonly PlanetData _planet;private readonly UniversePosition _body,_home;private readonly DVector3 _homeForward,_homeUp;
|
||||
private readonly double _cell;private readonly DVector3 _radial,_tangent,_across;
|
||||
private readonly List<Frame> _frames=[];private readonly List<double> _graph=[];
|
||||
private double _elapsed,_rendered,_graphTime;private int _graphFrames;private bool _stopped;
|
||||
private double _cpu,_upload,_vegetationGpu,_shadowGpu,_generation;private int _instances,_tiles,_pending,_models;private long _triangles;private int[] _lod=[0,0,0,0];
|
||||
private readonly Func<string,DVector3,TrunkCapsule[]>? _trunks;
|
||||
private sealed record Frame(double Time,double Milliseconds,string Phase,double CpuMs,double UploadMs,double VegetationMs,double ShadowMs,int Instances,long Triangles,int Tiles,int Pending,int Models,double GenerationMs,int[] Lod);
|
||||
public const double Duration=96;
|
||||
public DVector3 LandingBodyDirection {get;}
|
||||
public ForestBenchmark(StarSystemData system,PlanetData planet,VegetationCatalog catalog,FlyCamera camera,
|
||||
UniversePosition home,DVector3 homeForward,DVector3 homeUp,Func<string,DVector3,TrunkCapsule[]>? trunks=null)
|
||||
{
|
||||
_planet=planet;_cell=camera.CellSizeMeters;_home=home;_homeForward=homeForward;_homeUp=homeUp;_trunks=trunks;
|
||||
_body=UniversePosition.AtCellCenter(system.Coordinate,_cell).Add(planet.PositionMeters,_cell);
|
||||
var field=new BiomeField(planet,catalog);
|
||||
LandingBodyDirection=VegetationSiteFinder.FindForest(field,planet.Orientation.ToBody((system.Stars[planet.HostStarIndex].PositionMeters-planet.PositionMeters).Normalized()));
|
||||
_radial=planet.Orientation.ToWorld(LandingBodyDirection);
|
||||
_tangent=DVector3.Cross(planet.Orientation.ToWorld(new(0,1,0)),_radial).Normalized();_across=DVector3.Cross(_radial,_tangent).Normalized();
|
||||
camera.SetPlayerPose(_body.Add(_radial*(planet.RadiusMeters*4.8),_cell),-_radial,_across,true);
|
||||
}
|
||||
public bool IsRunning=>!_stopped&&_elapsed<Duration;
|
||||
public bool IsComplete=>!_stopped&&_elapsed>=Duration;
|
||||
public bool HasRun=>true;
|
||||
public double ElapsedSeconds=>_elapsed;
|
||||
public double DurationSeconds=>Duration;
|
||||
public long GraphRevision {get;private set;}
|
||||
public IReadOnlyList<double> GraphSamples=>_graph;
|
||||
public string TargetLabel=>"EARTH FOREST";
|
||||
public double TargetEarthRadii=>_planet.RadiusMeters/UniverseScale.EarthRadiusMeters;
|
||||
public double CurrentFps {get;private set;}
|
||||
public double AverageFps=>_rendered>0?_frames.Count/_rendered:0;
|
||||
public double MinimumFps=>_frames.Count>0?1000/_frames.Max(f=>f.Milliseconds):0;
|
||||
public double OnePercentLowFps=>_frames.Count>0?1000/_frames.OrderByDescending(f=>f.Milliseconds).Take(System.Math.Max(1,(int)System.Math.Ceiling(_frames.Count*.01))).Average(f=>f.Milliseconds):0;
|
||||
public double MaximumFrameMilliseconds=>_frames.Count>0?_frames.Max(f=>f.Milliseconds):0;
|
||||
public double VegetationGpuP99Milliseconds
|
||||
{get{if(_frames.Count==0)return 0;var values=_frames.Select(f=>f.VegetationMs+f.ShadowMs).Order().ToArray();return values[(int)((values.Length-1)*.99)];}}
|
||||
public bool PerformancePassed=>AverageFps>=60-1e-9&&OnePercentLowFps>=55-1e-9&&MaximumFrameMilliseconds<=50+1e-9&&VegetationGpuP99Milliseconds<=5+1e-9;
|
||||
public string Phase=>_stopped?"STOPPED":IsComplete?"COMPLETE":_elapsed<8?"SPACE VIEW":_elapsed<40?"FOREST DESCENT":_elapsed<64?"DENSE FOREST":_elapsed<80?"FOREST EDGE":"ASCENT";
|
||||
public string? ReportDirectory {get;private set;}
|
||||
public void Stop()=>_stopped=true;
|
||||
private static double Smooth(double t){t=System.Math.Clamp(t,0,1);return t*t*t*(10+t*(-15+6*t));}
|
||||
public void Update(double seconds,FlyCamera camera)
|
||||
{
|
||||
if(!IsRunning||!double.IsFinite(seconds)||seconds<=0)return;
|
||||
_elapsed=System.Math.Min(Duration,_elapsed+System.Math.Min(seconds,.1));
|
||||
var radial=_radial;var ground=TerrainHeight.Sample(_planet,radial)+1.8;double height;
|
||||
var forward=-radial;var up=_across;
|
||||
if(_elapsed<8)height=_planet.RadiusMeters*4.8;
|
||||
else if(_elapsed<40)
|
||||
{
|
||||
var t=(_elapsed-8)/32;
|
||||
height=t<.5?_planet.RadiusMeters*4.8+(ground+10000-_planet.RadiusMeters*4.8)*Smooth(t*2):ground+10000*(1-Smooth((t-.5)*2));
|
||||
var look=Smooth((t-.25)/.75);forward=(-radial*(1-look)+(_tangent-radial*.12)*look).Normalized();up=radial;
|
||||
}
|
||||
else
|
||||
{
|
||||
var walk=System.Math.Min(40,_elapsed-40);var distance=walk*4.5*Smooth(walk/2);
|
||||
var direction=(_tangent+_across*.15*System.Math.Sin(walk*.09)).Normalized();
|
||||
radial=(_radial*System.Math.Cos(distance/_planet.RadiusMeters)+direction*System.Math.Sin(distance/_planet.RadiusMeters)).Normalized();
|
||||
height=TerrainHeight.Sample(_planet,radial)+1.8;
|
||||
var head=.55*System.Math.Sin(walk*.18);forward=(_tangent*System.Math.Cos(head)+_across*System.Math.Sin(head)-radial*.08).Normalized();up=radial;
|
||||
if(_elapsed>=80)
|
||||
{
|
||||
var t=Smooth((_elapsed-80)/16);height+=15000*t;
|
||||
forward=(forward*(1-t)-radial*t).Normalized();
|
||||
}
|
||||
}
|
||||
var position=_body.Add(radial*height,_cell);
|
||||
if(_elapsed>=40&&_elapsed<80&&_trunks is not null)
|
||||
{
|
||||
var start=_planet.Orientation.ToBody(camera.Position.RelativeTo(_body,_cell));var end=_planet.Orientation.ToBody(radial*height);
|
||||
var resolved=VegetationCollision.Move(start,end,_planet.Orientation.ToBody(radial),1.8,_trunks(_planet.Id,start));
|
||||
var world=_planet.Orientation.ToWorld(resolved);var r=world.Normalized();
|
||||
position=_body.Add(r*(TerrainHeight.Sample(_planet,r)+1.8),_cell);
|
||||
}
|
||||
if(_elapsed>=Duration){camera.SetPlayerPose(_home,_homeForward,_homeUp,true);return;}
|
||||
up=(up-forward*DVector3.Dot(up,forward)).Normalized();if(up==DVector3.Zero)up=_across;
|
||||
camera.SetPlayerPose(position,forward,up);
|
||||
}
|
||||
public void Metrics(double cpu,double upload,double vegetationGpu,double shadows,int instances,long triangles,int tiles,int pending,int models,int[] lod,double generation=0)
|
||||
{_cpu=cpu;_upload=upload;_vegetationGpu=vegetationGpu;_shadowGpu=shadows;_instances=instances;_triangles=triangles;_tiles=tiles;_pending=pending;_models=models;_generation=generation;_lod=(int[])lod.Clone();}
|
||||
public void RecordFrame(double seconds)
|
||||
{
|
||||
if(_stopped||(_frames.Count>0&&_frames[^1].Time>=Duration)||!double.IsFinite(seconds)||seconds<=0)return;
|
||||
CurrentFps=1/seconds;_rendered+=seconds;_graphTime+=seconds;_graphFrames++;
|
||||
_frames.Add(new(_elapsed,seconds*1000,Phase,_cpu,_upload,_vegetationGpu,_shadowGpu,_instances,_triangles,_tiles,_pending,_models,_generation,_lod));
|
||||
if(_graphTime>=.2){_graph.Add(_graphFrames/_graphTime);_graphFrames=0;_graphTime=0;GraphRevision++;}
|
||||
}
|
||||
public void SaveReport(string? directory=null)
|
||||
{
|
||||
if(ReportDirectory is not null)return;
|
||||
ReportDirectory=directory??Path.Combine(AppContext.BaseDirectory,"benchmark-results","forest-"+DateTime.UtcNow.ToString("yyyyMMdd-HHmmss"));Directory.CreateDirectory(ReportDirectory);
|
||||
using(var csv=new StreamWriter(Path.Combine(ReportDirectory,"frames.csv")))
|
||||
{
|
||||
csv.WriteLine("time_seconds,frame_ms,phase,cpu_ms,upload_ms,vegetation_gpu_ms,shadow_gpu_ms,instances,triangles,tiles,pending,models,generation_worker_ms,lod0,lod1,lod2,impostors");
|
||||
foreach(var f in _frames)csv.WriteLine(string.Join(",",new object[]{f.Time,f.Milliseconds,f.Phase,f.CpuMs,f.UploadMs,f.VegetationMs,f.ShadowMs,f.Instances,f.Triangles,f.Tiles,f.Pending,f.Models,f.GenerationMs}.Concat(f.Lod.Cast<object>()).Select(v=>v is IFormattable n?n.ToString(null,CultureInfo.InvariantCulture):v.ToString())));
|
||||
}
|
||||
File.WriteAllText(Path.Combine(ReportDirectory,"summary.json"),JsonSerializer.Serialize(new{AverageFps,OnePercentLowFps,MinimumFps,MaximumFrameMilliseconds,VegetationGpuP99Milliseconds,
|
||||
Passed=PerformancePassed,Complete=IsComplete,Landing=new[]{LandingBodyDirection.X,LandingBodyDirection.Y,LandingBodyDirection.Z},
|
||||
Phases=_frames.GroupBy(f=>f.Phase).Select(g=>new{Phase=g.Key,AverageFps=1000/g.Average(f=>f.Milliseconds),MaxMs=g.Max(f=>f.Milliseconds)})},new JsonSerializerOptions{WriteIndented=true}));
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ public sealed class PlayerController
|
||||
public DVector3 Forward => (_heading * System.Math.Cos(_pitch) + Up * System.Math.Sin(_pitch)).Normalized();
|
||||
public double EyeHeight => Crouching ? CrouchHeight : StandHeight;
|
||||
public double Speed => _velocity.Length;
|
||||
public Func<string,DVector3,TrunkCapsule[]>? TrunksProvider {get;set;}
|
||||
|
||||
public PlayerController(UniversePosition position, DVector3 forward, DVector3 fallbackUp, double cellSize, UniverseNeighborhood neighborhood)
|
||||
{
|
||||
@@ -90,6 +91,7 @@ public sealed class PlayerController
|
||||
if (Grounded)
|
||||
{
|
||||
var proposed = relative + horizontal * dt;
|
||||
proposed=MoveThroughTrees(t.Planet,relative,proposed,radial);
|
||||
var nextRadial = proposed.Normalized();
|
||||
var nextSurface = TerrainHeight.Sample(t.Planet, nextRadial);
|
||||
var rise = nextSurface - surface;
|
||||
@@ -115,6 +117,8 @@ public sealed class PlayerController
|
||||
else vertical = System.Math.Max(vertical - gravity * dt, -180);
|
||||
_velocity = horizontal + radial * vertical;
|
||||
var destination = Position.Add(_velocity * dt, _cellSize);
|
||||
var treeMove=MoveThroughTrees(t.Planet,relative,destination.RelativeTo(t.Body,_cellSize),radial);
|
||||
destination=t.Body.Add(treeMove,_cellSize);
|
||||
var collision = PlanetCollisionSolver.Sweep(Position, destination,
|
||||
UniversePosition.AtCellCenter(t.System.Coordinate, _cellSize), [t.Planet], _cellSize, eye,
|
||||
_velocity.Length * dt < 100 ? 16 : 128);
|
||||
@@ -135,6 +139,13 @@ public sealed class PlayerController
|
||||
_heading = heading.LengthSquared > .0001 ? heading.Normalized() : Tangent(up);
|
||||
Up = up;
|
||||
}
|
||||
private DVector3 MoveThroughTrees(PlanetData planet,DVector3 start,DVector3 end,DVector3 up)
|
||||
{
|
||||
if(TrunksProvider is null)return end;
|
||||
var bodyStart=planet.Orientation.ToBody(start);var trunks=TrunksProvider(planet.Id,bodyStart);
|
||||
if(trunks.Length==0)return end;
|
||||
return planet.Orientation.ToWorld(VegetationCollision.Move(bodyStart,planet.Orientation.ToBody(end),planet.Orientation.ToBody(up),EyeHeight,trunks));
|
||||
}
|
||||
private static DVector3 Tangent(DVector3 up) => DVector3.Cross(System.Math.Abs(up.Y) < .9 ? new(0, 1, 0) : new DVector3(1, 0, 0), up).Normalized();
|
||||
private (StarSystemData System, PlanetData Planet, UniversePosition Body)? Nearest(UniverseNeighborhood neighborhood)
|
||||
{
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
<Content Include="..\..\config.json" Link="config.json" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content Include="..\..\KnownGalaxies.json" Link="KnownGalaxies.json" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content Include="..\..\KnownSolarSystem\**\*" Link="KnownSolarSystem\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content Include="..\..\Content\**\*.json;..\..\Content\**\*.jpg;..\..\Content\**\*.png;..\..\Content\**\*.f32" Link="Content\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content Include="..\..\Content\**\*.json;..\..\Content\**\*.jpg;..\..\Content\**\*.png;..\..\Content\**\*.f32" Exclude="..\..\Content\Vegitations\**\source\**\*" Link="Content\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
<Content Include="..\..\Content\Vegitations\**\prepared\*.glb" Link="Content\Vegitations\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -18,6 +19,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SharpGLTF.Core" Version="1.0.7" />
|
||||
<PackageReference Include="Silk.NET.Windowing" Version="2.23.0" />
|
||||
<PackageReference Include="Silk.NET.Windowing.Glfw" Version="2.23.0" />
|
||||
<PackageReference Include="Silk.NET.Input" Version="2.23.0" />
|
||||
|
||||
@@ -32,6 +32,7 @@ public sealed class UniverseApplication : IDisposable
|
||||
private PlayerController? _player;
|
||||
private IBenchmarkSession? _benchmark;
|
||||
private bool _benchmarkNeedsInitialFrame;
|
||||
private bool? _benchmarkVsync;
|
||||
private bool _menuOpen;
|
||||
private bool _debugMode;
|
||||
private int _menuSelection;
|
||||
@@ -83,8 +84,9 @@ public sealed class UniverseApplication : IDisposable
|
||||
var options = WindowOptions.Default with
|
||||
{
|
||||
Title = "Universe | loading",
|
||||
IsVisible = !Environment.GetCommandLineArgs().Any(a => a is "--simulation-report" or "--graphics-report" or "--scene-report" or "--ocean-report" or "--cloud-report" or "--terrain-report" or "--menu-report" or "--terrain-perf-report" or "--black-hole-report" or "--galaxy-journey-report" or "--star-trails-report"),
|
||||
Size = new Vector2D<int>(1440, 900),
|
||||
IsVisible = !Environment.GetCommandLineArgs().Any(a => a is "--shadow-report" or "--lookaround-report" or "--prepare-vegetation" or "--vegetation-report" or "--simulation-report" or "--graphics-report" or "--scene-report" or "--ocean-report" or "--cloud-report" or "--terrain-report" or "--menu-report" or "--terrain-perf-report" or "--black-hole-report" or "--galaxy-journey-report" or "--star-trails-report"),
|
||||
Size = Environment.GetCommandLineArgs().Contains("--vegetation-report")?new Vector2D<int>(2560,1440):new Vector2D<int>(1440, 900),
|
||||
WindowBorder = Environment.GetCommandLineArgs().Contains("--vegetation-report")?WindowBorder.Hidden:WindowBorder.Resizable,
|
||||
VSync = true,
|
||||
PreferredDepthBufferBits = 24,
|
||||
API = new GraphicsAPI(ContextAPI.OpenGL, new APIVersion(3, 3))
|
||||
@@ -102,8 +104,14 @@ public sealed class UniverseApplication : IDisposable
|
||||
private void OnLoad()
|
||||
{
|
||||
var gl = _window.CreateOpenGL();
|
||||
if(Environment.GetCommandLineArgs().Contains("--prepare-vegetation"))
|
||||
{VegetationPreparation.Run(gl,Path.Combine(AppContext.BaseDirectory,"../../../../..","Content","Vegitations"));_window.Close();return;}
|
||||
_input = _window.CreateInput();
|
||||
_renderer = new(gl, _window.FramebufferSize.X, _window.FramebufferSize.Y, _galaxies, _starMotionBlur);
|
||||
if(Environment.GetCommandLineArgs().Contains("--lookaround-report"))
|
||||
{GraphicsReport.RunLookaround(gl,_renderer,_camera,_neighborhood,_window.FramebufferSize.X,_window.FramebufferSize.Y);_window.Close();return;}
|
||||
if(Environment.GetCommandLineArgs().Contains("--vegetation-report"))
|
||||
{GraphicsReport.RunVegetation(gl,_renderer,_camera,_neighborhood,_window.FramebufferSize.X,_window.FramebufferSize.Y);_window.Close();return;}
|
||||
if (Environment.GetCommandLineArgs().Contains("--simulation-report"))
|
||||
{
|
||||
GraphicsReport.RunSimulation(gl, _renderer, _neighborhood, _generator.CellSizeMeters, _window.FramebufferSize.X, _window.FramebufferSize.Y);
|
||||
@@ -132,6 +140,8 @@ public sealed class UniverseApplication : IDisposable
|
||||
{ GraphicsReport.RunTerrainPerformance(gl, _renderer, _neighborhood, _generator.CellSizeMeters, _window.FramebufferSize.X, _window.FramebufferSize.Y); _window.Close(); return; }
|
||||
if (Environment.GetCommandLineArgs().Contains("--menu-report"))
|
||||
{ GraphicsReport.RunMenu(gl, _renderer, _camera, _neighborhood, _window.FramebufferSize.X, _window.FramebufferSize.Y); _window.Close(); return; }
|
||||
if (Environment.GetCommandLineArgs().Contains("--shadow-report"))
|
||||
{ GraphicsReport.RunShadows(gl); _window.Close(); return; }
|
||||
if (Environment.GetCommandLineArgs().Contains("--terrain-report"))
|
||||
{ GraphicsReport.RunTerrain(gl, _renderer, _neighborhood, _generator.CellSizeMeters, _window.FramebufferSize.X, _window.FramebufferSize.Y); _window.Close(); return; }
|
||||
if (Environment.GetCommandLineArgs().Contains("--cloud-report"))
|
||||
@@ -232,13 +242,19 @@ public sealed class UniverseApplication : IDisposable
|
||||
// Render one initial frame before starting the measurement clock.
|
||||
if (!_benchmarkNeedsInitialFrame) _benchmark?.RecordFrame(deltaSeconds);
|
||||
if (_benchmark is GalaxyJourneyBenchmark { IsComplete: true, ReportDirectory: null } journey) journey.SaveReport();
|
||||
if(_benchmark is ForestBenchmark {IsRunning:false,ReportDirectory:null} forestFinished)forestFinished.SaveReport();
|
||||
if(_benchmark?.IsRunning!=true&&_benchmarkVsync is {} vsync){_window.VSync=vsync;_benchmarkVsync=null;}
|
||||
if (_renderer is not null)
|
||||
{
|
||||
_renderer.DebugMode = _debugMode;
|
||||
_renderer.SimulationTimeSeconds = _clock.ElapsedSeconds;
|
||||
_renderer.SimulationTimeSeconds = _clock.ElapsedSeconds+(_benchmark is ForestBenchmark {IsRunning:true} activeForest?activeForest.ElapsedSeconds*_clock.TimeScale:0);
|
||||
_renderer.DebugProbeEnabled = _debugMode && !_menuOpen;
|
||||
_renderer.BenchmarkProfiling=_benchmark is ForestBenchmark {IsRunning:true};
|
||||
}
|
||||
_renderer?.Render(_camera, _neighborhood, _menuOpen ? 0 : deltaSeconds);
|
||||
if(_benchmark is ForestBenchmark forest&&_renderer is {} renderer)
|
||||
forest.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);
|
||||
_renderer?.DrawDebugOverlay(1 / _frameTime, _clock.TimeScale, _clock.Utc);
|
||||
if (_benchmark is { HasRun: true } benchmark) _renderer?.DrawBenchmarkOverlay(benchmark);
|
||||
if (_menuOpen) _renderer?.DrawMenu(_menuSelection, _debugMode, _clock.TimeScale, _clock.Utc);
|
||||
@@ -274,8 +290,8 @@ public sealed class UniverseApplication : IDisposable
|
||||
if (key == Key.Escape) SetMenu(false);
|
||||
else if (_debugMode && key is Key.Left or Key.Right)
|
||||
_clock.TimeScale += (key == Key.Right ? 1 : -1) * (IsDown(Key.ShiftLeft) || IsDown(Key.ShiftRight) ? 10 : 1);
|
||||
else if (key == Key.Up) _menuSelection = (_menuSelection + 6) % 7;
|
||||
else if (key == Key.Down) _menuSelection = (_menuSelection + 1) % 7;
|
||||
else if (key == Key.Up) _menuSelection = (_menuSelection + PauseMenu.Count-1) % PauseMenu.Count;
|
||||
else if (key == Key.Down) _menuSelection = (_menuSelection + 1) % PauseMenu.Count;
|
||||
else if (key is Key.Enter or Key.KeypadEnter) ActivateMenu();
|
||||
return;
|
||||
}
|
||||
@@ -410,7 +426,15 @@ public sealed class UniverseApplication : IDisposable
|
||||
|
||||
private void ActivateMenu()
|
||||
{
|
||||
if (_menuSelection == 6) { _window.Close(); return; }
|
||||
if (_menuSelection == 7) { _window.Close(); return; }
|
||||
if(_menuSelection==6)
|
||||
{
|
||||
var home=CurrentHomeView();
|
||||
try{_benchmark=new ForestBenchmark(home.System,home.Earth,_renderer!.Vegetation.Catalog,_camera,home.Position,home.Forward,home.Up,_renderer.Vegetation.Trunks);}
|
||||
catch(InvalidOperationException error){_window.Title="Universe | Forest benchmark: "+error.Message;return;}
|
||||
_player=null;_debugMode=false;_benchmarkVsync=_window.VSync;_window.VSync=false;
|
||||
_benchmarkNeedsInitialFrame=true;UpdateNeighborhood();SetMenu(false);return;
|
||||
}
|
||||
if (_menuSelection == 2) { _debugMode = !_debugMode; return; }
|
||||
if (_menuSelection == 5)
|
||||
{
|
||||
@@ -436,6 +460,7 @@ public sealed class UniverseApplication : IDisposable
|
||||
if (_menuSelection == 0)
|
||||
{
|
||||
_player ??= new PlayerController(_camera.Position, _camera.Forward, _camera.Up, _generator.CellSizeMeters, _neighborhood);
|
||||
_player.TrunksProvider=(id,position)=>_renderer?.Vegetation.Trunks(id,position)??[];
|
||||
_camera.SetPlayerPose(_player.Position, _player.Forward, _player.Up);
|
||||
}
|
||||
else _player = null;
|
||||
|
||||
@@ -128,6 +128,10 @@ public sealed class KnownSolarSystemCatalog
|
||||
var orbit = BuildOrbit(p.Orbit, (parent?.MassKg ?? stars[host].MassKg) + p.MassKg);
|
||||
var position = (parent?.PositionMeters ?? stars[host].PositionMeters) + orbit.Position(0);
|
||||
var surfaceTexture = p.SurfaceTexture is null ? null : LoadColor(p.SurfaceTexture);
|
||||
var vegetation=p.Vegetation??(p.SurfaceType==PlanetSurfaceType.Terrestrial&&air.HeightMeters>0?new VegetationSettings():null);
|
||||
vegetation?.Validate();
|
||||
if(vegetation is {Enabled:true}&&p.SurfaceType is PlanetSurfaceType.GasGiant or PlanetSurfaceType.IceGiant)
|
||||
throw new InvalidDataException($"'{p.Id}': vegetation requires solid ground.");
|
||||
var planetSeed = p.Seed ?? StableSeed(systemId + "/" + p.Id);
|
||||
var clouds = p.Clouds ?? CloudSettings.ForAtmosphere(planetSeed, air.HeightMeters, air.Density, p.SurfaceType);
|
||||
if (clouds is not null) VisualSettingsValidator.Clouds(clouds, p.Id, air.HeightMeters, air.Density, p.SurfaceType);
|
||||
@@ -158,6 +162,8 @@ public sealed class KnownSolarSystemCatalog
|
||||
StormStrength = p.StormStrength,
|
||||
SurfaceTexture = surfaceTexture,
|
||||
Materials = materials,
|
||||
Vegetation = vegetation,
|
||||
VegetationBiomeTexture = vegetation?.BiomeTexture is {} biome?LoadColor(biome):null,
|
||||
Ocean = p.Ocean,
|
||||
Clouds = clouds,
|
||||
CloudDensityMap = cloudDensityMap,
|
||||
|
||||
@@ -121,6 +121,7 @@ public sealed record PlanetDefinition
|
||||
public float[] SurfaceColor { get; init; } = [0.38f, 0.30f, 0.22f];
|
||||
public string? SurfaceTexture { get; init; }
|
||||
public TerrainMaterialSettings Materials { get; init; } = new();
|
||||
public VegetationSettings? Vegetation { get; init; }
|
||||
public OceanSettings? Ocean { get; init; }
|
||||
public CloudSettings? Clouds { get; init; }
|
||||
public TerrainDefinition Terrain { get; init; } = new();
|
||||
|
||||
@@ -46,7 +46,11 @@ internal static class VisualSettingsValidator
|
||||
Range(o.WaveAmplitudeMeters, 0, 30, id, "ocean.waveAmplitudeMeters");
|
||||
Range(o.WaveLengthMeters, 10, 2000, id, "ocean.waveLengthMeters");
|
||||
Range(o.WindDirectionDegrees, -360, 360, id, "ocean.windDirectionDegrees");
|
||||
Range(o.WindSpreadDegrees, 0, 180, id, "ocean.windSpreadDegrees");
|
||||
Range(o.WaveSpeed, 0, 10, id, "ocean.waveSpeed"); Range(o.Choppiness, 0, .9, id, "ocean.choppiness");
|
||||
Range(o.RippleStrength, 0, 3, id, "ocean.rippleStrength");
|
||||
Range(o.WaveGeometryDistanceMeters, 500, 10000, id, "ocean.waveGeometryDistanceMeters");
|
||||
Range(o.WhitecapThreshold, .05, 1, id, "ocean.whitecapThreshold");
|
||||
Range(o.Roughness, .04, 1, id, "ocean.roughness"); Range(o.ReflectionStrength, 0, 3, id, "ocean.reflectionStrength");
|
||||
Range(o.RefractionStrength, 0, .1, id, "ocean.refractionStrength"); Range(o.FoamStrength, 0, 3, id, "ocean.foamStrength");
|
||||
Color(o.DeepColor, id); Color(o.ShallowColor, id); Color(o.Absorption, id);
|
||||
|
||||
@@ -46,6 +46,8 @@ public sealed record PlanetData(
|
||||
public double StormStrength { get; init; }
|
||||
public RasterImage? SurfaceTexture { get; init; }
|
||||
public TerrainMaterialSettings Materials { get; init; } = new();
|
||||
public VegetationSettings? Vegetation { get; init; }
|
||||
public RasterImage? VegetationBiomeTexture { get; init; }
|
||||
public OceanSettings? Ocean { get; init; }
|
||||
public CloudSettings? Clouds { get; init; }
|
||||
public CloudDensityMap? CloudDensityMap { get; init; }
|
||||
|
||||
13
src/Universe.Core/Generation/GenerationJobs.cs
Normal file
13
src/Universe.Core/Generation/GenerationJobs.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Universe.Core.Generation;
|
||||
|
||||
/// <summary>One bounded pool for terrain and vegetation; GL work stays on the render thread.</summary>
|
||||
public static class GenerationJobs
|
||||
{
|
||||
private static readonly SemaphoreSlim Slots=new(System.Math.Clamp(Environment.ProcessorCount-2,1,4));
|
||||
public static Task<T> Run<T>(Func<T> work,CancellationToken token)=>Task.Run(async()=>
|
||||
{
|
||||
await Slots.WaitAsync(token).ConfigureAwait(false);
|
||||
try {token.ThrowIfCancellationRequested();return work();}
|
||||
finally {Slots.Release();}
|
||||
},token);
|
||||
}
|
||||
@@ -13,8 +13,12 @@ public sealed record OceanSettings
|
||||
public double WaveAmplitudeMeters { get; init; } = 1.6;
|
||||
public double WaveLengthMeters { get; init; } = 72;
|
||||
public double WindDirectionDegrees { get; init; } = 35;
|
||||
public double WindSpreadDegrees { get; init; } = 110;
|
||||
public double WaveSpeed { get; init; } = 1;
|
||||
public double Choppiness { get; init; } = .65;
|
||||
public double RippleStrength { get; init; } = .8;
|
||||
public double WaveGeometryDistanceMeters { get; init; } = 4000;
|
||||
public double WhitecapThreshold { get; init; } = .22;
|
||||
public double Roughness { get; init; } = .14;
|
||||
public double ReflectionStrength { get; init; } = 1;
|
||||
public double RefractionStrength { get; init; } = .018;
|
||||
@@ -39,11 +43,14 @@ public sealed record OceanReflectionSettings
|
||||
public double EdgeFade { get; init; } = .07;
|
||||
}
|
||||
|
||||
public readonly record struct OceanWave(DVector3 Direction, double WaveNumber, double Amplitude, double AngularFrequency);
|
||||
public readonly record struct OceanWave(DVector3 Direction, double WaveNumber, double Amplitude, double AngularFrequency, double Phase);
|
||||
|
||||
public static class OceanSurface
|
||||
{
|
||||
public const int WaveCount = 12;
|
||||
public const int WaveCount = 32;
|
||||
public const int GeometryWaveCount = 16;
|
||||
// Shared with the shader; origins are reduced in double precision before conversion to floats.
|
||||
public static ReadOnlySpan<double> DetailFrequencies => [.045, .35, 1.4];
|
||||
|
||||
public static OceanWave[] Spectrum(OceanSettings settings, ulong seed)
|
||||
{
|
||||
@@ -51,12 +58,19 @@ public static class OceanSurface
|
||||
var waves = new OceanWave[WaveCount];
|
||||
for (var i = 0; i < waves.Length; i++)
|
||||
{
|
||||
var angle = (settings.WindDirectionDegrees + (random.NextDouble() - .5) * 115 + i * 23) * System.Math.PI / 180;
|
||||
var direction = new DVector3(System.Math.Cos(angle), .23 * System.Math.Sin(angle * 1.7 + i), System.Math.Sin(angle)).Normalized();
|
||||
var wavelength = settings.WaveLengthMeters * System.Math.Pow(.63, i);
|
||||
var band = i / 4;
|
||||
var lobe = i % 4;
|
||||
// Several independently phased directions per octave avoid long, repeating foam rows.
|
||||
var spread = ((lobe + random.NextDouble()) / 4 - .5) * settings.WindSpreadDegrees;
|
||||
if (lobe == 3) spread += 70; // weaker cross swell, rather than one set of parallel waves
|
||||
var angle = (settings.WindDirectionDegrees + spread) * System.Math.PI / 180;
|
||||
var direction = new DVector3(System.Math.Cos(angle), (random.NextDouble() - .5) * .4, System.Math.Sin(angle)).Normalized();
|
||||
var wavelength = settings.WaveLengthMeters * System.Math.Pow(.55, band) * (.82 + .36 * random.NextDouble());
|
||||
var k = System.Math.Tau / wavelength;
|
||||
var amplitude = settings.WaveAmplitudeMeters * System.Math.Pow(.62, i);
|
||||
waves[i] = new(direction, k, amplitude, System.Math.Sqrt(9.81 * k) * settings.WaveSpeed);
|
||||
var weight = lobe switch { 0 => .38, 1 => .29, 2 => .21, _ => .12 };
|
||||
// Limit steepness even for unusually short/high waves configured in JSON.
|
||||
var amplitude = System.Math.Min(settings.WaveAmplitudeMeters * System.Math.Pow(.51, band) * weight, .18 / k);
|
||||
waves[i] = new(direction, k, amplitude, System.Math.Sqrt(9.81 * k) * settings.WaveSpeed, random.NextDouble() * System.Math.Tau);
|
||||
}
|
||||
return waves;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,19 @@ namespace Universe.Core.Generation;
|
||||
/// <summary>Unmodified image rows, north at the top. Color and height maps share equirectangular UVs.</summary>
|
||||
public sealed record RasterImage(int Width, int Height, byte[] Rgba, string? SourcePath = null)
|
||||
{
|
||||
public System.Numerics.Vector3 SampleColor(DVector3 direction)
|
||||
{
|
||||
var (u,v)=HeightMap.Coordinates(direction);
|
||||
var x=(u-System.Math.Floor(u))*Width-.5;var y=System.Math.Clamp(v*Height-.5,0,Height-1);
|
||||
var ix=(int)System.Math.Floor(x);var iy=(int)System.Math.Floor(y);
|
||||
System.Numerics.Vector3 At(int a,int b)
|
||||
{
|
||||
var offset=(System.Math.Clamp(b,0,Height-1)*Width+((a%Width+Width)%Width))*4;
|
||||
return new(Rgba[offset],Rgba[offset+1],Rgba[offset+2]);
|
||||
}
|
||||
return System.Numerics.Vector3.Lerp(System.Numerics.Vector3.Lerp(At(ix,iy),At(ix+1,iy),(float)(x-ix)),
|
||||
System.Numerics.Vector3.Lerp(At(ix,iy+1),At(ix+1,iy+1),(float)(x-ix)),(float)(y-iy));
|
||||
}
|
||||
public static RasterImage Load(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
|
||||
@@ -19,6 +19,9 @@ public sealed record TerrainPatchData(DVector3 Origin, float[] Vertices, uint[]
|
||||
public static class TerrainPatchBuilder
|
||||
{
|
||||
public const int Segments = 24;
|
||||
// Every patch has identical topology. Keep it once instead of allocating
|
||||
// hundreds of tiny index arrays for every worker result during a camera turn.
|
||||
private static readonly uint[] SharedIndices=BuildIndices();
|
||||
|
||||
public static TerrainPatchData Build(PlanetData planet, TerrainPatchKey key)
|
||||
{
|
||||
@@ -34,7 +37,8 @@ public static class TerrainPatchBuilder
|
||||
var d = TerrainChunkBuilder.FaceDirection(key.Face, startU + key.Width * x / Segments, startV + key.Width * y / Segments).Normalized();
|
||||
positions[(y + 1) * expanded + x + 1] = d * TerrainHeight.Sample(planet, d);
|
||||
}
|
||||
var vertices = new List<float>((side * side + side * 4) * 9);
|
||||
var vertices = new float[(side * side + side * 4) * 9];
|
||||
var vertexOffset=0;
|
||||
for (var y = 0; y < side; y++)
|
||||
for (var x = 0; x < side; x++)
|
||||
{
|
||||
@@ -42,14 +46,7 @@ public static class TerrainPatchBuilder
|
||||
var p = positions[index];
|
||||
var n = DVector3.Cross(positions[index + 1] - positions[index - 1], positions[index + expanded] - positions[index - expanded]).Normalized();
|
||||
if (DVector3.Dot(n, p) < 0) n = -n;
|
||||
AddVertex(vertices, p - origin, n, p.Normalized());
|
||||
}
|
||||
var indices = new List<uint>(Segments * Segments * 6 + Segments * 24);
|
||||
for (var y = 0; y < Segments; y++)
|
||||
for (var x = 0; x < Segments; x++)
|
||||
{
|
||||
var a = (uint)(y * side + x);
|
||||
indices.AddRange([a, a + (uint)side, a + 1, a + 1, a + (uint)side, a + (uint)side + 1]);
|
||||
AddVertex(vertices, ref vertexOffset, p - origin, n, p.Normalized());
|
||||
}
|
||||
var spacing = planet.RadiusMeters * key.Width / Segments;
|
||||
// A fixed fraction of mountain amplitude creates hundred-metre walls around
|
||||
@@ -57,21 +54,16 @@ public static class TerrainPatchBuilder
|
||||
var skirtDepth = spacing * 2 + System.Math.Min(planet.TerrainAmplitudeMeters * .012, spacing * 2);
|
||||
for (var edge = 0; edge < 4; edge++)
|
||||
{
|
||||
var skirtStart = (uint)(vertices.Count / 9);
|
||||
for (var t = 0; t < side; t++)
|
||||
{
|
||||
var source = edge switch { 0 => t, 1 => t * side + Segments, 2 => Segments * side + t, _ => t * side };
|
||||
var p = new DVector3(vertices[source * 9], vertices[source * 9 + 1], vertices[source * 9 + 2]) + origin;
|
||||
var n = new DVector3(vertices[source * 9 + 3], vertices[source * 9 + 4], vertices[source * 9 + 5]);
|
||||
var d = p.Normalized();
|
||||
AddVertex(vertices, p - d * skirtDepth - origin, n, d);
|
||||
if (t == Segments) continue;
|
||||
var next = source + (edge is 0 or 2 ? 1 : side);
|
||||
var b = skirtStart + (uint)t;
|
||||
indices.AddRange([(uint)source, b, (uint)next, (uint)next, b, b + 1]);
|
||||
AddVertex(vertices, ref vertexOffset, p - d * skirtDepth - origin, n, d);
|
||||
}
|
||||
}
|
||||
var vertexData = vertices.ToArray();
|
||||
var vertexData = vertices;
|
||||
var min = new DVector3(double.PositiveInfinity, double.PositiveInfinity, double.PositiveInfinity);
|
||||
var max = new DVector3(double.NegativeInfinity, double.NegativeInfinity, double.NegativeInfinity);
|
||||
for (var i = 0; i < vertexData.Length; i += 9)
|
||||
@@ -83,11 +75,33 @@ public static class TerrainPatchBuilder
|
||||
var radius = 0d;
|
||||
for (var i = 0; i < vertexData.Length; i += 9)
|
||||
radius = System.Math.Max(radius, (new DVector3(vertexData[i], vertexData[i + 1], vertexData[i + 2]) - center).Length);
|
||||
return new(origin, vertexData, indices.ToArray()) { BoundsCenter = origin + center, BoundsRadius = radius + .01 };
|
||||
return new(origin, vertexData, SharedIndices) { BoundsCenter = origin + center, BoundsRadius = radius + .01 };
|
||||
}
|
||||
|
||||
private static void AddVertex(List<float> target, DVector3 p, DVector3 n, DVector3 d)
|
||||
private static void AddVertex(float[] target, ref int offset, DVector3 p, DVector3 n, DVector3 d)
|
||||
{
|
||||
target.AddRange([(float)p.X, (float)p.Y, (float)p.Z, (float)n.X, (float)n.Y, (float)n.Z, (float)d.X, (float)d.Y, (float)d.Z]);
|
||||
target[offset++]=(float)p.X;target[offset++]=(float)p.Y;target[offset++]=(float)p.Z;
|
||||
target[offset++]=(float)n.X;target[offset++]=(float)n.Y;target[offset++]=(float)n.Z;
|
||||
target[offset++]=(float)d.X;target[offset++]=(float)d.Y;target[offset++]=(float)d.Z;
|
||||
}
|
||||
|
||||
private static uint[] BuildIndices()
|
||||
{
|
||||
const int side=Segments+1;
|
||||
var indices=new uint[Segments*Segments*6+Segments*24];var offset=0;
|
||||
void TrianglePair(uint a,uint b,uint c,uint d,uint e,uint f)
|
||||
{indices[offset++]=a;indices[offset++]=b;indices[offset++]=c;indices[offset++]=d;indices[offset++]=e;indices[offset++]=f;}
|
||||
for(var y=0;y<Segments;y++)for(var x=0;x<Segments;x++)
|
||||
{
|
||||
var a=(uint)(y*side+x);
|
||||
TrianglePair(a,a+side,a+1,a+1,a+side,a+side+1);
|
||||
}
|
||||
for(var edge=0;edge<4;edge++)for(var t=0;t<Segments;t++)
|
||||
{
|
||||
var source=edge switch {0=>t,1=>t*side+Segments,2=>Segments*side+t,_=>t*side};
|
||||
var next=source+(edge is 0 or 2?1:side);var b=(uint)(side*side+edge*side+t);
|
||||
TrianglePair((uint)source,b,(uint)next,(uint)next,b,b+1);
|
||||
}
|
||||
return indices;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,8 @@ public sealed class UniverseGenerator
|
||||
Orbit = MovingOrbit(position - stars[0].PositionMeters, orbitNormal, eccentricity, phase, stars[0].MassKg),
|
||||
Rotation = new((10 + weather.NextDouble() * 40) * 3600, weather.NextDouble() * 60, weather.NextDouble() * 360),
|
||||
AtmosphereDensity = atmosphereDensity,
|
||||
SurfaceType = atmosphereDensity>.6&&orbitAu is >.7 and <2.5?PlanetSurfaceType.Terrestrial:PlanetSurfaceType.Rocky,
|
||||
Vegetation = atmosphereDensity>.6&&orbitAu is >.7 and <2.5?VegetationSettings.Default:null,
|
||||
Clouds = CloudSettings.ForAtmosphere(seed, radius * .018, atmosphereDensity, PlanetSurfaceType.Rocky)
|
||||
});
|
||||
}
|
||||
|
||||
163
src/Universe.Core/Generation/Vegetation.cs
Normal file
163
src/Universe.Core/Generation/Vegetation.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using System.Numerics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Universe.Core.Math;
|
||||
|
||||
namespace Universe.Core.Generation;
|
||||
|
||||
public enum VegetationType { Tree, Shrub, Grass, Fern }
|
||||
|
||||
public sealed record VegetationSettings
|
||||
{
|
||||
public static VegetationSettings Default {get;}=new();
|
||||
public bool Enabled { get; init; } = true;
|
||||
public double DensityMultiplier { get; init; } = 1;
|
||||
public string[]? AllowedPlants { get; init; }
|
||||
public string? BiomeTexture { get; init; }
|
||||
public bool AffectGroundMaterials { get; init; } = true;
|
||||
public double TreeDistanceMeters { get; init; } = 2000;
|
||||
public double ShrubDistanceMeters { get; init; } = 150;
|
||||
public double GrassDistanceMeters { get; init; } = 80;
|
||||
public double[] TreeLodMeters { get; init; } = [40, 150, 400];
|
||||
public double TreeDensityPerHectare { get; init; } = 600;
|
||||
public double ShrubDensityPerHectare { get; init; } = 350;
|
||||
public double GroundDensityPerHectare { get; init; } = 20000;
|
||||
public double ForestScaleMeters { get; init; } = 450;
|
||||
public double UploadBudgetMilliseconds { get; init; } = 1;
|
||||
public bool DepthPrepass {get;init;}
|
||||
// Space for the full 2 km neighbourhood, including trees behind the player.
|
||||
public int MaximumCachedTiles { get; init; } = 2048;
|
||||
public int MaximumInstances { get; init; } = 650000;
|
||||
public int ShadowResolution { get; init; } = 2048;
|
||||
public double[] ShadowDistancesMeters { get; init; } = [100, 400];
|
||||
public double ShadowSoftness { get; init; } = 1;
|
||||
public double ShadowCascadeBlend { get; init; } = .2;
|
||||
public double ShadowBiasMeters { get; init; } = .035;
|
||||
public double CollisionDistanceMeters { get; init; } = 64;
|
||||
public void Validate()
|
||||
{
|
||||
static bool Range(double n,double min,double max)=>double.IsFinite(n)&&n>=min&&n<=max;
|
||||
if(!Range(DensityMultiplier,0,10)||!Range(TreeDistanceMeters,50,5000)||!Range(ShrubDistanceMeters,10,500)
|
||||
||!Range(GrassDistanceMeters,5,250)||!Range(ForestScaleMeters,20,10000)||!Range(UploadBudgetMilliseconds,.1,5)
|
||||
||!Range(TreeDensityPerHectare,0,2000)||!Range(ShrubDensityPerHectare,0,5000)||!Range(GroundDensityPerHectare,0,50000)
|
||||
||TreeLodMeters is not {Length:3}||TreeLodMeters.Any(n=>!Range(n,1,TreeDistanceMeters))
|
||||
||TreeLodMeters[0]>=TreeLodMeters[1]||TreeLodMeters[1]>=TreeLodMeters[2]
|
||||
||ShadowDistancesMeters is not {Length:2}||ShadowDistancesMeters.Any(n=>!Range(n,10,1000))
|
||||
||ShadowDistancesMeters[0]>=ShadowDistancesMeters[1]||ShadowResolution is <256 or >4096
|
||||
||!Range(ShadowSoftness,0,1)||!Range(ShadowCascadeBlend,0,.4)||!Range(ShadowBiasMeters,.001,.25)
|
||||
||(ShadowResolution&(ShadowResolution-1))!=0||MaximumCachedTiles is <16 or >4096
|
||||
||MaximumInstances is <1000 or >2000000||!Range(CollisionDistanceMeters,10,256)
|
||||
||AllowedPlants?.Any(string.IsNullOrWhiteSpace)==true)
|
||||
throw new InvalidDataException("Invalid vegetation density, LOD, streaming or shadow settings.");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record TrunkCollision(double RadiusMeters = .3, double HeightMeters = 8);
|
||||
public sealed record VegetationDefinition
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Model { get; init; }
|
||||
public VegetationType Type { get; init; }
|
||||
public int[] MatchColorRgb { get; init; } = [45,80,35];
|
||||
public double ColorTolerancePercent { get; init; } = 22;
|
||||
public double SpawnWeight { get; init; } = 1;
|
||||
public double[] ScaleRange { get; init; } = [.8,1.25];
|
||||
public double[] AltitudeMeters { get; init; } = [-100,2200];
|
||||
public double[] AbsoluteLatitudeDegrees { get; init; } = [0,80];
|
||||
public double MaxSlopeDegrees { get; init; } = 32;
|
||||
public double MinimumSpacingMeters { get; init; } = 2;
|
||||
public double WindStrength { get; init; } = .2;
|
||||
public double[]? LodMeters {get;init;}
|
||||
public double? DistanceMeters {get;init;}
|
||||
public TrunkCollision? TrunkCollision { get; init; }
|
||||
}
|
||||
public sealed record BiomeDefinition(string Id, int[] ColorRgb, double TolerancePercent, string Material);
|
||||
|
||||
public sealed record VegetationCatalog
|
||||
{
|
||||
public int SchemaVersion { get; init; } = 1;
|
||||
public BiomeDefinition[] Biomes { get; init; } = [];
|
||||
public VegetationDefinition[] Plants { get; init; } = [];
|
||||
public static VegetationCatalog Load(string path)
|
||||
{
|
||||
var options=new JsonSerializerOptions {PropertyNameCaseInsensitive=true,UnmappedMemberHandling=JsonUnmappedMemberHandling.Disallow};
|
||||
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
|
||||
var catalog=JsonSerializer.Deserialize<VegetationCatalog>(File.ReadAllText(path),options)
|
||||
??throw new InvalidDataException("Empty vegetation catalog.");
|
||||
string[] biomeIds=["forest","dry","sand","snow"];
|
||||
if(catalog.SchemaVersion!=1||catalog.Biomes.Length!=4||biomeIds.Any(id=>catalog.Biomes.Count(b=>b.Id==id)!=1)||catalog.Plants.Length==0
|
||||
||catalog.Plants.Select(p=>p.Id).Distinct(StringComparer.Ordinal).Count()!=catalog.Plants.Length)
|
||||
throw new InvalidDataException("Vegetation catalog needs four biomes and unique plant IDs.");
|
||||
catalog=catalog with {Biomes=biomeIds.Select(id=>catalog.Biomes.Single(b=>b.Id==id)).ToArray()};
|
||||
foreach(var b in catalog.Biomes){ValidateColor(b.ColorRgb,b.TolerancePercent);if(string.IsNullOrWhiteSpace(b.Material))throw new InvalidDataException("A biome needs a ground material ID.");}
|
||||
foreach(var p in catalog.Plants)
|
||||
{
|
||||
ValidateColor(p.MatchColorRgb,p.ColorTolerancePercent);
|
||||
if(string.IsNullOrWhiteSpace(p.Id)||string.IsNullOrWhiteSpace(p.Model)||!Enum.IsDefined(p.Type)
|
||||
||Path.IsPathRooted(p.Model)||p.Model.Replace('\\','/').Split('/').Contains("..")||!p.Model.EndsWith(".glb",StringComparison.OrdinalIgnoreCase)
|
||||
||!double.IsFinite(p.SpawnWeight)||p.SpawnWeight<=0||p.ScaleRange is not {Length:2}
|
||||
||p.ScaleRange.Any(n=>!double.IsFinite(n)||n<=0)||p.ScaleRange[0]>p.ScaleRange[1]
|
||||
||p.AltitudeMeters is not {Length:2}||p.AltitudeMeters.Any(n=>!double.IsFinite(n))||p.AltitudeMeters[0]>p.AltitudeMeters[1]
|
||||
||p.AbsoluteLatitudeDegrees is not {Length:2}||p.AbsoluteLatitudeDegrees.Any(n=>!double.IsFinite(n)||n<0||n>90)
|
||||
||p.AbsoluteLatitudeDegrees[0]>p.AbsoluteLatitudeDegrees[1]||!double.IsFinite(p.MaxSlopeDegrees)||p.MaxSlopeDegrees is <0 or >70
|
||||
||!double.IsFinite(p.MinimumSpacingMeters)||p.MinimumSpacingMeters is <0 or >20
|
||||
||!double.IsFinite(p.WindStrength)||p.WindStrength is <0 or >2
|
||||
||p.DistanceMeters is {} distance&&(!double.IsFinite(distance)||distance is <1 or >5000)
|
||||
||p.LodMeters is {} lod&&(lod.Length!=3||lod.Any(n=>!double.IsFinite(n)||n is <1 or >5000)||lod[0]>=lod[1]||lod[1]>=lod[2])
|
||||
||p.LodMeters is not null&&p.Type!=VegetationType.Tree
|
||||
||p.TrunkCollision is {} c && (!double.IsFinite(c.RadiusMeters)||c.RadiusMeters<=0||!double.IsFinite(c.HeightMeters)||c.HeightMeters<=0))
|
||||
throw new InvalidDataException($"Invalid vegetation definition '{p.Id}'.");
|
||||
}
|
||||
return catalog;
|
||||
}
|
||||
private static void ValidateColor(int[] color,double tolerance)
|
||||
{
|
||||
if(color is not {Length:3}||color.Any(c=>c is <0 or >255)||!double.IsFinite(tolerance)||tolerance is <0 or >100)
|
||||
throw new InvalidDataException("Vegetation RGB must be 0..255 and tolerance 0..100 percent.");
|
||||
}
|
||||
public static double Match(Vector3 color,int[] target,double tolerance)
|
||||
{
|
||||
var distance=Vector3.Distance(color,new(target[0],target[1],target[2]))*(100/(255*System.Math.Sqrt(3)));
|
||||
if(tolerance==0)return distance<1e-6?1:0;
|
||||
var t=System.Math.Clamp((distance-tolerance*.8)/(tolerance*.2),0,1);
|
||||
return 1-t*t*(3-2*t);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The same sRGB source and equirectangular mapping drive plants and terrain materials.</summary>
|
||||
public sealed class BiomeField
|
||||
{
|
||||
public PlanetData Planet {get;}
|
||||
public VegetationCatalog Catalog {get;}
|
||||
public RasterImage? Image {get;}
|
||||
public BiomeField(PlanetData planet,VegetationCatalog catalog)
|
||||
{
|
||||
Planet=planet with {Orientation=PlanetOrientation.Identity}; Catalog=catalog;
|
||||
Image=planet.VegetationBiomeTexture??planet.SurfaceTexture;
|
||||
}
|
||||
public Vector3 Color(DVector3 direction)
|
||||
{
|
||||
if(Image is not null)return Image.SampleColor(direction);
|
||||
var latitude=System.Math.Abs(direction.Normalized().Y);
|
||||
if(latitude>.91)return new(235,238,240);
|
||||
var n=ProceduralNoise.Sample(direction*5+ProceduralNoise.SeedOffset(Planet.Seed));
|
||||
return n<-.2&&latitude<.6?new(205,181,120):n<.1?new(145,150,80):new(45,80,35);
|
||||
}
|
||||
public Vector4 Weights(Vector3 color)
|
||||
{
|
||||
var b=Catalog.Biomes;
|
||||
var v=new Vector4((float)VegetationCatalog.Match(color,b[0].ColorRgb,b[0].TolerancePercent),
|
||||
(float)VegetationCatalog.Match(color,b[1].ColorRgb,b[1].TolerancePercent),
|
||||
(float)VegetationCatalog.Match(color,b[2].ColorRgb,b[2].TolerancePercent),
|
||||
(float)VegetationCatalog.Match(color,b[3].ColorRgb,b[3].TolerancePercent));
|
||||
var sum=v.X+v.Y+v.Z+v.W;
|
||||
return sum>1?v/sum:v;
|
||||
}
|
||||
public double ForestCover(DVector3 direction)
|
||||
{
|
||||
var p=direction*Planet.RadiusMeters/(Planet.Vegetation?.ForestScaleMeters??450)+ProceduralNoise.SeedOffset(Planet.Seed);
|
||||
var n=ProceduralNoise.Sample(p)*.7+ProceduralNoise.Sample(p*2.17)*.3;
|
||||
var x=System.Math.Clamp((n+.26)/.65,0,1);
|
||||
return x*x*(3-2*x);
|
||||
}
|
||||
}
|
||||
34
src/Universe.Core/Generation/VegetationSiteFinder.cs
Normal file
34
src/Universe.Core/Generation/VegetationSiteFinder.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Universe.Core.Math;
|
||||
|
||||
namespace Universe.Core.Generation;
|
||||
|
||||
public static class VegetationSiteFinder
|
||||
{
|
||||
public static DVector3 FindForest(BiomeField field,DVector3 sunBody)
|
||||
{
|
||||
var best=double.NegativeInfinity;var result=DVector3.Zero;var candidates=new int[6];var minimumHeight=double.PositiveInfinity;var maximumHeight=double.NegativeInfinity;
|
||||
// Scan the actual planet data, not an assumed geographic forest or a hard-coded texture colour.
|
||||
for(var latitude=-64;latitude<=64;latitude+=2)for(var longitude=-180;longitude<180;longitude+=2)
|
||||
{
|
||||
if(System.Math.Abs(latitude)<26)continue;
|
||||
var lat=latitude*System.Math.PI/180;var lon=longitude*System.Math.PI/180;
|
||||
var direction=new DVector3(System.Math.Cos(lat)*System.Math.Cos(lon),System.Math.Sin(lat),System.Math.Cos(lat)*System.Math.Sin(lon));
|
||||
var weight=field.Weights(field.Color(direction)).X;if(weight<.6)continue;candidates[0]++;
|
||||
if(field.Planet.OceanMask?.Sample(direction)>.05)continue;candidates[1]++;
|
||||
var sun=DVector3.Dot(direction,sunBody);if(sun<.2)continue;candidates[2]++;
|
||||
var height=TerrainHeight.Sample(field.Planet,direction)-field.Planet.RadiusMeters;
|
||||
minimumHeight=System.Math.Min(minimumHeight,height);maximumHeight=System.Math.Max(maximumHeight,height);
|
||||
if(height<1||!field.Catalog.Plants.Any(p=>p.Type==VegetationType.Tree
|
||||
&&(field.Planet.Vegetation?.AllowedPlants is null||field.Planet.Vegetation.AllowedPlants.Contains(p.Id,StringComparer.Ordinal))
|
||||
&&height>=p.AltitudeMeters[0]&&height<=p.AltitudeMeters[1]
|
||||
&&System.Math.Abs(latitude)>=p.AbsoluteLatitudeDegrees[0]&&System.Math.Abs(latitude)<=p.AbsoluteLatitudeDegrees[1]
|
||||
&&VegetationCatalog.Match(field.Color(direction),p.MatchColorRgb,p.ColorTolerancePercent)>.5))continue;candidates[3]++;
|
||||
var cover=field.ForestCover(direction);if(cover<.3)continue;candidates[4]++;
|
||||
var slope=DVector3.Dot(TerrainHeight.Normal(field.Planet,direction,1e-6),direction);if(slope<.95)continue;candidates[5]++;
|
||||
var score=weight*cover*(.4+sun)*slope;
|
||||
if(score>best){best=score;result=direction;}
|
||||
}
|
||||
if(result==DVector3.Zero)throw new InvalidOperationException($"No lit, dry forest site satisfies the vegetation/terrain rules. Filters: {string.Join('/',candidates)}, elevations {minimumHeight:F1}..{maximumHeight:F1} m.");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
89
src/Universe.Core/Generation/VegetationStream.cs
Normal file
89
src/Universe.Core/Generation/VegetationStream.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using System.Diagnostics;
|
||||
using Universe.Core.Math;
|
||||
|
||||
namespace Universe.Core.Generation;
|
||||
|
||||
/// <summary>Bounded asynchronous body-space tiles, shared by rendering and trunk collision.</summary>
|
||||
public sealed class VegetationStream : IDisposable
|
||||
{
|
||||
private sealed class Entry {public Task<VegetationTile>? Task;public VegetationTile? Tile;public long LastUsed;public CancellationTokenSource? Cancellation;}
|
||||
private readonly Dictionary<VegetationTileKey,Entry> _tiles=[];
|
||||
private readonly CancellationTokenSource _cancel=new();
|
||||
private long _frame;
|
||||
private DVector3 _lastCamera;
|
||||
private List<(VegetationTileKey Key,double Priority)>? _wanted;
|
||||
private HashSet<VegetationTileKey> _requested=[];
|
||||
public VegetationTileGenerator Generator {get;}
|
||||
public long Revision {get;private set;}
|
||||
public long TreeRevision {get;private set;}
|
||||
public int PendingTiles=>_tiles.Values.Count(e=>e.Task is not null);
|
||||
public int InstanceCount {get;private set;}
|
||||
public double CpuMilliseconds {get;private set;}
|
||||
public double GenerationMilliseconds {get;private set;}
|
||||
public IEnumerable<VegetationTile> Tiles=>_tiles.Values.Where(e=>e.Tile is not null).Select(e=>e.Tile!);
|
||||
public VegetationStream(BiomeField field)=>Generator=new(field);
|
||||
public void ResetFrameDiagnostics(){CpuMilliseconds=0;GenerationMilliseconds=0;}
|
||||
public void Update(DVector3 camera)
|
||||
{
|
||||
var timer=Stopwatch.StartNew();_frame++;GenerationMilliseconds=0;
|
||||
foreach(var entry in _tiles.Values)
|
||||
if(entry.Task is {IsCompleted:true} task)
|
||||
{
|
||||
entry.Task=null;
|
||||
entry.Cancellation?.Dispose();entry.Cancellation=null;
|
||||
if(task.IsCompletedSuccessfully){entry.Tile=task.Result;InstanceCount+=entry.Tile.Instances.Length;GenerationMilliseconds+=entry.Tile.GenerationMilliseconds;Revision++;if(entry.Tile.Key.Layer==0)TreeRevision++;}
|
||||
else if(task.IsFaulted)throw new InvalidOperationException("Vegetation generation failed.",task.Exception);
|
||||
}
|
||||
foreach(var key in _tiles.Where(p=>p.Value.Task is null&&p.Value.Tile is null).Select(p=>p.Key).ToArray())_tiles.Remove(key);
|
||||
var altitude=System.Math.Max(0,camera.Length-Generator.Field.Planet.MaximumTerrainRadiusMeters);
|
||||
if(altitude>Generator.Settings.TreeDistanceMeters*1.2){CpuMilliseconds=timer.Elapsed.TotalMilliseconds;return;}
|
||||
// Retain a full neighbourhood. Turning the camera must never invalidate tiles
|
||||
// or cancel generation behind the player, which is also needed by collision.
|
||||
if(_wanted is null||(camera-_lastCamera).Length>6)
|
||||
{
|
||||
var wanted=new List<(VegetationTileKey Key,double Priority)>();
|
||||
for(var layer=0;layer<3;layer++)
|
||||
{
|
||||
if(altitude>Generator.Range(layer))continue;
|
||||
foreach(var key in Generator.Around(camera,layer))
|
||||
{
|
||||
var offset=key.Direction*camera.Length-camera;
|
||||
var distance=offset.Length;
|
||||
wanted.Add((key,distance+(layer==0?0:layer==1?40:60)));
|
||||
}
|
||||
}
|
||||
_wanted=wanted.OrderBy(w=>w.Priority).Take(Generator.Settings.MaximumCachedTiles).ToList();
|
||||
_requested=_wanted.Select(w=>w.Key).ToHashSet();_lastCamera=camera;
|
||||
}
|
||||
var pending=PendingTiles;
|
||||
foreach(var (key,_) in _wanted)
|
||||
{
|
||||
if(_tiles.TryGetValue(key,out var entry)){entry.LastUsed=_frame;continue;}
|
||||
if(pending>=8||InstanceCount>=Generator.Settings.MaximumInstances)continue;
|
||||
if(_tiles.Count>=Generator.Settings.MaximumCachedTiles)
|
||||
{
|
||||
var victim=_tiles.Where(p=>p.Value.Task is null&&!_requested.Contains(p.Key)).MinBy(p=>p.Value.LastUsed);
|
||||
if(victim.Value is null)continue;
|
||||
InstanceCount-=victim.Value.Tile?.Instances.Length??0;_tiles.Remove(victim.Key);Revision++;if(victim.Key.Layer==0)TreeRevision++;
|
||||
}
|
||||
var cancellation=CancellationTokenSource.CreateLinkedTokenSource(_cancel.Token);var token=cancellation.Token;
|
||||
_tiles[key]=new(){LastUsed=_frame,Cancellation=cancellation,Task=GenerationJobs.Run(()=>Generator.Generate(key,token),token)};
|
||||
pending++;
|
||||
}
|
||||
foreach(var entry in _tiles.Values)if(entry.Task is not null&&_frame-entry.LastUsed>2)entry.Cancellation?.Cancel();
|
||||
foreach(var key in _tiles.Where(p=>p.Value.Task is null&&(_frame-p.Value.LastUsed>120
|
||||
||_tiles.Count>Generator.Settings.MaximumCachedTiles&&_frame-p.Value.LastUsed>2)).Select(p=>p.Key).ToArray())
|
||||
{
|
||||
InstanceCount-=_tiles[key].Tile?.Instances.Length??0;_tiles.Remove(key);Revision++;
|
||||
if(key.Layer==0)TreeRevision++;
|
||||
}
|
||||
CpuMilliseconds=timer.Elapsed.TotalMilliseconds;
|
||||
}
|
||||
public void Clear()
|
||||
{
|
||||
// In-flight tasks are harmless and bounded; disposal cancels them on a system switch.
|
||||
foreach(var key in _tiles.Where(p=>p.Value.Task is null).Select(p=>p.Key).ToArray())_tiles.Remove(key);
|
||||
InstanceCount=0;Revision++;TreeRevision++;
|
||||
}
|
||||
public void Dispose(){_cancel.Cancel();foreach(var entry in _tiles.Values)entry.Cancellation?.Dispose();_cancel.Dispose();}
|
||||
}
|
||||
139
src/Universe.Core/Generation/VegetationTiles.cs
Normal file
139
src/Universe.Core/Generation/VegetationTiles.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using Universe.Core.Math;
|
||||
|
||||
namespace Universe.Core.Generation;
|
||||
|
||||
public readonly record struct VegetationTileKey(int Face,int Level,int X,int Y,int Layer)
|
||||
{
|
||||
public double Width=>2d/(1L<<Level);
|
||||
public DVector3 Direction=>TerrainChunkBuilder.FaceDirection(Face,-1+(X+.5)*Width,-1+(Y+.5)*Width).Normalized();
|
||||
public static VegetationTileKey At(DVector3 direction,int level,int layer)
|
||||
{
|
||||
var d=direction.Normalized();var ax=System.Math.Abs(d.X);var ay=System.Math.Abs(d.Y);var az=System.Math.Abs(d.Z);
|
||||
int face;double u,v;
|
||||
if(ax>=ay&&ax>=az){face=d.X>=0?0:1;u=(d.X>=0?-d.Z:d.Z)/ax;v=d.Y/ax;}
|
||||
else if(ay>=az){face=d.Y>=0?2:3;u=d.X/ay;v=(d.Y>=0?-d.Z:d.Z)/ay;}
|
||||
else {face=d.Z>=0?4:5;u=(d.Z>=0?d.X:-d.X)/az;v=d.Y/az;}
|
||||
var count=1<<level;
|
||||
return new(face,level,System.Math.Clamp((int)((u+1)*.5*count),0,count-1),System.Math.Clamp((int)((v+1)*.5*count),0,count-1),layer);
|
||||
}
|
||||
public VegetationTileKey Offset(int x,int y)=>At(TerrainChunkBuilder.FaceDirection(Face,
|
||||
-1+(X+x+.5)*Width,-1+(Y+y+.5)*Width),Level,Layer);
|
||||
}
|
||||
|
||||
public readonly record struct VegetationInstance(ulong Id,int PlantIndex,DVector3 BodyPosition,DVector3 Up,double Scale,double Yaw,double Variation);
|
||||
public sealed record VegetationTile(VegetationTileKey Key,DVector3 Origin,VegetationInstance[] Instances)
|
||||
{public double GenerationMilliseconds {get;init;}}
|
||||
|
||||
public sealed class VegetationTileGenerator
|
||||
{
|
||||
public BiomeField Field {get;}
|
||||
public VegetationSettings Settings {get;}
|
||||
private readonly int[][] _plants;
|
||||
private readonly ulong _seed;
|
||||
public VegetationTileGenerator(BiomeField field)
|
||||
{
|
||||
Field=field;Settings=field.Planet.Vegetation??new();Settings.Validate();
|
||||
_seed=field.Planet.Seed;foreach(var c in field.Planet.Id)_seed=DeterministicRandom.Mix(_seed^c);
|
||||
_plants=Enumerable.Range(0,3).Select(layer=>field.Catalog.Plants.Select((p,i)=>(p,i)).Where(x=>Layer(x.p.Type)==layer
|
||||
&&(Settings.AllowedPlants is null||Settings.AllowedPlants.Contains(x.p.Id,StringComparer.Ordinal))).Select(x=>x.i).ToArray()).ToArray();
|
||||
if(Settings.AllowedPlants?.Any(id=>!field.Catalog.Plants.Any(p=>p.Id==id))==true)throw new InvalidDataException("Unknown vegetation plant ID.");
|
||||
}
|
||||
public static int Layer(VegetationType type)=>type==VegetationType.Tree?0:type==VegetationType.Shrub?1:2;
|
||||
public int Level(int layer)=>(int)System.Math.Clamp(System.Math.Ceiling(System.Math.Log2(2*Field.Planet.RadiusMeters/(layer==0?256:layer==1?128:32))),0,24);
|
||||
public double Range(int layer)=>layer==0?Settings.TreeDistanceMeters:layer==1?Settings.ShrubDistanceMeters:Settings.GrassDistanceMeters;
|
||||
public IEnumerable<VegetationTileKey> Around(DVector3 bodyCamera,int layer,double? range=null)
|
||||
{
|
||||
var level=Level(layer);var key=VegetationTileKey.At(bodyCamera,level,layer);
|
||||
var width=key.Width*Field.Planet.RadiusMeters;
|
||||
// Cube-face compression is strongest at the corners (sqrt(3)).
|
||||
var cells=(int)System.Math.Ceiling((range??Range(layer))/width*1.75)+2;
|
||||
var keys=new HashSet<VegetationTileKey>();
|
||||
var radial=bodyCamera.Normalized();
|
||||
for(var y=-cells;y<=cells;y++)for(var x=-cells;x<=cells;x++)
|
||||
{
|
||||
var next=key.Offset(x,y);
|
||||
if((next.Direction-radial).Length*Field.Planet.RadiusMeters<=(range??Range(layer))+width*1.5)keys.Add(next);
|
||||
}
|
||||
return keys.OrderBy(k=>(k.Direction-radial).LengthSquared);
|
||||
}
|
||||
public VegetationTile Generate(VegetationTileKey key,CancellationToken token=default)
|
||||
{
|
||||
var watch=System.Diagnostics.Stopwatch.StartNew();
|
||||
var planet=Field.Planet;var origin=key.Direction*planet.RadiusMeters;
|
||||
if(_plants[key.Layer].Length==0||Settings.DensityMultiplier<=0)return new(key,origin,[]);
|
||||
var density=(key.Layer==0?Settings.TreeDensityPerHectare:key.Layer==1?Settings.ShrubDensityPerHectare:Settings.GroundDensityPerHectare)/10000*Settings.DensityMultiplier;
|
||||
var side=System.Math.Max(1,(int)System.Math.Ceiling(key.Width*planet.RadiusMeters*System.Math.Sqrt(density)));
|
||||
var grid=(1L<<key.Level)*side;
|
||||
var spacing=_plants[key.Layer].Max(i=>Field.Catalog.Plants[i].MinimumSpacingMeters);
|
||||
var neighbors=(int)System.Math.Ceiling(spacing/(2*planet.RadiusMeters/grid/3));
|
||||
var output=new List<VegetationInstance>();
|
||||
Span<double> weights=stackalloc double[_plants[key.Layer].Length];
|
||||
for(var y=0;y<side;y++)for(var x=0;x<side;x++)
|
||||
{
|
||||
if((x&15)==0)token.ThrowIfCancellationRequested();
|
||||
var gx=(long)key.X*side+x;var gy=(long)key.Y*side+y;
|
||||
var hash=Hash(key.Face,gx,gy,key.Layer,_seed);
|
||||
var random=new DeterministicRandom(hash);
|
||||
var u=-1+2*(gx+.2+random.NextDouble()*.6)/grid;
|
||||
var v=-1+2*(gy+.2+random.NextDouble()*.6)/grid;
|
||||
if(random.NextDouble()>System.Math.Pow(1+u*u+v*v,-1.5))continue;
|
||||
var d=TerrainChunkBuilder.FaceDirection(key.Face,u,v).Normalized();
|
||||
if(planet.OceanMask?.Sample(d)>.2)continue;
|
||||
var color=Field.Color(d);var biome=Field.Weights(color);
|
||||
if(biome.W>.5)continue;
|
||||
var cover=Field.ForestCover(d);
|
||||
var probability=key.Layer==0?(biome.X+biome.Y*.25)*cover
|
||||
:key.Layer==1?(biome.X*.65+biome.Y*.3)*(.25+cover*.75)
|
||||
:(biome.X*(.18+.4*(1-cover))+biome.Y+biome.Z*.035)*(.4+cover*.6);
|
||||
if(random.NextDouble()>probability)continue;
|
||||
var height=TerrainHeight.Sample(planet,d);
|
||||
if(planet.Ocean is {Enabled:true} water&&height<planet.RadiusMeters+water.SeaLevelMeters+.4)continue;
|
||||
var altitude=height-planet.RadiusMeters;var latitude=System.Math.Abs(System.Math.Asin(d.Y)*180/System.Math.PI);
|
||||
var total=0d;
|
||||
for(var j=0;j<weights.Length;j++)
|
||||
{
|
||||
var p=Field.Catalog.Plants[_plants[key.Layer][j]];
|
||||
var eligible=altitude>=p.AltitudeMeters[0]&&altitude<=p.AltitudeMeters[1]
|
||||
&&latitude>=p.AbsoluteLatitudeDegrees[0]&&latitude<=p.AbsoluteLatitudeDegrees[1];
|
||||
weights[j]=eligible?VegetationCatalog.Match(color,p.MatchColorRgb,p.ColorTolerancePercent)*p.SpawnWeight:0;
|
||||
total+=weights[j];
|
||||
}
|
||||
if(total<=0)continue;
|
||||
var pick=random.NextDouble()*total;var selected=0;
|
||||
for(;selected<weights.Length-1;selected++){pick-=weights[selected];if(pick<=0)break;}
|
||||
var plantIndex=_plants[key.Layer][selected];var plant=Field.Catalog.Plants[plantIndex];
|
||||
// Stable Matérn thinning across cube-face boundaries: lower hashes win a close pair.
|
||||
var rejected=false;
|
||||
for(var dy=-neighbors;dy<=neighbors&&!rejected;dy++)for(var dx=-neighbors;dx<=neighbors;dx++)
|
||||
{
|
||||
if(dx==0&&dy==0)continue;
|
||||
var nu=-1+2*(gx+dx+.5)/grid;var nv=-1+2*(gy+dy+.5)/grid;
|
||||
var neighbor=VegetationTileKey.At(TerrainChunkBuilder.FaceDirection(key.Face,nu,nv),key.Level,key.Layer);
|
||||
var nc=TerrainChunkBuilder.FaceDirection(key.Face,nu,nv).Normalized();
|
||||
// Resolve neighbour microcell in its own face, including pole/cube seams.
|
||||
var f=FaceUv(nc,neighbor.Face);
|
||||
var nx=System.Math.Clamp((long)System.Math.Floor((f.U+1)*.5*grid),0,grid-1);var ny=System.Math.Clamp((long)System.Math.Floor((f.V+1)*.5*grid),0,grid-1);
|
||||
var nh=Hash(neighbor.Face,nx,ny,key.Layer,_seed);
|
||||
if(nh>=hash)continue;
|
||||
var nr=new DeterministicRandom(nh);
|
||||
var nd=TerrainChunkBuilder.FaceDirection(neighbor.Face,-1+2*(nx+.2+nr.NextDouble()*.6)/grid,
|
||||
-1+2*(ny+.2+nr.NextDouble()*.6)/grid).Normalized();
|
||||
if((nd-d).Length*planet.RadiusMeters<spacing){rejected=true;break;}
|
||||
}
|
||||
if(rejected)continue;
|
||||
var normal=TerrainHeight.Normal(planet,d,epsilon:1e-6);
|
||||
if(DVector3.Dot(normal,d)+1e-8<System.Math.Cos(plant.MaxSlopeDegrees*System.Math.PI/180))continue;
|
||||
var scale=plant.ScaleRange[0]+random.NextDouble()*(plant.ScaleRange[1]-plant.ScaleRange[0]);
|
||||
output.Add(new(hash,plantIndex,d*(height-(key.Layer==0?.22:.035)),key.Layer==0?d:normal,scale,random.NextDouble()*System.Math.Tau,random.NextDouble()));
|
||||
}
|
||||
return new(key,origin,output.ToArray()){GenerationMilliseconds=watch.Elapsed.TotalMilliseconds};
|
||||
}
|
||||
private static (double U,double V) FaceUv(DVector3 d,int face)=>face switch
|
||||
{
|
||||
0=>(-d.Z/d.X,d.Y/d.X),1=>(d.Z/-d.X,d.Y/-d.X),2=>(d.X/d.Y,-d.Z/d.Y),
|
||||
3=>(d.X/-d.Y,d.Z/-d.Y),4=>(d.X/d.Z,d.Y/d.Z),_=>(-d.X/-d.Z,d.Y/-d.Z)
|
||||
};
|
||||
private static ulong Hash(int face,long x,long y,int layer,ulong seed)=>DeterministicRandom.Mix(seed
|
||||
^DeterministicRandom.Mix(unchecked((ulong)x))^DeterministicRandom.Mix(unchecked((ulong)y)^0x535041574EUL)
|
||||
^((ulong)face<<52)^((ulong)layer<<58));
|
||||
}
|
||||
24
src/Universe.Core/Generation/VegetationView.cs
Normal file
24
src/Universe.Core/Generation/VegetationView.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Universe.Core.Math;
|
||||
|
||||
namespace Universe.Core.Generation;
|
||||
|
||||
/// <summary>A padded frustum lets small head movements reuse the same instance buffers.</summary>
|
||||
public readonly record struct VegetationView(DVector3 Forward,DVector3 Right,DVector3 Up,double TanHalfFov,double Aspect)
|
||||
{
|
||||
private const double ReuseAngle=4*System.Math.PI/180;
|
||||
private const double PaddingAngle=6*System.Math.PI/180;
|
||||
|
||||
public bool CanReuse(VegetationView next)
|
||||
=> TanHalfFov==next.TanHalfFov&&Aspect==next.Aspect
|
||||
// The trace of the relative basis gives the total rotation, including roll.
|
||||
&&DVector3.Dot(Forward,next.Forward)+DVector3.Dot(Right,next.Right)+DVector3.Dot(Up,next.Up)>=1+2*System.Math.Cos(ReuseAngle);
|
||||
|
||||
public bool Intersects(DVector3 offset,double radius)
|
||||
{
|
||||
radius+=offset.Length*2*System.Math.Sin(PaddingAngle*.5);
|
||||
var z=DVector3.Dot(offset,Forward);var horizontal=TanHalfFov*Aspect;
|
||||
return z>=-radius
|
||||
&&System.Math.Abs(DVector3.Dot(offset,Right))<=z*horizontal+radius*System.Math.Sqrt(1+horizontal*horizontal)
|
||||
&&System.Math.Abs(DVector3.Dot(offset,Up))<=z*TanHalfFov+radius*System.Math.Sqrt(1+TanHalfFov*TanHalfFov);
|
||||
}
|
||||
}
|
||||
82
src/Universe.Core/Physics/VegetationCollision.cs
Normal file
82
src/Universe.Core/Physics/VegetationCollision.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using Universe.Core.Generation;
|
||||
using Universe.Core.Math;
|
||||
|
||||
namespace Universe.Core.Physics;
|
||||
|
||||
public readonly record struct TrunkCapsule(DVector3 Base,DVector3 Up,double Radius,double Height);
|
||||
|
||||
public static class VegetationCollision
|
||||
{
|
||||
/// <summary>Conservative continuous capsule sweep with tangent sliding. Coordinates are body-local metres.</summary>
|
||||
public static DVector3 Move(DVector3 start,DVector3 end,DVector3 up,double eyeHeight,IReadOnlyList<TrunkCapsule> trunks,double playerRadius=.28)
|
||||
{
|
||||
var position=start;
|
||||
foreach(var trunk in trunks)
|
||||
{
|
||||
var separation=Closest(position-up*(eyeHeight-playerRadius),position-up*playerRadius,
|
||||
trunk.Base+trunk.Up*trunk.Radius,trunk.Base+trunk.Up*System.Math.Max(trunk.Radius,trunk.Height-trunk.Radius));
|
||||
var penetration=playerRadius+trunk.Radius-separation.Length;
|
||||
if(penetration>0)
|
||||
{
|
||||
var normal=separation.Normalized();
|
||||
if(normal==DVector3.Zero)normal=DVector3.Cross(up,System.Math.Abs(up.Z)<.9?new(0,0,1):new DVector3(1,0,0)).Normalized();
|
||||
var push=normal*(penetration+.001);position+=push;end+=push;
|
||||
}
|
||||
}
|
||||
// A player's body is a vertical capsule below the camera; crouching changes its segment length.
|
||||
for(var iteration=0;iteration<3;iteration++)
|
||||
{
|
||||
var delta=end-position;var length=delta.Length;if(length<1e-8)break;
|
||||
var best=1d;var normal=DVector3.Zero;
|
||||
foreach(var trunk in trunks)
|
||||
{
|
||||
var radius=playerRadius+trunk.Radius;
|
||||
var t=0d;
|
||||
for(var step=0;step<64&&t<=best;step++)
|
||||
{
|
||||
var point=position+delta*t;
|
||||
var a=point-up*(eyeHeight-playerRadius);var b=point-up*playerRadius;
|
||||
var c=trunk.Base+trunk.Up*trunk.Radius;var d=trunk.Base+trunk.Up*System.Math.Max(trunk.Radius,trunk.Height-trunk.Radius);
|
||||
var separation=Closest(a,b,c,d);
|
||||
var clearance=separation.Length-radius;
|
||||
if(clearance<.0002)
|
||||
{
|
||||
var n=separation.Normalized();if(n==DVector3.Zero)n=(point-trunk.Base-up*DVector3.Dot(point-trunk.Base,up)).Normalized();
|
||||
if(n==DVector3.Zero)n=DVector3.Cross(up,new DVector3(0,0,1)).Normalized();
|
||||
if(t<best&&DVector3.Dot(delta,n)<0){best=t;normal=n;}
|
||||
break;
|
||||
}
|
||||
t+=System.Math.Max(.000001,clearance/length*.9);
|
||||
}
|
||||
}
|
||||
if(best>=1){position=end;break;}
|
||||
position+=delta*System.Math.Max(0,best-.0001/length);
|
||||
var remaining=end-position;
|
||||
remaining-=normal*System.Math.Min(0,DVector3.Dot(remaining,normal));
|
||||
end=position+remaining;
|
||||
}
|
||||
return position;
|
||||
}
|
||||
private static DVector3 Closest(DVector3 p1,DVector3 q1,DVector3 p2,DVector3 q2)
|
||||
{
|
||||
var d1=q1-p1;var d2=q2-p2;var r=p1-p2;
|
||||
var a=d1.LengthSquared;var e=d2.LengthSquared;var f=DVector3.Dot(d2,r);
|
||||
double s,t;
|
||||
if(a<=1e-12&&e<=1e-12)return r;
|
||||
if(a<=1e-12){s=0;t=System.Math.Clamp(f/e,0,1);}
|
||||
else
|
||||
{
|
||||
var c=DVector3.Dot(d1,r);
|
||||
if(e<=1e-12){t=0;s=System.Math.Clamp(-c/a,0,1);}
|
||||
else
|
||||
{
|
||||
var b=DVector3.Dot(d1,d2);var den=a*e-b*b;
|
||||
s=den>1e-12?System.Math.Clamp((b*f-c*e)/den,0,1):0;
|
||||
t=(b*s+f)/e;
|
||||
if(t<0){t=0;s=System.Math.Clamp(-c/a,0,1);}
|
||||
else if(t>1){t=1;s=System.Math.Clamp((b-c)/a,0,1);}
|
||||
}
|
||||
}
|
||||
return p1+d1*s-p2-d2*t;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user