tidssystem

This commit is contained in:
2026-11-01 16:52:32 +01:00
parent 9d44d14cc9
commit a7207086b2
24 changed files with 849 additions and 174 deletions

View File

@@ -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:

View File

@@ -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,
@@ -203,17 +213,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 +276,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 +298,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 +317,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 +344,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 +359,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 +409,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 +424,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 +630,10 @@
0.76, 0.76,
0.69 0.69
] ]
} },
"rotationPeriodHours": 10.65624,
"massKg": 5.68317e+26,
"poleLongitudeDegrees": 0
}, },
{ {
"id": "uranus", "id": "uranus",
@@ -601,12 +645,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 +695,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 +710,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 +760,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"
} }
] ]
} }

View File

@@ -57,7 +57,19 @@ 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 ## Galaxer och nebulosor

View File

@@ -46,5 +46,9 @@
"longitudeDegrees": 20, "longitudeDegrees": 20,
"latitudeDegrees": 20, "latitudeDegrees": 20,
"speedMetersPerSecond": 200000 "speedMetersPerSecond": 200000
},
"simulation": {
"startUtc": "2000-01-01T12:00:00Z",
"timeScale": 1
} }
} }

View File

@@ -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) };
} }
} }

View File

@@ -13,6 +13,50 @@ 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) public static void RunStarTrails(GL gl,StarMotionBlurSettings settings,int width,int height)
{ {
using var trails=new StarTrailRenderer(gl,settings with {Enabled=true}); using var trails=new StarTrailRenderer(gl,settings with {Enabled=true});

View File

@@ -49,7 +49,18 @@ internal sealed class PauseMenu : IDisposable
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>();
@@ -84,6 +95,26 @@ internal sealed class PauseMenu : IDisposable
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 + 250*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();

View File

@@ -10,7 +10,8 @@ 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)>();
@@ -44,6 +45,8 @@ internal sealed class PerformanceOverlay : IDisposable
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;

View File

@@ -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;

View File

@@ -66,6 +66,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;
@@ -260,7 +261,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;
@@ -355,20 +356,21 @@ 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);
} }
@@ -490,14 +492,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)

View File

@@ -12,6 +12,8 @@ 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)
@@ -336,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)

View File

@@ -28,7 +28,7 @@ internal sealed class GalaxyJourneyBenchmark : IBenchmarkSession
public object? RenderEnvironment { get; set; } public object? RenderEnvironment { get; set; }
public GalaxyJourneyBenchmark(GalaxyUniverse galaxies, UniverseGenerator generator, StarSystemData homeSystem, PlanetData earth, public GalaxyJourneyBenchmark(GalaxyUniverse galaxies, UniverseGenerator generator, StarSystemData homeSystem, PlanetData earth,
UniversePosition home, DVector3 homeForward, DVector3 homeUp) UniversePosition home, DVector3 homeForward, DVector3 homeUp, double simulationSeconds = 0)
{ {
_galaxies = galaxies; _home = home; _homeForward = homeForward; _homeUp = homeUp; _galaxies = galaxies; _home = home; _homeForward = homeForward; _homeUp = homeUp;
_homeBody = UniversePosition.AtCellCenter(homeSystem.Coordinate, galaxies.CellSize).Add(earth.PositionMeters, galaxies.CellSize); _homeBody = UniversePosition.AtCellCenter(homeSystem.Coordinate, galaxies.CellSize).Add(earth.PositionMeters, galaxies.CellSize);
@@ -37,7 +37,7 @@ internal sealed class GalaxyJourneyBenchmark : IBenchmarkSession
HomeGalaxy = galaxies.GalaxyAt(home); HomeGalaxy = galaxies.GalaxyAt(home);
var region = HomeGalaxy.Region; var region = HomeGalaxy.Region;
TargetGalaxy = galaxies.Get(new(checked(region.X + 1), region.Y, region.Z)); TargetGalaxy = galaxies.Get(new(checked(region.X + 1), region.Y, region.Z));
TargetSystem = generator.Generate(galaxies.SelectSystem(TargetGalaxy)); 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(); 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); _targetBody = UniversePosition.AtCellCenter(TargetSystem.Coordinate, galaxies.CellSize).Add(TargetPlanet.PositionMeters, galaxies.CellSize);
// Exercise the illuminated terrain/shadow path, rather than accidentally // Exercise the illuminated terrain/shadow path, rather than accidentally

View File

@@ -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;

View File

@@ -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;
@@ -20,6 +21,7 @@ public sealed class UniverseApplication : IDisposable
private readonly UniverseStreamer _streamer; private readonly UniverseStreamer _streamer;
private readonly GalaxyUniverse _galaxies; private readonly GalaxyUniverse _galaxies;
private readonly StarMotionBlurSettings _starMotionBlur; 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;
@@ -52,6 +54,7 @@ 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; _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));
@@ -60,8 +63,9 @@ public sealed class UniverseApplication : IDisposable
Path.GetFullPath(Path.Combine(Path.GetDirectoryName(configPath)!, configuration.Galaxies.KnownGalaxiesFile))); Path.GetFullPath(Path.Combine(Path.GetDirectoryName(configPath)!, configuration.Galaxies.KnownGalaxiesFile)));
_generator = new(configuration.Seed, configuration.CellSizeMeters, catalog, configuration.BlackHoles, _galaxies); _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 = _generator.Generate(catalog.GetById("sol").Coordinate); _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;
@@ -79,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" or "--black-hole-report" or "--galaxy-journey-report" or "--star-trails-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,
@@ -100,6 +104,11 @@ 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, _galaxies, _starMotionBlur); _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")) if (Environment.GetCommandLineArgs().Contains("--star-trails-report"))
{ {
GraphicsReport.RunStarTrails(gl,_starMotionBlur,_window.FramebufferSize.X,_window.FramebufferSize.Y); GraphicsReport.RunStarTrails(gl,_starMotionBlur,_window.FramebufferSize.X,_window.FramebufferSize.Y);
@@ -164,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);
@@ -216,12 +235,13 @@ public sealed class UniverseApplication : IDisposable
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; _benchmarkNeedsInitialFrame = false;
} }
@@ -252,6 +272,8 @@ public sealed class UniverseApplication : IDisposable
if (_menuOpen) if (_menuOpen)
{ {
if (key == Key.Escape) SetMenu(false); if (key == Key.Escape) SetMenu(false);
else if (_debugMode && key is Key.Left or Key.Right)
_clock.TimeScale += (key == Key.Right ? 1 : -1) * (IsDown(Key.ShiftLeft) || IsDown(Key.ShiftRight) ? 10 : 1);
else if (key == Key.Up) _menuSelection = (_menuSelection + 6) % 7; else if (key == Key.Up) _menuSelection = (_menuSelection + 6) % 7;
else if (key == Key.Down) _menuSelection = (_menuSelection + 1) % 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();
@@ -337,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;
@@ -363,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(); }
} }
@@ -390,7 +417,8 @@ public sealed class UniverseApplication : IDisposable
_debugMode = false; _player = null; _debugMode = false; _player = null;
_benchmark = CreateGalaxyJourney(); _benchmark = CreateGalaxyJourney();
_benchmarkNeedsInitialFrame = true; _benchmarkNeedsInitialFrame = true;
_camera.SetPlayerPose(_homeCameraPosition, _homeCameraForward, _homeCameraUp, resetMotionHistory: true); var home = CurrentHomeView();
_camera.SetPlayerPose(home.Position, home.Forward, home.Up, resetMotionHistory: true);
UpdateNeighborhood(); SetMenu(false); return; UpdateNeighborhood(); SetMenu(false); return;
} }
if (_menuSelection is 3 or 4) if (_menuSelection is 3 or 4)
@@ -417,8 +445,9 @@ public sealed class UniverseApplication : IDisposable
private void StartBenchmark(bool unknownPlanet) private void StartBenchmark(bool unknownPlanet)
{ {
_camera.RestoreDefaultFieldOfView(); _camera.RestoreDefaultFieldOfView();
var system = _homeSystem; var home = CurrentHomeView();
var planet = _homeEarth; 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)
@@ -441,7 +470,7 @@ public sealed class UniverseApplication : IDisposable
_debugMode = false; _debugMode = false;
_player = null; _player = null;
_benchmark = new BenchmarkRunner(system, planet, _generator.CellSizeMeters, _camera, _benchmark = new BenchmarkRunner(system, planet, _generator.CellSizeMeters, _camera,
_homeCameraPosition, _homeCameraForward, _homeCameraUp); home.Position, home.Forward, home.Up);
_benchmarkNeedsInitialFrame = true; _benchmarkNeedsInitialFrame = true;
UpdateNeighborhood(); UpdateNeighborhood();
SetMenu(false); SetMenu(false);
@@ -450,14 +479,27 @@ public sealed class UniverseApplication : IDisposable
private GalaxyJourneyBenchmark CreateGalaxyJourney() private GalaxyJourneyBenchmark CreateGalaxyJourney()
{ {
_camera.RestoreDefaultFieldOfView(); _camera.RestoreDefaultFieldOfView();
var journey = new GalaxyJourneyBenchmark(_galaxies, _generator, _homeSystem, _homeEarth, var home = CurrentHomeView();
_homeCameraPosition, _homeCameraForward, _homeCameraUp); 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 }; 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 _homeSystem.Planets) _renderer?.PreparePlanet(_homeSystem, planet);
foreach (var planet in journey.TargetSystem.Planets) _renderer?.PreparePlanet(journey.TargetSystem, planet); foreach (var planet in journey.TargetSystem.Planets) _renderer?.PreparePlanet(journey.TargetSystem, planet);
return journey; 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();

View File

@@ -26,6 +26,7 @@ public sealed record EngineConfiguration
public BlackHoleGenerationSettings BlackHoles { get; init; } = new(); public BlackHoleGenerationSettings BlackHoles { get; init; } = new();
public GalaxySettings Galaxies { get; init; } = new(); public GalaxySettings Galaxies { get; init; } = new();
public StarMotionBlurSettings StarMotionBlur { 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)
@@ -45,6 +46,7 @@ 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.BlackHoles ?? throw new InvalidDataException("config.json: blackHoles cannot be null.")).Validate();
(settings.Galaxies ?? throw new InvalidDataException("config.json: galaxies 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(); (settings.StarMotionBlur ?? throw new InvalidDataException("config.json: starMotionBlur cannot be null.")).Validate();

View File

@@ -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)

View File

@@ -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;

View File

@@ -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,6 +14,7 @@ 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 BlackHoleData? BlackHole { get; init; }
public bool IsBlackHole => BlackHole is not null; public bool IsBlackHole => BlackHole is not null;
@@ -33,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;
@@ -65,6 +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? GalaxyId { get; init; }
public string? NebulaId { get; init; } public string? NebulaId { get; init; }
} }

View File

@@ -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);
} }
} }

View File

@@ -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;
@@ -68,7 +69,7 @@ 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) if (hasBlackHole)
@@ -77,7 +78,7 @@ public sealed class UniverseGenerator
var hole = new BlackHoleData(mass, UnitVector(ref holeRandom), BlackHoles.DiskOuterRadiusInHorizons, var hole = new BlackHoleData(mass, UnitVector(ref holeRandom), BlackHoles.DiskOuterRadiusInHorizons,
BlackHoles.DiskBrightness, BlackHoles.RaySteps); BlackHoles.DiskBrightness, BlackHoles.RaySteps);
stars[0] = stars[0] with { Name = $"{coordinate.X}:{coordinate.Y}:{coordinate.Z} Black Hole", RadiusMeters = hole.HorizonRadiusMeters, 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 }; 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);
@@ -113,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)
}); });
@@ -132,6 +136,9 @@ 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++;
@@ -165,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",

View 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 };
}
}

View 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;
}
}

View File

@@ -1,4 +1,5 @@
using Universe.Core.Generation; using Universe.Core.Generation;
using Universe.Core.Physics;
namespace Universe.Core.World; namespace Universe.Core.World;
@@ -10,6 +11,10 @@ public sealed class UniverseStreamer(UniverseGenerator generator)
private readonly Dictionary<SectorCoordinate, StarSystemData> _cache = []; private readonly Dictionary<SectorCoordinate, StarSystemData> _cache = [];
private SectorCoordinate? _nearBlock; private SectorCoordinate? _nearBlock;
private IReadOnlyList<StarSystemData> _nearSystems = []; 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) public (StarSystemData System, StarData Hole)? FindNearestBlackHole(UniversePosition position)
{ {
@@ -67,7 +72,7 @@ public sealed class UniverseStreamer(UniverseGenerator generator)
var keep = _nearSystems.Select(s => s.Coordinate).Append(activeCoordinate).ToHashSet(); var keep = _nearSystems.Select(s => s.Coordinate).Append(activeCoordinate).ToHashSet();
foreach (var key in _cache.Keys.ToArray()) if (!keep.Contains(key)) _cache.Remove(key); foreach (var key in _cache.Keys.ToArray()) if (!keep.Contains(key)) _cache.Remove(key);
} }
return new(active, _nearSystems.Where(s => s.Coordinate != activeCoordinate).ToArray()); 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++)
@@ -88,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)

View 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);
}
}