Snyggare moln

This commit is contained in:
2026-11-03 22:56:33 +01:00
parent 6ea397a9e5
commit 490da8e97f
34 changed files with 1270 additions and 72 deletions

View File

@@ -400,8 +400,32 @@ Ringens atmosfär och moln använder cylindriska volymer begränsade av kanterna
Terräng och hav tar emot trädens kaskadskuggor, styrda av `vegetation.shadowDistancesMeters`, `shadowResolution`, `shadowSoftness` och `shadowBiasMeters`. Stommen och båda kantväggarna skymmer direkt solljus enligt sina faktiska cylindriska volymer, inklusive väggarnas ovan- och utsidor. Mark, vatten, växter och atmosfär använder samma skugggeometri i ringens lokala koordinater, så skuggorna följer solen när ringen roterar. Molnskuggorna integrerar tätheten längs solstrålen genom det invändiga molnlagret. De följer de synliga molnens seed, vind och täckning; `clouds.shadowStrength` styr styrkan (0 stänger av molnskuggorna). Terräng och hav tar emot trädens kaskadskuggor, styrda av `vegetation.shadowDistancesMeters`, `shadowResolution`, `shadowSoftness` och `shadowBiasMeters`. Stommen och båda kantväggarna skymmer direkt solljus enligt sina faktiska cylindriska volymer, inklusive väggarnas ovan- och utsidor. Mark, vatten, växter och atmosfär använder samma skugggeometri i ringens lokala koordinater, så skuggorna följer solen när ringen roterar. Molnskuggorna integrerar tätheten längs solstrålen genom det invändiga molnlagret. De följer de synliga molnens seed, vind och täckning; `clouds.shadowStrength` styr styrkan (0 stänger av molnskuggorna).
Närmaterialen använder både färg, normal och roughness från materialkatalogen. Mark, växtmodeller och trädens impostorer delar ett diffust himmelsljus i ringens gravitationsram; löv får även ljusgenomsläpp. Stjärnor tonas ned när spelaren står i dagsljus i ringens atmosfär.
Molnen har två cylindriska volymlager. Huvudlagret använder befintliga `clouds` för höjd, täckning, täthet, vind och ljus. `shapeScaleMeters` styr molnformationernas storlek, `detailScaleMeters` och `erosion` detaljerna i deras kanter. Cachade 3D-volymer med fraktal- och Worley-brus ger rundade molnformer och mindre luckor. Större väderområden skiftar täckningen över ringen; `ringWorld.cloudDetail.weatherScaleMeters` styr deras fysiska skala (standard 90 km), `weatherVariation` variationens styrka (0–1, 0,9 i standardringen). Noll ger jämn täckning men behåller enskilda molnformer. Täckning 0 och 1 i huvudlagret är fortfarande helt klar respektive helt mulen himmel.
`ringWorld.cloudDetail` styr också ett tunnare närlager för små, finare moln. `nearEnabled` slår av och på lagret. Standardringen använder `nearBaseAltitudeMeters: 7200`, `nearThicknessMeters: 700`, `nearScaleMeters: 3200`, `nearDetailScaleMeters: 240`, `nearCoverage: 0.3` och `nearDensity: 0.24`. Båda höjdvärdena mäts från ringens invändiga nollnivå; hela lagret måste rymmas i atmosfären. Närmolnen samlas i större väderfläckar med tydliga blå luckor mellan grupperna. De finaste brusoktaverna bidrar svagare, och mycket tunna resttätheter tas bort för att undvika ett jämnt kornigt täcke. Standardringen använder även glesare huvudmoln med täckning 0,29, skala 26 km och erosion 0,2. Närlagret är oberoende av huvudlagrets täckning och täthet men kräver `clouds.enabled: true`.
`nearDistanceMeters` är 100 km i standardringen. Närlagret tonar bort från 65 procent av räckvidden och renderas inte bortom gränsen, så de små molnen hamnar inte på ringens bortre sida. Närdetaljer i huvudlagret tonas också bort med avstånd och pixelstorlek. På avstånd används större, varierade former och väderområden. Brusfält och vind är bundna till ringens lokala koordinater och saknar longitudskarv. Molnskuggorna använder samma väder och stora former med grövre filtrering för stabila skuggkanter.
Molnen renderas separat från luften med `clouds.resolutionScale` (standard 0,5) och skalas upp med hjälp av scenens djup. `clouds.viewSteps` och `lightSteps` begränsar provtagningen nära kameran (standard 96 och 2); avlägsna moln använder högst 24 respektive 2 steg. Närlagret får tätare provtagning för fina detaljer. Självskuggning, flera approximationer av ljusspridning och luftperspektiv ger ljusa kanter, mörkare undersidor och mjukare färg på avstånd. Ringens huvudlager begränsas inte av Jordens `clouds.maxViewDistanceMeters`, eftersom ringens bortre sida ska synas. Närlagret begränsas av sin egen räckvidd.
`ringWorld.meadow` lägger till ett separat gräslager med geometriska strån nära spelaren. `enabled` stänger av lagret, `distanceMeters` styr räckvidden (8–80 m), `spacingMeters` avståndet mellan tuvorna (0,3–2 m) och `heightMeters` grundhöjden (0,05–1 m). Standardringen använder 48 m, 0,35 m och 0,28 m. Tuvornas höjd varierar och tonas av mot räckviddens slut. Rötter genereras deterministiskt i små lokala rutor på en bakgrundstråd; stråna byggs på grafikkortet, följer marken och tar emot träd-, vägg- och molnskuggor. Lagret utesluter vatten, sand, snö och branta sluttningar. Det kastar inte egna individuella stråskuggor. Befintliga örter, buskar och träd renderas separat.
Gräsets täthet och höjd varierar i sammanhängande fläckar som går över rutgränserna. Varje strå har egen längd, böjning och färgvariation, tre böjda sektioner, en ljusare mittnerv och en svag glans. Vindrörelsen varierar mellan tuvorna.
`ringWorld.rendering.temporalAntialiasing` aktiverar åtta subpixelpositioner och återanvänder tidigare HDR-bildrutor nära marken (inom 100 m). Historiken följer ringens lokala koordinater genom omlopp, rotation och spelarens rörelse. Djupjämförelse och begränsning mot aktuella grannpixlar minskar släp vid nya silhuetter. Historiken återställs vid byte av värld, teleportering, ändrad upplösning, synfält och shaderomladdning. Gräsets och lövens vind har ingen separat rörelsevektortextur; små rörelser hanteras med djup- och färgavvisning. På avstånd används kortare historik för moln, och bakgrunden utan ytdjup ackumuleras inte.
`ringWorld.rendering.contactOcclusion` aktiverar korta kontaktskuggor från djupbilden nära marken. `contactStrength` styr mörkningen (0–1, standard 0,45). Effekten beräknas i halv upplösning och skalas upp med djupfiltrering för att undvika mörka kanter mot himlen. Den använder bara synlig geometri och ersätter inte trädens eller väggarnas solskuggor. Båda renderingseffekterna är aktiva i standardringen och kan stängas av var för sig i JSON.
Verifiera med `dotnet test tests/Universe.Tests/Universe.Tests.csproj -c Release -m:1` och `dotnet run -c Release --project src/Universe.App -- --ring-world-report`. Grafikrapporten kör de riktiga OpenGL-shadrarna och sparar översikt, insida, mark, fötter och hav i `graphics-report/ring-world/` bredvid appen. `surface-map.png` visar hela färgkartan som fyra efterföljande remsor med bevarade proportioner. Rapporten skriver ut faktisk upplösning i meter och kontrollerar även att vegetation laddas och att markdjup finns under spelaren. Verifiera med `dotnet test tests/Universe.Tests/Universe.Tests.csproj -c Release -m:1` och `dotnet run -c Release --project src/Universe.App -- --ring-world-report`. Grafikrapporten kör de riktiga OpenGL-shadrarna och sparar översikt, insida, mark, fötter och hav i `graphics-report/ring-world/` bredvid appen. `surface-map.png` visar hela färgkartan som fyra efterföljande remsor med bevarade proportioner. Rapporten skriver ut faktisk upplösning i meter och kontrollerar även att vegetation laddas och att markdjup finns under spelaren.
`performance.txt` redovisar median och 95:e percentilen för bildrutetid, kostnad per renderingspass samt en separat mätning med spelare, trädkollisioner, omlopp och rotation aktiva. Den mäter också oavsiktlig rörelse relativt ringen; högst 5 mm tillåts. Testsviten kontrollerar dessutom 60 sekunders stillastående på ojämn mark med både frusen och rörlig ring. FPS-värdena gäller aktuell dator, upplösning och inläst testscen. `performance.txt` redovisar median och 95:e percentilen för bildrutetid, kostnad per renderingspass samt en separat mätning med spelare, trädkollisioner, omlopp och rotation aktiva. Den mäter också oavsiktlig rörelse relativt ringen; högst 5 mm tillåts. Testsviten kontrollerar dessutom 60 sekunders stillastående på ojämn mark med både frusen och rörlig ring. FPS-värdena gäller aktuell dator, upplösning och inläst testscen.
Lägg till `--report-1440p` efter `--ring-world-report` för ett dolt fönster på 2560 × 1440. Rapporten mäter även gång, kamerarörelse och aktiv vind, skriver antal synliga grästuvor och sparar `walking.png`. Gräslagret ingår i tiden för terrängpasset. Kontaktskuggor och temporal kantutjämning redovisas separat; DLSS ingår inte.
GPU-rapporten kontrollerar också temporal kantutjämning mot ett medelvärde av åtta subpixelprov, återställning efter kameraklipp och att kontaktskuggorna lämnar en plan yta oförändrad men skuggar intill en upphöjning. `surface-effects-off.png`, `contact-on.png` och `surface-effects-on.png` jämför samma vy med effekterna av och på. Testsviten kontrollerar att geometri och rekonstruerade synstrålar använder samma jitter samt att återprojektionen följer en roterande och förflyttad ring.
Molnkontrollerna kör den riktiga molnshadern med isolerat närlager och huvudlager. De verifierar att närmoln är synliga nära kameran, helt saknas på avstånd och kan stängas av, samt att väderinställningen förändrar avlägsna moln. `cloud-near.png`, `cloud-distant.png` och `cloud-inside-near.png` visar marknära vyer och en vy inifrån närlagret. `cloud-near-fixture.png` och `cloud-far-fixture.png` visar de isolerade GPU-kontrollerna. Molnens kostnad redovisas separat under `clouds` i prestandarapporten.
Grafikrapporten testar även solstrålar över och under båda väggkanterna, stommens in- och utsida samt det öppna centrumet direkt i produktionsshadern. Den kontrollerar molnskuggornas täckning, vindrörelse och koordinattransformation, och sparar jämförelsebilderna `tree-shadows-on/off.png` och `cloud-shadows-on/off.png`. Trädjämförelsen isolerar skuggorna på ringens ytor; molnjämförelsen använder en kontrollerad mulen himmel med samma träd och terräng. Grafikrapporten testar även solstrålar över och under båda väggkanterna, stommens in- och utsida samt det öppna centrumet direkt i produktionsshadern. Den kontrollerar molnskuggornas täckning, vindrörelse och koordinattransformation, och sparar jämförelsebilderna `tree-shadows-on/off.png` och `cloud-shadows-on/off.png`. Trädjämförelsen isolerar skuggorna på ringens ytor; molnjämförelsen använder en kontrollerad mulen himmel med samma träd och terräng.

View File

@@ -23,7 +23,7 @@
"enabled": true, "enabled": true,
"flatMaterial": "leafy-grass", "flatMaterial": "leafy-grass",
"slopeMaterial": "rock-ground", "slopeMaterial": "rock-ground",
"scaleMeters": 8, "scaleMeters": 2,
"fadeDistanceMeters": 2000, "fadeDistanceMeters": 2000,
"strength": 0.8 "strength": 0.8
}, },
@@ -32,12 +32,12 @@
"densityTextureStrength": 0.0, "densityTextureStrength": 0.0,
"baseAltitudeMeters": 2100, "baseAltitudeMeters": 2100,
"thicknessMeters": 4300, "thicknessMeters": 4300,
"coverage": 0.4, "coverage": 0.29,
"density": 0.85, "density": 0.7,
"shapeScaleMeters": 18000, "shapeScaleMeters": 26000,
"detailScaleMeters": 2400, "detailScaleMeters": 1300,
"erosion": 0.45, "erosion": 0.2,
"cloudType": 0.9, "cloudType": 0.75,
"windSpeedMetersPerSecond": 18, "windSpeedMetersPerSecond": 18,
"windDirectionDegrees": 25, "windDirectionDegrees": 25,
"extinctionPerMeter": 0.0007, "extinctionPerMeter": 0.0007,
@@ -48,9 +48,9 @@
"sunlightStrength": 1.0, "sunlightStrength": 1.0,
"sunsetStrength": 1.4, "sunsetStrength": 1.4,
"shadowStrength": 0.7, "shadowStrength": 0.7,
"viewSteps": 48, "viewSteps": 96,
"lightSteps": 4, "lightSteps": 2,
"resolutionScale": 0.4, "resolutionScale": 0.5,
"maxViewDistanceMeters": 220000 "maxViewDistanceMeters": 220000
}, },
"ocean": { "ocean": {
@@ -133,7 +133,8 @@
150, 150,
400 400
], ],
"grassDistanceMeters": 80, "grassDistanceMeters": 65,
"groundDensityPerHectare": 16000,
"shrubDistanceMeters": 150, "shrubDistanceMeters": 150,
"affectGroundMaterials": true, "affectGroundMaterials": true,
"uploadBudgetMilliseconds": 1, "uploadBudgetMilliseconds": 1,
@@ -147,6 +148,29 @@
] ]
}, },
"ringWorld": { "ringWorld": {
"cloudDetail": {
"weatherScaleMeters": 90000,
"weatherVariation": 0.9,
"nearEnabled": true,
"nearBaseAltitudeMeters": 7200,
"nearThicknessMeters": 700,
"nearScaleMeters": 3200,
"nearDetailScaleMeters": 240,
"nearCoverage": 0.3,
"nearDensity": 0.24,
"nearDistanceMeters": 100000
},
"rendering": {
"temporalAntialiasing": true,
"contactOcclusion": true,
"contactStrength": 0.45
},
"meadow": {
"enabled": true,
"distanceMeters": 48,
"spacingMeters": 0.35,
"heightMeters": 0.28
},
"diameterMeters": 5000000, "diameterMeters": 5000000,
"widthMeters": 500000, "widthMeters": 500000,
"thicknessMeters": 50000, "thicknessMeters": 50000,

View File

@@ -0,0 +1,90 @@
using System.Numerics;
using Silk.NET.OpenGL;
using Universe.App.Runtime;
using Universe.Core.Generation;
using Universe.Core.Math;
using Universe.Core.World;
namespace Universe.App.Graphics;
internal static partial class GraphicsReport
{
private static void RunRingCloudFixtures(GL gl,UniverseRenderer renderer,PlanetData planet,string directory)
{
const int width=128,height=96;
using var target=new RenderTarget(gl,width,height,fullPrecision:true);
using var depth=new RenderTarget(gl,width,height,depth:true);
using var quad=new GpuMesh(gl,3,[-1,-1,0,3,-1,0,-1,3,0]);
using var shader=ShaderProgram.Create(gl,ShaderSources.Load("fullscreen.vert"),ShaderSources.Load("ring-world-cloud.frag"));
depth.Bind();gl.DepthMask(true);gl.ClearDepth(1);gl.Clear(ClearBufferMask.DepthBufferBit);
float[] Draw(PlanetData body,DVector3 camera,DVector3 forward,float tan)
{
target.Bind();gl.Disable(GLEnum.DepthTest);gl.Disable(GLEnum.Blend);gl.DepthMask(false);shader.Use();
renderer.BindRingShadowSettings(shader,body,camera*body.RadiusMeters);
shader.Set("uCameraBody",camera.ToVector3());shader.SetMatrix("uWorldToBody",Matrix4x4.Identity);
shader.Set("uRadius",(float)body.RadiusMeters);shader.Set("uAirHeight",(float)(body.AtmosphereHeightMeters/body.RadiusMeters));
shader.Set("uAirDensity",(float)body.AtmosphereDensity);shader.Set("uAirColor",body.AtmosphereColor);
shader.Set("uForward",forward.ToVector3());shader.Set("uRight",DVector3.Cross(forward,new(0,1,0)).ToVector3());
shader.Set("uUp",Vector3.UnitY);shader.Set("uAspect",(float)width/height);shader.Set("uTanHalfFov",tan);shader.Set("uRayJitter",Vector3.Zero);
shader.Set("uFar",1e12f);shader.Set("uDepth",1);shader.Set("uRingCloudViewSteps",80);shader.Set("uRingCloudLightSteps",3);
shader.Set("uRingCloudPixelAngle",2*tan/height);shader.Set("uRingTime",0f);
shader.Set("uLightCount",1);shader.Set("uLightDirections[0]",Vector3.Normalize(new(-1,.8f,0)));
shader.Set("uLightColors[0]",Vector3.One);shader.Set("uLightFlux[0]",1f);
gl.ActiveTexture(GLEnum.Texture1);gl.BindTexture(GLEnum.Texture2D,depth.Depth);quad.Draw();gl.Finish();
var pixels=Read(gl,width,height);
if(pixels.Any(p=>!float.IsFinite(p))||gl.GetError()!=GLEnum.NoError)throw new InvalidOperationException("Ring cloud GPU output is invalid.");
return pixels;
}
var basis=planet with {Orientation=PlanetOrientation.Identity};
var nearOnly=basis with {Clouds=basis.Clouds! with {Density=0}};
static double Opacity(float[] pixels){double total=0;for(var i=3;i<pixels.Length;i+=4)total+=(1-pixels[i])/(width*height);return total;}
var localCamera=new DVector3(.9996,0,0);
var local=Draw(nearOnly,localCamera,new(-1,0,0),.4f);
// Sparse weather deliberately includes clear regions. Check several sites
// rather than requiring a cloud directly above one fixed coordinate.
if(Opacity(local)<.005)for(var site=0;site<8;site++)
{
var candidateCamera=new DVector3(.9996,(site-3.5)*.016,0);
var candidate=Draw(nearOnly,candidateCamera,new(-1,0,0),.4f);
if(Opacity(candidate)>Opacity(local)){local=candidate;localCamera=candidateCamera;}
}
var noNear=Draw(nearOnly with {RingWorld=basis.RingWorld! with {CloudDetail=basis.RingWorld.CloudDetail with {NearEnabled=false}}},localCamera,new(-1,0,0),.4f);
var distantNear=Draw(nearOnly,DVector3.Zero,new(1,0,0),.09f);
double nearOpacity=0;var distanceError=0f;var disabledError=0f;
for(var i=3;i<local.Length;i+=4)
{
nearOpacity+=(1-local[i])/(width*height);distanceError=System.Math.Max(distanceError,System.Math.Abs(1-distantNear[i]));
disabledError=System.Math.Max(disabledError,System.Math.Abs(1-noNear[i]));
}
if(nearOpacity<.005||distanceError>.0001||disabledError>.0001)
throw new InvalidOperationException($"Near cloud range failure: opacity {nearOpacity}, far {distanceError}, disabled {disabledError}.");
var mainOnly=basis with {RingWorld=basis.RingWorld! with {CloudDetail=basis.RingWorld.CloudDetail with {NearEnabled=false}}};
var farClouds=Draw(mainOnly,DVector3.Zero,new(1,0,0),.09f);
var flatWeather=Draw(mainOnly with {RingWorld=mainOnly.RingWorld! with {CloudDetail=mainOnly.RingWorld.CloudDetail with {WeatherVariation=0}}},DVector3.Zero,new(1,0,0),.09f);
var low=1f;var high=0f;double weatherChange=0;
for(var i=3;i<farClouds.Length;i+=4)
{low=System.Math.Min(low,farClouds[i]);high=System.Math.Max(high,farClouds[i]);weatherChange+=System.Math.Abs(farClouds[i]-flatWeather[i])/(width*height);}
if(high-low<.1||weatherChange<.005)throw new InvalidOperationException($"Distant ring weather lacks variation: range {high-low}, weather {weatherChange}.");
WritePng(Path.Combine(directory,"cloud-near-fixture.png"),width,height,local,false);
WritePng(Path.Combine(directory,"cloud-far-fixture.png"),width,height,farClouds,false);
Console.WriteLine($"Ring cloud GPU fixtures PASS: near opacity {nearOpacity:F3}, zero distant near layer {distanceError:F5}, weather range {low:F3}–{high:F3}, weather change {weatherChange:F3}.");
}
private static void CaptureRingCloudViews(GL gl,UniverseRenderer renderer,FlyCamera camera,UniverseNeighborhood world,PlanetData planet,DVector3 ground,int width,int height,string directory)
{
var body=UniversePosition.AtCellCenter(world.ActiveSystem.Coordinate,camera.CellSizeMeters).Add(planet.PositionMeters,camera.CellSizeMeters);
var up=-RingWorldGeometry.Radial(ground);var tangent=DVector3.Cross(new(0,1,0),up);
void Capture(string name,DVector3 local,DVector3 forward,DVector3 skyUp)
{
camera.SetPlayerPose(body.Add(planet.Orientation.ToWorld(local),camera.CellSizeMeters),planet.Orientation.ToWorld(forward.Normalized()),planet.Orientation.ToWorld(skyUp),true);
renderer.ResetSurfaceHistory();for(var i=0;i<48;i++){renderer.Render(camera,world,0);gl.Finish();}
var pixels=Read(gl,width,height);if(pixels.Any(p=>!float.IsFinite(p))||gl.GetError()!=GLEnum.NoError)throw new InvalidOperationException("Ring cloud view is invalid.");
WritePng(Path.Combine(directory,name+".png"),width,height,pixels,false);
}
Capture("cloud-near",ground,tangent*.2+new DVector3(0,.8,0)+up*.65,up);
Capture("cloud-distant",ground,tangent+up*.35,up);
var layer=planet.RingWorld!.CloudDetail;
var local=-up*(planet.RadiusMeters-layer.NearBaseAltitudeMeters-layer.NearThicknessMeters*.4)+new DVector3(0,ground.Y,0);
Capture("cloud-inside-near",local,tangent*.4+new DVector3(0,1,0),up);
}
}

View File

@@ -0,0 +1,90 @@
using System.Numerics;
using Silk.NET.OpenGL;
using Universe.App.Runtime;
using Universe.Core.Generation;
using Universe.Core.Math;
using Universe.Core.World;
namespace Universe.App.Graphics;
internal static partial class GraphicsReport
{
private static void RunRingSurfaceFixtures(GL gl,PlanetData planet,string directory)
{
const int width=128,height=96;const float far=1e12f;
using var effects=new RingSurfaceEffects(gl);using var scene=new RenderTarget(gl,width,height,depth:true);
using var quad=new GpuMesh(gl,3,[-1,-1,0,3,-1,0,-1,3,0]);
using var source=ShaderProgram.Create(gl,ShaderSources.Load("fullscreen.vert"),"""
#version 330 core
in vec2 vUv;
uniform vec3 uJitter;
uniform int uMode;
uniform float uFar;
out vec4 fragColor;
void main(){
vec2 uv=vUv-uJitter.xy;
float pattern=step(.68,fract((uv.x+uv.y*.31)*128.0*.7));
fragColor=uMode==0?vec4(vec3(pattern),1):uMode==1?vec4(1,0,0,1):vec4(.5,.5,.5,1);
float distance=uMode==3&&all(lessThan(abs(uv-vec2(.5)),vec2(.12)))?4.8:5.0;
gl_FragDepth=log2(1.0+distance)/log2(1.0+uFar);
}
""");
var camera=new FlyCamera(UniversePosition.AtCellCenter(new(0,0,0),1e10),1e10);
camera.SetPlayerPose(camera.Position,new(0,0,-1),new(0,1,0));var history=new RingTemporalHistory();
void BindCamera(ShaderProgram shader)
{
shader.Set("uForward",new Vector3(0,0,-1));shader.Set("uRight",Vector3.UnitX);shader.Set("uUp",Vector3.UnitY);
shader.Set("uAspect",(float)width/height);shader.Set("uTanHalfFov",(float)System.Math.Tan(camera.FieldOfViewRadians*.5));
shader.Set("uRayJitter",new Vector3(history.Jitter.X/width,history.Jitter.Y/height,0));
}
void Draw(int mode)
{
scene.Bind();gl.Enable(GLEnum.DepthTest);gl.DepthFunc(GLEnum.Always);gl.DepthMask(true);gl.Disable(GLEnum.Blend);
source.Use();source.Set("uJitter",new Vector3(history.Jitter.X/width,history.Jitter.Y/height,0));source.Set("uMode",mode);source.Set("uFar",far);quad.Draw();
gl.DepthFunc(GLEnum.Lequal);
}
var reference=new float[width*height*4];float[] raw=[],resolved=[];
for(var frame=0;frame<64;frame++)
{
history.Advance(camera,planet,new(planet.RadiusMeters-2,0,0),width,height,far);Draw(0);
raw=Read(gl,width,height);
if(frame<8)for(var i=0;i<raw.Length;i++)reference[i]+=raw[i]/8;
effects.Resolve(scene,scene.Depth,far,history,BindCamera);resolved=Read(gl,width,height);
}
double Error(float[] pixels)
{
double error=0;for(var i=0;i<pixels.Length;i+=4){var d=pixels[i]-reference[i];error+=d*d/(width*height);}return error;
}
var before=Error(raw);var after=Error(resolved);
if(after>=before*.5)throw new InvalidOperationException($"Temporal coverage did not converge: {before}/{after}.");
WritePng(Path.Combine(directory,"temporal-fixture.png"),width,height,resolved,false);
camera.SetPlayerPose(camera.Position,camera.Forward,camera.Up,true);
history.Advance(camera,planet,new(planet.RadiusMeters-2,0,0),width,height,far);Draw(1);
effects.Resolve(scene,scene.Depth,far,history,BindCamera);var reset=Read(gl,width,height);
if(System.Math.Abs(reset[0]-1)>.001||reset[1]>.001)throw new InvalidOperationException("Temporal camera cut retained an old frame.");
history.Reset();Draw(2);effects.Contact(scene,scene.Depth,far,1,BindCamera);var flat=Read(gl,width,height);
Draw(3);effects.Contact(scene,scene.Depth,far,1,BindCamera);var contact=Read(gl,width,height);
var darkest=1f;var planeError=0f;
for(var i=0;i<flat.Length;i+=4){planeError=System.Math.Max(planeError,System.Math.Abs(flat[i]-.5f));darkest=System.Math.Min(darkest,contact[i]);}
if(planeError>.005||darkest>.49)throw new InvalidOperationException($"Contact occlusion failed: plane {planeError}, contact {darkest}.");
if(contact.Any(p=>!float.IsFinite(p))||gl.GetError()!=GLEnum.NoError)throw new InvalidOperationException("Ring surface GPU fixture error.");
WritePng(Path.Combine(directory,"contact-fixture.png"),width,height,contact,false);
Console.WriteLine($"Ring surface GPU fixtures PASS: temporal error {before:F4} -> {after:F4}; flat-plane error {planeError:F5}; contact {darkest:F3}; camera cut clean.");
}
private static void CaptureRingSurfaceComparison(GL gl,UniverseRenderer renderer,FlyCamera camera,UniverseNeighborhood world,int width,int height,string directory)
{
void Snapshot(string name)
{
renderer.ResetSurfaceHistory();for(var i=0;i<24;i++)renderer.Render(camera,world,0);gl.Finish();
WritePng(Path.Combine(directory,name+".png"),width,height,Read(gl,width,height),false);
}
try
{
renderer.RingTemporalAntialiasing=false;renderer.RingContactOcclusion=false;Snapshot("surface-effects-off");
renderer.RingContactOcclusion=true;Snapshot("contact-on");
renderer.RingTemporalAntialiasing=true;Snapshot("surface-effects-on");
}
finally{renderer.RingTemporalAntialiasing=true;renderer.RingContactOcclusion=true;renderer.ResetSurfaceHistory();}
}
}

View File

@@ -53,11 +53,14 @@ internal static partial class GraphicsReport
var overview = new DVector3(1, 1.35, .65).Normalized() * planet.RadiusMeters * 3; var overview = new DVector3(1, 1.35, .65).Normalized() * planet.RadiusMeters * 3;
Pose(overview, -overview.Normalized(), new(0, 1, 0)); Capture("overview"); Pose(overview, -overview.Normalized(), new(0, 1, 0)); Capture("overview");
RunRingWorldShadowFixtures(gl, renderer, planet, dir); RunRingWorldShadowFixtures(gl, renderer, planet, dir);
RunRingCloudFixtures(gl,renderer,planet,dir);
RunRingSurfaceFixtures(gl,planet,dir);
var local = FlyCamera.RingInteriorPoint(world.ActiveSystem, planet); var local = FlyCamera.RingInteriorPoint(world.ActiveSystem, planet);
var up = -RingWorldGeometry.Radial(local); var tangent = DVector3.Cross(new(0, 1, 0), up); var up = -RingWorldGeometry.Radial(local); var tangent = DVector3.Cross(new(0, 1, 0), up);
Pose(local, (tangent + up * .1).Normalized(), up); Capture("interior"); Pose(local, (tangent + up * .1).Normalized(), up); Capture("interior");
var ground = RingWorldGeometry.Point(planet, System.Math.Atan2(local.Z, local.X), local.Y, 1.7); var ground = RingWorldGeometry.Point(planet, System.Math.Atan2(local.Z, local.X), local.Y, 1.7);
Pose(ground, (tangent + up * .06).Normalized(), up); Capture("ground", 35); Pose(ground, (tangent + up * .06).Normalized(), up); Capture("ground", 35);
CaptureRingSurfaceComparison(gl,renderer,camera,world,width,height,dir);
var samples = new List<double>(); var samples = new List<double>();
for (var i = 0; i < 120; i++) for (var i = 0; i < 120; i++)
{ {
@@ -105,10 +108,26 @@ internal static partial class GraphicsReport
} }
samples.Sort(); samples.Sort();
measurements.Add($"Playing on moving ring: median {samples[60]:F2} ms ({1000 / samples[60]:F1} FPS), p95 {samples[114]:F2} ms; idle drift {maximumDrift:F6} m."); measurements.Add($"Playing on moving ring: median {samples[60]:F2} ms ({1000 / samples[60]:F1} FPS), p95 {samples[114]:F2} ms; idle drift {maximumDrift:F6} m.");
samples.Clear();
for(var i=0;i<240;i++)
{
var timer=System.Diagnostics.Stopwatch.StartNew();
renderer.SimulationTimeSeconds=i/60d;
player.Look(System.Math.Sin(i*.035)*3,0);
player.Update(1d/60,.75,.15,false,false,current);
camera.SetPlayerPose(player.Position,player.Forward,player.Up);
renderer.Render(camera,current,1d/60);gl.Finish();
samples.Add(timer.Elapsed.TotalMilliseconds);
}
samples.Sort();
measurements.Add($"Walking and looking around with wind: median {samples[120]:F2} ms ({1000/samples[120]:F1} FPS), p95 {samples[228]:F2} ms; meadow {renderer.RingMeadowTufts} tufts.");
WritePng(Path.Combine(dir,"walking.png"),width,height,Read(gl,width,height),false);
renderer.SimulationTimeSeconds=0;
if (maximumDrift > .005) throw new InvalidOperationException("Player drifts relative to the moving Ring World."); if (maximumDrift > .005) throw new InvalidOperationException("Player drifts relative to the moving Ring World.");
foreach (var line in measurements) Console.WriteLine(line); foreach (var line in measurements) Console.WriteLine(line);
File.WriteAllLines(Path.Combine(dir, "performance.txt"), measurements); File.WriteAllLines(Path.Combine(dir, "performance.txt"), measurements);
if (renderer.Vegetation.Stream?.InstanceCount is not > 0) throw new InvalidOperationException("Ring World vegetation did not stream."); if (renderer.Vegetation.Stream?.InstanceCount is not > 0) throw new InvalidOperationException("Ring World vegetation did not stream.");
CaptureRingCloudViews(gl,renderer,camera,world,planet,ground,width,height,dir);
RunRingWorldShadowViews(gl, renderer, camera, world, planet, ground, width, height, dir); RunRingWorldShadowViews(gl, renderer, camera, world, planet, ground, width, height, dir);
Pose(ground, (tangent * .2 - up).Normalized(), tangent); Capture("feet", 8); Pose(ground, (tangent * .2 - up).Normalized(), tangent); Capture("feet", 8);
var depth = new float[width * height]; gl.BindTexture(GLEnum.Texture2D, renderer.SceneDepthTexture); var depth = new float[width * height]; gl.BindTexture(GLEnum.Texture2D, renderer.SceneDepthTexture);

View File

@@ -28,7 +28,8 @@ internal static partial class GraphicsReport
vec3 ld=normalize(uFixtureLight); vec3 ld=normalize(uFixtureLight);
vec3 world=transpose(mat3(uRingWorldToBody))*(p-uRingCameraBody)*uRingRadius; vec3 world=transpose(mat3(uRingWorldToBody))*(p-uRingCameraBody)*uRingRadius;
vec3 light=transpose(mat3(uRingWorldToBody))*ld; vec3 light=transpose(mat3(uRingWorldToBody))*ld;
fragColor=vec4(ringHullVisibility(p,ld),ringCloudTransmission(p,ld),ringEnvironmentShadowWorld(world,light),1); vec3 returned=uRingCameraBody+mat3(uRingWorldToBody)*world/uRingRadius;
fragColor=vec4(ringHullVisibility(p,ld),ringCloudTransmission(p,ld),ringEnvironmentShadowWorld(world,light),length(returned-p)*uRingRadius);
} }
"""); """);
float[] Draw(PlanetData body, Vector3 origin, Vector3 light, float extent = 0, float time = 0) float[] Draw(PlanetData body, Vector3 origin, Vector3 light, float extent = 0, float time = 0)
@@ -42,7 +43,8 @@ internal static partial class GraphicsReport
throw new InvalidOperationException("Non-finite or invalid Ring World shadow shader output."); throw new InvalidOperationException("Non-finite or invalid Ring World shadow shader output.");
return pixels; return pixels;
} }
var clear = planet with { Clouds = planet.Clouds! with { Coverage = 0 }, CloudDensityMap = null }; var clear = planet with { Clouds = planet.Clouds! with { Coverage = 0 }, CloudDensityMap = null,
RingWorld=planet.RingWorld! with {CloudDetail=planet.RingWorld.CloudDetail with {NearEnabled=false}} };
var cases = new (Vector3 Origin, Vector3 Light, float Expected, string Name)[] var cases = new (Vector3 Origin, Vector3 Light, float Expected, string Name)[]
{ {
(new(.9996f,0,0),new(-1,.8f,0),1,"sun above positive rim"), (new(.9996f,0,0),new(-1,.8f,0),1,"sun above positive rim"),
@@ -71,19 +73,23 @@ internal static partial class GraphicsReport
if (disabled[1] < .999) throw new InvalidOperationException("Ring cloud shadow strength zero still casts shadows."); if (disabled[1] < .999) throw new InvalidOperationException("Ring cloud shadow strength zero still casts shadows.");
var weather = Draw(planet, new(.9996f,0,0), new(-1,.8f,0), .12f); var weather = Draw(planet, new(.9996f,0,0), new(-1,.8f,0), .12f);
var later = Draw(planet, new(.9996f,0,0), new(-1,.8f,0), .12f, 900); var later = Draw(planet, new(.9996f,0,0), new(-1,.8f,0), .12f, 900);
double windDelta = 0; var low = 1f; var high = 0f; var transformError = 0f; double windDelta = 0,meanTransformError=0; var low = 1f; var high = 0f; var transformError = 0f;var positionError=0f;
for (var i = 1; i < weather.Length; i += 4) for (var i = 1; i < weather.Length; i += 4)
{ {
windDelta += System.Math.Abs(weather[i] - later[i]) / (size * size); windDelta += System.Math.Abs(weather[i] - later[i]) / (size * size);
low = System.Math.Min(low, weather[i]); high = System.Math.Max(high, weather[i]); low = System.Math.Min(low, weather[i]); high = System.Math.Max(high, weather[i]);
transformError = System.Math.Max(transformError, System.Math.Abs(weather[i - 1] * weather[i] - weather[i + 1])); transformError = System.Math.Max(transformError, System.Math.Abs(weather[i - 1] * weather[i] - weather[i + 1]));
meanTransformError+=System.Math.Abs(weather[i-1]*weather[i]-weather[i+1])/(size*size);
positionError=System.Math.Max(positionError,weather[i+2]);
} }
// Ray fixtures above are binary/exact. Allow one percent for filtered density: // Exact hull fixtures above retain their binary tolerance. For filtered volume
// camera-relative float roundtrips can cross a hardware interpolation step. // densities, check mean error as well as outliers and the physical roundtrip.
if (transformError > .01) throw new InvalidOperationException($"Body/world cloud shadow error: {transformError}."); // Centimetre float differences can cross a 3D sampler interpolation step.
if(transformError>.025||meanTransformError>.001||positionError>.1)
throw new InvalidOperationException($"Body/world cloud shadow error: max {transformError}, mean {meanTransformError}, position {positionError}m.");
if (high - low < .1 || windDelta < .01) throw new InvalidOperationException("Ring cloud shadows do not vary spatially or follow wind."); if (high - low < .1 || windDelta < .01) throw new InvalidOperationException("Ring cloud shadows do not vary spatially or follow wind.");
WritePng(Path.Combine(directory, "shadow-field.png"), size, size, weather, false); WritePng(Path.Combine(directory, "shadow-field.png"), size, size, weather, false);
Console.WriteLine($"Ring shadow GPU fixtures PASS: {cases.Length} hull/wall rays, overcast transmission {overcast[1]:F3}, cloud range {low:F3}–{high:F3}, wind delta {windDelta:F3}, transform error {transformError:F4}."); Console.WriteLine($"Ring shadow GPU fixtures PASS: {cases.Length} hull/wall rays, overcast transmission {overcast[1]:F3}, cloud range {low:F3}–{high:F3}, wind delta {windDelta:F3}, transform error max/mean {transformError:F4}/{meanTransformError:F6}, position {positionError:F3}m.");
} }
private static void RunRingWorldShadowViews(GL gl, UniverseRenderer renderer, FlyCamera camera, UniverseNeighborhood world, private static void RunRingWorldShadowViews(GL gl, UniverseRenderer renderer, FlyCamera camera, UniverseNeighborhood world,

View File

@@ -0,0 +1,76 @@
using Silk.NET.OpenGL;
using Universe.Core.Generation;
using Universe.Core.Math;
namespace Universe.App.Graphics;
/// <summary>Bounded asynchronous grass streaming; only roots are uploaded, blades are built on the GPU.</summary>
internal sealed unsafe class RingMeadowRenderer(GL gl) : IDisposable
{
private sealed record Tile(uint Vao,uint Buffer,RingMeadowTile Data);
private readonly Dictionary<(int X,int Y),Tile> _tiles=[];
private Task<RingMeadowTile>? _pending;
private (int X,int Y) _building;
private CancellationTokenSource _cancellation=new();
private PlanetData? _planet;
public int DrawnTufts {get;private set;}
public void Draw(PlanetData planet,DVector3 camera,VegetationView view,Action<DVector3> bindOrigin)
{
DrawnTufts=0;
if(planet.RingWorld?.Meadow is not {Enabled:true} settings||planet.Vegetation is not {Enabled:true})return;
var altitude=planet.RadiusMeters-new DVector3(camera.X,0,camera.Z).Length-RingWorldGeometry.Elevation(planet,camera);
if(altitude>100||altitude< -5)return;
if(_planet?.Id!=planet.Id||_planet.Seed!=planet.Seed||_planet.RingWorld!=planet.RingWorld||_planet.SurfaceTexture!=planet.SurfaceTexture
||_planet.Terrain!=planet.Terrain||_planet.TerrainAmplitudeMeters!=planet.TerrainAmplitudeMeters||_planet.Ocean!=planet.Ocean||_planet.OceanMask!=planet.OceanMask)
{Clear();_planet=planet;}
if(_pending is {IsCompleted:true})
{
var data=_pending.GetAwaiter().GetResult();_pending=null;
var vao=gl.GenVertexArray();var buffer=gl.GenBuffer();
gl.BindVertexArray(vao);gl.BindBuffer(GLEnum.ArrayBuffer,buffer);
fixed(float* p=data.Instances)gl.BufferData(GLEnum.ArrayBuffer,(nuint)(data.Instances.Length*sizeof(float)),p,GLEnum.StaticDraw);
for(uint a=0;a<3;a++)
{gl.EnableVertexAttribArray(a);gl.VertexAttribPointer(a,3,VertexAttribPointerType.Float,false,36,(void*)(a*12));gl.VertexAttribDivisor(a,1);}
_tiles.Add(_building,new(vao,buffer,data));
}
var columns=RingMeadowTile.Columns(planet);
var angle=System.Math.Atan2(camera.Z,camera.X)/System.Math.Tau;if(angle<0)angle+=1;
var cx=(int)(angle*columns);var cy=(int)System.Math.Floor(camera.Y/RingMeadowTile.Size);
var range=(int)System.Math.Ceiling(settings.DistanceMeters/RingMeadowTile.Size)+1;
var needed=new List<((int X,int Y) Key,double Distance)>();
for(var y=cy-range;y<=cy+range;y++)for(var dx=-range;dx<=range;dx++)
{
var key=(((cx+dx)%columns+columns)%columns,y);
var distance=System.Math.Sqrt((dx-.5)*(dx-.5)+(y-cy-.5)*(y-cy-.5))*RingMeadowTile.Size;
if(distance<settings.DistanceMeters+RingMeadowTile.Size)needed.Add((key,distance));
}
if(_pending is null)
{
foreach(var candidate in needed.OrderBy(k=>k.Distance))if(!_tiles.ContainsKey(candidate.Key))
{
_building=candidate.Key;var key=_building;var token=_cancellation.Token;
_pending=Task.Run(()=>RingMeadowTile.Build(planet,key.X,key.Y,settings,token),token);break;
}
}
var required=needed.Select(k=>k.Key).ToHashSet();
foreach(var key in _tiles.Keys.Where(k=>!required.Contains(k)).ToArray())
{var t=_tiles[key];gl.DeleteVertexArray(t.Vao);gl.DeleteBuffer(t.Buffer);_tiles.Remove(key);}
gl.Enable(GLEnum.DepthTest);gl.DepthMask(true);gl.Disable(GLEnum.CullFace);gl.Disable(GLEnum.Blend);
foreach(var tile in _tiles.Values)
{
var offset=tile.Data.Origin-camera;
if(offset.Length>settings.DistanceMeters+RingMeadowTile.Size||!view.Intersects(offset,10))continue;
bindOrigin(tile.Data.Origin);gl.BindVertexArray(tile.Vao);
var count=tile.Data.Instances.Length/9;
gl.DrawArraysInstanced(GLEnum.Triangles,0,135,(uint)count);DrawnTufts+=count;
}
gl.BindVertexArray(0);
}
private void Clear()
{
_cancellation.Cancel();_cancellation.Dispose();_cancellation=new();_pending=null;
foreach(var t in _tiles.Values){gl.DeleteVertexArray(t.Vao);gl.DeleteBuffer(t.Buffer);}_tiles.Clear();
}
public void Dispose(){Clear();_cancellation.Dispose();}
}

View File

@@ -0,0 +1,65 @@
using System.Numerics;
using Silk.NET.OpenGL;
using Universe.App.Runtime;
namespace Universe.App.Graphics;
internal sealed class RingSurfaceEffects : IDisposable
{
private readonly GL _gl;
private readonly GpuMesh _quad;
private ShaderProgram _temporal,_contact,_composite;
private RenderTarget? _historyA,_historyB,_contactRaw,_contactColor;
private bool _hasHistory;
public uint ContactTexture=>_contactRaw?.Color??0;
public RingSurfaceEffects(GL gl)
{
_gl=gl;_quad=new(gl,3,[-1,-1,0,3,-1,0,-1,3,0]);
_temporal=Program("ring-temporal.frag");_contact=Program("ring-contact.frag");_composite=Program("ring-contact-composite.frag");
}
private ShaderProgram Program(string name)=>ShaderProgram.Create(_gl,ShaderSources.Load("fullscreen.vert"),ShaderSources.Load(name));
public void Reload()
{
var programs=new List<ShaderProgram>();
try{foreach(var name in new[]{"ring-temporal.frag","ring-contact.frag","ring-contact-composite.frag"})programs.Add(Program(name));}
catch{foreach(var p in programs)p.Dispose();throw;}
_temporal.Dispose();_contact.Dispose();_composite.Dispose();
_temporal=programs[0];_contact=programs[1];_composite=programs[2];_hasHistory=false;
}
public void Reset()=>_hasHistory=false;
public void Resize()
{
_historyA?.Dispose();_historyB?.Dispose();_contactRaw?.Dispose();_contactColor?.Dispose();
_historyA=null;_historyB=null;_contactRaw=null;_contactColor=null;_hasHistory=false;
}
private void Bind(int unit,uint texture)
{_gl.ActiveTexture((GLEnum)((int)GLEnum.Texture0+unit));_gl.BindTexture(GLEnum.Texture2D,texture);}
public RenderTarget Contact(RenderTarget source,uint depth,float far,float strength,Action<ShaderProgram> camera)
{
_contactRaw??=new(_gl,(source.Width+1)/2,(source.Height+1)/2);
_contactColor??=new(_gl,source.Width,source.Height);
_contactRaw.Bind();_gl.Disable(GLEnum.DepthTest);_gl.DepthMask(false);_gl.Disable(GLEnum.Blend);
_contact.Use();camera(_contact);Bind(0,depth);_contact.Set("uDepth",0);_contact.Set("uFar",far);
_contact.Set("uViewport",new Vector3(source.Width,source.Height,0));_quad.Draw();
_contactColor.Bind();_composite.Use();Bind(0,source.Color);Bind(1,_contactRaw.Color);Bind(2,depth);
_composite.Set("uScene",0);_composite.Set("uContact",1);_composite.Set("uDepth",2);_composite.Set("uFar",far);
_composite.Set("uStrength",strength);_composite.Set("uContactTexel",new Vector3(1f/_contactRaw.Width,1f/_contactRaw.Height,0));
_quad.Draw();return _contactColor;
}
public RenderTarget Resolve(RenderTarget source,uint depth,float far,RingTemporalHistory history,Action<ShaderProgram> camera)
{
_historyA??=new(_gl,source.Width,source.Height,depth:true);
_historyB??=new(_gl,source.Width,source.Height,depth:true);
_historyB.Bind();_gl.Enable(GLEnum.DepthTest);_gl.DepthFunc(GLEnum.Always);_gl.DepthMask(true);_gl.Disable(GLEnum.Blend);
_temporal.Use();camera(_temporal);Bind(0,source.Color);Bind(1,depth);Bind(2,_historyA.Color);Bind(3,_historyA.Depth);
_temporal.Set("uScene",0);_temporal.Set("uDepth",1);_temporal.Set("uHistory",2);_temporal.Set("uHistoryDepth",3);
_temporal.Set("uHistoryValid",_hasHistory&&history.Valid?1:0);_temporal.Set("uFar",far);
_temporal.Set("uTexel",new Vector3(1f/source.Width,1f/source.Height,0));
_temporal.SetMatrix("uCurrentToPrevious",history.CurrentToPrevious);
_temporal.SetMatrix("uPreviousViewProjection",history.PreviousViewProjection);
_temporal.Set("uPreviousCameraOffset",history.PreviousCameraOffset);_quad.Draw();
_gl.DepthFunc(GLEnum.Lequal);_gl.Disable(GLEnum.DepthTest);_gl.DepthMask(false);
(_historyA,_historyB)=(_historyB,_historyA);_hasHistory=true;return _historyA;
}
public void Dispose(){Resize();_quad.Dispose();_temporal.Dispose();_contact.Dispose();_composite.Dispose();}
}

View File

@@ -3,7 +3,8 @@ uniform vec3 uRight;
uniform vec3 uUp; uniform vec3 uUp;
uniform float uAspect; uniform float uAspect;
uniform float uTanHalfFov; uniform float uTanHalfFov;
uniform vec3 uRayJitter;
vec3 cameraRay(vec2 uv) { vec3 cameraRay(vec2 uv) {
vec2 xy=(uv*2.0-1.0)*uTanHalfFov; vec2 xy=((uv-uRayJitter.xy)*2.0-1.0)*uTanHalfFov;
return normalize(uForward+uRight*xy.x*uAspect+uUp*xy.y); return normalize(uForward+uRight*xy.x*uAspect+uUp*xy.y);
} }

View File

@@ -4,6 +4,7 @@ uniform sampler2D uScene;
uniform sampler2D uBloom; uniform sampler2D uBloom;
uniform vec3 uTexel; uniform vec3 uTexel;
uniform float uExposure; uniform float uExposure;
uniform int uSpatialAA;
out vec4 fragColor; out vec4 fragColor;
vec3 tone(vec2 uv) { vec3 tone(vec2 uv) {
vec3 c=(texture(uScene,uv).rgb+texture(uBloom,uv).rgb*0.16)*uExposure; vec3 c=(texture(uScene,uv).rgb+texture(uBloom,uv).rgb*0.16)*uExposure;
@@ -16,7 +17,7 @@ void main() {
vec3 luma=vec3(0.299,0.587,0.114); vec3 luma=vec3(0.299,0.587,0.114);
float l=dot(c,luma), lo=min(l,min(min(dot(n,luma),dot(s,luma)),min(dot(e,luma),dot(w,luma)))); float l=dot(c,luma), lo=min(l,min(min(dot(n,luma),dot(s,luma)),min(dot(e,luma),dot(w,luma))));
float hi=max(l,max(max(dot(n,luma),dot(s,luma)),max(dot(e,luma),dot(w,luma)))); float hi=max(l,max(max(dot(n,luma),dot(s,luma)),max(dot(e,luma),dot(w,luma))));
if(hi-lo>max(0.045,hi*0.18)) { if(uSpatialAA!=0&&hi-lo>max(0.045,hi*0.18)) {
vec2 dir=vec2(dot(s-n,luma),dot(e-w,luma)); vec2 dir=vec2(dot(s-n,luma),dot(e-w,luma));
dir=normalize(dir+vec2(0.000001))*uTexel.xy; dir=normalize(dir+vec2(0.000001))*uTexel.xy;
c=mix(c,(tone(vUv+dir*0.45)+tone(vUv-dir*0.45))*0.5,0.62); c=mix(c,(tone(vUv+dir*0.45)+tone(vUv-dir*0.45))*0.5,0.62);

View File

@@ -0,0 +1,22 @@
#version 330 core
in vec2 vUv;
uniform sampler2D uScene;
uniform sampler2D uContact;
uniform sampler2D uDepth;
uniform vec3 uContactTexel;
uniform float uFar;
uniform float uStrength;
out vec4 fragColor;
void main(){
float distance=exp2(texture(uDepth,vUv).r*log2(uFar+1.0))-1.0;
float total=0.0,visibility=0.0;
// Bilateral upsampling prevents the grass silhouette from darkening the sky behind it.
for(int y=0;y<2;y++)for(int x=0;x<2;x++){
vec2 uv=vUv+(vec2(x,y)-.5)*uContactTexel.xy;
vec2 sample=texture(uContact,uv).rg;
float weight=exp(-abs(sample.y-distance)/max(.08,distance*.015));
visibility+=sample.x*weight;total+=weight;
}
float contact=total>.0001?visibility/total:1.0;
fragColor=vec4(texture(uScene,vUv).rgb*mix(1.0,contact,uStrength),1);
}

View File

@@ -0,0 +1,31 @@
#version 330 core
#include "camera-ray.glsl"
in vec2 vUv;
uniform sampler2D uDepth;
uniform vec3 uViewport;
uniform float uFar;
out vec4 fragColor;
vec3 position(vec2 uv){
float d=texture(uDepth,uv).r;
vec3 ray=cameraRay(uv);
return ray*((exp2(d*log2(uFar+1.0))-1.0)/max(.001,dot(ray,uForward)));
}
void main(){
vec3 p=position(vUv);float distance=length(p);
vec3 dx=dFdx(p),dy=dFdy(p),n=normalize(cross(dx,dy));
if(dot(n,-p)<0.0)n=-n;
fragColor=vec4(1,min(60000.0,dot(p,uForward)),0,1);
if(distance>120.0||distance<.05)return;
float radius=clamp(.55*uViewport.y/(2.0*uTanHalfFov*distance),3.0,32.0);
float occlusion=0.0;
for(int i=0;i<8;i++){
float angle=float(i)*2.39996323;
float fraction=(float(i)+1.0)/8.0;
vec2 uv=vUv+vec2(cos(angle),sin(angle))*radius*sqrt(fraction)/uViewport.xy;
if(any(lessThan(uv,vec2(0)))||any(greaterThan(uv,vec2(1))))continue;
vec3 delta=position(uv)-p;float squared=dot(delta,delta);
float horizon=max(0.0,dot(n,delta)-.018);
occlusion+=horizon/sqrt(max(.0001,squared))*(1.0-smoothstep(.05,.65,sqrt(squared)));
}
fragColor.r=clamp(1.0-occlusion*.65,0.0,1.0);
}

View File

@@ -0,0 +1,31 @@
#version 330 core
#include "ring-world-common.glsl"
in vec3 vBody;
in vec3 vWorld;
in vec3 vNormal;
in vec4 vGrass;
in float vFragDepth;
uniform float uFar;
out vec4 fragColor;
void main(){
vec3 fresh=mix(vec3(.045,.09,.018),vec3(.14,.19,.035),vGrass.y);
vec3 dry=mix(vec3(.12,.13,.035),vec3(.27,.23,.09),vGrass.y);
float vein=.86+.14*exp(-vGrass.w*vGrass.w*14.0);
vec3 color=mix(fresh,dry,vGrass.z*.7)*mix(.4,1.1,vGrass.x)*vein;
color=mix(color,color*vec3(1.15,1.02,.72),smoothstep(.75,1.0,vGrass.x)*vGrass.y*.3);
vec3 n=normalize(vNormal);
vec3 up=-normalize(vec3(vBody.x,0,vBody.z));float daylight=0.0;
for(int i=0;i<2;i++){if(i>=uLightCount)break;daylight+=uLightFlux[i]*smoothstep(-.08,.25,dot(up,uLightDirections[i]));}
vec3 light=ringSkyIrradiance(n,up,min(daylight,2.0));
vec3 highlights=vec3(0),view=normalize(-mat3(uWorldToBody)*vWorld);
for(int i=0;i<2;i++){
if(i>=uLightCount)break;
float diffuse=max(0.0,dot(n,uLightDirections[i]))+max(0.0,dot(-n,uLightDirections[i]))*.3;
vec3 sun=uLightColors[i]*uLightFlux[i]*ringEnvironmentShadow(vBody,uLightDirections[i])*vegetationShadow(vWorld,i);
light+=sun*diffuse;
float sheen=pow(abs(dot(n,normalize(view+uLightDirections[i]))),18.0);
highlights+=sun*sheen*.015*smoothstep(.1,.5,vGrass.x);
}
fragColor=vec4(color*light+highlights,1);
gl_FragDepth=log2(max(.0001,vFragDepth))/log2(uFar+1.0);
}

View File

@@ -0,0 +1,42 @@
#version 330 core
layout(location=0) in vec3 aRoot;
layout(location=1) in vec3 aUp;
layout(location=2) in vec3 aVariation;
uniform mat4 uViewProjection;
uniform mat4 uBodyToWorld;
uniform vec3 uChunkPosition;
uniform vec3 uBodyOrigin;
uniform float uRadius;
uniform float uTime;
uniform float uFar;
uniform float uMeadowDistance;
out vec3 vBody;
out vec3 vWorld;
out vec3 vNormal;
out vec4 vGrass;
out float vFragDepth;
void main(){
int blade=gl_VertexID/15,vertex=gl_VertexID%15;
// Three curved sections and a tapered tip, with independent variation per blade.
float levels[15]=float[15](0,0,.34,0,.34,.34,.34,.34,.7,.34,.7,.7,.7,.7,1);
float sides[15]=float[15](-1,1,-1,1,1,-1,-1,1,-1,1,1,-1,-1,1,0);
float h=levels[vertex],side=sides[vertex];
vec3 up=normalize(aUp),right=normalize(cross(abs(up.y)<.9?vec3(0,1,0):vec3(1,0,0),up)),forward=cross(right,up);
float angle=aVariation.x*6.2831853+float(blade)*2.399963;
float variation=fract(sin(float(blade)*12.9898+aVariation.x*78.233)*43758.5453);
vec3 width=right*cos(angle)+forward*sin(angle),bend=cross(up,width);
vec3 root=uChunkPosition+mat3(uBodyToWorld)*aRoot;
float fade=1.0-smoothstep(uMeadowDistance*.65,uMeadowDistance,length(root));
float height=aVariation.y*fade*(.55+variation*.8);
float wind=sin(uTime*1.5+aVariation.x*18.0+dot(aRoot,vec3(.13,.07,.09)))*.18;
float lean=.3+variation*.65+wind;
float bladeWidth=mix(.006,.012,variation)*sqrt(max(0.0,1.0-h));
vec3 local=aRoot+width*(variation-.5)*.28+bend*(float(blade)-4.0)*.025
+up*(h*height-h*h*height*lean*.22)+width*side*bladeWidth*fade+bend*h*h*height*lean;
vWorld=uChunkPosition+mat3(uBodyToWorld)*local;
vBody=uBodyOrigin+local/uRadius;
vNormal=normalize(up*(.2+h*lean)+bend*.7+width*side*.4);
vGrass=vec4(h,mix(aVariation.x,variation,.3),aVariation.z,side);
vec4 clip=uViewProjection*vec4(vWorld,1);vFragDepth=clip.w+1.0;gl_Position=clip;
gl_Position.z=(2.0*log2(max(.0001,vFragDepth))/log2(uFar+1.0)-1.0)*clip.w;
}

View File

@@ -0,0 +1,47 @@
#version 330 core
#include "camera-ray.glsl"
in vec2 vUv;
uniform sampler2D uScene;
uniform sampler2D uDepth;
uniform sampler2D uHistory;
uniform sampler2D uHistoryDepth;
uniform int uHistoryValid;
uniform float uFar;
uniform vec3 uTexel;
uniform mat4 uCurrentToPrevious;
uniform mat4 uPreviousViewProjection;
uniform vec3 uPreviousCameraOffset;
out vec4 fragColor;
vec3 toYCoCg(vec3 c){return vec3(dot(c,vec3(.25,.5,.25)),c.r*.5-c.b*.5,-c.r*.25+c.g*.5-c.b*.25);}
vec3 fromYCoCg(vec3 c){return vec3(c.x+c.y-c.z,c.x+c.z,c.x-c.y-c.z);}
void main(){
float depth=texture(uDepth,vUv).r;gl_FragDepth=depth;
vec3 current=texture(uScene,vUv).rgb;
fragColor=vec4(current,1);
if(uHistoryValid==0||depth>=.999999)return;
// History lives on the stable output grid. Compensating projection jitter here
// lets successive subpixel coverage samples accumulate at the same output pixel.
vec3 ray=cameraRay(vUv+uRayJitter.xy);
float distance=exp2(depth*log2(uFar+1.0))-1.0;
vec3 point=ray*(distance/max(.0001,dot(ray,uForward)));
vec3 previous=mat3(uCurrentToPrevious)*point+uPreviousCameraOffset;
vec4 clip=uPreviousViewProjection*vec4(previous,1);
vec2 uv=clip.xy/max(.001,clip.w)*.5+.5;
if(clip.w<=0.0||any(lessThan(uv,uTexel.xy))||any(greaterThan(uv,vec2(1)-uTexel.xy)))return;
float oldDistance=exp2(texture(uHistoryDepth,uv).r*log2(uFar+1.0))-1.0;
// Reject disoccluded leaves/grass instead of borrowing the background behind them.
if(abs(oldDistance-clip.w)>max(.06,clip.w*.004))return;
vec3 minimum=vec3(1e20),maximum=vec3(-1e20),mean=vec3(0),moment=vec3(0);
for(int y=-1;y<=1;y++)for(int x=-1;x<=1;x++){
vec3 c=toYCoCg(texture(uScene,vUv+vec2(x,y)*uTexel.xy).rgb);
minimum=min(minimum,c);maximum=max(maximum,c);mean+=c;moment+=c*c;
}
mean/=9.0;vec3 sigma=sqrt(max(vec3(0),moment/9.0-mean*mean));
minimum=max(minimum,mean-sigma*1.5);maximum=min(maximum,mean+sigma*1.5);
vec3 old=clamp(toYCoCg(texture(uHistory,uv).rgb),minimum,maximum);
float motion=length((uv-vUv)/uTexel.xy);
float feedback=mix(.88,.65,smoothstep(0.0,8.0,motion));
// Distant volumetric clouds have no surface motion vectors; keep their history short.
feedback*=1.0-smoothstep(250.0,1500.0,distance)*.7;
fragColor=vec4(max(vec3(0),fromYCoCg(mix(toYCoCg(current),old,feedback))),1);
}

View File

@@ -5,8 +5,9 @@ in vec2 vUv;
uniform sampler2D uScene; uniform sampler2D uScene;
uniform sampler2D uDepth; uniform sampler2D uDepth;
uniform float uFar; uniform float uFar;
uniform vec3 uRingCloudLighting;
out vec4 fragColor; out vec4 fragColor;
vec3 integrate(vec3 source,vec3 o,vec3 d,vec2 interval,bool clouds) { vec3 integrate(vec3 source,vec3 o,vec3 d,vec2 interval) {
float a=max(0.0,interval.x),b=interval.y; float a=max(0.0,interval.x),b=interval.y;
if(b<=a)return source; if(b<=a)return source;
vec3 scatter=vec3(0);vec3 transmission=vec3(1); vec3 scatter=vec3(0);vec3 transmission=vec3(1);
@@ -24,17 +25,9 @@ vec3 integrate(vec3 source,vec3 o,vec3 d,vec2 interval,bool clouds) {
float mu=dot(d,uLightDirections[j]); float mu=dot(d,uLightDirections[j]);
light+=uLightColors[j]*uLightFlux[j]*sun*(.7+.3*mu*mu)*ringHullVisibility(p,uLightDirections[j]); light+=uLightColors[j]*uLightFlux[j]*sun*(.7+.3*mu*mu)*ringHullVisibility(p,uLightDirections[j]);
} }
vec3 extinction;
vec3 tint;
if(clouds) {
float density=ringCloudDensity(p);
extinction=vec3(density*uRingCloudShape.z);
tint=vec3(.92,.96,1.0)*light;
} else {
float density=exp(-height/max(100.0,uAirHeight*uRadius*.055))*uAirDensity; float density=exp(-height/max(100.0,uAirHeight*uRadius*.055))*uAirDensity;
extinction=(vec3(.45,.55,.7)+uAirColor)*density*.000012; vec3 extinction=(vec3(.45,.55,.7)+uAirColor)*density*.000012;
tint=(vec3(.18,.28,.45)+uAirColor*.4)*light; vec3 tint=(vec3(.18,.28,.45)+uAirColor*.4)*light;
}
vec3 stepTransmission=exp(-extinction*ds); vec3 stepTransmission=exp(-extinction*ds);
scatter+=transmission*(1.0-stepTransmission)*tint; scatter+=transmission*(1.0-stepTransmission)*tint;
transmission*=stepTransmission; transmission*=stepTransmission;
@@ -48,10 +41,6 @@ void main() {
float stop=depth>=.999999?1e8:(exp2(depth*log2(uFar+1.0))-1.0)/(uRadius*max(.0001,dot(worldRay,uForward))); float stop=depth>=.999999?1e8:(exp2(depth*log2(uFar+1.0))-1.0)/(uRadius*max(.0001,dot(worldRay,uForward)));
vec3 color=texture(uScene,vUv).rgb; vec3 color=texture(uScene,vUv).rgb;
vec4 air=shellSegments(o,d,1.0-uAirHeight,1.0,uRingSize.x,stop); vec4 air=shellSegments(o,d,1.0-uAirHeight,1.0,uRingSize.x,stop);
color=integrate(color,o,d,air.zw,false);color=integrate(color,o,d,air.xy,false); color=integrate(color,o,d,air.zw);color=integrate(color,o,d,air.xy);
if(uRingCloudEnabled!=0) {
vec4 cloud=shellSegments(o,d,1.0-uRingCloudLayer.x-uRingCloudLayer.y,1.0-uRingCloudLayer.x,uRingSize.x,stop);
color=integrate(color,o,d,cloud.zw,true);color=integrate(color,o,d,cloud.xy,true);
}
fragColor=vec4(color,1); fragColor=vec4(color,1);
} }

View File

@@ -0,0 +1,124 @@
#version 330 core
#include "ring-world-common.glsl"
#include "camera-ray.glsl"
in vec2 vUv;
uniform sampler2D uDepth;
uniform float uFar;
uniform int uRingCloudViewSteps;
uniform int uRingCloudLightSteps;
uniform float uRingCloudPixelAngle;
uniform vec3 uRingCloudLighting;
uniform vec3 uRingCloudPhase;
out vec4 fragColor;
float opaqueDistance(){
ivec2 size=textureSize(uDepth,0);vec2 pixel=vUv*vec2(size)-.5,f=fract(pixel);ivec2 base=ivec2(floor(pixel));
float inverse=0.0;
for(int y=0;y<2;y++)for(int x=0;x<2;x++){
float weight=(x==0?1.0-f.x:f.x)*(y==0?1.0-f.y:f.y);
float depth=texelFetch(uDepth,clamp(base+ivec2(x,y),ivec2(0),size-1),0).r;
inverse+=weight/max(.001,exp2(depth*log2(uFar+1.0))-1.0);
}
return 1.0/max(1e-25,inverse);
}
float phaseHG(float mu,float g){return (1.0-g*g)/pow(max(.025,1.0+g*g-2.0*g*mu),1.5);}
float densityAt(vec3 p,float footprint,bool nearLayer,bool fine){
return nearLayer?ringNearCloudDensity(p,footprint,fine):ringMainCloudDensity(p,footprint,fine);
}
float sunlight(vec3 p,vec3 light,bool nearLayer,float footprint,int count){
vec3 layer=nearLayer?uRingNearLayer:uRingCloudLayer;
vec4 path=shellSegments(p,light,1.0-layer.x-layer.y,1.0-layer.x,uRingSize.x,layer.y*8.0);
float end=max(0.0,path.y);if(end<=0.0)return 1.0;
float optical=0.0;
for(int i=0;i<16;i++){
if(i>=count)break;
float a=float(i)/float(count),b=float(i+1)/float(count);
float t=end*pow((a+b)*.5,2.0);
optical+=densityAt(p+light*t,max(footprint,150.0),nearLayer,false)*end*(b*b-a*a)*uRingRadius;
}
return exp(-min(25.0,optical*uRingCloudShape.z));
}
vec3 lighting(vec3 p,vec3 ray,float density,float footprint,bool nearLayer,float travel){
vec3 up=-normalize(vec3(p.x,0,p.z));
vec3 layer=nearLayer?uRingNearLayer:uRingCloudLayer;
float height=clamp((1.0-length(p.xz)-layer.x)/layer.y,0.0,1.0);
vec3 light=vec3(.004,.006,.012);
int steps=travel<uRingNearDistance?uRingCloudLightSteps:min(2,uRingCloudLightSteps);
for(int i=0;i<2;i++){
if(i>=uLightCount)break;
vec3 ld=uLightDirections[i];float day=smoothstep(-.08,.16,dot(up,ld));
float transmission=sunlight(p,ld,nearLayer,footprint,steps);
float mu=dot(ray,ld);
float phase=.8*phaseHG(mu,uRingCloudPhase.x)+.2*phaseHG(mu,-.2);
float single=transmission*(.14*phase+.17),multiple=pow(transmission,.35)*.32;
float edge=pow(max(0.0,mu),16.0)*sqrt(transmission)*uRingCloudPhase.y*.35;
light+=uLightColors[i]*uLightFlux[i]*uRingCloudLighting.y*(single+multiple+edge)*ringHullVisibility(p,ld);
vec3 sky=mix(vec3(.18,.23,.34),vec3(.55,.65,.8),height);
light+=sky*uRingCloudLighting.x*day*sqrt(uLightFlux[i])*(.7+.3*exp(-density*2.0));
}
return light*uRingCloudLighting.z;
}
void march(vec3 range,vec3 origin,vec3 ray,inout vec3 radiance,inout float transmission,inout float weightedDistance){
float start=max(0.0,range.x),end=range.y;
bool nearLayer=range.z>.5;
if(nearLayer)end=min(end,uRingNearDistance/uRingRadius);
if(end<=start||transmission<.01)return;
float length=(end-start)*uRingRadius;
bool close=start*uRingRadius<uRingNearDistance;
float maximum=close?float(uRingCloudViewSteps):min(24.0,float(uRingCloudViewSteps));
float count=clamp(ceil(length/(nearLayer?16.0:90.0)),16.0,maximum);
bool inside=start*uRingRadius<1000.0;
float jitter=fract(52.9829189*fract(dot(floor(gl_FragCoord.xy),vec2(.06711056,.00583715))));
for(int i=0;i<192;i++){
if(float(i)>=count||transmission<.01)break;
float a=float(i)/count,b=(float(i)+1.0)/count;
// While flying inside a layer, spend samples close to the camera instead
// of stepping over nearby wisps on a long, almost horizontal ray.
if(inside){a*=a;b*=b;}
float ds=(end-start)*(b-a);
float t=start+(end-start)*mix(a,b,.15+jitter*.7),travel=t*uRingRadius;
vec3 p=origin+ray*t;
float footprint=max(travel*uRingCloudPixelAngle,ds*uRingRadius*.45);
float density=densityAt(p,footprint,nearLayer,true);
if(nearLayer)density*=1.0-smoothstep(uRingNearDistance*.65,uRingNearDistance,travel);
if(density<.001)continue;
float alpha=1.0-exp(-density*ds*uRingRadius*uRingCloudShape.z);
float weight=alpha*transmission;
radiance+=lighting(p,ray,density,footprint,nearLayer,travel)*weight;
weightedDistance+=travel*weight;transmission*=1.0-alpha;
}
}
float airColumn(vec3 o,vec3 d,vec2 range){
float column=0.0;
if(range.y<=range.x)return 0.0;
for(int i=0;i<8;i++){
float a=float(i)/8.0,b=float(i+1)/8.0;
float t=range.x+(range.y-range.x)*pow((a+b)*.5,2.0);
float altitude=max(0.0,(1.0-length((o+d*t).xz))*uRingRadius);
column+=exp(-altitude/max(100.0,uAirHeight*uRingRadius*.055))*(range.y-range.x)*(b*b-a*a)*uRingRadius;
}
return column*uAirDensity;
}
void main(){
fragColor=vec4(0,0,0,1);
vec3 worldRay=cameraRay(vUv),ray=mat3(uWorldToBody)*worldRay,o=uCameraBody;
float stop=opaqueDistance()/(uRingRadius*max(.001,dot(worldRay,uForward)));
vec4 mainRanges=shellSegments(o,ray,1.0-uRingCloudLayer.x-uRingCloudLayer.y,1.0-uRingCloudLayer.x,uRingSize.x,stop);
vec4 detail=vec4(1,-1,1,-1);
if(uRingNearEnabled!=0)detail=shellSegments(o,ray,1.0-uRingNearLayer.x-uRingNearLayer.y,1.0-uRingNearLayer.x,uRingSize.x,min(stop,uRingNearDistance/uRingRadius));
vec3 ranges[4]=vec3[4](vec3(mainRanges.xy,0),vec3(mainRanges.zw,0),vec3(detail.xy,1),vec3(detail.zw,1));
// Correct front-to-back order also works while flying between or inside either layer.
for(int i=0;i<4;i++)for(int j=i+1;j<4;j++)if(ranges[j].x<ranges[i].x){vec3 swap=ranges[i];ranges[i]=ranges[j];ranges[j]=swap;}
vec3 radiance=vec3(0);float transmission=1.0,weightedDistance=0.0;
for(int i=0;i<4;i++)march(ranges[i],o,ray,radiance,transmission,weightedDistance);
float alpha=1.0-transmission;
if(alpha>.0001){
float distance=weightedDistance/alpha;
vec4 air=shellSegments(o,ray,1.0-uAirHeight,1.0,uRingSize.x,distance/uRingRadius);
float column=airColumn(o,ray,air.xy)+airColumn(o,ray,air.zw);
vec3 extinction=exp(-(vec3(.45,.55,.7)+uAirColor)*column*.000012);
vec3 haze=(vec3(.18,.28,.45)+uAirColor*.4)*.6;
radiance=radiance*extinction+haze*(1.0-extinction)*alpha;
}
fragColor=vec4(radiance,transmission);
}

View File

@@ -22,7 +22,10 @@ vec3 ringDirection(vec3 p) {
return vec3(normalize(p.xz).x*cos(latitude),sin(latitude),normalize(p.xz).y*cos(latitude)); return vec3(normalize(p.xz).x*cos(latitude),sin(latitude),normalize(p.xz).y*cos(latitude));
} }
vec3 ringLight(vec3 p,vec3 n,vec3 world) { vec3 ringLight(vec3 p,vec3 n,vec3 world) {
vec3 light=vec3(.09); vec3 up=-normalize(vec3(p.x,0,p.z));
float daylight=0.0;
for(int i=0;i<2;i++){if(i>=uLightCount)break;daylight+=uLightFlux[i]*smoothstep(-.08,.25,dot(up,uLightDirections[i]));}
vec3 light=ringSkyIrradiance(n,up,min(daylight,2.0));
for(int i=0;i<2;i++) { for(int i=0;i<2;i++) {
if(i>=uLightCount)break; if(i>=uLightCount)break;
vec3 ld=uLightDirections[i]; vec3 ld=uLightDirections[i];

View File

@@ -13,9 +13,24 @@ uniform sampler2D uRingCloudMap;
uniform float uRingCloudMapStrength; uniform float uRingCloudMapStrength;
uniform vec3 uRingCloudLayer; // base/radius, thickness/radius, density uniform vec3 uRingCloudLayer; // base/radius, thickness/radius, density
uniform vec3 uRingCloudShape; // shape scale in metres, coverage, extinction per metre uniform vec3 uRingCloudShape; // shape scale in metres, coverage, extinction per metre
uniform vec3 uRingCloudDetail; // detail scale in metres, erosion, cloud type
uniform sampler3D uRingCloudShapeTexture;
uniform sampler3D uRingCloudDetailTexture;
uniform vec3 uRingWeather; // scale in metres, variation, unused
uniform int uRingNearEnabled;
uniform vec3 uRingNearLayer; // base/radius, thickness/radius, density
uniform vec3 uRingNearShape; // shape scale, detail scale, coverage
uniform float uRingNearDistance;
uniform vec3 uRingCloudWind; uniform vec3 uRingCloudWind;
uniform float uRingCloudShadowStrength; uniform float uRingCloudShadowStrength;
// Diffuse sky and ground bounce in the local gravity frame. Shared by terrain,
// foliage and impostors so a LOD transition does not change the lighting model.
vec3 ringSkyIrradiance(vec3 normal,vec3 up,float daylight) {
float hemisphere=clamp(dot(normal,up)*.5+.5,0.0,1.0);
return mix(vec3(.105,.095,.065),vec3(.24,.32,.43),hemisphere)*daylight+vec3(.012,.018,.027);
}
vec2 cylinder(vec3 o,vec3 d,float radius) { vec2 cylinder(vec3 o,vec3 d,float radius) {
float a=dot(d.xz,d.xz); float a=dot(d.xz,d.xz);
if(a<1e-12)return length(o.xz)<radius?vec2(-1e10,1e10):vec2(1e10,-1e10); if(a<1e-12)return length(o.xz)<radius?vec2(-1e10,1e10):vec2(1e10,-1e10);
@@ -49,8 +64,16 @@ float ringHullVisibility(vec3 p,vec3 ld) {
if(ringSolidHit(o+vec3(0,wallCenter,0),ld,1.0-uRingAirHeight,outer,uRingSize.z*.5))return 0.0; if(ringSolidHit(o+vec3(0,wallCenter,0),ld,1.0-uRingAirHeight,outer,uRingSize.z*.5))return 0.0;
return 1.0; return 1.0;
} }
float ringCloudDensity(vec3 p) { float ringWeatherCoverage(vec3 p) {
if(uRingCloudEnabled==0||abs(p.y)>uRingSize.x)return 0.0; vec3 surface=normalize(vec3(p.x,0,p.z))*uRingRadius+vec3(0,p.y*uRingRadius,0)-uRingCloudWind*uRingTime;
vec3 q=surface/uRingWeather.x*.0625+uRingNoiseOffset*.0137;
float weather=textureLod(uRingCloudShapeTexture,q,0.0).a-.55;
float coverage=uRingCloudShape.y;
// The endpoints remain truly clear / overcast, including weather variation.
return clamp(coverage+weather*uRingWeather.y*4.0*coverage*(1.0-coverage),0.0,1.0);
}
float ringMainCloudDensity(vec3 p,float footprint,bool fine) {
if(uRingCloudEnabled==0||uRingCloudLayer.z<=0.0||abs(p.y)>uRingSize.x)return 0.0;
float altitude=1.0-length(p.xz); float altitude=1.0-length(p.xz);
float h=(altitude-uRingCloudLayer.x)/uRingCloudLayer.y; float h=(altitude-uRingCloudLayer.x)/uRingCloudLayer.y;
if(h<=0.0||h>=1.0)return 0.0; if(h<=0.0||h>=1.0)return 0.0;
@@ -58,10 +81,69 @@ float ringCloudDensity(vec3 p) {
vec3 q=p-wind; vec3 q=p-wind;
vec2 uv=vec2(.5-atan(q.z,q.x)/6.28318530718,clamp(.5-q.y/(uRingSize.x*2.0),0.0,1.0)); vec2 uv=vec2(.5-atan(q.z,q.x)/6.28318530718,clamp(.5-q.y/(uRingSize.x*2.0),0.0,1.0));
float map=uRingCloudHasMap!=0?textureLod(uRingCloudMap,uv,0.0).r:1.0; float map=uRingCloudHasMap!=0?textureLod(uRingCloudMap,uv,0.0).r:1.0;
float shape=noise3((p*uRingRadius-uRingCloudWind*uRingTime)/uRingCloudShape.x+uRingNoiseOffset)*.5+.5; vec3 physical=p*uRingRadius-uRingCloudWind*uRingTime;
float coverage=mix(uRingCloudShape.y,map,uRingCloudMapStrength*float(uRingCloudHasMap)); vec3 domain=physical/uRingCloudShape.x+uRingNoiseOffset;
float density=smoothstep(1.0-coverage,1.16-coverage,shape); float coverage=mix(ringWeatherCoverage(p),map,uRingCloudMapStrength*float(uRingCloudHasMap));
return density*smoothstep(0.0,.2,h)*(1.0-smoothstep(.65,1.0,h))*uRingCloudLayer.z; if(coverage<=0.001)return 0.0;
vec3 warp=textureLod(uRingCloudShapeTexture,domain*.057,1.0).gba*2.0-1.0;
domain+=warp*1.35;
float lod=max(0.0,log2(max(1.0,footprint*128.0*1.5/uRingCloudShape.x)));
vec3 structure=textureLod(uRingCloudShapeTexture,domain*1.5,lod).rgb;
// Broad cloud bodies carry the silhouette; cellular octaves only round their edges.
float shape=dot(structure,vec3(.9,.08,.02));
// Large puffs survive the pixel footprint on the opposite side of the ring.
if(lod>1.5&&footprint>500.0){
float distant=(noise3(domain*.7)*.68+noise3(domain*1.83+vec3(7.1,12.8,2.9))*.32)*.5+.5;
shape=mix(shape,distant,smoothstep(1.5,4.5,lod)*smoothstep(500.0,1000.0,footprint));
}
float edge=(1.0-coverage)*.8;
// Shadow/light probes integrate hundreds of metres per sample. Filter their
// boundary more broadly than the detailed view to avoid shimmering penumbrae.
float density=smoothstep(edge,edge+.22,shape);
float fineFade=1.0-smoothstep(uRingNearDistance*.65,uRingNearDistance,length(p-uRingCameraBody)*uRingRadius);
if(fine&&fineFade>0.0&&density>.001&&lod<3.5){
float detailLod=max(0.0,log2(max(1.0,footprint*64.0/uRingCloudDetail.x)));
vec3 detailSample=textureLod(uRingCloudDetailTexture,physical/uRingCloudDetail.x+uRingNoiseOffset*.0133,detailLod).rgb;
float detail=dot(detailSample,vec3(.6,.3,.1));
float erosion=(1.0-detail)*uRingCloudDetail.y*.16;
float detailed=smoothstep(edge+erosion,edge+erosion+.11,shape);
density=mix(density,detailed,fineFade*(1.0-smoothstep(1.5,3.5,lod)));
}
density=mix(density,1.0,smoothstep(.96,1.0,coverage));
float type=clamp(uRingCloudDetail.z+(coverage-.5)*.6,0.0,1.0);
float profile=smoothstep(0.0,.08,h)*(1.0-smoothstep(mix(.35,.65,type),mix(.6,1.0,type),h));
return density*profile*uRingCloudLayer.z;
}
float ringNearCloudDensity(vec3 p,float footprint,bool fine) {
if(uRingCloudEnabled==0||uRingNearEnabled==0||uRingNearShape.z<=0.0||uRingNearLayer.z<=0.0||abs(p.y)>uRingSize.x)return 0.0;
float h=(1.0-length(p.xz)-uRingNearLayer.x)/uRingNearLayer.y;
if(h<=0.0||h>=1.0)return 0.0;
vec3 physical=p*uRingRadius-uRingCloudWind*uRingTime*1.3;
// Group wisps into weather patches with genuinely clear gaps. Small-scale
// detail stays inside those patches instead of covering the whole sky.
float patch=textureLod(uRingCloudShapeTexture,physical/(uRingNearShape.x*4.0)*.0625+uRingNoiseOffset*.0231,0.0).a;
float patchEdge=.66-uRingNearShape.z*.45;
float patchMask=smoothstep(patchEdge,patchEdge+.16,patch);
if(patchMask<.001)return 0.0;
vec3 domain=physical/uRingNearShape.x*vec3(.35,1.3,2.2)+uRingNoiseOffset*.0191+vec3(8.7,17.1,3.4);
domain+=(textureLod(uRingCloudShapeTexture,physical/(uRingNearShape.x*3.0)+uRingNoiseOffset*.0137,1.0).gba-.5)*1.5;
// The base fractal's smallest octaves otherwise form a grainy veil over
// entire weather patches. Retain the broad wisps and add edge detail below.
float lod=max(2.5,log2(max(1.0,footprint*128.0/uRingNearShape.x)));
float shape=textureLod(uRingCloudShapeTexture,domain,lod).r;
float weather=noise3(physical/(uRingNearShape.x*2.0)+uRingNoiseOffset)*.13;
float density=smoothstep((1.0-uRingNearShape.z)*.65,(1.0-uRingNearShape.z)*.65+.12,shape+weather);
if(fine&&density>.001){
float detailLod=max(0.0,log2(max(1.0,footprint*64.0/uRingNearShape.y)));
vec3 detail=textureLod(uRingCloudDetailTexture,physical/uRingNearShape.y+uRingNoiseOffset*.0133,detailLod).rgb;
float erosion=(1.0-dot(detail,vec3(.6,.3,.1)))*.04;
density=smoothstep((1.0-uRingNearShape.z)*.65+erosion,(1.0-uRingNearShape.z)*.65+erosion+.12,shape+weather);
}
density*=smoothstep(.28,.55,density)*patchMask;
return density*smoothstep(0.0,.18,h)*(1.0-smoothstep(.55,1.0,h))*uRingNearLayer.z;
}
float ringCloudDensity(vec3 p) {
return ringMainCloudDensity(p,300.0,false);
} }
float ringCloudOpticalDepth(vec3 p,vec3 ld,vec2 interval) { float ringCloudOpticalDepth(vec3 p,vec3 ld,vec2 interval) {
float length=max(0.0,interval.y-interval.x); float length=max(0.0,interval.y-interval.x);
@@ -75,6 +157,16 @@ float ringCloudTransmission(vec3 p,vec3 ld) {
vec4 segments=shellSegments(p,ld,1.0-uRingCloudLayer.x-uRingCloudLayer.y, vec4 segments=shellSegments(p,ld,1.0-uRingCloudLayer.x-uRingCloudLayer.y,
1.0-uRingCloudLayer.x,uRingSize.x,1e6); 1.0-uRingCloudLayer.x,uRingSize.x,1e6);
float optical=ringCloudOpticalDepth(p,ld,segments.xy)+ringCloudOpticalDepth(p,ld,segments.zw); float optical=ringCloudOpticalDepth(p,ld,segments.xy)+ringCloudOpticalDepth(p,ld,segments.zw);
float nearFade=1.0-smoothstep(uRingNearDistance*.65,uRingNearDistance,length(p-uRingCameraBody)*uRingRadius);
if(uRingNearEnabled!=0&&nearFade>0.0){
vec4 nearSegments=shellSegments(p,ld,1.0-uRingNearLayer.x-uRingNearLayer.y,1.0-uRingNearLayer.x,uRingSize.x,uRingNearDistance/uRingRadius);
float nearOptical=0.0;
for(int interval=0;interval<2;interval++){
vec2 range=interval==0?nearSegments.xy:nearSegments.zw;float ds=max(0.0,range.y-range.x)/3.0;
for(int i=0;i<3;i++)nearOptical+=ringNearCloudDensity(p+ld*(range.x+(float(i)+.5)*ds),250.0,false)*ds*uRingRadius*uRingCloudShape.z;
}
optical+=nearOptical*nearFade;
}
return mix(1.0,exp(-min(30.0,optical)),uRingCloudShadowStrength); return mix(1.0,exp(-min(30.0,optical)),uRingCloudShadowStrength);
} }
float ringEnvironmentShadow(vec3 p,vec3 ld) { float ringEnvironmentShadow(vec3 p,vec3 ld) {

View File

@@ -9,16 +9,35 @@ in vec3 vMaterial;
uniform int uMaterialsEnabled; uniform int uMaterialsEnabled;
uniform vec3 uMaterialSettings; uniform vec3 uMaterialSettings;
uniform sampler2D uFlatColor; uniform sampler2D uFlatColor;
uniform sampler2D uFlatNormal;
uniform sampler2D uSlopeColor; uniform sampler2D uSlopeColor;
uniform sampler2D uSlopeNormal;
uniform sampler2D uSurfaceTexture; uniform sampler2D uSurfaceTexture;
uniform int uHasSurfaceTexture; uniform int uHasSurfaceTexture;
uniform vec3 uSurfaceColor; uniform vec3 uSurfaceColor;
uniform float uFar; uniform float uFar;
uniform sampler2D uVegetationDepth;
uniform int uVegetationDepthCullEnabled;
out vec4 fragColor; out vec4 fragColor;
vec3 triColor(sampler2D raster,vec3 p,vec3 w) { vec4 triColor(sampler2D raster,vec3 p,vec3 w) {
return texture(raster,p.yz).rgb*w.x+texture(raster,p.xz).rgb*w.y+texture(raster,p.xy).rgb*w.z; vec3 dx=dFdx(p),dy=dFdy(p);
vec4 color=vec4(0);
if(w.x>0.0)color+=textureGrad(raster,p.yz,dx.yz,dy.yz)*w.x;
if(w.y>0.0)color+=textureGrad(raster,p.xz,dx.xz,dy.xz)*w.y;
if(w.z>0.0)color+=textureGrad(raster,p.xy,dx.xy,dy.xy)*w.z;
return color;
}
vec3 triNormal(sampler2D raster,vec3 p,vec3 n,vec3 w) {
vec3 dx=dFdx(p),dy=dFdy(p);
vec3 x=vec3(0),y=vec3(0),z=vec3(0);
if(w.x>0.0)x=textureGrad(raster,p.yz,dx.yz,dy.yz).xyz*2.0-1.0;
if(w.y>0.0)y=textureGrad(raster,p.xz,dx.xz,dy.xz).xyz*2.0-1.0;
if(w.z>0.0)z=textureGrad(raster,p.xy,dx.xy,dy.xy).xyz*2.0-1.0;
// Perturb the geometric normal; a neutral normal map returns n on every slope.
return normalize(n+vec3(0,x.x,x.y)*w.x+vec3(y.x,0,y.y)*w.y+vec3(z.x,z.y,0)*w.z);
} }
void main() { void main() {
if(uVegetationDepthCullEnabled!=0&&log2(max(.0001,vFragDepth))/log2(uFar+1.0)>texelFetch(uVegetationDepth,ivec2(gl_FragCoord.xy),0).r+.0000002)discard;
vec3 n=normalize(vNormal); vec3 n=normalize(vNormal);
// Cylindrical UVs have constant physical scale across the band, including at the rims. // Cylindrical UVs have constant physical scale across the band, including at the rims.
// Analytic derivatives keep the longitude seam from selecting the coarsest mip level. // Analytic derivatives keep the longitude seam from selecting the coarsest mip level.
@@ -29,16 +48,35 @@ void main() {
color*=1.0+detail*(1.0-smoothstep(100.0,3000.0,length(vWorld))); color*=1.0+detail*(1.0-smoothstep(100.0,3000.0,length(vWorld)));
float slope=1.0-max(0.0,dot(n,-normalize(vec3(vBody.x,0,vBody.z)))); float slope=1.0-max(0.0,dot(n,-normalize(vec3(vBody.x,0,vBody.z))));
color=mix(color,vec3(.22,.20,.17),smoothstep(.16,.65,slope)*.7); color=mix(color,vec3(.22,.20,.17),smoothstep(.16,.65,slope)*.7);
float roughness=.92;
if(uMaterialsEnabled!=0) { if(uMaterialsEnabled!=0) {
vec3 weights=pow(abs(n),vec3(4));weights/=max(.0001,weights.x+weights.y+weights.z); vec3 weights=max(vec3(0),pow(abs(n),vec3(4))-vec3(.01));weights/=max(.0001,weights.x+weights.y+weights.z);
vec3 materialUv=vMaterial/uMaterialSettings.x; vec3 materialUv=vMaterial/uMaterialSettings.x;
vec3 flatColor=triColor(uFlatColor,materialUv,weights); vec4 flatColor=triColor(uFlatColor,materialUv,weights),rock=triColor(uSlopeColor,materialUv,weights);
// Retain the generated biome hue when adding the close-range ground texture. float rocky=smoothstep(.06,.3,slope);
float grain=dot(flatColor,vec3(.2126,.7152,.0722)); // Macro albedo chooses the biome; local material supplies metre-scale structure.
vec3 material=mix(color*clamp(grain*4.0,.45,1.7),triColor(uSlopeColor,materialUv,weights),smoothstep(.06,.3,slope)); float green=smoothstep(.01,.07,color.g-max(color.r,color.b));
float macro=noise3(vBody*uRadius*.037)*.12+noise3(vBody*uRadius*.008)*.12;
vec3 ground=mix(color,flatColor.rgb,mix(.32,.55,green));
ground=mix(ground,ground*vec3(.72,.93,.56),green*.65);
vec3 material=mix(ground,rock.rgb,rocky)*(1.0+macro);
float strength=uMaterialSettings.z*(1.0-smoothstep(uMaterialSettings.y*.35,uMaterialSettings.y,length(vWorld))); float strength=uMaterialSettings.z*(1.0-smoothstep(uMaterialSettings.y*.35,uMaterialSettings.y,length(vWorld)));
color=mix(color,material,strength); color=mix(color,material,strength);
roughness=mix(.92,mix(flatColor.a,rock.a,rocky),strength);
vec3 detailNormal=normalize(mix(triNormal(uFlatNormal,materialUv,n,weights),triNormal(uSlopeNormal,materialUv,n,weights),rocky));
n=normalize(mix(n,detailNormal,strength*.85));
} }
fragColor=vec4(color*ringLight(vBody,n,vWorld),1); vec3 up=-normalize(vec3(vBody.x,0,vBody.z));float daylight=0.0;
for(int i=0;i<2;i++){if(i>=uLightCount)break;daylight+=uLightFlux[i]*smoothstep(-.08,.25,dot(up,uLightDirections[i]));}
vec3 lit=color*ringSkyIrradiance(n,up,min(daylight,2.0));
vec3 view=normalize(-mat3(uWorldToBody)*vWorld);
for(int i=0;i<2;i++){
if(i>=uLightCount)break;
vec3 ld=uLightDirections[i],halfway=normalize(view+ld);
float ndl=max(0.0,dot(n,ld));if(ndl<=0.0)continue;
float highlight=pow(max(0.0,dot(n,halfway)),mix(64.0,5.0,roughness));
lit+=uLightColors[i]*uLightFlux[i]*(color+highlight*.025)*ndl*ringEnvironmentShadow(vBody,ld)*vegetationShadow(vWorld,i);
}
fragColor=vec4(lit,1);
gl_FragDepth=log2(max(.0001,vFragDepth))/log2(uFar+1.0); gl_FragDepth=log2(max(.0001,vFragDepth))/log2(uFar+1.0);
} }

View File

@@ -38,10 +38,17 @@ void main(){
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 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 normal=normalize(vBasis*mix(na*a.a,nb*b.a,blend));
vec3 light=vec3(.04,.055,.07); vec3 light=vec3(.04,.055,.07);
if(uRingShadowEnabled!=0){
float daylight=0.0;
for(int i=0;i<2;i++){if(i>=uLightCount)break;daylight+=uLightFlux[i]*smoothstep(-.08,.25,dot(uPlanetUp,uLightDirections[i]));}
light=ringSkyIrradiance(normal,uPlanetUp,min(daylight,2.0))*.85;
}
for(int i=0;i<2;i++){ for(int i=0;i<2;i++){
if(i>=uLightCount)break;float sunHeight=dot(uPlanetUp,uLightDirections[i]); if(i>=uLightCount)break;float sunHeight=dot(uPlanetUp,uLightDirections[i]);
vec3 extinction=exp(-vec3(.035,.07,.16)/(max(sunHeight,.04)+.08)); 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)*ringEnvironmentShadowWorld(vWorldPosition,uLightDirections[i]); float diffuse=max(.12,dot(normal,uLightDirections[i]));
if(uRingShadowEnabled!=0)diffuse=max(diffuse,(dot(normal,uLightDirections[i])+.35)/1.35)*.9+max(0.0,dot(-normal,uLightDirections[i]))*.35;
light+=uLightColors[i]*extinction*uLightFlux[i]*smoothstep(-.025,.018,sunHeight)*diffuse*vegetationShadow(vWorldPosition,i)*ringEnvironmentShadowWorld(vWorldPosition,uLightDirections[i]);
} }
fragColor=vec4(tex.rgb*light*(.9+vVariation.x*.2),1); 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); gl_FragDepth=clamp(log2(max(.0001,vFragDepth))/log2(uFar+1.0),0.0,1.0);

View File

@@ -57,12 +57,21 @@ void main(){
} }
vec3 arm=texture(uArm,vUv).rgb; vec3 arm=texture(uArm,vUv).rgb;
vec3 light=vec3(.035,.05,.065)*(.35+.65*arm.r); vec3 light=vec3(.035,.05,.065)*(.35+.65*arm.r);
if(uRingShadowEnabled!=0){
float daylight=0.0;
for(int i=0;i<2;i++){if(i>=uLightCount)break;daylight+=uLightFlux[i]*smoothstep(-.08,.25,dot(uPlanetUp,uLightDirections[i]));}
light=ringSkyIrradiance(normal,uPlanetUp,min(daylight,2.0))*(.4+.6*arm.r);
}
vec3 highlights=vec3(0); vec3 highlights=vec3(0);
vec3 view=normalize(-vWorldPosition); vec3 view=normalize(-vWorldPosition);
for(int i=0;i<2;i++){ for(int i=0;i<2;i++){
if(i>=uLightCount)break; if(i>=uLightCount)break;
vec3 ld=uLightDirections[i];float ndl=max(0.0,dot(normal,ld)); 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 back=uFoliage!=0?pow(max(0.0,dot(view,-ld)),3.0)*.32:0.0;
if(uRingShadowEnabled!=0&&uFoliage!=0){
back+=max(0.0,dot(-normal,ld))*.35;
ndl=max(ndl,(dot(normal,ld)+.35)/1.35)*.9;
}
float visibility=vegetationShadow(vWorldPosition,i)*ringEnvironmentShadowWorld(vWorldPosition,uLightDirections[i]); float visibility=vegetationShadow(vWorldPosition,i)*ringEnvironmentShadowWorld(vWorldPosition,uLightDirections[i]);
float sunHeight=dot(uPlanetUp,ld); float sunHeight=dot(uPlanetUp,ld);
vec3 extinction=exp(-vec3(.035,.07,.16)/(max(sunHeight,.04)+.08)); vec3 extinction=exp(-vec3(.035,.07,.16)/(max(sunHeight,.04)+.08));

View File

@@ -10,6 +10,11 @@ namespace Universe.App.Graphics;
internal sealed partial class UniverseRenderer internal sealed partial class UniverseRenderer
{ {
private ShaderProgram _ringWorldTerrain = null!, _ringWorldHull = null!, _ringWorldAir = null!; private ShaderProgram _ringWorldTerrain = null!, _ringWorldHull = null!, _ringWorldAir = null!;
private ShaderProgram _ringWorldMeadow = null!;
private ShaderProgram _ringWorldClouds = null!;
private RenderTarget? _ringCloudRaw;
private RingMeadowRenderer _ringMeadow = null!;
internal int RingMeadowTufts => _ringMeadow.DrawnTufts;
internal bool RingVegetationShadows { get; set; } = true; internal bool RingVegetationShadows { get; set; } = true;
internal bool RingCloudShadows { get; set; } = true; internal bool RingCloudShadows { get; set; } = true;
private void InitializeRingWorld() private void InitializeRingWorld()
@@ -18,6 +23,9 @@ internal sealed partial class UniverseRenderer
_ringWorldTerrain = Program("ring-world.vert", "ring-world-terrain.frag"); _ringWorldTerrain = Program("ring-world.vert", "ring-world-terrain.frag");
_ringWorldHull = Program("fullscreen.vert", "ring-world-hull.frag"); _ringWorldHull = Program("fullscreen.vert", "ring-world-hull.frag");
_ringWorldAir = Program("fullscreen.vert", "ring-world-air.frag"); _ringWorldAir = Program("fullscreen.vert", "ring-world-air.frag");
_ringWorldMeadow = Program("ring-meadow.vert", "ring-meadow.frag");
_ringWorldClouds = Program("fullscreen.vert", "ring-world-cloud.frag");
_ringMeadow = new(_gl);
} }
private void ReloadRingWorldShaders() private void ReloadRingWorldShaders()
{ {
@@ -27,18 +35,23 @@ internal sealed partial class UniverseRenderer
replacements.Add(Program("ring-world.vert", "ring-world-terrain.frag")); replacements.Add(Program("ring-world.vert", "ring-world-terrain.frag"));
replacements.Add(Program("fullscreen.vert", "ring-world-hull.frag")); replacements.Add(Program("fullscreen.vert", "ring-world-hull.frag"));
replacements.Add(Program("fullscreen.vert", "ring-world-air.frag")); replacements.Add(Program("fullscreen.vert", "ring-world-air.frag"));
replacements.Add(Program("ring-meadow.vert", "ring-meadow.frag"));
replacements.Add(Program("fullscreen.vert", "ring-world-cloud.frag"));
} }
catch { foreach (var p in replacements) p.Dispose(); throw; } catch { foreach (var p in replacements) p.Dispose(); throw; }
DisposeRingWorld(); _ringWorldTerrain.Dispose(); _ringWorldHull.Dispose(); _ringWorldAir.Dispose(); _ringWorldMeadow.Dispose();
_ringWorldTerrain = replacements[0]; _ringWorldHull = replacements[1]; _ringWorldAir = replacements[2]; _ringWorldTerrain = replacements[0]; _ringWorldHull = replacements[1]; _ringWorldAir = replacements[2];
_ringWorldMeadow = replacements[3];
_ringWorldClouds.Dispose();_ringWorldClouds=replacements[4];
} }
private void DisposeRingWorld() { _ringWorldTerrain.Dispose(); _ringWorldHull.Dispose(); _ringWorldAir.Dispose(); } private void DisposeRingWorld() { _ringWorldTerrain.Dispose(); _ringWorldHull.Dispose(); _ringWorldAir.Dispose(); _ringWorldMeadow.Dispose(); _ringMeadow.Dispose(); _ringWorldClouds.Dispose(); _ringCloudRaw?.Dispose(); }
internal void BindRingShadowSettings(ShaderProgram shader, PlanetData planet, DVector3 bodyCamera) internal void BindRingShadowSettings(ShaderProgram shader, PlanetData planet, DVector3 bodyCamera)
{ {
// Set sampler units even on spherical bodies; GLSL forbids active samplers of // Set sampler units even on spherical bodies; GLSL forbids active samplers of
// different types sharing the default unit, including behind a disabled branch. // different types sharing the default unit, including behind a disabled branch.
shader.Set("uNoise", 4); shader.Set("uRingCloudMap", 10); shader.Set("uNoise", 4); shader.Set("uRingCloudMap", 10);
shader.Set("uRingCloudShapeTexture",9);shader.Set("uRingCloudDetailTexture",7);
shader.Set("uRingShadowEnabled", planet.IsRingWorld ? 1 : 0); shader.Set("uRingShadowEnabled", planet.IsRingWorld ? 1 : 0);
if (planet.RingWorld is not { } ring) return; if (planet.RingWorld is not { } ring) return;
shader.Set("uRingRadius", (float)ring.RadiusMeters); shader.Set("uRingRadius", (float)ring.RadiusMeters);
@@ -54,6 +67,18 @@ internal sealed partial class UniverseRenderer
shader.Set("uRingCloudLayer", cloud is null ? Vector3.Zero : new((float)(cloud.BaseAltitudeMeters / ring.RadiusMeters), shader.Set("uRingCloudLayer", cloud is null ? Vector3.Zero : new((float)(cloud.BaseAltitudeMeters / ring.RadiusMeters),
(float)(cloud.ThicknessMeters / ring.RadiusMeters), (float)cloud.Density)); (float)(cloud.ThicknessMeters / ring.RadiusMeters), (float)cloud.Density));
shader.Set("uRingCloudShape", cloud is null ? Vector3.One : new((float)cloud.ShapeScaleMeters, (float)cloud.Coverage, (float)cloud.ExtinctionPerMeter)); shader.Set("uRingCloudShape", cloud is null ? Vector3.One : new((float)cloud.ShapeScaleMeters, (float)cloud.Coverage, (float)cloud.ExtinctionPerMeter));
shader.Set("uRingCloudDetail", cloud is null ? Vector3.One : new((float)cloud.DetailScaleMeters, (float)cloud.Erosion, (float)cloud.CloudType));
shader.Set("uRingCloudLighting", cloud is null ? Vector3.One : new((float)cloud.AmbientStrength, (float)cloud.SunlightStrength, (float)cloud.ScatteringAlbedo));
var detail=ring.CloudDetail;
shader.Set("uRingWeather",new Vector3((float)detail.WeatherScaleMeters,(float)detail.WeatherVariation,0));
shader.Set("uRingNearEnabled",detail.NearEnabled?1:0);
shader.Set("uRingNearLayer",new Vector3((float)(detail.NearBaseAltitudeMeters/ring.RadiusMeters),
(float)(detail.NearThicknessMeters/ring.RadiusMeters),(float)detail.NearDensity));
shader.Set("uRingNearShape",new Vector3((float)detail.NearScaleMeters,(float)detail.NearDetailScaleMeters,(float)detail.NearCoverage));
shader.Set("uRingNearDistance",(float)detail.NearDistanceMeters);
shader.Set("uRingCloudPhase",cloud is null?Vector3.Zero:new((float)cloud.Anisotropy,(float)cloud.SilverLiningStrength,0));
_gl.ActiveTexture(GLEnum.Texture9);_gl.BindTexture(GLEnum.Texture3D,_cloudNoise.Shape);
_gl.ActiveTexture(GLEnum.Texture7);_gl.BindTexture(GLEnum.Texture3D,_cloudNoise.Detail);
shader.Set("uRingCloudWind", cloud is null ? Vector3.Zero : CloudLighting.Wind(cloud).ToVector3()); shader.Set("uRingCloudWind", cloud is null ? Vector3.Zero : CloudLighting.Wind(cloud).ToVector3());
shader.Set("uRingCloudHasMap", planet.CloudDensityMap is null ? 0 : 1); shader.Set("uRingCloudHasMap", planet.CloudDensityMap is null ? 0 : 1);
shader.Set("uRingCloudMapStrength", (float)(cloud?.DensityTextureStrength ?? 0)); shader.Set("uRingCloudMapStrength", (float)(cloud?.DensityTextureStrength ?? 0));
@@ -95,17 +120,20 @@ internal sealed partial class UniverseRenderer
if (DVector3.Dot(relative, camera.Forward) < -planet.MaximumTerrainRadiusMeters || relative.Length > planet.RadiusMeters * 2000) continue; if (DVector3.Dot(relative, camera.Forward) < -planet.MaximumTerrainRadiusMeters || relative.Length > planet.RadiusMeters * 2000) continue;
if (!_planetMeshes.TryGetValue(planet.Id, out var mesh)) _planetMeshes.Add(planet.Id, mesh = new(_gl, planet)); if (!_planetMeshes.TryGetValue(planet.Id, out var mesh)) _planetMeshes.Add(planet.Id, mesh = new(_gl, planet));
BindRingWorld(_ringWorldTerrain, planet, system, camera, relative, far); BindRingWorld(_ringWorldTerrain, planet, system, camera, relative, far);
Vegetation.BindDepthCulling(_ringWorldTerrain, planet.Id==Vegetation.PlanetId);
_ringWorldTerrain.SetMatrix("uViewProjection", vp); _ringWorldTerrain.SetMatrix("uViewProjection", vp);
var materialSettings = planet.Materials; var materialSettings = planet.Materials;
var materialsEnabled = materialSettings.Enabled && materialSettings.Strength > 0 var materialsEnabled = materialSettings.Enabled && materialSettings.Strength > 0
&& PlanetSurface.Distance(planet, -relative) < materialSettings.FadeDistanceMeters; && PlanetSurface.Distance(planet, -relative) < materialSettings.FadeDistanceMeters;
_ringWorldTerrain.Set("uMaterialsEnabled", materialsEnabled ? 1 : 0); _ringWorldTerrain.Set("uMaterialsEnabled", materialsEnabled ? 1 : 0);
_ringWorldTerrain.Set("uMaterialSettings", new Vector3((float)materialSettings.ScaleMeters, (float)materialSettings.FadeDistanceMeters, (float)materialSettings.Strength)); _ringWorldTerrain.Set("uMaterialSettings", new Vector3((float)materialSettings.ScaleMeters, (float)materialSettings.FadeDistanceMeters, (float)materialSettings.Strength));
_ringWorldTerrain.Set("uFlatColor", 11); _ringWorldTerrain.Set("uSlopeColor", 13); _ringWorldTerrain.Set("uFlatColor", 11); _ringWorldTerrain.Set("uFlatNormal", 12);
_ringWorldTerrain.Set("uSlopeColor", 13); _ringWorldTerrain.Set("uSlopeNormal", 14);
if (materialsEnabled) if (materialsEnabled)
{ {
var materials = _materials.ForPlanet(planet); var materials = _materials.ForPlanet(planet);
BindTexture(11, materials.FlatColor); BindTexture(13, materials.SlopeColor); BindTexture(11, materials.FlatColor); BindTexture(12, materials.FlatNormal);
BindTexture(13, materials.SlopeColor); BindTexture(14, materials.SlopeNormal);
} }
var bodyCamera = planet.Orientation.ToBody(-relative); var bodyCamera = planet.Orientation.ToBody(-relative);
mesh.Draw(bodyCamera, planet.Orientation.ToBody(camera.Forward), planet.Orientation.ToBody(camera.Right), planet.Orientation.ToBody(camera.Up), mesh.Draw(bodyCamera, planet.Orientation.ToBody(camera.Forward), planet.Orientation.ToBody(camera.Right), planet.Orientation.ToBody(camera.Up),
@@ -123,8 +151,22 @@ internal sealed partial class UniverseRenderer
if (mesh.NearestDistance < TerrainNearestDistance) { TerrainNearestDistance = mesh.NearestDistance; TerrainNearestLevel = mesh.NearestLevel; } if (mesh.NearestDistance < TerrainNearestDistance) { TerrainNearestDistance = mesh.NearestDistance; TerrainNearestLevel = mesh.NearestLevel; }
BindRingWorld(_ringWorldHull, planet, system, camera, relative, far); BindRingWorld(_ringWorldHull, planet, system, camera, relative, far);
_gl.Enable(GLEnum.DepthTest); _gl.DepthMask(true); _fullscreen.Draw(); _gl.Enable(GLEnum.DepthTest); _gl.DepthMask(true); _fullscreen.Draw();
if (planet.RingWorld?.Meadow is {Enabled:true})
{
BindRingWorld(_ringWorldMeadow, planet, system, camera, relative, far);
_ringWorldMeadow.SetMatrix("uViewProjection", vp);
_ringWorldMeadow.Set("uMeadowDistance", (float)planet.RingWorld!.Meadow!.DistanceMeters);
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), (double)_width/_height);
_ringMeadow.Draw(planet, bodyCamera, view, origin =>
{
_ringWorldMeadow.Set("uChunkPosition", (relative+planet.Orientation.ToWorld(origin)).ToVector3());
_ringWorldMeadow.Set("uBodyOrigin", (origin/planet.RadiusMeters).ToVector3());
});
} }
} }
}
private RenderTarget DrawRingWorldAtmospheres(FlyCamera camera, StarSystemData system, float far, RenderTarget source) private RenderTarget DrawRingWorldAtmospheres(FlyCamera camera, StarSystemData system, float far, RenderTarget source)
{ {
@@ -142,4 +184,31 @@ internal sealed partial class UniverseRenderer
} }
return source; return source;
} }
private RenderTarget DrawRingWorldClouds(FlyCamera camera,StarSystemData system,float far,RenderTarget source)
{
var center=UniversePosition.AtCellCenter(system.Coordinate,camera.CellSizeMeters);
_gl.Disable(GLEnum.DepthTest);_gl.DepthMask(false);
foreach(var planet in system.Planets.Where(p=>p.IsRingWorld&&p.Clouds is {Enabled:true}).OrderByDescending(p=>center.Add(p.PositionMeters,camera.CellSizeMeters).RelativeTo(camera.Position,camera.CellSizeMeters).Length))
{
var relative=center.Add(planet.PositionMeters,camera.CellSizeMeters).RelativeTo(camera.Position,camera.CellSizeMeters);
if(relative.Length>planet.RadiusMeters*100||DVector3.Dot(relative,camera.Forward)< -planet.MaximumTerrainRadiusMeters)continue;
var clouds=planet.Clouds!;
var width=System.Math.Max(1,(int)System.Math.Ceiling(_width*clouds.ResolutionScale));
var height=System.Math.Max(1,(int)System.Math.Ceiling(_height*clouds.ResolutionScale));
if(_ringCloudRaw is null||_ringCloudRaw.Width!=width||_ringCloudRaw.Height!=height)
{_ringCloudRaw?.Dispose();_ringCloudRaw=new(_gl,width,height);}
_ringCloudRaw.Bind();BindRingWorld(_ringWorldClouds,planet,system,camera,relative,far);
BindTexture(1,_scene.Depth);_ringWorldClouds.Set("uDepth",1);
_ringWorldClouds.Set("uRingCloudViewSteps",clouds.ViewSteps);_ringWorldClouds.Set("uRingCloudLightSteps",clouds.LightSteps);
_ringWorldClouds.Set("uRingCloudPixelAngle",(float)(2*System.Math.Tan(camera.FieldOfViewRadians*.5)/height));
_fullscreen.Draw();
var target=source==_scatterA?_scatterB:_scatterA;target.Bind();_cloudComposite.Use();
BindTexture(0,source.Color);BindTexture(1,_scene.Depth);BindTexture(2,_ringCloudRaw.Color);
_cloudComposite.Set("uScene",0);_cloudComposite.Set("uDepth",1);_cloudComposite.Set("uClouds",2);
_cloudComposite.Set("uCloudTexel",new Vector3(1f/width,1f/height,0));
_fullscreen.Draw();source=target;
}
return source;
}
} }

View File

@@ -53,6 +53,14 @@ internal sealed partial class UniverseRenderer : IDisposable
private readonly NoiseTexture _noise; private readonly NoiseTexture _noise;
private readonly CloudNoiseTexture _cloudNoise; private readonly CloudNoiseTexture _cloudNoise;
private readonly SurfaceMaterials _materials; private readonly SurfaceMaterials _materials;
private readonly RingSurfaceEffects _surfaceEffects;
private readonly RingTemporalHistory _temporalHistory=new();
private Vector2 _rayJitter;
private PlanetData? _surfaceRing;
internal bool RingTemporalAntialiasing {get;set;}=true;
internal bool RingContactOcclusion {get;set;}=true;
internal bool RingTemporalHistoryValid=>_temporalHistory.Valid;
internal void ResetSurfaceHistory(){_temporalHistory.Reset();_surfaceEffects.Reset();}
internal VegetationRenderer Vegetation {get;} internal VegetationRenderer Vegetation {get;}
private RenderTarget? _cloudRaw; private RenderTarget? _cloudRaw;
private RenderTarget? _waterReflectionRaw; private RenderTarget? _waterReflectionRaw;
@@ -127,6 +135,7 @@ internal sealed partial class UniverseRenderer : IDisposable
_bright = Program("fullscreen.vert", "bright.frag"); _bright = Program("fullscreen.vert", "bright.frag");
_blur = Program("fullscreen.vert", "blur.frag"); _blur = Program("fullscreen.vert", "blur.frag");
_final = Program("fullscreen.vert", "final.frag"); _final = Program("fullscreen.vert", "final.frag");
_surfaceEffects=new(gl);
_ring = Program("fullscreen.vert", "ring.frag"); _ring = Program("fullscreen.vert", "ring.frag");
_gas = Program("fullscreen.vert", "gas.frag"); _gas = Program("fullscreen.vert", "gas.frag");
_rock = Program("rock.vert", "rock.frag"); _rock = Program("rock.vert", "rock.frag");
@@ -202,6 +211,7 @@ internal sealed partial class UniverseRenderer : IDisposable
_starTrails.ReloadShaders(); _starTrails.ReloadShaders();
Vegetation.ReloadShaders(); Vegetation.ReloadShaders();
ReloadRingWorldShaders(); ReloadRingWorldShaders();
_surfaceEffects.Reload();_temporalHistory.Reset();
} }
catch (Exception error) catch (Exception error)
{ {
@@ -227,6 +237,7 @@ internal sealed partial class UniverseRenderer : IDisposable
{ {
if (coordinate == _activeSystem) return; if (coordinate == _activeSystem) return;
_activeSystem = coordinate; _activeSystem = coordinate;
_temporalHistory.Reset();_surfaceEffects.Reset();
_gpuProfiler.Clear(); _gpuProfiler.Clear();
_probeFrameCounter = 0; _probeFrameCounter = 0;
_debugProbePlanetId = null; _debugProbePlanetId = null;
@@ -258,6 +269,7 @@ internal sealed partial class UniverseRenderer : IDisposable
{ {
_width = System.Math.Max(width, 1); _width = System.Math.Max(width, 1);
_height = System.Math.Max(height, 1); _height = System.Math.Max(height, 1);
_surfaceEffects.Resize();_temporalHistory.Reset();
_cloudRaw?.Dispose(); _cloudRaw = null; _cloudRaw?.Dispose(); _cloudRaw = null;
_waterReflectionRaw?.Dispose(); _waterReflectionRaw = null; _waterReflectionTarget?.Dispose(); _waterReflectionRaw?.Dispose(); _waterReflectionRaw = null; _waterReflectionTarget?.Dispose();
_scene?.Dispose(); _gasInput?.Dispose(); _glareTarget?.Dispose(); _scatterA?.Dispose(); _scatterB?.Dispose(); _bloomA?.Dispose(); _bloomB?.Dispose(); _scene?.Dispose(); _gasInput?.Dispose(); _glareTarget?.Dispose(); _scatterA?.Dispose(); _scatterB?.Dispose(); _bloomA?.Dispose(); _bloomB?.Dispose();
@@ -292,14 +304,25 @@ internal sealed partial class UniverseRenderer : IDisposable
TerrainNearestLevel = 0; TerrainNearestDistance = double.PositiveInfinity; TerrainNearestLevel = 0; TerrainNearestDistance = double.PositiveInfinity;
SetActiveSystem(neighborhood.ActiveSystem.Coordinate); SetActiveSystem(neighborhood.ActiveSystem.Coordinate);
var far = (float)(camera.CellSizeMeters * 4.5); var far = (float)(camera.CellSizeMeters * 4.5);
var view = Matrix4x4.CreateLookAt(Vector3.Zero, camera.Forward.ToVector3(), camera.Up.ToVector3());
var projection = Matrix4x4.CreatePerspectiveFieldOfView((float)camera.FieldOfViewRadians, (float)_width / _height, 0.02f, far);
var vp = view * projection;
var system = neighborhood.ActiveSystem; var system = neighborhood.ActiveSystem;
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
_surfaceRing=null;var bodyCamera=DVector3.Zero;
foreach(var planet in system.Planets.Where(p=>p.IsRingWorld))
{
var body=planet.Orientation.ToBody(camera.Position.RelativeTo(center.Add(planet.PositionMeters,camera.CellSizeMeters),camera.CellSizeMeters));
if(System.Math.Abs(body.Y)>planet.RingWorld!.WidthMeters*.5)continue;
var altitude=planet.RadiusMeters-new DVector3(body.X,0,body.Z).Length;
if(altitude< -100||altitude>planet.Terrain.HeightmapMaxMeters+planet.TerrainAmplitudeMeters*2+100)continue;
if(System.Math.Abs(altitude-RingWorldGeometry.Elevation(planet,body))>100)continue;
_surfaceRing=planet;bodyCamera=body;break;
}
var temporal=RingTemporalAntialiasing&&_surfaceRing?.RingWorld?.Rendering.TemporalAntialiasing==true;
var vp=_temporalHistory.Advance(camera,temporal?_surfaceRing:null,bodyCamera,_width,_height,far);
_rayJitter=new(_temporalHistory.Jitter.X/_width,_temporalHistory.Jitter.Y/_height);
if(!temporal)_surfaceEffects.Reset();
Vegetation.Aspect=(double)_width/_height;Vegetation.Update(camera,system,_time); Vegetation.Aspect=(double)_width/_height;Vegetation.Update(camera,system,_time);
Mark("vegetation streaming"); Mark("vegetation streaming");
RetainSystem(system); 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(); var starPositions = system.Stars.Select(s => center.Add(s.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters)).ToArray();
BindNoise(); BindNoise();
_gpuProfiler.BeginFrame(DebugMode||BenchmarkProfiling); _gpuProfiler.BeginFrame(DebugMode||BenchmarkProfiling);
@@ -364,13 +387,19 @@ internal sealed partial class UniverseRenderer : IDisposable
DrawOceans(camera, system, far); DrawOceans(camera, system, far);
_gpuProfiler.EndPass(); _gpuProfiler.EndPass();
Mark("bodies and ocean"); Mark("bodies and ocean");
_gpuProfiler.BeginPass("CONTACT OCCLUSION");
var opaque=_scene;
if(RingContactOcclusion&&_surfaceRing?.RingWorld?.Rendering is {ContactOcclusion:true,ContactStrength:>0} quality)
opaque=_surfaceEffects.Contact(_scene,_scene.Depth,far,(float)quality.ContactStrength,p=>SetCameraRay(p,camera));
_gpuProfiler.EndPass();Mark("contact occlusion");
_gpuProfiler.BeginPass("ATMOSPHERE"); _gpuProfiler.BeginPass("ATMOSPHERE");
var composed = DrawAtmospheres(camera, system, far); var composed = DrawAtmospheres(camera, system, far,opaque);
composed = DrawRingWorldAtmospheres(camera, system, far, composed); composed = DrawRingWorldAtmospheres(camera, system, far, composed);
_gpuProfiler.EndPass(); _gpuProfiler.EndPass();
Mark("atmosphere"); Mark("atmosphere");
_gpuProfiler.BeginPass("CLOUDS / COMPOSITE"); _gpuProfiler.BeginPass("CLOUDS / COMPOSITE");
composed = DrawClouds(camera, system, far, composed); composed = DrawClouds(camera, system, far, composed);
composed = DrawRingWorldClouds(camera,system,far,composed);
_gpuProfiler.EndPass(); _gpuProfiler.EndPass();
Mark("clouds"); Mark("clouds");
_gpuProfiler.BeginPass("WATER REFLECTIONS"); _gpuProfiler.BeginPass("WATER REFLECTIONS");
@@ -384,8 +413,11 @@ internal sealed partial class UniverseRenderer : IDisposable
_gpuProfiler.BeginPass("SUN GLOW"); _gpuProfiler.BeginPass("SUN GLOW");
DrawSunGlow(camera, system, starPositions, far, composed); DrawSunGlow(camera, system, starPositions, far, composed);
_gpuProfiler.EndPass(); _gpuProfiler.EndPass();
_gpuProfiler.BeginPass("TEMPORAL AA");
var resolved=temporal?_surfaceEffects.Resolve(_glareTarget,_scene.Depth,far,_temporalHistory,p=>SetCameraRay(p,camera)):_glareTarget;
_gpuProfiler.EndPass();Mark("temporal antialiasing");
_gpuProfiler.BeginPass("BLOOM / POST"); _gpuProfiler.BeginPass("BLOOM / POST");
DrawPostProcessing(_glareTarget, CalculateExposure(camera, system) * ExposureCompensation); DrawPostProcessing(resolved, CalculateExposure(camera, system) * ExposureCompensation,temporal);
_gpuProfiler.EndPass(); _gpuProfiler.EndPass();
Mark("glow and post"); Mark("glow and post");
_gl.Disable(GLEnum.DepthTest); _gl.Disable(GLEnum.DepthTest);
@@ -452,6 +484,7 @@ internal sealed partial class UniverseRenderer : IDisposable
program.Set("uUp", camera.Up.ToVector3()); program.Set("uUp", camera.Up.ToVector3());
program.Set("uAspect", (float)_width / _height); program.Set("uAspect", (float)_width / _height);
program.Set("uTanHalfFov", (float)System.Math.Tan(camera.FieldOfViewRadians * 0.5)); program.Set("uTanHalfFov", (float)System.Math.Tan(camera.FieldOfViewRadians * 0.5));
program.Set("uRayJitter",new Vector3(_rayJitter,0));
} }
private void DrawStarfield(FlyCamera camera, UniverseNeighborhood neighborhood, Matrix4x4 vp, float far) private void DrawStarfield(FlyCamera camera, UniverseNeighborhood neighborhood, Matrix4x4 vp, float far)
@@ -949,13 +982,12 @@ internal sealed partial class UniverseRenderer : IDisposable
return _blackHoleTarget; return _blackHoleTarget;
} }
private RenderTarget DrawAtmospheres(FlyCamera camera, StarSystemData system, float far) private RenderTarget DrawAtmospheres(FlyCamera camera, StarSystemData system, float far,RenderTarget source)
{ {
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters); var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
var candidates = system.Planets.Select(p => (Planet: p, Relative: center.Add(p.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters))) var candidates = system.Planets.Select(p => (Planet: p, Relative: center.Add(p.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters)))
.Where(p => !p.Planet.IsGasPlanet && !p.Planet.IsRingWorld && p.Planet.AtmosphereHeightMeters > 0 && p.Relative.Length < p.Planet.RadiusMeters * 35 && DVector3.Dot(p.Relative, camera.Forward) > -p.Planet.RadiusMeters * 1.04) .Where(p => !p.Planet.IsGasPlanet && !p.Planet.IsRingWorld && p.Planet.AtmosphereHeightMeters > 0 && p.Relative.Length < p.Planet.RadiusMeters * 35 && DVector3.Dot(p.Relative, camera.Forward) > -p.Planet.RadiusMeters * 1.04)
.OrderBy(p => p.Relative.Length / p.Planet.RadiusMeters).Take(3).OrderByDescending(p => p.Relative.Length).ToArray(); .OrderBy(p => p.Relative.Length / p.Planet.RadiusMeters).Take(3).OrderByDescending(p => p.Relative.Length).ToArray();
var source = _scene;
_gl.Disable(GLEnum.DepthTest); _gl.DepthMask(false); _gl.Disable(GLEnum.DepthTest); _gl.DepthMask(false);
foreach (var candidate in candidates) foreach (var candidate in candidates)
{ {
@@ -1116,11 +1148,21 @@ internal sealed partial class UniverseRenderer : IDisposable
var visibility = 1d; var visibility = 1d;
foreach (var planet in system.Planets) foreach (var planet in system.Planets)
{ {
if (planet.AtmosphereHeightMeters <= 0 || planet.IsRingWorld) continue; if (planet.AtmosphereHeightMeters <= 0) continue;
var radial = local - planet.PositionMeters; var radial = local - planet.PositionMeters;
var altitude = radial.Length - planet.RadiusMeters; double altitude;
DVector3 up;
if(planet.IsRingWorld)
{
var body=planet.Orientation.ToBody(radial);
if(System.Math.Abs(body.Y)>planet.RingWorld!.WidthMeters*.5)continue;
altitude=planet.RadiusMeters-new DVector3(body.X,0,body.Z).Length;
if(altitude<0)continue;
up=planet.Orientation.ToWorld(-RingWorldGeometry.Radial(body));
}
else {altitude=radial.Length-planet.RadiusMeters;up=radial.Normalized();}
if (altitude > planet.AtmosphereHeightMeters) continue; if (altitude > planet.AtmosphereHeightMeters) continue;
var sunHeight = system.Stars.Max(s => DVector3.Dot(radial.Normalized(), (s.PositionMeters - local).Normalized())); var sunHeight = system.Stars.Max(s => DVector3.Dot(up, (s.PositionMeters - local).Normalized()));
var air = System.Math.Exp(-System.Math.Max(altitude, 0) / (planet.AtmosphereHeightMeters * 0.10)) * planet.AtmosphereDensity; var air = System.Math.Exp(-System.Math.Max(altitude, 0) / (planet.AtmosphereHeightMeters * 0.10)) * planet.AtmosphereDensity;
visibility *= System.Math.Exp(-System.Math.Max(0, sunHeight + 0.05) * air * 35); visibility *= System.Math.Exp(-System.Math.Max(0, sunHeight + 0.05) * air * 35);
} }
@@ -1166,7 +1208,7 @@ internal sealed partial class UniverseRenderer : IDisposable
return star.RadiusMeters * (1 + (star.Glow.ApparentRadiusMultiplier - 1) * blend); return star.RadiusMeters * (1 + (star.Glow.ApparentRadiusMultiplier - 1) * blend);
} }
private void DrawPostProcessing(RenderTarget source, float exposure) private void DrawPostProcessing(RenderTarget source, float exposure,bool temporal)
{ {
_gl.Disable(GLEnum.DepthTest); _gl.Disable(GLEnum.Blend); _gl.Disable(GLEnum.DepthTest); _gl.Disable(GLEnum.Blend);
_bloomA.Bind(); _bright.Use(); _bloomA.Bind(); _bright.Use();
@@ -1186,6 +1228,7 @@ internal sealed partial class UniverseRenderer : IDisposable
_final.Use(); BindTexture(0, source.Color); BindTexture(2, bloom.Color); _final.Use(); BindTexture(0, source.Color); BindTexture(2, bloom.Color);
_final.Set("uScene", 0); _final.Set("uBloom", 2); _final.Set("uExposure", exposure); _final.Set("uScene", 0); _final.Set("uBloom", 2); _final.Set("uExposure", exposure);
_final.Set("uTexel", new Vector3(1f / _width, 1f / _height, 0)); _final.Set("uTexel", new Vector3(1f / _width, 1f / _height, 0));
_final.Set("uSpatialAA",temporal?0:1);
_fullscreen.Draw(); _gl.DepthMask(true); _fullscreen.Draw(); _gl.DepthMask(true);
} }
@@ -1237,6 +1280,7 @@ internal sealed partial class UniverseRenderer : IDisposable
_performanceOverlay.Dispose(); _gpuProfiler.Dispose(); _performanceOverlay.Dispose(); _gpuProfiler.Dispose();
_cosmos?.Dispose(); _cosmos?.Dispose();
_starTrails.Dispose(); _starTrails.Dispose();
_surfaceEffects.Dispose();
_surface.Dispose(); _star.Dispose(); _sky.Dispose(); _points.Dispose(); _atmosphere.Dispose(); _bright.Dispose(); _blur.Dispose(); _final.Dispose(); _overlay.Dispose(); _ring.Dispose(); _surface.Dispose(); _star.Dispose(); _sky.Dispose(); _points.Dispose(); _atmosphere.Dispose(); _bright.Dispose(); _blur.Dispose(); _final.Dispose(); _overlay.Dispose(); _ring.Dispose();
_benchmarkOverlay.Dispose(); _benchmarkOverlay.Dispose();
_gas.Dispose(); _rock.Dispose(); _rockPoints.Dispose(); _emptyRing.Dispose(); _gasInput.Dispose(); _gas.Dispose(); _rock.Dispose(); _rockPoints.Dispose(); _emptyRing.Dispose(); _gasInput.Dispose();

View File

@@ -70,6 +70,7 @@ internal static unsafe class VegetationPreparation
var catalog=VegetationCatalog.Load(Path.Combine(directory,"vegetation.json")); var catalog=VegetationCatalog.Load(Path.Combine(directory,"vegetation.json"));
using var program=ShaderProgram.Create(gl,ShaderSources.Load("vegetation.vert"),ShaderSources.Load("vegetation.frag")); using var program=ShaderProgram.Create(gl,ShaderSources.Load("vegetation.vert"),ShaderSources.Load("vegetation.frag"));
program.Use();program.Set("uNoise",4);program.Set("uRingCloudMap",10);program.Set("uRingShadowEnabled",0); program.Use();program.Set("uNoise",4);program.Set("uRingCloudMap",10);program.Set("uRingShadowEnabled",0);
program.Set("uRingCloudShapeTexture",9);program.Set("uRingCloudDetailTexture",7);
var instance=gl.GenBuffer();gl.BindBuffer(GLEnum.ArrayBuffer,instance); 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]; 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); fixed(float* p=data)gl.BufferData(GLEnum.ArrayBuffer,(nuint)(data.Length*4),p,GLEnum.StaticDraw);

View File

@@ -0,0 +1,48 @@
using System.Numerics;
using Universe.Core.Generation;
using Universe.Core.Math;
namespace Universe.App.Runtime;
internal sealed class RingTemporalHistory
{
private sealed record Pose(string Id,DVector3 BodyCamera,PlanetOrientation Orientation,Matrix4x4 ViewProjection,
DVector3 Forward,long CutVersion,double Fov,int Width,int Height);
private Pose? _previous;
private int _frame;
public bool Valid {get;private set;}
public Vector2 Jitter {get;private set;}
public Matrix4x4 CurrentToPrevious {get;private set;}=Matrix4x4.Identity;
public Vector3 PreviousCameraOffset {get;private set;}
public Matrix4x4 PreviousViewProjection {get;private set;}=Matrix4x4.Identity;
public static Vector2 Sample(int frame)
{
static float Halton(int n,int radix){float result=0,fraction=1;while(n>0){fraction/=radix;result+=fraction*(n%radix);n/=radix;}return result;}
var index=(frame&7)+1;return new(Halton(index,2)-.5f,Halton(index,3)-.5f);
}
public void Reset(){_previous=null;Valid=false;_frame=0;Jitter=Vector2.Zero;}
public Matrix4x4 Advance(FlyCamera camera,PlanetData? planet,DVector3 bodyCamera,int width,int height,float far)
{
var view=Matrix4x4.CreateLookAt(Vector3.Zero,camera.Forward.ToVector3(),camera.Up.ToVector3());
var projection=Matrix4x4.CreatePerspectiveFieldOfView((float)camera.FieldOfViewRadians,(float)width/height,.02f,far);
if(planet is null){Reset();return view*projection;}
var stableViewProjection=view*projection;
Jitter=Sample(_frame++);
projection.M31-=2*Jitter.X/width;projection.M32-=2*Jitter.Y/height;
var vp=view*projection;
var forward=planet.Orientation.ToBody(camera.Forward);
Valid=_previous is {} old&&old.Id==planet.Id&&old.CutVersion==camera.MotionHistoryVersion
&&old.Width==width&&old.Height==height&&System.Math.Abs(old.Fov-camera.FieldOfViewRadians)<.0001
&&(old.BodyCamera-bodyCamera).Length<20&&DVector3.Dot(old.Forward,forward)>.8;
if(Valid)
{
var prior=_previous!;
CurrentToPrevious=planet.Orientation.WorldToBody*prior.Orientation.BodyToWorld;
PreviousCameraOffset=prior.Orientation.ToWorld(bodyCamera-prior.BodyCamera).ToVector3();
PreviousViewProjection=prior.ViewProjection;
}
_previous=new(planet.Id,bodyCamera,planet.Orientation,stableViewProjection,forward,camera.MotionHistoryVersion,camera.FieldOfViewRadians,width,height);
return vp;
}
}

View File

@@ -85,8 +85,8 @@ public sealed class UniverseApplication : IDisposable
{ {
Title = "Universe | loading", Title = "Universe | loading",
IsVisible = !Environment.GetCommandLineArgs().Any(a => a is "--ring-world-report" or "--shadow-report" or "--lookaround-report" or "--prepare-vegetation" or "--vegetation-report" or "--simulation-report" or "--graphics-report" or "--scene-report" or "--ocean-report" or "--cloud-report" or "--terrain-report" or "--menu-report" or "--terrain-perf-report" or "--black-hole-report" or "--galaxy-journey-report" or "--star-trails-report"), IsVisible = !Environment.GetCommandLineArgs().Any(a => a is "--ring-world-report" or "--shadow-report" or "--lookaround-report" or "--prepare-vegetation" or "--vegetation-report" or "--simulation-report" or "--graphics-report" or "--scene-report" or "--ocean-report" or "--cloud-report" or "--terrain-report" or "--menu-report" or "--terrain-perf-report" or "--black-hole-report" or "--galaxy-journey-report" or "--star-trails-report"),
Size = Environment.GetCommandLineArgs().Contains("--vegetation-report")?new Vector2D<int>(2560,1440):new Vector2D<int>(1440, 900), Size = Environment.GetCommandLineArgs().Any(a => a is "--vegetation-report" or "--report-1440p")?new Vector2D<int>(2560,1440):new Vector2D<int>(1440, 900),
WindowBorder = Environment.GetCommandLineArgs().Contains("--vegetation-report")?WindowBorder.Hidden:WindowBorder.Resizable, WindowBorder = Environment.GetCommandLineArgs().Any(a => a is "--vegetation-report" or "--report-1440p")?WindowBorder.Hidden:WindowBorder.Resizable,
VSync = true, VSync = true,
PreferredDepthBufferBits = 24, PreferredDepthBufferBits = 24,
API = new GraphicsAPI(ContextAPI.OpenGL, new APIVersion(3, 3)) API = new GraphicsAPI(ContextAPI.OpenGL, new APIVersion(3, 3))

View File

@@ -0,0 +1,25 @@
namespace Universe.Core.Generation;
public sealed record RingCloudDetailSettings
{
public double WeatherScaleMeters {get;init;}=90000;
public double WeatherVariation {get;init;}=.75;
public bool NearEnabled {get;init;}=true;
public double NearBaseAltitudeMeters {get;init;}=7200;
public double NearThicknessMeters {get;init;}=700;
public double NearScaleMeters {get;init;}=3200;
public double NearDetailScaleMeters {get;init;}=240;
public double NearCoverage {get;init;}=.3;
public double NearDensity {get;init;}=.24;
public double NearDistanceMeters {get;init;}=100000;
public void Validate(double atmosphere)
{
static bool Range(double n,double a,double b)=>double.IsFinite(n)&&n>=a&&n<=b;
if(!Range(WeatherScaleMeters,10000,1000000)||!Range(WeatherVariation,0,1)
||!Range(NearBaseAltitudeMeters,100,100000)||!Range(NearThicknessMeters,100,10000)
||!Range(NearScaleMeters,1000,100000)||!Range(NearDetailScaleMeters,30,NearScaleMeters)
||!Range(NearCoverage,0,1)||!Range(NearDensity,0,2)||!Range(NearDistanceMeters,10000,300000)
||NearEnabled&&NearBaseAltitudeMeters+NearThicknessMeters>atmosphere)
throw new InvalidDataException("Invalid ringWorld.cloudDetail weather scale, near cloud layer, density or distance.");
}
}

View File

@@ -0,0 +1,72 @@
using Universe.Core.Math;
namespace Universe.Core.Generation;
public sealed record RingMeadowSettings
{
public bool Enabled { get; init; } = true;
public double DistanceMeters { get; init; } = 48;
public double SpacingMeters { get; init; } = .35;
public double HeightMeters { get; init; } = .28;
public void Validate()
{
if (!double.IsFinite(DistanceMeters) || DistanceMeters is <8 or >80
|| !double.IsFinite(SpacingMeters) || SpacingMeters is <.3 or >2
|| !double.IsFinite(HeightMeters) || HeightMeters is <.05 or >1)
throw new InvalidDataException("Invalid ring meadow distance, spacing or height.");
}
}
/// <summary>Small immutable, body-local grass tiles. Terrain samples are shared on a 2 m grid.</summary>
public sealed record RingMeadowTile(DVector3 Origin, float[] Instances)
{
public const double Size = 12;
public static int Columns(PlanetData planet) => (int)System.Math.Ceiling(System.Math.Tau * planet.RadiusMeters / Size);
public static RingMeadowTile Build(PlanetData planet, int along, int across, RingMeadowSettings settings, CancellationToken token = default)
{
var columns = Columns(planet); var arc = System.Math.Tau * planet.RadiusMeters / columns;
var origin = RingWorldGeometry.Point(planet, (along + .5) / columns * System.Math.Tau, (across + .5) * Size);
const int grid = 6;
var heights = new double[grid+1,grid+1];
for (var y=0;y<=grid;y++) for(var x=0;x<=grid;x++)
{
var p = new DVector3(System.Math.Cos((along+x/(double)grid)/columns*System.Math.Tau)*planet.RadiusMeters,
(across+y/(double)grid)*Size,System.Math.Sin((along+x/(double)grid)/columns*System.Math.Tau)*planet.RadiusMeters);
heights[x,y] = RingWorldGeometry.Elevation(planet,p);
}
var data = new List<float>(); var side = (int)System.Math.Ceiling(Size/settings.SpacingMeters);
var seed = DeterministicRandom.Mix(planet.Seed ^ (ulong)(long)along*0x9E3779B97F4A7C15UL ^ (ulong)(long)across*0xBF58476D1CE4E5B9UL);
var random = new DeterministicRandom(seed);
for(var y=0;y<side;y++) for(var x=0;x<side;x++)
{
token.ThrowIfCancellationRequested();
var u=(x+.03+random.NextDouble()*.94)/side; var v=(y+.03+random.NextDouble()*.94)/side;
var angle=(along+u)/columns*System.Math.Tau; var width=(across+v)*Size;
if(System.Math.Abs(width)>planet.RingWorld!.WidthMeters*.5-2)continue;
var radial=new DVector3(System.Math.Cos(angle),0,System.Math.Sin(angle));
var direction=RingWorldGeometry.TextureDirection(planet,radial*planet.RadiusMeters+new DVector3(0,width,0));
if(planet.OceanMask?.Sample(direction)>.2)continue;
var color=planet.SurfaceTexture?.SampleColor(direction)??new System.Numerics.Vector3(45,80,35);
if(color.Y<8 || color.Y<color.Z*1.12 || color.Y<color.X*.85 || color.X>185)continue;
var patch=ProceduralNoise.Sample((radial*planet.RadiusMeters+new DVector3(0,width,0))*.18+ProceduralNoise.SeedOffset(planet.Seed));
// Continuous metre-scale patches cross tile boundaries without a visible grid.
if(random.NextDouble()>.82+patch*.22)continue;
var gx=System.Math.Min(grid-1,(int)(u*grid));var gy=System.Math.Min(grid-1,(int)(v*grid));
var fx=u*grid-gx;var fy=v*grid-gy;
var a=heights[gx,gy]*(1-fx)+heights[gx+1,gy]*fx;
var b=heights[gx,gy+1]*(1-fx)+heights[gx+1,gy+1]*fx;
var h=a*(1-fy)+b*fy;
if(planet.Ocean is {Enabled:true} ocean&&h<ocean.SeaLevelMeters+.5)continue;
var du=((heights[gx+1,gy]-heights[gx,gy])*(1-fy)+(heights[gx+1,gy+1]-heights[gx,gy+1])*fy)/(arc/grid);
var dv=(b-a)/(Size/grid);
if(du*du+dv*dv>.7)continue;
var tangent=new DVector3(-radial.Z,0,radial.X);
var normal=(-radial-tangent*du-new DVector3(0,dv,0)).Normalized();
var p=radial*(planet.RadiusMeters-h+.025)+new DVector3(0,width,0)-origin;
data.AddRange([(float)p.X,(float)p.Y,(float)p.Z,(float)normal.X,(float)normal.Y,(float)normal.Z,
(float)random.NextDouble(),(float)(settings.HeightMeters*(.3+random.NextDouble()*1.2)*(1+patch*.45)),
(float)System.Math.Clamp((color.X/color.Y-.6)*1.8+patch*.25,0,1)]);
}
return new(origin,data.ToArray());
}
}

View File

@@ -0,0 +1,13 @@
namespace Universe.Core.Generation;
public sealed record RingSurfaceRenderingSettings
{
public bool TemporalAntialiasing {get;init;}=true;
public bool ContactOcclusion {get;init;}=true;
public double ContactStrength {get;init;}=.45;
public void Validate()
{
if(!double.IsFinite(ContactStrength)||ContactStrength is <0 or >1)
throw new InvalidDataException("ringWorld.rendering.contactStrength must be between zero and one.");
}
}

View File

@@ -12,6 +12,9 @@ public sealed record RingWorldSettings
public double WallThicknessMeters { get; init; } = 5_000; public double WallThicknessMeters { get; init; } = 5_000;
public double GravityMetersPerSecondSquared { get; init; } = 9.81; public double GravityMetersPerSecondSquared { get; init; } = 9.81;
public RingWorldSurfaceSettings? ProceduralSurface { get; init; } public RingWorldSurfaceSettings? ProceduralSurface { get; init; }
public RingMeadowSettings? Meadow { get; init; }
public RingSurfaceRenderingSettings Rendering {get;init;}=new();
public RingCloudDetailSettings CloudDetail {get;init;}=new();
public double RadiusMeters => DiameterMeters * .5; public double RadiusMeters => DiameterMeters * .5;
public double BoundingRadiusMeters => System.Math.Sqrt(System.Math.Pow(RadiusMeters + ThicknessMeters, 2) public double BoundingRadiusMeters => System.Math.Sqrt(System.Math.Pow(RadiusMeters + ThicknessMeters, 2)
+ System.Math.Pow(WidthMeters * .5 + WallThicknessMeters, 2)); + System.Math.Pow(WidthMeters * .5 + WallThicknessMeters, 2));
@@ -26,6 +29,9 @@ public sealed record RingWorldSettings
|| ocean is { Enabled: true } && (ocean.SeaLevelMeters - ocean.DepthMeters <= -ThicknessMeters || ocean.SeaLevelMeters >= atmosphere)) || ocean is { Enabled: true } && (ocean.SeaLevelMeters - ocean.DepthMeters <= -ThicknessMeters || ocean.SeaLevelMeters >= atmosphere))
throw new InvalidDataException("Invalid ringWorld: positive finite dimensions/gravity required; radiusMeters must equal diameterMeters/2, and terrain/ocean must fit between the hull and atmospheric rim."); throw new InvalidDataException("Invalid ringWorld: positive finite dimensions/gravity required; radiusMeters must equal diameterMeters/2, and terrain/ocean must fit between the hull and atmospheric rim.");
ProceduralSurface?.Validate(); ProceduralSurface?.Validate();
Meadow?.Validate();
Rendering.Validate();
CloudDetail.Validate(atmosphere);
} }
} }

View File

@@ -66,7 +66,8 @@ public sealed partial class VegetationTileGenerator
var eligible = elevation >= plant.AltitudeMeters[0] && elevation <= plant.AltitudeMeters[1] var eligible = elevation >= plant.AltitudeMeters[0] && elevation <= plant.AltitudeMeters[1]
&& latitude >= plant.AbsoluteLatitudeDegrees[0] && latitude <= plant.AbsoluteLatitudeDegrees[1] && latitude >= plant.AbsoluteLatitudeDegrees[0] && latitude <= plant.AbsoluteLatitudeDegrees[1]
&& DVector3.Dot(normal, up) >= System.Math.Cos(plant.MaxSlopeDegrees * System.Math.PI / 180); && DVector3.Dot(normal, up) >= System.Math.Cos(plant.MaxSlopeDegrees * System.Math.PI / 180);
weights[i] = eligible ? VegetationCatalog.Match(color, plant.MatchColorRgb, plant.ColorTolerancePercent) * plant.SpawnWeight : 0; var groundWeight = key.Layer == 2 ? plant.Type == VegetationType.Grass ? 3.0 : .35 : 1.0;
weights[i] = eligible ? VegetationCatalog.Match(color, plant.MatchColorRgb, plant.ColorTolerancePercent) * plant.SpawnWeight * groundWeight : 0;
total += weights[i]; total += weights[i];
} }
if (total <= 0) continue; if (total <= 0) continue;

View File

@@ -0,0 +1,58 @@
using System.Numerics;
using Universe.App.Runtime;
using Universe.Core.Generation;
using Universe.Core.Math;
using Universe.Core.World;
namespace Universe.Tests;
public sealed class RingTemporalHistoryTests
{
private const float Far=1e12f;
private static PlanetData Ring(PlanetOrientation orientation)=>new(0,"Ring",DVector3.Zero,2_500_000,0,220_000,
Vector3.One,Vector3.UnitZ,0,0,0,110){Id="ring-world",RingWorld=new(),Orientation=orientation};
private static FlyCamera Camera()=>new(UniversePosition.AtCellCenter(new(0,0,0),1e10),1e10);
[Fact]
public void JitteredGeometryAndRayReconstructionUseTheSameSubpixelPosition()
{
var camera=Camera();camera.SetPlayerPose(camera.Position,new(0,0,-1),new(0,1,0));
var ring=Ring(PlanetOrientation.Identity);var history=new RingTemporalHistory();
const int width=2560,height=1440;
for(var frame=0;frame<16;frame++)
{
var vp=history.Advance(camera,ring,new(2_499_998,0,0),width,height,Far);
var point=new Vector4(1.4f,-.7f,-12,1);var clip=Vector4.Transform(point,vp);
var uv=new Vector2(clip.X/clip.W,clip.Y/clip.W)*.5f+new Vector2(.5f);
var xy=(uv-new Vector2(history.Jitter.X/width,history.Jitter.Y/height))*2-Vector2.One;
var tan=(float)System.Math.Tan(camera.FieldOfViewRadians*.5);
var ray=Vector3.Normalize(new(xy.X*tan*width/height,xy.Y*tan,-1));
Assert.InRange(Vector3.Distance(ray,Vector3.Normalize(new(point.X,point.Y,point.Z))),0,1e-6);
}
}
[Fact]
public void ReprojectionTracksRingRotationAndTranslationAtUniverseScale()
{
var camera=Camera();var history=new RingTemporalHistory();
var prior=Ring(new(35,73));var oldCamera=new DVector3(2_499_998,12345,0);
camera.SetPlayerPose(camera.Position,prior.Orientation.ToWorld(new(0,0,1)),prior.Orientation.ToWorld(new(-1,0,0)));
history.Advance(camera,prior,oldCamera,2560,1440,Far);
var next=Ring(new(35,73.001));var newCamera=oldCamera+new DVector3(0,.1,.2);
camera.SetPlayerPose(camera.Position.Add(new(100000,-4000,200),camera.CellSizeMeters),
next.Orientation.ToWorld(new(0,0,1)),next.Orientation.ToWorld(new(-1,0,0)));
history.Advance(camera,next,newCamera,2560,1440,Far);
Assert.True(history.Valid);
var bodyPoint=newCamera+new DVector3(-.2,3,12);
var current=next.Orientation.ToWorld(bodyPoint-newCamera).ToVector3();
var reprojected=Vector3.TransformNormal(current,history.CurrentToPrevious)+history.PreviousCameraOffset;
var expected=prior.Orientation.ToWorld(bodyPoint-oldCamera).ToVector3();
Assert.InRange(Vector3.Distance(reprojected,expected),0,1e-5);
camera.SetPlayerPose(camera.Position,camera.Forward,camera.Up,true);
history.Advance(camera,next,newCamera,2560,1440,Far);Assert.False(history.Valid);
history.Advance(camera,next,newCamera,2560,1440,Far);Assert.True(history.Valid);
history.Advance(camera,next,newCamera,1440,900,Far);Assert.False(history.Valid);
history.Advance(camera,next,newCamera+new DVector3(0,1000,0),1440,900,Far);Assert.False(history.Valid);
history.Advance(camera,null,newCamera,1440,900,Far);Assert.Equal(Vector2.Zero,history.Jitter);
}
}

View File

@@ -88,6 +88,9 @@ public sealed class RingWorldTests
Assert.Throws<InvalidDataException>(() => settings.Validate(1, 220_000, terrain, null)); Assert.Throws<InvalidDataException>(() => settings.Validate(1, 220_000, terrain, null));
Assert.Throws<InvalidDataException>(() => settings.Validate(2_500_000, 2_500_000, terrain, null)); Assert.Throws<InvalidDataException>(() => settings.Validate(2_500_000, 2_500_000, terrain, null));
Assert.Throws<InvalidDataException>(() => settings.Validate(2_500_000, 220_000, terrain, new() { DepthMeters = 60_000 })); Assert.Throws<InvalidDataException>(() => settings.Validate(2_500_000, 220_000, terrain, new() { DepthMeters = 60_000 }));
Assert.Throws<InvalidDataException>(() => (settings with {CloudDetail=new(){NearBaseAltitudeMeters=219000,NearThicknessMeters=2000}}).Validate(2_500_000,220_000,terrain,null));
Assert.Throws<InvalidDataException>(() => (settings with {CloudDetail=new(){NearDistanceMeters=double.NaN}}).Validate(2_500_000,220_000,terrain,null));
Assert.Throws<InvalidDataException>(() => (settings with {CloudDetail=new(){WeatherVariation=2}}).Validate(2_500_000,220_000,terrain,null));
} }
[Fact] [Fact]
@@ -216,6 +219,33 @@ public sealed class RingWorldTests
Assert.True(camera.Position.RelativeTo(body, Cell).Length > planet.MaximumTerrainRadiusMeters * 2); Assert.True(camera.Position.RelativeTo(body, Cell).Length > planet.MaximumTerrainRadiusMeters * 2);
} }
[Fact]
public void MeadowWrapsWithTheRingAndExcludesWaterAndBareBiomes()
{
var green=new RasterImage(2,2,Enumerable.Repeat(new byte[]{45,80,35,255},4).SelectMany(p=>p).ToArray());
var planet=Ring() with {SurfaceTexture=green};var settings=new RingMeadowSettings();
foreach(var column in new[]{0,RingMeadowTile.Columns(planet)-1})
{
var tile=RingMeadowTile.Build(planet,column,0,settings);
Assert.NotEmpty(tile.Instances);
Assert.Equal(tile.Instances,RingMeadowTile.Build(planet,column,0,settings).Instances);
Assert.All(tile.Instances,n=>Assert.True(float.IsFinite(n)));
for(var i=0;i<tile.Instances.Length;i+=9)
{
var point=tile.Origin+new DVector3(tile.Instances[i],tile.Instances[i+1],tile.Instances[i+2]);
var normal=new DVector3(tile.Instances[i+3],tile.Instances[i+4],tile.Instances[i+5]);
Assert.InRange(RingWorldGeometry.Clearance(planet,point),-.026,-.024);
Assert.True(DVector3.Dot(normal,-RingWorldGeometry.Radial(point))>.9999);
}
}
var wet=planet with {OceanMask=new(2,2,[1,1,1,1])};
Assert.Empty(RingMeadowTile.Build(wet,0,0,settings).Instances);
var sand=planet with {SurfaceTexture=new(1,1,[205,181,120,255])};
Assert.Empty(RingMeadowTile.Build(sand,0,0,settings).Instances);
Assert.Throws<InvalidDataException>(()=>(settings with {SpacingMeters=double.NaN}).Validate());
Assert.Throws<InvalidDataException>(()=>(settings with {DistanceMeters=1000}).Validate());
}
[Fact] [Fact]
public void VegetationIsDeterministicFacesInwardAndDoesNotGrowInWater() public void VegetationIsDeterministicFacesInwardAndDoesNotGrowInWater()
{ {