Compare commits
4 Commits
c4a5a4b4ed
...
cb5070dc1c
| Author | SHA1 | Date | |
|---|---|---|---|
| cb5070dc1c | |||
| a7207086b2 | |||
| 9d44d14cc9 | |||
| 8274a20001 |
BIN
Content/Galaxies/milky-way.png
Normal file
BIN
Content/Galaxies/milky-way.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 182 KiB |
BIN
Content/Nebulae/hourglass.jpg
Normal file
BIN
Content/Nebulae/hourglass.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
25
KnownGalaxies.json
Normal file
25
KnownGalaxies.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"galaxies": [
|
||||||
|
{
|
||||||
|
"id": "milky-way",
|
||||||
|
"name": "Vintergatan",
|
||||||
|
"region": [0, 0, 0],
|
||||||
|
"centerOffsetLightYears": [-26000, 0, 0],
|
||||||
|
"radiusLightYears": 50000,
|
||||||
|
"thicknessLightYears": 1800,
|
||||||
|
"axialTiltDegrees": 0,
|
||||||
|
"rotationDegrees": 0,
|
||||||
|
"spiralArms": 4,
|
||||||
|
"densityGamma": 0.7,
|
||||||
|
"texture": "Content/Galaxies/milky-way.png",
|
||||||
|
"nebulae": [
|
||||||
|
{
|
||||||
|
"id": "milky-way:hourglass",
|
||||||
|
"positionLightYears": [26000, 0, 300],
|
||||||
|
"radiusLightYears": 80,
|
||||||
|
"texture": "Content/Nebulae/hourglass.jpg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -47,13 +47,41 @@ Tryck **Q** för att flyga mjukt till den närmaste planeten från kamerans nuva
|
|||||||
- `systems`: lägg till fler system med eget `id`, `name`, `sector`, `centerOffsetMeters`, `stars` och `planets`. Två kända system får inte ligga i samma cell.
|
- `systems`: lägg till fler system med eget `id`, `name`, `sector`, `centerOffsetMeters`, `stars` och `planets`. Två kända system får inte ligga i samma cell.
|
||||||
- `stars`: `id`, `name`, `positionMeters`, `radiusMeters`, `luminositySolar`, linjär RGB-`color` och valfri `seed`. En eller två stjärnor stöds.
|
- `stars`: `id`, `name`, `positionMeters`, `radiusMeters`, `luminositySolar`, linjär RGB-`color` och valfri `seed`. En eller två stjärnor stöds.
|
||||||
- `planets`: `id`, `name`, `description`, `hostStarId`, `radiusMeters`, `seed`, `axialTiltDegrees`, `rotationDegrees`, `orbit`, `surfaceType`, `surfaceColor`, valfri `surfaceTexture`, `terrain`, `atmosphere`, valfria `clouds`, `ocean`, `rings` och `moons`.
|
- `planets`: `id`, `name`, `description`, `hostStarId`, `radiusMeters`, `seed`, `axialTiltDegrees`, `rotationDegrees`, `orbit`, `surfaceType`, `surfaceColor`, valfri `surfaceTexture`, `terrain`, `atmosphere`, valfria `clouds`, `ocean`, `rings` och `moons`.
|
||||||
- `orbit`: `semiMajorAxisAu`, `eccentricity`, `phaseDegrees`, `inclinationDegrees`, `ascendingNodeDegrees`, `periapsisDegrees`. Banorna är statiska elliptiska ögonblicksbilder. En satellits bana är relativ till moderplanetens centrum.
|
- `orbit`: `semiMajorAxisAu`, `eccentricity`, `meanAnomalyDegrees`, `inclinationDegrees`, `ascendingNodeDegrees`, `periapsisDegrees`, valfri `periodDays`. Banorna beräknas analytiskt från systemets `epochUtc`. En satellits bana är relativ till den rörliga moderplanetens centrum. Äldre `phaseDegrees` stöds som sann anomali vid epoken om `meanAnomalyDegrees` saknas.
|
||||||
- `surfaceType`: `rocky`, `terrestrial`, `cloudCovered`, `gasGiant` eller `iceGiant`. Gas- och isjättarna har molnband, inte berg. `cloudBandFrequency` och `stormStrength` styr bandtäthet och en större storm.
|
- `surfaceType`: `rocky`, `terrestrial`, `cloudCovered`, `gasGiant` eller `iceGiant`. Gas- och isjättarna har molnband, inte berg. `cloudBandFrequency` och `stormStrength` styr bandtäthet och en större storm.
|
||||||
- `atmosphere`: `heightMeters`, `density`, RGB-`color`, `composition`. Sammansättningen är beskrivande metadata; `density`, höjd och färg styr grafiken.
|
- `atmosphere`: `heightMeters`, `density`, RGB-`color`, `composition`. Sammansättningen är beskrivande metadata; `density`, höjd och färg styr grafiken.
|
||||||
- `rings`: `innerRadiusMeters`, `outerRadiusMeters`, RGB-`color`, `opacity` samt valfria `gapInnerRadiusMeters` och `gapOuterRadiusMeters`. Ringarnas plan följer planetens axellutning och skuggar planeten.
|
- `rings`: `innerRadiusMeters`, `outerRadiusMeters`, RGB-`color`, `opacity` samt valfria `gapInnerRadiusMeters` och `gapOuterRadiusMeters`. Ringarnas plan följer planetens axellutning och skuggar planeten.
|
||||||
|
|
||||||
Alla längder anges i **meter**, utom banornas halvstoraxel som anges i **AU**. Färger har tre komponenter i intervallet 0–1. `seed` är ett heltal; utelämnad seed härleds stabilt från id. Felaktiga id:n, saknade kartor, orimliga värden och objekt utanför sin gridcell ger ett tydligt startfel.
|
Alla längder anges i **meter**, utom banornas halvstoraxel som anges i **AU**. Färger har tre komponenter i intervallet 0–1. `seed` är ett heltal; utelämnad seed härleds stabilt från id. Felaktiga id:n, saknade kartor, orimliga värden och objekt utanför sin gridcell ger ett tydligt startfel.
|
||||||
|
|
||||||
|
## Tid, banor och rotation
|
||||||
|
|
||||||
|
Varje system har `epochUtc` (standard `2000-01-01T12:00:00Z`). Banfaser och rotationsfaser är relativa till denna tidpunkt. `simulation.startUtc` och `simulation.timeScale` i projektets `config.json` väljer startdatum och tidsfaktor; standard är 1× verklig tid. Menyn **P → DEBUG ON** har ett tidsreglage 0–100×. Menyn och benchmarkrutterna pausar världstiden, och 0× pausar bara simulationen, så att kameran fortfarande kan användas.
|
||||||
|
|
||||||
|
| JSON-fält | Betydelse |
|
||||||
|
| --- | --- |
|
||||||
|
| Stjärnans `massKg` | Massa i kg; används för beräkning av omloppstid om perioden saknas. Standard en solmassa. |
|
||||||
|
| Planetens `massKg` | Massa i kg; används för tvåkroppsperiod och satellitbanor. Standard en jordmassa. |
|
||||||
|
| `orbit.semiMajorAxisAu` | Ellipsens halvstoraxel i AU. |
|
||||||
|
| `orbit.eccentricity` | Excentricitet från 0 till mindre än 1. |
|
||||||
|
| `orbit.periodDays` | Siderisk omloppstid i dygn om 86400 sekunder; om utelämnad används `2π √(a³ / G(M+m))`. |
|
||||||
|
| `orbit.meanAnomalyDegrees` | Medelanomali vid epoken. Har företräde framför äldre `phaseDegrees` (sann anomali). |
|
||||||
|
| `orbit.inclinationDegrees` | Banplanets lutning mot systemets XZ-plan. |
|
||||||
|
| `orbit.ascendingNodeDegrees` | Uppstigande nodens longitud. |
|
||||||
|
| `orbit.periapsisDegrees` | Periapsisargument relativt noden, inte periheliets absoluta longitud. |
|
||||||
|
| `rotationPeriodHours` | Siderisk axelrotation i timmar. Standard 24. Måste vara ändlig och skild från noll. |
|
||||||
|
| `axialTiltDegrees` | Axellutning mot systemets +Y-riktning, 0–180 grader. Värden över 90 ger retrograd rotation med positiv rotationsperiod. |
|
||||||
|
| `rotationDegrees` | Meridianens rotationsfas vid epoken. |
|
||||||
|
| `poleLongitudeDegrees` | Vridning av den lutade axelns riktning runt systemets Y-axel; standard 0. |
|
||||||
|
|
||||||
|
`rotationPeriodHours` anges som positivt belopp i vårt solsystem. Venus (177,4°) och Uranus (97,8°) har redan omvänd rotationsriktning genom axellutningen; en negativ period skulle vända dem en gång till. Negativa perioder stöds för andra konfigurationer som vill vända spinnen kring den angivna axeln. Jorden använder en siderisk dag på cirka 23 h 56 min, medan dess soldygn blir ungefär 24 timmar genom kombinationen av rotation och omlopp.
|
||||||
|
|
||||||
|
Solsystemets åtta planeter använder [JPL:s banelement vid J2000](https://ssd.jpl.nasa.gov/planets/approx_pos.html) och [sideriska perioder och massor](https://ssd.jpl.nasa.gov/planets/phys_par.html). Månen följer jorden med ungefär 27,32 dygns omlopps- och rotationstid. Proceduriella planeter får deterministiska rotationstider och omloppstider beräknade från avstånd och värdkroppens massa. Deras stjärnor har fasta lägen; dubbelstjärnornas inbördes banor integreras inte.
|
||||||
|
|
||||||
|
Modellen är en spelbar tvåkroppsapproximation med fasta banelement, inte JPL Horizons eller en fullständig N-kroppssimulering. Sekulära störningar, precession/nutation, tidvattenkrafter, relativitet och ljusets gångtid ingår inte. Jordbanan använder jord–måne-barycentrums element som approximation. UTC används direkt för epoken utan TT/TDB-korrigering. Axelfaser och polernas longituder är visuellt konfigurerade och inte IAU-kalibrerade; atmosfärens vind kan dessutom röra molnen relativt ytan. Detta gör att perioder och omloppshastigheter är astronomiskt grundade utan att ange exakta observerade lägen för ett datum.
|
||||||
|
|
||||||
|
Meshdata, terräng-LOD och material behåller planetens lokala koordinater. Rendering och kollision använder samma aktuella rotation utan att generera om terräng för varje rotationssteg. En markbunden spelare följer både omlopp och rotation, medan tangentbordsstyrning och hopp använder vanlig, oskalad bildrutetid. Fokusanimeringar följer rörliga planeter. `--simulation-report` genererar bilder av jordrotation, markvy och debugreglage i appens `graphics-report/`.
|
||||||
|
|
||||||
## Jordens heightmap och noise
|
## Jordens heightmap och noise
|
||||||
|
|
||||||
Jordens inställningar kombinerar kartan med finare terräng:
|
Jordens inställningar kombinerar kartan med finare terräng:
|
||||||
@@ -99,7 +127,31 @@ Jordens `ocean`-block aktiverar ett eget vattenpass på planetens havsnivå. Try
|
|||||||
| `deepColor`, `shallowColor`, `absorption` | Linjära RGB-färger för djup/grunt vatten och absorption per meter för rött, grönt och blått ljus |
|
| `deepColor`, `shallowColor`, `absorption` | Linjära RGB-färger för djup/grunt vatten och absorption per meter för rött, grönt och blått ljus |
|
||||||
| `foamStrength` | Skum på vågkammar och vid grunda stränder |
|
| `foamStrength` | Skum på vågkammar och vid grunda stränder |
|
||||||
|
|
||||||
Vågspektrumet består av Gerstner-komponenter som filtreras efter pixelstorleken. Ytan söks med strålar i ett lokalt koordinatsystem förankrat i dubbelprecision, så samma hav fungerar från rymden till våghöjd. Reflektionerna kombinerar en analytisk atmosfärshimmel med skärmreflektioner av synlig terräng; objekt utanför bilden använder himlen som reserv. Refraktionen använder scenens färg och djup från ett separat framebuffer. Havet simulerar ingen fluiddynamik, fartygskölvatten eller bestående skumtransport.
|
Vågspektrumet består av Gerstner-komponenter som filtreras efter pixelstorleken. Ytan söks med strålar i ett lokalt koordinatsystem förankrat i dubbelprecision, så samma hav fungerar från rymden till våghöjd. Refraktionen använder scenens färg och djup från ett separat framebuffer. Havet simulerar ingen fluiddynamik, fartygskölvatten eller bestående skumtransport.
|
||||||
|
|
||||||
|
### Skärmrymdsreflektioner på vatten
|
||||||
|
|
||||||
|
`ocean.screenSpaceReflections` styr en separat reflektionspassering efter atmosfär och moln. Den söker synlig geometri med perspektivkorrekt interpolation mot det logaritmiska djupet och förfinar träffen i sju binära steg. Där en stråle fortsätter ut mot himlen används den färdiga scenens himmel, inklusive molnkartan och stjärnorna. Havets vågnormaler och Fresnelvikt styr reflektionen; skum minskar den. Färg-mipmaps filtrerar reflektionen efter vattnets `roughness`.
|
||||||
|
|
||||||
|
| Fält | Standard | Betydelse |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `enabled` | `true` | Slår på den extra skärmreflektionen. Grundläggande himmel/molnreflektion finns även avstängd. |
|
||||||
|
| `reflectSky` | `true` | Använd bildens färdiga himmel och avlägsna objekt som reflektionskälla. |
|
||||||
|
| `strength` | `1` | Inblandning av skärmreflektionen, 0–1. `reflectionStrength` på havet styr hela reflektionsmaterialet. |
|
||||||
|
| `resolutionScale` | `0.5` | Upplösning 0,25–1. Halv upplösning med djup- och normalstyrd uppskalning är standard. |
|
||||||
|
| `maxSteps` | `48` | Högst 8–128 djupsöksteg per reflektionspixel. |
|
||||||
|
| `stridePixels` | `2` | Minsta skärmsteg i fullupplösta bildpunkter; större steg kan missa små objekt. |
|
||||||
|
| `maxDistanceMeters` | `3000` | Geometrisökningens längd, 10–50000 m. |
|
||||||
|
| `maxSurfaceDistanceMeters` | `4000` | Största kamera–vatten-avstånd där SSR används, 10–20000 m. Effekten tonas ut innan gränsen. |
|
||||||
|
| `thicknessMeters` | `1.5` | Tillåten djupmarginal vid träff, 0,01–50 m. |
|
||||||
|
| `normalBiasMeters` | `0.12` | Startförskjutning från vattenytan för att undvika självträffar, 0,01–5 m. |
|
||||||
|
| `edgeFade` | `0.07` | Tonar ut träffar nära skärmkanten; värdet är en andel av bildens bredd/höjd. |
|
||||||
|
|
||||||
|
Upplösning, raymarch, kantutfasning och uppskalning mäts tillsammans som **WATER REFLECTIONS** i debugpanelen. Från rymden används den billigare grundreflektionen. Reflektioner läggs endast på vatten och befintlig molntransmission dämpar dem när moln skymmer ytan. Vid flera hav används det närmaste havets SSR-inställningar för den lokala vyn.
|
||||||
|
|
||||||
|
SSR kan bara träffa geometri som finns i kamerans färg- och djupbild. Dolda objekt och objekt utanför bilden får därför en analytisk himmel/molnreserv, och övergången tonas ut. Det är inte RTX-strålspårning eller en fullständig reflektionskarta av omgivningen. Metoden bygger på perspektivkorrekt skärmsökning enligt [McGuire och Mara, Efficient GPU Screen-Space Ray Tracing](https://jcgt.org/published/0003/04/04/).
|
||||||
|
|
||||||
|
`--ocean-report` kör ett kontrollerat väggtest (även att mark inte ändras), sparar dag-/nattbilder med reflektionen av/på samt undervattens- och omloppsvyer. Jämförelser och GPU-tid skrivs till konsolen; bilder hamnar i appens `graphics-report/`.
|
||||||
|
|
||||||
Tekniken är inspirerad av principerna i [Crests beskrivning](https://github.com/JJarvis89/crest-oceanrender#the-tech), med en egen implementation för OpenGL 3.3. Unity-paketet ingår inte som ett beroende.
|
Tekniken är inspirerad av principerna i [Crests beskrivning](https://github.com/JJarvis89/crest-oceanrender#the-tech), med en egen implementation för OpenGL 3.3. Unity-paketet ingår inte som ett beroende.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"source": "https://nssdc.gsfc.nasa.gov/planetary/factsheet/ ; https://solarsystem.nasa.gov/planet-compare/",
|
"source": "https://ssd.jpl.nasa.gov/planets/approx_pos.html ; https://ssd.jpl.nasa.gov/planets/phys_par.html ; https://nssdc.gsfc.nasa.gov/planetary/factsheet/",
|
||||||
"notes": "Avrundade planetdata i SI-enheter och AU. Sfariska radier och statiska illustrativa banfaser, inte efemerider for dagens datum. Material och atmosfarer ar visuella modeller.",
|
"notes": "Keplerbanor med JPL J2000-element och sideriska perioder. Epok 2000-01-01 12:00 UTC (approximation av J2000 TT). Fasta tvåkroppsbanor, utan sekulära störningar, precession eller N-kroppsintegration; jordens bana approximeras med Earth/Moon-barycentrum. Rotationstid anges positivt om den följer den lutade axelns riktning: Venus och Uranus får retrograd rotation genom axialTiltDegrees > 90, inte dubbelt negativ rotationstid. RotationDegrees och poleLongitudeDegrees är justerbara visuella epokfaser, inte IAU-kalibrerade meridianer. Gasjättars rotation avser en representativ siderisk period; väder har separat vind. Radier/material/atmosfärer behåller spelbara visuella modeller.",
|
||||||
"systems": [
|
"systems": [
|
||||||
{
|
{
|
||||||
"id": "sol",
|
"id": "sol",
|
||||||
@@ -50,7 +50,8 @@
|
|||||||
"ghostStrength": 0.04,
|
"ghostStrength": 0.04,
|
||||||
"ghostSpacing": 0.42,
|
"ghostSpacing": 0.42,
|
||||||
"chromaticAberration": 0.08
|
"chromaticAberration": 0.08
|
||||||
}
|
},
|
||||||
|
"massKg": 1.98847e+30
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"planets": [
|
"planets": [
|
||||||
@@ -64,12 +65,13 @@
|
|||||||
"axialTiltDegrees": 0.034,
|
"axialTiltDegrees": 0.034,
|
||||||
"rotationDegrees": 15,
|
"rotationDegrees": 15,
|
||||||
"orbit": {
|
"orbit": {
|
||||||
"semiMajorAxisAu": 0.3871,
|
"semiMajorAxisAu": 0.38709927,
|
||||||
"eccentricity": 0.2056,
|
"eccentricity": 0.20563593,
|
||||||
"phaseDegrees": 160,
|
"inclinationDegrees": 7.00497902,
|
||||||
"inclinationDegrees": 7.0,
|
"ascendingNodeDegrees": 48.33076593,
|
||||||
"ascendingNodeDegrees": 48.3,
|
"periapsisDegrees": 29.12703035,
|
||||||
"periapsisDegrees": 29.1
|
"meanAnomalyDegrees": 174.79252722,
|
||||||
|
"periodDays": 87.96925717
|
||||||
},
|
},
|
||||||
"surfaceType": "rocky",
|
"surfaceType": "rocky",
|
||||||
"surfaceColor": [
|
"surfaceColor": [
|
||||||
@@ -90,7 +92,10 @@
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"composition": "Mycket tunn exosfär"
|
"composition": "Mycket tunn exosfär"
|
||||||
}
|
},
|
||||||
|
"rotationPeriodHours": 1407.5088,
|
||||||
|
"massKg": 3.30103e+23,
|
||||||
|
"poleLongitudeDegrees": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "venus",
|
"id": "venus",
|
||||||
@@ -102,12 +107,13 @@
|
|||||||
"axialTiltDegrees": 177.4,
|
"axialTiltDegrees": 177.4,
|
||||||
"rotationDegrees": 20,
|
"rotationDegrees": 20,
|
||||||
"orbit": {
|
"orbit": {
|
||||||
"semiMajorAxisAu": 0.7233,
|
"semiMajorAxisAu": 0.72333566,
|
||||||
"eccentricity": 0.0068,
|
"eccentricity": 0.00677672,
|
||||||
"phaseDegrees": 75,
|
"inclinationDegrees": 3.39467605,
|
||||||
"inclinationDegrees": 3.4,
|
"ascendingNodeDegrees": 76.67984255,
|
||||||
"ascendingNodeDegrees": 76.7,
|
"periapsisDegrees": 54.92262463,
|
||||||
"periapsisDegrees": 54.9
|
"meanAnomalyDegrees": 50.37663232,
|
||||||
|
"periodDays": 224.70079921
|
||||||
},
|
},
|
||||||
"surfaceType": "cloudCovered",
|
"surfaceType": "cloudCovered",
|
||||||
"surfaceColor": [
|
"surfaceColor": [
|
||||||
@@ -128,7 +134,10 @@
|
|||||||
0.22
|
0.22
|
||||||
],
|
],
|
||||||
"composition": "Koldioxid, kväve och svavelsyramoln"
|
"composition": "Koldioxid, kväve och svavelsyramoln"
|
||||||
}
|
},
|
||||||
|
"rotationPeriodHours": 5832.432,
|
||||||
|
"massKg": 4.86731e+24,
|
||||||
|
"poleLongitudeDegrees": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "earth",
|
"id": "earth",
|
||||||
@@ -140,12 +149,13 @@
|
|||||||
"axialTiltDegrees": 23.44,
|
"axialTiltDegrees": 23.44,
|
||||||
"rotationDegrees": 135,
|
"rotationDegrees": 135,
|
||||||
"orbit": {
|
"orbit": {
|
||||||
"semiMajorAxisAu": 1.0,
|
"semiMajorAxisAu": 1.00000261,
|
||||||
"eccentricity": 0.0167,
|
"eccentricity": 0.01671123,
|
||||||
"phaseDegrees": 0,
|
"inclinationDegrees": -1.531e-05,
|
||||||
"inclinationDegrees": 0,
|
|
||||||
"ascendingNodeDegrees": 0,
|
"ascendingNodeDegrees": 0,
|
||||||
"periapsisDegrees": 0
|
"periapsisDegrees": 102.93768193,
|
||||||
|
"meanAnomalyDegrees": -2.47311027,
|
||||||
|
"periodDays": 365.25635535
|
||||||
},
|
},
|
||||||
"surfaceType": "terrestrial",
|
"surfaceType": "terrestrial",
|
||||||
"surfaceColor": [
|
"surfaceColor": [
|
||||||
@@ -168,7 +178,7 @@
|
|||||||
"densityTextureStrength": 1.0,
|
"densityTextureStrength": 1.0,
|
||||||
"baseAltitudeMeters": 2100,
|
"baseAltitudeMeters": 2100,
|
||||||
"thicknessMeters": 4300,
|
"thicknessMeters": 4300,
|
||||||
"coverage": 0.40,
|
"coverage": 0.4,
|
||||||
"density": 0.85,
|
"density": 0.85,
|
||||||
"shapeScaleMeters": 18000,
|
"shapeScaleMeters": 18000,
|
||||||
"detailScaleMeters": 2400,
|
"detailScaleMeters": 2400,
|
||||||
@@ -191,6 +201,19 @@
|
|||||||
},
|
},
|
||||||
"ocean": {
|
"ocean": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
"screenSpaceReflections": {
|
||||||
|
"enabled": true,
|
||||||
|
"reflectSky": true,
|
||||||
|
"strength": 1,
|
||||||
|
"resolutionScale": 0.5,
|
||||||
|
"maxSteps": 48,
|
||||||
|
"stridePixels": 2,
|
||||||
|
"maxDistanceMeters": 3000,
|
||||||
|
"maxSurfaceDistanceMeters": 4000,
|
||||||
|
"thicknessMeters": 1.5,
|
||||||
|
"normalBiasMeters": 0.12,
|
||||||
|
"edgeFade": 0.07
|
||||||
|
},
|
||||||
"maskFromSurfaceTexture": true,
|
"maskFromSurfaceTexture": true,
|
||||||
"seaLevelMeters": 0,
|
"seaLevelMeters": 0,
|
||||||
"depthMeters": 4000,
|
"depthMeters": 4000,
|
||||||
@@ -203,17 +226,33 @@
|
|||||||
"reflectionStrength": 1.0,
|
"reflectionStrength": 1.0,
|
||||||
"refractionStrength": 0.018,
|
"refractionStrength": 0.018,
|
||||||
"foamStrength": 0.65,
|
"foamStrength": 0.65,
|
||||||
"deepColor": [0.006, 0.045, 0.075],
|
"deepColor": [
|
||||||
"shallowColor": [0.025, 0.28, 0.24],
|
0.006,
|
||||||
"absorption": [0.12, 0.045, 0.022]
|
0.045,
|
||||||
|
0.075
|
||||||
|
],
|
||||||
|
"shallowColor": [
|
||||||
|
0.025,
|
||||||
|
0.28,
|
||||||
|
0.24
|
||||||
|
],
|
||||||
|
"absorption": [
|
||||||
|
0.12,
|
||||||
|
0.045,
|
||||||
|
0.022
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"terrain": {
|
"terrain": {
|
||||||
"heightmap": "Textures/earth-height.jpg",
|
"heightmap": "Textures/earth-height.jpg",
|
||||||
"regionalHeight": {
|
"regionalHeight": {
|
||||||
"file": "Heightdata/Copernicus/alps_n46_e007.f32",
|
"file": "Heightdata/Copernicus/alps_n46_e007.f32",
|
||||||
"width": 3600, "height": 3600,
|
"width": 3600,
|
||||||
"westDegrees": 7, "southDegrees": 46,
|
"height": 3600,
|
||||||
"eastDegrees": 8, "northDegrees": 47, "fadeDegrees": 0.04
|
"westDegrees": 7,
|
||||||
|
"southDegrees": 46,
|
||||||
|
"eastDegrees": 8,
|
||||||
|
"northDegrees": 47,
|
||||||
|
"fadeDegrees": 0.04
|
||||||
},
|
},
|
||||||
"heightmapMinMeters": 0,
|
"heightmapMinMeters": 0,
|
||||||
"heightmapMaxMeters": 10848,
|
"heightmapMaxMeters": 10848,
|
||||||
@@ -250,7 +289,8 @@
|
|||||||
"phaseDegrees": 60,
|
"phaseDegrees": 60,
|
||||||
"inclinationDegrees": 5.1,
|
"inclinationDegrees": 5.1,
|
||||||
"ascendingNodeDegrees": 0,
|
"ascendingNodeDegrees": 0,
|
||||||
"periapsisDegrees": 0
|
"periapsisDegrees": 0,
|
||||||
|
"periodDays": 27.321661
|
||||||
},
|
},
|
||||||
"surfaceType": "rocky",
|
"surfaceType": "rocky",
|
||||||
"surfaceColor": [
|
"surfaceColor": [
|
||||||
@@ -271,9 +311,14 @@
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"composition": "Mycket tunn exosfär"
|
"composition": "Mycket tunn exosfär"
|
||||||
|
},
|
||||||
|
"rotationPeriodHours": 655.719864,
|
||||||
|
"massKg": 7.342e+22
|
||||||
}
|
}
|
||||||
}
|
],
|
||||||
]
|
"rotationPeriodHours": 23.93447232,
|
||||||
|
"massKg": 5.97217e+24,
|
||||||
|
"poleLongitudeDegrees": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "mars",
|
"id": "mars",
|
||||||
@@ -285,12 +330,13 @@
|
|||||||
"axialTiltDegrees": 25.2,
|
"axialTiltDegrees": 25.2,
|
||||||
"rotationDegrees": 20,
|
"rotationDegrees": 20,
|
||||||
"orbit": {
|
"orbit": {
|
||||||
"semiMajorAxisAu": 1.5237,
|
"semiMajorAxisAu": 1.52371034,
|
||||||
"eccentricity": 0.0934,
|
"eccentricity": 0.0933941,
|
||||||
"phaseDegrees": 140,
|
"inclinationDegrees": 1.84969142,
|
||||||
"inclinationDegrees": 1.8,
|
"ascendingNodeDegrees": 49.55953891,
|
||||||
"ascendingNodeDegrees": 49.6,
|
"periapsisDegrees": -73.5031685,
|
||||||
"periapsisDegrees": 286.5
|
"meanAnomalyDegrees": 19.39019754,
|
||||||
|
"periodDays": 686.9795859
|
||||||
},
|
},
|
||||||
"surfaceType": "rocky",
|
"surfaceType": "rocky",
|
||||||
"surfaceColor": [
|
"surfaceColor": [
|
||||||
@@ -311,7 +357,10 @@
|
|||||||
0.36
|
0.36
|
||||||
],
|
],
|
||||||
"composition": "Tunn koldioxidatmosfär"
|
"composition": "Tunn koldioxidatmosfär"
|
||||||
}
|
},
|
||||||
|
"rotationPeriodHours": 24.62296224,
|
||||||
|
"massKg": 6.41691e+23,
|
||||||
|
"poleLongitudeDegrees": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "jupiter",
|
"id": "jupiter",
|
||||||
@@ -323,12 +372,13 @@
|
|||||||
"axialTiltDegrees": 3.1,
|
"axialTiltDegrees": 3.1,
|
||||||
"rotationDegrees": 20,
|
"rotationDegrees": 20,
|
||||||
"orbit": {
|
"orbit": {
|
||||||
"semiMajorAxisAu": 5.2034,
|
"semiMajorAxisAu": 5.202887,
|
||||||
"eccentricity": 0.0489,
|
"eccentricity": 0.04838624,
|
||||||
"phaseDegrees": 220,
|
"inclinationDegrees": 1.30439695,
|
||||||
"inclinationDegrees": 1.3,
|
"ascendingNodeDegrees": 100.47390909,
|
||||||
"ascendingNodeDegrees": 100.5,
|
"periapsisDegrees": -85.74542926,
|
||||||
"periapsisDegrees": 273.9
|
"meanAnomalyDegrees": 19.66796068,
|
||||||
|
"periodDays": 4332.82012875
|
||||||
},
|
},
|
||||||
"surfaceType": "gasGiant",
|
"surfaceType": "gasGiant",
|
||||||
"surfaceColor": [
|
"surfaceColor": [
|
||||||
@@ -372,7 +422,10 @@
|
|||||||
],
|
],
|
||||||
"fineBandContrast": 0.16,
|
"fineBandContrast": 0.16,
|
||||||
"cloudTurbulence": 0.55
|
"cloudTurbulence": 0.55
|
||||||
}
|
},
|
||||||
|
"rotationPeriodHours": 9.92496,
|
||||||
|
"massKg": 1.898125e+27,
|
||||||
|
"poleLongitudeDegrees": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "saturn",
|
"id": "saturn",
|
||||||
@@ -384,12 +437,13 @@
|
|||||||
"axialTiltDegrees": 26.7,
|
"axialTiltDegrees": 26.7,
|
||||||
"rotationDegrees": 0,
|
"rotationDegrees": 0,
|
||||||
"orbit": {
|
"orbit": {
|
||||||
"semiMajorAxisAu": 9.5371,
|
"semiMajorAxisAu": 9.53667594,
|
||||||
"eccentricity": 0.052,
|
"eccentricity": 0.05386179,
|
||||||
"phaseDegrees": 290,
|
"inclinationDegrees": 2.48599187,
|
||||||
"inclinationDegrees": 2.5,
|
"ascendingNodeDegrees": 113.66242448,
|
||||||
"ascendingNodeDegrees": 113.7,
|
"periapsisDegrees": -21.06354617,
|
||||||
"periapsisDegrees": 339.4
|
"meanAnomalyDegrees": -42.64463408,
|
||||||
|
"periodDays": 10755.6986445
|
||||||
},
|
},
|
||||||
"surfaceType": "gasGiant",
|
"surfaceType": "gasGiant",
|
||||||
"surfaceColor": [
|
"surfaceColor": [
|
||||||
@@ -589,7 +643,10 @@
|
|||||||
0.76,
|
0.76,
|
||||||
0.69
|
0.69
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"rotationPeriodHours": 10.65624,
|
||||||
|
"massKg": 5.68317e+26,
|
||||||
|
"poleLongitudeDegrees": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "uranus",
|
"id": "uranus",
|
||||||
@@ -601,12 +658,13 @@
|
|||||||
"axialTiltDegrees": 97.8,
|
"axialTiltDegrees": 97.8,
|
||||||
"rotationDegrees": 0,
|
"rotationDegrees": 0,
|
||||||
"orbit": {
|
"orbit": {
|
||||||
"semiMajorAxisAu": 19.1913,
|
"semiMajorAxisAu": 19.18916464,
|
||||||
"eccentricity": 0.0472,
|
"eccentricity": 0.04725744,
|
||||||
"phaseDegrees": 45,
|
"inclinationDegrees": 0.77263783,
|
||||||
"inclinationDegrees": 0.8,
|
"ascendingNodeDegrees": 74.01692503,
|
||||||
"ascendingNodeDegrees": 74.0,
|
"periapsisDegrees": 96.93735127,
|
||||||
"periapsisDegrees": 97.0
|
"meanAnomalyDegrees": 142.28382821,
|
||||||
|
"periodDays": 30687.1530015
|
||||||
},
|
},
|
||||||
"surfaceType": "iceGiant",
|
"surfaceType": "iceGiant",
|
||||||
"surfaceColor": [
|
"surfaceColor": [
|
||||||
@@ -650,7 +708,10 @@
|
|||||||
],
|
],
|
||||||
"fineBandContrast": 0.025,
|
"fineBandContrast": 0.025,
|
||||||
"cloudTurbulence": 0.04
|
"cloudTurbulence": 0.04
|
||||||
}
|
},
|
||||||
|
"rotationPeriodHours": 17.23992,
|
||||||
|
"massKg": 8.68099e+25,
|
||||||
|
"poleLongitudeDegrees": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "neptune",
|
"id": "neptune",
|
||||||
@@ -662,12 +723,13 @@
|
|||||||
"axialTiltDegrees": 28.3,
|
"axialTiltDegrees": 28.3,
|
||||||
"rotationDegrees": 0,
|
"rotationDegrees": 0,
|
||||||
"orbit": {
|
"orbit": {
|
||||||
"semiMajorAxisAu": 30.069,
|
"semiMajorAxisAu": 30.06992276,
|
||||||
"eccentricity": 0.0086,
|
"eccentricity": 0.00859048,
|
||||||
"phaseDegrees": 105,
|
"inclinationDegrees": 1.77004347,
|
||||||
"inclinationDegrees": 1.8,
|
"ascendingNodeDegrees": 131.78422574,
|
||||||
"ascendingNodeDegrees": 131.8,
|
"periapsisDegrees": -86.81946347,
|
||||||
"periapsisDegrees": 273.2
|
"meanAnomalyDegrees": -100.08479196,
|
||||||
|
"periodDays": 60190.02963
|
||||||
},
|
},
|
||||||
"surfaceType": "iceGiant",
|
"surfaceType": "iceGiant",
|
||||||
"surfaceColor": [
|
"surfaceColor": [
|
||||||
@@ -711,9 +773,13 @@
|
|||||||
],
|
],
|
||||||
"fineBandContrast": 0.05,
|
"fineBandContrast": 0.05,
|
||||||
"cloudTurbulence": 0.18
|
"cloudTurbulence": 0.18
|
||||||
|
},
|
||||||
|
"rotationPeriodHours": 16.11,
|
||||||
|
"massKg": 1.024092e+26,
|
||||||
|
"poleLongitudeDegrees": 0
|
||||||
}
|
}
|
||||||
}
|
],
|
||||||
]
|
"epochUtc": "2000-01-01T12:00:00Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
65
README.md
65
README.md
@@ -17,10 +17,11 @@ Solen och de åtta planeterna definieras i [KnownSolarSystem/solar-systems.json]
|
|||||||
| Kontroll | Funktion |
|
| Kontroll | Funktion |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| WASD + mus | Fri flygning och riktning |
|
| WASD + mus | Fri flygning och riktning |
|
||||||
| P | Öppna/stäng menyn. Välj Play, Free Camera, Debug, Benchmark Earth, Benchmark Unknown eller Quit med musen eller piltangenter och Enter |
|
| P | Öppna/stäng menyn. Välj Play, Free Camera, Debug, Benchmark Earth, Benchmark Unknown, Galaxy Journey eller Quit med musen eller piltangenter och Enter |
|
||||||
| Space / Ctrl | Upp / ner relativt kameran |
|
| Space / Ctrl | Upp / ner relativt kameran |
|
||||||
| Mushjul | Logaritmisk hastighet, 0,1 m/s till 1 000 000 c |
|
| Mushjul | Logaritmisk hastighet, från 0,1 m/s upp till galaktisk FTL-fart (10^15 c) |
|
||||||
| Q | Mjuk flygning till närmaste planet |
|
| Q | Mjuk flygning till närmaste planet |
|
||||||
|
| E | Mjuk flygning till närmaste svarta hål, även från en planets markyta |
|
||||||
| 1–9 | Flyg till planeten på motsvarande plats i `startup.planetHotkeys` i `config.json` |
|
| 1–9 | Flyg till planeten på motsvarande plats i `startup.planetHotkeys` i `config.json` |
|
||||||
| F2 | Mjuk flygning till en bergsvy nära ytan; hastigheten sänks till 1 200 m/s |
|
| F2 | Mjuk flygning till en bergsvy nära ytan; hastigheten sänks till 1 200 m/s |
|
||||||
| F3 | Närbild av närmaste stjärna |
|
| F3 | Närbild av närmaste stjärna |
|
||||||
@@ -29,6 +30,8 @@ Solen och de åtta planeterna definieras i [KnownSolarSystem/solar-systems.json]
|
|||||||
| F5 | Ladda om shaders; Debug läser direkt från shaderfilerna |
|
| F5 | Ladda om shaders; Debug läser direkt från shaderfilerna |
|
||||||
| F6 | Flyg till en solbelyst havsvy nära vattenytan; hastigheten sänks till 10 m/s |
|
| F6 | Flyg till en solbelyst havsvy nära vattenytan; hastigheten sänks till 10 m/s |
|
||||||
| F7 | Flyg till en vy ovanför närmaste molnlager; hastigheten sänks till 100 m/s |
|
| F7 | Flyg till en vy ovanför närmaste molnlager; hastigheten sänks till 100 m/s |
|
||||||
|
| F8 | Flyg ut till en översiktsvy av den aktuella galaxen |
|
||||||
|
| F9 | Flyg till närmaste nebulosa i galaxen |
|
||||||
| Esc | Stäng pausmenyn, eller avsluta när menyn är stängd |
|
| Esc | Stäng pausmenyn, eller avsluta när menyn är stängd |
|
||||||
|
|
||||||
**Play** växlar till förstapersonsläge. WASD går längs marken, musen ser sig omkring, Space hoppar och Ctrl hukar. Spelaren dras mot närmaste fasta planet och orienteras efter dess lokala gravitation. Från stora avstånd används en snabbare inflygning för att landningen ska vara spelbar. **Free Camera** återställer fri flygning. Pausmenyn stoppar rörelsen medan den är öppen. Gasjättar har ingen fast mark och väljs därför inte som landningsmål.
|
**Play** växlar till förstapersonsläge. WASD går längs marken, musen ser sig omkring, Space hoppar och Ctrl hukar. Spelaren dras mot närmaste fasta planet och orienteras efter dess lokala gravitation. Från stora avstånd används en snabbare inflygning för att landningen ska vara spelbar. **Free Camera** återställer fri flygning. Pausmenyn stoppar rörelsen medan den är öppen. Gasjättar har ingen fast mark och väljs därför inte som landningsmål.
|
||||||
@@ -54,7 +57,63 @@ Välj **Debug** i P-menyn för en panel till vänster med asynkron GPU-tid för
|
|||||||
- Jorden har ett separat sfäriskt hav med animerade vågor, Fresnelreflektion, solljusglitter, skum, refraktion och djupberoende ljusabsorption. Havsbotten och kustmasken används av både terrängen och kollisionen. `ocean` i planetens JSON styr vattenmaterialet; F6 visar havet nära ytan.
|
- Jorden har ett separat sfäriskt hav med animerade vågor, Fresnelreflektion, solljusglitter, skum, refraktion och djupberoende ljusabsorption. Havsbotten och kustmasken används av både terrängen och kollisionen. `ocean` i planetens JSON styr vattenmaterialet; F6 visar havet nära ytan.
|
||||||
- Sfäriska 3D-moln använder fraktal noise och Worley-noise, vind, självskuggning och ljus från systemets solar. Valfria equirektangulära gråskalekartor styr den geografiska täckningen; jordens `Content/Clouds/earth-clouds.png` används i solsystemets JSON, där vitt betyder moln och svart klart. Molnen skuggar mark och hav, dämpar solglöden och speglas i vattnet. `clouds` i planetens JSON styr karta, utseende och kvalitet.
|
- Sfäriska 3D-moln använder fraktal noise och Worley-noise, vind, självskuggning och ljus från systemets solar. Valfria equirektangulära gråskalekartor styr den geografiska täckningen; jordens `Content/Clouds/earth-clouds.png` används i solsystemets JSON, där vitt betyder moln och svart klart. Molnen skuggar mark och hav, dämpar solglöden och speglas i vattnet. `clouds` i planetens JSON styr karta, utseende och kvalitet.
|
||||||
|
|
||||||
Detta är en visuell realtidsprototyp. Banorna är statiska och terrängen använder proceduriell relief; ingen hydrologisk erosionssimulering eller atmosfär med multipel spridning beräknas.
|
Detta är en visuell realtidsprototyp. Planeter och månar följer elliptiska Keplerbanor och roterar om sina axlar. Terrängen använder proceduriell relief; ingen hydrologisk erosionssimulering eller atmosfär med multipel spridning beräknas.
|
||||||
|
|
||||||
|
## Simulationstid
|
||||||
|
|
||||||
|
Öppna **P → DEBUG ON**. Tidsreglaget i menyn går från **0× till 100×**; knapparna 0×, 1×, 10× och 100× ger snabba val. Vänster/höger ändrar med 1×, eller 10× med Shift. **1× är standard och motsvarar verklig tid**, 0× fryser himlakroppar och vädereffekter medan kameran fortfarande kan användas. Menyn pausar simulationen; vald hastighet börjar gälla när menyn stängs. Debugpanelen visar både tidsfaktor och simulerat UTC-datum.
|
||||||
|
|
||||||
|
I `config.json` styr `simulation.startUtc` startdatum och `simulation.timeScale` starthastighet. Standarddatumet är J2000, `2000-01-01T12:00:00Z`, för en reproducerbar start. Datumet kan ändras oberoende av solsystemets banepok.
|
||||||
|
|
||||||
|
Solsystemets JSON innehåller JPL:s banparametrar vid J2000, sideriska omlopps- och rotationstider samt planetmassor. Venus och Uranus roterar retrogradt. Månen kretsar kring den rörliga jorden. Terräng, texturer, moln, hav och kollision använder samma kroppsrotation, och en spelare på marken följer planetens rörelse. Nära planeten följer fri kamera dess omloppsförflyttning; under 100 meters höjd följer den även markens rotation. Från rymden syns axelrotationen.
|
||||||
|
|
||||||
|
Modellen använder analytiska tvåkroppsbanor med fasta banelement. Den inkluderar inte N-kroppsstörningar, precession eller exakta IAU-meridianer; jordbanan approximerar jord–måne-barycentrum. Se [JPL:s banelement](https://ssd.jpl.nasa.gov/planets/approx_pos.html) och [fysiska planetdata](https://ssd.jpl.nasa.gov/planets/phys_par.html). Benchmarkrutterna fryser världstiden på den aktuella tidpunkten under mätningen och återställer sedan kameran framför den aktuella jorden, så att mätningar kan upprepas.
|
||||||
|
|
||||||
|
`--simulation-report` kör den riktiga OpenGL-renderingen vid 0 och 6 timmar samt kontrollerar roterad terräng och debugmenyn. Bilder sparas i appens `graphics-report/`.
|
||||||
|
|
||||||
|
## Galaxer och nebulosor
|
||||||
|
|
||||||
|
`KnownGalaxies.json` definierar Vintergatan. Den bifogade galaxbilden finns i `Content/Galaxies/milky-way.png` och används både för utseende och faktisk systemtäthet: luminans × alpha, formad av `densityGamma`, blir sannolikheten att en seedad stjärnkandidat finns. Svarta eller genomskinliga områden får inga kandidater. Solsystemet är alltid fastlagt i sin kända cell. Byt bildsökvägen i `texture`, eller utelämna den för en automatiskt genererad spiralgalax.
|
||||||
|
|
||||||
|
Varje galaxregion är normalt en miljon ljusår bred. Proceduriella galaxer får egna radier, lutningar, spiralarmar och nebulosor från universums seed. `nebulae` i en känd galax kan ange `id`, `positionLightYears` relativt galaxcentrum, `radiusLightYears` och en valfri bild. Systemdata har `GalaxyId` och ett `NebulaId` för närmaste nebulosagrupp; gruppmedlemskapet innebär inte att varje planet ligger inne i det synliga gasmolnet.
|
||||||
|
|
||||||
|
`GalaxyAddress` delar adressen i en galaxregion, en lokal solsystemscell och lokala meter. De två första delarna är heltal. Befintliga `UniversePosition` använder fortfarande det globala heltalsrutnätet, så lokala kameror, terräng och kollision behöver aldrig en enda gigantisk flyttalsposition. Avlägsna objekt omvandlas till kamerarelativa koordinater före GPU-rendering.
|
||||||
|
|
||||||
|
`galaxies` i `config.json` styr regionstorlek, `systemSpacingLightYears`, `galaxyStarBudget`, `localStarBudget` och `volumeSteps`. Budgeterna begränsar representativa stjärnpunkter och volymsteg, inte universums storlek. Varje stjärnpunkt härleds från en verklig seedad systemkandidat. Glesa systemceller mellan stjärnorna är tomma. Närliggande system laddas först på solsystemsnivå; bakgrundspunkter, galaxvolymer och nebulosavolymer ersätter dyr planetgeometri på större skalor. Stjärnmoln beräknas på högst två bakgrundsjobb, högst en färdig punktmesh laddas upp per bildruta, och högst tre detaljerade system hålls kvar på GPU:n.
|
||||||
|
|
||||||
|
Mushjulet kan nu skala fri flyghastighet upp till galaktiska avstånd. F8 och F9 ger översiktsvyer. Vyn växlar mjukt mellan galax-/nebulosapunkter, volymer, individuella systemstjärnor och lokala planeter.
|
||||||
|
|
||||||
|
### Stjärnornas rörelsespår
|
||||||
|
|
||||||
|
Stjärnsprites ritas som mjuka ljusstreck från föregående till aktuell skärmposition. Både kamerans förflyttning, rotation och gradvisa synfältsändring ingår; beräkningen använder kamerarelativa dubbelprecisionspositioner även mellan galaxer. Strecken växer med skärmrörelsen och ger fartkänsla när stjärnor passerar. Stilla stjärnor blir mjuka punkter igen utan kvarhängande efterbilder. Galax-/nebulosamarkörer, planetmeshar och gränssnitt ingår inte i effekten.
|
||||||
|
|
||||||
|
`starMotionBlur` i `config.json` styr `enabled`, `shutterFraction` (hur stor del av bildintervallet som bildar spår), `maximumTrailPixels`, `minimumWidthPixels`, `movingBrightness`, `historyResetSeconds` och `starFadeSeconds`. Standardinställningen är 90 % av bildintervallet och högst 180 bildpunkters spår. Strecken fördelar samma totala HDR-ljus som punkterna över en normaliserad Gaussprofil, så kamerarörelse tillför inte ljus. `movingBrightness` är nu **1** som standard; lägre värden dämpar rörliga stjärnor ytterligare, medan högre värden uttryckligen förstärker dem. Kamerahopp, återställda benchmarkvyer, paus, fönsterstorleksbyte och långa bildintervall nollställer historiken. F5 laddar även om stjärnornas vertex-, geometry- och fragmentshaders.
|
||||||
|
|
||||||
|
Det lokala stjärnurvalet förankras över 32 stjärngridblock och tonas mellan färdiga urval under 0,3 sekunder. Det gamla urvalet visas tills ersättaren är klar. Detta minskar blinkningar vid laddning utöver den jämnare pixelprofilen och rörelsespåren.
|
||||||
|
|
||||||
|
`Universe.App.exe --star-trails-report` kör ett OpenGL-test med syntetiska stjärnor: rörelse måste ge längre streck än stillastående, den konfigurerade maxlängden kontrolleras och stopp/kamerahopp måste ge rena punktbilder. Testet jämför också integrerat HDR-ljus för rörelser från 0,05 till 300 bildpunkter. Bilderna sparas som `graphics-report/star-motion-blur.png` och `star-motion-stopped.png`.
|
||||||
|
|
||||||
|
Vattnet har en separat **WATER REFLECTIONS**-rad i debugprofileringen. Jordens `ocean.screenSpaceReflections` i `KnownSolarSystem/solar-systems.json` aktiverar reflektioner av synlig geometri samt den färdiga himlen med moln och stjärnor. Standard är halv upplösning och högst 48 söksteg. `enabled: false` behåller havets analytiska himmel- och molnreflektion. Se solsystemets dokumentation för inställningar och SSR:s begränsningar. `--ocean-report` testar en väggreflektion på GPU:n, jämför dag/natt med effekten av/på och mäter reflektionspasseringen.
|
||||||
|
|
||||||
|
### Galaxresetest
|
||||||
|
|
||||||
|
Välj **Galaxy Journey** i P-menyn. Den fasta 94-sekundersbanan lämnar Jorden, visar Vintergatan, flyger genom tomrummet till galaxregion `(1,0,0)`, går in i ett deterministiskt utvalt solsystem och landar på dess största fasta huvudplanet, 1,8 m över terrängen. Därefter återställs Jordvyn. Båda ändsystemens grova geometri och kartor förbereds innan mätningen börjar.
|
||||||
|
|
||||||
|
Grafen visar FPS över resans tid. Efteråt visas PASS eller FAIL och `benchmark-results/galaxy-journey-.../` innehåller `frames.csv` och `summary.json` med bildrutetider, planet-/system-/galax-id och statistik per fas. En fas måste uppfylla både `minimumBenchmarkFps` för sin 1 %-låga FPS och `maximumBenchmarkFrameMilliseconds` för sin långsammaste bildruta. Standardgränserna är 45 FPS respektive 50 ms. Ett högt medelvärde döljer alltså inte en lång laddningsbildruta.
|
||||||
|
|
||||||
|
Kör `Universe.App.exe --galaxy-journey-report` för ett automatiskt OpenGL-test av samma flöde. Den använder fixerade simuleringssteg och mäter faktisk CPU-/GPU-tid med GPU-synkronisering; det är en accelererad integrationstestkörning utan VSync, inte ett löfte om samma FPS under alla spelinställningar. Bildfiler för galax, nebulosa, planet och resultat hamnar i `graphics-report/`. Processen returnerar kod 2 om prestandagränserna överskrids. Enhetstester kontrollerar koordinatprecision, textur-/alpha-täthet, reproducerbarhet, hierarkiskt medlemskap, markhöjd, hela resan och att injicerade bildrutetoppar ger FAIL.
|
||||||
|
|
||||||
|
## Svarta hål
|
||||||
|
|
||||||
|
I proceduriella system ersätter ett svart hål stjärnan med 5 % sannolikhet. Samma seed och cell ger samma hål, massa och skivlutning. Kända system, inklusive Solsystemet, behåller sina definierade stjärnor. Planeterna placeras utanför ackretionsskivan och belyses av dess utstrålning.
|
||||||
|
|
||||||
|
Tryck **E** för en nio sekunders kameraresa till en sned vy över skivan. Sökningen jämför dubbelprecisionsavstånd. I galaxläget söker den bland stjärnkandidater inom åtta stjärngridblock (normalt omkring 32 ljusår); utan galaxlagret expanderar den upp till åtta solsystemsceller. Kameran lämnar först planetens yta om den behöver det.
|
||||||
|
|
||||||
|
`blackHoles` i `config.json` styr `occurrenceChance` (0–1), `minimumMassSolar`, `maximumMassSolar`, `diskOuterRadiusInHorizons`, `diskBrightness` och `raySteps` (128–512). Sätt sannolikheten till 0 för att stänga av genereringen. Standardvärdena ger 200 000–600 000 solmassor, en skiva som sträcker sig till 14 horisontradier och 256 strålsteg.
|
||||||
|
|
||||||
|
Renderingen integrerar böjda ljusbanor i en förenklad Schwarzschildmodell och en tunn, animerad skiva. Horisontskugga, böjda bilder av skivans baksida, fotonring, dopplerförstärkning, gravitationell rödskiftning och HDR-bloom skapar utseendet. Se [Gralla, Holz och Wald: Black Hole Shadows, Photon Rings, and Lensing Rings](https://arxiv.org/abs/1906.00873) för bakgrunden till lins- och fotonringarna. Bakgrunden samplas från scenbilden när den finns i bild; riktningar bakom kameran får ett proceduriellt stjärnfält. Detta är en visuell realtidsmodell med en konstnärligt styrd emissionsskiva.
|
||||||
|
|
||||||
|
Debug-panelen visar `BLACK HOLE / LENSING` separat. Kör `Universe.App.exe --black-hole-report` för att rendera närmaste hål till `graphics-report/black-hole.png` och kontrollera shaderkompilering, OpenGL-fel och giltiga bildvärden.
|
||||||
|
|
||||||
## Projekt
|
## Projekt
|
||||||
|
|
||||||
|
|||||||
31
config.json
31
config.json
@@ -2,6 +2,33 @@
|
|||||||
"universeSeed": "0x554E495645525345",
|
"universeSeed": "0x554E495645525345",
|
||||||
"cellSizeAu": 100,
|
"cellSizeAu": 100,
|
||||||
"knownSolarSystemsFile": "KnownSolarSystem/solar-systems.json",
|
"knownSolarSystemsFile": "KnownSolarSystem/solar-systems.json",
|
||||||
|
"starMotionBlur": {
|
||||||
|
"enabled": true,
|
||||||
|
"shutterFraction": 0.9,
|
||||||
|
"maximumTrailPixels": 180,
|
||||||
|
"minimumWidthPixels": 1.4,
|
||||||
|
"movingBrightness": 1.0,
|
||||||
|
"historyResetSeconds": 0.25,
|
||||||
|
"starFadeSeconds": 0.3
|
||||||
|
},
|
||||||
|
"galaxies": {
|
||||||
|
"knownGalaxiesFile": "KnownGalaxies.json",
|
||||||
|
"regionSizeLightYears": 1000000,
|
||||||
|
"systemSpacingLightYears": 4,
|
||||||
|
"galaxyStarBudget": 6000,
|
||||||
|
"localStarBudget": 2600,
|
||||||
|
"volumeSteps": 40,
|
||||||
|
"minimumBenchmarkFps": 45,
|
||||||
|
"maximumBenchmarkFrameMilliseconds": 50
|
||||||
|
},
|
||||||
|
"blackHoles": {
|
||||||
|
"occurrenceChance": 0.05,
|
||||||
|
"minimumMassSolar": 200000,
|
||||||
|
"maximumMassSolar": 600000,
|
||||||
|
"diskOuterRadiusInHorizons": 14,
|
||||||
|
"diskBrightness": 4,
|
||||||
|
"raySteps": 256
|
||||||
|
},
|
||||||
"startup": {
|
"startup": {
|
||||||
"systemId": "sol",
|
"systemId": "sol",
|
||||||
"planetId": "earth",
|
"planetId": "earth",
|
||||||
@@ -19,5 +46,9 @@
|
|||||||
"longitudeDegrees": 20,
|
"longitudeDegrees": 20,
|
||||||
"latitudeDegrees": 20,
|
"latitudeDegrees": 20,
|
||||||
"speedMetersPerSecond": 200000
|
"speedMetersPerSecond": 200000
|
||||||
|
},
|
||||||
|
"simulation": {
|
||||||
|
"startUtc": "2000-01-01T12:00:00Z",
|
||||||
|
"timeScale": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,11 +34,12 @@ internal sealed class AdaptivePlanetMesh : IDisposable
|
|||||||
public AdaptivePlanetMesh(GL gl, PlanetData planet)
|
public AdaptivePlanetMesh(GL gl, PlanetData planet)
|
||||||
{
|
{
|
||||||
_gl = gl;
|
_gl = gl;
|
||||||
_planet = planet;
|
// Meshes live in a stable body frame. Spin is applied on the GPU, never by rebuilding terrain.
|
||||||
|
_planet = planet with { Orientation = PlanetOrientation.Identity };
|
||||||
for (var face = 0; face < 6; face++)
|
for (var face = 0; face < 6; face++)
|
||||||
{
|
{
|
||||||
var key = new TerrainPatchKey(face, 0, 0, 0);
|
var key = new TerrainPatchKey(face, 0, 0, 0);
|
||||||
var data = TerrainPatchBuilder.Build(planet, key);
|
var data = TerrainPatchBuilder.Build(_planet, key);
|
||||||
_tiles[key] = new() { Data = data, Mesh = new(gl, 9, data.Vertices, data.Indices) };
|
_tiles[key] = new() { Data = data, Mesh = new(gl, 9, data.Vertices, data.Indices) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ internal sealed class BenchmarkOverlay : IDisposable
|
|||||||
private long _lastRevision = -1;
|
private long _lastRevision = -1;
|
||||||
private int _lastTextTick = -1;
|
private int _lastTextTick = -1;
|
||||||
private string _lastPhase = string.Empty;
|
private string _lastPhase = string.Empty;
|
||||||
|
private IBenchmarkSession? _session;
|
||||||
|
private (int Width, int Height) _size;
|
||||||
|
|
||||||
public BenchmarkOverlay(GL gl)
|
public BenchmarkOverlay(GL gl)
|
||||||
{
|
{
|
||||||
@@ -21,8 +23,10 @@ internal sealed class BenchmarkOverlay : IDisposable
|
|||||||
_curve = new(gl, 6, [], dynamic: true);
|
_curve = new(gl, 6, [], dynamic: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Draw(int width, int height, BenchmarkRunner benchmark)
|
public void Draw(int width, int height, IBenchmarkSession benchmark)
|
||||||
{
|
{
|
||||||
|
if (_session != benchmark || _size != (width, height))
|
||||||
|
{ _session = benchmark; _size = (width, height); _lastRevision = -1; _lastTextTick = -1; _lastPhase = string.Empty; }
|
||||||
var scale = System.Math.Clamp(System.Math.Min(width / 1440d, height / 900d), .58, 1.2);
|
var scale = System.Math.Clamp(System.Math.Min(width / 1440d, height / 900d), .58, 1.2);
|
||||||
var x = width - 402 * scale;
|
var x = width - 402 * scale;
|
||||||
var y = 14 * scale;
|
var y = 14 * scale;
|
||||||
@@ -53,13 +57,16 @@ internal sealed class BenchmarkOverlay : IDisposable
|
|||||||
_curve.Draw(GLEnum.LineStrip);
|
_curve.Draw(GLEnum.LineStrip);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdatePanel(int width, int height, BenchmarkRunner benchmark, double x, double y,
|
private void UpdatePanel(int width, int height, IBenchmarkSession benchmark, double x, double y,
|
||||||
double panelWidth, double panelHeight, double scale)
|
double panelWidth, double panelHeight, double scale)
|
||||||
{
|
{
|
||||||
var vertices = new List<float>(20_000);
|
var vertices = new List<float>(20_000);
|
||||||
Rect(x, y, panelWidth, panelHeight, .018f, .028f, .05f);
|
Rect(x, y, panelWidth, panelHeight, .018f, .028f, .05f);
|
||||||
Rect(x, y, 3 * scale, panelHeight, .13f, .55f, .8f);
|
Rect(x, y, 3 * scale, panelHeight, .13f, .55f, .8f);
|
||||||
Text(benchmark.IsRunning ? "BENCHMARK LIVE" : benchmark.Phase == "STOPPED" ? "BENCHMARK STOPPED" : "BENCHMARK COMPLETE",
|
var title=benchmark is GalaxyJourneyBenchmark journey
|
||||||
|
? journey.IsComplete ? journey.PerformancePassed ? "GALAXY JOURNEY PASS" : "GALAXY JOURNEY FAIL" : journey.IsRunning ? "GALAXY JOURNEY LIVE" : "GALAXY JOURNEY STOPPED"
|
||||||
|
: benchmark.IsRunning ? "BENCHMARK LIVE" : benchmark.Phase == "STOPPED" ? "BENCHMARK STOPPED" : "BENCHMARK COMPLETE";
|
||||||
|
Text(title,
|
||||||
x + 16 * scale, y + 11 * scale, 1.75 * scale, .76f, .91f, 1f);
|
x + 16 * scale, y + 11 * scale, 1.75 * scale, .76f, .91f, 1f);
|
||||||
Text($"PHASE {benchmark.Phase}", x + 16 * scale, y + 29 * scale, 1.32 * scale, .52f, .7f, .82f);
|
Text($"PHASE {benchmark.Phase}", x + 16 * scale, y + 29 * scale, 1.32 * scale, .52f, .7f, .82f);
|
||||||
Text($"TARGET {benchmark.TargetLabel}", x + 16 * scale, y + 47 * scale,
|
Text($"TARGET {benchmark.TargetLabel}", x + 16 * scale, y + 47 * scale,
|
||||||
@@ -74,6 +81,11 @@ internal sealed class BenchmarkOverlay : IDisposable
|
|||||||
1.20 * scale, .7f, .83f, .91f);
|
1.20 * scale, .7f, .83f, .91f);
|
||||||
Text($"MIN {Number(benchmark.MinimumFps)} 1 PCT LOW {Number(benchmark.OnePercentLowFps)}",
|
Text($"MIN {Number(benchmark.MinimumFps)} 1 PCT LOW {Number(benchmark.OnePercentLowFps)}",
|
||||||
x + 16 * scale, y + 132 * scale, 1.02 * scale, .7f, .83f, .91f);
|
x + 16 * scale, y + 132 * scale, 1.02 * scale, .7f, .83f, .91f);
|
||||||
|
var maximum=benchmark.GraphSamples.Count==0?60:System.Math.Max(60,System.Math.Ceiling(benchmark.GraphSamples.Max()/30)*30);
|
||||||
|
Text($"{maximum:0} FPS",x+21*scale,y+154*scale,.95*scale,.4f,.6f,.7f);
|
||||||
|
Text("0 S",x+17*scale,y+219*scale,.95*scale,.4f,.6f,.7f);
|
||||||
|
var end=$"{benchmark.ElapsedSeconds:0} S";
|
||||||
|
Text(end,x+panelWidth-(end.Length*6*.95+17)*scale,y+219*scale,.95*scale,.4f,.6f,.7f);
|
||||||
|
|
||||||
_panel.ReplaceVertices(vertices.ToArray());
|
_panel.ReplaceVertices(vertices.ToArray());
|
||||||
|
|
||||||
@@ -108,7 +120,6 @@ internal sealed class BenchmarkOverlay : IDisposable
|
|||||||
var grid = new List<float>(1800);
|
var grid = new List<float>(1800);
|
||||||
var curve = new List<float>(samples.Count * 6);
|
var curve = new List<float>(samples.Count * 6);
|
||||||
var maximum = samples.Count == 0 ? 60 : System.Math.Max(60, System.Math.Ceiling(samples.Max() / 30) * 30);
|
var maximum = samples.Count == 0 ? 60 : System.Math.Max(60, System.Math.Ceiling(samples.Max() / 30) * 30);
|
||||||
maximum = System.Math.Min(maximum, 240);
|
|
||||||
for (var i = 0; i <= 4; i++)
|
for (var i = 0; i <= 4; i++)
|
||||||
{
|
{
|
||||||
var py = y + graphHeight * i / 4;
|
var py = y + graphHeight * i / 4;
|
||||||
|
|||||||
187
src/Universe.App/Graphics/CosmicRenderer.cs
Normal file
187
src/Universe.App/Graphics/CosmicRenderer.cs
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Silk.NET.OpenGL;
|
||||||
|
using Universe.App.Runtime;
|
||||||
|
using Universe.Core.Generation;
|
||||||
|
using Universe.Core.Math;
|
||||||
|
using Universe.Core.World;
|
||||||
|
|
||||||
|
namespace Universe.App.Graphics;
|
||||||
|
|
||||||
|
internal sealed class CosmicRenderer : IDisposable
|
||||||
|
{
|
||||||
|
private sealed class Cloud(UniversePosition center, double scale, PlanetOrientation orientation)
|
||||||
|
{
|
||||||
|
public UniversePosition Center = center;
|
||||||
|
public double Scale = scale;
|
||||||
|
public PlanetOrientation Orientation = orientation;
|
||||||
|
public Task<float[]>? Pending;
|
||||||
|
public GpuMesh? Mesh;
|
||||||
|
public int LastFrame;
|
||||||
|
}
|
||||||
|
private readonly GL _gl;
|
||||||
|
private readonly GalaxyUniverse _universe;
|
||||||
|
private readonly StarTrailRenderer _trails;
|
||||||
|
private readonly ShaderProgram _volume, _stars;
|
||||||
|
private readonly GpuMesh _fullscreen;
|
||||||
|
private readonly GpuMesh _markers;
|
||||||
|
private readonly Dictionary<string, Cloud> _clouds = [];
|
||||||
|
private readonly Dictionary<string, uint> _textures = [];
|
||||||
|
private readonly uint _white;
|
||||||
|
private int _frame;
|
||||||
|
private int _uploads;
|
||||||
|
private Cloud? _localCurrent, _localPrevious;
|
||||||
|
private double _localBlend = 1;
|
||||||
|
private string? _localGalaxyId;
|
||||||
|
private long _localCutVersion = -1;
|
||||||
|
public int StarCount { get; private set; }
|
||||||
|
public CosmicRenderer(GL gl, GalaxyUniverse universe, StarTrailRenderer trails)
|
||||||
|
{
|
||||||
|
_gl = gl; _universe = universe; _trails = trails;
|
||||||
|
_volume = ShaderProgram.Create(gl, ShaderSources.Load("fullscreen.vert"), ShaderSources.Load("galaxy.frag"));
|
||||||
|
_stars = ShaderProgram.Create(gl, ShaderSources.Load("cosmic.vert"), ShaderSources.Load("cosmic.frag"));
|
||||||
|
_fullscreen = new(gl, 3, [-1,-1,0,3,-1,0,-1,3,0]);
|
||||||
|
_markers = new(gl,7,[],dynamic:true);
|
||||||
|
_white = Texture(new RasterImage(2,2,Enumerable.Repeat((byte)255,16).ToArray()));
|
||||||
|
}
|
||||||
|
public void Draw(FlyCamera camera, int width, int height, double frameSeconds)
|
||||||
|
{
|
||||||
|
_frame++; _uploads = 0; StarCount = 0;
|
||||||
|
_gl.Disable(GLEnum.DepthTest); _gl.DepthMask(false); _gl.Enable(GLEnum.Blend); _gl.BlendFunc(GLEnum.One, GLEnum.One);
|
||||||
|
var galaxies = _universe.VisibleGalaxies(camera.Position).OrderBy(g => camera.Position.RelativeTo(g.Center, camera.CellSizeMeters).LengthSquared).ToArray();
|
||||||
|
foreach (var galaxy in galaxies.Take(10))
|
||||||
|
{
|
||||||
|
DrawVolume(camera, width, height, galaxy.Id, galaxy.Center, galaxy.RadiusMeters, galaxy.ThicknessMeters / galaxy.RadiusMeters,
|
||||||
|
galaxy.Orientation, galaxy.SpiralArms, galaxy.DensityGamma, galaxy.Seed, galaxy.Texture, 0);
|
||||||
|
if(camera.Position.RelativeTo(galaxy.Center,camera.CellSizeMeters).Length < galaxy.RadiusMeters*6)
|
||||||
|
{
|
||||||
|
var cloud = GetCloud(galaxy.Id, galaxy.Center, galaxy.RadiusMeters, galaxy.Orientation,
|
||||||
|
() => BuildStars(galaxy, null, _universe.Settings.GalaxyStarBudget));
|
||||||
|
DrawCloud(camera, width, height, cloud, _universe.SpacingCells * _universe.CellSize * 24 / galaxy.RadiusMeters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var current = _universe.GalaxyAt(camera.Position);
|
||||||
|
if (_localGalaxyId != current.Id || _localCutVersion != camera.MotionHistoryVersion)
|
||||||
|
{
|
||||||
|
_localCurrent = null; _localPrevious = null; _localBlend = 1;
|
||||||
|
_localGalaxyId = current.Id; _localCutVersion = camera.MotionHistoryVersion;
|
||||||
|
}
|
||||||
|
var markerScale=_universe.Settings.RegionSizeLightYears*UniverseScale.LightYearMeters;
|
||||||
|
var markers=new List<float>(256);
|
||||||
|
void Marker(UniversePosition position,double radius,Vector3 color,bool forcePoint=false)
|
||||||
|
{
|
||||||
|
var relative=position.RelativeTo(camera.Position,camera.CellSizeMeters);
|
||||||
|
var angular=radius/System.Math.Max(radius,relative.Length);
|
||||||
|
var t=System.Math.Clamp((angular-.004)/.021,0,1);var fade=forcePoint?1:1-t*t*(3-2*t);
|
||||||
|
if(fade<.01)return;
|
||||||
|
var p=relative/markerScale;
|
||||||
|
markers.AddRange([(float)p.X,(float)p.Y,(float)p.Z,color.X*(float)fade,color.Y*(float)fade,color.Z*(float)fade,2.5f]);
|
||||||
|
}
|
||||||
|
var volumeIds=galaxies.Take(10).Select(g=>g.Id).ToHashSet();
|
||||||
|
foreach(var galaxy in galaxies)Marker(galaxy.Center,galaxy.RadiusMeters,new(.3f,.3f,.5f),!volumeIds.Contains(galaxy.Id));
|
||||||
|
var nearNebulae=current.Nebulae.OrderBy(n=>camera.Position.RelativeTo(n.Center,camera.CellSizeMeters).LengthSquared).Take(5).Select(n=>n.Id).ToHashSet();
|
||||||
|
foreach(var nebula in current.Nebulae)Marker(nebula.Center,nebula.RadiusMeters,new(.2f,.12f,.26f),!nearNebulae.Contains(nebula.Id));
|
||||||
|
_markers.ReplaceVertices(markers.ToArray());
|
||||||
|
Camera(_stars,camera,width,height,camera.Position,markerScale,PlanetOrientation.Identity);
|
||||||
|
_stars.Set("uNearFade",1e-10f);_markers.Draw(GLEnum.Points);
|
||||||
|
if (camera.Position.RelativeTo(current.Center, camera.CellSizeMeters).Length < current.RadiusMeters * 1.8)
|
||||||
|
{
|
||||||
|
var at = _universe.StellarBlock(camera.Position.Sector);
|
||||||
|
// A stable sampling anchor spans 32 stellar blocks. Crossing every
|
||||||
|
// individual system grid boundary must not replace the whole sky.
|
||||||
|
var block = new SectorCoordinate(GalaxyUniverse.FloorDiv(at.X,32)*32,
|
||||||
|
GalaxyUniverse.FloorDiv(at.Y,32)*32,GalaxyUniverse.FloorDiv(at.Z,32)*32);
|
||||||
|
var origin = UniversePosition.AtCellCenter(new(block.X * _universe.SpacingCells, block.Y * _universe.SpacingCells, block.Z * _universe.SpacingCells), camera.CellSizeMeters);
|
||||||
|
var scale = _universe.SpacingCells * _universe.CellSize * 64;
|
||||||
|
var cloud = GetCloud($"local:{block.X}:{block.Y}:{block.Z}", origin, scale, PlanetOrientation.Identity,
|
||||||
|
() => BuildStars(current, (origin, scale, block), _universe.Settings.LocalStarBudget));
|
||||||
|
if (cloud.Mesh is not null && cloud != _localCurrent)
|
||||||
|
{
|
||||||
|
_localPrevious = _localCurrent; _localCurrent = cloud;
|
||||||
|
_localBlend = 0;
|
||||||
|
}
|
||||||
|
_localBlend = System.Math.Min(1,_localBlend+System.Math.Clamp(frameSeconds,0,.1)/_trails.Settings.StarFadeSeconds);
|
||||||
|
var fadeDistance = _universe.SpacingCells * _universe.CellSize * .15 / scale;
|
||||||
|
if (_localPrevious is not null && _localBlend < 1) DrawCloud(camera,width,height,_localPrevious,fadeDistance,(float)(1-_localBlend));
|
||||||
|
if (_localCurrent is not null) DrawCloud(camera,width,height,_localCurrent,fadeDistance,(float)_localBlend);
|
||||||
|
if (_localBlend >= 1) _localPrevious = null;
|
||||||
|
foreach (var n in current.Nebulae.OrderBy(n => camera.Position.RelativeTo(n.Center, camera.CellSizeMeters).LengthSquared).Take(5))
|
||||||
|
DrawVolume(camera, width, height, n.Id, n.Center, n.RadiusMeters, 1, n.Orientation, 0, 1, n.Seed, n.Texture, 1);
|
||||||
|
}
|
||||||
|
if (_clouds.Count > 48)
|
||||||
|
foreach (var key in _clouds.Where(p => p.Value != _localCurrent && p.Value != _localPrevious && p.Value.LastFrame < _frame - 2 && p.Value.Pending?.IsCompleted != false).Select(p => p.Key).Take(_clouds.Count - 48).ToArray())
|
||||||
|
{ _clouds[key].Mesh?.Dispose(); _clouds.Remove(key); }
|
||||||
|
_gl.Disable(GLEnum.Blend); _gl.Enable(GLEnum.DepthTest);
|
||||||
|
}
|
||||||
|
private Cloud GetCloud(string key, UniversePosition center, double scale, PlanetOrientation orientation, Func<float[]> build)
|
||||||
|
{
|
||||||
|
if (!_clouds.TryGetValue(key, out var cloud))
|
||||||
|
{ cloud = new(center, scale, orientation); _clouds.Add(key, cloud); }
|
||||||
|
cloud.LastFrame = _frame;
|
||||||
|
if (cloud.Pending is null && cloud.Mesh is null && _clouds.Values.Count(c => c.Pending?.IsCompleted == false) < 2)
|
||||||
|
cloud.Pending = Task.Run(build);
|
||||||
|
if (cloud.Pending is { IsCompletedSuccessfully: true } ready && _uploads < 1)
|
||||||
|
{ cloud.Mesh = new(_gl, 7, ready.Result); cloud.Pending = null; _uploads++; }
|
||||||
|
return cloud;
|
||||||
|
}
|
||||||
|
private float[] BuildStars(GalaxyData galaxy, (UniversePosition Center, double Scale, SectorCoordinate Block)? local, int budget)
|
||||||
|
{
|
||||||
|
var random = new DeterministicRandom(galaxy.Seed ^ (local.HasValue ? GalaxyUniverse.Hash(local.Value.Block, 55) : 77));
|
||||||
|
var vertices = new List<float>(budget * 7); var used = new HashSet<SectorCoordinate>();
|
||||||
|
for (var i = 0; i < budget * 24 && used.Count < budget; i++)
|
||||||
|
{
|
||||||
|
SectorCoordinate block;
|
||||||
|
if (local is { } l) block = new(l.Block.X + random.NextInt(-64,65), l.Block.Y + random.NextInt(-64,65), l.Block.Z + random.NextInt(-64,65));
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var p = new DVector3(random.NextDouble()*2-1,(random.NextDouble()*6-3)*galaxy.ThicknessMeters/galaxy.RadiusMeters,random.NextDouble()*2-1) * galaxy.RadiusMeters;
|
||||||
|
block = _universe.StellarBlock(galaxy.Center.Add(galaxy.Orientation.ToWorld(p), _universe.CellSize).Sector);
|
||||||
|
}
|
||||||
|
if (!_universe.Candidate(galaxy, block, out var sector) || !used.Add(sector)) continue;
|
||||||
|
var position = UniversePosition.AtCellCenter(sector, _universe.CellSize);
|
||||||
|
var delta = local is { } ll ? position.RelativeTo(ll.Center, _universe.CellSize) / ll.Scale
|
||||||
|
: galaxy.Orientation.ToBody(position.RelativeTo(galaxy.Center, _universe.CellSize)) / galaxy.RadiusMeters;
|
||||||
|
var tint = random.NextDouble(); var brightness = .14 + random.NextDouble() * .9;
|
||||||
|
vertices.AddRange([(float)delta.X,(float)delta.Y,(float)delta.Z,(float)(brightness*(.7+tint*.3)),(float)(brightness*.82),(float)(brightness*(1.0-tint*.4)),(float)(1.2+random.NextDouble()*2.3)]);
|
||||||
|
}
|
||||||
|
return vertices.ToArray();
|
||||||
|
}
|
||||||
|
private void Camera(ShaderProgram shader, FlyCamera camera, int width, int height, UniversePosition center, double scale, PlanetOrientation orientation)
|
||||||
|
{
|
||||||
|
shader.Use(); shader.Set("uOrigin", (camera.Position.RelativeTo(center, camera.CellSizeMeters)/scale).ToVector3());
|
||||||
|
shader.Set("uAxisX", orientation.ToWorld(new(1,0,0)).ToVector3()); shader.Set("uAxisY", orientation.ToWorld(new(0,1,0)).ToVector3()); shader.Set("uAxisZ", orientation.ToWorld(new(0,0,1)).ToVector3());
|
||||||
|
shader.Set("uForward",camera.Forward.ToVector3()); shader.Set("uRight",camera.Right.ToVector3()); shader.Set("uUp",camera.Up.ToVector3());
|
||||||
|
shader.Set("uAspect",(float)width/height); shader.Set("uTanHalfFov",(float)System.Math.Tan(camera.FieldOfViewRadians*.5));
|
||||||
|
}
|
||||||
|
private void DrawCloud(FlyCamera camera,int width,int height,Cloud cloud,double nearFade,float visibility=1)
|
||||||
|
{
|
||||||
|
if (cloud.Mesh is null) return;
|
||||||
|
cloud.LastFrame = _frame;
|
||||||
|
_trails.Draw(cloud.Mesh,cloud.Center,cloud.Scale,cloud.Orientation,nearFade,visibility);
|
||||||
|
}
|
||||||
|
private void DrawVolume(FlyCamera camera,int width,int height,string id,UniversePosition center,double radius,double thickness,
|
||||||
|
PlanetOrientation orientation,int arms,double gamma,ulong seed,RasterImage? texture,int kind)
|
||||||
|
{
|
||||||
|
var relative = center.RelativeTo(camera.Position,camera.CellSizeMeters);
|
||||||
|
if (DVector3.Dot(relative,camera.Forward)<-radius*1.2 || radius / System.Math.Max(radius,relative.Length) < .0003) return;
|
||||||
|
var angular=radius/System.Math.Max(radius,relative.Length);
|
||||||
|
var opacity=System.Math.Clamp((angular-.004)/.021,0,1);opacity=opacity*opacity*(3-2*opacity);
|
||||||
|
if(opacity<=0)return;
|
||||||
|
Camera(_volume,camera,width,height,center,radius,orientation);
|
||||||
|
_volume.Set("uOpacity",(float)opacity);
|
||||||
|
_volume.Set("uThickness",(float)thickness); _volume.Set("uArms",(float)arms); _volume.Set("uGamma",(float)gamma);
|
||||||
|
_volume.Set("uSeed",(float)(seed&65535)*.001f); _volume.Set("uKind",kind); _volume.Set("uSteps",_universe.Settings.VolumeSteps);
|
||||||
|
_volume.Set("uHasTexture",texture is null?0:1); _volume.Set("uDensity",0);
|
||||||
|
_gl.ActiveTexture(GLEnum.Texture0);
|
||||||
|
if (texture is not null && !_textures.ContainsKey(id)) _textures[id]=Texture(texture);
|
||||||
|
_gl.BindTexture(GLEnum.Texture2D,texture is null?_white:_textures[id]); _fullscreen.Draw();
|
||||||
|
}
|
||||||
|
private unsafe uint Texture(RasterImage image)
|
||||||
|
{
|
||||||
|
var handle=_gl.GenTexture();_gl.BindTexture(GLEnum.Texture2D,handle);
|
||||||
|
fixed(byte* pixels=image.Rgba) _gl.TexImage2D(GLEnum.Texture2D,0,InternalFormat.Rgba8,(uint)image.Width,(uint)image.Height,0,PixelFormat.Rgba,PixelType.UnsignedByte,pixels);
|
||||||
|
_gl.TexParameter(GLEnum.Texture2D,GLEnum.TextureMinFilter,(int)GLEnum.Linear);_gl.TexParameter(GLEnum.Texture2D,GLEnum.TextureMagFilter,(int)GLEnum.Linear);
|
||||||
|
_gl.TexParameter(GLEnum.Texture2D,GLEnum.TextureWrapS,(int)GLEnum.ClampToEdge);_gl.TexParameter(GLEnum.Texture2D,GLEnum.TextureWrapT,(int)GLEnum.ClampToEdge);
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
public void Dispose(){foreach(var cloud in _clouds.Values)cloud.Mesh?.Dispose();foreach(var texture in _textures.Values)_gl.DeleteTexture(texture);_gl.DeleteTexture(_white);_volume.Dispose();_stars.Dispose();_fullscreen.Dispose();_markers.Dispose();}
|
||||||
|
}
|
||||||
@@ -13,6 +13,182 @@ namespace Universe.App.Graphics;
|
|||||||
/// <summary>Offscreen tests of the production shaders; no desktop capture or mouse control.</summary>
|
/// <summary>Offscreen tests of the production shaders; no desktop capture or mouse control.</summary>
|
||||||
internal static class GraphicsReport
|
internal static class GraphicsReport
|
||||||
{
|
{
|
||||||
|
public static void RunSimulation(GL gl, UniverseRenderer renderer, UniverseNeighborhood initial, double cellSize, int width, int height)
|
||||||
|
{
|
||||||
|
var directory = Path.Combine(AppContext.BaseDirectory, "graphics-report"); Directory.CreateDirectory(directory);
|
||||||
|
var center = UniversePosition.AtCellCenter(initial.ActiveSystem.Coordinate, cellSize);
|
||||||
|
var camera = new FlyCamera(center, cellSize);
|
||||||
|
var basis = initial.ActiveSystem;
|
||||||
|
camera.SetInitialView(basis, new StartupConfiguration { PlanetId = "earth", DistanceInRadii = 3, LongitudeDegrees = 20, LatitudeDegrees = 20 });
|
||||||
|
var earth = basis.Planets.Single(p => p.Id == "earth");
|
||||||
|
var offset = camera.Position.RelativeTo(center.Add(earth.PositionMeters, cellSize), cellSize);
|
||||||
|
var forward = camera.Forward; var up = camera.Up;
|
||||||
|
float[] Frame(double seconds, string file)
|
||||||
|
{
|
||||||
|
var system = Universe.Core.Physics.CelestialSimulation.Evaluate(basis, basis.EpochSecondsSinceJ2000 + seconds);
|
||||||
|
var p = system.Planets.Single(p => p.Id == "earth");
|
||||||
|
camera.SetPlayerPose(center.Add(p.PositionMeters + offset, cellSize), forward, up, resetMotionHistory: true);
|
||||||
|
renderer.SimulationTimeSeconds = seconds;
|
||||||
|
for (var i = 0; i < 24; i++) renderer.Render(camera, initial with { ActiveSystem = system }, 0);
|
||||||
|
gl.Finish(); var pixels = Read(gl, width, height);
|
||||||
|
if (gl.GetError() != GLEnum.NoError || pixels.Any(v => !float.IsFinite(v)))
|
||||||
|
throw new InvalidOperationException("Simulation produced invalid OpenGL output.");
|
||||||
|
WritePng(Path.Combine(directory, file), width, height, pixels, false);
|
||||||
|
return pixels;
|
||||||
|
}
|
||||||
|
var first = Frame(0, "earth-time-0h.png");
|
||||||
|
var rotated = Frame(21600, "earth-time-6h.png");
|
||||||
|
var difference = first.Zip(rotated, (a,b) => System.Math.Abs(a-b)).Average();
|
||||||
|
if (difference < .002) throw new InvalidOperationException($"Earth spin did not visibly change the production rendering: {difference}.");
|
||||||
|
renderer.DrawMenu(2, true, 100, Universe.Core.Physics.SimulationClock.J2000.AddHours(6));
|
||||||
|
gl.Finish(); WritePng(Path.Combine(directory, "time-debug-menu.png"), width, height, Read(gl, width, height), false);
|
||||||
|
// Sample a detailed surface after spin: mesh generation, culling and displacement must use the body frame.
|
||||||
|
var after = Universe.Core.Physics.CelestialSimulation.Evaluate(basis, basis.EpochSecondsSinceJ2000 + 21600);
|
||||||
|
var planet = after.Planets.Single(p => p.Id == "earth");
|
||||||
|
var lat = 46.5 * System.Math.PI / 180; var lon = -7.5 * System.Math.PI / 180;
|
||||||
|
var radial = planet.Orientation.ToWorld(new(System.Math.Cos(lat)*System.Math.Cos(lon), System.Math.Sin(lat), System.Math.Cos(lat)*System.Math.Sin(lon)));
|
||||||
|
var tangent = DVector3.Cross(radial, planet.Orientation.ToWorld(new(0,1,0))).Normalized();
|
||||||
|
var radius = TerrainHeight.Sample(planet, radial);
|
||||||
|
camera.SetPlayerPose(center.Add(planet.PositionMeters + radial * (radius + 400), cellSize), (tangent - radial * .3).Normalized(), radial, true);
|
||||||
|
for (var i = 0; i < 72; i++) renderer.Render(camera, initial with { ActiveSystem = after }, 0);
|
||||||
|
gl.Finish(); var ground = Read(gl, width, height);
|
||||||
|
if (gl.GetError() != GLEnum.NoError) throw new InvalidOperationException("Rotating terrain GL error.");
|
||||||
|
WritePng(Path.Combine(directory, "earth-rotated-terrain.png"), width, height, ground, false);
|
||||||
|
Console.WriteLine($"Simulation GPU report passed: Earth 0/6h visual difference {difference:F5}, rotated terrain {renderer.TerrainTiles} tiles, LOD {renderer.TerrainDeepestLevel}; debug time menu. Images: {directory}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RunStarTrails(GL gl,StarMotionBlurSettings settings,int width,int height)
|
||||||
|
{
|
||||||
|
using var trails=new StarTrailRenderer(gl,settings with {Enabled=true});
|
||||||
|
using var target=new RenderTarget(gl,width,height);
|
||||||
|
var cell=UniverseScale.DefaultCellSizeMeters;const double scale=1e12;
|
||||||
|
var center=UniversePosition.AtCellCenter(new(600_000_000_000_000_000,0,0),cell);
|
||||||
|
var camera=new FlyCamera(center,cell);
|
||||||
|
var random=new DeterministicRandom(0x5354415254524149UL);var data=new List<float>();
|
||||||
|
for(var i=0;i<900;i++)
|
||||||
|
{
|
||||||
|
var z=8+random.NextDouble()*52;
|
||||||
|
data.AddRange([(float)((random.NextDouble()*2-1)*z*.7),(float)((random.NextDouble()*2-1)*z*.43),-(float)z,
|
||||||
|
1.1f,(float)(.75+random.NextDouble()*.35),.8f,(float)(1.5+random.NextDouble()*1.5)]);
|
||||||
|
}
|
||||||
|
using var stars=new GpuMesh(gl,7,data.ToArray());
|
||||||
|
float[] Frame(GpuMesh mesh)
|
||||||
|
{
|
||||||
|
target.Bind();gl.Disable(GLEnum.DepthTest);gl.DepthMask(false);
|
||||||
|
gl.ClearColor(0,0,0,1);gl.Clear(ClearBufferMask.ColorBufferBit);
|
||||||
|
gl.Enable(GLEnum.Blend);gl.BlendFunc(GLEnum.One,GLEnum.One);
|
||||||
|
trails.BeginFrame(camera,width,height,1d/60);
|
||||||
|
trails.Draw(mesh,center,scale,PlanetOrientation.Identity);
|
||||||
|
gl.Finish();return Read(gl,width,height);
|
||||||
|
}
|
||||||
|
Frame(stars);
|
||||||
|
camera.SetPlayerPose(center.Add(new(0,0,-12*scale),cell),camera.Forward,camera.Up);
|
||||||
|
var moving=Frame(stars);
|
||||||
|
var stopped=Frame(stars);
|
||||||
|
int Coverage(float[] pixels)=>Enumerable.Range(0,width*height).Count(i=>pixels[i*4]>.001);
|
||||||
|
var movingCoverage=Coverage(moving);var stillCoverage=Coverage(stopped);
|
||||||
|
if(moving.Any(p=>!float.IsFinite(p))||movingCoverage<stillCoverage*1.5)
|
||||||
|
throw new InvalidOperationException($"Star trails did not stretch the moving stars: moving {movingCoverage}, stopped {stillCoverage} pixels.");
|
||||||
|
trails.ReloadShaders();
|
||||||
|
camera.SetPlayerPose(camera.Position,camera.Forward,camera.Up,resetMotionHistory:true);
|
||||||
|
var cut=Frame(stars);
|
||||||
|
if(trails.History.Valid||cut.Zip(stopped,(a,b)=>System.Math.Abs(a-b)).Max()>.0001)
|
||||||
|
throw new InvalidOperationException("Star trails retained an afterimage after stopping or a camera cut.");
|
||||||
|
using var single=new GpuMesh(gl,7,[0,0,-10,1,1,1,2]);
|
||||||
|
double Energy(float[] pixels) => pixels.Where((_,i)=>i%4<3).Sum(v=>(double)v);
|
||||||
|
var energyRatios = new List<double>();
|
||||||
|
foreach (var travel in new[] { .05, 1d, 12d, 80d, 180d, 300d })
|
||||||
|
{
|
||||||
|
camera.SetPlayerPose(center,new(0,0,-1),new(0,1,0),resetMotionHistory:true);
|
||||||
|
var point = Frame(single);
|
||||||
|
var dx=travel*20*System.Math.Tan(camera.FieldOfViewRadians*.5)/width;
|
||||||
|
camera.SetPlayerPose(center.Add(new(dx*scale,0,0),cell),camera.Forward,camera.Up);
|
||||||
|
var streak = Frame(single);
|
||||||
|
var ratio=Energy(streak)/Energy(point); energyRatios.Add(ratio);
|
||||||
|
// movingBrightness is an optional artistic multiplier; the default 1 conserves HDR energy.
|
||||||
|
var expected=1+(settings.MovingBrightness-1)*(1-System.Math.Exp(-System.Math.Min(travel*settings.ShutterFraction,settings.MaximumTrailPixels)/14));
|
||||||
|
if(System.Math.Abs(ratio/expected-1)>.04)
|
||||||
|
throw new InvalidOperationException($"Star energy changed with motion: {travel} px, ratio {ratio:F4}, expected {expected:F4}.");
|
||||||
|
}
|
||||||
|
camera.SetPlayerPose(center,camera.Forward,camera.Up,resetMotionHistory:true);Frame(single);
|
||||||
|
camera.SetPlayerPose(center.Add(new(-6*scale,0,0),cell),camera.Forward,camera.Up);
|
||||||
|
var bounded=Frame(single);
|
||||||
|
var columns=Enumerable.Range(0,width).Where(x=>Enumerable.Range(0,height).Any(y=>bounded[(y*width+x)*4]>.001)).ToArray();
|
||||||
|
var span=columns.Length==0?0:columns[^1]-columns[0]+1;
|
||||||
|
if(span<settings.MaximumTrailPixels*.5||span>settings.MaximumTrailPixels+14)
|
||||||
|
throw new InvalidOperationException($"Star trail pixel limit failed: span {span}, limit {settings.MaximumTrailPixels}.");
|
||||||
|
if(gl.GetError() is var error&&error!=GLEnum.NoError)throw new InvalidOperationException($"Star trails GL error: {error}");
|
||||||
|
var directory=Path.Combine(AppContext.BaseDirectory,"graphics-report");Directory.CreateDirectory(directory);
|
||||||
|
WritePng(Path.Combine(directory,"star-motion-blur.png"),width,height,moving);
|
||||||
|
WritePng(Path.Combine(directory,"star-motion-stopped.png"),width,height,stopped);
|
||||||
|
Console.WriteLine($"Star trails passed: {movingCoverage} moving vs {stillCoverage} stationary pixels; energy ratios {string.Join(", ",energyRatios.Select(r=>r.ToString("F3")))}; capped trail {span} px; camera cut/stop clean; OpenGL {gl.GetStringS(GLEnum.Renderer)}.");
|
||||||
|
gl.BindFramebuffer(GLEnum.Framebuffer,0);gl.Disable(GLEnum.Blend);gl.Enable(GLEnum.DepthTest);gl.DepthMask(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RunGalaxyJourney(GL gl, UniverseRenderer renderer, FlyCamera camera, UniverseStreamer streamer,
|
||||||
|
GalaxyJourneyBenchmark journey, int width, int height)
|
||||||
|
{
|
||||||
|
var neighborhood = streamer.Load(camera.Position.Sector);
|
||||||
|
journey.RenderEnvironment = new { Width = width, Height = height, Gpu = renderer.HardwareName, Mode = "synchronized-accelerated", SimulationStepSeconds = 1d/12, FieldOfViewDegrees = camera.FieldOfViewRadians * 180 / System.Math.PI };
|
||||||
|
var startPosition=camera.Position;var startForward=camera.Forward;var startUp=camera.Up;
|
||||||
|
var nebula=journey.HomeGalaxy.Nebulae[0];
|
||||||
|
var radial=nebula.Orientation.ToWorld(new DVector3(.1,.05,1).Normalized());
|
||||||
|
camera.SetPlayerPose(nebula.Center.Add(radial*nebula.RadiusMeters*2.8,camera.CellSizeMeters),-radial,nebula.Orientation.ToWorld(new(0,1,0)),resetMotionHistory:true);
|
||||||
|
for(var i=0;i<6;i++){renderer.Render(camera,streamer.Load(camera.Position.Sector),1d/60);gl.Finish();}
|
||||||
|
var nebulaDirectory=Path.Combine(AppContext.BaseDirectory,"graphics-report");Directory.CreateDirectory(nebulaDirectory);
|
||||||
|
WritePng(Path.Combine(nebulaDirectory,"galaxy-nebula.png"),width,height,Read(gl,width,height),false);
|
||||||
|
camera.SetPlayerPose(startPosition,startForward,startUp,resetMotionHistory:true);
|
||||||
|
neighborhood=streamer.Load(camera.Position.Sector);
|
||||||
|
for (var i=0;i<8;i++) {renderer.Render(camera,neighborhood,1d/60);gl.Finish();}
|
||||||
|
var directory = Path.Combine(AppContext.BaseDirectory,"graphics-report");Directory.CreateDirectory(directory);
|
||||||
|
var captured = new HashSet<string>();
|
||||||
|
var timer = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
while(journey.IsRunning)
|
||||||
|
{
|
||||||
|
journey.Update(1d/12,camera);
|
||||||
|
neighborhood=streamer.Load(camera.Position.Sector);
|
||||||
|
renderer.Render(camera,neighborhood,1d/12);renderer.DrawBenchmarkOverlay(journey);gl.Finish();
|
||||||
|
var seconds=timer.Elapsed.TotalSeconds;timer.Restart();
|
||||||
|
journey.RecordFrame(seconds);
|
||||||
|
if(captured.Add(journey.Phase))Console.WriteLine($"Galaxy journey: {journey.Phase}; sector {camera.Position.Sector}; galaxy {neighborhood.ActiveSystem.GalaxyId}");
|
||||||
|
// Capture late in each phase, after the point-cloud workers have had time to publish.
|
||||||
|
if(journey.ElapsedSeconds is >23.8 and <24 || journey.ElapsedSeconds is >44.8 and <45 || journey.ElapsedSeconds is >53.8 and <54 || journey.ElapsedSeconds is >89.8 and <90)
|
||||||
|
{
|
||||||
|
renderer.DrawBenchmarkOverlay(journey);
|
||||||
|
WritePng(Path.Combine(directory,$"galaxy-journey-{(int)journey.ElapsedSeconds}.png"),width,height,Read(gl,width,height),false);
|
||||||
|
timer.Restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderer.DrawBenchmarkOverlay(journey);
|
||||||
|
WritePng(Path.Combine(directory,"galaxy-journey-results.png"),width,height,Read(gl,width,height),false);
|
||||||
|
if(gl.GetError() is var error && error!=GLEnum.NoError)throw new InvalidOperationException($"Galaxy journey GL error: {error}");
|
||||||
|
var report=journey.SaveReport();
|
||||||
|
Console.WriteLine($"Galaxy journey completed; target {journey.TargetGalaxy.Id} / {journey.TargetPlanet.Id}; average {journey.AverageFps:F1} FPS; 1% low {journey.OnePercentLowFps:F1} FPS; performance passed {journey.PerformancePassed}; report {report}");
|
||||||
|
if(!journey.PerformancePassed)Environment.ExitCode=2;
|
||||||
|
}
|
||||||
|
public static void RunBlackHole(GL gl, UniverseRenderer renderer, UniverseNeighborhood neighborhood, StarData hole,
|
||||||
|
double cellSize, int width, int height)
|
||||||
|
{
|
||||||
|
var center = UniversePosition.AtCellCenter(neighborhood.ActiveSystem.Coordinate, cellSize);
|
||||||
|
var body = center.Add(hole.PositionMeters, cellSize);
|
||||||
|
var camera = new FlyCamera(body.Add(new DVector3(0, 0, hole.RadiusMeters * 80), cellSize), cellSize);
|
||||||
|
camera.RequestBlackHoleView(neighborhood.ActiveSystem, hole, [neighborhood.ActiveSystem]);
|
||||||
|
for (var i = 0; i < 600; i++) camera.Update(1d / 60, DVector3.Zero, neighborhood);
|
||||||
|
for (var i = 0; i < 4; i++) renderer.Render(camera, neighborhood, 1d / 60);
|
||||||
|
renderer.ProfileFrame = true;
|
||||||
|
renderer.Render(camera, neighborhood, 1d / 60);
|
||||||
|
renderer.ProfileFrame = false;
|
||||||
|
gl.Finish();
|
||||||
|
if (gl.GetError() is var error && error != GLEnum.NoError) throw new InvalidOperationException($"Black hole GL error: {error}");
|
||||||
|
var pixels = Read(gl, width, height);
|
||||||
|
if (pixels.Any(p => !float.IsFinite(p)) || pixels.Where((_, i) => i % 4 != 3).Max() < .2)
|
||||||
|
throw new InvalidOperationException("Black hole produced invalid or empty output.");
|
||||||
|
var directory = Path.Combine(AppContext.BaseDirectory, "graphics-report"); Directory.CreateDirectory(directory);
|
||||||
|
var path = Path.Combine(directory, "black-hole.png");
|
||||||
|
WritePng(path, width, height, pixels, false);
|
||||||
|
Console.WriteLine($"Black hole: {hole.Name}; {hole.BlackHole!.MassSolar:F0} solar masses; camera {camera.Position.RelativeTo(body, cellSize).Length / hole.RadiusMeters:F2} horizons; lensing {renderer.FrameTimings["black hole lensing"]:F2} ms at {width}x{height}; image {path}");
|
||||||
|
}
|
||||||
|
|
||||||
public static void RunTerrainPerformance(GL gl, UniverseRenderer renderer, UniverseNeighborhood neighborhood, double cellSize, int width, int height)
|
public static void RunTerrainPerformance(GL gl, UniverseRenderer renderer, UniverseNeighborhood neighborhood, double cellSize, int width, int height)
|
||||||
{
|
{
|
||||||
var system = neighborhood.ActiveSystem;
|
var system = neighborhood.ActiveSystem;
|
||||||
@@ -51,12 +227,7 @@ internal static class GraphicsReport
|
|||||||
var dir = Path.Combine(AppContext.BaseDirectory, "graphics-report"); Directory.CreateDirectory(dir);
|
var dir = Path.Combine(AppContext.BaseDirectory, "graphics-report"); Directory.CreateDirectory(dir);
|
||||||
WritePng(Path.Combine(dir, "pause-menu.png"), width, height, pixels, false);
|
WritePng(Path.Combine(dir, "pause-menu.png"), width, height, pixels, false);
|
||||||
var scale = System.Math.Min(width / 1440d, height / 900d);
|
var scale = System.Math.Min(width / 1440d, height / 900d);
|
||||||
if (renderer.MenuHit(width * .5, height * .5 - 152 * scale, width, height) != 0
|
if (Enumerable.Range(0,7).Any(i => renderer.MenuHit(width*.5,height*.5+(-182+i*57)*scale,width,height)!=i)
|
||||||
|| renderer.MenuHit(width * .5, height * .5 - 95 * scale, width, height) != 1
|
|
||||||
|| renderer.MenuHit(width * .5, height * .5 - 38 * scale, width, height) != 2
|
|
||||||
|| renderer.MenuHit(width * .5, height * .5 + 19 * scale, width, height) != 3
|
|
||||||
|| renderer.MenuHit(width * .5, height * .5 + 76 * scale, width, height) != 4
|
|
||||||
|| renderer.MenuHit(width * .5, height * .5 + 133 * scale, width, height) != 5
|
|
||||||
|| renderer.MenuHit(0, 0, width, height) != -1)
|
|| renderer.MenuHit(0, 0, width, height) != -1)
|
||||||
throw new InvalidOperationException("Menu hit testing failed.");
|
throw new InvalidOperationException("Menu hit testing failed.");
|
||||||
Console.WriteLine($"Pause menu rendered and hit testing passed. Image: {dir}");
|
Console.WriteLine($"Pause menu rendered and hit testing passed. Image: {dir}");
|
||||||
@@ -183,6 +354,7 @@ internal static class GraphicsReport
|
|||||||
|
|
||||||
public static void RunOcean(GL gl, UniverseRenderer renderer, UniverseNeighborhood neighborhood, double cellSize, int width, int height)
|
public static void RunOcean(GL gl, UniverseRenderer renderer, UniverseNeighborhood neighborhood, double cellSize, int width, int height)
|
||||||
{
|
{
|
||||||
|
ValidateOceanReflections(gl);
|
||||||
var directory = Path.Combine(AppContext.BaseDirectory, "graphics-report"); Directory.CreateDirectory(directory);
|
var directory = Path.Combine(AppContext.BaseDirectory, "graphics-report"); Directory.CreateDirectory(directory);
|
||||||
var origin = UniversePosition.AtCellCenter(neighborhood.ActiveSystem.Coordinate, cellSize);
|
var origin = UniversePosition.AtCellCenter(neighborhood.ActiveSystem.Coordinate, cellSize);
|
||||||
var camera = new FlyCamera(origin, cellSize);
|
var camera = new FlyCamera(origin, cellSize);
|
||||||
@@ -206,12 +378,96 @@ internal static class GraphicsReport
|
|||||||
var after = Capture("earth-ocean-animated.png");
|
var after = Capture("earth-ocean-animated.png");
|
||||||
var difference = before.Zip(after, (a, b) => System.Math.Abs(a - b)).Average();
|
var difference = before.Zip(after, (a, b) => System.Math.Abs(a - b)).Average();
|
||||||
if (difference < .00001) throw new InvalidOperationException("Ocean animation did not change the rendered image.");
|
if (difference < .00001) throw new InvalidOperationException("Ocean animation did not change the rendered image.");
|
||||||
|
renderer.SimulationTimeSeconds = 12;
|
||||||
|
UniverseNeighborhood WithoutSsr(UniverseNeighborhood world) => world with { ActiveSystem = world.ActiveSystem with
|
||||||
|
{ Planets = world.ActiveSystem.Planets.Select(p => p.Ocean is { } ocean ? p with
|
||||||
|
{ Ocean = ocean with { ScreenSpaceReflections = ocean.ScreenSpaceReflections with { Enabled = false } } } : p).ToArray() } };
|
||||||
|
float[] Snapshot(UniverseNeighborhood world, string file)
|
||||||
|
{
|
||||||
|
camera.SetPlayerPose(camera.Position,camera.Forward,camera.Up,resetMotionHistory:true);
|
||||||
|
renderer.Render(camera,world,0); gl.Finish();
|
||||||
|
if(gl.GetError()!=GLEnum.NoError)throw new InvalidOperationException("Ocean SSR GL error.");
|
||||||
|
var pixels=Read(gl,width,height);WritePng(Path.Combine(directory,file),width,height,pixels,false);return pixels;
|
||||||
|
}
|
||||||
|
Snapshot(WithoutSsr(neighborhood),"earth-ocean-reflections-off.png");
|
||||||
|
var withReflections=Snapshot(neighborhood,"earth-ocean-reflections-on.png");
|
||||||
|
var withoutReflections=Snapshot(WithoutSsr(neighborhood),"earth-ocean-reflections-off.png");
|
||||||
|
var reflectionDifference=withReflections.Zip(withoutReflections,(a,b)=>System.Math.Abs(a-b)).Average();
|
||||||
|
if(reflectionDifference<.0001)throw new InvalidOperationException($"Water reflections did not change the production image: {reflectionDifference}.");
|
||||||
|
renderer.ProfileFrame=true;Snapshot(neighborhood,"earth-ocean-reflections-on.png");renderer.ProfileFrame=false;
|
||||||
|
var reflectionMs=renderer.FrameTimings.GetValueOrDefault("water reflections");
|
||||||
|
var earth=neighborhood.ActiveSystem.Planets.Single(p=>p.Id=="earth");
|
||||||
|
var up=(camera.Position.RelativeTo(origin,cellSize)-earth.PositionMeters).Normalized();
|
||||||
|
var night=neighborhood with { ActiveSystem=neighborhood.ActiveSystem with
|
||||||
|
{ Stars=neighborhood.ActiveSystem.Stars.Select(s=>s with {PositionMeters=earth.PositionMeters-up*UniverseScale.AstronomicalUnitMeters}).ToArray() } };
|
||||||
|
Snapshot(WithoutSsr(night),"earth-ocean-night-reflections-off.png");
|
||||||
|
var nightOn=Snapshot(night,"earth-ocean-night-reflections-on.png");
|
||||||
|
var nightOff=Snapshot(WithoutSsr(night),"earth-ocean-night-reflections-off.png");
|
||||||
|
var nightDifference=nightOn.Zip(nightOff,(a,b)=>System.Math.Abs(a-b)).Average();
|
||||||
|
if(nightDifference<.00001)throw new InvalidOperationException("The night sky did not reflect in the water.");
|
||||||
|
renderer.SimulationTimeSeconds = null;
|
||||||
for (var i = 0; i < 18; i++) camera.Update(.1, -camera.Up, neighborhood);
|
for (var i = 0; i < 18; i++) camera.Update(.1, -camera.Up, neighborhood);
|
||||||
Capture("earth-underwater.png");
|
Capture("earth-underwater.png");
|
||||||
camera.RequestFocus([neighborhood.ActiveSystem], "earth");
|
camera.RequestFocus([neighborhood.ActiveSystem], "earth");
|
||||||
for (var i = 0; i < 400; i++) camera.Update(1d / 60, DVector3.Zero, neighborhood);
|
for (var i = 0; i < 400; i++) camera.Update(1d / 60, DVector3.Zero, neighborhood);
|
||||||
Capture("earth-ocean-orbit.png");
|
Capture("earth-ocean-orbit.png");
|
||||||
Console.WriteLine($"Ocean scene passed: finite frames, wave animation delta {difference:G5}, underwater and orbit views. Full rendering {timer.Elapsed.TotalMilliseconds / 10:F1} ms/frame. Images: {directory}");
|
Console.WriteLine($"Ocean scene passed: finite frames, wave animation delta {difference:G5}, reflection delta {reflectionDifference:G5} day / {nightDifference:G5} night; SSR {reflectionMs:F2} ms at {width}x{height}; underwater and orbit views. Full rendering {timer.Elapsed.TotalMilliseconds / 10:F1} ms/frame. Images: {directory}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateOceanReflections(GL gl)
|
||||||
|
{
|
||||||
|
const int width=640,height=360; const float far=100000;
|
||||||
|
using var source=new RenderTarget(gl,width,height,true,auxiliaryColors:2);
|
||||||
|
using var reflection=new RenderTarget(gl,width/2,height/2);
|
||||||
|
using var composite=new RenderTarget(gl,width,height);
|
||||||
|
using var fullscreen=new GpuMesh(gl,3,[-1,-1,0,3,-1,0,-1,3,0]);
|
||||||
|
using var fixture=ShaderProgram.Create(gl,ShaderSources.Load("fullscreen.vert"),"""
|
||||||
|
#version 330 core
|
||||||
|
in vec2 vUv;
|
||||||
|
layout(location=0)out vec4 color;
|
||||||
|
layout(location=1)out vec4 normal;
|
||||||
|
layout(location=2)out vec4 fallback;
|
||||||
|
void main(){
|
||||||
|
vec2 p=(vUv*2.0-1.0)*.67;
|
||||||
|
vec3 ray=normalize(vec3(p.x*1.7777778,p.y,-1));
|
||||||
|
float wall=-12.0/ray.z,water=ray.y<0.0?-2.0/ray.y:1e8;
|
||||||
|
vec3 hit=ray*wall;
|
||||||
|
bool object=abs(hit.x)<3.0&&hit.y>-2.0&&hit.y<4.0;
|
||||||
|
float t=object?wall:1e8;
|
||||||
|
color=vec4(.08,.2,.4,1);normal=vec4(0);fallback=vec4(0);
|
||||||
|
if(object)color=vec4(2,.04,.02,1);
|
||||||
|
if(water<t){t=water;color=vec4(.03,.07,.12,1);normal=vec4(0,.7,0,1);fallback=vec4(.03,.07,.12,1);}
|
||||||
|
gl_FragDepth=t<1e7?log2(1.0-t*ray.z)/log2(100001.0):1.0;
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
source.Bind(allColors:true);gl.Disable(GLEnum.Blend);gl.Enable(GLEnum.DepthTest);gl.DepthFunc(GLEnum.Always);gl.DepthMask(true);
|
||||||
|
fixture.Use();fullscreen.Draw();source.GenerateColorMipmaps();
|
||||||
|
source.Bind();gl.Finish();var original=Read(gl,width,height);
|
||||||
|
using var trace=ShaderProgram.Create(gl,ShaderSources.Load("fullscreen.vert"),ShaderSources.Load("ocean-reflection.frag"));
|
||||||
|
using var resolve=ShaderProgram.Create(gl,ShaderSources.Load("fullscreen.vert"),ShaderSources.Load("ocean-reflection-composite.frag"));
|
||||||
|
void Bind(int unit,uint texture){gl.ActiveTexture((GLEnum)((int)GLEnum.Texture0+unit));gl.BindTexture(GLEnum.Texture2D,texture);}
|
||||||
|
reflection.Bind();gl.Disable(GLEnum.DepthTest);gl.DepthMask(false);trace.Use();
|
||||||
|
Bind(0,source.Color);Bind(1,source.Depth);Bind(2,source.AuxiliaryColors[0]);
|
||||||
|
trace.Set("uScene",0);trace.Set("uDepth",1);trace.Set("uWaterNormals",2);trace.Set("uFar",far);
|
||||||
|
trace.Set("uForward",new Vector3(0,0,-1));trace.Set("uRight",Vector3.UnitX);trace.Set("uUp",Vector3.UnitY);
|
||||||
|
trace.Set("uAspect",(float)width/height);trace.Set("uTanHalfFov",.67f);
|
||||||
|
trace.Set("uMaxSteps",48);trace.Set("uTraceSettings",new Vector3(100,1.5f,.12f));
|
||||||
|
trace.Set("uReflectionSettings",new Vector3(.07f,2,.08f));trace.Set("uMaxSurfaceDistance",1000f);
|
||||||
|
trace.Set("uReflectionStrength",1f);trace.Set("uReflectSky",0);fullscreen.Draw();gl.Finish();
|
||||||
|
var raw=Read(gl,width/2,height/2);var offset=(height/2/5*(width/2)+width/4)*4;
|
||||||
|
if(raw[offset+3]<.4 || raw[offset]<raw[offset+1]*4)
|
||||||
|
throw new InvalidOperationException($"SSR did not reflect the visible red wall: {string.Join(",",raw.Skip(offset).Take(4))}.");
|
||||||
|
composite.Bind();resolve.Use();Bind(0,source.Color);Bind(1,source.Depth);Bind(2,source.AuxiliaryColors[0]);
|
||||||
|
Bind(3,source.AuxiliaryColors[1]);Bind(5,reflection.Color);
|
||||||
|
resolve.Set("uScene",0);resolve.Set("uDepth",1);resolve.Set("uWaterNormals",2);resolve.Set("uWaterFallback",3);
|
||||||
|
resolve.Set("uReflection",5);resolve.Set("uClouds",0);resolve.Set("uHasClouds",0);resolve.Set("uFar",far);fullscreen.Draw();gl.Finish();
|
||||||
|
var result=Read(gl,width,height);
|
||||||
|
var landError=Enumerable.Range(width*height/2,width*height/2).SelectMany(i=>Enumerable.Range(0,3).Select(c=>System.Math.Abs(original[i*4+c]-result[i*4+c]))).Max();
|
||||||
|
if(landError>.001 || gl.GetError()!=GLEnum.NoError)throw new InvalidOperationException($"SSR modified non-water geometry: {landError}.");
|
||||||
|
var dir=Path.Combine(AppContext.BaseDirectory,"graphics-report");Directory.CreateDirectory(dir);
|
||||||
|
WritePng(Path.Combine(dir,"ocean-ssr-wall-test.png"),width,height,result);
|
||||||
|
Console.WriteLine($"SSR GPU test passed: visible wall reflected, confidence {raw[offset+3]:F3}, non-water error {landError:G3}.");
|
||||||
|
gl.BindFramebuffer(GLEnum.Framebuffer,0);gl.Enable(GLEnum.DepthTest);gl.DepthFunc(GLEnum.Lequal);gl.DepthMask(true);
|
||||||
}
|
}
|
||||||
public static void RunScene(GL gl, UniverseRenderer renderer, UniverseNeighborhood neighborhood, double cellSize, int width, int height)
|
public static void RunScene(GL gl, UniverseRenderer renderer, UniverseNeighborhood neighborhood, double cellSize, int width, int height)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ internal sealed class PauseMenu : IDisposable
|
|||||||
private readonly GpuMesh _mesh;
|
private readonly GpuMesh _mesh;
|
||||||
public int Selected { get; set; }
|
public int Selected { get; set; }
|
||||||
public PauseMenu(GL gl) => _mesh = new(gl, 6, [], dynamic: true);
|
public PauseMenu(GL gl) => _mesh = new(gl, 6, [], dynamic: true);
|
||||||
private static readonly string[] Labels = ["PLAY", "FREE CAMERA", "DEBUG OFF", "BENCHMARK EARTH", "BENCHMARK UNKNOWN", "QUIT"];
|
private static readonly string[] Labels = ["PLAY", "FREE CAMERA", "DEBUG OFF", "BENCHMARK EARTH", "BENCHMARK UNKNOWN", "GALAXY JOURNEY", "QUIT"];
|
||||||
internal static readonly Dictionary<char, string[]> Font = new()
|
internal static readonly Dictionary<char, string[]> Font = new()
|
||||||
{
|
{
|
||||||
['A']=["01110","10001","10001","11111","10001","10001","10001"], ['C']=["01111","10000","10000","10000","10000","10000","01111"],
|
['A']=["01110","10001","10001","11111","10001","10001","10001"], ['C']=["01111","10000","10000","10000","10000","10000","01111"],
|
||||||
@@ -18,6 +18,7 @@ internal sealed class PauseMenu : IDisposable
|
|||||||
['H']=["10001","10001","10001","11111","10001","10001","10001"], ['N']=["10001","11001","10101","10011","10001","10001","10001"],
|
['H']=["10001","10001","10001","11111","10001","10001","10001"], ['N']=["10001","11001","10101","10011","10001","10001","10001"],
|
||||||
['V']=["10001","10001","10001","10001","10001","01010","00100"], ['W']=["10001","10001","10001","10101","10101","10101","01010"],
|
['V']=["10001","10001","10001","10001","10001","01010","00100"], ['W']=["10001","10001","10001","10101","10101","10101","01010"],
|
||||||
['K']=["10001","10010","10100","11000","10100","10010","10001"],
|
['K']=["10001","10010","10100","11000","10100","10010","10001"],
|
||||||
|
['J']=["00111","00010","00010","00010","10010","10010","01100"], ['X']=["10001","10001","01010","00100","01010","10001","10001"],
|
||||||
['L']=["10000","10000","10000","10000","10000","10000","11111"], ['M']=["10001","11011","10101","10101","10001","10001","10001"],
|
['L']=["10000","10000","10000","10000","10000","10000","11111"], ['M']=["10001","11011","10101","10101","10001","10001","10001"],
|
||||||
['O']=["01110","10001","10001","10001","10001","10001","01110"], ['P']=["11110","10001","10001","11110","10000","10000","10000"],
|
['O']=["01110","10001","10001","10001","10001","10001","01110"], ['P']=["11110","10001","10001","11110","10000","10000","10000"],
|
||||||
['Q']=["01110","10001","10001","10001","10101","10010","01101"], ['R']=["11110","10001","10001","11110","10100","10010","10001"],
|
['Q']=["01110","10001","10001","10001","10101","10010","01101"], ['R']=["11110","10001","10001","11110","10100","10010","10001"],
|
||||||
@@ -44,11 +45,22 @@ internal sealed class PauseMenu : IDisposable
|
|||||||
var (s, cx, cy) = Layout(width, height);
|
var (s, cx, cy) = Layout(width, height);
|
||||||
if (s <= 0 || x < cx - 190 * s || x > cx + 190 * s) return -1;
|
if (s <= 0 || x < cx - 190 * s || x > cx + 190 * s) return -1;
|
||||||
for (var i = 0; i < Labels.Length; i++)
|
for (var i = 0; i < Labels.Length; i++)
|
||||||
if (y >= cy + (-174 + i * 57) * s && y <= cy + (-130 + i * 57) * s) return i;
|
if (y >= cy + (-204 + i * 57) * s && y <= cy + (-160 + i * 57) * s) return i;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Draw(int width, int height, bool debugEnabled)
|
public static double? TimeScaleHit(double x, double y, int width, int height)
|
||||||
|
{
|
||||||
|
var (s, cx, cy) = Layout(width, height);
|
||||||
|
x = (x - cx) / s; y = (y - cy) / s;
|
||||||
|
if (x >= 290 && x <= 610 && y >= -80 && y <= -40) return System.Math.Round((x - 290) / 320 * 100);
|
||||||
|
if (y >= 0 && y <= 38)
|
||||||
|
for (var i = 0; i < 4; i++)
|
||||||
|
if (x >= 290 + i * 82 && x <= 364 + i * 82) return new double[] { 0, 1, 10, 100 }[i];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Draw(int width, int height, bool debugEnabled, double timeScale = 1, DateTimeOffset? simulationUtc = null)
|
||||||
{
|
{
|
||||||
var (s, cx, cy) = Layout(width, height);
|
var (s, cx, cy) = Layout(width, height);
|
||||||
var data = new List<float>();
|
var data = new List<float>();
|
||||||
@@ -70,19 +82,39 @@ internal sealed class PauseMenu : IDisposable
|
|||||||
x += 6 * unit;
|
x += 6 * unit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Rect(cx - 230*s, cy - 260*s, 460*s, 520*s, .025f, .04f, .075f);
|
Rect(cx - 230*s, cy - 290*s, 460*s, 580*s, .025f, .04f, .075f);
|
||||||
Rect(cx - 230*s, cy - 260*s, 460*s, 4*s, .25f, .65f, .9f);
|
Rect(cx - 230*s, cy - 290*s, 460*s, 4*s, .25f, .65f, .9f);
|
||||||
Text("PAUSED", cx, cy - 226*s, 5*s, .9f, .95f, 1);
|
Text("PAUSED", cx, cy - 256*s, 5*s, .9f, .95f, 1);
|
||||||
for (var i = 0; i < Labels.Length; i++)
|
for (var i = 0; i < Labels.Length; i++)
|
||||||
{
|
{
|
||||||
var selected = i == Selected;
|
var selected = i == Selected;
|
||||||
var y = cy + (-174 + i * 57) * s;
|
var y = cy + (-204 + i * 57) * s;
|
||||||
Rect(cx - 190*s, y, 380*s, 44*s, selected ? .12f : .055f, selected ? .25f : .10f, selected ? .36f : .16f);
|
Rect(cx - 190*s, y, 380*s, 44*s, selected ? .12f : .055f, selected ? .25f : .10f, selected ? .36f : .16f);
|
||||||
if (selected) Rect(cx - 190*s, y, 4*s, 44*s, .35f, .8f, 1);
|
if (selected) Rect(cx - 190*s, y, 4*s, 44*s, .35f, .8f, 1);
|
||||||
var label = i == 2 ? debugEnabled ? "DEBUG ON" : "DEBUG OFF" : Labels[i];
|
var label = i == 2 ? debugEnabled ? "DEBUG ON" : "DEBUG OFF" : Labels[i];
|
||||||
Text(label, cx, y + 12*s, (i >= 3 ? 3.35 : 4) * s, .92f, .95f, 1);
|
Text(label, cx, y + 12*s, (i >= 3 ? 3.35 : 4) * s, .92f, .95f, 1);
|
||||||
}
|
}
|
||||||
Text("P OR ESC TO RESUME", cx, cy + 220*s, 2*s, .6f, .7f, .8f);
|
Text("P OR ESC TO RESUME", cx, cy + 250*s, 2*s, .6f, .7f, .8f);
|
||||||
|
if (debugEnabled)
|
||||||
|
{
|
||||||
|
Rect(cx + 250*s, cy - 180*s, 400*s, 340*s, .025f, .04f, .075f);
|
||||||
|
Text("WORLD TIME", cx + 450*s, cy - 150*s, 3.5*s, .9f, .95f, 1);
|
||||||
|
Text($"SPEED {timeScale:0.##} X", cx + 450*s, cy - 108*s, 2.8*s, .4f, .85f, 1);
|
||||||
|
Rect(cx + 290*s, cy - 64*s, 320*s, 8*s, .13f, .24f, .34f);
|
||||||
|
Rect(cx + 290*s, cy - 64*s, timeScale / 100 * 320*s, 8*s, .3f, .7f, .95f);
|
||||||
|
Rect(cx + (286 + timeScale / 100 * 320)*s, cy - 74*s, 8*s, 28*s, .8f, .95f, 1);
|
||||||
|
Text("0 X 100 X", cx + 450*s, cy - 30*s, 1.8*s, .7f, .8f, .9f);
|
||||||
|
var speeds = new[] { "0 X", "1 X", "10 X", "100 X" };
|
||||||
|
for (var i = 0; i < speeds.Length; i++)
|
||||||
|
{
|
||||||
|
Rect(cx + (290 + i * 82)*s, cy, 74*s, 38*s, .08f, .18f, .28f);
|
||||||
|
Text(speeds[i], cx + (327 + i * 82)*s, cy + 12*s, 2*s, .9f, .95f, 1);
|
||||||
|
}
|
||||||
|
Text("LEFT / RIGHT TO ADJUST", cx + 450*s, cy + 60*s, 2*s, .65f, .8f, .9f);
|
||||||
|
Text("PAUSED IN MENU", cx + 450*s, cy + 88*s, 2*s, .65f, .8f, .9f);
|
||||||
|
Text((simulationUtc ?? Universe.Core.Physics.SimulationClock.J2000).ToString("yyyy-MM-dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture),
|
||||||
|
cx + 450*s, cy + 121*s, 2*s, .7f, .85f, .95f);
|
||||||
|
}
|
||||||
_mesh.ReplaceVertices(data.ToArray()); _mesh.Draw(GLEnum.Triangles);
|
_mesh.ReplaceVertices(data.ToArray()); _mesh.Draw(GLEnum.Triangles);
|
||||||
}
|
}
|
||||||
public void Dispose() => _mesh.Dispose();
|
public void Dispose() => _mesh.Dispose();
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ internal sealed class PerformanceOverlay : IDisposable
|
|||||||
public PerformanceOverlay(GL gl) => _mesh = new(gl, 6, [], dynamic: true);
|
public PerformanceOverlay(GL gl) => _mesh = new(gl, 6, [], dynamic: true);
|
||||||
|
|
||||||
public void Draw(int width, int height, IReadOnlyList<KeyValuePair<string, double>> samples,
|
public void Draw(int width, int height, IReadOnlyList<KeyValuePair<string, double>> samples,
|
||||||
double cpuMilliseconds, double fps, int terrainTiles, int terrainLod, int pendingTiles, string? probePlanetId)
|
double cpuMilliseconds, double fps, int terrainTiles, int terrainLod, int pendingTiles, string? probePlanetId,
|
||||||
|
double timeScale = 1, DateTimeOffset? simulationUtc = null)
|
||||||
{
|
{
|
||||||
var values = samples.ToDictionary(p => p.Key, p => p.Value, StringComparer.Ordinal);
|
var values = samples.ToDictionary(p => p.Key, p => p.Value, StringComparer.Ordinal);
|
||||||
var rows = new List<(string Label, string Value, bool Emphasis)>();
|
var rows = new List<(string Label, string Value, bool Emphasis)>();
|
||||||
AddMetric("GPU FRAME", "GPU FRAME", true);
|
AddMetric("GPU FRAME", "GPU FRAME", true);
|
||||||
AddMetric("SKY / STARFIELD", "SKY / STARFIELD");
|
AddMetric("SKY / STARFIELD", "SKY / STARFIELD");
|
||||||
|
AddMetric("GALAXIES / NEBULAE", "GALAXIES / NEBULAE");
|
||||||
AddMetric("PLANETS TOTAL", "PLANETS TOTAL");
|
AddMetric("PLANETS TOTAL", "PLANETS TOTAL");
|
||||||
foreach (var key in values.Keys.Where(k => k.StartsWith("PLANET ", StringComparison.Ordinal) && k != "PLANETS TOTAL" && k != "GPU FRAME")
|
foreach (var key in values.Keys.Where(k => k.StartsWith("PLANET ", StringComparison.Ordinal) && k != "PLANETS TOTAL" && k != "GPU FRAME")
|
||||||
.OrderBy(k => k, StringComparer.Ordinal))
|
.OrderBy(k => k, StringComparer.Ordinal))
|
||||||
@@ -33,15 +35,19 @@ internal sealed class PerformanceOverlay : IDisposable
|
|||||||
AddMetric("GAS GIANTS", "GAS GIANTS");
|
AddMetric("GAS GIANTS", "GAS GIANTS");
|
||||||
AddMetric("RINGS / ROCKS", "RINGS / ROCKS");
|
AddMetric("RINGS / ROCKS", "RINGS / ROCKS");
|
||||||
AddMetric("OCEAN", "OCEAN");
|
AddMetric("OCEAN", "OCEAN");
|
||||||
|
AddMetric("WATER REFLECTIONS", "WATER REFLECTIONS");
|
||||||
AddMetric("ATMOSPHERE", "ATMOSPHERE");
|
AddMetric("ATMOSPHERE", "ATMOSPHERE");
|
||||||
AddMetric("CLOUDS / COMPOSITE", "CLOUDS / COMPOSITE");
|
AddMetric("CLOUDS / COMPOSITE", "CLOUDS / COMPOSITE");
|
||||||
AddMetric("SUN GLOW", "SUN GLOW");
|
AddMetric("SUN GLOW", "SUN GLOW");
|
||||||
|
AddMetric("BLACK HOLE / LENSING", "BLACK HOLE / LENSING");
|
||||||
AddMetric("BLOOM / POST", "BLOOM / POST");
|
AddMetric("BLOOM / POST", "BLOOM / POST");
|
||||||
rows.Add(("SHADOWS IN PLANET", "SEE PLANET MS", false));
|
rows.Add(("SHADOWS IN PLANET", "SEE PLANET MS", false));
|
||||||
rows.Add(("CPU SUBMIT", $"{Number(cpuMilliseconds)} MS", false));
|
rows.Add(("CPU SUBMIT", $"{Number(cpuMilliseconds)} MS", false));
|
||||||
rows.Add(("FPS", $"{Number(fps, "F1")}", false));
|
rows.Add(("FPS", $"{Number(fps, "F1")}", false));
|
||||||
rows.Add(("TERRAIN TILES", terrainTiles.ToString(CultureInfo.InvariantCulture), false));
|
rows.Add(("TERRAIN TILES", terrainTiles.ToString(CultureInfo.InvariantCulture), false));
|
||||||
rows.Add(("LOD / PENDING", $"{terrainLod} / {pendingTiles}", false));
|
rows.Add(("LOD / PENDING", $"{terrainLod} / {pendingTiles}", false));
|
||||||
|
rows.Add(("TIME SCALE", $"{Number(timeScale, "F1")} X", true));
|
||||||
|
if (simulationUtc is { } date) rows.Add(("WORLD UTC", date.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), false));
|
||||||
|
|
||||||
var scale = System.Math.Clamp(System.Math.Min(width / 1440d, height / 900d), .58, 1.25);
|
var scale = System.Math.Clamp(System.Math.Min(width / 1440d, height / 900d), .58, 1.25);
|
||||||
var x = 14 * scale;
|
var x = 14 * scale;
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ internal sealed unsafe class RenderTarget : IDisposable
|
|||||||
public uint Framebuffer { get; }
|
public uint Framebuffer { get; }
|
||||||
public uint Color { get; }
|
public uint Color { get; }
|
||||||
public uint Depth { get; }
|
public uint Depth { get; }
|
||||||
|
public uint[] AuxiliaryColors { get; }
|
||||||
public int Width { get; }
|
public int Width { get; }
|
||||||
public int Height { get; }
|
public int Height { get; }
|
||||||
|
|
||||||
public RenderTarget(GL gl, int width, int height, bool depth = false, bool fullPrecision = false)
|
public RenderTarget(GL gl, int width, int height, bool depth = false, bool fullPrecision = false, int auxiliaryColors = 0)
|
||||||
{
|
{
|
||||||
_gl = gl;
|
_gl = gl;
|
||||||
Width = width;
|
Width = width;
|
||||||
@@ -23,6 +24,15 @@ internal sealed unsafe class RenderTarget : IDisposable
|
|||||||
gl.TexImage2D(GLEnum.Texture2D, 0, fullPrecision ? InternalFormat.Rgba32f : InternalFormat.Rgba16f, (uint)width, (uint)height, 0, PixelFormat.Rgba, PixelType.Float, null);
|
gl.TexImage2D(GLEnum.Texture2D, 0, fullPrecision ? InternalFormat.Rgba32f : InternalFormat.Rgba16f, (uint)width, (uint)height, 0, PixelFormat.Rgba, PixelType.Float, null);
|
||||||
SetParameters(gl, GLEnum.Linear);
|
SetParameters(gl, GLEnum.Linear);
|
||||||
gl.FramebufferTexture2D(GLEnum.Framebuffer, GLEnum.ColorAttachment0, GLEnum.Texture2D, Color, 0);
|
gl.FramebufferTexture2D(GLEnum.Framebuffer, GLEnum.ColorAttachment0, GLEnum.Texture2D, Color, 0);
|
||||||
|
AuxiliaryColors = new uint[auxiliaryColors];
|
||||||
|
for (var i = 0; i < auxiliaryColors; i++)
|
||||||
|
{
|
||||||
|
var texture = gl.GenTexture(); AuxiliaryColors[i] = texture;
|
||||||
|
gl.BindTexture(GLEnum.Texture2D, texture);
|
||||||
|
gl.TexImage2D(GLEnum.Texture2D, 0, InternalFormat.Rgba16f, (uint)width, (uint)height, 0, PixelFormat.Rgba, PixelType.Float, null);
|
||||||
|
SetParameters(gl, GLEnum.Nearest);
|
||||||
|
gl.FramebufferTexture2D(GLEnum.Framebuffer, (GLEnum)((uint)GLEnum.ColorAttachment1 + i), GLEnum.Texture2D, texture, 0);
|
||||||
|
}
|
||||||
if (depth)
|
if (depth)
|
||||||
{
|
{
|
||||||
Depth = gl.GenTexture();
|
Depth = gl.GenTexture();
|
||||||
@@ -36,10 +46,24 @@ internal sealed unsafe class RenderTarget : IDisposable
|
|||||||
gl.BindFramebuffer(GLEnum.Framebuffer, 0);
|
gl.BindFramebuffer(GLEnum.Framebuffer, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Bind()
|
public void Bind(bool allColors = false)
|
||||||
{
|
{
|
||||||
_gl.BindFramebuffer(GLEnum.Framebuffer, Framebuffer);
|
_gl.BindFramebuffer(GLEnum.Framebuffer, Framebuffer);
|
||||||
_gl.Viewport(0, 0, (uint)Width, (uint)Height);
|
_gl.Viewport(0, 0, (uint)Width, (uint)Height);
|
||||||
|
if (allColors && AuxiliaryColors.Length > 0)
|
||||||
|
{
|
||||||
|
var attachments = stackalloc GLEnum[AuxiliaryColors.Length + 1];
|
||||||
|
for (var i = 0; i <= AuxiliaryColors.Length; i++) attachments[i] = (GLEnum)((uint)GLEnum.ColorAttachment0 + i);
|
||||||
|
_gl.DrawBuffers((uint)(AuxiliaryColors.Length + 1), attachments);
|
||||||
|
}
|
||||||
|
else _gl.DrawBuffer(GLEnum.ColorAttachment0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GenerateColorMipmaps()
|
||||||
|
{
|
||||||
|
_gl.BindTexture(GLEnum.Texture2D, Color);
|
||||||
|
_gl.GenerateMipmap(GLEnum.Texture2D);
|
||||||
|
_gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureMinFilter, (int)GLEnum.LinearMipmapLinear);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SetParameters(GL gl, GLEnum filter)
|
private static void SetParameters(GL gl, GLEnum filter)
|
||||||
@@ -53,6 +77,7 @@ internal sealed unsafe class RenderTarget : IDisposable
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_gl.DeleteTexture(Color);
|
_gl.DeleteTexture(Color);
|
||||||
|
foreach (var texture in AuxiliaryColors) _gl.DeleteTexture(texture);
|
||||||
if (Depth != 0) _gl.DeleteTexture(Depth);
|
if (Depth != 0) _gl.DeleteTexture(Depth);
|
||||||
_gl.DeleteFramebuffer(Framebuffer);
|
_gl.DeleteFramebuffer(Framebuffer);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,25 +17,30 @@ internal sealed unsafe class ShaderProgram : IDisposable
|
|||||||
|
|
||||||
public uint Handle { get; }
|
public uint Handle { get; }
|
||||||
|
|
||||||
public static ShaderProgram Create(GL gl, string vertexSource, string fragmentSource)
|
public static ShaderProgram Create(GL gl, string vertexSource, string fragmentSource, string? geometrySource = null)
|
||||||
{
|
{
|
||||||
var vertex = Compile(gl, ShaderType.VertexShader, vertexSource);
|
uint vertex = 0, fragment = 0, geometry = 0, program = 0;
|
||||||
var fragment = Compile(gl, ShaderType.FragmentShader, fragmentSource);
|
try
|
||||||
var program = gl.CreateProgram();
|
{
|
||||||
gl.AttachShader(program, vertex);
|
vertex = Compile(gl, ShaderType.VertexShader, vertexSource);
|
||||||
gl.AttachShader(program, fragment);
|
fragment = Compile(gl, ShaderType.FragmentShader, fragmentSource);
|
||||||
|
if (geometrySource is not null) geometry = Compile(gl, ShaderType.GeometryShader, geometrySource);
|
||||||
|
program = gl.CreateProgram();
|
||||||
|
gl.AttachShader(program, vertex); gl.AttachShader(program, fragment);
|
||||||
|
if (geometry != 0) gl.AttachShader(program, geometry);
|
||||||
gl.LinkProgram(program);
|
gl.LinkProgram(program);
|
||||||
gl.GetProgram(program, GLEnum.LinkStatus, out var linked);
|
gl.GetProgram(program, GLEnum.LinkStatus, out var linked);
|
||||||
gl.DeleteShader(vertex);
|
if (linked == 0) throw new InvalidOperationException($"OpenGL shader link failed: {gl.GetProgramInfoLog(program)}");
|
||||||
gl.DeleteShader(fragment);
|
|
||||||
if (linked == 0)
|
|
||||||
{
|
|
||||||
var log = gl.GetProgramInfoLog(program);
|
|
||||||
gl.DeleteProgram(program);
|
|
||||||
throw new InvalidOperationException($"OpenGL shader link failed: {log}");
|
|
||||||
}
|
|
||||||
return new(gl, program);
|
return new(gl, program);
|
||||||
}
|
}
|
||||||
|
catch { if (program != 0) gl.DeleteProgram(program); throw; }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (vertex != 0) gl.DeleteShader(vertex);
|
||||||
|
if (fragment != 0) gl.DeleteShader(fragment);
|
||||||
|
if (geometry != 0) gl.DeleteShader(geometry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Use() => _gl.UseProgram(Handle);
|
public void Use() => _gl.UseProgram(Handle);
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ internal static class ShaderSources
|
|||||||
}
|
}
|
||||||
private static string Expand(string name, string source)
|
private static string Expand(string name, string source)
|
||||||
{
|
{
|
||||||
foreach (var include in new[] { "noise", "planet-height", "camera-ray", "ring-optics", "rock-common", "asteroid-shape", "cloud-common" })
|
foreach (var include in new[] { "noise", "planet-height", "camera-ray", "ring-optics", "rock-common", "asteroid-shape", "cloud-common", "screen-space-reflection" })
|
||||||
{
|
{
|
||||||
var directive = $"#include \"{include}.glsl\"";
|
var directive = $"#include \"{include}.glsl\"";
|
||||||
if (source.Contains(directive, StringComparison.Ordinal))
|
if (source.Contains(directive, StringComparison.Ordinal))
|
||||||
|
|||||||
118
src/Universe.App/Graphics/Shaders/black-hole.frag.glsl
Normal file
118
src/Universe.App/Graphics/Shaders/black-hole.frag.glsl
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
#version 330 core
|
||||||
|
#include "camera-ray.glsl"
|
||||||
|
in vec2 vUv;
|
||||||
|
uniform sampler2D uScene;
|
||||||
|
uniform sampler2D uDepth;
|
||||||
|
uniform vec3 uCamera; // Relative to the hole, in Schwarzschild radii.
|
||||||
|
uniform vec3 uDiskX;
|
||||||
|
uniform vec3 uDiskNormal;
|
||||||
|
uniform vec3 uDiskZ;
|
||||||
|
uniform float uHorizonMeters;
|
||||||
|
uniform float uOuterRadius;
|
||||||
|
uniform float uBrightness;
|
||||||
|
uniform float uTime;
|
||||||
|
uniform float uSeed;
|
||||||
|
uniform float uFar;
|
||||||
|
uniform int uSteps;
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
float hash21(vec2 p) {
|
||||||
|
vec3 p3=fract(vec3(p.xyx)*.1031);
|
||||||
|
p3+=dot(p3,p3.yzx+33.33);
|
||||||
|
return fract((p3.x+p3.y)*p3.z);
|
||||||
|
}
|
||||||
|
float valueNoise(vec2 p) {
|
||||||
|
vec2 i=floor(p),f=fract(p); f=f*f*(3.0-2.0*f);
|
||||||
|
float y=mod(i.y,64.0),nextY=mod(i.y+1.0,64.0);
|
||||||
|
return mix(mix(hash21(vec2(i.x,y)),hash21(vec2(i.x+1.0,y)),f.x),
|
||||||
|
mix(hash21(vec2(i.x,nextY)),hash21(vec2(i.x+1.0,nextY)),f.x),f.y);
|
||||||
|
}
|
||||||
|
vec3 diskLight(vec3 p,vec3 ray) {
|
||||||
|
float radius=length(p),angle=atan(dot(p,uDiskZ),dot(p,uDiskX));
|
||||||
|
float rotation=uTime*.24/pow(radius/3.0,1.5);
|
||||||
|
float azimuth=angle*10.185916; // 64 cells around the disk: no atan seam.
|
||||||
|
vec2 flow=vec2(log(radius)*38.0,azimuth-rotation+log(radius)*11.0);
|
||||||
|
float turbulence=valueNoise(flow+uSeed)*.55+valueNoise(flow*vec2(2.03,2.0))*.28+valueNoise(flow*vec2(4.1,4.0))*.17;
|
||||||
|
float threads=valueNoise(vec2(radius*110.0+turbulence*3.0,azimuth*2.0-rotation));
|
||||||
|
float bands=valueNoise(vec2(log(radius)*92.0,azimuth-rotation*.3));
|
||||||
|
float structure=(.35+.95*turbulence)*(.55+.65*threads)*(.6+.5*bands);
|
||||||
|
float inner=smoothstep(3.0,3.5,radius);
|
||||||
|
float outer=1.0-smoothstep(uOuterRadius*.68,uOuterRadius,radius);
|
||||||
|
float thermal=pow(3.0/max(radius,3.0),1.7);
|
||||||
|
vec3 cool=vec3(.42,.16,.07),hot=vec3(1.0,.87,.68);
|
||||||
|
vec3 tint=mix(cool,hot,pow(clamp(thermal*1.4,0.0,1.0),.6));
|
||||||
|
// Local orbital velocity and Schwarzschild redshift. This is an artistic
|
||||||
|
// thin-disk emission model, rather than a full relativistic plasma simulation.
|
||||||
|
vec3 tangent=normalize(cross(uDiskNormal,p));
|
||||||
|
float beta=sqrt(.5/max(radius-1.0,1.0));
|
||||||
|
float doppler=sqrt(1.0-beta*beta)/max(.25,1.0-beta*dot(tangent,-normalize(ray)));
|
||||||
|
float redshift=sqrt(max(.05,1.0-1.0/radius));
|
||||||
|
float boost=clamp(pow(doppler*redshift,3.0),.18,3.5);
|
||||||
|
return tint*structure*inner*outer*thermal*boost*uBrightness;
|
||||||
|
}
|
||||||
|
vec3 sky(vec3 direction) {
|
||||||
|
float front=dot(direction,uForward);
|
||||||
|
if(front>.001) {
|
||||||
|
vec2 uv=vec2(dot(direction,uRight)/(front*uAspect),dot(direction,uUp)/front)/uTanHalfFov*.5+.5;
|
||||||
|
if(all(greaterThanEqual(uv,vec2(0)))&&all(lessThanEqual(uv,vec2(1))))return texture(uScene,uv).rgb;
|
||||||
|
}
|
||||||
|
// Rays can curl behind the camera. Fill those directions with a fixed
|
||||||
|
// world-oriented starfield, without mirroring the screen at its edges.
|
||||||
|
vec2 sphere=vec2(atan(direction.z,direction.x),asin(clamp(direction.y,-1.0,1.0)))*vec2(700,700);
|
||||||
|
vec2 cell=floor(sphere),q=fract(sphere)-.5;
|
||||||
|
float star=step(.996,hash21(cell))*exp(-dot(q,q)*150.0);
|
||||||
|
float band=pow(max(0.0,1.0-abs(dot(direction,normalize(vec3(.32,.72,.48))))),17.0);
|
||||||
|
return vec3(.00018,.00025,.0005)+vec3(.0013,.0016,.0026)*band+star*vec3(.35,.42,.55);
|
||||||
|
}
|
||||||
|
vec3 acceleration(vec3 p,float angularMomentumSquared) {
|
||||||
|
float r2=dot(p,p);
|
||||||
|
// The null-geodesic orbit equation u''+u=1.5*u² (rs=1), expressed
|
||||||
|
// as a central force with conserved angular momentum.
|
||||||
|
return -1.5*angularMomentumSquared*p/(r2*r2*sqrt(r2));
|
||||||
|
}
|
||||||
|
void main() {
|
||||||
|
vec4 original=texture(uScene,vUv);
|
||||||
|
vec3 rd=cameraRay(vUv),p=uCamera;
|
||||||
|
float domain=max(32.0,uOuterRadius*2.3);
|
||||||
|
float b=dot(p,rd),c=dot(p,p)-domain*domain,discriminant=b*b-c;
|
||||||
|
if(discriminant<0.0||-b+sqrt(max(0.0,discriminant))<0.0){fragColor=original;return;}
|
||||||
|
float entry=max(0.0,-b-sqrt(discriminant));
|
||||||
|
float sceneDepth=texture(uDepth,vUv).r;
|
||||||
|
float sceneDistance=(exp2(sceneDepth*log2(1.0+uFar))-1.0)/max(.001,dot(rd,uForward))/uHorizonMeters;
|
||||||
|
if(sceneDepth<.999999&&sceneDistance<entry){fragColor=original;return;}
|
||||||
|
p+=rd*entry;
|
||||||
|
vec3 velocity=rd;
|
||||||
|
vec3 momentum=cross(p,velocity);
|
||||||
|
float h2=dot(momentum,momentum);
|
||||||
|
vec3 emission=vec3(0);
|
||||||
|
float transmission=1.0;
|
||||||
|
bool captured=false,escaped=false;
|
||||||
|
for(int i=0;i<512;i++) {
|
||||||
|
if(i>=uSteps)break;
|
||||||
|
float r=length(p);
|
||||||
|
if(r<1.025){captured=true;break;}
|
||||||
|
if(r>domain+1.0&&dot(p,velocity)>0.0){escaped=true;break;}
|
||||||
|
float stepSize=clamp(r*.085,.025,2.5);
|
||||||
|
vec3 a=acceleration(p,h2);
|
||||||
|
vec3 middle=p+velocity*stepSize*.5;
|
||||||
|
vec3 next=p+(velocity+a*stepSize*.5)*stepSize;
|
||||||
|
vec3 nextVelocity=velocity+acceleration(middle,h2)*stepSize;
|
||||||
|
float height=dot(p,uDiskNormal),nextHeight=dot(next,uDiskNormal);
|
||||||
|
if(height*nextHeight<0.0) {
|
||||||
|
vec3 hit=mix(p,next,height/(height-nextHeight));
|
||||||
|
float diskRadius=length(hit);
|
||||||
|
if(diskRadius>3.0&&diskRadius<uOuterRadius) {
|
||||||
|
float opacity=.88*(1.0-smoothstep(uOuterRadius*.8,uOuterRadius,diskRadius));
|
||||||
|
emission+=transmission*diskLight(hit,velocity)*opacity;
|
||||||
|
transmission*=1.0-opacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p=next;velocity=nextVelocity;
|
||||||
|
if(transmission<.008)break;
|
||||||
|
}
|
||||||
|
vec3 background=(captured||(!escaped&&transmission>.008))?vec3(0):sky(normalize(velocity));
|
||||||
|
float impact=length(cross(uCamera,rd));
|
||||||
|
float photonRing=exp(-pow((impact-2.59808)/.022,2.0))*.15;
|
||||||
|
vec3 color=emission+background*transmission+vec3(1.0,.66,.34)*photonRing;
|
||||||
|
fragColor=vec4(color,original.a);
|
||||||
|
}
|
||||||
4
src/Universe.App/Graphics/Shaders/cosmic.frag.glsl
Normal file
4
src/Universe.App/Graphics/Shaders/cosmic.frag.glsl
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#version 330 core
|
||||||
|
in vec3 vColor;
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main(){vec2 p=gl_PointCoord*2.0-1.0;float r=dot(p,p);if(r>1.0)discard;fragColor=vec4(vColor*exp(-r*3.5),0);}
|
||||||
23
src/Universe.App/Graphics/Shaders/cosmic.vert.glsl
Normal file
23
src/Universe.App/Graphics/Shaders/cosmic.vert.glsl
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#version 330 core
|
||||||
|
layout(location=0) in vec3 aPosition;
|
||||||
|
layout(location=1) in vec3 aColor;
|
||||||
|
layout(location=2) in float aSize;
|
||||||
|
uniform vec3 uOrigin;
|
||||||
|
uniform vec3 uAxisX;
|
||||||
|
uniform vec3 uAxisY;
|
||||||
|
uniform vec3 uAxisZ;
|
||||||
|
uniform vec3 uForward;
|
||||||
|
uniform vec3 uRight;
|
||||||
|
uniform vec3 uUp;
|
||||||
|
uniform float uAspect;
|
||||||
|
uniform float uTanHalfFov;
|
||||||
|
uniform float uNearFade;
|
||||||
|
out vec3 vColor;
|
||||||
|
void main(){
|
||||||
|
vec3 delta=uAxisX*aPosition.x+uAxisY*aPosition.y+uAxisZ*aPosition.z-uOrigin;
|
||||||
|
float d=length(delta),z=dot(delta,uForward);
|
||||||
|
vec2 xy=vec2(dot(delta,uRight)/uAspect,dot(delta,uUp))/uTanHalfFov;
|
||||||
|
gl_Position=z>0.0?vec4(xy,z*.999999,z):vec4(2,2,2,1);
|
||||||
|
gl_PointSize=clamp(aSize,1.0,4.5);
|
||||||
|
vColor=aColor*smoothstep(uNearFade,uNearFade*2.0,d);
|
||||||
|
}
|
||||||
62
src/Universe.App/Graphics/Shaders/galaxy.frag.glsl
Normal file
62
src/Universe.App/Graphics/Shaders/galaxy.frag.glsl
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#version 330 core
|
||||||
|
#include "camera-ray.glsl"
|
||||||
|
in vec2 vUv;
|
||||||
|
uniform vec3 uOrigin;
|
||||||
|
uniform vec3 uAxisX;
|
||||||
|
uniform vec3 uAxisY;
|
||||||
|
uniform vec3 uAxisZ;
|
||||||
|
uniform float uThickness;
|
||||||
|
uniform float uArms;
|
||||||
|
uniform float uGamma;
|
||||||
|
uniform float uSeed;
|
||||||
|
uniform int uKind;
|
||||||
|
uniform int uSteps;
|
||||||
|
uniform int uHasTexture;
|
||||||
|
uniform sampler2D uDensity;
|
||||||
|
uniform float uOpacity;
|
||||||
|
out vec4 fragColor;
|
||||||
|
float turbulence(vec3 p){return .5+.23*sin(p.x*17.0+sin(p.y*11.0))*sin(p.z*19.0+p.y*7.0)+.12*sin(dot(p,vec3(61,37,83)));}
|
||||||
|
vec3 local(vec3 p){return vec3(dot(p,uAxisX),dot(p,uAxisY),dot(p,uAxisZ));}
|
||||||
|
void main(){
|
||||||
|
vec3 rd=local(cameraRay(vUv)),ro=local(uOrigin);
|
||||||
|
vec3 bounds=uKind==0?vec3(1.05,max(.24,uThickness*5.0),1.05):vec3(1.1);
|
||||||
|
vec3 o=ro/bounds,d=rd/bounds;
|
||||||
|
float a=dot(d,d),b=dot(o,d),c=dot(o,o)-1.0,disc=b*b-a*c;
|
||||||
|
if(disc<0.0)discard;
|
||||||
|
float entry=max(0.0,(-b-sqrt(disc))/a),exit=(-b+sqrt(disc))/a;
|
||||||
|
if(exit<entry)discard;
|
||||||
|
float ds=(exit-entry)/float(uSteps);
|
||||||
|
vec3 emission=vec3(0);float trans=1.0;
|
||||||
|
for(int i=0;i<96;i++){
|
||||||
|
if(i>=uSteps)break;
|
||||||
|
vec3 p=ro+rd*(entry+(float(i)+.5)*ds);
|
||||||
|
float n=turbulence(p*3.0+uSeed),density=0.0;
|
||||||
|
vec3 color;
|
||||||
|
if(uKind==0){
|
||||||
|
float radius=length(p.xz);
|
||||||
|
vec2 uv=p.xz*vec2(.5,-.5)+.5;
|
||||||
|
vec4 map=texture(uDensity,clamp(uv,vec2(0),vec2(1)));
|
||||||
|
float spiral=atan(p.z,p.x)*uArms-log(radius+.05)*5.8;
|
||||||
|
float pattern=uHasTexture==1?dot(map.rgb,vec3(.2126,.7152,.0722))*map.a:
|
||||||
|
(.05+pow(.5+.5*cos(spiral),5.0)*.8)*pow(max(0.0,1.0-radius),.7)+exp(-radius*radius*65.0)*.8;
|
||||||
|
float disk=pow(clamp(pattern,0.0,1.0),uGamma)*exp(-abs(p.y)/uThickness)*(1.0-smoothstep(.94,1.0,radius));
|
||||||
|
float bulge=exp(-dot(p,p)*100.0)*2.0;
|
||||||
|
density=disk*(.55+.65*n)+bulge;
|
||||||
|
color=mix(vec3(.24,.24,.65),vec3(1.6,1.35,.95),clamp(bulge+pattern*.7,0.0,1.0));
|
||||||
|
if(uHasTexture==1)color=mix(color,map.rgb*2.1,.55);
|
||||||
|
emission+=trans*color*density*ds*1.7;
|
||||||
|
trans*=exp(-disk*(1.0-n)*ds*.7);
|
||||||
|
}else{
|
||||||
|
float shell1=exp(-pow((length((p-vec3(0,.36,0))*vec3(1,1.25,1))-.43)/.055,2.0));
|
||||||
|
float shell2=exp(-pow((length((p+vec3(0,.36,0))*vec3(1,1.25,1))-.43)/.055,2.0));
|
||||||
|
float torus=exp(-pow((length(p.xz)-.21)/.055,2.0)-p.y*p.y*240.0);
|
||||||
|
density=(shell1+shell2+torus*2.5)*(.25+n*n);
|
||||||
|
color=mix(vec3(1.2,.16,.025),vec3(.05,1.1,.68),exp(-dot(p,p)*18.0));
|
||||||
|
if(uHasTexture==1){vec4 map=texture(uDensity,clamp(p.xy*vec2(.5,-.5)+.5,vec2(0),vec2(1)));density*=.35+dot(map.rgb,vec3(.33))*map.a;color=mix(color,map.rgb*2.5,.5);}
|
||||||
|
emission+=trans*color*density*ds*.85;
|
||||||
|
trans*=exp(-density*ds*.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float localExposure=uKind==0&&length(ro)<1.1?.14:1.0;
|
||||||
|
fragColor=vec4(emission*uOpacity*localExposure,0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#version 330 core
|
||||||
|
in vec2 vUv;
|
||||||
|
uniform sampler2D uScene;
|
||||||
|
uniform sampler2D uDepth;
|
||||||
|
uniform sampler2D uWaterNormals;
|
||||||
|
uniform sampler2D uWaterFallback;
|
||||||
|
uniform sampler2D uReflection;
|
||||||
|
uniform sampler2D uClouds;
|
||||||
|
uniform int uHasClouds;
|
||||||
|
uniform float uFar;
|
||||||
|
out vec4 fragColor;
|
||||||
|
float depth(vec2 uv) { return exp2(textureLod(uDepth,uv,0).r*log2(uFar+1.0))-1.0; }
|
||||||
|
void main() {
|
||||||
|
vec3 scene=textureLod(uScene,vUv,0).rgb;
|
||||||
|
vec4 surface=texture(uWaterNormals,vUv);
|
||||||
|
float reflectance=length(surface.rgb);
|
||||||
|
fragColor=vec4(scene,1);
|
||||||
|
if(surface.a<.5 || reflectance<.0001)return;
|
||||||
|
// Depth/normal-aware bilinear upsampling prevents a half-resolution shoreline from reflecting onto land.
|
||||||
|
ivec2 size=textureSize(uReflection,0);
|
||||||
|
vec2 p=vUv*vec2(size)-.5,base=floor(p),f=fract(p);
|
||||||
|
float z=depth(vUv),weight=0.0;
|
||||||
|
vec4 sum=vec4(0);
|
||||||
|
for(int y=0;y<2;y++)for(int x=0;x<2;x++) {
|
||||||
|
ivec2 coord=clamp(ivec2(base)+ivec2(x,y),ivec2(0),size-1);
|
||||||
|
vec2 uv=(vec2(coord)+.5)/vec2(size);
|
||||||
|
vec4 other=textureLod(uWaterNormals,uv,0);
|
||||||
|
if(other.a<.5 || length(other.rgb)<.0001)continue;
|
||||||
|
float w=(x==0?1.0-f.x:f.x)*(y==0?1.0-f.y:f.y);
|
||||||
|
w*=exp(-abs(depth(uv)-z)/max(.3,z*.025));
|
||||||
|
w*=smoothstep(.8,.99,dot(normalize(surface.rgb),normalize(other.rgb)));
|
||||||
|
sum+=texelFetch(uReflection,coord,0)*w;weight+=w;
|
||||||
|
}
|
||||||
|
if(weight<=.00001 || sum.a<=.00001)return;
|
||||||
|
float confidence=clamp(sum.a/weight,0.0,1.0);
|
||||||
|
if(uHasClouds!=0)confidence*=texture(uClouds,vUv).a;
|
||||||
|
vec3 reflected=sum.rgb/sum.a;
|
||||||
|
vec3 fallback=texture(uWaterFallback,vUv).rgb;
|
||||||
|
fragColor=vec4(max(vec3(0),scene+(reflected*reflectance-fallback)*confidence),1);
|
||||||
|
}
|
||||||
17
src/Universe.App/Graphics/Shaders/ocean-reflection.frag.glsl
Normal file
17
src/Universe.App/Graphics/Shaders/ocean-reflection.frag.glsl
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#version 330 core
|
||||||
|
#include "camera-ray.glsl"
|
||||||
|
#include "screen-space-reflection.glsl"
|
||||||
|
in vec2 vUv;
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main() {
|
||||||
|
fragColor=vec4(0);
|
||||||
|
vec4 surface=textureLod(uWaterNormals,vUv,0.0);
|
||||||
|
if(surface.a<.5 || length(surface.rgb)<.0001)return;
|
||||||
|
vec3 ray=cameraRay(vUv);
|
||||||
|
float distance=linearDepth(vUv)/max(.001,dot(ray,uForward));
|
||||||
|
if(distance<=.03 || distance>uMaxSurfaceDistance)return;
|
||||||
|
vec4 hit=traceReflection(ray*distance,normalize(surface.rgb));
|
||||||
|
float fade=1.0-smoothstep(uMaxSurfaceDistance*.6,uMaxSurfaceDistance,distance);
|
||||||
|
fade*=1.0-smoothstep(.6,1.0,uReflectionSettings.z);
|
||||||
|
fragColor=hit*(fade*uReflectionStrength);
|
||||||
|
}
|
||||||
@@ -27,7 +27,9 @@ uniform vec3 uLightDirections[2];
|
|||||||
uniform vec3 uLightColors[2];
|
uniform vec3 uLightColors[2];
|
||||||
uniform float uLightFlux[2];
|
uniform float uLightFlux[2];
|
||||||
uniform float uTime;
|
uniform float uTime;
|
||||||
out vec4 fragColor;
|
layout(location=0) out vec4 fragColor;
|
||||||
|
layout(location=1) out vec4 reflectionNormal;
|
||||||
|
layout(location=2) out vec4 reflectionFallback;
|
||||||
const float PI=3.14159265359;
|
const float PI=3.14159265359;
|
||||||
vec3 toLocal(vec3 v) { return vec3(dot(v,uAxisX),dot(v,uAxisY),dot(v,uAxisZ)); }
|
vec3 toLocal(vec3 v) { return vec3(dot(v,uAxisX),dot(v,uAxisY),dot(v,uAxisZ)); }
|
||||||
vec3 toWorld(vec3 v) { return uAxisX*v.x+uAxisY*v.y+uAxisZ*v.z; }
|
vec3 toWorld(vec3 v) { return uAxisX*v.x+uAxisY*v.y+uAxisZ*v.z; }
|
||||||
@@ -98,25 +100,9 @@ vec3 skyReflection(vec3 direction,vec3 up) {
|
|||||||
}
|
}
|
||||||
return sky*(.006+daylight*1.35);
|
return sky*(.006+daylight*1.35);
|
||||||
}
|
}
|
||||||
vec3 screenReflection(vec3 position,vec3 direction,vec3 fallback) {
|
|
||||||
vec3 world=toWorld(position-vec3(0,uCameraHeight,0));
|
|
||||||
vec3 reflected=toWorld(direction);
|
|
||||||
float stepSize=3.0;
|
|
||||||
for(int i=0;i<9;i++) {
|
|
||||||
vec3 q=world+reflected*stepSize;
|
|
||||||
float z=dot(q,uForward);
|
|
||||||
if(z>.1) {
|
|
||||||
vec2 uv=.5+vec2(dot(q,uRight)/(uAspect*uTanHalfFov),dot(q,uUp)/uTanHalfFov)/(2.0*z);
|
|
||||||
if(all(greaterThan(uv,vec2(.015))) && all(lessThan(uv,vec2(.985)))) {
|
|
||||||
float depth=exp2(texture(uDepth,uv).r*log2(uFar+1.0))-1.0;
|
|
||||||
if(z>depth && z-depth<max(1.0,stepSize*.09))return texture(uScene,uv).rgb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stepSize*=1.9;
|
|
||||||
}
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
void main() {
|
void main() {
|
||||||
|
// Premultiplied-alpha MRT blending preserves earlier ocean planets outside this one's silhouette.
|
||||||
|
reflectionNormal=vec4(0); reflectionFallback=vec4(0);
|
||||||
vec3 scene=texture(uScene,vUv).rgb;
|
vec3 scene=texture(uScene,vUv).rgb;
|
||||||
float originalDepth=texture(uDepth,vUv).r;
|
float originalDepth=texture(uDepth,vUv).r;
|
||||||
gl_FragDepth=originalDepth;
|
gl_FragDepth=originalDepth;
|
||||||
@@ -167,7 +153,6 @@ void main() {
|
|||||||
vec3 reflectionDirection=reflect(rd,n);
|
vec3 reflectionDirection=reflect(rd,n);
|
||||||
vec3 reflected=skyReflection(reflectionDirection,up);
|
vec3 reflected=skyReflection(reflectionDirection,up);
|
||||||
reflected=cloudReflection(toWorld(p-ro),toWorld(reflectionDirection),reflected);
|
reflected=cloudReflection(toWorld(p-ro),toWorld(reflectionDirection),reflected);
|
||||||
if(t<1500.0 && !underwater)reflected=screenReflection(p,reflectionDirection,reflected);
|
|
||||||
vec2 refractUv=clamp(vUv+vec2(dot(toWorld(n),uRight),dot(toWorld(n),uUp))*uWaterOptics.z/(1.0+t*.012),vec2(.001),vec2(.999));
|
vec2 refractUv=clamp(vUv+vec2(dot(toWorld(n),uRight),dot(toWorld(n),uUp))*uWaterOptics.z/(1.0+t*.012),vec2(.001),vec2(.999));
|
||||||
float floorDistance=sceneDistance(refractUv,cameraRay(refractUv));
|
float floorDistance=sceneDistance(refractUv,cameraRay(refractUv));
|
||||||
float thickness=max(.0,min(400.0,floorDistance-t));
|
float thickness=max(.0,min(400.0,floorDistance-t));
|
||||||
@@ -204,5 +189,8 @@ void main() {
|
|||||||
color=color*through+uShallowColor*(1.0-through)*.5;
|
color=color*through+uShallowColor*(1.0-through)*.5;
|
||||||
}
|
}
|
||||||
fragColor=vec4(max(color,vec3(0)),1);
|
fragColor=vec4(max(color,vec3(0)),1);
|
||||||
|
float reflectance=underwater?0.0:fresnel*uWaterOptics.y*(1.0-foam);
|
||||||
|
reflectionNormal=vec4(toWorld(n)*reflectance,1);
|
||||||
|
reflectionFallback=vec4(reflected*reflectance,1);
|
||||||
gl_FragDepth=clamp(log2(1.0+t*max(.001,dot(worldRay,uForward)))/log2(uFar+1.0),0.0,1.0);
|
gl_FragDepth=clamp(log2(1.0+t*max(.001,dot(worldRay,uForward)))/log2(uFar+1.0),0.0,1.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// Perspective-correct screen-space segment traversal, following the DDA principle in
|
||||||
|
// McGuire & Mara, JCGT 3(4), 2014. Camera-space Z is positive and depth is logarithmic.
|
||||||
|
uniform sampler2D uScene;
|
||||||
|
uniform sampler2D uDepth;
|
||||||
|
uniform sampler2D uWaterNormals;
|
||||||
|
uniform float uFar;
|
||||||
|
uniform int uMaxSteps;
|
||||||
|
uniform vec3 uTraceSettings; // maximum ray distance, thickness, normal bias (metres)
|
||||||
|
uniform vec3 uReflectionSettings; // edge fade, pixel stride, roughness
|
||||||
|
uniform float uMaxSurfaceDistance;
|
||||||
|
uniform float uReflectionStrength;
|
||||||
|
uniform int uReflectSky;
|
||||||
|
|
||||||
|
float linearDepth(vec2 uv) { return exp2(textureLod(uDepth,uv,0.0).r*log2(uFar+1.0))-1.0; }
|
||||||
|
vec3 viewPoint(vec3 world) { return vec3(dot(world,uRight),dot(world,uUp),dot(world,uForward)); }
|
||||||
|
vec2 projectPoint(vec3 p) { return .5+p.xy/(2.0*p.z*uTanHalfFov*vec2(uAspect,1)); }
|
||||||
|
float edgeConfidence(vec2 uv) {
|
||||||
|
float edge=min(min(uv.x,uv.y),min(1.0-uv.x,1.0-uv.y));
|
||||||
|
return smoothstep(0.0,uReflectionSettings.x,edge);
|
||||||
|
}
|
||||||
|
vec4 traceReflection(vec3 world,vec3 normal) {
|
||||||
|
vec3 reflection=reflect(normalize(world),normal);
|
||||||
|
vec3 origin=viewPoint(world+normal*uTraceSettings.z),direction=viewPoint(reflection);
|
||||||
|
if(origin.z<=.02)return vec4(0);
|
||||||
|
float distanceLimit=uTraceSettings.x;
|
||||||
|
if(direction.z<0.0)distanceLimit=min(distanceLimit,(.05-origin.z)/direction.z);
|
||||||
|
vec4 result=vec4(0);
|
||||||
|
float roughness=uReflectionSettings.z;
|
||||||
|
float mip=roughness*5.0;
|
||||||
|
// Infinite/distant sky lookup includes the actual stars, atmospheric colour and cloud texture.
|
||||||
|
// Opaque foreground pixels are never treated as sky or used to fill missing geometry.
|
||||||
|
if(uReflectSky!=0 && direction.z>.001) {
|
||||||
|
vec2 uv=projectPoint(direction);
|
||||||
|
if(all(greaterThan(uv,vec2(0)))&&all(lessThan(uv,vec2(1)))) {
|
||||||
|
float z=linearDepth(uv);
|
||||||
|
if(z>uTraceSettings.x*4.0 && textureLod(uWaterNormals,uv,0.0).a<.5) {
|
||||||
|
float confidence=edgeConfidence(uv);
|
||||||
|
result=vec4(textureLod(uScene,uv,mip).rgb*confidence,confidence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(distanceLimit<=uTraceSettings.z)return result;
|
||||||
|
vec3 end=origin+direction*distanceLimit;
|
||||||
|
vec2 startUv=projectPoint(origin),endUv=projectPoint(end);
|
||||||
|
vec2 size=vec2(textureSize(uDepth,0));
|
||||||
|
float pixelLength=length((endUv-startUv)*size);
|
||||||
|
float stepFraction=max(uReflectionSettings.y/max(pixelLength,1.0),1.0/float(uMaxSteps));
|
||||||
|
vec3 q0=origin/origin.z,q1=end/end.z;
|
||||||
|
float k0=1.0/origin.z,k1=1.0/end.z;
|
||||||
|
float previous=0.0,previousZ=origin.z;
|
||||||
|
for(int i=0;i<128;i++) {
|
||||||
|
if(i>=uMaxSteps)break;
|
||||||
|
float fraction=min(1.0,float(i+1)*stepFraction);
|
||||||
|
vec2 uv=mix(startUv,endUv,fraction);
|
||||||
|
if(any(lessThanEqual(uv,vec2(0)))||any(greaterThanEqual(uv,vec2(1))))break;
|
||||||
|
float rayZ=1.0/mix(k0,k1,fraction);
|
||||||
|
float sceneZ=linearDepth(uv);
|
||||||
|
float thickness=uTraceSettings.y+sceneZ*.00005;
|
||||||
|
bool water=textureLod(uWaterNormals,uv,0.0).a>.5;
|
||||||
|
if(!water && sceneZ<uFar*.99 && max(previousZ,rayZ)>=sceneZ && min(previousZ,rayZ)<=sceneZ+thickness) {
|
||||||
|
float lo=previous,hi=fraction;
|
||||||
|
// Refine the front crossing instead of accepting an entire coarse depth interval.
|
||||||
|
for(int j=0;j<7;j++) {
|
||||||
|
float mid=(lo+hi)*.5;
|
||||||
|
vec2 midUv=mix(startUv,endUv,mid);
|
||||||
|
float delta=1.0/mix(k0,k1,mid)-linearDepth(midUv);
|
||||||
|
if(delta>0.0)hi=mid;else lo=mid;
|
||||||
|
}
|
||||||
|
float hit=(lo+hi)*.5;
|
||||||
|
vec2 hitUv=mix(startUv,endUv,hit);
|
||||||
|
vec3 point=mix(q0,q1,hit)/mix(k0,k1,hit);
|
||||||
|
float error=point.z-linearDepth(hitUv);
|
||||||
|
float travel=length(point-origin);
|
||||||
|
if(abs(error)<thickness && travel>uTraceSettings.z*2.0 && textureLod(uWaterNormals,hitUv,0.0).a<.5) {
|
||||||
|
float confidence=edgeConfidence(hitUv)*(1.0-smoothstep(distanceLimit*.7,distanceLimit,travel));
|
||||||
|
confidence*=1.0-smoothstep(thickness*.3,thickness,abs(error));
|
||||||
|
return vec4(textureLod(uScene,hitUv,mip).rgb*confidence,confidence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previous=fraction; previousZ=rayZ;
|
||||||
|
if(fraction>=1.0)break;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ uniform vec3 uStarDirections[2];
|
|||||||
uniform vec3 uStarColors[2];
|
uniform vec3 uStarColors[2];
|
||||||
uniform float uStarAngles[2];
|
uniform float uStarAngles[2];
|
||||||
uniform float uTime;
|
uniform float uTime;
|
||||||
|
uniform float uBackgroundStrength;
|
||||||
out vec4 fragColor;
|
out vec4 fragColor;
|
||||||
void main() {
|
void main() {
|
||||||
vec2 xy=(vUv*2.0-1.0)*uTanHalfFov;
|
vec2 xy=(vUv*2.0-1.0)*uTanHalfFov;
|
||||||
@@ -18,6 +19,7 @@ void main() {
|
|||||||
float band=pow(max(0.0,1.0-abs(dot(rd,normalize(vec3(0.32,0.72,0.48))))),17.0);
|
float band=pow(max(0.0,1.0-abs(dot(rd,normalize(vec3(0.32,0.72,0.48))))),17.0);
|
||||||
float dust=smoothstep(-0.26,0.36,fbm(rd*8.0+vec3(4.1,9.2,3.4)));
|
float dust=smoothstep(-0.26,0.36,fbm(rd*8.0+vec3(4.1,9.2,3.4)));
|
||||||
vec3 color=vec3(0.00013,0.00020,0.00042)+vec3(0.0013,0.0016,0.0026)*band*dust;
|
vec3 color=vec3(0.00013,0.00020,0.00042)+vec3(0.0013,0.0016,0.0026)*band*dust;
|
||||||
|
color*=uBackgroundStrength;
|
||||||
for(int i=0;i<2;i++) {
|
for(int i=0;i<2;i++) {
|
||||||
if(i>=uLightCount) break;
|
if(i>=uLightCount) break;
|
||||||
float angle=acos(clamp(dot(rd,uStarDirections[i]),-1.0,1.0));
|
float angle=acos(clamp(dot(rd,uStarDirections[i]),-1.0,1.0));
|
||||||
|
|||||||
24
src/Universe.App/Graphics/Shaders/star-trail.frag.glsl
Normal file
24
src/Universe.App/Graphics/Shaders/star-trail.frag.glsl
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#version 330 core
|
||||||
|
noperspective in vec2 gPixel;
|
||||||
|
flat in vec3 gColor;
|
||||||
|
flat in float gLength;
|
||||||
|
flat in float gSigma;
|
||||||
|
out vec4 fragColor;
|
||||||
|
// Gaussian convolved with a normalized shutter interval. Its integral is the
|
||||||
|
// same for a stationary point and a long streak (no extra light from area).
|
||||||
|
float erfApprox(float x) {
|
||||||
|
float t=1.0/(1.0+.3275911*abs(x));
|
||||||
|
float p=(((((1.061405429*t-1.453152027)*t)+1.421413741)*t-.284496736)*t+.254829592)*t;
|
||||||
|
return sign(x)*(1.0-p*exp(-x*x));
|
||||||
|
}
|
||||||
|
void main() {
|
||||||
|
float sigma2=gSigma*gSigma+1.0/12.0;
|
||||||
|
float sigma=sqrt(sigma2);
|
||||||
|
float along;
|
||||||
|
if(gLength<.2)along=exp(-pow(gPixel.x-gLength*.5,2.0)/(2.0*sigma2));
|
||||||
|
else along=1.253314137*sigma/gLength*(erfApprox(gPixel.x/(1.414213562*sigma))
|
||||||
|
-erfApprox((gPixel.x-gLength)/(1.414213562*sigma)));
|
||||||
|
float brightness=exp(-gPixel.y*gPixel.y/(2.0*sigma2))*max(0.0,along);
|
||||||
|
if(brightness<.00001)discard;
|
||||||
|
fragColor=vec4(gColor*brightness,0);
|
||||||
|
}
|
||||||
60
src/Universe.App/Graphics/Shaders/star-trail.geom.glsl
Normal file
60
src/Universe.App/Graphics/Shaders/star-trail.geom.glsl
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
#version 330 core
|
||||||
|
layout(points) in;
|
||||||
|
layout(triangle_strip,max_vertices=4) out;
|
||||||
|
in VS_OUT { vec4 previousClip; vec3 color; float size; } star[];
|
||||||
|
uniform vec3 uViewport;
|
||||||
|
uniform float uShutter;
|
||||||
|
uniform float uMaximumTrailPixels;
|
||||||
|
uniform float uMinimumWidthPixels;
|
||||||
|
uniform float uMovingBrightness;
|
||||||
|
noperspective out vec2 gPixel;
|
||||||
|
flat out vec3 gColor;
|
||||||
|
flat out float gLength;
|
||||||
|
flat out float gSigma;
|
||||||
|
|
||||||
|
// Clip the swept segment to the viewport before bounding its length. A star
|
||||||
|
// crossing an edge still leaves a visible trail without a full-screen triangle.
|
||||||
|
bool clipAxis(float origin,float delta,float bound,inout float lo,inout float hi) {
|
||||||
|
if(abs(delta)<1e-7)return abs(origin)<=bound;
|
||||||
|
float a=(-bound-origin)/delta,b=(bound-origin)/delta;
|
||||||
|
lo=max(lo,min(a,b));hi=min(hi,max(a,b));return lo<=hi;
|
||||||
|
}
|
||||||
|
void vertex(vec2 pixel,vec2 uv) {
|
||||||
|
gl_Position=vec4(pixel/uViewport.xy*2.0,.999999,1);
|
||||||
|
gPixel=uv;EmitVertex();
|
||||||
|
}
|
||||||
|
void main() {
|
||||||
|
if(max(max(star[0].color.r,star[0].color.g),star[0].color.b)<.00001)return;
|
||||||
|
vec4 head=gl_in[0].gl_Position,tail=star[0].previousClip;
|
||||||
|
if(uShutter<=0.0)tail=head;
|
||||||
|
const float nearPlane=.00001;
|
||||||
|
if(head.w<=nearPlane&&tail.w<=nearPlane)return;
|
||||||
|
if(head.w<nearPlane)head=mix(tail,head,(tail.w-nearPlane)/(tail.w-head.w));
|
||||||
|
if(tail.w<nearPlane)tail=mix(head,tail,(head.w-nearPlane)/(head.w-tail.w));
|
||||||
|
vec2 h=head.xy/head.w*uViewport.xy*.5;
|
||||||
|
vec2 t=mix(h,tail.xy/tail.w*uViewport.xy*.5,uShutter);
|
||||||
|
float fullLength=length(h-t);
|
||||||
|
if(fullLength>uMaximumTrailPixels)t=h-(h-t)*(uMaximumTrailPixels/fullLength);
|
||||||
|
vec2 originalTail=t;
|
||||||
|
gLength=length(h-t);
|
||||||
|
vec2 delta=h-t;
|
||||||
|
float lo=0.0,hi=1.0;
|
||||||
|
vec2 bounds=uViewport.xy*.5+vec2(8.0);
|
||||||
|
if(!clipAxis(t.x,delta.x,bounds.x,lo,hi)||!clipAxis(t.y,delta.y,bounds.y,lo,hi))return;
|
||||||
|
h=t+delta*hi;t+=delta*lo;
|
||||||
|
vec2 direction=length(h-t)>.001?normalize(h-t):vec2(1,0);
|
||||||
|
vec2 perpendicular=vec2(-direction.y,direction.x);
|
||||||
|
// A minimum Gaussian footprint averages subpixel motion instead of toggling
|
||||||
|
// a one-pixel point on and off as the camera passes it.
|
||||||
|
gSigma=max(uMinimumWidthPixels/2.0,star[0].size*.3);
|
||||||
|
float radius=sqrt(gSigma*gSigma+1.0/12.0)*3.5;
|
||||||
|
float moving=1.0-exp(-gLength/14.0);
|
||||||
|
float boost=mix(1.0,uMovingBrightness,moving);
|
||||||
|
gColor=star[0].color*.805*boost;
|
||||||
|
float start=length(t-originalTail),end=length(h-originalTail);
|
||||||
|
vertex(t-direction*radius-perpendicular*radius,vec2(start-radius,-radius));
|
||||||
|
vertex(t-direction*radius+perpendicular*radius,vec2(start-radius,radius));
|
||||||
|
vertex(h+direction*radius-perpendicular*radius,vec2(end+radius,-radius));
|
||||||
|
vertex(h+direction*radius+perpendicular*radius,vec2(end+radius,radius));
|
||||||
|
EndPrimitive();
|
||||||
|
}
|
||||||
35
src/Universe.App/Graphics/Shaders/star-trail.vert.glsl
Normal file
35
src/Universe.App/Graphics/Shaders/star-trail.vert.glsl
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#version 330 core
|
||||||
|
layout(location=0) in vec3 aPosition;
|
||||||
|
layout(location=1) in vec3 aColor;
|
||||||
|
layout(location=2) in float aSize;
|
||||||
|
uniform vec3 uOrigin;
|
||||||
|
uniform vec3 uPreviousOrigin;
|
||||||
|
uniform vec3 uAxisX;
|
||||||
|
uniform vec3 uAxisY;
|
||||||
|
uniform vec3 uAxisZ;
|
||||||
|
uniform vec3 uForward;
|
||||||
|
uniform vec3 uRight;
|
||||||
|
uniform vec3 uUp;
|
||||||
|
uniform vec3 uPreviousForward;
|
||||||
|
uniform vec3 uPreviousRight;
|
||||||
|
uniform vec3 uPreviousUp;
|
||||||
|
uniform float uAspect;
|
||||||
|
uniform float uTanHalfFov;
|
||||||
|
uniform float uPreviousAspect;
|
||||||
|
uniform float uPreviousTanHalfFov;
|
||||||
|
uniform float uNearFade;
|
||||||
|
uniform float uVisibility;
|
||||||
|
out VS_OUT { vec4 previousClip; vec3 color; float size; } star;
|
||||||
|
vec4 projectStar(vec3 p,vec3 forward,vec3 right,vec3 up,float aspect,float fov) {
|
||||||
|
float z=dot(p,forward);
|
||||||
|
return vec4(dot(p,right)/(aspect*fov),dot(p,up)/fov,z*.999999,z);
|
||||||
|
}
|
||||||
|
void main() {
|
||||||
|
vec3 world=uAxisX*aPosition.x+uAxisY*aPosition.y+uAxisZ*aPosition.z;
|
||||||
|
vec3 current=world-uOrigin;
|
||||||
|
gl_Position=projectStar(current,uForward,uRight,uUp,uAspect,uTanHalfFov);
|
||||||
|
star.previousClip=projectStar(world-uPreviousOrigin,uPreviousForward,uPreviousRight,uPreviousUp,uPreviousAspect,uPreviousTanHalfFov);
|
||||||
|
float visibility=uNearFade>0.0?smoothstep(uNearFade,uNearFade*2.0,length(current)):1.0;
|
||||||
|
star.color=aColor*visibility*uVisibility;
|
||||||
|
star.size=aSize;
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ uniform float uRadius;
|
|||||||
uniform mat4 uViewProjection;
|
uniform mat4 uViewProjection;
|
||||||
uniform float uFar;
|
uniform float uFar;
|
||||||
uniform mat4 uWorldToBody;
|
uniform mat4 uWorldToBody;
|
||||||
|
uniform mat4 uBodyToWorld;
|
||||||
uniform vec3 uMaterialAnchor;
|
uniform vec3 uMaterialAnchor;
|
||||||
out vec3 vWorldPosition;
|
out vec3 vWorldPosition;
|
||||||
out vec3 vPlanetLocal;
|
out vec3 vPlanetLocal;
|
||||||
@@ -15,10 +16,11 @@ out vec3 vNormal;
|
|||||||
out float vFragDepth;
|
out float vFragDepth;
|
||||||
out vec3 vMaterialPosition;
|
out vec3 vMaterialPosition;
|
||||||
void main() {
|
void main() {
|
||||||
vWorldPosition=uChunkPosition+aPosition;
|
vec3 worldOffset=mat3(uBodyToWorld)*aPosition;
|
||||||
vPlanetLocal=uChunkOrigin+aPosition/uRadius;
|
vWorldPosition=uChunkPosition+worldOffset;
|
||||||
vNormal=aNormal;
|
vPlanetLocal=uChunkOrigin+worldOffset/uRadius;
|
||||||
vMaterialPosition=mat3(uWorldToBody)*aPosition+uMaterialAnchor;
|
vNormal=mat3(uBodyToWorld)*aNormal;
|
||||||
|
vMaterialPosition=aPosition+uMaterialAnchor;
|
||||||
vec4 clip=uViewProjection*vec4(vWorldPosition,1.0);
|
vec4 clip=uViewProjection*vec4(vWorldPosition,1.0);
|
||||||
vFragDepth=clip.w+1.0;
|
vFragDepth=clip.w+1.0;
|
||||||
gl_Position=clip;
|
gl_Position=clip;
|
||||||
|
|||||||
57
src/Universe.App/Graphics/StarTrailRenderer.cs
Normal file
57
src/Universe.App/Graphics/StarTrailRenderer.cs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
using Silk.NET.OpenGL;
|
||||||
|
using Universe.App.Runtime;
|
||||||
|
using Universe.Core.Generation;
|
||||||
|
using Universe.Core.Math;
|
||||||
|
using Universe.Core.World;
|
||||||
|
|
||||||
|
namespace Universe.App.Graphics;
|
||||||
|
|
||||||
|
/// <summary>Draws only stellar sprites as swept Gaussian capsules. No scene-color accumulation.</summary>
|
||||||
|
internal sealed class StarTrailRenderer : IDisposable
|
||||||
|
{
|
||||||
|
private readonly GL _gl;
|
||||||
|
private ShaderProgram _program;
|
||||||
|
public StarMotionBlurSettings Settings { get; }
|
||||||
|
public StarMotionHistory History { get; } = new();
|
||||||
|
public float Brightness { get; set; } = 1;
|
||||||
|
private double _cellSize;
|
||||||
|
|
||||||
|
public StarTrailRenderer(GL gl, StarMotionBlurSettings settings)
|
||||||
|
{
|
||||||
|
settings.Validate(); _gl = gl; Settings = settings; _program = CreateProgram();
|
||||||
|
}
|
||||||
|
private ShaderProgram CreateProgram() => ShaderProgram.Create(_gl, ShaderSources.Load("star-trail.vert"),
|
||||||
|
ShaderSources.Load("star-trail.frag"), ShaderSources.Load("star-trail.geom"));
|
||||||
|
public void ReloadShaders()
|
||||||
|
{
|
||||||
|
var replacement = CreateProgram(); _program.Dispose(); _program = replacement;
|
||||||
|
}
|
||||||
|
public void BeginFrame(FlyCamera camera, int width, int height, double seconds)
|
||||||
|
{
|
||||||
|
_cellSize = camera.CellSizeMeters;
|
||||||
|
History.Advance(camera, width, height, seconds, Settings);
|
||||||
|
}
|
||||||
|
public void Draw(GpuMesh mesh, UniversePosition center, double scale, PlanetOrientation orientation,
|
||||||
|
double nearFade = 0, float visibility = 1, bool directional = false)
|
||||||
|
{
|
||||||
|
var current = History.Current; var previous = History.Previous;
|
||||||
|
_program.Use();
|
||||||
|
_program.Set("uOrigin", directional ? System.Numerics.Vector3.Zero : (current.Position.RelativeTo(center, _cellSize) / scale).ToVector3());
|
||||||
|
_program.Set("uPreviousOrigin", directional ? System.Numerics.Vector3.Zero : (previous.Position.RelativeTo(center, _cellSize) / scale).ToVector3());
|
||||||
|
_program.Set("uAxisX", orientation.ToWorld(new(1,0,0)).ToVector3());
|
||||||
|
_program.Set("uAxisY", orientation.ToWorld(new(0,1,0)).ToVector3());
|
||||||
|
_program.Set("uAxisZ", orientation.ToWorld(new(0,0,1)).ToVector3());
|
||||||
|
_program.Set("uForward", current.Forward.ToVector3()); _program.Set("uRight", current.Right.ToVector3()); _program.Set("uUp", current.Up.ToVector3());
|
||||||
|
_program.Set("uPreviousForward", previous.Forward.ToVector3()); _program.Set("uPreviousRight", previous.Right.ToVector3()); _program.Set("uPreviousUp", previous.Up.ToVector3());
|
||||||
|
_program.Set("uAspect", (float)current.Width/current.Height); _program.Set("uTanHalfFov", (float)current.TanHalfFov);
|
||||||
|
_program.Set("uPreviousAspect", (float)previous.Width/previous.Height); _program.Set("uPreviousTanHalfFov", (float)previous.TanHalfFov);
|
||||||
|
_program.Set("uNearFade", (float)nearFade); _program.Set("uVisibility", visibility*Brightness);
|
||||||
|
_program.Set("uViewport", new System.Numerics.Vector3(current.Width,current.Height,0));
|
||||||
|
_program.Set("uShutter", History.Valid ? (float)Settings.ShutterFraction : 0);
|
||||||
|
_program.Set("uMaximumTrailPixels", (float)Settings.MaximumTrailPixels);
|
||||||
|
_program.Set("uMinimumWidthPixels", (float)Settings.MinimumWidthPixels);
|
||||||
|
_program.Set("uMovingBrightness", (float)Settings.MovingBrightness);
|
||||||
|
mesh.Draw(GLEnum.Points);
|
||||||
|
}
|
||||||
|
public void Dispose() => _program.Dispose();
|
||||||
|
}
|
||||||
@@ -27,6 +27,9 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
private readonly PerformanceOverlay _performanceOverlay;
|
private readonly PerformanceOverlay _performanceOverlay;
|
||||||
private readonly BenchmarkOverlay _benchmarkOverlay;
|
private readonly BenchmarkOverlay _benchmarkOverlay;
|
||||||
private readonly GpuPassProfiler _gpuProfiler;
|
private readonly GpuPassProfiler _gpuProfiler;
|
||||||
|
private readonly CosmicRenderer? _cosmos;
|
||||||
|
private readonly StarTrailRenderer _starTrails;
|
||||||
|
private readonly Dictionary<SectorCoordinate, StarSystemData> _residentSystems = [];
|
||||||
private ShaderProgram _surface;
|
private ShaderProgram _surface;
|
||||||
private ShaderProgram _star;
|
private ShaderProgram _star;
|
||||||
private ShaderProgram _sky;
|
private ShaderProgram _sky;
|
||||||
@@ -40,7 +43,10 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
private ShaderProgram _rock;
|
private ShaderProgram _rock;
|
||||||
private ShaderProgram _rockPoints;
|
private ShaderProgram _rockPoints;
|
||||||
private ShaderProgram _sunGlow;
|
private ShaderProgram _sunGlow;
|
||||||
|
private ShaderProgram _blackHole;
|
||||||
private ShaderProgram _ocean;
|
private ShaderProgram _ocean;
|
||||||
|
private ShaderProgram _oceanReflection;
|
||||||
|
private ShaderProgram _oceanReflectionComposite;
|
||||||
private ShaderProgram _clouds;
|
private ShaderProgram _clouds;
|
||||||
private ShaderProgram _cloudComposite;
|
private ShaderProgram _cloudComposite;
|
||||||
private readonly ShaderProgram _overlay;
|
private readonly ShaderProgram _overlay;
|
||||||
@@ -48,11 +54,16 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
private readonly CloudNoiseTexture _cloudNoise;
|
private readonly CloudNoiseTexture _cloudNoise;
|
||||||
private readonly SurfaceMaterials _materials;
|
private readonly SurfaceMaterials _materials;
|
||||||
private RenderTarget? _cloudRaw;
|
private RenderTarget? _cloudRaw;
|
||||||
|
private RenderTarget? _waterReflectionRaw;
|
||||||
|
private RenderTarget _waterReflectionTarget = null!;
|
||||||
|
private bool _oceanRendered;
|
||||||
|
private string? _cloudLayerPlanetId;
|
||||||
internal RenderTarget? CloudLayer => _cloudRaw;
|
internal RenderTarget? CloudLayer => _cloudRaw;
|
||||||
private readonly GpuMesh _navigationStars;
|
private readonly GpuMesh _navigationStars;
|
||||||
private RenderTarget _scene = null!;
|
private RenderTarget _scene = null!;
|
||||||
private RenderTarget _gasInput = null!;
|
private RenderTarget _gasInput = null!;
|
||||||
private RenderTarget _glareTarget = null!;
|
private RenderTarget _glareTarget = null!;
|
||||||
|
private RenderTarget _blackHoleTarget = null!;
|
||||||
private RenderTarget _scatterA = null!;
|
private RenderTarget _scatterA = null!;
|
||||||
private RenderTarget _scatterB = null!;
|
private RenderTarget _scatterB = null!;
|
||||||
private RenderTarget _bloomA = null!;
|
private RenderTarget _bloomA = null!;
|
||||||
@@ -61,6 +72,7 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
private int _width;
|
private int _width;
|
||||||
private int _height;
|
private int _height;
|
||||||
private double _time;
|
private double _time;
|
||||||
|
public double? SimulationTimeSeconds { get; set; }
|
||||||
private int _probeFrameCounter;
|
private int _probeFrameCounter;
|
||||||
private int _probeEffectIndex;
|
private int _probeEffectIndex;
|
||||||
private string? _debugProbePlanetId;
|
private string? _debugProbePlanetId;
|
||||||
@@ -75,10 +87,11 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
public bool DebugProbeEnabled { get; set; }
|
public bool DebugProbeEnabled { get; set; }
|
||||||
public double CpuRenderMilliseconds { get; private set; }
|
public double CpuRenderMilliseconds { get; private set; }
|
||||||
public float ExposureCompensation { get; set; } = 1f;
|
public float ExposureCompensation { get; set; } = 1f;
|
||||||
|
public string HardwareName => _gl.GetStringS(GLEnum.Renderer);
|
||||||
internal bool ProfileFrame { get; set; }
|
internal bool ProfileFrame { get; set; }
|
||||||
internal Dictionary<string, double> FrameTimings { get; } = [];
|
internal Dictionary<string, double> FrameTimings { get; } = [];
|
||||||
|
|
||||||
public UniverseRenderer(GL gl, int width, int height)
|
public UniverseRenderer(GL gl, int width, int height, GalaxyUniverse? galaxies = null, StarMotionBlurSettings? starMotionBlur = null)
|
||||||
{
|
{
|
||||||
_gl = gl;
|
_gl = gl;
|
||||||
_noise = new(gl);
|
_noise = new(gl);
|
||||||
@@ -94,6 +107,8 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
_performanceOverlay = new(gl);
|
_performanceOverlay = new(gl);
|
||||||
_benchmarkOverlay = new(gl);
|
_benchmarkOverlay = new(gl);
|
||||||
_gpuProfiler = new(gl);
|
_gpuProfiler = new(gl);
|
||||||
|
_starTrails = new(gl, starMotionBlur ?? new());
|
||||||
|
if (galaxies is not null) _cosmos = new(gl, galaxies, _starTrails);
|
||||||
_surface = Program("terrain.vert", "terrain.frag");
|
_surface = Program("terrain.vert", "terrain.frag");
|
||||||
_star = Program("star.vert", "star.frag");
|
_star = Program("star.vert", "star.frag");
|
||||||
_points = Program("point.vert", "point.frag");
|
_points = Program("point.vert", "point.frag");
|
||||||
@@ -107,7 +122,10 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
_rock = Program("rock.vert", "rock.frag");
|
_rock = Program("rock.vert", "rock.frag");
|
||||||
_rockPoints = Program("rock-point.vert", "rock-point.frag");
|
_rockPoints = Program("rock-point.vert", "rock-point.frag");
|
||||||
_sunGlow = Program("fullscreen.vert", "sun-glow.frag");
|
_sunGlow = Program("fullscreen.vert", "sun-glow.frag");
|
||||||
|
_blackHole = Program("fullscreen.vert", "black-hole.frag");
|
||||||
_ocean = Program("fullscreen.vert", "ocean.frag");
|
_ocean = Program("fullscreen.vert", "ocean.frag");
|
||||||
|
_oceanReflection = Program("fullscreen.vert", "ocean-reflection.frag");
|
||||||
|
_oceanReflectionComposite = Program("fullscreen.vert", "ocean-reflection-composite.frag");
|
||||||
_clouds = Program("fullscreen.vert", "cloud.frag");
|
_clouds = Program("fullscreen.vert", "cloud.frag");
|
||||||
_cloudComposite = Program("fullscreen.vert", "cloud-composite.frag");
|
_cloudComposite = Program("fullscreen.vert", "cloud-composite.frag");
|
||||||
_overlay = ShaderProgram.Create(gl, """
|
_overlay = ShaderProgram.Create(gl, """
|
||||||
@@ -168,8 +186,10 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
foreach (var pair in new[] { ("terrain.vert","terrain.frag"),("star.vert","star.frag"),("fullscreen.vert","sky.frag"),("point.vert","point.frag"),
|
foreach (var pair in new[] { ("terrain.vert","terrain.frag"),("star.vert","star.frag"),("fullscreen.vert","sky.frag"),("point.vert","point.frag"),
|
||||||
("fullscreen.vert","atmosphere.frag"),("fullscreen.vert","bright.frag"),("fullscreen.vert","blur.frag"),("fullscreen.vert","final.frag"),("fullscreen.vert","ring.frag"),
|
("fullscreen.vert","atmosphere.frag"),("fullscreen.vert","bright.frag"),("fullscreen.vert","blur.frag"),("fullscreen.vert","final.frag"),("fullscreen.vert","ring.frag"),
|
||||||
("fullscreen.vert","gas.frag"),("rock.vert","rock.frag"),("rock-point.vert","rock-point.frag"),("fullscreen.vert","sun-glow.frag"),("fullscreen.vert","ocean.frag"),
|
("fullscreen.vert","gas.frag"),("rock.vert","rock.frag"),("rock-point.vert","rock-point.frag"),("fullscreen.vert","sun-glow.frag"),("fullscreen.vert","ocean.frag"),
|
||||||
("fullscreen.vert","cloud.frag"),("fullscreen.vert","cloud-composite.frag") })
|
("fullscreen.vert","cloud.frag"),("fullscreen.vert","cloud-composite.frag"),("fullscreen.vert","black-hole.frag"),
|
||||||
|
("fullscreen.vert","ocean-reflection.frag"),("fullscreen.vert","ocean-reflection-composite.frag") })
|
||||||
replacements.Add(Program(pair.Item1, pair.Item2));
|
replacements.Add(Program(pair.Item1, pair.Item2));
|
||||||
|
_starTrails.ReloadShaders();
|
||||||
}
|
}
|
||||||
catch (Exception error)
|
catch (Exception error)
|
||||||
{
|
{
|
||||||
@@ -186,34 +206,55 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
_sunGlow.Dispose(); _sunGlow = replacements[12];
|
_sunGlow.Dispose(); _sunGlow = replacements[12];
|
||||||
_ocean.Dispose(); _ocean = replacements[13];
|
_ocean.Dispose(); _ocean = replacements[13];
|
||||||
_clouds.Dispose(); _clouds = replacements[14]; _cloudComposite.Dispose(); _cloudComposite = replacements[15];
|
_clouds.Dispose(); _clouds = replacements[14]; _cloudComposite.Dispose(); _cloudComposite = replacements[15];
|
||||||
|
_blackHole.Dispose(); _blackHole = replacements[16];
|
||||||
|
_oceanReflection.Dispose(); _oceanReflection = replacements[17];
|
||||||
|
_oceanReflectionComposite.Dispose(); _oceanReflectionComposite = replacements[18];
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetActiveSystem(SectorCoordinate coordinate)
|
public void SetActiveSystem(SectorCoordinate coordinate)
|
||||||
{
|
{
|
||||||
if (coordinate == _activeSystem) return;
|
if (coordinate == _activeSystem) return;
|
||||||
_activeSystem = coordinate;
|
_activeSystem = coordinate;
|
||||||
foreach (var mesh in _planetMeshes.Values) mesh.Dispose();
|
|
||||||
_planetMeshes.Clear();
|
|
||||||
foreach (var textures in _planetTextures.Values) textures.Dispose();
|
|
||||||
foreach (var rings in _ringProfiles.Values) rings.Dispose();
|
|
||||||
foreach (var rocks in _ringParticles.Values) rocks.Dispose();
|
|
||||||
_planetTextures.Clear(); _ringProfiles.Clear(); _ringParticles.Clear();
|
|
||||||
foreach (var texture in _cloudDensityTextures.Values) texture.Dispose();
|
|
||||||
_cloudDensityTextures.Clear();
|
|
||||||
_gpuProfiler.Clear();
|
_gpuProfiler.Clear();
|
||||||
_probeFrameCounter = 0;
|
_probeFrameCounter = 0;
|
||||||
_debugProbePlanetId = null;
|
_debugProbePlanetId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RetainSystem(StarSystemData system)
|
||||||
|
{
|
||||||
|
if (system.Stars.Count == 0) return;
|
||||||
|
_residentSystems.Remove(system.Coordinate); _residentSystems.Add(system.Coordinate, system);
|
||||||
|
if (_residentSystems.Count <= 3) return;
|
||||||
|
var victim = _residentSystems.First(); _residentSystems.Remove(victim.Key);
|
||||||
|
foreach (var planet in victim.Value.Planets)
|
||||||
|
{
|
||||||
|
if (_planetMeshes.Remove(planet.Id, out var mesh)) mesh.Dispose();
|
||||||
|
if (_planetTextures.Remove(planet.Id, out var textures)) textures.Dispose();
|
||||||
|
if (_ringProfiles.Remove(planet.Id, out var rings)) rings.Dispose();
|
||||||
|
if (_ringParticles.Remove(planet.Id, out var rocks)) rocks.Dispose();
|
||||||
|
if (_cloudDensityTextures.Remove(planet.Id, out var clouds)) clouds.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void PreparePlanet(StarSystemData system, PlanetData planet)
|
||||||
|
{
|
||||||
|
RetainSystem(system);
|
||||||
|
if (!planet.IsGasPlanet && !_planetMeshes.ContainsKey(planet.Id)) _planetMeshes[planet.Id] = new(_gl, planet);
|
||||||
|
if (!_planetTextures.ContainsKey(planet.Id)) _planetTextures[planet.Id] = new(_gl, planet);
|
||||||
|
}
|
||||||
|
|
||||||
public void Resize(int width, int height)
|
public void Resize(int width, int height)
|
||||||
{
|
{
|
||||||
_width = System.Math.Max(width, 1);
|
_width = System.Math.Max(width, 1);
|
||||||
_height = System.Math.Max(height, 1);
|
_height = System.Math.Max(height, 1);
|
||||||
_cloudRaw?.Dispose(); _cloudRaw = null;
|
_cloudRaw?.Dispose(); _cloudRaw = null;
|
||||||
|
_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();
|
||||||
_scene = new(_gl, _width, _height, true);
|
_blackHoleTarget?.Dispose();
|
||||||
|
_scene = new(_gl, _width, _height, true, auxiliaryColors: 2);
|
||||||
|
_waterReflectionTarget = new(_gl, _width, _height);
|
||||||
_gasInput = new(_gl, _width, _height, true);
|
_gasInput = new(_gl, _width, _height, true);
|
||||||
_glareTarget = new(_gl, _width, _height);
|
_glareTarget = new(_gl, _width, _height);
|
||||||
|
_blackHoleTarget = new(_gl, _width, _height);
|
||||||
_scatterA = new(_gl, _width, _height);
|
_scatterA = new(_gl, _width, _height);
|
||||||
_scatterB = new(_gl, _width, _height);
|
_scatterB = new(_gl, _width, _height);
|
||||||
_bloomA = new(_gl, System.Math.Max(1, _width / 4), System.Math.Max(1, _height / 4));
|
_bloomA = new(_gl, System.Math.Max(1, _width / 4), System.Math.Max(1, _height / 4));
|
||||||
@@ -224,6 +265,7 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
public void Render(FlyCamera camera, UniverseNeighborhood neighborhood, double deltaSeconds)
|
public void Render(FlyCamera camera, UniverseNeighborhood neighborhood, double deltaSeconds)
|
||||||
{
|
{
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
|
_starTrails.BeginFrame(camera,_width,_height,deltaSeconds);
|
||||||
var cpuTimer = DebugMode ? System.Diagnostics.Stopwatch.StartNew() : null;
|
var cpuTimer = DebugMode ? System.Diagnostics.Stopwatch.StartNew() : null;
|
||||||
var timer = ProfileFrame ? System.Diagnostics.Stopwatch.StartNew() : null;
|
var timer = ProfileFrame ? System.Diagnostics.Stopwatch.StartNew() : null;
|
||||||
if (timer is not null) { _gl.Finish(); timer.Restart(); FrameTimings.Clear(); }
|
if (timer is not null) { _gl.Finish(); timer.Restart(); FrameTimings.Clear(); }
|
||||||
@@ -232,7 +274,7 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
if (timer is null) return;
|
if (timer is null) return;
|
||||||
_gl.Finish(); FrameTimings[name] = timer.Elapsed.TotalMilliseconds; timer.Restart();
|
_gl.Finish(); FrameTimings[name] = timer.Elapsed.TotalMilliseconds; timer.Restart();
|
||||||
}
|
}
|
||||||
_time += System.Math.Min(deltaSeconds, 0.1);
|
_time = SimulationTimeSeconds ?? (_time + System.Math.Min(deltaSeconds, 0.1));
|
||||||
TerrainTiles = 0;
|
TerrainTiles = 0;
|
||||||
TerrainDeepestLevel = 0; TerrainPendingTiles = 0;
|
TerrainDeepestLevel = 0; TerrainPendingTiles = 0;
|
||||||
TerrainNearestLevel = 0; TerrainNearestDistance = double.PositiveInfinity;
|
TerrainNearestLevel = 0; TerrainNearestDistance = double.PositiveInfinity;
|
||||||
@@ -242,23 +284,27 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
var projection = Matrix4x4.CreatePerspectiveFieldOfView((float)camera.FieldOfViewRadians, (float)_width / _height, 0.02f, far);
|
var projection = Matrix4x4.CreatePerspectiveFieldOfView((float)camera.FieldOfViewRadians, (float)_width / _height, 0.02f, far);
|
||||||
var vp = view * projection;
|
var vp = view * projection;
|
||||||
var system = neighborhood.ActiveSystem;
|
var system = neighborhood.ActiveSystem;
|
||||||
|
RetainSystem(system);
|
||||||
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
|
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);
|
_gpuProfiler.BeginFrame(DebugMode);
|
||||||
_scene.Bind();
|
_oceanRendered = false; _cloudLayerPlanetId = null;
|
||||||
|
_scene.Bind(allColors: true);
|
||||||
_gl.DepthMask(true);
|
_gl.DepthMask(true);
|
||||||
_gl.ClearColor(0, 0, 0, 1);
|
_gl.ClearColor(0, 0, 0, 0);
|
||||||
_gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
|
_gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
|
||||||
|
_scene.Bind();
|
||||||
_gl.Disable(GLEnum.Blend);
|
_gl.Disable(GLEnum.Blend);
|
||||||
_gl.Disable(GLEnum.DepthTest);
|
_gl.Disable(GLEnum.DepthTest);
|
||||||
_gl.DepthMask(false);
|
_gl.DepthMask(false);
|
||||||
_sky.Use();
|
_sky.Use();
|
||||||
SetCameraRay(_sky, camera);
|
SetCameraRay(_sky, camera);
|
||||||
_sky.Set("uNoise", 4); _sky.Set("uTime", (float)_time);
|
_sky.Set("uNoise", 4); _sky.Set("uTime", (float)_time);
|
||||||
|
_sky.Set("uBackgroundStrength", _cosmos is null ? 1f : 0f);
|
||||||
_sky.Set("uLightCount", system.Stars.Count);
|
_sky.Set("uLightCount", system.Stars.Count);
|
||||||
_sky.SetVectorArray("uStarDirections", starPositions.Select(p => p.Normalized().ToVector3()).ToArray());
|
_sky.SetVectorArray("uStarDirections", starPositions.Select(p => p.Normalized().ToVector3()).ToArray());
|
||||||
_sky.SetVectorArray("uStarColors", system.Stars.Select(s => s.Color).ToArray());
|
_sky.SetVectorArray("uStarColors", system.Stars.Select(s => s.IsBlackHole ? Vector3.Zero : s.Color).ToArray());
|
||||||
_sky.SetFloatArray("uStarAngles", system.Stars.Select((s, i) => (float)System.Math.Asin(System.Math.Clamp(ApparentStarRadius(s, starPositions[i].Length) / starPositions[i].Length, 0, 1))).ToArray());
|
_sky.SetFloatArray("uStarAngles", system.Stars.Select((s, i) => (float)System.Math.Asin(System.Math.Clamp(ApparentStarRadius(s, starPositions[i].Length) / starPositions[i].Length, 0, 1))).ToArray());
|
||||||
_gpuProfiler.BeginPass("SKY / STARFIELD");
|
_gpuProfiler.BeginPass("SKY / STARFIELD");
|
||||||
_fullscreen.Draw();
|
_fullscreen.Draw();
|
||||||
@@ -266,6 +312,10 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
DrawStarfield(camera, neighborhood, vp, far);
|
DrawStarfield(camera, neighborhood, vp, far);
|
||||||
_gpuProfiler.EndPass();
|
_gpuProfiler.EndPass();
|
||||||
Mark("sky");
|
Mark("sky");
|
||||||
|
_gpuProfiler.BeginPass("GALAXIES / NEBULAE");
|
||||||
|
_cosmos?.Draw(camera, _width, _height, deltaSeconds);
|
||||||
|
_gpuProfiler.EndPass();
|
||||||
|
Mark("galaxies and nebulae");
|
||||||
_gl.DepthMask(true);
|
_gl.DepthMask(true);
|
||||||
_gl.Disable(GLEnum.Blend);
|
_gl.Disable(GLEnum.Blend);
|
||||||
_gpuProfiler.BeginPass("PLANETS TOTAL");
|
_gpuProfiler.BeginPass("PLANETS TOTAL");
|
||||||
@@ -295,6 +345,14 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
composed = DrawClouds(camera, system, far, composed);
|
composed = DrawClouds(camera, system, far, composed);
|
||||||
_gpuProfiler.EndPass();
|
_gpuProfiler.EndPass();
|
||||||
Mark("clouds");
|
Mark("clouds");
|
||||||
|
_gpuProfiler.BeginPass("WATER REFLECTIONS");
|
||||||
|
composed = DrawOceanReflections(camera, system, far, composed);
|
||||||
|
_gpuProfiler.EndPass();
|
||||||
|
Mark("water reflections");
|
||||||
|
_gpuProfiler.BeginPass("BLACK HOLE / LENSING");
|
||||||
|
composed = DrawBlackHoles(camera, system, far, composed);
|
||||||
|
_gpuProfiler.EndPass();
|
||||||
|
Mark("black hole lensing");
|
||||||
_gpuProfiler.BeginPass("SUN GLOW");
|
_gpuProfiler.BeginPass("SUN GLOW");
|
||||||
DrawSunGlow(camera, system, starPositions, far, composed);
|
DrawSunGlow(camera, system, starPositions, far, composed);
|
||||||
_gpuProfiler.EndPass();
|
_gpuProfiler.EndPass();
|
||||||
@@ -317,24 +375,25 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int MenuHit(double x, double y, int width, int height) => _pauseMenu.Hit(x, y, width, height);
|
public int MenuHit(double x, double y, int width, int height) => _pauseMenu.Hit(x, y, width, height);
|
||||||
public void DrawMenu(int selection, bool debugEnabled)
|
public double? TimeScaleHit(double x, double y, int width, int height) => PauseMenu.TimeScaleHit(x, y, width, height);
|
||||||
|
public void DrawMenu(int selection, bool debugEnabled, double timeScale = 1, DateTimeOffset? simulationUtc = null)
|
||||||
{
|
{
|
||||||
_gl.Disable(GLEnum.DepthTest); _gl.Disable(GLEnum.Blend);
|
_gl.Disable(GLEnum.DepthTest); _gl.Disable(GLEnum.Blend);
|
||||||
_pauseMenu.Selected = selection; _overlay.Use(); _pauseMenu.Draw(_width, _height, debugEnabled);
|
_pauseMenu.Selected = selection; _overlay.Use(); _pauseMenu.Draw(_width, _height, debugEnabled, timeScale, simulationUtc);
|
||||||
_gl.Enable(GLEnum.DepthTest);
|
_gl.Enable(GLEnum.DepthTest);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DrawDebugOverlay(double fps)
|
public void DrawDebugOverlay(double fps, double timeScale = 1, DateTimeOffset? simulationUtc = null)
|
||||||
{
|
{
|
||||||
if (!DebugMode) return;
|
if (!DebugMode) return;
|
||||||
_gl.Disable(GLEnum.DepthTest); _gl.Disable(GLEnum.Blend); _gl.DepthMask(false);
|
_gl.Disable(GLEnum.DepthTest); _gl.Disable(GLEnum.Blend); _gl.DepthMask(false);
|
||||||
_overlay.Use();
|
_overlay.Use();
|
||||||
_performanceOverlay.Draw(_width, _height, DebugSamples(), CpuRenderMilliseconds, fps,
|
_performanceOverlay.Draw(_width, _height, DebugSamples(), CpuRenderMilliseconds, fps,
|
||||||
TerrainTiles, TerrainDeepestLevel, TerrainPendingTiles, _debugProbePlanetId);
|
TerrainTiles, TerrainDeepestLevel, TerrainPendingTiles, _debugProbePlanetId, timeScale, simulationUtc);
|
||||||
_gl.DepthMask(true); _gl.Enable(GLEnum.DepthTest);
|
_gl.DepthMask(true); _gl.Enable(GLEnum.DepthTest);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DrawBenchmarkOverlay(BenchmarkRunner benchmark)
|
public void DrawBenchmarkOverlay(IBenchmarkSession benchmark)
|
||||||
{
|
{
|
||||||
_gl.Disable(GLEnum.DepthTest); _gl.Disable(GLEnum.Blend); _gl.DepthMask(false);
|
_gl.Disable(GLEnum.DepthTest); _gl.Disable(GLEnum.Blend); _gl.DepthMask(false);
|
||||||
_overlay.Use();
|
_overlay.Use();
|
||||||
@@ -371,22 +430,22 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
{
|
{
|
||||||
_gl.Enable(GLEnum.Blend);
|
_gl.Enable(GLEnum.Blend);
|
||||||
_gl.BlendFunc(GLEnum.One, GLEnum.One);
|
_gl.BlendFunc(GLEnum.One, GLEnum.One);
|
||||||
_points.Use(); _points.SetMatrix("uViewProjection", vp); _points.Set("uFar", far); _points.Set("uScale", far * 0.55f);
|
_starTrails.Brightness = StarVisibility(camera, neighborhood.ActiveSystem);
|
||||||
_points.Set("uBrightness", StarVisibility(camera, neighborhood.ActiveSystem));
|
if (_cosmos is null) _starTrails.Draw(_backgroundStars,camera.Position,1,PlanetOrientation.Identity,directional:true);
|
||||||
_backgroundStars.Draw(GLEnum.Points);
|
var scale = _cosmos is null ? (double)far : UniverseScale.LightYearMeters;
|
||||||
var points = new List<float>(26 * 14);
|
var points = new List<float>(26 * 14);
|
||||||
foreach (var system in neighborhood.NearbySystems)
|
foreach (var system in neighborhood.NearbySystems)
|
||||||
{
|
{
|
||||||
var origin = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
|
var origin = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
|
||||||
foreach (var star in system.Stars)
|
foreach (var star in system.Stars)
|
||||||
{
|
{
|
||||||
|
if (star.IsBlackHole) continue;
|
||||||
var rel = origin.Add(star.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters);
|
var rel = origin.Add(star.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters);
|
||||||
AddPoint(points, rel, star.Color * 1.3f, 3.2f);
|
AddPoint(points, rel/scale, star.Color * 1.3f, 3.2f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_navigationStars.ReplaceVertices(points.ToArray());
|
_navigationStars.ReplaceVertices(points.ToArray());
|
||||||
_points.Set("uScale", 1f);
|
_starTrails.Draw(_navigationStars,camera.Position,scale,PlanetOrientation.Identity);
|
||||||
_navigationStars.Draw(GLEnum.Points);
|
|
||||||
_gl.Disable(GLEnum.Blend);
|
_gl.Disable(GLEnum.Blend);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,6 +469,7 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
if (planet.IsGasPlanet) continue;
|
if (planet.IsGasPlanet) continue;
|
||||||
var relative = relativePlanets[i];
|
var relative = relativePlanets[i];
|
||||||
if (DVector3.Dot(relative, camera.Forward) < -planet.MaximumTerrainRadiusMeters) continue;
|
if (DVector3.Dot(relative, camera.Forward) < -planet.MaximumTerrainRadiusMeters) continue;
|
||||||
|
if (planet.RadiusMeters / System.Math.Max(1,relative.Length) * _height / (2 * System.Math.Tan(camera.FieldOfViewRadians*.5)) < .5) continue;
|
||||||
if (!_planetMeshes.TryGetValue(planet.Id, out var mesh))
|
if (!_planetMeshes.TryGetValue(planet.Id, out var mesh))
|
||||||
{
|
{
|
||||||
mesh = new(_gl, planet); _planetMeshes.Add(planet.Id, mesh);
|
mesh = new(_gl, planet); _planetMeshes.Add(planet.Id, mesh);
|
||||||
@@ -451,14 +511,16 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
BindRingProfile(_surface, planet);
|
BindRingProfile(_surface, planet);
|
||||||
_surface.Set("uTerrainShadows", relative.Length < planet.RadiusMeters * 1.18 && probeEffect != PlanetProbeEffect.MountainShadows ? 1 : 0);
|
_surface.Set("uTerrainShadows", relative.Length < planet.RadiusMeters * 1.18 && probeEffect != PlanetProbeEffect.MountainShadows ? 1 : 0);
|
||||||
if (onlyPlanetId is null) _gpuProfiler.BeginPass($"PLANET {planet.Id.ToUpperInvariant()}");
|
if (onlyPlanetId is null) _gpuProfiler.BeginPass($"PLANET {planet.Id.ToUpperInvariant()}");
|
||||||
mesh.Draw(-relative, camera.Forward, camera.Right, camera.Up, _height, (double)_width / _height, System.Math.Tan(camera.FieldOfViewRadians * 0.5), (origin, gpu) =>
|
_surface.SetMatrix("uBodyToWorld", planet.Orientation.BodyToWorld);
|
||||||
|
mesh.Draw(planet.Orientation.ToBody(-relative), planet.Orientation.ToBody(camera.Forward), planet.Orientation.ToBody(camera.Right), planet.Orientation.ToBody(camera.Up), _height, (double)_width / _height, System.Math.Tan(camera.FieldOfViewRadians * 0.5), (origin, gpu) =>
|
||||||
{
|
{
|
||||||
var anchor = planet.Orientation.ToBody(origin);
|
var anchor = origin;
|
||||||
var scale = materialSettings.ScaleMeters;
|
var scale = materialSettings.ScaleMeters;
|
||||||
static double Mod(double value, double divisor) => value - System.Math.Floor(value / divisor) * divisor;
|
static double Mod(double value, double divisor) => value - System.Math.Floor(value / divisor) * divisor;
|
||||||
_surface.Set("uMaterialAnchor", new Vector3((float)Mod(anchor.X, scale), (float)Mod(anchor.Y, scale), (float)Mod(anchor.Z, scale)));
|
_surface.Set("uMaterialAnchor", new Vector3((float)Mod(anchor.X, scale), (float)Mod(anchor.Y, scale), (float)Mod(anchor.Z, scale)));
|
||||||
_surface.Set("uChunkPosition", (relative + origin).ToVector3());
|
var worldOrigin = planet.Orientation.ToWorld(origin);
|
||||||
_surface.Set("uChunkOrigin", (origin / planet.RadiusMeters).ToVector3());
|
_surface.Set("uChunkPosition", (relative + worldOrigin).ToVector3());
|
||||||
|
_surface.Set("uChunkOrigin", (worldOrigin / planet.RadiusMeters).ToVector3());
|
||||||
gpu.Draw();
|
gpu.Draw();
|
||||||
});
|
});
|
||||||
if (onlyPlanetId is null)
|
if (onlyPlanetId is null)
|
||||||
@@ -573,7 +635,8 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
|
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
|
||||||
var candidates = system.Planets.Where(p => p.IsGasPlanet).Select(p => (Planet: p,
|
var candidates = system.Planets.Where(p => p.IsGasPlanet).Select(p => (Planet: p,
|
||||||
Relative: center.Add(p.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters)))
|
Relative: center.Add(p.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters)))
|
||||||
.Where(p => DVector3.Dot(p.Relative, camera.Forward) > -p.Planet.RadiusMeters * 1.3)
|
.Where(p => DVector3.Dot(p.Relative, camera.Forward) > -p.Planet.RadiusMeters * 1.3
|
||||||
|
&& p.Planet.RadiusMeters / System.Math.Max(1,p.Relative.Length) * _height / (2 * System.Math.Tan(camera.FieldOfViewRadians*.5)) >= .5)
|
||||||
.OrderByDescending(p => p.Relative.Length);
|
.OrderByDescending(p => p.Relative.Length);
|
||||||
foreach (var (planet, relative) in candidates)
|
foreach (var (planet, relative) in candidates)
|
||||||
{
|
{
|
||||||
@@ -630,7 +693,9 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
_gl.BindFramebuffer(GLEnum.DrawFramebuffer, _gasInput.Framebuffer);
|
_gl.BindFramebuffer(GLEnum.DrawFramebuffer, _gasInput.Framebuffer);
|
||||||
_gl.BlitFramebuffer(0, 0, _width, _height, 0, 0, _width, _height,
|
_gl.BlitFramebuffer(0, 0, _width, _height, 0, 0, _width, _height,
|
||||||
ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit, BlitFramebufferFilter.Nearest);
|
ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit, BlitFramebufferFilter.Nearest);
|
||||||
_scene.Bind(); _gl.Enable(GLEnum.DepthTest); _gl.DepthFunc(GLEnum.Always); _gl.DepthMask(true);
|
_scene.Bind(allColors: true); _gl.Enable(GLEnum.DepthTest); _gl.DepthFunc(GLEnum.Always); _gl.DepthMask(true);
|
||||||
|
_gl.Enable(GLEnum.Blend); _gl.BlendFunc(GLEnum.One, GLEnum.OneMinusSrcAlpha);
|
||||||
|
_oceanRendered = true;
|
||||||
_ocean.Use(); SetCameraRay(_ocean, camera); BindPlanetTerrain(_ocean, planet, textures);
|
_ocean.Use(); SetCameraRay(_ocean, camera); BindPlanetTerrain(_ocean, planet, textures);
|
||||||
BindCloudSettings(_ocean, camera, system, planet, relative);
|
BindCloudSettings(_ocean, camera, system, planet, relative);
|
||||||
BindTexture(0, _gasInput.Color); BindTexture(1, _gasInput.Depth);
|
BindTexture(0, _gasInput.Color); BindTexture(1, _gasInput.Depth);
|
||||||
@@ -673,9 +738,49 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
System.Math.Pow((s.PositionMeters - planet.PositionMeters).Length / UniverseScale.AstronomicalUnitMeters, 2), .02, 7)).ToArray());
|
System.Math.Pow((s.PositionMeters - planet.PositionMeters).Length / UniverseScale.AstronomicalUnitMeters, 2), .02, 7)).ToArray());
|
||||||
_fullscreen.Draw();
|
_fullscreen.Draw();
|
||||||
}
|
}
|
||||||
|
_gl.Disable(GLEnum.Blend); _scene.Bind();
|
||||||
_gl.DepthFunc(GLEnum.Lequal);
|
_gl.DepthFunc(GLEnum.Lequal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RenderTarget DrawOceanReflections(FlyCamera camera, StarSystemData system, float far, RenderTarget source)
|
||||||
|
{
|
||||||
|
if (!_oceanRendered) return source;
|
||||||
|
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
|
||||||
|
// Only a nearby ocean needs detailed SSR; orbital views retain the cheaper analytic reflection.
|
||||||
|
var planet = system.Planets.Where(p => p.Ocean is { Enabled: true, ScreenSpaceReflections: { Enabled: true, Strength: > 0 } })
|
||||||
|
.Where(p => System.Math.Abs(center.Add(p.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters).Length
|
||||||
|
- p.RadiusMeters - p.Ocean!.SeaLevelMeters) < p.Ocean.ScreenSpaceReflections.MaxSurfaceDistanceMeters)
|
||||||
|
.MinBy(p => center.Add(p.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters).Length - p.RadiusMeters);
|
||||||
|
if (planet is null) return source;
|
||||||
|
var r = planet.Ocean!.ScreenSpaceReflections;
|
||||||
|
var width = System.Math.Max(1, (int)System.Math.Round(_width * r.ResolutionScale));
|
||||||
|
var height = System.Math.Max(1, (int)System.Math.Round(_height * r.ResolutionScale));
|
||||||
|
if (_waterReflectionRaw is null || _waterReflectionRaw.Width != width || _waterReflectionRaw.Height != height)
|
||||||
|
{ _waterReflectionRaw?.Dispose(); _waterReflectionRaw = new(_gl, width, height); }
|
||||||
|
source.GenerateColorMipmaps();
|
||||||
|
_gl.Disable(GLEnum.Blend); _gl.Disable(GLEnum.DepthTest); _gl.DepthMask(false);
|
||||||
|
_waterReflectionRaw.Bind(); _oceanReflection.Use(); SetCameraRay(_oceanReflection, camera);
|
||||||
|
BindTexture(0, source.Color); BindTexture(1, _scene.Depth); BindTexture(2, _scene.AuxiliaryColors[0]);
|
||||||
|
_oceanReflection.Set("uScene", 0); _oceanReflection.Set("uDepth", 1); _oceanReflection.Set("uWaterNormals", 2);
|
||||||
|
_oceanReflection.Set("uFar", far); _oceanReflection.Set("uMaxSteps", r.MaxSteps);
|
||||||
|
_oceanReflection.Set("uTraceSettings", new Vector3((float)r.MaxDistanceMeters, (float)r.ThicknessMeters, (float)r.NormalBiasMeters));
|
||||||
|
_oceanReflection.Set("uReflectionSettings", new Vector3((float)r.EdgeFade, (float)r.StridePixels, (float)planet.Ocean.Roughness));
|
||||||
|
_oceanReflection.Set("uMaxSurfaceDistance", (float)r.MaxSurfaceDistanceMeters);
|
||||||
|
_oceanReflection.Set("uReflectionStrength", (float)r.Strength); _oceanReflection.Set("uReflectSky", r.ReflectSky ? 1 : 0);
|
||||||
|
_fullscreen.Draw();
|
||||||
|
_waterReflectionTarget.Bind(); _oceanReflectionComposite.Use();
|
||||||
|
BindTexture(0, source.Color); BindTexture(1, _scene.Depth); BindTexture(2, _scene.AuxiliaryColors[0]);
|
||||||
|
BindTexture(3, _scene.AuxiliaryColors[1]); BindTexture(5, _waterReflectionRaw.Color);
|
||||||
|
if (_cloudRaw is not null) BindTexture(6, _cloudRaw.Color);
|
||||||
|
_oceanReflectionComposite.Set("uScene", 0); _oceanReflectionComposite.Set("uDepth", 1);
|
||||||
|
_oceanReflectionComposite.Set("uWaterNormals", 2); _oceanReflectionComposite.Set("uWaterFallback", 3);
|
||||||
|
_oceanReflectionComposite.Set("uReflection", 5); _oceanReflectionComposite.Set("uClouds", 6);
|
||||||
|
_oceanReflectionComposite.Set("uHasClouds", _cloudLayerPlanetId == planet.Id ? 1 : 0);
|
||||||
|
_oceanReflectionComposite.Set("uFar", far); _fullscreen.Draw();
|
||||||
|
_gl.Enable(GLEnum.DepthTest); _gl.DepthMask(true);
|
||||||
|
return _waterReflectionTarget;
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawRings(FlyCamera camera, StarSystemData system, float far)
|
private void DrawRings(FlyCamera camera, StarSystemData system, float far)
|
||||||
{
|
{
|
||||||
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
|
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
|
||||||
@@ -748,6 +853,7 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
for (var i = 0; i < system.Stars.Count; i++)
|
for (var i = 0; i < system.Stars.Count; i++)
|
||||||
{
|
{
|
||||||
var star = system.Stars[i];
|
var star = system.Stars[i];
|
||||||
|
if (star.IsBlackHole) continue;
|
||||||
_star.Set("uCenter", positions[i].ToVector3());
|
_star.Set("uCenter", positions[i].ToVector3());
|
||||||
_star.Set("uRadius", (float)ApparentStarRadius(star, positions[i].Length));
|
_star.Set("uRadius", (float)ApparentStarRadius(star, positions[i].Length));
|
||||||
_star.Set("uColor", star.Color);
|
_star.Set("uColor", star.Color);
|
||||||
@@ -756,6 +862,32 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RenderTarget DrawBlackHoles(FlyCamera camera, StarSystemData system, float far, RenderTarget source)
|
||||||
|
{
|
||||||
|
var star = system.Stars.FirstOrDefault(s => s.IsBlackHole);
|
||||||
|
if (star?.BlackHole is not { } hole) return source;
|
||||||
|
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
|
||||||
|
var relative = center.Add(star.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters);
|
||||||
|
if (DVector3.Dot(relative, camera.Forward) < -hole.HorizonRadiusMeters * 32) return source;
|
||||||
|
_gl.Disable(GLEnum.DepthTest); _gl.Disable(GLEnum.Blend); _gl.DepthMask(false);
|
||||||
|
_blackHoleTarget.Bind(); _blackHole.Use(); SetCameraRay(_blackHole, camera);
|
||||||
|
BindTexture(0, source.Color); BindTexture(1, _scene.Depth);
|
||||||
|
var axis = System.Math.Abs(hole.DiskNormal.Y) < .9 ? new DVector3(0, 1, 0) : new DVector3(1, 0, 0);
|
||||||
|
var x = DVector3.Cross(hole.DiskNormal, axis).Normalized();
|
||||||
|
var z = DVector3.Cross(x, hole.DiskNormal).Normalized();
|
||||||
|
_blackHole.Set("uScene", 0); _blackHole.Set("uDepth", 1); _blackHole.Set("uFar", far);
|
||||||
|
_blackHole.Set("uCamera", (-relative / hole.HorizonRadiusMeters).ToVector3());
|
||||||
|
_blackHole.Set("uDiskX", x.ToVector3()); _blackHole.Set("uDiskZ", z.ToVector3());
|
||||||
|
_blackHole.Set("uDiskNormal", hole.DiskNormal.ToVector3());
|
||||||
|
_blackHole.Set("uHorizonMeters", (float)hole.HorizonRadiusMeters);
|
||||||
|
_blackHole.Set("uOuterRadius", (float)hole.DiskOuterRadiusInHorizons);
|
||||||
|
_blackHole.Set("uBrightness", (float)hole.DiskBrightness);
|
||||||
|
_blackHole.Set("uSteps", hole.RaySteps); _blackHole.Set("uTime", (float)_time);
|
||||||
|
_blackHole.Set("uSeed", (float)(star.Seed & 65535) * .01f);
|
||||||
|
_fullscreen.Draw();
|
||||||
|
return _blackHoleTarget;
|
||||||
|
}
|
||||||
|
|
||||||
private RenderTarget DrawAtmospheres(FlyCamera camera, StarSystemData system, float far)
|
private RenderTarget DrawAtmospheres(FlyCamera camera, StarSystemData system, float far)
|
||||||
{
|
{
|
||||||
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
|
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
|
||||||
@@ -897,6 +1029,7 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
_cloudComposite.Set("uScene", 0); _cloudComposite.Set("uDepth", 1); _cloudComposite.Set("uClouds", 2);
|
_cloudComposite.Set("uScene", 0); _cloudComposite.Set("uDepth", 1); _cloudComposite.Set("uClouds", 2);
|
||||||
_cloudComposite.Set("uCloudTexel", new Vector3(1f / width, 1f / height, 0));
|
_cloudComposite.Set("uCloudTexel", new Vector3(1f / width, 1f / height, 0));
|
||||||
_fullscreen.Draw(); source = target;
|
_fullscreen.Draw(); source = target;
|
||||||
|
_cloudLayerPlanetId = planet.Id;
|
||||||
}
|
}
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
@@ -948,7 +1081,7 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
var z = DVector3.Dot(position, camera.Forward); var distance = position.Length;
|
var z = DVector3.Dot(position, camera.Forward); var distance = position.Length;
|
||||||
var apparentRadius = ApparentStarRadius(star, distance);
|
var apparentRadius = ApparentStarRadius(star, distance);
|
||||||
var front = System.Math.Max(1, z - apparentRadius * 1.06);
|
var front = System.Math.Max(1, z - apparentRadius * 1.06);
|
||||||
var intensity = z > 0 ? glow.Attenuation(distance, star.LuminositySolar) : 0;
|
var intensity = z > 0 && !star.IsBlackHole ? glow.Attenuation(distance, star.LuminositySolar) : 0;
|
||||||
var uv = new Vector3((float)(.5 + DVector3.Dot(position, camera.Right) / (2 * System.Math.Max(1, z) * tan * aspect)),
|
var uv = new Vector3((float)(.5 + DVector3.Dot(position, camera.Right) / (2 * System.Math.Max(1, z) * tan * aspect)),
|
||||||
(float)(.5 + DVector3.Dot(position, camera.Up) / (2 * System.Math.Max(1, z) * tan)),
|
(float)(.5 + DVector3.Dot(position, camera.Up) / (2 * System.Math.Max(1, z) * tan)),
|
||||||
(float)(System.Math.Log2(1 + front) / System.Math.Log2(1 + far)));
|
(float)(System.Math.Log2(1 + front) / System.Math.Log2(1 + far)));
|
||||||
@@ -1040,11 +1173,15 @@ internal sealed class UniverseRenderer : IDisposable
|
|||||||
_planetTextures.Clear(); _cloudDensityTextures.Clear(); _ringProfiles.Clear(); _ringParticles.Clear();
|
_planetTextures.Clear(); _cloudDensityTextures.Clear(); _ringProfiles.Clear(); _ringParticles.Clear();
|
||||||
_sphere.Dispose(); _fullscreen.Dispose(); _backgroundStars.Dispose(); _navigationStars.Dispose(); _crosshair.Dispose(); _pauseMenu.Dispose();
|
_sphere.Dispose(); _fullscreen.Dispose(); _backgroundStars.Dispose(); _navigationStars.Dispose(); _crosshair.Dispose(); _pauseMenu.Dispose();
|
||||||
_performanceOverlay.Dispose(); _gpuProfiler.Dispose();
|
_performanceOverlay.Dispose(); _gpuProfiler.Dispose();
|
||||||
|
_cosmos?.Dispose();
|
||||||
|
_starTrails.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();
|
||||||
_sunGlow.Dispose(); _glareTarget.Dispose();
|
_sunGlow.Dispose(); _glareTarget.Dispose();
|
||||||
|
_blackHole.Dispose(); _blackHoleTarget.Dispose();
|
||||||
_ocean.Dispose();
|
_ocean.Dispose();
|
||||||
|
_oceanReflection.Dispose(); _oceanReflectionComposite.Dispose(); _waterReflectionRaw?.Dispose(); _waterReflectionTarget.Dispose();
|
||||||
_clouds.Dispose(); _cloudComposite.Dispose(); _cloudNoise.Dispose(); _emptyCloudDensity.Dispose(); _materials.Dispose(); _cloudRaw?.Dispose();
|
_clouds.Dispose(); _cloudComposite.Dispose(); _cloudNoise.Dispose(); _emptyCloudDensity.Dispose(); _materials.Dispose(); _cloudRaw?.Dispose();
|
||||||
_scene.Dispose(); _scatterA.Dispose(); _scatterB.Dispose(); _bloomA.Dispose(); _bloomB.Dispose(); _noise.Dispose();
|
_scene.Dispose(); _scatterA.Dispose(); _scatterB.Dispose(); _bloomA.Dispose(); _bloomB.Dispose(); _noise.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
2
src/Universe.App/Properties/AssemblyInfo.cs
Normal file
2
src/Universe.App/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
[assembly: InternalsVisibleTo("Universe.Tests")]
|
||||||
@@ -6,7 +6,7 @@ using Universe.Core.World;
|
|||||||
namespace Universe.App.Runtime;
|
namespace Universe.App.Runtime;
|
||||||
|
|
||||||
/// <summary>Repeatable camera path and frame-time capture for a selected planetary terrain scene.</summary>
|
/// <summary>Repeatable camera path and frame-time capture for a selected planetary terrain scene.</summary>
|
||||||
internal sealed class BenchmarkRunner
|
internal sealed class BenchmarkRunner : IBenchmarkSession
|
||||||
{
|
{
|
||||||
private const double SpaceSeconds = 8;
|
private const double SpaceSeconds = 8;
|
||||||
private const double DescentSeconds = 40;
|
private const double DescentSeconds = 40;
|
||||||
@@ -77,7 +77,7 @@ internal sealed class BenchmarkRunner
|
|||||||
_orbitBitangent = DVector3.Cross(_targetRadial, _orbitTangent).Normalized();
|
_orbitBitangent = DVector3.Cross(_targetRadial, _orbitTangent).Normalized();
|
||||||
|
|
||||||
var initialRadius = _planet.RadiusMeters * 4.8;
|
var initialRadius = _planet.RadiusMeters * 4.8;
|
||||||
camera.SetPlayerPose(_body.Add(_spaceRadial * initialRadius, cellSizeMeters), -_spaceRadial, _spaceUp);
|
camera.SetPlayerPose(_body.Add(_spaceRadial * initialRadius, cellSizeMeters), -_spaceRadial, _spaceUp, resetMotionHistory: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsRunning => !_stopped && _elapsed < TotalSeconds;
|
public bool IsRunning => !_stopped && _elapsed < TotalSeconds;
|
||||||
@@ -169,7 +169,7 @@ internal sealed class BenchmarkRunner
|
|||||||
_cellSize, cameraRadiusMeters: 1.5, samples: 32);
|
_cellSize, cameraRadiusMeters: 1.5, samples: 32);
|
||||||
if (_elapsed >= TotalSeconds)
|
if (_elapsed >= TotalSeconds)
|
||||||
{
|
{
|
||||||
camera.SetPlayerPose(_returnPosition, _returnForward, _returnUp);
|
camera.SetPlayerPose(_returnPosition, _returnForward, _returnUp, resetMotionHistory: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var finalPosition = proposed;
|
var finalPosition = proposed;
|
||||||
|
|||||||
@@ -12,17 +12,23 @@ public sealed class FlyCamera
|
|||||||
private DVector3 _up = new(0, 1, 0);
|
private DVector3 _up = new(0, 1, 0);
|
||||||
private FocusFlight? _flight;
|
private FocusFlight? _flight;
|
||||||
private double _flightElapsed;
|
private double _flightElapsed;
|
||||||
|
private StarSystemData? _flightSystem;
|
||||||
|
private PlanetData? _flightPlanet;
|
||||||
private double _defaultFieldOfView = 2 * System.Math.Atan(.67);
|
private double _defaultFieldOfView = 2 * System.Math.Atan(.67);
|
||||||
public double FieldOfViewRadians { get; private set; } = 2 * System.Math.Atan(.67);
|
public double FieldOfViewRadians { get; private set; } = 2 * System.Math.Atan(.67);
|
||||||
public FlyCamera(UniversePosition initialPosition, double cellSizeMeters)
|
public FlyCamera(UniversePosition initialPosition, double cellSizeMeters)
|
||||||
{
|
{
|
||||||
Position = initialPosition;
|
Position = initialPosition;
|
||||||
CellSizeMeters = cellSizeMeters;
|
CellSizeMeters = cellSizeMeters;
|
||||||
|
TravelLimitMeters = cellSizeMeters * .45;
|
||||||
SpeedMetersPerSecond = 250_000;
|
SpeedMetersPerSecond = 250_000;
|
||||||
}
|
}
|
||||||
public UniversePosition Position { get; private set; }
|
public UniversePosition Position { get; private set; }
|
||||||
public double CellSizeMeters { get; }
|
public double CellSizeMeters { get; }
|
||||||
public double SpeedMetersPerSecond { get; private set; }
|
public double SpeedMetersPerSecond { get; private set; }
|
||||||
|
public double TravelLimitMeters { get; set; }
|
||||||
|
public long MotionHistoryVersion { get; private set; }
|
||||||
|
public void RestoreDefaultFieldOfView() => FieldOfViewRadians = _defaultFieldOfView;
|
||||||
public bool IsFocusing => _flight is not null;
|
public bool IsFocusing => _flight is not null;
|
||||||
public DVector3 Forward => _forward;
|
public DVector3 Forward => _forward;
|
||||||
public DVector3 Right => DVector3.Cross(_forward, _up).Normalized();
|
public DVector3 Right => DVector3.Cross(_forward, _up).Normalized();
|
||||||
@@ -37,16 +43,18 @@ public sealed class FlyCamera
|
|||||||
_forward = RotateAround(_forward, axis, -mouseDeltaY * 0.0022).Normalized();
|
_forward = RotateAround(_forward, axis, -mouseDeltaY * 0.0022).Normalized();
|
||||||
_up = RotateAround(beforePitchUp, axis, -mouseDeltaY * 0.0022).Normalized();
|
_up = RotateAround(beforePitchUp, axis, -mouseDeltaY * 0.0022).Normalized();
|
||||||
}
|
}
|
||||||
public void AdjustSpeed(double delta) => SpeedMetersPerSecond = System.Math.Clamp(SpeedMetersPerSecond * System.Math.Pow(1.7, delta), 0.1, UniverseScale.LightSpeedMetersPerSecond * 1_000_000);
|
public void AdjustSpeed(double delta) => SpeedMetersPerSecond = System.Math.Clamp(SpeedMetersPerSecond * System.Math.Pow(1.7, delta), 0.1, UniverseScale.LightSpeedMetersPerSecond * 1e15);
|
||||||
|
|
||||||
public void SetPlayerPose(UniversePosition position, DVector3 forward, DVector3 up)
|
public void SetPlayerPose(UniversePosition position, DVector3 forward, DVector3 up, bool resetMotionHistory = false)
|
||||||
{
|
{
|
||||||
|
if (resetMotionHistory) MotionHistoryVersion++;
|
||||||
_flight = null;
|
_flight = null;
|
||||||
Position = position; _forward = forward.Normalized(); _up = up.Normalized();
|
Position = position; _forward = forward.Normalized(); _up = up.Normalized();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetInitialView(StarSystemData system)
|
public void SetInitialView(StarSystemData system)
|
||||||
{
|
{
|
||||||
|
MotionHistoryVersion++;
|
||||||
var planet = system.Planets[0];
|
var planet = system.Planets[0];
|
||||||
var center = UniversePosition.AtCellCenter(system.Coordinate, CellSizeMeters);
|
var center = UniversePosition.AtCellCenter(system.Coordinate, CellSizeMeters);
|
||||||
var sun = (system.Stars[0].PositionMeters - planet.PositionMeters).Normalized();
|
var sun = (system.Stars[0].PositionMeters - planet.PositionMeters).Normalized();
|
||||||
@@ -59,6 +67,7 @@ public sealed class FlyCamera
|
|||||||
|
|
||||||
public void SetInitialView(StarSystemData system, StartupConfiguration startup)
|
public void SetInitialView(StarSystemData system, StartupConfiguration startup)
|
||||||
{
|
{
|
||||||
|
MotionHistoryVersion++;
|
||||||
var planet = system.Planets.FirstOrDefault(p => p.Id == startup.PlanetId)
|
var planet = system.Planets.FirstOrDefault(p => p.Id == startup.PlanetId)
|
||||||
?? throw new InvalidDataException($"Startup planet '{startup.PlanetId}' does not exist in system '{system.Id}'.");
|
?? throw new InvalidDataException($"Startup planet '{startup.PlanetId}' does not exist in system '{system.Id}'.");
|
||||||
var longitude = startup.LongitudeDegrees * System.Math.PI / 180;
|
var longitude = startup.LongitudeDegrees * System.Math.PI / 180;
|
||||||
@@ -272,7 +281,7 @@ public sealed class FlyCamera
|
|||||||
|
|
||||||
public void RequestStarView(IEnumerable<StarSystemData> systems)
|
public void RequestStarView(IEnumerable<StarSystemData> systems)
|
||||||
{
|
{
|
||||||
var nearest = systems.SelectMany(system => system.Stars.Select(star => (System: system, Star: star)))
|
var nearest = systems.SelectMany(system => system.Stars.Where(s => !s.IsBlackHole).Select(star => (System: system, Star: star)))
|
||||||
.MinBy(body => World(body.System, body.Star.PositionMeters).RelativeTo(Position, CellSizeMeters).LengthSquared);
|
.MinBy(body => World(body.System, body.Star.PositionMeters).RelativeTo(Position, CellSizeMeters).LengthSquared);
|
||||||
if (nearest.Star is null) return;
|
if (nearest.Star is null) return;
|
||||||
var bodyPosition = World(nearest.System, nearest.Star.PositionMeters);
|
var bodyPosition = World(nearest.System, nearest.Star.PositionMeters);
|
||||||
@@ -280,6 +289,44 @@ public sealed class FlyCamera
|
|||||||
BeginFlight(bodyPosition, radial, nearest.Star.RadiusMeters * 2.55, nearest.Star.RadiusMeters * 5, -radial, new(0, 1, 0), 10_000_000);
|
BeginFlight(bodyPosition, radial, nearest.Star.RadiusMeters * 2.55, nearest.Star.RadiusMeters * 5, -radial, new(0, 1, 0), 10_000_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RequestBlackHoleView(StarSystemData system, StarData star, IEnumerable<StarSystemData> departureSystems)
|
||||||
|
{
|
||||||
|
if (star.BlackHole is not { } hole) return;
|
||||||
|
var body = World(system, star.PositionMeters);
|
||||||
|
var equator = Perpendicular(hole.DiskNormal);
|
||||||
|
var radial = (equator * System.Math.Cos(.075) + hole.DiskNormal * System.Math.Sin(.075)).Normalized();
|
||||||
|
var up = RotateAround(hole.DiskNormal, -radial, .18);
|
||||||
|
Departure? departure = null;
|
||||||
|
var escape = departureSystems.SelectMany(s => s.Planets.Where(p => !p.IsGasPlanet).Select(p => (System: s, Planet: p)))
|
||||||
|
.MinBy(p => World(p.System, p.Planet.PositionMeters).RelativeTo(Position, CellSizeMeters).Length / p.Planet.RadiusMeters);
|
||||||
|
if (escape.Planet is { } solid)
|
||||||
|
{
|
||||||
|
var origin = World(escape.System, solid.PositionMeters);
|
||||||
|
var start = Position.RelativeTo(origin, CellSizeMeters);
|
||||||
|
if (start.Length < solid.MaximumTerrainRadiusMeters * 1.1)
|
||||||
|
departure = new(origin, start.Normalized(), body.RelativeTo(origin, CellSizeMeters).Normalized(),
|
||||||
|
start.Length, solid.MaximumTerrainRadiusMeters * 1.12);
|
||||||
|
}
|
||||||
|
BeginFlight(body, radial, star.RadiusMeters * 26, star.RadiusMeters * 70, -radial, up,
|
||||||
|
star.RadiusMeters * .08, duration: 9, fieldOfView: 32 * System.Math.PI / 180, departure: departure);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequestCosmicView(UniversePosition center,double radius,PlanetOrientation orientation,bool nebula,IEnumerable<StarSystemData> departureSystems)
|
||||||
|
{
|
||||||
|
var radial=orientation.ToWorld((nebula?new DVector3(.1,.05,1):new DVector3(.1,1,.3)).Normalized());
|
||||||
|
var up=orientation.ToWorld(nebula?new(0,1,0):new DVector3(0,0,-1));
|
||||||
|
Departure? departure=null;
|
||||||
|
var escape=departureSystems.SelectMany(s=>s.Planets.Where(p=>!p.IsGasPlanet).Select(p=>(System:s,Planet:p)))
|
||||||
|
.MinBy(p=>World(p.System,p.Planet.PositionMeters).RelativeTo(Position,CellSizeMeters).Length/p.Planet.RadiusMeters);
|
||||||
|
if(escape.Planet is {} solid)
|
||||||
|
{
|
||||||
|
var origin=World(escape.System,solid.PositionMeters);var start=Position.RelativeTo(origin,CellSizeMeters);
|
||||||
|
if(start.Length<solid.MaximumTerrainRadiusMeters*1.1)
|
||||||
|
departure=new(origin,start.Normalized(),center.RelativeTo(origin,CellSizeMeters).Normalized(),start.Length,solid.MaximumTerrainRadiusMeters*1.12);
|
||||||
|
}
|
||||||
|
BeginFlight(center,radial,radius*2.5,radius*3.5,-radial,up,radius*.2,duration:8,departure:departure);
|
||||||
|
}
|
||||||
|
|
||||||
private UniversePosition World(StarSystemData system, DVector3 offset) => UniversePosition.AtCellCenter(system.Coordinate, CellSizeMeters).Add(offset, CellSizeMeters);
|
private UniversePosition World(StarSystemData system, DVector3 offset) => UniversePosition.AtCellCenter(system.Coordinate, CellSizeMeters).Add(offset, CellSizeMeters);
|
||||||
|
|
||||||
private void BeginFlight(UniversePosition body, DVector3 radial, double finalRadius, double approachRadius, DVector3 forward, DVector3 up, double speed,
|
private void BeginFlight(UniversePosition body, DVector3 radial, double finalRadius, double approachRadius, DVector3 forward, DVector3 up, double speed,
|
||||||
@@ -291,6 +338,58 @@ public sealed class FlyCamera
|
|||||||
_flight = new(travelStart, body, startRadial, radial, approachRadius, finalRadius, Forward, Up, forward, up, speed,
|
_flight = new(travelStart, body, startRadial, radial, approachRadius, finalRadius, Forward, Up, forward, up, speed,
|
||||||
duration, FieldOfViewRadians, fieldOfView ?? _defaultFieldOfView, departure);
|
duration, FieldOfViewRadians, fieldOfView ?? _defaultFieldOfView, departure);
|
||||||
_flightElapsed = 0;
|
_flightElapsed = 0;
|
||||||
|
_flightSystem = null; _flightPlanet = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyCelestialMotion(UniverseNeighborhood previous, UniverseNeighborhood current, double secondsSinceJ2000)
|
||||||
|
{
|
||||||
|
var bodies = previous.NearbySystems.Append(previous.ActiveSystem)
|
||||||
|
.SelectMany(s => s.Planets.Select(p => (System: s, Planet: p, Body: World(s, p.PositionMeters)))).ToArray();
|
||||||
|
if (_flight is { } flight)
|
||||||
|
{
|
||||||
|
if (flight.Departure is { } departure)
|
||||||
|
{
|
||||||
|
var source = bodies.FirstOrDefault(b => b.Body.RelativeTo(departure.Body, CellSizeMeters).Length < 1);
|
||||||
|
if (source.Planet is { } sourcePlanet)
|
||||||
|
{
|
||||||
|
var movedSystem = CelestialSimulation.Evaluate(source.System, secondsSinceJ2000);
|
||||||
|
var movedPlanet = movedSystem.Planets.First(p => p.Id == sourcePlanet.Id);
|
||||||
|
DVector3 TurnDeparture(DVector3 v) => movedPlanet.Orientation.ToWorld(sourcePlanet.Orientation.ToBody(v));
|
||||||
|
var movedBody = World(movedSystem, movedPlanet.PositionMeters);
|
||||||
|
var movedDeparture = departure with { Body = movedBody, StartRadial = TurnDeparture(departure.StartRadial), TargetRadial = TurnDeparture(departure.TargetRadial) };
|
||||||
|
flight = flight with { Departure = movedDeparture,
|
||||||
|
Start = movedBody.Add(movedDeparture.TargetRadial * departure.ClearanceRadius, CellSizeMeters) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_flightPlanet is null)
|
||||||
|
{
|
||||||
|
var target = bodies.FirstOrDefault(b => b.Body.RelativeTo(flight.Body, CellSizeMeters).Length < 1);
|
||||||
|
_flightSystem = target.System; _flightPlanet = target.Planet;
|
||||||
|
}
|
||||||
|
if (_flightSystem is not null && _flightPlanet is { } old)
|
||||||
|
{
|
||||||
|
var updated = CelestialSimulation.Evaluate(_flightSystem, secondsSinceJ2000).Planets.First(p => p.Id == old.Id);
|
||||||
|
DVector3 Turn(DVector3 v) => updated.Orientation.ToWorld(old.Orientation.ToBody(v));
|
||||||
|
_flight = flight with { Body = World(_flightSystem, updated.PositionMeters), TargetRadial = Turn(flight.TargetRadial),
|
||||||
|
TargetForward = Turn(flight.TargetForward), TargetUp = Turn(flight.TargetUp) };
|
||||||
|
_flightPlanet = updated;
|
||||||
|
}
|
||||||
|
else _flight = flight;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var nearest = bodies.MinBy(b => b.Body.RelativeTo(Position, CellSizeMeters).Length / b.Planet.RadiusMeters);
|
||||||
|
if (nearest.Planet is null) return;
|
||||||
|
var offset = Position.RelativeTo(nearest.Body, CellSizeMeters);
|
||||||
|
if (offset.Length > nearest.Planet.RadiusMeters * 12) return;
|
||||||
|
var system = current.NearbySystems.Append(current.ActiveSystem).FirstOrDefault(s => s.Coordinate == nearest.System.Coordinate);
|
||||||
|
var planet = system?.Planets.FirstOrDefault(p => p.Id == nearest.Planet.Id);
|
||||||
|
if (planet is null) return;
|
||||||
|
if (offset.Length - TerrainHeight.Sample(nearest.Planet, offset.Normalized()) < 100)
|
||||||
|
{
|
||||||
|
DVector3 Turn(DVector3 v) => planet.Orientation.ToWorld(nearest.Planet.Orientation.ToBody(v));
|
||||||
|
offset = Turn(offset); _forward = Turn(_forward); _up = Turn(_up);
|
||||||
|
}
|
||||||
|
Position = World(system!, planet.PositionMeters).Add(offset, CellSizeMeters);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(double deltaSeconds, DVector3 movement, UniverseNeighborhood neighborhood)
|
public void Update(double deltaSeconds, DVector3 movement, UniverseNeighborhood neighborhood)
|
||||||
@@ -339,7 +438,8 @@ public sealed class FlyCamera
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (movement.LengthSquared < 1e-12) return;
|
if (movement.LengthSquared < 1e-12) return;
|
||||||
var distance = System.Math.Min(SpeedMetersPerSecond * System.Math.Min(deltaSeconds, 0.1), CellSizeMeters * 0.45);
|
var nearSurface = systems.Any(s => s.Planets.Any(p => Position.RelativeTo(World(s, p.PositionMeters), CellSizeMeters).Length < p.RadiusMeters * 10));
|
||||||
|
var distance = System.Math.Min(SpeedMetersPerSecond * System.Math.Min(deltaSeconds, 0.1), nearSurface ? CellSizeMeters * .45 : TravelLimitMeters);
|
||||||
var end = Position.Add(movement.Normalized() * distance, CellSizeMeters);
|
var end = Position.Add(movement.Normalized() * distance, CellSizeMeters);
|
||||||
Position = SweepNeighborhood(Position, end, systems).Position;
|
Position = SweepNeighborhood(Position, end, systems).Position;
|
||||||
}
|
}
|
||||||
|
|||||||
188
src/Universe.App/Runtime/GalaxyJourneyBenchmark.cs
Normal file
188
src/Universe.App/Runtime/GalaxyJourneyBenchmark.cs
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Universe.Core.Generation;
|
||||||
|
using Universe.Core.Math;
|
||||||
|
using Universe.Core.World;
|
||||||
|
|
||||||
|
namespace Universe.App.Runtime;
|
||||||
|
|
||||||
|
internal sealed class GalaxyJourneyBenchmark : IBenchmarkSession
|
||||||
|
{
|
||||||
|
private readonly GalaxyUniverse _galaxies;
|
||||||
|
private readonly UniversePosition _home, _homeBody, _milkyView, _otherView, _targetBody;
|
||||||
|
private readonly DVector3 _homeForward, _homeUp, _homeRadial, _targetRadial, _targetHeading;
|
||||||
|
private readonly double _homeRadius;
|
||||||
|
private readonly List<(double Time, double Milliseconds, string Phase)> _frames = [];
|
||||||
|
private readonly List<double> _graph = [];
|
||||||
|
private readonly HashSet<string> _visited = [];
|
||||||
|
private double _elapsed, _sampleTime, _totalFrameTime;
|
||||||
|
private double _lastSampleSequenceTime;
|
||||||
|
private int _sampleFrames;
|
||||||
|
private bool _stopped, _finalRecorded;
|
||||||
|
public GalaxyData HomeGalaxy { get; }
|
||||||
|
public GalaxyData TargetGalaxy { get; }
|
||||||
|
public StarSystemData TargetSystem { get; }
|
||||||
|
public PlanetData TargetPlanet { get; }
|
||||||
|
public string? ReportDirectory { get; private set; }
|
||||||
|
public object? RenderEnvironment { get; set; }
|
||||||
|
|
||||||
|
public GalaxyJourneyBenchmark(GalaxyUniverse galaxies, UniverseGenerator generator, StarSystemData homeSystem, PlanetData earth,
|
||||||
|
UniversePosition home, DVector3 homeForward, DVector3 homeUp, double simulationSeconds = 0)
|
||||||
|
{
|
||||||
|
_galaxies = galaxies; _home = home; _homeForward = homeForward; _homeUp = homeUp;
|
||||||
|
_homeBody = UniversePosition.AtCellCenter(homeSystem.Coordinate, galaxies.CellSize).Add(earth.PositionMeters, galaxies.CellSize);
|
||||||
|
_homeRadial = home.RelativeTo(_homeBody, galaxies.CellSize).Normalized();
|
||||||
|
_homeRadius = home.RelativeTo(_homeBody, galaxies.CellSize).Length;
|
||||||
|
HomeGalaxy = galaxies.GalaxyAt(home);
|
||||||
|
var region = HomeGalaxy.Region;
|
||||||
|
TargetGalaxy = galaxies.Get(new(checked(region.X + 1), region.Y, region.Z));
|
||||||
|
TargetSystem = Universe.Core.Physics.CelestialSimulation.Evaluate(generator.Generate(galaxies.SelectSystem(TargetGalaxy)), simulationSeconds);
|
||||||
|
TargetPlanet = TargetSystem.Planets.Where(p => !p.IsGasPlanet && p.ParentPlanetIndex is null).OrderByDescending(p => p.RadiusMeters).First();
|
||||||
|
_targetBody = UniversePosition.AtCellCenter(TargetSystem.Coordinate, galaxies.CellSize).Add(TargetPlanet.PositionMeters, galaxies.CellSize);
|
||||||
|
// Exercise the illuminated terrain/shadow path, rather than accidentally
|
||||||
|
// benchmarking the cheap night-side shading branch.
|
||||||
|
var sun=(TargetSystem.Stars[TargetPlanet.HostStarIndex].PositionMeters-TargetPlanet.PositionMeters).Normalized();
|
||||||
|
var side=DVector3.Cross(sun,System.Math.Abs(sun.Y)<.9?new(0,1,0):new DVector3(1,0,0)).Normalized();
|
||||||
|
_targetRadial=(sun*.6+side*.8).Normalized();
|
||||||
|
_targetHeading=DVector3.Cross(_targetRadial,sun).Normalized();
|
||||||
|
_milkyView = HomeGalaxy.Center.Add(HomeGalaxy.Orientation.ToWorld(new DVector3(.1,1,.3).Normalized()) * HomeGalaxy.RadiusMeters * 2.5, galaxies.CellSize);
|
||||||
|
_otherView = TargetGalaxy.Center.Add(TargetGalaxy.Orientation.ToWorld(new DVector3(.3,.8,1).Normalized()) * TargetGalaxy.RadiusMeters * 2.5, galaxies.CellSize);
|
||||||
|
}
|
||||||
|
public bool IsRunning => !_stopped && _elapsed < DurationSeconds;
|
||||||
|
public bool IsComplete => !_stopped && _elapsed >= DurationSeconds;
|
||||||
|
public bool HasRun => true;
|
||||||
|
public double ElapsedSeconds => _elapsed;
|
||||||
|
public double DurationSeconds => 94;
|
||||||
|
public long GraphRevision { get; private set; }
|
||||||
|
public IReadOnlyList<double> GraphSamples => _graph;
|
||||||
|
public string TargetLabel => TargetPlanet.Name.ToUpperInvariant();
|
||||||
|
public double TargetEarthRadii => TargetPlanet.RadiusMeters / UniverseScale.EarthRadiusMeters;
|
||||||
|
public double CurrentFps { get; private set; }
|
||||||
|
public double AverageFps => _totalFrameTime <= 0 ? 0 : _frames.Count / _totalFrameTime;
|
||||||
|
public double MinimumFps => _frames.Count == 0 ? 0 : 1000 / _frames.Max(f => f.Milliseconds);
|
||||||
|
public double OnePercentLowFps => Low(_frames.Select(f => f.Milliseconds).ToArray());
|
||||||
|
public string Phase => _stopped ? "STOPPED" : IsComplete ? "COMPLETE" : PhaseAt(_elapsed);
|
||||||
|
public IReadOnlyCollection<string> VisitedPhases => _visited;
|
||||||
|
private static string PhaseAt(double t) => t < 18 ? "LEAVE EARTH" : t < 25 ? "MILKY WAY" : t < 40 ? "BETWEEN GALAXIES"
|
||||||
|
: t < 55 ? "ENTER GALAXY" : t < 65 ? "TARGET SYSTEM" : t < 85 ? "LANDING" : "GROUND WALK";
|
||||||
|
public void Stop() => _stopped = true;
|
||||||
|
public void Update(double seconds, FlyCamera camera)
|
||||||
|
{
|
||||||
|
if (!IsRunning || !double.IsFinite(seconds) || seconds <= 0) return;
|
||||||
|
_elapsed = System.Math.Min(DurationSeconds, _elapsed + seconds);
|
||||||
|
_visited.Add(PhaseAt(System.Math.Min(_elapsed, DurationSeconds - .001)));
|
||||||
|
var cell = _galaxies.CellSize;
|
||||||
|
UniversePosition position; DVector3 forward; DVector3 up = _homeUp;
|
||||||
|
if (_elapsed < 18)
|
||||||
|
{
|
||||||
|
var t = Smooth(_elapsed / 18);
|
||||||
|
var viewRadial = _milkyView.RelativeTo(_homeBody,cell).Normalized();
|
||||||
|
var radius = LogLerp(_homeRadius, _milkyView.RelativeTo(_homeBody,cell).Length, t);
|
||||||
|
var direction = DVector3.Lerp(_homeRadial,viewRadial,Smooth(System.Math.Clamp((t-.6)/.4,0,1))).Normalized();
|
||||||
|
position = _homeBody.Add(direction * radius, cell);
|
||||||
|
var focus = System.Math.Clamp((System.Math.Log(System.Math.Max(radius,1)) - System.Math.Log(UniverseScale.LightYearMeters)) / 12, 0, 1);
|
||||||
|
forward = DVector3.Lerp(_homeForward, HomeGalaxy.Center.RelativeTo(position, cell).Normalized(), Smooth(focus)).Normalized();
|
||||||
|
}
|
||||||
|
else if (_elapsed < 25)
|
||||||
|
{
|
||||||
|
position = _milkyView;
|
||||||
|
forward = HomeGalaxy.Center.RelativeTo(position, cell).Normalized();
|
||||||
|
}
|
||||||
|
else if (_elapsed < 40)
|
||||||
|
{
|
||||||
|
var t = Smooth((_elapsed - 25) / 15);
|
||||||
|
position = Interpolate(_milkyView, _otherView, t, cell);
|
||||||
|
var from = HomeGalaxy.Center.RelativeTo(_milkyView, cell).Normalized();
|
||||||
|
var to = _targetBody.RelativeTo(_otherView, cell).Normalized();
|
||||||
|
forward = DVector3.Lerp(from, to, t).Normalized();
|
||||||
|
}
|
||||||
|
else if (_elapsed < 55)
|
||||||
|
{
|
||||||
|
var t = Smooth((_elapsed - 40) / 15);
|
||||||
|
var radial = _otherView.RelativeTo(_targetBody, cell).Normalized();
|
||||||
|
var radius = LogLerp(_otherView.RelativeTo(_targetBody, cell).Length, TargetPlanet.RadiusMeters * 4.8, t);
|
||||||
|
// Rotate at galaxy distances; keep the final solar approach radial and precise.
|
||||||
|
var direction = DVector3.Lerp(radial, _targetRadial, Smooth(System.Math.Min(t * 3, 1))).Normalized();
|
||||||
|
position = _targetBody.Add(direction * radius, cell);
|
||||||
|
forward = _targetBody.RelativeTo(position, cell).Normalized();
|
||||||
|
up = new(0,1,0);
|
||||||
|
}
|
||||||
|
else if (_elapsed < 65)
|
||||||
|
{
|
||||||
|
position = _targetBody.Add(_targetRadial * TargetPlanet.RadiusMeters * 4.8, cell);
|
||||||
|
forward = -_targetRadial; up = new(0,1,0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var t = System.Math.Clamp((_elapsed - 65) / 20, 0, 1);
|
||||||
|
var surface = TerrainHeight.Sample(TargetPlanet, _targetRadial);
|
||||||
|
var radius = t < .5 ? TargetPlanet.RadiusMeters * 4.8 + (surface + 10001.8 - TargetPlanet.RadiusMeters * 4.8) * Smooth(t * 2)
|
||||||
|
: surface + 1.8 + 10000 * (1 - Smooth((t - .5) * 2));
|
||||||
|
var radial = _targetRadial;
|
||||||
|
if (_elapsed >= 85)
|
||||||
|
{
|
||||||
|
var walk = (_elapsed - 85) / 9;
|
||||||
|
radial = (_targetRadial + _targetHeading * (walk * 12 / TargetPlanet.RadiusMeters)).Normalized();
|
||||||
|
radius = TerrainHeight.Sample(TargetPlanet, radial) + 1.8;
|
||||||
|
}
|
||||||
|
position = _targetBody.Add(radial * radius, cell);
|
||||||
|
var landingLook = (_targetHeading * System.Math.Cos(.2) - radial * System.Math.Sin(.2)).Normalized();
|
||||||
|
forward = DVector3.Lerp(-radial, landingLook, Smooth(System.Math.Clamp((t - .4) / .6,0,1))).Normalized();
|
||||||
|
if (_elapsed >= 85)
|
||||||
|
{
|
||||||
|
var yaw = System.Math.Sin((_elapsed - 85) * .7) * .6;
|
||||||
|
var right = DVector3.Cross(_targetHeading, radial).Normalized();
|
||||||
|
forward = (_targetHeading * System.Math.Cos(yaw) + right * System.Math.Sin(yaw) - radial * .15).Normalized();
|
||||||
|
}
|
||||||
|
up = radial;
|
||||||
|
}
|
||||||
|
if (_elapsed >= DurationSeconds) { position = _home; forward = _homeForward; up = _homeUp; }
|
||||||
|
up = (up - forward * DVector3.Dot(up,forward)).Normalized();
|
||||||
|
if (up.LengthSquared < .1) up = DVector3.Cross(forward,new(1,0,0)).Normalized();
|
||||||
|
camera.SetPlayerPose(position, forward, up, resetMotionHistory: IsComplete);
|
||||||
|
}
|
||||||
|
public void RecordFrame(double seconds)
|
||||||
|
{
|
||||||
|
if (_stopped || _finalRecorded || !double.IsFinite(seconds) || seconds <= 0) return;
|
||||||
|
CurrentFps = 1 / seconds; _totalFrameTime += seconds;
|
||||||
|
_frames.Add((_elapsed, seconds * 1000, PhaseAt(System.Math.Min(_elapsed,DurationSeconds - .001))));
|
||||||
|
_sampleTime += seconds; _sampleFrames++;
|
||||||
|
if (_elapsed - _lastSampleSequenceTime >= .2 || IsComplete)
|
||||||
|
{
|
||||||
|
_graph.Add(_sampleFrames / _sampleTime); _sampleTime = 0; _sampleFrames = 0; _lastSampleSequenceTime = _elapsed; GraphRevision++;
|
||||||
|
}
|
||||||
|
if (IsComplete) _finalRecorded = true;
|
||||||
|
}
|
||||||
|
public object[] PhaseResults() => _frames.GroupBy(f => f.Phase).Select(group =>
|
||||||
|
{
|
||||||
|
var values = group.Select(f => f.Milliseconds).Order().ToArray();
|
||||||
|
var p99 = values[System.Math.Min(values.Length - 1,(int)System.Math.Ceiling(values.Length * .99) - 1)];
|
||||||
|
return (object)new { Phase = group.Key, Frames = values.Length, AverageFps = 1000 / values.Average(), LowOnePercentFps = Low(values),
|
||||||
|
P99Milliseconds = p99, MaximumMilliseconds = values.Max(),
|
||||||
|
Passed = Low(values) >= _galaxies.Settings.MinimumBenchmarkFps && values.Max() <= _galaxies.Settings.MaximumBenchmarkFrameMilliseconds };
|
||||||
|
}).ToArray();
|
||||||
|
public bool PerformancePassed => IsComplete && _frames.Count > 0 && _frames.GroupBy(f => f.Phase).All(g =>
|
||||||
|
Low(g.Select(f => f.Milliseconds).ToArray()) >= _galaxies.Settings.MinimumBenchmarkFps
|
||||||
|
&& g.Max(f => f.Milliseconds) <= _galaxies.Settings.MaximumBenchmarkFrameMilliseconds);
|
||||||
|
public string SaveReport(string? root = null)
|
||||||
|
{
|
||||||
|
if (ReportDirectory is not null) return ReportDirectory;
|
||||||
|
var directory = Path.Combine(root ?? Path.Combine(AppContext.BaseDirectory,"benchmark-results"),"galaxy-journey-"+DateTime.UtcNow.ToString("yyyyMMdd-HHmmss"));
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
var csv = new StringBuilder("timeSeconds,phase,frameMilliseconds,fps\n");
|
||||||
|
foreach (var f in _frames) csv.AppendLine(FormattableString.Invariant($"{f.Time:F4},{f.Phase},{f.Milliseconds:F4},{1000/f.Milliseconds:F4}"));
|
||||||
|
File.WriteAllText(Path.Combine(directory,"frames.csv"),csv.ToString());
|
||||||
|
File.WriteAllText(Path.Combine(directory,"summary.json"),JsonSerializer.Serialize(new { Completed = IsComplete, PerformancePassed,
|
||||||
|
UniverseSeed = _galaxies.Seed.ToString("X16"), RenderEnvironment,
|
||||||
|
Quality = new { _galaxies.Settings.GalaxyStarBudget, _galaxies.Settings.LocalStarBudget, _galaxies.Settings.VolumeSteps, _galaxies.Settings.SystemSpacingLightYears },
|
||||||
|
TargetGalaxy = TargetGalaxy.Id, TargetSystem = TargetSystem.Id, TargetPlanet = TargetPlanet.Id,
|
||||||
|
MinimumFps = _galaxies.Settings.MinimumBenchmarkFps, MaximumFrameMilliseconds = _galaxies.Settings.MaximumBenchmarkFrameMilliseconds,
|
||||||
|
Visited = _visited, Phases = PhaseResults() },new JsonSerializerOptions { WriteIndented = true }));
|
||||||
|
ReportDirectory = directory; return directory;
|
||||||
|
}
|
||||||
|
private static double Low(double[] milliseconds) => milliseconds.Length == 0 ? 0 : 1000 / milliseconds.OrderDescending().Take(System.Math.Max(1,(int)System.Math.Ceiling(milliseconds.Length * .01))).Average();
|
||||||
|
private static double Smooth(double t) { t=System.Math.Clamp(t,0,1);return t*t*t*(t*(t*6-15)+10); }
|
||||||
|
private static double LogLerp(double a,double b,double t) => System.Math.Exp(System.Math.Log(a)*(1-t)+System.Math.Log(b)*t);
|
||||||
|
private static UniversePosition Interpolate(UniversePosition a,UniversePosition b,double t,double cell) => t<.5?a.Add(b.RelativeTo(a,cell)*t,cell):b.Add(a.RelativeTo(b,cell)*(1-t),cell);
|
||||||
|
}
|
||||||
24
src/Universe.App/Runtime/IBenchmarkSession.cs
Normal file
24
src/Universe.App/Runtime/IBenchmarkSession.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using Universe.Core.Generation;
|
||||||
|
|
||||||
|
namespace Universe.App.Runtime;
|
||||||
|
|
||||||
|
internal interface IBenchmarkSession
|
||||||
|
{
|
||||||
|
bool IsRunning { get; }
|
||||||
|
bool IsComplete { get; }
|
||||||
|
bool HasRun { get; }
|
||||||
|
double ElapsedSeconds { get; }
|
||||||
|
double DurationSeconds { get; }
|
||||||
|
long GraphRevision { get; }
|
||||||
|
IReadOnlyList<double> GraphSamples { get; }
|
||||||
|
string TargetLabel { get; }
|
||||||
|
double TargetEarthRadii { get; }
|
||||||
|
double CurrentFps { get; }
|
||||||
|
double AverageFps { get; }
|
||||||
|
double MinimumFps { get; }
|
||||||
|
double OnePercentLowFps { get; }
|
||||||
|
string Phase { get; }
|
||||||
|
void Stop();
|
||||||
|
void Update(double seconds, FlyCamera camera);
|
||||||
|
void RecordFrame(double seconds);
|
||||||
|
}
|
||||||
@@ -39,6 +39,24 @@ public sealed class PlayerController
|
|||||||
_pitch = System.Math.Clamp(_pitch - dy * .0022, -1.5, 1.5);
|
_pitch = System.Math.Clamp(_pitch - dy * .0022, -1.5, 1.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ApplyCelestialMotion(UniverseNeighborhood previous, UniverseNeighborhood current)
|
||||||
|
{
|
||||||
|
if (Nearest(previous) is not { } old) return;
|
||||||
|
var system = current.NearbySystems.Append(current.ActiveSystem).FirstOrDefault(s => s.Coordinate == old.System.Coordinate);
|
||||||
|
var planet = system?.Planets.FirstOrDefault(p => p.Id == old.Planet.Id);
|
||||||
|
if (planet is null) return;
|
||||||
|
var body = UniversePosition.AtCellCenter(system!.Coordinate, _cellSize).Add(planet.PositionMeters, _cellSize);
|
||||||
|
var relative = Position.RelativeTo(old.Body, _cellSize);
|
||||||
|
// Use the rotating local frame near the surface, including jumps: the player inherits the ground's
|
||||||
|
// rotational motion instead of the planet sliding hundreds of metres sideways during a jump.
|
||||||
|
if (Grounded || relative.Length - TerrainHeight.Sample(old.Planet, relative.Normalized()) <= System.Math.Max(2000, old.Planet.AtmosphereHeightMeters))
|
||||||
|
{
|
||||||
|
DVector3 Rotate(DVector3 v) => planet.Orientation.ToWorld(old.Planet.Orientation.ToBody(v));
|
||||||
|
relative = Rotate(relative); _heading = Rotate(_heading); Up = Rotate(Up); _velocity = Rotate(_velocity);
|
||||||
|
}
|
||||||
|
Position = body.Add(relative, _cellSize);
|
||||||
|
}
|
||||||
|
|
||||||
public void Update(double dt, double forward, double side, bool jump, bool crouch, UniverseNeighborhood neighborhood)
|
public void Update(double dt, double forward, double side, bool jump, bool crouch, UniverseNeighborhood neighborhood)
|
||||||
{
|
{
|
||||||
if (!double.IsFinite(dt) || dt <= 0) return;
|
if (!double.IsFinite(dt) || dt <= 0) return;
|
||||||
|
|||||||
29
src/Universe.App/Runtime/StarMotionHistory.cs
Normal file
29
src/Universe.App/Runtime/StarMotionHistory.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using Universe.Core.Generation;
|
||||||
|
using Universe.Core.Math;
|
||||||
|
using Universe.Core.World;
|
||||||
|
|
||||||
|
namespace Universe.App.Runtime;
|
||||||
|
|
||||||
|
internal readonly record struct StarCameraPose(UniversePosition Position, DVector3 Forward, DVector3 Right, DVector3 Up,
|
||||||
|
double TanHalfFov, int Width, int Height, long CutVersion);
|
||||||
|
|
||||||
|
internal sealed class StarMotionHistory
|
||||||
|
{
|
||||||
|
private bool _initialized;
|
||||||
|
public StarCameraPose Current { get; private set; }
|
||||||
|
public StarCameraPose Previous { get; private set; }
|
||||||
|
public bool Valid { get; private set; }
|
||||||
|
|
||||||
|
public void Advance(FlyCamera camera, int width, int height, double frameSeconds, StarMotionBlurSettings settings)
|
||||||
|
{
|
||||||
|
var pose = new StarCameraPose(camera.Position, camera.Forward, camera.Right, camera.Up,
|
||||||
|
System.Math.Tan(camera.FieldOfViewRadians * .5), width, height, camera.MotionHistoryVersion);
|
||||||
|
Previous = Current;
|
||||||
|
Valid = _initialized && settings.Enabled && double.IsFinite(frameSeconds) && frameSeconds > 0
|
||||||
|
&& frameSeconds <= settings.HistoryResetSeconds && Previous.Width == width && Previous.Height == height
|
||||||
|
&& Previous.CutVersion == pose.CutVersion && System.Math.Abs(Previous.TanHalfFov / pose.TanHalfFov - 1) < .15;
|
||||||
|
Current = pose;
|
||||||
|
if (!Valid) Previous = Current;
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
<ProjectReference Include="..\Universe.Core\Universe.Core.csproj" />
|
<ProjectReference Include="..\Universe.Core\Universe.Core.csproj" />
|
||||||
<EmbeddedResource Include="Graphics\Shaders\*.glsl" />
|
<EmbeddedResource Include="Graphics\Shaders\*.glsl" />
|
||||||
<Content Include="..\..\config.json" Link="config.json" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
<Content Include="..\..\config.json" Link="config.json" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||||
|
<Content Include="..\..\KnownGalaxies.json" Link="KnownGalaxies.json" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||||
<Content Include="..\..\KnownSolarSystem\**\*" Link="KnownSolarSystem\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
<Content Include="..\..\KnownSolarSystem\**\*" Link="KnownSolarSystem\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||||
<Content Include="..\..\Content\**\*.json;..\..\Content\**\*.jpg;..\..\Content\**\*.png;..\..\Content\**\*.f32" Link="Content\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
<Content Include="..\..\Content\**\*.json;..\..\Content\**\*.jpg;..\..\Content\**\*.png;..\..\Content\**\*.f32" Link="Content\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using Universe.Core.Generation;
|
|||||||
using Universe.Core.Math;
|
using Universe.Core.Math;
|
||||||
using Universe.Core.World;
|
using Universe.Core.World;
|
||||||
using Universe.Core.Configuration;
|
using Universe.Core.Configuration;
|
||||||
|
using Universe.Core.Physics;
|
||||||
|
|
||||||
namespace Universe.App;
|
namespace Universe.App;
|
||||||
|
|
||||||
@@ -18,6 +19,9 @@ public sealed class UniverseApplication : IDisposable
|
|||||||
private readonly IWindow _window;
|
private readonly IWindow _window;
|
||||||
private readonly UniverseGenerator _generator;
|
private readonly UniverseGenerator _generator;
|
||||||
private readonly UniverseStreamer _streamer;
|
private readonly UniverseStreamer _streamer;
|
||||||
|
private readonly GalaxyUniverse _galaxies;
|
||||||
|
private readonly StarMotionBlurSettings _starMotionBlur;
|
||||||
|
private readonly SimulationClock _clock;
|
||||||
private readonly IReadOnlyList<string> _planetHotkeys;
|
private readonly IReadOnlyList<string> _planetHotkeys;
|
||||||
private readonly StarSystemData _homeSystem;
|
private readonly StarSystemData _homeSystem;
|
||||||
private readonly PlanetData _homeEarth;
|
private readonly PlanetData _homeEarth;
|
||||||
@@ -26,7 +30,8 @@ public sealed class UniverseApplication : IDisposable
|
|||||||
private readonly DVector3 _homeCameraUp;
|
private readonly DVector3 _homeCameraUp;
|
||||||
private FlyCamera _camera;
|
private FlyCamera _camera;
|
||||||
private PlayerController? _player;
|
private PlayerController? _player;
|
||||||
private BenchmarkRunner? _benchmark;
|
private IBenchmarkSession? _benchmark;
|
||||||
|
private bool _benchmarkNeedsInitialFrame;
|
||||||
private bool _menuOpen;
|
private bool _menuOpen;
|
||||||
private bool _debugMode;
|
private bool _debugMode;
|
||||||
private int _menuSelection;
|
private int _menuSelection;
|
||||||
@@ -49,19 +54,25 @@ public sealed class UniverseApplication : IDisposable
|
|||||||
{
|
{
|
||||||
var configPath = Path.Combine(AppContext.BaseDirectory, "config.json");
|
var configPath = Path.Combine(AppContext.BaseDirectory, "config.json");
|
||||||
var configuration = EngineConfiguration.Load(configPath);
|
var configuration = EngineConfiguration.Load(configPath);
|
||||||
|
_clock = new(configuration.Simulation);
|
||||||
|
_starMotionBlur = configuration.StarMotionBlur;
|
||||||
_planetHotkeys = configuration.Startup.PlanetHotkeys;
|
_planetHotkeys = configuration.Startup.PlanetHotkeys;
|
||||||
var catalogPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(configPath)!, configuration.KnownSolarSystemsFile));
|
var catalogPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(configPath)!, configuration.KnownSolarSystemsFile));
|
||||||
var catalog = KnownSolarSystemCatalog.Load(catalogPath, configuration.CellSizeMeters);
|
var catalog = KnownSolarSystemCatalog.Load(catalogPath, configuration.CellSizeMeters);
|
||||||
_generator = new(configuration.Seed, configuration.CellSizeMeters, catalog);
|
_galaxies = new(configuration.Seed, configuration.CellSizeMeters, configuration.Galaxies,
|
||||||
|
Path.GetFullPath(Path.Combine(Path.GetDirectoryName(configPath)!, configuration.Galaxies.KnownGalaxiesFile)));
|
||||||
|
_generator = new(configuration.Seed, configuration.CellSizeMeters, catalog, configuration.BlackHoles, _galaxies);
|
||||||
_streamer = new(_generator);
|
_streamer = new(_generator);
|
||||||
|
_streamer.SimulationSeconds = _clock.SecondsSinceJ2000;
|
||||||
var initialSystem = configuration.ResolveStartup(catalog).System;
|
var initialSystem = configuration.ResolveStartup(catalog).System;
|
||||||
_homeSystem = catalog.GetById("sol");
|
_homeSystem = _streamer.AtCurrentTime(_generator.Generate(catalog.GetById("sol").Coordinate));
|
||||||
_homeEarth = _homeSystem.Planets.FirstOrDefault(p => p.Id.Equals("earth", StringComparison.OrdinalIgnoreCase))
|
_homeEarth = _homeSystem.Planets.FirstOrDefault(p => p.Id.Equals("earth", StringComparison.OrdinalIgnoreCase))
|
||||||
?? throw new InvalidDataException("The known solar system must define an Earth planet with id 'earth'.");
|
?? throw new InvalidDataException("The known solar system must define an Earth planet with id 'earth'.");
|
||||||
var startCoordinate = initialSystem.Coordinate;
|
var startCoordinate = initialSystem.Coordinate;
|
||||||
var cellCenter = UniversePosition.AtCellCenter(startCoordinate, _generator.CellSizeMeters);
|
var cellCenter = UniversePosition.AtCellCenter(startCoordinate, _generator.CellSizeMeters);
|
||||||
var startPosition = cellCenter.Add(new DVector3(0, 0, 4 * UniverseScale.AstronomicalUnitMeters), _generator.CellSizeMeters);
|
var startPosition = cellCenter.Add(new DVector3(0, 0, 4 * UniverseScale.AstronomicalUnitMeters), _generator.CellSizeMeters);
|
||||||
_camera = new(startPosition, _generator.CellSizeMeters);
|
_camera = new(startPosition, _generator.CellSizeMeters);
|
||||||
|
_camera.TravelLimitMeters = _galaxies.Settings.RegionSizeLightYears * UniverseScale.LightYearMeters * .25;
|
||||||
_neighborhood = _streamer.Load(startCoordinate);
|
_neighborhood = _streamer.Load(startCoordinate);
|
||||||
_camera.SetInitialView(_neighborhood.ActiveSystem, configuration.Startup);
|
_camera.SetInitialView(_neighborhood.ActiveSystem, configuration.Startup);
|
||||||
_homeCameraPosition = _camera.Position;
|
_homeCameraPosition = _camera.Position;
|
||||||
@@ -72,7 +83,7 @@ public sealed class UniverseApplication : IDisposable
|
|||||||
var options = WindowOptions.Default with
|
var options = WindowOptions.Default with
|
||||||
{
|
{
|
||||||
Title = "Universe | loading",
|
Title = "Universe | loading",
|
||||||
IsVisible = !Environment.GetCommandLineArgs().Any(a => a is "--graphics-report" or "--scene-report" or "--ocean-report" or "--cloud-report" or "--terrain-report" or "--menu-report" or "--terrain-perf-report"),
|
IsVisible = !Environment.GetCommandLineArgs().Any(a => a is "--simulation-report" or "--graphics-report" or "--scene-report" or "--ocean-report" or "--cloud-report" or "--terrain-report" or "--menu-report" or "--terrain-perf-report" or "--black-hole-report" or "--galaxy-journey-report" or "--star-trails-report"),
|
||||||
Size = new Vector2D<int>(1440, 900),
|
Size = new Vector2D<int>(1440, 900),
|
||||||
VSync = true,
|
VSync = true,
|
||||||
PreferredDepthBufferBits = 24,
|
PreferredDepthBufferBits = 24,
|
||||||
@@ -92,7 +103,31 @@ public sealed class UniverseApplication : IDisposable
|
|||||||
{
|
{
|
||||||
var gl = _window.CreateOpenGL();
|
var gl = _window.CreateOpenGL();
|
||||||
_input = _window.CreateInput();
|
_input = _window.CreateInput();
|
||||||
_renderer = new(gl, _window.FramebufferSize.X, _window.FramebufferSize.Y);
|
_renderer = new(gl, _window.FramebufferSize.X, _window.FramebufferSize.Y, _galaxies, _starMotionBlur);
|
||||||
|
if (Environment.GetCommandLineArgs().Contains("--simulation-report"))
|
||||||
|
{
|
||||||
|
GraphicsReport.RunSimulation(gl, _renderer, _neighborhood, _generator.CellSizeMeters, _window.FramebufferSize.X, _window.FramebufferSize.Y);
|
||||||
|
_window.Close(); return;
|
||||||
|
}
|
||||||
|
if (Environment.GetCommandLineArgs().Contains("--star-trails-report"))
|
||||||
|
{
|
||||||
|
GraphicsReport.RunStarTrails(gl,_starMotionBlur,_window.FramebufferSize.X,_window.FramebufferSize.Y);
|
||||||
|
_window.Close(); return;
|
||||||
|
}
|
||||||
|
if (Environment.GetCommandLineArgs().Contains("--galaxy-journey-report"))
|
||||||
|
{
|
||||||
|
var journey = CreateGalaxyJourney();
|
||||||
|
GraphicsReport.RunGalaxyJourney(gl, _renderer, _camera, _streamer, journey, _window.FramebufferSize.X, _window.FramebufferSize.Y);
|
||||||
|
_window.Close(); return;
|
||||||
|
}
|
||||||
|
if (Environment.GetCommandLineArgs().Contains("--black-hole-report"))
|
||||||
|
{
|
||||||
|
var target = _streamer.FindNearestBlackHole(_camera.Position)
|
||||||
|
?? throw new InvalidOperationException("No black hole found; check blackHoles.occurrenceChance.");
|
||||||
|
GraphicsReport.RunBlackHole(gl, _renderer, _streamer.Load(target.System.Coordinate), target.Hole,
|
||||||
|
_generator.CellSizeMeters, _window.FramebufferSize.X, _window.FramebufferSize.Y);
|
||||||
|
_window.Close(); return;
|
||||||
|
}
|
||||||
if (Environment.GetCommandLineArgs().Contains("--terrain-perf-report"))
|
if (Environment.GetCommandLineArgs().Contains("--terrain-perf-report"))
|
||||||
{ GraphicsReport.RunTerrainPerformance(gl, _renderer, _neighborhood, _generator.CellSizeMeters, _window.FramebufferSize.X, _window.FramebufferSize.Y); _window.Close(); return; }
|
{ GraphicsReport.RunTerrainPerformance(gl, _renderer, _neighborhood, _generator.CellSizeMeters, _window.FramebufferSize.X, _window.FramebufferSize.Y); _window.Close(); return; }
|
||||||
if (Environment.GetCommandLineArgs().Contains("--menu-report"))
|
if (Environment.GetCommandLineArgs().Contains("--menu-report"))
|
||||||
@@ -130,6 +165,7 @@ public sealed class UniverseApplication : IDisposable
|
|||||||
|
|
||||||
if (_benchmark?.IsRunning == true)
|
if (_benchmark?.IsRunning == true)
|
||||||
{
|
{
|
||||||
|
if (_benchmarkNeedsInitialFrame) return;
|
||||||
_benchmark.Update(deltaSeconds, _camera);
|
_benchmark.Update(deltaSeconds, _camera);
|
||||||
UpdateNeighborhood();
|
UpdateNeighborhood();
|
||||||
return;
|
return;
|
||||||
@@ -137,6 +173,16 @@ public sealed class UniverseApplication : IDisposable
|
|||||||
|
|
||||||
if (_menuOpen) { _mouseDelta = Vector2.Zero; return; }
|
if (_menuOpen) { _mouseDelta = Vector2.Zero; return; }
|
||||||
|
|
||||||
|
var previous = _neighborhood;
|
||||||
|
_clock.Advance(deltaSeconds);
|
||||||
|
_streamer.SimulationSeconds = _clock.SecondsSinceJ2000;
|
||||||
|
if (_clock.TimeScale > 0)
|
||||||
|
{
|
||||||
|
_neighborhood = _streamer.Refresh(previous);
|
||||||
|
if (_player is { } movingPlayer) movingPlayer.ApplyCelestialMotion(previous, _neighborhood);
|
||||||
|
else _camera.ApplyCelestialMotion(previous, _neighborhood, _clock.SecondsSinceJ2000);
|
||||||
|
}
|
||||||
|
|
||||||
if (_mouseCaptured)
|
if (_mouseCaptured)
|
||||||
{
|
{
|
||||||
if (_player is null) _camera.Rotate(_mouseDelta.X, _mouseDelta.Y);
|
if (_player is null) _camera.Rotate(_mouseDelta.X, _mouseDelta.Y);
|
||||||
@@ -182,16 +228,21 @@ public sealed class UniverseApplication : IDisposable
|
|||||||
private void OnRender(double deltaSeconds)
|
private void OnRender(double deltaSeconds)
|
||||||
{
|
{
|
||||||
_frameTime += (System.Math.Clamp(deltaSeconds, 0.001, 1) - _frameTime) * 0.08;
|
_frameTime += (System.Math.Clamp(deltaSeconds, 0.001, 1) - _frameTime) * 0.08;
|
||||||
_benchmark?.RecordFrame(deltaSeconds);
|
// The first interval still includes menu preparation and GPU preloading.
|
||||||
|
// Render one initial frame before starting the measurement clock.
|
||||||
|
if (!_benchmarkNeedsInitialFrame) _benchmark?.RecordFrame(deltaSeconds);
|
||||||
|
if (_benchmark is GalaxyJourneyBenchmark { IsComplete: true, ReportDirectory: null } journey) journey.SaveReport();
|
||||||
if (_renderer is not null)
|
if (_renderer is not null)
|
||||||
{
|
{
|
||||||
_renderer.DebugMode = _debugMode;
|
_renderer.DebugMode = _debugMode;
|
||||||
|
_renderer.SimulationTimeSeconds = _clock.ElapsedSeconds;
|
||||||
_renderer.DebugProbeEnabled = _debugMode && !_menuOpen;
|
_renderer.DebugProbeEnabled = _debugMode && !_menuOpen;
|
||||||
}
|
}
|
||||||
_renderer?.Render(_camera, _neighborhood, _menuOpen ? 0 : deltaSeconds);
|
_renderer?.Render(_camera, _neighborhood, _menuOpen ? 0 : deltaSeconds);
|
||||||
_renderer?.DrawDebugOverlay(1 / _frameTime);
|
_renderer?.DrawDebugOverlay(1 / _frameTime, _clock.TimeScale, _clock.Utc);
|
||||||
if (_benchmark is { HasRun: true } benchmark) _renderer?.DrawBenchmarkOverlay(benchmark);
|
if (_benchmark is { HasRun: true } benchmark) _renderer?.DrawBenchmarkOverlay(benchmark);
|
||||||
if (_menuOpen) _renderer?.DrawMenu(_menuSelection, _debugMode);
|
if (_menuOpen) _renderer?.DrawMenu(_menuSelection, _debugMode, _clock.TimeScale, _clock.Utc);
|
||||||
|
_benchmarkNeedsInitialFrame = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateNeighborhood()
|
private void UpdateNeighborhood()
|
||||||
@@ -221,8 +272,10 @@ public sealed class UniverseApplication : IDisposable
|
|||||||
if (_menuOpen)
|
if (_menuOpen)
|
||||||
{
|
{
|
||||||
if (key == Key.Escape) SetMenu(false);
|
if (key == Key.Escape) SetMenu(false);
|
||||||
else if (key == Key.Up) _menuSelection = (_menuSelection + 5) % 6;
|
else if (_debugMode && key is Key.Left or Key.Right)
|
||||||
else if (key == Key.Down) _menuSelection = (_menuSelection + 1) % 6;
|
_clock.TimeScale += (key == Key.Right ? 1 : -1) * (IsDown(Key.ShiftLeft) || IsDown(Key.ShiftRight) ? 10 : 1);
|
||||||
|
else if (key == Key.Up) _menuSelection = (_menuSelection + 6) % 7;
|
||||||
|
else if (key == Key.Down) _menuSelection = (_menuSelection + 1) % 7;
|
||||||
else if (key is Key.Enter or Key.KeypadEnter) ActivateMenu();
|
else if (key is Key.Enter or Key.KeypadEnter) ActivateMenu();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -235,6 +288,16 @@ public sealed class UniverseApplication : IDisposable
|
|||||||
if (_benchmark?.IsRunning == true && key != Key.F1) return;
|
if (_benchmark?.IsRunning == true && key != Key.F1) return;
|
||||||
if (key == Key.Escape)
|
if (key == Key.Escape)
|
||||||
_window.Close();
|
_window.Close();
|
||||||
|
else if (key == Key.E)
|
||||||
|
{
|
||||||
|
var target = _streamer.FindNearestBlackHole(_camera.Position);
|
||||||
|
if (target is { } hole)
|
||||||
|
{
|
||||||
|
_player = null;
|
||||||
|
_camera.RequestBlackHoleView(hole.System, hole.Hole, _neighborhood.NearbySystems.Append(_neighborhood.ActiveSystem));
|
||||||
|
}
|
||||||
|
else _window.Title = "Universe | No black hole found in the search area";
|
||||||
|
}
|
||||||
else if (_player is not null && key != Key.F1) return;
|
else if (_player is not null && key != Key.F1) return;
|
||||||
else if (key == Key.Q)
|
else if (key == Key.Q)
|
||||||
_camera.RequestFocus(_neighborhood.NearbySystems.Append(_neighborhood.ActiveSystem));
|
_camera.RequestFocus(_neighborhood.NearbySystems.Append(_neighborhood.ActiveSystem));
|
||||||
@@ -255,6 +318,16 @@ public sealed class UniverseApplication : IDisposable
|
|||||||
_camera.RequestOceanView(_neighborhood.NearbySystems.Append(_neighborhood.ActiveSystem));
|
_camera.RequestOceanView(_neighborhood.NearbySystems.Append(_neighborhood.ActiveSystem));
|
||||||
else if (key == Key.F7)
|
else if (key == Key.F7)
|
||||||
_camera.RequestCloudView(_neighborhood.NearbySystems.Append(_neighborhood.ActiveSystem));
|
_camera.RequestCloudView(_neighborhood.NearbySystems.Append(_neighborhood.ActiveSystem));
|
||||||
|
else if(key==Key.F8)
|
||||||
|
{
|
||||||
|
var galaxy=_galaxies.GalaxyAt(_camera.Position);
|
||||||
|
_camera.RequestCosmicView(galaxy.Center,galaxy.RadiusMeters,galaxy.Orientation,false,_neighborhood.NearbySystems.Append(_neighborhood.ActiveSystem));
|
||||||
|
}
|
||||||
|
else if(key==Key.F9)
|
||||||
|
{
|
||||||
|
var nebula=_galaxies.GalaxyAt(_camera.Position).Nebulae.MinBy(n=>n.Center.RelativeTo(_camera.Position,_generator.CellSizeMeters).LengthSquared)!;
|
||||||
|
_camera.RequestCosmicView(nebula.Center,nebula.RadiusMeters,nebula.Orientation,true,_neighborhood.NearbySystems.Append(_neighborhood.ActiveSystem));
|
||||||
|
}
|
||||||
else if (key == Key.F1 && _mouse is not null)
|
else if (key == Key.F1 && _mouse is not null)
|
||||||
{
|
{
|
||||||
_mouseCaptured = !_mouseCaptured;
|
_mouseCaptured = !_mouseCaptured;
|
||||||
@@ -286,6 +359,9 @@ public sealed class UniverseApplication : IDisposable
|
|||||||
{
|
{
|
||||||
if (_menuOpen)
|
if (_menuOpen)
|
||||||
{
|
{
|
||||||
|
if (_debugMode && mouse.IsButtonPressed(MouseButton.Left)
|
||||||
|
&& _renderer?.TimeScaleHit(position.X, position.Y, _window.Size.X, _window.Size.Y) is { } rate)
|
||||||
|
_clock.TimeScale = rate;
|
||||||
var hit = _renderer?.MenuHit(position.X, position.Y, _window.Size.X, _window.Size.Y) ?? -1;
|
var hit = _renderer?.MenuHit(position.X, position.Y, _window.Size.X, _window.Size.Y) ?? -1;
|
||||||
if (hit >= 0) _menuSelection = hit;
|
if (hit >= 0) _menuSelection = hit;
|
||||||
return;
|
return;
|
||||||
@@ -312,6 +388,8 @@ public sealed class UniverseApplication : IDisposable
|
|||||||
{
|
{
|
||||||
if (!_menuOpen || button != MouseButton.Left) return;
|
if (!_menuOpen || button != MouseButton.Left) return;
|
||||||
var position = mouse.Position;
|
var position = mouse.Position;
|
||||||
|
if (_debugMode && _renderer?.TimeScaleHit(position.X, position.Y, _window.Size.X, _window.Size.Y) is { } rate)
|
||||||
|
{ _clock.TimeScale = rate; return; }
|
||||||
var hit = _renderer?.MenuHit(position.X, position.Y, _window.Size.X, _window.Size.Y) ?? -1;
|
var hit = _renderer?.MenuHit(position.X, position.Y, _window.Size.X, _window.Size.Y) ?? -1;
|
||||||
if (hit >= 0) { _menuSelection = hit; ActivateMenu(); }
|
if (hit >= 0) { _menuSelection = hit; ActivateMenu(); }
|
||||||
}
|
}
|
||||||
@@ -332,8 +410,17 @@ public sealed class UniverseApplication : IDisposable
|
|||||||
|
|
||||||
private void ActivateMenu()
|
private void ActivateMenu()
|
||||||
{
|
{
|
||||||
if (_menuSelection == 5) { _window.Close(); return; }
|
if (_menuSelection == 6) { _window.Close(); return; }
|
||||||
if (_menuSelection == 2) { _debugMode = !_debugMode; return; }
|
if (_menuSelection == 2) { _debugMode = !_debugMode; return; }
|
||||||
|
if (_menuSelection == 5)
|
||||||
|
{
|
||||||
|
_debugMode = false; _player = null;
|
||||||
|
_benchmark = CreateGalaxyJourney();
|
||||||
|
_benchmarkNeedsInitialFrame = true;
|
||||||
|
var home = CurrentHomeView();
|
||||||
|
_camera.SetPlayerPose(home.Position, home.Forward, home.Up, resetMotionHistory: true);
|
||||||
|
UpdateNeighborhood(); SetMenu(false); return;
|
||||||
|
}
|
||||||
if (_menuSelection is 3 or 4)
|
if (_menuSelection is 3 or 4)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -357,8 +444,10 @@ public sealed class UniverseApplication : IDisposable
|
|||||||
|
|
||||||
private void StartBenchmark(bool unknownPlanet)
|
private void StartBenchmark(bool unknownPlanet)
|
||||||
{
|
{
|
||||||
var system = _homeSystem;
|
_camera.RestoreDefaultFieldOfView();
|
||||||
var planet = _homeEarth;
|
var home = CurrentHomeView();
|
||||||
|
var system = home.System;
|
||||||
|
var planet = home.Earth;
|
||||||
if (unknownPlanet)
|
if (unknownPlanet)
|
||||||
{
|
{
|
||||||
var candidate = _neighborhood.NearbySystems.Append(_neighborhood.ActiveSystem)
|
var candidate = _neighborhood.NearbySystems.Append(_neighborhood.ActiveSystem)
|
||||||
@@ -380,12 +469,37 @@ public sealed class UniverseApplication : IDisposable
|
|||||||
|
|
||||||
_debugMode = false;
|
_debugMode = false;
|
||||||
_player = null;
|
_player = null;
|
||||||
_benchmark = new(system, planet, _generator.CellSizeMeters, _camera,
|
_benchmark = new BenchmarkRunner(system, planet, _generator.CellSizeMeters, _camera,
|
||||||
_homeCameraPosition, _homeCameraForward, _homeCameraUp);
|
home.Position, home.Forward, home.Up);
|
||||||
|
_benchmarkNeedsInitialFrame = true;
|
||||||
UpdateNeighborhood();
|
UpdateNeighborhood();
|
||||||
SetMenu(false);
|
SetMenu(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private GalaxyJourneyBenchmark CreateGalaxyJourney()
|
||||||
|
{
|
||||||
|
_camera.RestoreDefaultFieldOfView();
|
||||||
|
var home = CurrentHomeView();
|
||||||
|
var journey = new GalaxyJourneyBenchmark(_galaxies, _generator, home.System, home.Earth,
|
||||||
|
home.Position, home.Forward, home.Up, _clock.SecondsSinceJ2000);
|
||||||
|
journey.RenderEnvironment = new { Width = _window.FramebufferSize.X, Height = _window.FramebufferSize.Y, Gpu = _renderer?.HardwareName, Mode = "interactive-vsync", FieldOfViewDegrees = _camera.FieldOfViewRadians * 180 / System.Math.PI };
|
||||||
|
foreach (var planet in _homeSystem.Planets) _renderer?.PreparePlanet(_homeSystem, planet);
|
||||||
|
foreach (var planet in journey.TargetSystem.Planets) _renderer?.PreparePlanet(journey.TargetSystem, planet);
|
||||||
|
return journey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (StarSystemData System, PlanetData Earth, UniversePosition Position, DVector3 Forward, DVector3 Up) CurrentHomeView()
|
||||||
|
{
|
||||||
|
var system = _streamer.AtCurrentTime(_homeSystem);
|
||||||
|
var earth = system.Planets.First(p => p.Id == _homeEarth.Id);
|
||||||
|
var center = UniversePosition.AtCellCenter(system.Coordinate, _generator.CellSizeMeters);
|
||||||
|
var oldBody = center.Add(_homeEarth.PositionMeters, _generator.CellSizeMeters);
|
||||||
|
var body = center.Add(earth.PositionMeters, _generator.CellSizeMeters);
|
||||||
|
DVector3 Turn(DVector3 v) => earth.Orientation.ToWorld(_homeEarth.Orientation.ToBody(v));
|
||||||
|
return (system, earth, body.Add(Turn(_homeCameraPosition.RelativeTo(oldBody, _generator.CellSizeMeters)), _generator.CellSizeMeters),
|
||||||
|
Turn(_homeCameraForward), Turn(_homeCameraUp));
|
||||||
|
}
|
||||||
|
|
||||||
private void OnClosing()
|
private void OnClosing()
|
||||||
{
|
{
|
||||||
_renderer?.Dispose();
|
_renderer?.Dispose();
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ public sealed record EngineConfiguration
|
|||||||
public double CellSizeAu { get; init; } = 100;
|
public double CellSizeAu { get; init; } = 100;
|
||||||
public string KnownSolarSystemsFile { get; init; } = "KnownSolarSystem/solar-systems.json";
|
public string KnownSolarSystemsFile { get; init; } = "KnownSolarSystem/solar-systems.json";
|
||||||
public StartupConfiguration Startup { get; init; } = new();
|
public StartupConfiguration Startup { get; init; } = new();
|
||||||
|
public BlackHoleGenerationSettings BlackHoles { get; init; } = new();
|
||||||
|
public GalaxySettings Galaxies { get; init; } = new();
|
||||||
|
public StarMotionBlurSettings StarMotionBlur { get; init; } = new();
|
||||||
|
public Physics.SimulationSettings Simulation { get; init; } = new();
|
||||||
public double CellSizeMeters => CellSizeAu * UniverseScale.AstronomicalUnitMeters;
|
public double CellSizeMeters => CellSizeAu * UniverseScale.AstronomicalUnitMeters;
|
||||||
public ulong Seed => UniverseSeed.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
public ulong Seed => UniverseSeed.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
||||||
? ulong.Parse(UniverseSeed.AsSpan(2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture)
|
? ulong.Parse(UniverseSeed.AsSpan(2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture)
|
||||||
@@ -42,6 +46,10 @@ public sealed record EngineConfiguration
|
|||||||
public static EngineConfiguration Load(string path)
|
public static EngineConfiguration Load(string path)
|
||||||
{
|
{
|
||||||
var settings = JsonSettings.Read<EngineConfiguration>(path);
|
var settings = JsonSettings.Read<EngineConfiguration>(path);
|
||||||
|
(settings.Simulation ?? throw new InvalidDataException("config.json: simulation cannot be null.")).Validate();
|
||||||
|
(settings.BlackHoles ?? throw new InvalidDataException("config.json: blackHoles cannot be null.")).Validate();
|
||||||
|
(settings.Galaxies ?? throw new InvalidDataException("config.json: galaxies cannot be null.")).Validate();
|
||||||
|
(settings.StarMotionBlur ?? throw new InvalidDataException("config.json: starMotionBlur cannot be null.")).Validate();
|
||||||
if (string.IsNullOrWhiteSpace(settings.UniverseSeed)) throw new InvalidDataException("config.json: universeSeed cannot be empty.");
|
if (string.IsNullOrWhiteSpace(settings.UniverseSeed)) throw new InvalidDataException("config.json: universeSeed cannot be empty.");
|
||||||
try { _ = settings.Seed; }
|
try { _ = settings.Seed; }
|
||||||
catch (Exception error) when (error is FormatException or OverflowException or ArgumentNullException)
|
catch (Exception error) when (error is FormatException or OverflowException or ArgumentNullException)
|
||||||
@@ -55,7 +63,7 @@ public sealed record EngineConfiguration
|
|||||||
|| !double.IsFinite(startup.DistanceInRadii) || startup.DistanceInRadii < 1.05
|
|| !double.IsFinite(startup.DistanceInRadii) || startup.DistanceInRadii < 1.05
|
||||||
|| !double.IsFinite(startup.LongitudeDegrees) || !double.IsFinite(startup.LatitudeDegrees) || System.Math.Abs(startup.LatitudeDegrees) > 90
|
|| !double.IsFinite(startup.LongitudeDegrees) || !double.IsFinite(startup.LatitudeDegrees) || System.Math.Abs(startup.LatitudeDegrees) > 90
|
||||||
|| !double.IsFinite(startup.SpeedMetersPerSecond) || startup.SpeedMetersPerSecond < 0.1
|
|| !double.IsFinite(startup.SpeedMetersPerSecond) || startup.SpeedMetersPerSecond < 0.1
|
||||||
|| startup.SpeedMetersPerSecond > UniverseScale.LightSpeedMetersPerSecond * 1_000_000
|
|| startup.SpeedMetersPerSecond > UniverseScale.LightSpeedMetersPerSecond * 1e15
|
||||||
|| !double.IsFinite(startup.FieldOfViewDegrees) || startup.FieldOfViewDegrees is < 30 or > 120)
|
|| !double.IsFinite(startup.FieldOfViewDegrees) || startup.FieldOfViewDegrees is < 30 or > 120)
|
||||||
throw new InvalidDataException("config.json: invalid startup target, planet hotkeys, view angles, distance or flight speed. Planet hotkeys must contain at most nine non-empty planet ids.");
|
throw new InvalidDataException("config.json: invalid startup target, planet hotkeys, view angles, distance or flight speed. Planet hotkeys must contain at most nine non-empty planet ids.");
|
||||||
if (string.IsNullOrWhiteSpace(settings.KnownSolarSystemsFile)) throw new InvalidDataException("config.json: knownSolarSystemsFile is required.");
|
if (string.IsNullOrWhiteSpace(settings.KnownSolarSystemsFile)) throw new InvalidDataException("config.json: knownSolarSystemsFile is required.");
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ public sealed class KnownSolarSystemCatalog
|
|||||||
VisualSettingsValidator.StarGlow(star.Glow, star.Id);
|
VisualSettingsValidator.StarGlow(star.Glow, star.Id);
|
||||||
stars.Add(new(stars.Count, star.Name, offset + Vector(star.PositionMeters), star.RadiusMeters, star.LuminositySolar,
|
stars.Add(new(stars.Count, star.Name, offset + Vector(star.PositionMeters), star.RadiusMeters, star.LuminositySolar,
|
||||||
Color(star.Color), star.Seed ?? StableSeed(definition.Id + "/" + star.Id))
|
Color(star.Color), star.Seed ?? StableSeed(definition.Id + "/" + star.Id))
|
||||||
{ Id = star.Id, Glow = star.Glow });
|
{ Id = star.Id, Glow = star.Glow, MassKg = star.MassKg });
|
||||||
|
Positive(star.MassKg, "star mass");
|
||||||
}
|
}
|
||||||
var planets = new List<PlanetData>();
|
var planets = new List<PlanetData>();
|
||||||
foreach (var planet in definition.Planets) planets.Add(BuildPlanet(planet, definition.Id, stars, planets.Count, null, ids));
|
foreach (var planet in definition.Planets) planets.Add(BuildPlanet(planet, definition.Id, stars, planets.Count, null, ids));
|
||||||
@@ -76,10 +77,21 @@ public sealed class KnownSolarSystemCatalog
|
|||||||
foreach (var planet in planets)
|
foreach (var planet in planets)
|
||||||
{
|
{
|
||||||
CheckBounds(planet.PositionMeters, System.Math.Max(planet.MaximumTerrainRadiusMeters, planet.Rings?.OuterRadiusMeters ?? 0), cellSize, planet.Id);
|
CheckBounds(planet.PositionMeters, System.Math.Max(planet.MaximumTerrainRadiusMeters, planet.Rings?.OuterRadiusMeters ?? 0), cellSize, planet.Id);
|
||||||
|
if (planet.Orbit is { } orbit)
|
||||||
|
{
|
||||||
|
var host = planet.ParentPlanetIndex is { } parent ? planets[parent] : null;
|
||||||
|
var star = stars[planet.HostStarIndex];
|
||||||
|
var extent = orbit.SemiMajorAxisMeters * (1 + orbit.Eccentricity) + planet.MaximumTerrainRadiusMeters;
|
||||||
|
if (host?.Orbit is { } parentOrbit) extent += parentOrbit.SemiMajorAxisMeters * (1 + parentOrbit.Eccentricity);
|
||||||
|
CheckBounds(star.PositionMeters, extent, cellSize, planet.Id + " orbit");
|
||||||
|
if (orbit.SemiMajorAxisMeters * (1 - orbit.Eccentricity) <= (host?.MaximumTerrainRadiusMeters ?? star.RadiusMeters) + planet.MaximumTerrainRadiusMeters)
|
||||||
|
throw new InvalidDataException($"'{planet.Id}': periapsis intersects its host body.");
|
||||||
|
}
|
||||||
if (stars.Any(s => (s.PositionMeters - planet.PositionMeters).Length <= s.RadiusMeters + planet.MaximumTerrainRadiusMeters))
|
if (stars.Any(s => (s.PositionMeters - planet.PositionMeters).Length <= s.RadiusMeters + planet.MaximumTerrainRadiusMeters))
|
||||||
throw new InvalidDataException($"Planet '{planet.Id}' overlaps a star.");
|
throw new InvalidDataException($"Planet '{planet.Id}' overlaps a star.");
|
||||||
}
|
}
|
||||||
return new(coordinate, definition.Seed ?? StableSeed(definition.Id), stars, planets) { Id = definition.Id, Name = definition.Name };
|
return new(coordinate, definition.Seed ?? StableSeed(definition.Id), stars, planets)
|
||||||
|
{ Id = definition.Id, Name = definition.Name, EpochSecondsSinceJ2000 = (definition.EpochUtc - Physics.SimulationClock.J2000).TotalSeconds };
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlanetData BuildPlanet(PlanetDefinition p, string systemId, List<StarData> stars, int index, PlanetData? parent, HashSet<string> ids)
|
private PlanetData BuildPlanet(PlanetDefinition p, string systemId, List<StarData> stars, int index, PlanetData? parent, HashSet<string> ids)
|
||||||
@@ -110,7 +122,11 @@ public sealed class KnownSolarSystemCatalog
|
|||||||
throw new InvalidDataException($"'{p.Id}': gas planets use gas volume settings, not a terrain heightmap or terrain amplitude.");
|
throw new InvalidDataException($"'{p.Id}': gas planets use gas volume settings, not a terrain heightmap or terrain amplitude.");
|
||||||
if (terrain.NoiseMask == TerrainNoiseMask.HeightmapLand && heightmap is null)
|
if (terrain.NoiseMask == TerrainNoiseMask.HeightmapLand && heightmap is null)
|
||||||
throw new InvalidDataException($"'{p.Id}': heightmapLand noise mask requires a heightmap.");
|
throw new InvalidDataException($"'{p.Id}': heightmapLand noise mask requires a heightmap.");
|
||||||
var position = (parent?.PositionMeters ?? stars[host].PositionMeters) + OrbitPosition(p.Orbit);
|
Positive(p.MassKg, "planet mass");
|
||||||
|
if (!double.IsFinite(p.RotationPeriodHours * 3600) || p.RotationPeriodHours == 0 || !double.IsFinite(p.PoleLongitudeDegrees))
|
||||||
|
throw new InvalidDataException($"'{p.Id}': rotationPeriodHours must be finite and nonzero; pole longitude must be finite.");
|
||||||
|
var orbit = BuildOrbit(p.Orbit, (parent?.MassKg ?? stars[host].MassKg) + p.MassKg);
|
||||||
|
var position = (parent?.PositionMeters ?? stars[host].PositionMeters) + orbit.Position(0);
|
||||||
var surfaceTexture = p.SurfaceTexture is null ? null : LoadColor(p.SurfaceTexture);
|
var surfaceTexture = p.SurfaceTexture is null ? null : LoadColor(p.SurfaceTexture);
|
||||||
var planetSeed = p.Seed ?? StableSeed(systemId + "/" + p.Id);
|
var planetSeed = p.Seed ?? StableSeed(systemId + "/" + p.Id);
|
||||||
var clouds = p.Clouds ?? CloudSettings.ForAtmosphere(planetSeed, air.HeightMeters, air.Density, p.SurfaceType);
|
var clouds = p.Clouds ?? CloudSettings.ForAtmosphere(planetSeed, air.HeightMeters, air.Density, p.SurfaceType);
|
||||||
@@ -131,6 +147,9 @@ public sealed class KnownSolarSystemCatalog
|
|||||||
p.Orbit.PhaseDegrees * System.Math.PI / 180, host, p.Seed ?? StableSeed(systemId + "/" + p.Id))
|
p.Orbit.PhaseDegrees * System.Math.PI / 180, host, p.Seed ?? StableSeed(systemId + "/" + p.Id))
|
||||||
{
|
{
|
||||||
Id = p.Id,
|
Id = p.Id,
|
||||||
|
MassKg = p.MassKg,
|
||||||
|
Orbit = orbit,
|
||||||
|
Rotation = new(p.RotationPeriodHours * 3600, p.AxialTiltDegrees, p.RotationDegrees, p.PoleLongitudeDegrees),
|
||||||
Description = p.Description,
|
Description = p.Description,
|
||||||
ParentPlanetIndex = parent?.Index,
|
ParentPlanetIndex = parent?.Index,
|
||||||
SurfaceType = p.SurfaceType,
|
SurfaceType = p.SurfaceType,
|
||||||
@@ -145,7 +164,7 @@ public sealed class KnownSolarSystemCatalog
|
|||||||
OceanMask = p.Ocean is { Enabled: true, MaskFromSurfaceTexture: true } ? OceanSurface.CreateMask(surfaceTexture!) : null,
|
OceanMask = p.Ocean is { Enabled: true, MaskFromSurfaceTexture: true } ? OceanSurface.CreateMask(surfaceTexture!) : null,
|
||||||
AtmosphereDensity = air.Density,
|
AtmosphereDensity = air.Density,
|
||||||
AtmosphereComposition = air.Composition,
|
AtmosphereComposition = air.Composition,
|
||||||
Orientation = new(p.AxialTiltDegrees, p.RotationDegrees),
|
Orientation = new(p.AxialTiltDegrees, p.RotationDegrees, p.PoleLongitudeDegrees),
|
||||||
Terrain = new()
|
Terrain = new()
|
||||||
{
|
{
|
||||||
HeightMap = heightmap,
|
HeightMap = heightmap,
|
||||||
@@ -185,22 +204,27 @@ public sealed class KnownSolarSystemCatalog
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DVector3 OrbitPosition(OrbitDefinition orbit)
|
private static Physics.KeplerOrbit BuildOrbit(OrbitDefinition orbit, double massKg)
|
||||||
{
|
{
|
||||||
Positive(orbit.SemiMajorAxisAu, "semi-major axis");
|
Positive(orbit.SemiMajorAxisAu, "semi-major axis");
|
||||||
if (!Finite(orbit.Eccentricity, orbit.PhaseDegrees, orbit.InclinationDegrees, orbit.AscendingNodeDegrees, orbit.PeriapsisDegrees)
|
if (!Finite(orbit.Eccentricity, orbit.PhaseDegrees, orbit.InclinationDegrees, orbit.AscendingNodeDegrees, orbit.PeriapsisDegrees)
|
||||||
|| orbit.Eccentricity is < 0 or >= 1) throw new InvalidDataException("Only finite elliptical orbits with eccentricity in [0,1) are supported.");
|
|| orbit.Eccentricity is < 0 or >= 1) throw new InvalidDataException("Only finite elliptical orbits with eccentricity in [0,1) are supported.");
|
||||||
var phase = orbit.PhaseDegrees * System.Math.PI / 180;
|
var phase = orbit.PhaseDegrees * System.Math.PI / 180;
|
||||||
var distance = orbit.SemiMajorAxisAu * UniverseScale.AstronomicalUnitMeters * (1 - orbit.Eccentricity * orbit.Eccentricity)
|
var axis = orbit.SemiMajorAxisAu * UniverseScale.AstronomicalUnitMeters;
|
||||||
/ (1 + orbit.Eccentricity * System.Math.Cos(phase));
|
var period = orbit.PeriodDays is { } days ? days * 86400 : Physics.KeplerOrbit.Period(axis, massKg);
|
||||||
var angle = phase + orbit.PeriapsisDegrees * System.Math.PI / 180;
|
Positive(period, "orbit period");
|
||||||
|
if (orbit.MeanAnomalyDegrees is { } mean && !double.IsFinite(mean)) throw new InvalidDataException("meanAnomalyDegrees must be finite.");
|
||||||
|
var anomaly = orbit.MeanAnomalyDegrees is { } m ? m * System.Math.PI / 180 : Physics.KeplerOrbit.MeanAnomalyFromTrue(phase, orbit.Eccentricity);
|
||||||
|
var angle = orbit.PeriapsisDegrees * System.Math.PI / 180;
|
||||||
var inclination = orbit.InclinationDegrees * System.Math.PI / 180;
|
var inclination = orbit.InclinationDegrees * System.Math.PI / 180;
|
||||||
var node = orbit.AscendingNodeDegrees * System.Math.PI / 180;
|
var node = orbit.AscendingNodeDegrees * System.Math.PI / 180;
|
||||||
var x = System.Math.Cos(angle) * distance;
|
DVector3 Basis(double a)
|
||||||
var z = System.Math.Sin(angle) * distance;
|
{
|
||||||
var y = z * System.Math.Sin(inclination);
|
var x = System.Math.Cos(a); var z = System.Math.Sin(a) * System.Math.Cos(inclination);
|
||||||
z *= System.Math.Cos(inclination);
|
return new(x * System.Math.Cos(node) - z * System.Math.Sin(node), System.Math.Sin(a) * System.Math.Sin(inclination),
|
||||||
return new(x * System.Math.Cos(node) - z * System.Math.Sin(node), y, x * System.Math.Sin(node) + z * System.Math.Cos(node));
|
x * System.Math.Sin(node) + z * System.Math.Cos(node));
|
||||||
|
}
|
||||||
|
return new(axis, orbit.Eccentricity, anomaly, period, Basis(angle), Basis(angle + System.Math.PI * .5));
|
||||||
}
|
}
|
||||||
|
|
||||||
private string AssetPath(string relative)
|
private string AssetPath(string relative)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public sealed record SolarSystemDefinition
|
|||||||
public required string Name { get; init; }
|
public required string Name { get; init; }
|
||||||
public required SectorDefinition Sector { get; init; }
|
public required SectorDefinition Sector { get; init; }
|
||||||
public ulong? Seed { get; init; }
|
public ulong? Seed { get; init; }
|
||||||
|
public DateTimeOffset EpochUtc { get; init; } = Physics.SimulationClock.J2000;
|
||||||
public double[] CenterOffsetMeters { get; init; } = [0, 0, 0];
|
public double[] CenterOffsetMeters { get; init; } = [0, 0, 0];
|
||||||
public required List<StarDefinition> Stars { get; init; }
|
public required List<StarDefinition> Stars { get; init; }
|
||||||
public required List<PlanetDefinition> Planets { get; init; }
|
public required List<PlanetDefinition> Planets { get; init; }
|
||||||
@@ -31,6 +32,7 @@ public sealed record StarDefinition
|
|||||||
public double[] PositionMeters { get; init; } = [0, 0, 0];
|
public double[] PositionMeters { get; init; } = [0, 0, 0];
|
||||||
public required double RadiusMeters { get; init; }
|
public required double RadiusMeters { get; init; }
|
||||||
public double LuminositySolar { get; init; } = 1;
|
public double LuminositySolar { get; init; } = 1;
|
||||||
|
public double MassKg { get; init; } = 1.98847e30;
|
||||||
public float[] Color { get; init; } = [1, 0.94f, 0.85f];
|
public float[] Color { get; init; } = [1, 0.94f, 0.85f];
|
||||||
public ulong? Seed { get; init; }
|
public ulong? Seed { get; init; }
|
||||||
}
|
}
|
||||||
@@ -40,6 +42,8 @@ public sealed record OrbitDefinition
|
|||||||
public required double SemiMajorAxisAu { get; init; }
|
public required double SemiMajorAxisAu { get; init; }
|
||||||
public double Eccentricity { get; init; }
|
public double Eccentricity { get; init; }
|
||||||
public double PhaseDegrees { get; init; }
|
public double PhaseDegrees { get; init; }
|
||||||
|
public double? MeanAnomalyDegrees { get; init; }
|
||||||
|
public double? PeriodDays { get; init; }
|
||||||
public double InclinationDegrees { get; init; }
|
public double InclinationDegrees { get; init; }
|
||||||
public double AscendingNodeDegrees { get; init; }
|
public double AscendingNodeDegrees { get; init; }
|
||||||
public double PeriapsisDegrees { get; init; }
|
public double PeriapsisDegrees { get; init; }
|
||||||
@@ -107,6 +111,9 @@ public sealed record PlanetDefinition
|
|||||||
public ulong? Seed { get; init; }
|
public ulong? Seed { get; init; }
|
||||||
public double AxialTiltDegrees { get; init; }
|
public double AxialTiltDegrees { get; init; }
|
||||||
public double RotationDegrees { get; init; }
|
public double RotationDegrees { get; init; }
|
||||||
|
public double MassKg { get; init; } = 5.97217e24;
|
||||||
|
public double RotationPeriodHours { get; init; } = 24;
|
||||||
|
public double PoleLongitudeDegrees { get; init; }
|
||||||
public PlanetSurfaceType SurfaceType { get; init; }
|
public PlanetSurfaceType SurfaceType { get; init; }
|
||||||
public GasVolumeSettings Gas { get; init; } = new();
|
public GasVolumeSettings Gas { get; init; } = new();
|
||||||
public double CloudBandFrequency { get; init; } = 28;
|
public double CloudBandFrequency { get; init; } = 28;
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ internal static class VisualSettingsValidator
|
|||||||
}
|
}
|
||||||
public static void Ocean(OceanSettings o, string id)
|
public static void Ocean(OceanSettings o, string id)
|
||||||
{
|
{
|
||||||
|
var r = o.ScreenSpaceReflections ?? throw new InvalidDataException($"'{id}': ocean.screenSpaceReflections cannot be null.");
|
||||||
|
Range(r.Strength, 0, 1, id, "ocean.screenSpaceReflections.strength");
|
||||||
|
Range(r.ResolutionScale, .25, 1, id, "ocean.screenSpaceReflections.resolutionScale");
|
||||||
|
Range(r.MaxSteps, 8, 128, id, "ocean.screenSpaceReflections.maxSteps");
|
||||||
|
Range(r.StridePixels, 1, 16, id, "ocean.screenSpaceReflections.stridePixels");
|
||||||
|
Range(r.MaxDistanceMeters, 10, 50000, id, "ocean.screenSpaceReflections.maxDistanceMeters");
|
||||||
|
Range(r.MaxSurfaceDistanceMeters, 10, 20000, id, "ocean.screenSpaceReflections.maxSurfaceDistanceMeters");
|
||||||
|
Range(r.ThicknessMeters, .01, 50, id, "ocean.screenSpaceReflections.thicknessMeters");
|
||||||
|
Range(r.NormalBiasMeters, .01, 5, id, "ocean.screenSpaceReflections.normalBiasMeters");
|
||||||
|
Range(r.EdgeFade, .01, .3, id, "ocean.screenSpaceReflections.edgeFade");
|
||||||
Range(o.SeaLevelMeters, -100000, 100000, id, "ocean.seaLevelMeters");
|
Range(o.SeaLevelMeters, -100000, 100000, id, "ocean.seaLevelMeters");
|
||||||
Range(o.DepthMeters, 1, 100000, id, "ocean.depthMeters");
|
Range(o.DepthMeters, 1, 100000, id, "ocean.depthMeters");
|
||||||
Range(o.WaveAmplitudeMeters, 0, 30, id, "ocean.waveAmplitudeMeters");
|
Range(o.WaveAmplitudeMeters, 0, 30, id, "ocean.waveAmplitudeMeters");
|
||||||
|
|||||||
31
src/Universe.Core/Generation/BlackHoleSettings.cs
Normal file
31
src/Universe.Core/Generation/BlackHoleSettings.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using Universe.Core.Math;
|
||||||
|
|
||||||
|
namespace Universe.Core.Generation;
|
||||||
|
|
||||||
|
public sealed record BlackHoleGenerationSettings
|
||||||
|
{
|
||||||
|
public double OccurrenceChance { get; init; } = .05;
|
||||||
|
public double MinimumMassSolar { get; init; } = 200_000;
|
||||||
|
public double MaximumMassSolar { get; init; } = 600_000;
|
||||||
|
public double DiskOuterRadiusInHorizons { get; init; } = 14;
|
||||||
|
public double DiskBrightness { get; init; } = 4;
|
||||||
|
public int RaySteps { get; init; } = 256;
|
||||||
|
|
||||||
|
public void Validate()
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(OccurrenceChance) || OccurrenceChance is < 0 or > 1
|
||||||
|
|| !double.IsFinite(MinimumMassSolar) || MinimumMassSolar < 1
|
||||||
|
|| !double.IsFinite(MaximumMassSolar) || MaximumMassSolar < MinimumMassSolar || MaximumMassSolar > 10_000_000
|
||||||
|
|| !double.IsFinite(DiskOuterRadiusInHorizons) || DiskOuterRadiusInHorizons is < 5 or > 30
|
||||||
|
|| !double.IsFinite(DiskBrightness) || DiskBrightness is < .01 or > 100
|
||||||
|
|| RaySteps is < 128 or > 512)
|
||||||
|
throw new InvalidDataException("config.json: invalid blackHoles settings. RaySteps must be 128–512 and occurrenceChance 0–1.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record BlackHoleData(double MassSolar, DVector3 DiskNormal, double DiskOuterRadiusInHorizons,
|
||||||
|
double DiskBrightness, int RaySteps)
|
||||||
|
{
|
||||||
|
// Schwarzschild radius 2GM/c², in metres per solar mass.
|
||||||
|
public double HorizonRadiusMeters => MassSolar * 2953.25;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Universe.Core.Math;
|
using Universe.Core.Math;
|
||||||
using Universe.Core.World;
|
using Universe.Core.World;
|
||||||
|
using Universe.Core.Physics;
|
||||||
|
|
||||||
namespace Universe.Core.Generation;
|
namespace Universe.Core.Generation;
|
||||||
|
|
||||||
@@ -13,7 +14,10 @@ public sealed record StarData(
|
|||||||
ulong Seed)
|
ulong Seed)
|
||||||
{
|
{
|
||||||
public string Id { get; init; } = $"star-{Index}";
|
public string Id { get; init; } = $"star-{Index}";
|
||||||
|
public double MassKg { get; init; } = 1.98847e30;
|
||||||
public StarGlowSettings Glow { get; init; } = new();
|
public StarGlowSettings Glow { get; init; } = new();
|
||||||
|
public BlackHoleData? BlackHole { get; init; }
|
||||||
|
public bool IsBlackHole => BlackHole is not null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record PlanetData(
|
public sealed record PlanetData(
|
||||||
@@ -31,6 +35,9 @@ public sealed record PlanetData(
|
|||||||
ulong Seed)
|
ulong Seed)
|
||||||
{
|
{
|
||||||
public string Id { get; init; } = $"planet-{Index}";
|
public string Id { get; init; } = $"planet-{Index}";
|
||||||
|
public double MassKg { get; init; } = 5.97217e24;
|
||||||
|
public KeplerOrbit? Orbit { get; init; }
|
||||||
|
public AxialRotation? Rotation { get; init; }
|
||||||
public string Description { get; init; } = string.Empty;
|
public string Description { get; init; } = string.Empty;
|
||||||
public PlanetSurfaceType SurfaceType { get; init; }
|
public PlanetSurfaceType SurfaceType { get; init; }
|
||||||
public bool IsGasPlanet => SurfaceType is PlanetSurfaceType.GasGiant or PlanetSurfaceType.IceGiant;
|
public bool IsGasPlanet => SurfaceType is PlanetSurfaceType.GasGiant or PlanetSurfaceType.IceGiant;
|
||||||
@@ -63,4 +70,7 @@ public sealed record StarSystemData(
|
|||||||
{
|
{
|
||||||
public string Id { get; init; } = $"procedural:{Coordinate.X},{Coordinate.Y},{Coordinate.Z}";
|
public string Id { get; init; } = $"procedural:{Coordinate.X},{Coordinate.Y},{Coordinate.Z}";
|
||||||
public string Name { get; init; } = "Proceduriellt solsystem";
|
public string Name { get; init; } = "Proceduriellt solsystem";
|
||||||
|
public double EpochSecondsSinceJ2000 { get; init; }
|
||||||
|
public string? GalaxyId { get; init; }
|
||||||
|
public string? NebulaId { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
225
src/Universe.Core/Generation/GalaxyUniverse.cs
Normal file
225
src/Universe.Core/Generation/GalaxyUniverse.cs
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Universe.Core.Configuration;
|
||||||
|
using Universe.Core.Math;
|
||||||
|
using Universe.Core.World;
|
||||||
|
|
||||||
|
namespace Universe.Core.Generation;
|
||||||
|
|
||||||
|
public sealed record GalaxySettings
|
||||||
|
{
|
||||||
|
public string KnownGalaxiesFile { get; init; } = "KnownGalaxies.json";
|
||||||
|
public double RegionSizeLightYears { get; init; } = 1_000_000;
|
||||||
|
public double SystemSpacingLightYears { get; init; } = 4;
|
||||||
|
public int GalaxyStarBudget { get; init; } = 6000;
|
||||||
|
public int LocalStarBudget { get; init; } = 2600;
|
||||||
|
public int VolumeSteps { get; init; } = 40;
|
||||||
|
public double MinimumBenchmarkFps { get; init; } = 45;
|
||||||
|
public double MaximumBenchmarkFrameMilliseconds { get; init; } = 50;
|
||||||
|
public void Validate()
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(RegionSizeLightYears) || RegionSizeLightYears is < 300_000 or > 100_000_000
|
||||||
|
|| !double.IsFinite(SystemSpacingLightYears) || SystemSpacingLightYears is < .01 or > 100
|
||||||
|
|| GalaxyStarBudget is < 256 or > 30000 || LocalStarBudget is < 128 or > 10000
|
||||||
|
|| VolumeSteps is < 16 or > 96 || !double.IsFinite(MinimumBenchmarkFps) || MinimumBenchmarkFps is < 1 or > 1000
|
||||||
|
|| !double.IsFinite(MaximumBenchmarkFrameMilliseconds) || MaximumBenchmarkFrameMilliseconds is < 1 or > 1000)
|
||||||
|
throw new InvalidDataException("Invalid galaxies settings in config.json.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record NebulaDefinition
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = "nebula";
|
||||||
|
public double[] PositionLightYears { get; init; } = [26000, 0, 300];
|
||||||
|
public double RadiusLightYears { get; init; } = 80;
|
||||||
|
public string? Texture { get; init; }
|
||||||
|
}
|
||||||
|
public sealed record GalaxyDefinition
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = "milky-way";
|
||||||
|
public string Name { get; init; } = "Vintergatan";
|
||||||
|
public long[] Region { get; init; } = [0, 0, 0];
|
||||||
|
public double[] CenterOffsetLightYears { get; init; } = [-26000, 0, 0];
|
||||||
|
public double RadiusLightYears { get; init; } = 50000;
|
||||||
|
public double ThicknessLightYears { get; init; } = 1800;
|
||||||
|
public double AxialTiltDegrees { get; init; }
|
||||||
|
public double RotationDegrees { get; init; }
|
||||||
|
public int SpiralArms { get; init; } = 4;
|
||||||
|
public string? Texture { get; init; }
|
||||||
|
public double DensityGamma { get; init; } = .7;
|
||||||
|
public NebulaDefinition[] Nebulae { get; init; } = [];
|
||||||
|
}
|
||||||
|
public sealed record KnownGalaxyDefinitions
|
||||||
|
{
|
||||||
|
public GalaxyDefinition[] Galaxies { get; init; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record NebulaData(string Id, UniversePosition Center, double RadiusMeters, PlanetOrientation Orientation, ulong Seed, RasterImage? Texture);
|
||||||
|
|
||||||
|
public sealed record GalaxyData(string Id, string Name, GalaxyCoordinate Region, UniversePosition Center, double RadiusMeters,
|
||||||
|
double ThicknessMeters, PlanetOrientation Orientation, int SpiralArms, ulong Seed, double DensityGamma, RasterImage? Texture,
|
||||||
|
IReadOnlyList<NebulaData> Nebulae)
|
||||||
|
{
|
||||||
|
public double PlanarDensity(double x, double z)
|
||||||
|
{
|
||||||
|
var r = System.Math.Sqrt(x * x + z * z);
|
||||||
|
if (r >= 1) return 0;
|
||||||
|
double density;
|
||||||
|
if (Texture is { } map)
|
||||||
|
{
|
||||||
|
var u = System.Math.Clamp((x * .5 + .5) * (map.Width - 1), 0, map.Width - 1);
|
||||||
|
var v = System.Math.Clamp((.5 - z * .5) * (map.Height - 1), 0, map.Height - 1);
|
||||||
|
var ix = (int)u; var iy = (int)v;
|
||||||
|
double At(int px, int py)
|
||||||
|
{
|
||||||
|
var p = (py * map.Width + px) * 4;
|
||||||
|
return (map.Rgba[p] * .2126 + map.Rgba[p + 1] * .7152 + map.Rgba[p + 2] * .0722) / 255 * map.Rgba[p + 3] / 255;
|
||||||
|
}
|
||||||
|
var tx = u - ix; var ty = v - iy;
|
||||||
|
var a = At(ix, iy) * (1 - tx) + At(System.Math.Min(ix + 1, map.Width - 1), iy) * tx;
|
||||||
|
var b = At(ix, System.Math.Min(iy + 1, map.Height - 1)) * (1 - tx)
|
||||||
|
+ At(System.Math.Min(ix + 1, map.Width - 1), System.Math.Min(iy + 1, map.Height - 1)) * tx;
|
||||||
|
density = a * (1 - ty) + b * ty;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var spiral = System.Math.Atan2(z, x) * SpiralArms - System.Math.Log(r + .05) * 5.8;
|
||||||
|
var arms = System.Math.Pow(.5 + .5 * System.Math.Cos(spiral), 5);
|
||||||
|
density = (.05 + arms * .8) * System.Math.Pow(1 - r, .7) + System.Math.Exp(-r * r * 65) * .8;
|
||||||
|
}
|
||||||
|
return System.Math.Pow(System.Math.Clamp(density, 0, 1), DensityGamma);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double Density(UniversePosition position, double cellSize)
|
||||||
|
{
|
||||||
|
var p = Orientation.ToBody(position.RelativeTo(Center, cellSize));
|
||||||
|
var vertical = System.Math.Exp(-System.Math.Abs(p.Y) / ThicknessMeters);
|
||||||
|
return vertical < .001 ? 0 : PlanarDensity(p.X / RadiusMeters, p.Z / RadiusMeters) * vertical;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Procedural macrocells and sparse deterministic stellar candidates; textures control real occupancy.</summary>
|
||||||
|
public sealed class GalaxyUniverse
|
||||||
|
{
|
||||||
|
private readonly Dictionary<GalaxyCoordinate, GalaxyData> _cache = [];
|
||||||
|
private readonly Dictionary<GalaxyCoordinate, GalaxyDefinition> _known;
|
||||||
|
private readonly string _root;
|
||||||
|
public ulong Seed { get; }
|
||||||
|
public double CellSize { get; }
|
||||||
|
public GalaxySettings Settings { get; }
|
||||||
|
public long RegionCells { get; }
|
||||||
|
public long SpacingCells { get; }
|
||||||
|
|
||||||
|
public GalaxyUniverse(ulong seed, double cellSize, GalaxySettings settings, string catalogPath)
|
||||||
|
{
|
||||||
|
settings.Validate(); Seed = seed; CellSize = cellSize; Settings = settings;
|
||||||
|
RegionCells = checked((long)System.Math.Round(settings.RegionSizeLightYears * UniverseScale.LightYearMeters / cellSize));
|
||||||
|
SpacingCells = System.Math.Max(2, checked((long)System.Math.Round(settings.SystemSpacingLightYears * UniverseScale.LightYearMeters / cellSize)));
|
||||||
|
_root = Path.GetDirectoryName(Path.GetFullPath(catalogPath))!;
|
||||||
|
_known = new();
|
||||||
|
foreach (var definition in JsonSettings.Read<KnownGalaxyDefinitions>(catalogPath).Galaxies)
|
||||||
|
{
|
||||||
|
if (definition.Region.Length != 3 || definition.CenterOffsetLightYears.Length != 3 || string.IsNullOrWhiteSpace(definition.Id)
|
||||||
|
|| !double.IsFinite(definition.RadiusLightYears) || definition.RadiusLightYears <= 0 || definition.RadiusLightYears > settings.RegionSizeLightYears * .2
|
||||||
|
|| !double.IsFinite(definition.ThicknessLightYears) || definition.ThicknessLightYears <= 0
|
||||||
|
|| !double.IsFinite(definition.DensityGamma) || definition.DensityGamma is < .1 or > 8
|
||||||
|
|| definition.SpiralArms is < 1 or > 8 || definition.CenterOffsetLightYears.Any(v => !double.IsFinite(v)))
|
||||||
|
throw new InvalidDataException("Invalid galaxy in KnownGalaxies.json.");
|
||||||
|
_known.Add(new(definition.Region[0], definition.Region[1], definition.Region[2]), definition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public GalaxyCoordinate RegionAt(UniversePosition position) => GalaxyAddress.FromPosition(position, RegionCells).Region;
|
||||||
|
public GalaxyData GalaxyAt(UniversePosition position) => Get(RegionAt(position));
|
||||||
|
|
||||||
|
public GalaxyData Get(GalaxyCoordinate coordinate)
|
||||||
|
{
|
||||||
|
if (_cache.TryGetValue(coordinate, out var cached)) return cached;
|
||||||
|
var seed = Hash(new(coordinate.X, coordinate.Y, coordinate.Z), Seed ^ 0x47414C415859UL);
|
||||||
|
var random = new DeterministicRandom(seed);
|
||||||
|
_known.TryGetValue(coordinate, out var known);
|
||||||
|
var regionOrigin = new GalaxyAddress(coordinate, new(0, 0, 0), DVector3.Zero).ToPosition(RegionCells);
|
||||||
|
var offset = known is null ? new DVector3(random.NextDouble() - .5, random.NextDouble() - .5, random.NextDouble() - .5) * (Settings.RegionSizeLightYears * .15)
|
||||||
|
: new(known.CenterOffsetLightYears[0], known.CenterOffsetLightYears[1], known.CenterOffsetLightYears[2]);
|
||||||
|
var center = regionOrigin.Add(offset * UniverseScale.LightYearMeters, CellSize);
|
||||||
|
var radius = (known?.RadiusLightYears ?? (25000 + random.NextDouble() * 45000)) * UniverseScale.LightYearMeters;
|
||||||
|
var thickness = (known?.ThicknessLightYears ?? (800 + random.NextDouble() * 1800)) * UniverseScale.LightYearMeters;
|
||||||
|
var orientation = new PlanetOrientation(known?.AxialTiltDegrees ?? random.NextDouble() * 130 - 65, known?.RotationDegrees ?? random.NextDouble() * 360);
|
||||||
|
var id = known?.Id ?? $"galaxy:{coordinate.X}:{coordinate.Y}:{coordinate.Z}";
|
||||||
|
var nebulae = new List<NebulaData>();
|
||||||
|
foreach (var n in known?.Nebulae ?? [])
|
||||||
|
{
|
||||||
|
if (n.PositionLightYears.Length != 3 || n.PositionLightYears.Any(v => !double.IsFinite(v)) || !double.IsFinite(n.RadiusLightYears) || n.RadiusLightYears <= 0)
|
||||||
|
throw new InvalidDataException("Invalid nebula in KnownGalaxies.json.");
|
||||||
|
var local = new DVector3(n.PositionLightYears[0], n.PositionLightYears[1], n.PositionLightYears[2]) * UniverseScale.LightYearMeters;
|
||||||
|
nebulae.Add(new(n.Id, center.Add(orientation.ToWorld(local), CellSize), n.RadiusLightYears * UniverseScale.LightYearMeters,
|
||||||
|
orientation, seed ^ (ulong)nebulae.Count, Load(n.Texture)));
|
||||||
|
}
|
||||||
|
for (var i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
var r = .18 + random.NextDouble() * .6; var phi = random.NextDouble() * System.Math.Tau;
|
||||||
|
var local = new DVector3(System.Math.Cos(phi) * r * radius, (random.NextDouble() - .5) * thickness, System.Math.Sin(phi) * r * radius);
|
||||||
|
nebulae.Add(new($"{id}:nebula:{i}", center.Add(orientation.ToWorld(local), CellSize), (150 + random.NextDouble() * 700) * UniverseScale.LightYearMeters,
|
||||||
|
new(random.NextDouble() * 180, random.NextDouble() * 360), random.NextUInt64(), null));
|
||||||
|
}
|
||||||
|
var galaxy = new GalaxyData(id, known?.Name ?? $"Galax {coordinate.X}:{coordinate.Y}:{coordinate.Z}", coordinate, center, radius, thickness, orientation,
|
||||||
|
known?.SpiralArms ?? random.NextInt(2, 6), seed, known?.DensityGamma ?? .7, Load(known?.Texture), nebulae);
|
||||||
|
if (_cache.Count >= 96)
|
||||||
|
{
|
||||||
|
var victim = _cache.Keys.FirstOrDefault(k => !_known.ContainsKey(k));
|
||||||
|
_cache.Remove(victim);
|
||||||
|
}
|
||||||
|
_cache[coordinate] = galaxy;
|
||||||
|
return galaxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RasterImage? Load(string? path) => path is null ? null : RasterImage.Load(Path.GetFullPath(Path.Combine(_root, path)));
|
||||||
|
public IReadOnlyList<GalaxyData> VisibleGalaxies(UniversePosition position)
|
||||||
|
{
|
||||||
|
var r = RegionAt(position); var result = new List<GalaxyData>(27);
|
||||||
|
for (var z = -1; z <= 1; z++) for (var y = -1; y <= 1; y++) for (var x = -1; x <= 1; x++)
|
||||||
|
{
|
||||||
|
try { result.Add(Get(new(checked(r.X + x), checked(r.Y + y), checked(r.Z + z)))); }
|
||||||
|
catch (OverflowException) { }
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long FloorDiv(long a, long b) { var q = a / b; return a < 0 && a % b != 0 ? q - 1 : q; }
|
||||||
|
public SectorCoordinate StellarBlock(SectorCoordinate sector) => new(FloorDiv(sector.X, SpacingCells), FloorDiv(sector.Y, SpacingCells), FloorDiv(sector.Z, SpacingCells));
|
||||||
|
public bool Candidate(GalaxyData galaxy, SectorCoordinate block, out SectorCoordinate sector)
|
||||||
|
{
|
||||||
|
var random = new DeterministicRandom(Hash(block, Seed ^ 0x5354415253UL));
|
||||||
|
long Offset() => (long)((.12 + random.NextDouble() * .76) * SpacingCells);
|
||||||
|
sector = new(checked(block.X * SpacingCells + Offset()), checked(block.Y * SpacingCells + Offset()), checked(block.Z * SpacingCells + Offset()));
|
||||||
|
return random.NextDouble() < galaxy.Density(UniversePosition.AtCellCenter(sector, CellSize), CellSize);
|
||||||
|
}
|
||||||
|
public bool HasSystem(SectorCoordinate sector)
|
||||||
|
{
|
||||||
|
var position = UniversePosition.AtCellCenter(sector, CellSize);
|
||||||
|
return Candidate(GalaxyAt(position), StellarBlock(sector), out var candidate) && candidate == sector;
|
||||||
|
}
|
||||||
|
public IReadOnlyList<SectorCoordinate> NearbySystemCells(SectorCoordinate sector, int radius = 3, int maximum = 16)
|
||||||
|
{
|
||||||
|
var center = UniversePosition.AtCellCenter(sector, CellSize); var galaxy = GalaxyAt(center); var block = StellarBlock(sector);
|
||||||
|
var found = new List<SectorCoordinate>();
|
||||||
|
for (var z = -radius; z <= radius; z++) for (var y = -radius; y <= radius; y++) for (var x = -radius; x <= radius; x++)
|
||||||
|
if (Candidate(galaxy, new(block.X + x, block.Y + y, block.Z + z), out var c)) found.Add(c);
|
||||||
|
// Sol is pinned and never rejected by the density map.
|
||||||
|
if (galaxy.Id == "milky-way" && center.RelativeTo(UniversePosition.AtCellCenter(new(), CellSize), CellSize).Length < SpacingCells * CellSize * (radius + 1)) found.Add(new());
|
||||||
|
return found.Distinct().OrderBy(c => UniversePosition.AtCellCenter(c, CellSize).RelativeTo(center, CellSize).LengthSquared).Take(maximum).ToArray();
|
||||||
|
}
|
||||||
|
public SectorCoordinate SelectSystem(GalaxyData galaxy)
|
||||||
|
{
|
||||||
|
var random = new DeterministicRandom(galaxy.Seed ^ 0x544152474554UL);
|
||||||
|
for (var i = 0; i < 4096; i++)
|
||||||
|
{
|
||||||
|
var local = new DVector3(random.NextDouble() * 1.4 - .7, (random.NextDouble() - .5) * galaxy.ThicknessMeters / galaxy.RadiusMeters,
|
||||||
|
random.NextDouble() * 1.4 - .7) * galaxy.RadiusMeters;
|
||||||
|
var block = StellarBlock(galaxy.Center.Add(galaxy.Orientation.ToWorld(local), CellSize).Sector);
|
||||||
|
if (Candidate(galaxy, block, out var sector)) return sector;
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException("Galaxy density contains no selectable system. Check the texture brightness and alpha.");
|
||||||
|
}
|
||||||
|
public string NebulaFor(GalaxyData galaxy, UniversePosition position) => galaxy.Nebulae.MinBy(n => position.RelativeTo(n.Center, CellSize).LengthSquared)!.Id;
|
||||||
|
public static ulong Hash(SectorCoordinate p, ulong seed) => DeterministicRandom.Mix(DeterministicRandom.Mix(DeterministicRandom.Mix(seed ^ unchecked((ulong)p.X)) ^ unchecked((ulong)p.Y)) ^ unchecked((ulong)p.Z));
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ namespace Universe.Core.Generation;
|
|||||||
/// <summary>Metres, seconds and linear RGB. A null ocean on a planet disables water.</summary>
|
/// <summary>Metres, seconds and linear RGB. A null ocean on a planet disables water.</summary>
|
||||||
public sealed record OceanSettings
|
public sealed record OceanSettings
|
||||||
{
|
{
|
||||||
|
public OceanReflectionSettings ScreenSpaceReflections { get; init; } = new();
|
||||||
public bool Enabled { get; init; } = true;
|
public bool Enabled { get; init; } = true;
|
||||||
public bool MaskFromSurfaceTexture { get; init; } = true;
|
public bool MaskFromSurfaceTexture { get; init; } = true;
|
||||||
public double SeaLevelMeters { get; init; }
|
public double SeaLevelMeters { get; init; }
|
||||||
@@ -23,6 +24,21 @@ public sealed record OceanSettings
|
|||||||
public float[] Absorption { get; init; } = [.12f, .045f, .022f];
|
public float[] Absorption { get; init; } = [.12f, .045f, .022f];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed record OceanReflectionSettings
|
||||||
|
{
|
||||||
|
public bool Enabled { get; init; } = true;
|
||||||
|
public bool ReflectSky { get; init; } = true;
|
||||||
|
public double Strength { get; init; } = 1;
|
||||||
|
public double ResolutionScale { get; init; } = .5;
|
||||||
|
public int MaxSteps { get; init; } = 48;
|
||||||
|
public double StridePixels { get; init; } = 2;
|
||||||
|
public double MaxDistanceMeters { get; init; } = 3000;
|
||||||
|
public double MaxSurfaceDistanceMeters { get; init; } = 4000;
|
||||||
|
public double ThicknessMeters { get; init; } = 1.5;
|
||||||
|
public double NormalBiasMeters { get; init; } = .12;
|
||||||
|
public double EdgeFade { get; init; } = .07;
|
||||||
|
}
|
||||||
|
|
||||||
public readonly record struct OceanWave(DVector3 Direction, double WaveNumber, double Amplitude, double AngularFrequency);
|
public readonly record struct OceanWave(DVector3 Direction, double WaveNumber, double Amplitude, double AngularFrequency);
|
||||||
|
|
||||||
public static class OceanSurface
|
public static class OceanSurface
|
||||||
|
|||||||
@@ -32,30 +32,36 @@ public sealed record PlanetRings(double InnerRadiusMeters, double OuterRadiusMet
|
|||||||
public RingPreviewSettings Preview { get; init; } = new();
|
public RingPreviewSettings Preview { get; init; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Fixed body orientation. Terrain, texture UVs, ring planes and collision use this same transform.</summary>
|
/// <summary>Body orientation shared by terrain, texture UVs, ring planes and collision.</summary>
|
||||||
public sealed class PlanetOrientation
|
public sealed class PlanetOrientation
|
||||||
{
|
{
|
||||||
public static readonly PlanetOrientation Identity = new(0, 0);
|
public static readonly PlanetOrientation Identity = new(0, 0);
|
||||||
private readonly double _sinTilt, _cosTilt, _sinSpin, _cosSpin;
|
private readonly double _sinTilt, _cosTilt, _sinSpin, _cosSpin;
|
||||||
|
private readonly double _sinPole, _cosPole;
|
||||||
public double AxialTiltDegrees { get; }
|
public double AxialTiltDegrees { get; }
|
||||||
public double RotationDegrees { get; }
|
public double RotationDegrees { get; }
|
||||||
|
public double PoleLongitudeDegrees { get; }
|
||||||
public Matrix4x4 BodyToWorld { get; }
|
public Matrix4x4 BodyToWorld { get; }
|
||||||
public Matrix4x4 WorldToBody { get; }
|
public Matrix4x4 WorldToBody { get; }
|
||||||
|
|
||||||
public PlanetOrientation(double axialTiltDegrees, double rotationDegrees)
|
public PlanetOrientation(double axialTiltDegrees, double rotationDegrees, double poleLongitudeDegrees = 0)
|
||||||
{
|
{
|
||||||
AxialTiltDegrees = axialTiltDegrees;
|
AxialTiltDegrees = axialTiltDegrees;
|
||||||
RotationDegrees = rotationDegrees;
|
RotationDegrees = rotationDegrees;
|
||||||
|
PoleLongitudeDegrees = poleLongitudeDegrees;
|
||||||
var tilt = axialTiltDegrees * System.Math.PI / 180;
|
var tilt = axialTiltDegrees * System.Math.PI / 180;
|
||||||
var spin = rotationDegrees * System.Math.PI / 180;
|
var spin = rotationDegrees * System.Math.PI / 180;
|
||||||
(_sinTilt, _cosTilt) = System.Math.SinCos(tilt);
|
(_sinTilt, _cosTilt) = System.Math.SinCos(tilt);
|
||||||
(_sinSpin, _cosSpin) = System.Math.SinCos(spin);
|
(_sinSpin, _cosSpin) = System.Math.SinCos(spin);
|
||||||
BodyToWorld = Matrix4x4.CreateRotationY((float)spin) * Matrix4x4.CreateRotationZ((float)tilt);
|
var pole = poleLongitudeDegrees * System.Math.PI / 180;
|
||||||
|
(_sinPole, _cosPole) = System.Math.SinCos(pole);
|
||||||
|
BodyToWorld = Matrix4x4.CreateRotationY((float)spin) * Matrix4x4.CreateRotationZ((float)tilt) * Matrix4x4.CreateRotationY((float)pole);
|
||||||
WorldToBody = Matrix4x4.Transpose(BodyToWorld);
|
WorldToBody = Matrix4x4.Transpose(BodyToWorld);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DVector3 ToBody(DVector3 d)
|
public DVector3 ToBody(DVector3 d)
|
||||||
{
|
{
|
||||||
|
d = new(_cosPole * d.X - _sinPole * d.Z, d.Y, _sinPole * d.X + _cosPole * d.Z);
|
||||||
var x = _cosTilt * d.X + _sinTilt * d.Y;
|
var x = _cosTilt * d.X + _sinTilt * d.Y;
|
||||||
var y = -_sinTilt * d.X + _cosTilt * d.Y;
|
var y = -_sinTilt * d.X + _cosTilt * d.Y;
|
||||||
return new(_cosSpin * x - _sinSpin * d.Z, y, _sinSpin * x + _cosSpin * d.Z);
|
return new(_cosSpin * x - _sinSpin * d.Z, y, _sinSpin * x + _cosSpin * d.Z);
|
||||||
@@ -65,6 +71,7 @@ public sealed class PlanetOrientation
|
|||||||
{
|
{
|
||||||
var x = _cosSpin * d.X + _sinSpin * d.Z;
|
var x = _cosSpin * d.X + _sinSpin * d.Z;
|
||||||
var z = -_sinSpin * d.X + _cosSpin * d.Z;
|
var z = -_sinSpin * d.X + _cosSpin * d.Z;
|
||||||
return new(_cosTilt * x - _sinTilt * d.Y, _sinTilt * x + _cosTilt * d.Y, z);
|
var tx = _cosTilt * x - _sinTilt * d.Y;
|
||||||
|
return new(_cosPole * tx + _sinPole * z, _sinTilt * x + _cosTilt * d.Y, -_sinPole * tx + _cosPole * z);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/Universe.Core/Generation/StarMotionBlurSettings.cs
Normal file
23
src/Universe.Core/Generation/StarMotionBlurSettings.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
namespace Universe.Core.Generation;
|
||||||
|
|
||||||
|
public sealed record StarMotionBlurSettings
|
||||||
|
{
|
||||||
|
public bool Enabled { get; init; } = true;
|
||||||
|
public double ShutterFraction { get; init; } = .9;
|
||||||
|
public double MaximumTrailPixels { get; init; } = 180;
|
||||||
|
public double MinimumWidthPixels { get; init; } = 1.4;
|
||||||
|
public double MovingBrightness { get; init; } = 1;
|
||||||
|
public double HistoryResetSeconds { get; init; } = .25;
|
||||||
|
public double StarFadeSeconds { get; init; } = .3;
|
||||||
|
|
||||||
|
public void Validate()
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(ShutterFraction) || ShutterFraction is < 0 or > 2
|
||||||
|
|| !double.IsFinite(MaximumTrailPixels) || MaximumTrailPixels is < 1 or > 1024
|
||||||
|
|| !double.IsFinite(MinimumWidthPixels) || MinimumWidthPixels is < .5 or > 6
|
||||||
|
|| !double.IsFinite(MovingBrightness) || MovingBrightness is < 0 or > 4
|
||||||
|
|| !double.IsFinite(HistoryResetSeconds) || HistoryResetSeconds is < .05 or > 2
|
||||||
|
|| !double.IsFinite(StarFadeSeconds) || StarFadeSeconds is < .05 or > 2)
|
||||||
|
throw new InvalidDataException("config.json: invalid starMotionBlur settings.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Universe.Core.Math;
|
using Universe.Core.Math;
|
||||||
using Universe.Core.World;
|
using Universe.Core.World;
|
||||||
|
using Universe.Core.Physics;
|
||||||
using Universe.Core.Configuration;
|
using Universe.Core.Configuration;
|
||||||
|
|
||||||
namespace Universe.Core.Generation;
|
namespace Universe.Core.Generation;
|
||||||
@@ -15,21 +16,36 @@ public sealed class UniverseGenerator
|
|||||||
private static readonly Vector3[] AtmosphereColors =
|
private static readonly Vector3[] AtmosphereColors =
|
||||||
[new(0.13f, 0.32f, 1f), new(0.16f, 0.42f, 1f), new(0.09f, 0.22f, 0.88f), new(0.80f, 0.48f, 0.25f)];
|
[new(0.13f, 0.32f, 1f), new(0.16f, 0.42f, 1f), new(0.09f, 0.22f, 0.88f), new(0.80f, 0.48f, 0.25f)];
|
||||||
|
|
||||||
public UniverseGenerator(ulong universeSeed, double cellSizeMeters = UniverseScale.DefaultCellSizeMeters, KnownSolarSystemCatalog? knownSystems = null)
|
public UniverseGenerator(ulong universeSeed, double cellSizeMeters = UniverseScale.DefaultCellSizeMeters, KnownSolarSystemCatalog? knownSystems = null,
|
||||||
|
BlackHoleGenerationSettings? blackHoles = null, GalaxyUniverse? galaxies = null)
|
||||||
{
|
{
|
||||||
if (!double.IsFinite(cellSizeMeters) || cellSizeMeters <= 0)
|
if (!double.IsFinite(cellSizeMeters) || cellSizeMeters <= 0)
|
||||||
throw new ArgumentOutOfRangeException(nameof(cellSizeMeters));
|
throw new ArgumentOutOfRangeException(nameof(cellSizeMeters));
|
||||||
UniverseSeed = universeSeed;
|
UniverseSeed = universeSeed;
|
||||||
CellSizeMeters = cellSizeMeters;
|
CellSizeMeters = cellSizeMeters;
|
||||||
_knownSystems = knownSystems;
|
_knownSystems = knownSystems;
|
||||||
|
BlackHoles = blackHoles ?? new();
|
||||||
|
BlackHoles.Validate();
|
||||||
|
Galaxies = galaxies;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ulong UniverseSeed { get; }
|
public ulong UniverseSeed { get; }
|
||||||
public double CellSizeMeters { get; }
|
public double CellSizeMeters { get; }
|
||||||
|
public BlackHoleGenerationSettings BlackHoles { get; }
|
||||||
|
public GalaxyUniverse? Galaxies { get; }
|
||||||
|
|
||||||
public StarSystemData Generate(SectorCoordinate coordinate)
|
public StarSystemData Generate(SectorCoordinate coordinate)
|
||||||
{
|
{
|
||||||
if (_knownSystems is not null && _knownSystems.TryGet(coordinate, out var known)) return known;
|
StarSystemData Assign(StarSystemData s)
|
||||||
|
{
|
||||||
|
if (Galaxies is null) return s;
|
||||||
|
var position = UniversePosition.AtCellCenter(coordinate, CellSizeMeters);
|
||||||
|
var galaxy = Galaxies.GalaxyAt(position);
|
||||||
|
return s with { GalaxyId = galaxy.Id, NebulaId = Galaxies.NebulaFor(galaxy, position) };
|
||||||
|
}
|
||||||
|
if (_knownSystems is not null && _knownSystems.TryGet(coordinate, out var known)) return Assign(known);
|
||||||
|
if (Galaxies is not null && !Galaxies.HasSystem(coordinate))
|
||||||
|
return new(coordinate, SeedFor(coordinate), [], []) { Name = "Interstellar space" };
|
||||||
var systemSeed = SeedFor(coordinate);
|
var systemSeed = SeedFor(coordinate);
|
||||||
var random = new DeterministicRandom(systemSeed);
|
var random = new DeterministicRandom(systemSeed);
|
||||||
var noiseOffset = ProceduralNoise.SeedOffset(systemSeed);
|
var noiseOffset = ProceduralNoise.SeedOffset(systemSeed);
|
||||||
@@ -39,6 +55,9 @@ public sealed class UniverseGenerator
|
|||||||
ProceduralNoise.Sample(noiseOffset + new DVector3(17.7, 6.3, 3.1)),
|
ProceduralNoise.Sample(noiseOffset + new DVector3(17.7, 6.3, 3.1)),
|
||||||
ProceduralNoise.Sample(noiseOffset + new DVector3(2.6, 22.3, 11.8))) * (CellSizeMeters * 0.18);
|
ProceduralNoise.Sample(noiseOffset + new DVector3(2.6, 22.3, 11.8))) * (CellSizeMeters * 0.18);
|
||||||
var starCount = random.NextDouble() < 0.46 ? 2 : 1;
|
var starCount = random.NextDouble() < 0.46 ? 2 : 1;
|
||||||
|
var holeRandom = new DeterministicRandom(DeterministicRandom.Mix(systemSeed ^ 0x424C41434B484F4CUL));
|
||||||
|
var hasBlackHole = holeRandom.NextDouble() < BlackHoles.OccurrenceChance;
|
||||||
|
if (hasBlackHole) starCount = 1;
|
||||||
var binaryAxis = UnitVector(ref random);
|
var binaryAxis = UnitVector(ref random);
|
||||||
var separation = (0.025 + random.NextDouble() * 0.17) * UniverseScale.AstronomicalUnitMeters;
|
var separation = (0.025 + random.NextDouble() * 0.17) * UniverseScale.AstronomicalUnitMeters;
|
||||||
var massRatio = 0.25 + random.NextDouble() * 0.6;
|
var massRatio = 0.25 + random.NextDouble() * 0.6;
|
||||||
@@ -50,12 +69,23 @@ public sealed class UniverseGenerator
|
|||||||
stars.Add(new(i, $"{coordinate.X}:{coordinate.Y}:{coordinate.Z} Star {i + 1}", position,
|
stars.Add(new(i, $"{coordinate.X}:{coordinate.Y}:{coordinate.Z} Star {i + 1}", position,
|
||||||
UniverseScale.SolarRadiusMeters * scale, System.Math.Pow(scale, 3.5),
|
UniverseScale.SolarRadiusMeters * scale, System.Math.Pow(scale, 3.5),
|
||||||
StarColors[random.NextInt(0, StarColors.Length)], DeterministicRandom.Mix(systemSeed + (ulong)i + 1))
|
StarColors[random.NextInt(0, StarColors.Length)], DeterministicRandom.Mix(systemSeed + (ulong)i + 1))
|
||||||
{ Id = $"procedural:{coordinate.X},{coordinate.Y},{coordinate.Z}:star:{i}" });
|
{ Id = $"procedural:{coordinate.X},{coordinate.Y},{coordinate.Z}:star:{i}", MassKg = 1.98847e30 * scale });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasBlackHole)
|
||||||
|
{
|
||||||
|
var mass = BlackHoles.MinimumMassSolar + holeRandom.NextDouble() * (BlackHoles.MaximumMassSolar - BlackHoles.MinimumMassSolar);
|
||||||
|
var hole = new BlackHoleData(mass, UnitVector(ref holeRandom), BlackHoles.DiskOuterRadiusInHorizons,
|
||||||
|
BlackHoles.DiskBrightness, BlackHoles.RaySteps);
|
||||||
|
stars[0] = stars[0] with { Name = $"{coordinate.X}:{coordinate.Y}:{coordinate.Z} Black Hole", RadiusMeters = hole.HorizonRadiusMeters,
|
||||||
|
LuminositySolar = .8, Color = new(1f, .73f, .44f), BlackHole = hole, MassKg = mass * 1.98847e30 };
|
||||||
}
|
}
|
||||||
|
|
||||||
var planetCount = random.NextInt(5, 10);
|
var planetCount = random.NextInt(5, 10);
|
||||||
var planets = new List<PlanetData>(planetCount);
|
var planets = new List<PlanetData>(planetCount);
|
||||||
var orbitAu = 0.38 + random.NextDouble() * 0.48;
|
var orbitAu = 0.38 + random.NextDouble() * 0.48;
|
||||||
|
if (stars[0].BlackHole is { } centralHole)
|
||||||
|
orbitAu = System.Math.Max(orbitAu, centralHole.HorizonRadiusMeters * centralHole.DiskOuterRadiusInHorizons * 2.5 / UniverseScale.AstronomicalUnitMeters);
|
||||||
var planeNormal = UnitVector(ref random);
|
var planeNormal = UnitVector(ref random);
|
||||||
var barycentreExtent = System.Math.Max(System.Math.Abs(barycentre.X), System.Math.Max(System.Math.Abs(barycentre.Y), System.Math.Abs(barycentre.Z)));
|
var barycentreExtent = System.Math.Max(System.Math.Abs(barycentre.X), System.Math.Max(System.Math.Abs(barycentre.Y), System.Math.Abs(barycentre.Z)));
|
||||||
var maximumOrbit = (CellSizeMeters * 0.47 - barycentreExtent) / UniverseScale.AstronomicalUnitMeters / 1.32;
|
var maximumOrbit = (CellSizeMeters * 0.47 - barycentreExtent) / UniverseScale.AstronomicalUnitMeters / 1.32;
|
||||||
@@ -84,6 +114,9 @@ public sealed class UniverseGenerator
|
|||||||
orbitRadius, phase, 0, seed)
|
orbitRadius, phase, 0, seed)
|
||||||
{
|
{
|
||||||
Id = $"procedural:{coordinate.X},{coordinate.Y},{coordinate.Z}:planet:{i}",
|
Id = $"procedural:{coordinate.X},{coordinate.Y},{coordinate.Z}:planet:{i}",
|
||||||
|
MassKg = 5.97217e24 * System.Math.Pow(radius / UniverseScale.EarthRadiusMeters, 3),
|
||||||
|
Orbit = MovingOrbit(position - stars[0].PositionMeters, orbitNormal, eccentricity, phase, stars[0].MassKg),
|
||||||
|
Rotation = new((10 + weather.NextDouble() * 40) * 3600, weather.NextDouble() * 60, weather.NextDouble() * 360),
|
||||||
AtmosphereDensity = atmosphereDensity,
|
AtmosphereDensity = atmosphereDensity,
|
||||||
Clouds = CloudSettings.ForAtmosphere(seed, radius * .018, atmosphereDensity, PlanetSurfaceType.Rocky)
|
Clouds = CloudSettings.ForAtmosphere(seed, radius * .018, atmosphereDensity, PlanetSurfaceType.Rocky)
|
||||||
});
|
});
|
||||||
@@ -103,11 +136,14 @@ public sealed class UniverseGenerator
|
|||||||
moonOrbit, random.NextDouble() * System.Math.Tau, parent.HostStarIndex, moonSeed)
|
moonOrbit, random.NextDouble() * System.Math.Tau, parent.HostStarIndex, moonSeed)
|
||||||
{
|
{
|
||||||
ParentPlanetIndex = parent.Index, Id = parent.Id + ":moon", AtmosphereDensity = .18,
|
ParentPlanetIndex = parent.Index, Id = parent.Id + ":moon", AtmosphereDensity = .18,
|
||||||
|
MassKg = parent.MassKg * System.Math.Pow(moonRadius / parent.RadiusMeters, 3) * .6,
|
||||||
|
Orbit = MovingOrbit(moonDirection * moonOrbit, planeNormal, 0, 0, parent.MassKg),
|
||||||
|
Rotation = new(KeplerOrbit.Period(moonOrbit, parent.MassKg), 6, 0),
|
||||||
Clouds = CloudSettings.ForAtmosphere(moonSeed, atmosphereHeight, .18, PlanetSurfaceType.Rocky)
|
Clouds = CloudSettings.ForAtmosphere(moonSeed, atmosphereHeight, .18, PlanetSurfaceType.Rocky)
|
||||||
});
|
});
|
||||||
moonCount++;
|
moonCount++;
|
||||||
}
|
}
|
||||||
return new(coordinate, systemSeed, stars, planets);
|
return Assign(new(coordinate, systemSeed, stars, planets));
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<StarSystemData> GenerateNeighborhood(SectorCoordinate center, int radius = 1)
|
public IEnumerable<StarSystemData> GenerateNeighborhood(SectorCoordinate center, int radius = 1)
|
||||||
@@ -136,6 +172,17 @@ public sealed class UniverseGenerator
|
|||||||
return new(r * System.Math.Cos(phi), y, r * System.Math.Sin(phi));
|
return new(r * System.Math.Cos(phi), y, r * System.Math.Sin(phi));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static KeplerOrbit MovingOrbit(DVector3 initial, DVector3 normal, double eccentricity, double phase, double mass)
|
||||||
|
{
|
||||||
|
var direction = initial.Normalized();
|
||||||
|
var tangent = DVector3.Cross(normal, direction).Normalized();
|
||||||
|
if (tangent.LengthSquared < .1) tangent = DVector3.Cross(new(1, 0, 0), direction).Normalized();
|
||||||
|
var axis = initial.Length * (1 + eccentricity * System.Math.Cos(phase)) / (1 - eccentricity * eccentricity);
|
||||||
|
return new(axis, eccentricity, KeplerOrbit.MeanAnomalyFromTrue(phase, eccentricity), KeplerOrbit.Period(axis, mass),
|
||||||
|
direction * System.Math.Cos(phase) - tangent * System.Math.Sin(phase),
|
||||||
|
direction * System.Math.Sin(phase) + tangent * System.Math.Cos(phase));
|
||||||
|
}
|
||||||
|
|
||||||
private static string RomanNumeral(int number) => number switch
|
private static string RomanNumeral(int number) => number switch
|
||||||
{
|
{
|
||||||
1 => "I",
|
1 => "I",
|
||||||
|
|||||||
71
src/Universe.Core/Physics/CelestialSimulation.cs
Normal file
71
src/Universe.Core/Physics/CelestialSimulation.cs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
using Universe.Core.Generation;
|
||||||
|
using Universe.Core.Math;
|
||||||
|
|
||||||
|
namespace Universe.Core.Physics;
|
||||||
|
|
||||||
|
/// <summary>Analytic two-body orbit, referenced to its system epoch. U/V span the orbital plane.</summary>
|
||||||
|
public sealed record KeplerOrbit(double SemiMajorAxisMeters, double Eccentricity, double MeanAnomalyAtEpochRadians,
|
||||||
|
double PeriodSeconds, DVector3 U, DVector3 V)
|
||||||
|
{
|
||||||
|
public const double GravitationalConstant = 6.67430e-11;
|
||||||
|
public static double Period(double semiMajorAxisMeters, double massKg) =>
|
||||||
|
System.Math.Tau * System.Math.Sqrt(semiMajorAxisMeters * semiMajorAxisMeters * semiMajorAxisMeters / (GravitationalConstant * massKg));
|
||||||
|
|
||||||
|
public DVector3 Position(double seconds)
|
||||||
|
{
|
||||||
|
var m = System.Math.IEEERemainder(MeanAnomalyAtEpochRadians + System.Math.Tau * (seconds % PeriodSeconds) / PeriodSeconds, System.Math.Tau);
|
||||||
|
// Bisection is bounded and robust even for e very close to 1; Newton accelerates ordinary orbits.
|
||||||
|
var lo = -System.Math.PI; var hi = System.Math.PI; var e = m;
|
||||||
|
for (var i = 0; i < 64; i++)
|
||||||
|
{
|
||||||
|
var error = e - Eccentricity * System.Math.Sin(e) - m;
|
||||||
|
if (System.Math.Abs(error) < 1e-14) break;
|
||||||
|
if (error > 0) hi = e; else lo = e;
|
||||||
|
var next = e - error / (1 - Eccentricity * System.Math.Cos(e));
|
||||||
|
e = next > lo && next < hi ? next : (lo + hi) * .5;
|
||||||
|
}
|
||||||
|
return U * (SemiMajorAxisMeters * (System.Math.Cos(e) - Eccentricity))
|
||||||
|
+ V * (SemiMajorAxisMeters * System.Math.Sqrt(1 - Eccentricity * Eccentricity) * System.Math.Sin(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double MeanAnomalyFromTrue(double trueAnomaly, double eccentricity)
|
||||||
|
{
|
||||||
|
var e = System.Math.Atan2(System.Math.Sqrt(1 - eccentricity * eccentricity) * System.Math.Sin(trueAnomaly),
|
||||||
|
eccentricity + System.Math.Cos(trueAnomaly));
|
||||||
|
return e - eccentricity * System.Math.Sin(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Positive period turns about the angular-momentum pole; tilt > 90 degrees gives retrograde spin.</summary>
|
||||||
|
public sealed record AxialRotation(double PeriodSeconds, double AxialTiltDegrees, double PhaseDegrees, double PoleLongitudeDegrees = 0)
|
||||||
|
{
|
||||||
|
public PlanetOrientation At(double seconds) => new(AxialTiltDegrees,
|
||||||
|
PhaseDegrees - 360 * (seconds % PeriodSeconds) / PeriodSeconds, PoleLongitudeDegrees);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CelestialSimulation
|
||||||
|
{
|
||||||
|
public static StarSystemData Evaluate(StarSystemData system, double secondsSinceJ2000)
|
||||||
|
{
|
||||||
|
if (system.Planets.Count == 0) return system;
|
||||||
|
var time = secondsSinceJ2000 - system.EpochSecondsSinceJ2000;
|
||||||
|
var planets = new PlanetData[system.Planets.Count];
|
||||||
|
// Catalog/generator place parents before their satellites.
|
||||||
|
for (var i = 0; i < planets.Length; i++)
|
||||||
|
{
|
||||||
|
var p = system.Planets[i];
|
||||||
|
var position = p.PositionMeters;
|
||||||
|
if (p.Orbit is { } orbit)
|
||||||
|
{
|
||||||
|
var center = p.ParentPlanetIndex is { } parent ? planets[parent].PositionMeters : system.Stars[p.HostStarIndex].PositionMeters;
|
||||||
|
position = center + orbit.Position(time);
|
||||||
|
}
|
||||||
|
planets[i] = p with
|
||||||
|
{
|
||||||
|
PositionMeters = position,
|
||||||
|
Orientation = p.Rotation?.At(time) ?? p.Orientation
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return system with { Planets = planets };
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/Universe.Core/Physics/SimulationClock.cs
Normal file
45
src/Universe.Core/Physics/SimulationClock.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
namespace Universe.Core.Physics;
|
||||||
|
|
||||||
|
public sealed record SimulationSettings
|
||||||
|
{
|
||||||
|
public DateTimeOffset StartUtc { get; init; } = SimulationClock.J2000;
|
||||||
|
public double TimeScale { get; init; } = 1;
|
||||||
|
|
||||||
|
public void Validate()
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(TimeScale) || TimeScale is < 0 or > 100)
|
||||||
|
throw new InvalidDataException("simulation.timeScale must be between 0 and 100.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>World time only: camera input, player movement and UI use unscaled time.</summary>
|
||||||
|
public sealed class SimulationClock
|
||||||
|
{
|
||||||
|
public static readonly DateTimeOffset J2000 = new(2000, 1, 1, 12, 0, 0, TimeSpan.Zero);
|
||||||
|
private double _timeScale;
|
||||||
|
public double SecondsSinceJ2000 { get; private set; }
|
||||||
|
public double ElapsedSeconds { get; private set; }
|
||||||
|
public DateTimeOffset Utc => J2000.AddSeconds(SecondsSinceJ2000);
|
||||||
|
public double TimeScale
|
||||||
|
{
|
||||||
|
get => _timeScale;
|
||||||
|
set => _timeScale = double.IsFinite(value) ? System.Math.Clamp(value, 0, 100)
|
||||||
|
: throw new ArgumentOutOfRangeException(nameof(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimulationClock(SimulationSettings settings)
|
||||||
|
{
|
||||||
|
settings.Validate();
|
||||||
|
SecondsSinceJ2000 = (settings.StartUtc - J2000).TotalSeconds;
|
||||||
|
TimeScale = settings.TimeScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Advance(double realSeconds)
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(realSeconds) || realSeconds < 0) throw new ArgumentOutOfRangeException(nameof(realSeconds));
|
||||||
|
// Do not clamp frame time: a slow frame must not slow the astronomical clock.
|
||||||
|
var step = realSeconds * TimeScale;
|
||||||
|
SecondsSinceJ2000 += step;
|
||||||
|
ElapsedSeconds += step;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/Universe.Core/World/GalaxyAddress.cs
Normal file
28
src/Universe.Core/World/GalaxyAddress.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using Universe.Core.Math;
|
||||||
|
|
||||||
|
namespace Universe.Core.World;
|
||||||
|
|
||||||
|
public readonly record struct GalaxyCoordinate(long X, long Y, long Z);
|
||||||
|
|
||||||
|
/// <summary>Integer macrocell, integer solar-system cell, then local metres. No huge global float position.</summary>
|
||||||
|
public readonly record struct GalaxyAddress(GalaxyCoordinate Region, SectorCoordinate SystemCell, DVector3 LocalMeters)
|
||||||
|
{
|
||||||
|
public static GalaxyAddress FromPosition(UniversePosition position, long regionCells)
|
||||||
|
{
|
||||||
|
static (long Region, long Local) Split(long value, long size)
|
||||||
|
{
|
||||||
|
var wide = (Int128)value + size / 2;
|
||||||
|
var region = wide / size;
|
||||||
|
if (wide < 0 && wide % size != 0) region--;
|
||||||
|
return (checked((long)region), checked((long)((Int128)value - region * size)));
|
||||||
|
}
|
||||||
|
if (regionCells < 2) throw new ArgumentOutOfRangeException(nameof(regionCells));
|
||||||
|
var x = Split(position.Sector.X, regionCells); var y = Split(position.Sector.Y, regionCells); var z = Split(position.Sector.Z, regionCells);
|
||||||
|
return new(new(x.Region, y.Region, z.Region), new(x.Local, y.Local, z.Local), position.LocalOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UniversePosition ToPosition(long regionCells) => new(new(
|
||||||
|
checked((long)((Int128)Region.X * regionCells + SystemCell.X)),
|
||||||
|
checked((long)((Int128)Region.Y * regionCells + SystemCell.Y)),
|
||||||
|
checked((long)((Int128)Region.Z * regionCells + SystemCell.Z))), LocalMeters);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ public static class UniverseScale
|
|||||||
{
|
{
|
||||||
public const double AstronomicalUnitMeters = 149_597_870_700d;
|
public const double AstronomicalUnitMeters = 149_597_870_700d;
|
||||||
public const double LightSpeedMetersPerSecond = 299_792_458d;
|
public const double LightSpeedMetersPerSecond = 299_792_458d;
|
||||||
|
public const double LightYearMeters = 9_460_730_472_580_800d;
|
||||||
public const double SolarRadiusMeters = 695_700_000d;
|
public const double SolarRadiusMeters = 695_700_000d;
|
||||||
public const double EarthRadiusMeters = 6_371_000d;
|
public const double EarthRadiusMeters = 6_371_000d;
|
||||||
public const double DefaultCellSizeMeters = 100d * AstronomicalUnitMeters;
|
public const double DefaultCellSizeMeters = 100d * AstronomicalUnitMeters;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Universe.Core.Generation;
|
using Universe.Core.Generation;
|
||||||
|
using Universe.Core.Physics;
|
||||||
|
|
||||||
namespace Universe.Core.World;
|
namespace Universe.Core.World;
|
||||||
|
|
||||||
@@ -8,10 +9,71 @@ public sealed record UniverseNeighborhood(StarSystemData ActiveSystem, IReadOnly
|
|||||||
public sealed class UniverseStreamer(UniverseGenerator generator)
|
public sealed class UniverseStreamer(UniverseGenerator generator)
|
||||||
{
|
{
|
||||||
private readonly Dictionary<SectorCoordinate, StarSystemData> _cache = [];
|
private readonly Dictionary<SectorCoordinate, StarSystemData> _cache = [];
|
||||||
|
private SectorCoordinate? _nearBlock;
|
||||||
|
private IReadOnlyList<StarSystemData> _nearSystems = [];
|
||||||
|
public double SimulationSeconds { get; set; }
|
||||||
|
public StarSystemData AtCurrentTime(StarSystemData system) => CelestialSimulation.Evaluate(system, SimulationSeconds);
|
||||||
|
public UniverseNeighborhood Refresh(UniverseNeighborhood neighborhood) => new(
|
||||||
|
AtCurrentTime(neighborhood.ActiveSystem), neighborhood.NearbySystems.Select(AtCurrentTime).ToArray());
|
||||||
|
|
||||||
|
public (StarSystemData System, StarData Hole)? FindNearestBlackHole(UniversePosition position)
|
||||||
|
{
|
||||||
|
if (generator.BlackHoles.OccurrenceChance <= 0) return null;
|
||||||
|
if (generator.Galaxies is { } galaxies)
|
||||||
|
{
|
||||||
|
return galaxies.NearbySystemCells(position.Sector, 8, 2048).Select(GetOrCreate)
|
||||||
|
.SelectMany(s => s.Stars.Where(h => h.IsBlackHole).Select(h => ((StarSystemData System, StarData Hole)?)(s, h)))
|
||||||
|
.OrderBy(b => UniversePosition.AtCellCenter(b!.Value.System.Coordinate, generator.CellSizeMeters)
|
||||||
|
.Add(b.Value.Hole.PositionMeters, generator.CellSizeMeters).RelativeTo(position, generator.CellSizeMeters).LengthSquared)
|
||||||
|
.FirstOrDefault();
|
||||||
|
}
|
||||||
|
(StarSystemData System, StarData Hole)? nearest = null;
|
||||||
|
var bestDistance = double.PositiveInfinity;
|
||||||
|
var cell = generator.CellSizeMeters;
|
||||||
|
for (var radius = 0; radius <= 8; radius++)
|
||||||
|
{
|
||||||
|
for (var z = -radius; z <= radius; z++)
|
||||||
|
for (var y = -radius; y <= radius; y++)
|
||||||
|
for (var x = -radius; x <= radius; x++)
|
||||||
|
{
|
||||||
|
if (System.Math.Max(System.Math.Abs(x), System.Math.Max(System.Math.Abs(y), System.Math.Abs(z))) != radius) continue;
|
||||||
|
if (!position.Sector.TryOffset(x, y, z, out var coordinate)) continue;
|
||||||
|
var system = GetOrCreate(coordinate);
|
||||||
|
var center = UniversePosition.AtCellCenter(coordinate, cell);
|
||||||
|
foreach (var hole in system.Stars.Where(s => s.IsBlackHole))
|
||||||
|
{
|
||||||
|
var distance = center.Add(hole.PositionMeters, cell).RelativeTo(position, cell).Length;
|
||||||
|
if (distance >= bestDistance) continue;
|
||||||
|
nearest = (system, hole); bestDistance = distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Any unvisited cell lies beyond at least one face of this cube.
|
||||||
|
var p = position.LocalOffset;
|
||||||
|
var edge = System.Math.Min(System.Math.Min(p.X, cell - p.X),
|
||||||
|
System.Math.Min(System.Math.Min(p.Y, cell - p.Y), System.Math.Min(p.Z, cell - p.Z)));
|
||||||
|
if (bestDistance < radius * cell + edge) break;
|
||||||
|
}
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
|
||||||
public UniverseNeighborhood Load(SectorCoordinate activeCoordinate)
|
public UniverseNeighborhood Load(SectorCoordinate activeCoordinate)
|
||||||
{
|
{
|
||||||
var active = GetOrCreate(activeCoordinate);
|
var active = GetOrCreate(activeCoordinate);
|
||||||
|
if (generator.Galaxies is { } galaxies)
|
||||||
|
{
|
||||||
|
var block = galaxies.StellarBlock(activeCoordinate);
|
||||||
|
if (_nearBlock != block)
|
||||||
|
{
|
||||||
|
_nearSystems = galaxies.NearbySystemCells(activeCoordinate).Select(GetOrCreate).ToArray();
|
||||||
|
_nearBlock = block;
|
||||||
|
}
|
||||||
|
if (_cache.Count > 96)
|
||||||
|
{
|
||||||
|
var keep = _nearSystems.Select(s => s.Coordinate).Append(activeCoordinate).ToHashSet();
|
||||||
|
foreach (var key in _cache.Keys.ToArray()) if (!keep.Contains(key)) _cache.Remove(key);
|
||||||
|
}
|
||||||
|
return Refresh(new(active, _nearSystems.Where(s => s.Coordinate != activeCoordinate).ToArray()));
|
||||||
|
}
|
||||||
var nearby = new List<StarSystemData>(26);
|
var nearby = new List<StarSystemData>(26);
|
||||||
for (var z = -1; z <= 1; z++)
|
for (var z = -1; z <= 1; z++)
|
||||||
for (var y = -1; y <= 1; y++)
|
for (var y = -1; y <= 1; y++)
|
||||||
@@ -31,7 +93,7 @@ public sealed class UniverseStreamer(UniverseGenerator generator)
|
|||||||
_cache.Remove(key);
|
_cache.Remove(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new(active, nearby);
|
return Refresh(new(active, nearby));
|
||||||
}
|
}
|
||||||
|
|
||||||
private StarSystemData GetOrCreate(SectorCoordinate coordinate)
|
private StarSystemData GetOrCreate(SectorCoordinate coordinate)
|
||||||
@@ -39,6 +101,7 @@ public sealed class UniverseStreamer(UniverseGenerator generator)
|
|||||||
if (!_cache.TryGetValue(coordinate, out var system))
|
if (!_cache.TryGetValue(coordinate, out var system))
|
||||||
{
|
{
|
||||||
system = generator.Generate(coordinate);
|
system = generator.Generate(coordinate);
|
||||||
|
if (system.Stars.Count == 0) return system;
|
||||||
_cache.Add(coordinate, system);
|
_cache.Add(coordinate, system);
|
||||||
}
|
}
|
||||||
return system;
|
return system;
|
||||||
|
|||||||
65
tests/Universe.Tests/BlackHoleTests.cs
Normal file
65
tests/Universe.Tests/BlackHoleTests.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
using Universe.App.Runtime;
|
||||||
|
using Universe.Core.Generation;
|
||||||
|
using Universe.Core.Math;
|
||||||
|
using Universe.Core.World;
|
||||||
|
|
||||||
|
namespace Universe.Tests;
|
||||||
|
|
||||||
|
public sealed class BlackHoleTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ReplacementIsDeterministicAndPlanetsClearTheDisk()
|
||||||
|
{
|
||||||
|
var generator = new UniverseGenerator(123, blackHoles: new() { OccurrenceChance = 1 });
|
||||||
|
var first = generator.Generate(new(-2, 1, 3));
|
||||||
|
var second = generator.Generate(first.Coordinate);
|
||||||
|
var hole = Assert.Single(first.Stars);
|
||||||
|
Assert.True(hole.IsBlackHole);
|
||||||
|
Assert.Equal(hole, Assert.Single(second.Stars));
|
||||||
|
Assert.Equal(hole.RadiusMeters, hole.BlackHole!.HorizonRadiusMeters);
|
||||||
|
Assert.All(first.Planets, p => Assert.True((p.PositionMeters - hole.PositionMeters).Length >
|
||||||
|
hole.RadiusMeters * hole.BlackHole.DiskOuterRadiusInHorizons));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NearestSearchMatchesWideExhaustiveSearchAndFlightReachesTheHole()
|
||||||
|
{
|
||||||
|
var generator = new UniverseGenerator(0x554E495645525345UL);
|
||||||
|
var streamer = new UniverseStreamer(generator);
|
||||||
|
var start = UniversePosition.AtCellCenter(new(0, 0, 0), generator.CellSizeMeters);
|
||||||
|
var found = streamer.FindNearestBlackHole(start);
|
||||||
|
Assert.True(found.HasValue);
|
||||||
|
var target = found!.Value;
|
||||||
|
var exhaustive = generator.GenerateNeighborhood(start.Sector, 3)
|
||||||
|
.SelectMany(s => s.Stars.Where(h => h.IsBlackHole).Select(h => (System: s, Hole: h)))
|
||||||
|
.MinBy(b => UniversePosition.AtCellCenter(b.System.Coordinate, generator.CellSizeMeters)
|
||||||
|
.Add(b.Hole.PositionMeters, generator.CellSizeMeters).RelativeTo(start, generator.CellSizeMeters).Length);
|
||||||
|
Assert.Equal(exhaustive.Hole.Id, target.Hole.Id);
|
||||||
|
var neighborhood = streamer.Load(start.Sector);
|
||||||
|
var groundPlanet = neighborhood.ActiveSystem.Planets[0];
|
||||||
|
var groundBody = start.Add(groundPlanet.PositionMeters, generator.CellSizeMeters);
|
||||||
|
var groundRadial = new DVector3(1, .3, -.2).Normalized();
|
||||||
|
var groundPosition = groundBody.Add(groundRadial * (TerrainHeight.Sample(groundPlanet, groundRadial) + 1.8), generator.CellSizeMeters);
|
||||||
|
var camera = new FlyCamera(groundPosition, generator.CellSizeMeters);
|
||||||
|
camera.RequestBlackHoleView(target.System, target.Hole, neighborhood.NearbySystems.Append(neighborhood.ActiveSystem));
|
||||||
|
for (var i = 0; i < 650; i++)
|
||||||
|
{
|
||||||
|
camera.Update(1d / 60, DVector3.Zero, neighborhood);
|
||||||
|
if (camera.Position.Sector != neighborhood.ActiveSystem.Coordinate) neighborhood = streamer.Load(camera.Position.Sector);
|
||||||
|
}
|
||||||
|
var body = UniversePosition.AtCellCenter(target.System.Coordinate, generator.CellSizeMeters)
|
||||||
|
.Add(target.Hole.PositionMeters, generator.CellSizeMeters);
|
||||||
|
var relative = body.RelativeTo(camera.Position, generator.CellSizeMeters);
|
||||||
|
Assert.False(camera.IsFocusing);
|
||||||
|
Assert.InRange(relative.Length / target.Hole.RadiusMeters, 25.99, 26.01);
|
||||||
|
Assert.InRange(DVector3.Dot(relative.Normalized(), camera.Forward), .999, 1.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ZeroChanceDisablesGenerationAndSearch()
|
||||||
|
{
|
||||||
|
var generator = new UniverseGenerator(123, blackHoles: new() { OccurrenceChance = 0 });
|
||||||
|
Assert.All(generator.GenerateNeighborhood(new()), s => Assert.All(s.Stars, h => Assert.False(h.IsBlackHole)));
|
||||||
|
Assert.Null(new UniverseStreamer(generator).FindNearestBlackHole(UniversePosition.AtCellCenter(new(), generator.CellSizeMeters)));
|
||||||
|
}
|
||||||
|
}
|
||||||
194
tests/Universe.Tests/CelestialSimulationTests.cs
Normal file
194
tests/Universe.Tests/CelestialSimulationTests.cs
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Universe.App.Graphics;
|
||||||
|
using Universe.App.Runtime;
|
||||||
|
using Universe.Core.Configuration;
|
||||||
|
using Universe.Core.Generation;
|
||||||
|
using Universe.Core.Math;
|
||||||
|
using Universe.Core.Physics;
|
||||||
|
using Universe.Core.World;
|
||||||
|
|
||||||
|
namespace Universe.Tests;
|
||||||
|
|
||||||
|
public sealed class CelestialSimulationTests
|
||||||
|
{
|
||||||
|
private static readonly Lazy<StarSystemData> Sol = new(() => KnownSolarSystemCatalog.Load(
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "KnownSolarSystem", "solar-systems.json")).GetById("sol"));
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ClockRunsAtRealTimePausesAndAcceleratesWithoutDroppingSlowFrames()
|
||||||
|
{
|
||||||
|
var clock = new SimulationClock(new());
|
||||||
|
Assert.Equal(1, clock.TimeScale);
|
||||||
|
clock.Advance(2.5);
|
||||||
|
Assert.Equal(2.5, clock.SecondsSinceJ2000);
|
||||||
|
clock.TimeScale = 0; clock.Advance(120);
|
||||||
|
Assert.Equal(2.5, clock.ElapsedSeconds);
|
||||||
|
clock.TimeScale = 100; clock.Advance(60);
|
||||||
|
Assert.Equal(6002.5, clock.ElapsedSeconds);
|
||||||
|
Assert.Equal(SimulationClock.J2000.AddSeconds(6002.5), clock.Utc);
|
||||||
|
Assert.Throws<InvalidDataException>(() => new SimulationClock(new() { TimeScale = 101 }));
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => clock.Advance(double.NaN));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0)]
|
||||||
|
[InlineData(.20563593)]
|
||||||
|
[InlineData(.9999)]
|
||||||
|
public void EllipseHasCorrectApsesAndClosesAfterAnOrbit(double eccentricity)
|
||||||
|
{
|
||||||
|
const double axis = 1e8, period = 100000;
|
||||||
|
var orbit = new KeplerOrbit(axis, eccentricity, 0, period, new(1, 0, 0), new(0, 0, 1));
|
||||||
|
Assert.InRange(System.Math.Abs(orbit.Position(0).Length - axis * (1 - eccentricity)), 0, 1e-6);
|
||||||
|
Assert.InRange(System.Math.Abs(orbit.Position(period * .5).Length - axis * (1 + eccentricity)), 0, 1e-6);
|
||||||
|
Assert.InRange((orbit.Position(1200) - orbit.Position(period * 1000000 + 1200)).Length, 0, 1e-5);
|
||||||
|
Assert.InRange((orbit.Position(-1200) - orbit.Position(period - 1200)).Length, 0, 1e-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MercuryAcceleratesAtPerihelionAndConservesKeplerEnergyAndAngularMomentum()
|
||||||
|
{
|
||||||
|
var orbit = Sol.Value.Planets.First(p => p.Id == "mercury").Orbit! with { MeanAnomalyAtEpochRadians = 0 };
|
||||||
|
DVector3 Velocity(double t) => (orbit.Position(t + 1) - orbit.Position(t - 1)) * .5;
|
||||||
|
var periSpeed = Velocity(0).Length;
|
||||||
|
var apSpeed = Velocity(orbit.PeriodSeconds * .5).Length;
|
||||||
|
Assert.InRange(periSpeed / apSpeed, 1.517, 1.519);
|
||||||
|
var mu = System.Math.Pow(System.Math.Tau / orbit.PeriodSeconds, 2) * System.Math.Pow(orbit.SemiMajorAxisMeters, 3);
|
||||||
|
var energy = -mu / (2 * orbit.SemiMajorAxisMeters);
|
||||||
|
var h = DVector3.Cross(orbit.Position(0), Velocity(0)).Length;
|
||||||
|
foreach (var fraction in new[] { .01, .17, .4, .8 })
|
||||||
|
{
|
||||||
|
var t = fraction * orbit.PeriodSeconds;
|
||||||
|
var v = Velocity(t); var r = orbit.Position(t);
|
||||||
|
Assert.InRange(System.Math.Abs((v.LengthSquared * .5 - mu / r.Length) / energy - 1), 0, 1e-7);
|
||||||
|
Assert.InRange(System.Math.Abs(DVector3.Cross(r, v).Length / h - 1), 0, 1e-7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EarthUsesSiderealDayAndYearAndMoonOrbitsTheMovingEarth()
|
||||||
|
{
|
||||||
|
var start = CelestialSimulation.Evaluate(Sol.Value, 0);
|
||||||
|
var hour = CelestialSimulation.Evaluate(Sol.Value, 3600);
|
||||||
|
var earth = start.Planets.First(p => p.Id == "earth");
|
||||||
|
var later = hour.Planets[earth.Index];
|
||||||
|
Assert.InRange(earth.Rotation!.PeriodSeconds, 86164, 86165);
|
||||||
|
Assert.InRange(earth.Orbit!.PeriodSeconds / 86400, 365.25, 365.26);
|
||||||
|
Assert.InRange((later.PositionMeters - earth.PositionMeters).Length, 106e6, 110e6);
|
||||||
|
var moon = start.Planets.First(p => p.Id == "moon");
|
||||||
|
var day = CelestialSimulation.Evaluate(Sol.Value, 86400);
|
||||||
|
var relative = day.Planets[moon.Index].PositionMeters - day.Planets[earth.Index].PositionMeters;
|
||||||
|
Assert.InRange(relative.Length, 363e6, 406e6);
|
||||||
|
Assert.InRange((relative - moon.Orbit!.Position(86400)).Length, 0, .0001);
|
||||||
|
foreach (var p in start.Planets)
|
||||||
|
{
|
||||||
|
Assert.NotNull(p.Orbit); Assert.NotNull(p.Rotation);
|
||||||
|
Assert.Same(p.Terrain, hour.Planets[p.Index].Terrain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("earth", true)]
|
||||||
|
[InlineData("venus", false)]
|
||||||
|
[InlineData("uranus", false)]
|
||||||
|
public void SpinDirectionMatchesProgradeOrRetrogradeWithoutDoubleNegation(string id, bool prograde)
|
||||||
|
{
|
||||||
|
var p = Sol.Value.Planets.First(p => p.Id == id);
|
||||||
|
var a = p.Rotation!.At(0).ToWorld(new(1, 0, 0));
|
||||||
|
var b = p.Rotation.At(System.Math.Abs(p.Rotation.PeriodSeconds) / 100).ToWorld(new(1, 0, 0));
|
||||||
|
var orbit = p.Orbit!;
|
||||||
|
var angular = DVector3.Dot(DVector3.Cross(a, b), DVector3.Cross(orbit.U, orbit.V));
|
||||||
|
Assert.Equal(prograde, angular > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StreamedSystemUsesSameTimeAfterUnloadingAndDoesNotAccumulateOrbitError()
|
||||||
|
{
|
||||||
|
var generator = new UniverseGenerator(42);
|
||||||
|
var streamer = new UniverseStreamer(generator) { SimulationSeconds = 1234567 };
|
||||||
|
var first = streamer.Load(new(0, 0, 0));
|
||||||
|
streamer.Load(new(1000, 0, 0));
|
||||||
|
var again = streamer.Load(new(0, 0, 0));
|
||||||
|
Assert.Equal(first.ActiveSystem.Planets.Select(p => p.PositionMeters), again.ActiveSystem.Planets.Select(p => p.PositionMeters));
|
||||||
|
var refreshed = streamer.Refresh(again);
|
||||||
|
Assert.Equal(again.ActiveSystem.Planets.Select(p => p.PositionMeters), refreshed.ActiveSystem.Planets.Select(p => p.PositionMeters));
|
||||||
|
streamer.SimulationSeconds += 1000;
|
||||||
|
var moved = streamer.Refresh(again);
|
||||||
|
Assert.NotEqual(again.ActiveSystem.Planets[0].PositionMeters, moved.ActiveSystem.Planets[0].PositionMeters);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GroundedPlayerFollowsOrbitAndTerrainRotationWithoutLosingClearance()
|
||||||
|
{
|
||||||
|
const double cell = 10000000;
|
||||||
|
var planet = new PlanetData(0, "Test", DVector3.Zero, 10000, 150, 0, Vector3.One, Vector3.Zero, 1e6, 0, 0, 20)
|
||||||
|
{
|
||||||
|
Orbit = new(1e6, .1, 0, 86400, new(1, 0, 0), new(0, 0, 1)),
|
||||||
|
Rotation = new(1000, 23.4, 30)
|
||||||
|
};
|
||||||
|
var basis = new StarSystemData(new(0, 0, 0), 1, [new(0, "Sun", DVector3.Zero, 1000, 1, Vector3.One, 1)], [planet]);
|
||||||
|
var before = new UniverseNeighborhood(CelestialSimulation.Evaluate(basis, 0), []);
|
||||||
|
var p = before.ActiveSystem.Planets[0];
|
||||||
|
var center = UniversePosition.AtCellCenter(basis.Coordinate, cell);
|
||||||
|
var direction = p.Orientation.ToWorld(new DVector3(.6, .8, 0));
|
||||||
|
var body = center.Add(p.PositionMeters, cell);
|
||||||
|
var player = new PlayerController(body.Add(direction * (TerrainHeight.Sample(p, direction) + 1.7), cell),
|
||||||
|
p.Orientation.ToWorld(new(0, 0, 1)), direction, cell, before);
|
||||||
|
player.Update(.01, 0, 0, false, false, before);
|
||||||
|
Assert.True(player.Grounded);
|
||||||
|
var local = p.Orientation.ToBody(player.Position.RelativeTo(body, cell));
|
||||||
|
for (var i = 1; i <= 20; i++)
|
||||||
|
{
|
||||||
|
var current = new UniverseNeighborhood(CelestialSimulation.Evaluate(basis, i * 10), []);
|
||||||
|
player.ApplyCelestialMotion(before, current);
|
||||||
|
p = current.ActiveSystem.Planets[0];
|
||||||
|
var offset = player.Position.RelativeTo(center.Add(p.PositionMeters, cell), cell);
|
||||||
|
Assert.InRange((p.Orientation.ToBody(offset) - local).Length, 0, .0001);
|
||||||
|
Assert.InRange(offset.Length - TerrainHeight.Sample(p, offset.Normalized()), 1.6999, 1.7001);
|
||||||
|
Assert.InRange(DVector3.Dot(player.Up, offset.Normalized()), .999999, 1.000001);
|
||||||
|
before = current;
|
||||||
|
}
|
||||||
|
player.Update(.01, 0, 0, true, false, before);
|
||||||
|
Assert.False(player.Grounded);
|
||||||
|
var jumpBody = center.Add(before.ActiveSystem.Planets[0].PositionMeters, cell);
|
||||||
|
var jumpLocal = before.ActiveSystem.Planets[0].Orientation.ToBody(player.Position.RelativeTo(jumpBody, cell));
|
||||||
|
var whileJumping = new UniverseNeighborhood(CelestialSimulation.Evaluate(basis, 210), []);
|
||||||
|
player.ApplyCelestialMotion(before, whileJumping);
|
||||||
|
p = whileJumping.ActiveSystem.Planets[0];
|
||||||
|
var carriedJump = p.Orientation.ToBody(player.Position.RelativeTo(center.Add(p.PositionMeters, cell), cell));
|
||||||
|
Assert.InRange((carriedJump - jumpLocal).Length, 0, .0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DebugTimeSliderSupportsPauseRealTimeAndMaximum()
|
||||||
|
{
|
||||||
|
Assert.Equal(0d, PauseMenu.TimeScaleHit(1010, 390, 1440, 900));
|
||||||
|
Assert.Equal(100d, PauseMenu.TimeScaleHit(1330, 390, 1440, 900));
|
||||||
|
Assert.Equal(1d, PauseMenu.TimeScaleHit(1129, 469, 1440, 900));
|
||||||
|
Assert.Null(PauseMenu.TimeScaleHit(720, 450, 1440, 900));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FocusFlightTracksMovingEarthAtMaximumTimeScale()
|
||||||
|
{
|
||||||
|
var cell = UniverseScale.DefaultCellSizeMeters;
|
||||||
|
var world = new UniverseNeighborhood(CelestialSimulation.Evaluate(Sol.Value, 0), []);
|
||||||
|
var camera = new FlyCamera(UniversePosition.AtCellCenter(new(0, 0, 0), cell), cell);
|
||||||
|
camera.SetInitialView(world.ActiveSystem, new StartupConfiguration { PlanetId = "earth", DistanceInRadii = 5 });
|
||||||
|
camera.RequestFocus([world.ActiveSystem], "earth");
|
||||||
|
var time = 0d;
|
||||||
|
for (var i = 0; i < 60 && camera.IsFocusing; i++)
|
||||||
|
{
|
||||||
|
time += 10;
|
||||||
|
var next = new UniverseNeighborhood(CelestialSimulation.Evaluate(Sol.Value, time), []);
|
||||||
|
camera.ApplyCelestialMotion(world, next, time);
|
||||||
|
camera.Update(.1, DVector3.Zero, next);
|
||||||
|
world = next;
|
||||||
|
}
|
||||||
|
Assert.False(camera.IsFocusing);
|
||||||
|
var earth = world.ActiveSystem.Planets.Single(p => p.Id == "earth");
|
||||||
|
var body = UniversePosition.AtCellCenter(new(0, 0, 0), cell).Add(earth.PositionMeters, cell);
|
||||||
|
var toward = body.RelativeTo(camera.Position, cell);
|
||||||
|
Assert.InRange(toward.Length / earth.RadiusMeters, 1.1, 10);
|
||||||
|
Assert.InRange(DVector3.Dot(toward.Normalized(), camera.Forward), .999, 1.001);
|
||||||
|
}
|
||||||
|
}
|
||||||
84
tests/Universe.Tests/GalaxyTests.cs
Normal file
84
tests/Universe.Tests/GalaxyTests.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using Universe.App.Runtime;
|
||||||
|
using Universe.Core.Generation;
|
||||||
|
using Universe.Core.Math;
|
||||||
|
using Universe.Core.World;
|
||||||
|
|
||||||
|
namespace Universe.Tests;
|
||||||
|
|
||||||
|
public sealed class GalaxyTests
|
||||||
|
{
|
||||||
|
private static GalaxyUniverse Create() => new(0x554E495645525345UL,UniverseScale.DefaultCellSizeMeters,new(),Path.Combine(AppContext.BaseDirectory,"KnownGalaxies.json"));
|
||||||
|
[Fact]
|
||||||
|
public void HierarchicalAddressKeepsLocalMetresAtRemoteGalaxyCoordinates()
|
||||||
|
{
|
||||||
|
var universe=Create();
|
||||||
|
var address=new GalaxyAddress(new(1_000_000_000,-1_000_000_000,500),new(-53,21,700),new(456.75,901.125,18));
|
||||||
|
var position=address.ToPosition(universe.RegionCells);
|
||||||
|
Assert.Equal(address,GalaxyAddress.FromPosition(position,universe.RegionCells));
|
||||||
|
var moved=position.Add(new(.125,.25,.5),universe.CellSize);
|
||||||
|
Assert.Equal(new DVector3(.125,.25,.5),moved.RelativeTo(position,universe.CellSize));
|
||||||
|
var negative=new UniversePosition(new(-universe.RegionCells/2-1,0,0),DVector3.Zero);
|
||||||
|
Assert.Equal(-1,GalaxyAddress.FromPosition(negative,universe.RegionCells).Region.X);
|
||||||
|
}
|
||||||
|
[Fact]
|
||||||
|
public void TextureBrightnessAndAlphaControlActualSystemOccupancy()
|
||||||
|
{
|
||||||
|
var universe=Create();var galaxy=universe.Get(new());
|
||||||
|
var white=new RasterImage(2,2,Enumerable.Repeat((byte)255,16).ToArray());
|
||||||
|
var transparent=white.Rgba.ToArray();for(var i=3;i<16;i+=4)transparent[i]=0;
|
||||||
|
var bright=galaxy with {Texture=white,Center=UniversePosition.AtCellCenter(new(),universe.CellSize)};
|
||||||
|
var dark=bright with {Texture=new(2,2,new byte[16])};
|
||||||
|
var alpha=bright with {Texture=new(2,2,transparent)};
|
||||||
|
Assert.Equal(1,bright.PlanarDensity(0,0),10);
|
||||||
|
Assert.Equal(0,dark.PlanarDensity(0,0));Assert.Equal(0,alpha.PlanarDensity(0,0));
|
||||||
|
var count=0;
|
||||||
|
for(var x=-3;x<=3;x++)for(var z=-3;z<=3;z++)
|
||||||
|
{
|
||||||
|
if(universe.Candidate(bright,new(x,0,z),out _))count++;
|
||||||
|
Assert.False(universe.Candidate(dark,new(x,0,z),out _));
|
||||||
|
Assert.False(universe.Candidate(alpha,new(x,0,z),out _));
|
||||||
|
}
|
||||||
|
Assert.True(count>20);
|
||||||
|
}
|
||||||
|
[Fact]
|
||||||
|
public void GeneratedGalaxiesAreDeterministicAndSystemsHaveParents()
|
||||||
|
{
|
||||||
|
var a=Create();var b=Create();var coordinate=new GalaxyCoordinate(1,0,0);
|
||||||
|
var first=a.Get(coordinate);var second=b.Get(coordinate);
|
||||||
|
Assert.Equal(first.Center,second.Center);Assert.Equal(first.Seed,second.Seed);
|
||||||
|
var sector=a.SelectSystem(first);Assert.Equal(sector,b.SelectSystem(second));
|
||||||
|
var generator=new UniverseGenerator(a.Seed,galaxies:a);
|
||||||
|
var system=generator.Generate(sector);
|
||||||
|
Assert.NotEmpty(system.Stars);Assert.NotEmpty(system.Planets);
|
||||||
|
Assert.Equal(first.Id,system.GalaxyId);Assert.NotNull(system.NebulaId);
|
||||||
|
Assert.Empty(generator.Generate(first.Center.Add(new(first.RadiusMeters*2,0,0),a.CellSize).Sector).Stars);
|
||||||
|
}
|
||||||
|
[Fact]
|
||||||
|
public void JourneyVisitsBothGalaxiesLandsAtEyeHeightAndDetectsFrameSpikes()
|
||||||
|
{
|
||||||
|
var galaxies=Create();
|
||||||
|
var home=new UniverseGenerator(123).Generate(new());
|
||||||
|
var earth=home.Planets[0] with {Id="earth"};home=home with {Planets=[earth]};
|
||||||
|
var body=UniversePosition.AtCellCenter(home.Coordinate,galaxies.CellSize).Add(earth.PositionMeters,galaxies.CellSize);
|
||||||
|
var radial=new DVector3(1,.2,.1).Normalized();var start=body.Add(radial*earth.RadiusMeters*3,galaxies.CellSize);
|
||||||
|
var generator=new UniverseGenerator(galaxies.Seed,galaxies:galaxies);
|
||||||
|
var journey=new GalaxyJourneyBenchmark(galaxies,generator,home,earth,start,-radial,new(0,1,0));
|
||||||
|
var camera=new FlyCamera(start,galaxies.CellSize);var streamer=new UniverseStreamer(generator);var visitedTarget=false;
|
||||||
|
while(journey.IsRunning)
|
||||||
|
{
|
||||||
|
journey.Update(.25,camera);
|
||||||
|
var loaded=streamer.Load(camera.Position.Sector);
|
||||||
|
if(loaded.ActiveSystem.Coordinate==journey.TargetSystem.Coordinate && loaded.ActiveSystem.Planets.Count>0)visitedTarget=true;
|
||||||
|
if(journey.Phase=="GROUND WALK")
|
||||||
|
{
|
||||||
|
var targetBody=UniversePosition.AtCellCenter(journey.TargetSystem.Coordinate,galaxies.CellSize).Add(journey.TargetPlanet.PositionMeters,galaxies.CellSize);
|
||||||
|
var delta=camera.Position.RelativeTo(targetBody,galaxies.CellSize);
|
||||||
|
Assert.InRange(delta.Length-TerrainHeight.Sample(journey.TargetPlanet,delta),1.79,1.81);
|
||||||
|
}
|
||||||
|
journey.RecordFrame(journey.ElapsedSeconds is >30 and <31?.1:.01);
|
||||||
|
}
|
||||||
|
Assert.True(visitedTarget);Assert.Equal(7,journey.VisitedPhases.Count);Assert.Equal(start,camera.Position);
|
||||||
|
Assert.False(journey.PerformancePassed);
|
||||||
|
Assert.Contains(journey.GraphSamples,fps=>fps<20);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,11 +3,44 @@ using Universe.Core.Generation;
|
|||||||
using Universe.Core.Math;
|
using Universe.Core.Math;
|
||||||
using Universe.Core.Physics;
|
using Universe.Core.Physics;
|
||||||
using Universe.Core.World;
|
using Universe.Core.World;
|
||||||
|
using Universe.Core.Configuration;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Universe.Tests;
|
namespace Universe.Tests;
|
||||||
|
|
||||||
public sealed class OceanTests
|
public sealed class OceanTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public void PlanetJsonRoundTripsReflectionQualityAndRejectsInvalidGpuBudgets()
|
||||||
|
{
|
||||||
|
var quality = new OceanReflectionSettings { Enabled = true, ReflectSky = false, Strength = .7, ResolutionScale = .75,
|
||||||
|
MaxSteps = 96, StridePixels = 3, MaxDistanceMeters = 8000, MaxSurfaceDistanceMeters = 6000,
|
||||||
|
ThicknessMeters = 2.5, NormalBiasMeters = .2, EdgeFade = .1 };
|
||||||
|
var path = Path.Combine(Path.GetTempPath(), $"universe-ocean-{Guid.NewGuid():N}.json");
|
||||||
|
KnownSolarSystemCatalog Load(OceanReflectionSettings? settings)
|
||||||
|
{
|
||||||
|
var definition = new SolarSystemDocument { Systems = [new SolarSystemDefinition
|
||||||
|
{
|
||||||
|
Id = "water-system", Name = "Water test", Sector = new(0, 0, 0),
|
||||||
|
Stars = [new StarDefinition { Id = "sun", Name = "Sun", RadiusMeters = 695700000 }],
|
||||||
|
Planets = [new PlanetDefinition { Id = "ocean", Name = "Ocean", RadiusMeters = 6371000,
|
||||||
|
Orbit = new() { SemiMajorAxisAu = 1 }, Ocean = new() { MaskFromSurfaceTexture = false, ScreenSpaceReflections = settings! } }]
|
||||||
|
}] };
|
||||||
|
File.WriteAllText(path, JsonSerializer.Serialize(definition));
|
||||||
|
return KnownSolarSystemCatalog.Load(path);
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parsed = Load(quality).GetById("water-system").Planets.Single().Ocean!.ScreenSpaceReflections;
|
||||||
|
Assert.Equal(quality, parsed);
|
||||||
|
foreach (var invalid in new OceanReflectionSettings?[] { quality with { MaxSteps = 0 }, quality with { MaxSteps = 129 },
|
||||||
|
quality with { ResolutionScale = 0 }, quality with { ThicknessMeters = 0 }, quality with { NormalBiasMeters = -1 }, null })
|
||||||
|
{
|
||||||
|
Assert.Throws<InvalidDataException>(() => Load(invalid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally { File.Delete(path); }
|
||||||
|
}
|
||||||
[Fact]
|
[Fact]
|
||||||
public void WaterMaskDistinguishesBlueWaterFromLandAndIce()
|
public void WaterMaskDistinguishesBlueWaterFromLandAndIce()
|
||||||
{
|
{
|
||||||
|
|||||||
42
tests/Universe.Tests/StarMotionTests.cs
Normal file
42
tests/Universe.Tests/StarMotionTests.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using Universe.App.Runtime;
|
||||||
|
using Universe.Core.Generation;
|
||||||
|
using Universe.Core.Math;
|
||||||
|
using Universe.Core.World;
|
||||||
|
|
||||||
|
namespace Universe.Tests;
|
||||||
|
|
||||||
|
public sealed class StarMotionTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void HistoryRetainsSmallMovementAtLargeCoordinatesAndResetsOnCameraCuts()
|
||||||
|
{
|
||||||
|
var cell=UniverseScale.DefaultCellSizeMeters;
|
||||||
|
var position=UniversePosition.AtCellCenter(new(600_000_000_000_000_000,-500,200),cell);
|
||||||
|
var camera=new FlyCamera(position,cell);
|
||||||
|
var history=new StarMotionHistory();var settings=new StarMotionBlurSettings();
|
||||||
|
history.Advance(camera,1440,900,1d/60,settings);
|
||||||
|
Assert.False(history.Valid);
|
||||||
|
camera.SetPlayerPose(position.Add(new(.125,.25,.5),cell),camera.Forward,camera.Up);
|
||||||
|
history.Advance(camera,1440,900,1d/60,settings);
|
||||||
|
Assert.True(history.Valid);
|
||||||
|
Assert.Equal(new DVector3(.125,.25,.5),history.Current.Position.RelativeTo(history.Previous.Position,cell));
|
||||||
|
camera.SetPlayerPose(position.Add(new(1e20,0,0),cell),camera.Forward,camera.Up,resetMotionHistory:true);
|
||||||
|
history.Advance(camera,1440,900,1d/60,settings);
|
||||||
|
Assert.False(history.Valid);
|
||||||
|
Assert.Equal(history.Current,history.Previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PauseResizeAndLongFramesCannotSmearUnrelatedFramesTogether()
|
||||||
|
{
|
||||||
|
var camera=new FlyCamera(UniversePosition.AtCellCenter(new(),UniverseScale.DefaultCellSizeMeters),UniverseScale.DefaultCellSizeMeters);
|
||||||
|
var history=new StarMotionHistory();var settings=new StarMotionBlurSettings();
|
||||||
|
history.Advance(camera,1440,900,1d/60,settings);
|
||||||
|
history.Advance(camera,1440,900,1d/60,settings);Assert.True(history.Valid);
|
||||||
|
history.Advance(camera,1920,1080,1d/60,settings);Assert.False(history.Valid);
|
||||||
|
history.Advance(camera,1920,1080,0,settings);Assert.False(history.Valid);
|
||||||
|
history.Advance(camera,1920,1080,.5,settings);Assert.False(history.Valid);
|
||||||
|
history.Advance(camera,1920,1080,1d/60,settings);Assert.True(history.Valid);
|
||||||
|
history.Advance(camera,1920,1080,1d/60,settings with {Enabled=false});Assert.False(history.Valid);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,9 @@
|
|||||||
<ProjectReference Include="..\..\src\Universe.Core\Universe.Core.csproj" />
|
<ProjectReference Include="..\..\src\Universe.Core\Universe.Core.csproj" />
|
||||||
<ProjectReference Include="..\..\src\Universe.App\Universe.App.csproj" />
|
<ProjectReference Include="..\..\src\Universe.App\Universe.App.csproj" />
|
||||||
<Content Include="..\..\config.json" Link="config.json" CopyToOutputDirectory="PreserveNewest" />
|
<Content Include="..\..\config.json" Link="config.json" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
<Content Include="..\..\KnownGalaxies.json" Link="KnownGalaxies.json" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
<Content Include="..\..\Content\Galaxies\*" Link="Content\Galaxies\%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
<Content Include="..\..\Content\Nebulae\*" Link="Content\Nebulae\%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
|
||||||
<Content Include="..\..\KnownSolarSystem\**\*" Link="KnownSolarSystem\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
|
<Content Include="..\..\KnownSolarSystem\**\*" Link="KnownSolarSystem\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user