diff --git a/.gitignore b/.gitignore index eba1864..1b2476a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ TestResults/ *.log /artifacts +**/node_modules/ +/Content/Vegitations/*/source/ +*.partial diff --git a/Content/README.md b/Content/README.md index 170a11e..a26a568 100644 --- a/Content/README.md +++ b/Content/README.md @@ -1,5 +1,7 @@ # Markmaterial och höjddata +Vegetationsmodeller och skogs-/snö-/torrgräsmaterial finns nu i `Vegitations/`. Se `Vegitations/README.md` för färgregler, planetinställningar, CC0-källor, import och skogsbenchmark. Den befintliga `Vegetations/`-mappen med markmaterial behåller sitt namn. + `materials.json` anger tillgängliga material och seedade urvalslistor för fasta planeter. Varje material har färg (`color`), OpenGL-normal (`normal`) och ytjämnhet (`roughness`). Sökvägarna är relativa till `Content/`. Materialen är från [Poly Haven](https://polyhaven.com/textures) och är [CC0](https://polyhaven.com/license): - [Leafy Grass](https://polyhaven.com/a/leafy_grass), Charlotte Baglioni, `Vegetations/leafy_grass/`. diff --git a/Content/Vegitations/README.md b/Content/Vegitations/README.md new file mode 100644 index 0000000..812771a --- /dev/null +++ b/Content/Vegitations/README.md @@ -0,0 +1,100 @@ +# Färgstyrd vegetation + +`vegetation.json` är den gemensamma katalogen. Sökvägar till modeller utgår från denna mapp. Den äldre mappen `Content/Vegetations/` med markmaterial finns kvar. + +## Tillgångar och offlineverktyg + +De tio modellpaketen kommer från [Poly Haven](https://polyhaven.com), med [CC0-licens](https://polyhaven.com/license). `sources.json` innehåller ursprungliga nedladdningsadresser, filstorlekar och MD5-kontrollsummor. Startpaketet omfattar 1 697 MiB originaldata inklusive 2K-material för skogsmark, torrt gräs och snö. Nedladdaren hämtar bara filer som saknas eller har fel kontrollsumma och stoppar över 2 GB nedladdning. + +Förberedda modeller, alphakort och avståndsbilder ingår i spelets innehåll. Originalen och mellansteget `source/bake-source.glb` behövs bara vid import, ignoreras av Git och kopieras inte till distributionen. Spelet behöver inte nätåtkomst, Node eller Python. + +Återskapa resurser från projektroten med Python 3, Node och .NET 10: + +```powershell +python tools/vegetation/download.py +npm ci --prefix tools/vegetation +node --max-old-space-size=16384 tools/vegetation/prepare.mjs --source-only pine_tree_01 fir_tree_01 tree_small_02 fir_sapling_medium +dotnet build src/Universe.App -c Release +./src/Universe.App/bin/Release/net10.0/Universe.App.exe --prepare-vegetation --bake-foliage +node --max-old-space-size=16384 tools/vegetation/prepare.mjs +dotnet build src/Universe.App -c Release +./src/Universe.App/bin/Release/net10.0/Universe.App.exe --prepare-vegetation --force +dotnet build src/Universe.App -c Release +``` + +Importen använder glTF Transform och Meshoptimizer. Tungt barr-/lövverk bakas från full geometri till 24 rumsliga grupper med tre alphakort per grupp. Stammar och grenar behåller geometri. GLB-scenerna `LOD0`, `LOD1`, `LOD2` delar material. Startpaketets träd använder cirka 12 000 / 2 000 / 350 trianglar **per trädvariant**, under planens övre budget på 60 000 / 12 000 / 2 000. `preparation.json` redovisar summerade antal över alla varianter. Slutligen bakas 16 riktningar och tre elevationsvinklar per variant till impostorer med färg och normaler. Gräsets torra variant heter `dry.glb`. + +## Färgregler + +`matchColorRgb` anger kartfärgen för placering, inte modellens färg. Avståndet är `100 * length(sRGB - target) / (255 * sqrt(3))`, med mjuk uttoning under toleransens yttersta 20 %. RGB-värden anges 0–255. Överlappande biomvikter normaliseras och används också av markshadern. Katalogens fyra biomer ligger i ordningen skog, torr mark, sand och snö. + +Varje växt anger `id`, `model`, `type` (`tree`, `shrub`, `grass`, `fern`), `matchColorRgb`, `colorTolerancePercent`, `spawnWeight`, `scaleRange`, `altitudeMeters`, `absoluteLatitudeDegrees`, `maxSlopeDegrees`, `minimumSpacingMeters`, `windStrength` och valfri `trunkCollision` med `radiusMeters` och `heightMeters`. Material och växtfärger kommer från GLB-filen. Arter väljs viktat inom respektive skikts täthetsbudget; fler arter ökar inte automatiskt antalet växter. + +Valfri `lodMeters` på en träddefinition ersätter planetens tre meshgränser. `distanceMeters` kan förkorta artens räckvidd. Planetens `depthPrepass` aktiverar ett extra djupsteg före terrängen. Det döljer dyr terrängskuggning bakom lövverk och låter endast de främsta löven få full ljusberäkning. Standard är av: mätningen på Jorden visade att steget kostade mer vegetationstid än det sparade i terrängen. Dess GPU-tid ingår i vegetationsbudgeten och redovisas också separat i debugpanelen när det aktiveras. + +Vatten, snö och olämplig höjd/lutning filtreras bort. Höjdintervall räknas från planetens referensradie, i projektets höjddata. Jordens globala höjdkarta har höga värden även över många gröna områden, så startkatalogen tillåter träd upp till 6 000 m. Det är ingen exakt karta över jordens verkliga skogar. + +## Inställningar per planet + +Ange exempelvis i planetens JSON: + +```json +"vegetation": { + "enabled": true, + "densityMultiplier": 1, + "allowedPlants": ["pine_tree_01", "fir_tree_01", "fern_02"], + "affectGroundMaterials": true, + "treeDensityPerHectare": 600, + "shrubDensityPerHectare": 350, + "groundDensityPerHectare": 20000, + "forestScaleMeters": 450, + "treeLodMeters": [40, 150, 400], + "treeDistanceMeters": 2000, + "shrubDistanceMeters": 150, + "grassDistanceMeters": 80, + "uploadBudgetMilliseconds": 1, + "maximumCachedTiles": 2048, + "maximumInstances": 650000, + "shadowResolution": 2048, + "shadowDistancesMeters": [100, 400], + "shadowSoftness": 1, + "shadowCascadeBlend": 0.2, + "shadowBiasMeters": 0.035, + "collisionDistanceMeters": 64 +} +``` + +Utelämnad `allowedPlants` tillåter hela katalogen. Valfri `biomeTexture` anger en separat kartbild relativt `KnownSolarSystem/`; annars används `surfaceTexture`. Utan färgkarta används seedad proceduriell biomfördelning. Jorden är uttryckligen aktiverad. Atmosfäriska planeter av typen `terrestrial` får standardprofilen; andra fasta planeter kan aktiveras explicit. Gasplaneter tillåts inte. Proceduriella jordliknande planeter får också en standardprofil. + +Jordens startprofil använder `densityMultiplier: 0.7`, motsvarande nominellt 420 träd, 245 buskar och 14 000 markväxter per hektar före biom-, täthetsbrus- och terrängfiltrering. Sätt multiplikatorn till 1 för den tätare grundprofilen. + +Placeringarna bestäms av planet-ID, seed och fasta kubsfärsrutor, oberoende av terrängens LOD och planetens rotation. Trädrutor är cirka 256 m, buskrutor 128 m och markväxtrutor 32 m för att slippa generera stora mängder gräs långt bort. En begränsad arbetspool delas med terrängen. Inaktuella jobb avbryts. Resurs- och avståndsinstansdata laddas upp stegvis med den angivna tidsbudgeten; ett enskilt drivrutinsanrop kan inte avbrytas mitt i en uppladdning. + +Avståndsinstanser byggs i bakgrunden och använder två uppsättningar återanvända CPU-/GPU-buffertar. Terrängcachen behåller inaktiva rutor i upp till 60 sekunder, med en budget på 16 384 rutor per planet. De äldsta överflödiga rutorna frigörs stegvis, så korta utflykter till omloppsbana inte omedelbart tvingar fram samma terräng igen. + +Vegetationscachen följer spelarens position och täcker alla riktningar. En huvudvridning byter därför inte ut rutor eller startar nya vegetationsjobb. Standardgränsen är 2 048 rutor och fortfarande högst 650 000 instanser; detta behåller mer vegetation i minnet än den tidigare gränsen på 640 rutor. Små kameravridningar återanvänder instansbuffertarna med en konservativ marginal i synfältet. Skuggornas instanslistor uppdateras vid förflyttning eller nytt innehåll. Terrängrutor delar sin indextopologi och skriver vertexdata direkt till färdiga arrayer för att minska tillfälliga allokeringar. Detaljmaterial för avlägsna himlakroppar laddas först inom materialets synliga avstånd, så månens marktexturer inte läses in när man tittar runt på Jorden. + +Vind följer simulationstiden. Alphamippar bevarar täckningen. Träd växlar med dither mellan tre mesh-LOD och därefter flervyade impostorer. Två lokala skuggkaskader stöder upp till två solar. Vegetationen ritas före havet och kan därför speglas av befintlig SSR när den syns i scenbilden. FPS-spelaren sveps mot stamkapslar och glider längs dem; fri kamera, gräs, buskar och löv påverkas inte. + +Vegetationen ritas även före terrängen. Dess färdiga djupbild används för att undvika full terrängskuggning bakom stammar och löv, utan ett extra geometristeg. Separat `depthPrepass` är ett ytterligare, valfritt steg för lövens egen överlappning. + +## Benchmark och felsökning + +Vegetationsskuggor filtreras med bilinära djupjämförelser på GPU:n. Marken använder ett viktat 5×5-filter med nio texturhämtningar; löv använder ett 3×3-filter med fyra hämtningar. Ingen slumpmässig sampling används. `shadowSoftness` (0–1) blandar mellan en bilinjär djupjämförelse och det mjuka filtret; standard är 1. `shadowCascadeBlend` (0–0,4, standard 0,2) anger hur stor del av närskuggans räckvidd som blandas med fjärrskuggan. `shadowBiasMeters` (0,001–0,25, standard 0,035) är djupmarginalen i meter och är oberoende av kaskadens räckvidd. Kartorna centreras på spelarens position och snäpps till texlar i dubbelprecision; att vrida huvudet flyttar inte kartorna. + +`Universe.App.exe --shadow-report` verifierar produktionsfiltret mot en separat CPU-referens, mjukare skuggkanter, löv, två solar och kontinuerliga kaskadövergångar. Testbilder sparas i `graphics-report/shadows/`. + +P-menyn innehåller **BENCHMARK FOREST**: omloppsvy, mjuk landning till 1,8 m, promenad, skogsbryn och uppstigning. Därefter återställs Jordvyn. Resultat med FPS-graf samt CSV/JSON sparas i `benchmark-results/forest-*` bredvid appen. Debugpanelen visar vegetation, vegetationsskuggor, CPU-/uppladdningstid, inskickade instanser/trianglar, LOD och väntande rutor. + +`Universe.App.exe --lookaround-report` står still i skogen på Jorden och mäter en stillastående vy följd av två hela kameravarv i 1440 × 900. Efter uppvärmningen sparas bildrutetider, vegetationsgenerering, cacheändringar, terränguppladdningar, minnesallokeringar och GC-räkningar i `graphics-report/lookaround/`. GPU:n synkroniseras per bildruta. Lägg till `--profile-passes` för att lokalisera långsamma pass; den extra synkroniseringen påverkar då tiderna. + +`Universe.App.exe --vegetation-report` kör samma rutt i 2560 × 1440 utan VSync, med vegetation av/på och kall/varm vegetationscache. Initial modell-/texturinläsning samt uppvärmning av shaders och terräng sker före mätningen. Kall körning tömmer vegetationsrutorna och deras instansbuffertar; varm körning återanvänder dem från föregående resa. Rapporten synkroniserar GPU:n per bildruta och sparar bilder, FPS-grafer, bildrutetider, statistik och PASS/FAIL i `graphics-report/vegetation/`. `--preview-only` avslutar efter bildkontrollen. Gränserna är 60 FPS i medel, 55 FPS i 1 %-låg, högst 50 ms och vegetation inklusive skuggor högst 5 ms vid 99:e percentilen. Hela scenens kostnad ingår i FPS; rapporten visar missade gränser i stället för att dölja dem. + +Senaste kontrollen på RTX 5080, 2560 × 1440 och Jordens starttäthet 0,7: + +| Vegetationscache | Medel-FPS | 1 %-låg FPS | Max bildruta | Vegetation + skuggor, GPU p99 | +| --- | ---: | ---: | ---: | ---: | +| Kall | 102,7 | 56,5 | 19,03 ms | 4,23 ms | +| Varm | 99,2 | 48,1 | 23,11 ms | 4,23 ms | + +Varm körning klarar ännu inte gränsen 55 FPS för 1 %-låg; hela prestandamatrisen rapporteras därför som FAIL. Medel-FPS, vegetationsbudgeten och gränsen 50 ms klaras. Mätvärden varierar med vy och belastning. Grafikrapporten kontrollerar också två solar, giltiga OpenGL-resultat och minst 99 % närliggande yttäckning i djupbilden under spelarens fötter efter en orbital resa; senaste kontrollen gav 100 % täckning. diff --git a/Content/Vegitations/fern_02/prepared/impostor-0-color.png b/Content/Vegitations/fern_02/prepared/impostor-0-color.png new file mode 100644 index 0000000..a9b3ce3 Binary files /dev/null and b/Content/Vegitations/fern_02/prepared/impostor-0-color.png differ diff --git a/Content/Vegitations/fern_02/prepared/impostor-0-normal.png b/Content/Vegitations/fern_02/prepared/impostor-0-normal.png new file mode 100644 index 0000000..61fbae1 Binary files /dev/null and b/Content/Vegitations/fern_02/prepared/impostor-0-normal.png differ diff --git a/Content/Vegitations/fern_02/prepared/impostor-1-color.png b/Content/Vegitations/fern_02/prepared/impostor-1-color.png new file mode 100644 index 0000000..a832b80 Binary files /dev/null and b/Content/Vegitations/fern_02/prepared/impostor-1-color.png differ diff --git a/Content/Vegitations/fern_02/prepared/impostor-1-normal.png b/Content/Vegitations/fern_02/prepared/impostor-1-normal.png new file mode 100644 index 0000000..3ef32e1 Binary files /dev/null and b/Content/Vegitations/fern_02/prepared/impostor-1-normal.png differ diff --git a/Content/Vegitations/fern_02/prepared/impostor-2-color.png b/Content/Vegitations/fern_02/prepared/impostor-2-color.png new file mode 100644 index 0000000..21dab1d Binary files /dev/null and b/Content/Vegitations/fern_02/prepared/impostor-2-color.png differ diff --git a/Content/Vegitations/fern_02/prepared/impostor-2-normal.png b/Content/Vegitations/fern_02/prepared/impostor-2-normal.png new file mode 100644 index 0000000..6e3b9e3 Binary files /dev/null and b/Content/Vegitations/fern_02/prepared/impostor-2-normal.png differ diff --git a/Content/Vegitations/fern_02/prepared/impostors.json b/Content/Vegitations/fern_02/prepared/impostors.json new file mode 100644 index 0000000..ac441c1 --- /dev/null +++ b/Content/Vegitations/fern_02/prepared/impostors.json @@ -0,0 +1 @@ +{"Views":16,"Elevations":3,"Variants":[{"Center":[0,0.21384735,0],"Size":1.4935448},{"Center":[0,0.17451018,0],"Size":1.3013301},{"Center":[0,0.14340177,0],"Size":0.92420286}]} \ No newline at end of file diff --git a/Content/Vegitations/fern_02/prepared/model.glb b/Content/Vegitations/fern_02/prepared/model.glb new file mode 100644 index 0000000..6f8be7c Binary files /dev/null and b/Content/Vegitations/fern_02/prepared/model.glb differ diff --git a/Content/Vegitations/fern_02/prepared/preparation.json b/Content/Vegitations/fern_02/prepared/preparation.json new file mode 100644 index 0000000..def036b --- /dev/null +++ b/Content/Vegitations/fern_02/prepared/preparation.json @@ -0,0 +1,52 @@ +{ + "source": "https://polyhaven.com/a/fern_02", + "license": "CC0", + "budgets": [ + 2500, + 800, + 200 + ], + "triangles": [ + 5416, + 1946, + 486 + ], + "variants": [ + { + "min": [ + -0.49487370252609253, + 0, + -0.4468448758125305 + ], + "max": [ + 0.49487370252609253, + 0.427694708108902, + 0.4468448758125305 + ] + }, + { + "min": [ + -0.4371629059314728, + 0, + -0.38261282444000244 + ], + "max": [ + 0.4371629059314728, + 0.3490203619003296, + 0.38261282444000244 + ] + }, + { + "min": [ + -0.27597030997276306, + 0, + -0.30671054124832153 + ], + "max": [ + 0.27597030997276306, + 0.28680354356765747, + 0.30671054124832153 + ] + } + ] +} \ No newline at end of file diff --git a/Content/Vegitations/fern_02/prepared/stamp.json b/Content/Vegitations/fern_02/prepared/stamp.json new file mode 100644 index 0000000..68ed22b --- /dev/null +++ b/Content/Vegitations/fern_02/prepared/stamp.json @@ -0,0 +1 @@ +{"version":4,"cards":false,"files":["904c73900125e017f5e76ac1cdd906fe","ba8bbb9dc9ff55bdc081565cd926d776","7f7b46e813a3cb4811405191de0ecdce","c061e9255f7d36ae9ce0e92ef3e100b2","00936dff4661dc9fe1f2fbe6c000615a","ff18f7e8e869c58eac8f9c7d7c519f4a"]} \ No newline at end of file diff --git a/Content/Vegitations/fir_sapling_medium/prepared/cards-0-color.png b/Content/Vegitations/fir_sapling_medium/prepared/cards-0-color.png new file mode 100644 index 0000000..153be95 Binary files /dev/null and b/Content/Vegitations/fir_sapling_medium/prepared/cards-0-color.png differ diff --git a/Content/Vegitations/fir_sapling_medium/prepared/cards-0-normal.png b/Content/Vegitations/fir_sapling_medium/prepared/cards-0-normal.png new file mode 100644 index 0000000..91fdf43 Binary files /dev/null and b/Content/Vegitations/fir_sapling_medium/prepared/cards-0-normal.png differ diff --git a/Content/Vegitations/fir_sapling_medium/prepared/cards-1-color.png b/Content/Vegitations/fir_sapling_medium/prepared/cards-1-color.png new file mode 100644 index 0000000..3da6948 Binary files /dev/null and b/Content/Vegitations/fir_sapling_medium/prepared/cards-1-color.png differ diff --git a/Content/Vegitations/fir_sapling_medium/prepared/cards-1-normal.png b/Content/Vegitations/fir_sapling_medium/prepared/cards-1-normal.png new file mode 100644 index 0000000..eb7c60c Binary files /dev/null and b/Content/Vegitations/fir_sapling_medium/prepared/cards-1-normal.png differ diff --git a/Content/Vegitations/fir_sapling_medium/prepared/cards-2-color.png b/Content/Vegitations/fir_sapling_medium/prepared/cards-2-color.png new file mode 100644 index 0000000..9270b48 Binary files /dev/null and b/Content/Vegitations/fir_sapling_medium/prepared/cards-2-color.png differ diff --git a/Content/Vegitations/fir_sapling_medium/prepared/cards-2-normal.png b/Content/Vegitations/fir_sapling_medium/prepared/cards-2-normal.png new file mode 100644 index 0000000..640bd77 Binary files /dev/null and b/Content/Vegitations/fir_sapling_medium/prepared/cards-2-normal.png differ diff --git a/Content/Vegitations/fir_sapling_medium/prepared/cards.json b/Content/Vegitations/fir_sapling_medium/prepared/cards.json new file mode 100644 index 0000000..5ce1dab --- /dev/null +++ b/Content/Vegitations/fir_sapling_medium/prepared/cards.json @@ -0,0 +1 @@ +{"Variants":[[{"Center":[-1.2469723,0.7355272,-1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0,0.8888889,0.125,0.11111111]},{"Center":[-1.2469723,0.7355272,-1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.125,0.8888889,0.125,0.11111111]},{"Center":[-1.2469723,0.7355272,-1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.25,0.8888889,0.125,0.11111111]},{"Center":[1.2469723,0.7355272,-1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.375,0.8888889,0.125,0.11111111]},{"Center":[1.2469723,0.7355272,-1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.5,0.8888889,0.125,0.11111111]},{"Center":[1.2469723,0.7355272,-1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.625,0.8888889,0.125,0.11111111]},{"Center":[-1.2469723,0.7355272,1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.75,0.8888889,0.125,0.11111111]},{"Center":[-1.2469723,0.7355272,1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.875,0.8888889,0.125,0.11111111]},{"Center":[-1.2469723,0.7355272,1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0,0.7777778,0.125,0.11111111]},{"Center":[1.2469723,0.7355272,1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.125,0.7777778,0.125,0.11111111]},{"Center":[1.2469723,0.7355272,1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.25,0.7777778,0.125,0.11111111]},{"Center":[1.2469723,0.7355272,1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.375,0.7777778,0.125,0.11111111]},{"Center":[-1.2469723,2.2065816,-1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.5,0.7777778,0.125,0.11111111]},{"Center":[-1.2469723,2.2065816,-1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.625,0.7777778,0.125,0.11111111]},{"Center":[-1.2469723,2.2065816,-1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.75,0.7777778,0.125,0.11111111]},{"Center":[1.2469723,2.2065816,-1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.875,0.7777778,0.125,0.11111111]},{"Center":[1.2469723,2.2065816,-1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0,0.6666666,0.125,0.11111111]},{"Center":[1.2469723,2.2065816,-1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.125,0.6666666,0.125,0.11111111]},{"Center":[-1.2469723,2.2065816,1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.25,0.6666666,0.125,0.11111111]},{"Center":[-1.2469723,2.2065816,1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.375,0.6666666,0.125,0.11111111]},{"Center":[-1.2469723,2.2065816,1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.5,0.6666666,0.125,0.11111111]},{"Center":[1.2469723,2.2065816,1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.625,0.6666666,0.125,0.11111111]},{"Center":[1.2469723,2.2065816,1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.75,0.6666666,0.125,0.11111111]},{"Center":[1.2469723,2.2065816,1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.875,0.6666666,0.125,0.11111111]},{"Center":[-1.2469723,3.6776361,-1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0,0.5555556,0.125,0.11111111]},{"Center":[-1.2469723,3.6776361,-1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.125,0.5555556,0.125,0.11111111]},{"Center":[-1.2469723,3.6776361,-1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.25,0.5555556,0.125,0.11111111]},{"Center":[1.2469723,3.6776361,-1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.375,0.5555556,0.125,0.11111111]},{"Center":[1.2469723,3.6776361,-1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.5,0.5555556,0.125,0.11111111]},{"Center":[1.2469723,3.6776361,-1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.625,0.5555556,0.125,0.11111111]},{"Center":[-1.2469723,3.6776361,1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.75,0.5555556,0.125,0.11111111]},{"Center":[-1.2469723,3.6776361,1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.875,0.5555556,0.125,0.11111111]},{"Center":[-1.2469723,3.6776361,1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0,0.44444442,0.125,0.11111111]},{"Center":[1.2469723,3.6776361,1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.125,0.44444442,0.125,0.11111111]},{"Center":[1.2469723,3.6776361,1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.25,0.44444442,0.125,0.11111111]},{"Center":[1.2469723,3.6776361,1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.375,0.44444442,0.125,0.11111111]},{"Center":[-1.2469723,5.14869,-1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.5,0.44444442,0.125,0.11111111]},{"Center":[-1.2469723,5.14869,-1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.625,0.44444442,0.125,0.11111111]},{"Center":[-1.2469723,5.14869,-1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.75,0.44444442,0.125,0.11111111]},{"Center":[1.2469723,5.14869,-1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.875,0.44444442,0.125,0.11111111]},{"Center":[1.2469723,5.14869,-1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0,0.3333333,0.125,0.11111111]},{"Center":[1.2469723,5.14869,-1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.125,0.3333333,0.125,0.11111111]},{"Center":[-1.2469723,5.14869,1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.25,0.3333333,0.125,0.11111111]},{"Center":[-1.2469723,5.14869,1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.375,0.3333333,0.125,0.11111111]},{"Center":[-1.2469723,5.14869,1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.5,0.3333333,0.125,0.11111111]},{"Center":[1.2469723,5.14869,1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.625,0.3333333,0.125,0.11111111]},{"Center":[1.2469723,5.14869,1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.75,0.3333333,0.125,0.11111111]},{"Center":[1.2469723,5.14869,1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.875,0.3333333,0.125,0.11111111]},{"Center":[-1.2469723,6.6197453,-1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0,0.22222221,0.125,0.11111111]},{"Center":[-1.2469723,6.6197453,-1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.125,0.22222221,0.125,0.11111111]},{"Center":[-1.2469723,6.6197453,-1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.25,0.22222221,0.125,0.11111111]},{"Center":[1.2469723,6.6197453,-1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.375,0.22222221,0.125,0.11111111]},{"Center":[1.2469723,6.6197453,-1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.5,0.22222221,0.125,0.11111111]},{"Center":[1.2469723,6.6197453,-1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.625,0.22222221,0.125,0.11111111]},{"Center":[-1.2469723,6.6197453,1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.75,0.22222221,0.125,0.11111111]},{"Center":[-1.2469723,6.6197453,1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.875,0.22222221,0.125,0.11111111]},{"Center":[-1.2469723,6.6197453,1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0,0.111111104,0.125,0.11111111]},{"Center":[1.2469723,6.6197453,1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.125,0.111111104,0.125,0.11111111]},{"Center":[1.2469723,6.6197453,1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.25,0.111111104,0.125,0.11111111]},{"Center":[1.2469723,6.6197453,1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.375,0.111111104,0.125,0.11111111]},{"Center":[-1.2469723,8.090799,-1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.5,0.111111104,0.125,0.11111111]},{"Center":[-1.2469723,8.090799,-1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.625,0.111111104,0.125,0.11111111]},{"Center":[-1.2469723,8.090799,-1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.75,0.111111104,0.125,0.11111111]},{"Center":[1.2469723,8.090799,-1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.875,0.111111104,0.125,0.11111111]},{"Center":[1.2469723,8.090799,-1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0,0,0.125,0.11111111]},{"Center":[1.2469723,8.090799,-1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.125,0,0.125,0.11111111]},{"Center":[-1.2469723,8.090799,1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.25,0,0.125,0.11111111]},{"Center":[-1.2469723,8.090799,1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.375,0,0.125,0.11111111]},{"Center":[-1.2469723,8.090799,1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.5,0,0.125,0.11111111]},{"Center":[1.2469723,8.090799,1.3189683],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.034455,"Uv":[0.625,0,0.125,0.11111111]},{"Center":[1.2469723,8.090799,1.3189683],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.034455,"Uv":[0.75,0,0.125,0.11111111]},{"Center":[1.2469723,8.090799,1.3189683],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.034455,"Uv":[0.875,0,0.125,0.11111111]}],[{"Center":[-1.0671537,0.64960444,-0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830114,"Uv":[0,0.8888889,0.125,0.11111111]},{"Center":[-1.0671537,0.64960444,-0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830114,"Uv":[0.125,0.8888889,0.125,0.11111111]},{"Center":[-1.0671537,0.64960444,-0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830114,"Uv":[0.25,0.8888889,0.125,0.11111111]},{"Center":[1.0671537,0.64960444,-0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830114,"Uv":[0.375,0.8888889,0.125,0.11111111]},{"Center":[1.0671537,0.64960444,-0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830114,"Uv":[0.5,0.8888889,0.125,0.11111111]},{"Center":[1.0671537,0.64960444,-0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830114,"Uv":[0.625,0.8888889,0.125,0.11111111]},{"Center":[-1.0671537,0.64960444,0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830114,"Uv":[0.75,0.8888889,0.125,0.11111111]},{"Center":[-1.0671537,0.64960444,0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830114,"Uv":[0.875,0.8888889,0.125,0.11111111]},{"Center":[-1.0671537,0.64960444,0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830114,"Uv":[0,0.7777778,0.125,0.11111111]},{"Center":[1.0671537,0.64960444,0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830114,"Uv":[0.125,0.7777778,0.125,0.11111111]},{"Center":[1.0671537,0.64960444,0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830114,"Uv":[0.25,0.7777778,0.125,0.11111111]},{"Center":[1.0671537,0.64960444,0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830114,"Uv":[0.375,0.7777778,0.125,0.11111111]},{"Center":[-1.0671537,1.9488133,-0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830114,"Uv":[0.5,0.7777778,0.125,0.11111111]},{"Center":[-1.0671537,1.9488133,-0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830114,"Uv":[0.625,0.7777778,0.125,0.11111111]},{"Center":[-1.0671537,1.9488133,-0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830114,"Uv":[0.75,0.7777778,0.125,0.11111111]},{"Center":[1.0671537,1.9488133,-0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830114,"Uv":[0.875,0.7777778,0.125,0.11111111]},{"Center":[1.0671537,1.9488133,-0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830114,"Uv":[0,0.6666666,0.125,0.11111111]},{"Center":[1.0671537,1.9488133,-0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830114,"Uv":[0.125,0.6666666,0.125,0.11111111]},{"Center":[-1.0671537,1.9488133,0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830114,"Uv":[0.25,0.6666666,0.125,0.11111111]},{"Center":[-1.0671537,1.9488133,0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830114,"Uv":[0.375,0.6666666,0.125,0.11111111]},{"Center":[-1.0671537,1.9488133,0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830114,"Uv":[0.5,0.6666666,0.125,0.11111111]},{"Center":[1.0671537,1.9488133,0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830114,"Uv":[0.625,0.6666666,0.125,0.11111111]},{"Center":[1.0671537,1.9488133,0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830114,"Uv":[0.75,0.6666666,0.125,0.11111111]},{"Center":[1.0671537,1.9488133,0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830114,"Uv":[0.875,0.6666666,0.125,0.11111111]},{"Center":[-1.0671537,3.248022,-0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830114,"Uv":[0,0.5555556,0.125,0.11111111]},{"Center":[-1.0671537,3.248022,-0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830114,"Uv":[0.125,0.5555556,0.125,0.11111111]},{"Center":[-1.0671537,3.248022,-0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830114,"Uv":[0.25,0.5555556,0.125,0.11111111]},{"Center":[1.0671537,3.248022,-0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830114,"Uv":[0.375,0.5555556,0.125,0.11111111]},{"Center":[1.0671537,3.248022,-0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830114,"Uv":[0.5,0.5555556,0.125,0.11111111]},{"Center":[1.0671537,3.248022,-0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830114,"Uv":[0.625,0.5555556,0.125,0.11111111]},{"Center":[-1.0671537,3.248022,0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830114,"Uv":[0.75,0.5555556,0.125,0.11111111]},{"Center":[-1.0671537,3.248022,0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830114,"Uv":[0.875,0.5555556,0.125,0.11111111]},{"Center":[-1.0671537,3.248022,0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830114,"Uv":[0,0.44444442,0.125,0.11111111]},{"Center":[1.0671537,3.248022,0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830114,"Uv":[0.125,0.44444442,0.125,0.11111111]},{"Center":[1.0671537,3.248022,0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830114,"Uv":[0.25,0.44444442,0.125,0.11111111]},{"Center":[1.0671537,3.248022,0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830114,"Uv":[0.375,0.44444442,0.125,0.11111111]},{"Center":[-1.0671537,4.547231,-0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830114,"Uv":[0.5,0.44444442,0.125,0.11111111]},{"Center":[-1.0671537,4.547231,-0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830114,"Uv":[0.625,0.44444442,0.125,0.11111111]},{"Center":[-1.0671537,4.547231,-0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830114,"Uv":[0.75,0.44444442,0.125,0.11111111]},{"Center":[1.0671537,4.547231,-0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830114,"Uv":[0.875,0.44444442,0.125,0.11111111]},{"Center":[1.0671537,4.547231,-0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830114,"Uv":[0,0.3333333,0.125,0.11111111]},{"Center":[1.0671537,4.547231,-0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830114,"Uv":[0.125,0.3333333,0.125,0.11111111]},{"Center":[-1.0671537,4.547231,0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830114,"Uv":[0.25,0.3333333,0.125,0.11111111]},{"Center":[-1.0671537,4.547231,0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830114,"Uv":[0.375,0.3333333,0.125,0.11111111]},{"Center":[-1.0671537,4.547231,0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830114,"Uv":[0.5,0.3333333,0.125,0.11111111]},{"Center":[1.0671537,4.547231,0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830114,"Uv":[0.625,0.3333333,0.125,0.11111111]},{"Center":[1.0671537,4.547231,0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830114,"Uv":[0.75,0.3333333,0.125,0.11111111]},{"Center":[1.0671537,4.547231,0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830114,"Uv":[0.875,0.3333333,0.125,0.11111111]},{"Center":[-1.0671537,5.84644,-0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830112,"Uv":[0,0.22222221,0.125,0.11111111]},{"Center":[-1.0671537,5.84644,-0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830112,"Uv":[0.125,0.22222221,0.125,0.11111111]},{"Center":[-1.0671537,5.84644,-0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830112,"Uv":[0.25,0.22222221,0.125,0.11111111]},{"Center":[1.0671537,5.84644,-0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830112,"Uv":[0.375,0.22222221,0.125,0.11111111]},{"Center":[1.0671537,5.84644,-0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830112,"Uv":[0.5,0.22222221,0.125,0.11111111]},{"Center":[1.0671537,5.84644,-0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830112,"Uv":[0.625,0.22222221,0.125,0.11111111]},{"Center":[-1.0671537,5.84644,0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830112,"Uv":[0.75,0.22222221,0.125,0.11111111]},{"Center":[-1.0671537,5.84644,0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830112,"Uv":[0.875,0.22222221,0.125,0.11111111]},{"Center":[-1.0671537,5.84644,0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830112,"Uv":[0,0.111111104,0.125,0.11111111]},{"Center":[1.0671537,5.84644,0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830112,"Uv":[0.125,0.111111104,0.125,0.11111111]},{"Center":[1.0671537,5.84644,0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830112,"Uv":[0.25,0.111111104,0.125,0.11111111]},{"Center":[1.0671537,5.84644,0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830112,"Uv":[0.375,0.111111104,0.125,0.11111111]},{"Center":[-1.0671537,7.1456485,-0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830112,"Uv":[0.5,0.111111104,0.125,0.11111111]},{"Center":[-1.0671537,7.1456485,-0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830112,"Uv":[0.625,0.111111104,0.125,0.11111111]},{"Center":[-1.0671537,7.1456485,-0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830112,"Uv":[0.75,0.111111104,0.125,0.11111111]},{"Center":[1.0671537,7.1456485,-0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830112,"Uv":[0.875,0.111111104,0.125,0.11111111]},{"Center":[1.0671537,7.1456485,-0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830112,"Uv":[0,0,0.125,0.11111111]},{"Center":[1.0671537,7.1456485,-0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830112,"Uv":[0.125,0,0.125,0.11111111]},{"Center":[-1.0671537,7.1456485,0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830112,"Uv":[0.25,0,0.125,0.11111111]},{"Center":[-1.0671537,7.1456485,0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830112,"Uv":[0.375,0,0.125,0.11111111]},{"Center":[-1.0671537,7.1456485,0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830112,"Uv":[0.5,0,0.125,0.11111111]},{"Center":[1.0671537,7.1456485,0.98947483],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.2830112,"Uv":[0.625,0,0.125,0.11111111]},{"Center":[1.0671537,7.1456485,0.98947483],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.2830112,"Uv":[0.75,0,0.125,0.11111111]},{"Center":[1.0671537,7.1456485,0.98947483],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.2830112,"Uv":[0.875,0,0.125,0.11111111]}],[{"Center":[-1.0766318,0.49222934,-1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0,0.8888889,0.125,0.11111111]},{"Center":[-1.0766318,0.49222934,-1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.125,0.8888889,0.125,0.11111111]},{"Center":[-1.0766318,0.49222934,-1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.25,0.8888889,0.125,0.11111111]},{"Center":[1.0766318,0.49222934,-1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.375,0.8888889,0.125,0.11111111]},{"Center":[1.0766318,0.49222934,-1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.5,0.8888889,0.125,0.11111111]},{"Center":[1.0766318,0.49222934,-1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.625,0.8888889,0.125,0.11111111]},{"Center":[-1.0766318,0.49222934,1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.75,0.8888889,0.125,0.11111111]},{"Center":[-1.0766318,0.49222934,1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.875,0.8888889,0.125,0.11111111]},{"Center":[-1.0766318,0.49222934,1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0,0.7777778,0.125,0.11111111]},{"Center":[1.0766318,0.49222934,1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.125,0.7777778,0.125,0.11111111]},{"Center":[1.0766318,0.49222934,1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.25,0.7777778,0.125,0.11111111]},{"Center":[1.0766318,0.49222934,1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.375,0.7777778,0.125,0.11111111]},{"Center":[-1.0766318,1.476688,-1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.5,0.7777778,0.125,0.11111111]},{"Center":[-1.0766318,1.476688,-1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.625,0.7777778,0.125,0.11111111]},{"Center":[-1.0766318,1.476688,-1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.75,0.7777778,0.125,0.11111111]},{"Center":[1.0766318,1.476688,-1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.875,0.7777778,0.125,0.11111111]},{"Center":[1.0766318,1.476688,-1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0,0.6666666,0.125,0.11111111]},{"Center":[1.0766318,1.476688,-1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.125,0.6666666,0.125,0.11111111]},{"Center":[-1.0766318,1.476688,1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.25,0.6666666,0.125,0.11111111]},{"Center":[-1.0766318,1.476688,1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.375,0.6666666,0.125,0.11111111]},{"Center":[-1.0766318,1.476688,1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.5,0.6666666,0.125,0.11111111]},{"Center":[1.0766318,1.476688,1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.625,0.6666666,0.125,0.11111111]},{"Center":[1.0766318,1.476688,1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.75,0.6666666,0.125,0.11111111]},{"Center":[1.0766318,1.476688,1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.875,0.6666666,0.125,0.11111111]},{"Center":[-1.0766318,2.4611468,-1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0,0.5555556,0.125,0.11111111]},{"Center":[-1.0766318,2.4611468,-1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.125,0.5555556,0.125,0.11111111]},{"Center":[-1.0766318,2.4611468,-1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.25,0.5555556,0.125,0.11111111]},{"Center":[1.0766318,2.4611468,-1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.375,0.5555556,0.125,0.11111111]},{"Center":[1.0766318,2.4611468,-1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.5,0.5555556,0.125,0.11111111]},{"Center":[1.0766318,2.4611468,-1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.625,0.5555556,0.125,0.11111111]},{"Center":[-1.0766318,2.4611468,1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.75,0.5555556,0.125,0.11111111]},{"Center":[-1.0766318,2.4611468,1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.875,0.5555556,0.125,0.11111111]},{"Center":[-1.0766318,2.4611468,1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0,0.44444442,0.125,0.11111111]},{"Center":[1.0766318,2.4611468,1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.125,0.44444442,0.125,0.11111111]},{"Center":[1.0766318,2.4611468,1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.25,0.44444442,0.125,0.11111111]},{"Center":[1.0766318,2.4611468,1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.375,0.44444442,0.125,0.11111111]},{"Center":[-1.0766318,3.4456053,-1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.5,0.44444442,0.125,0.11111111]},{"Center":[-1.0766318,3.4456053,-1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.625,0.44444442,0.125,0.11111111]},{"Center":[-1.0766318,3.4456053,-1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.75,0.44444442,0.125,0.11111111]},{"Center":[1.0766318,3.4456053,-1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.875,0.44444442,0.125,0.11111111]},{"Center":[1.0766318,3.4456053,-1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0,0.3333333,0.125,0.11111111]},{"Center":[1.0766318,3.4456053,-1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.125,0.3333333,0.125,0.11111111]},{"Center":[-1.0766318,3.4456053,1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.25,0.3333333,0.125,0.11111111]},{"Center":[-1.0766318,3.4456053,1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.375,0.3333333,0.125,0.11111111]},{"Center":[-1.0766318,3.4456053,1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.5,0.3333333,0.125,0.11111111]},{"Center":[1.0766318,3.4456053,1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.625,0.3333333,0.125,0.11111111]},{"Center":[1.0766318,3.4456053,1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.75,0.3333333,0.125,0.11111111]},{"Center":[1.0766318,3.4456053,1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.875,0.3333333,0.125,0.11111111]},{"Center":[-1.0766318,4.430064,-1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0,0.22222221,0.125,0.11111111]},{"Center":[-1.0766318,4.430064,-1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.125,0.22222221,0.125,0.11111111]},{"Center":[-1.0766318,4.430064,-1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.25,0.22222221,0.125,0.11111111]},{"Center":[1.0766318,4.430064,-1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.375,0.22222221,0.125,0.11111111]},{"Center":[1.0766318,4.430064,-1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.5,0.22222221,0.125,0.11111111]},{"Center":[1.0766318,4.430064,-1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.625,0.22222221,0.125,0.11111111]},{"Center":[-1.0766318,4.430064,1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.75,0.22222221,0.125,0.11111111]},{"Center":[-1.0766318,4.430064,1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.875,0.22222221,0.125,0.11111111]},{"Center":[-1.0766318,4.430064,1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0,0.111111104,0.125,0.11111111]},{"Center":[1.0766318,4.430064,1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.125,0.111111104,0.125,0.11111111]},{"Center":[1.0766318,4.430064,1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.25,0.111111104,0.125,0.11111111]},{"Center":[1.0766318,4.430064,1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.375,0.111111104,0.125,0.11111111]},{"Center":[-1.0766318,5.414522,-1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.5,0.111111104,0.125,0.11111111]},{"Center":[-1.0766318,5.414522,-1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.625,0.111111104,0.125,0.11111111]},{"Center":[-1.0766318,5.414522,-1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.75,0.111111104,0.125,0.11111111]},{"Center":[1.0766318,5.414522,-1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.875,0.111111104,0.125,0.11111111]},{"Center":[1.0766318,5.414522,-1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0,0,0.125,0.11111111]},{"Center":[1.0766318,5.414522,-1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.125,0,0.125,0.11111111]},{"Center":[-1.0766318,5.414522,1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.25,0,0.125,0.11111111]},{"Center":[-1.0766318,5.414522,1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.375,0,0.125,0.11111111]},{"Center":[-1.0766318,5.414522,1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.5,0,0.125,0.11111111]},{"Center":[1.0766318,5.414522,1.0033259],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":3.1967115,"Uv":[0.625,0,0.125,0.11111111]},{"Center":[1.0766318,5.414522,1.0033259],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":3.1967115,"Uv":[0.75,0,0.125,0.11111111]},{"Center":[1.0766318,5.414522,1.0033259],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":3.1967115,"Uv":[0.875,0,0.125,0.11111111]}]]} \ No newline at end of file diff --git a/Content/Vegitations/fir_sapling_medium/prepared/impostor-0-color.png b/Content/Vegitations/fir_sapling_medium/prepared/impostor-0-color.png new file mode 100644 index 0000000..a043598 Binary files /dev/null and b/Content/Vegitations/fir_sapling_medium/prepared/impostor-0-color.png differ diff --git a/Content/Vegitations/fir_sapling_medium/prepared/impostor-0-normal.png b/Content/Vegitations/fir_sapling_medium/prepared/impostor-0-normal.png new file mode 100644 index 0000000..84c5c8b Binary files /dev/null and b/Content/Vegitations/fir_sapling_medium/prepared/impostor-0-normal.png differ diff --git a/Content/Vegitations/fir_sapling_medium/prepared/impostor-1-color.png b/Content/Vegitations/fir_sapling_medium/prepared/impostor-1-color.png new file mode 100644 index 0000000..3d25d20 Binary files /dev/null and b/Content/Vegitations/fir_sapling_medium/prepared/impostor-1-color.png differ diff --git a/Content/Vegitations/fir_sapling_medium/prepared/impostor-1-normal.png b/Content/Vegitations/fir_sapling_medium/prepared/impostor-1-normal.png new file mode 100644 index 0000000..e74e32f Binary files /dev/null and b/Content/Vegitations/fir_sapling_medium/prepared/impostor-1-normal.png differ diff --git a/Content/Vegitations/fir_sapling_medium/prepared/impostor-2-color.png b/Content/Vegitations/fir_sapling_medium/prepared/impostor-2-color.png new file mode 100644 index 0000000..0e6ce1e Binary files /dev/null and b/Content/Vegitations/fir_sapling_medium/prepared/impostor-2-color.png differ diff --git a/Content/Vegitations/fir_sapling_medium/prepared/impostor-2-normal.png b/Content/Vegitations/fir_sapling_medium/prepared/impostor-2-normal.png new file mode 100644 index 0000000..cf136e1 Binary files /dev/null and b/Content/Vegitations/fir_sapling_medium/prepared/impostor-2-normal.png differ diff --git a/Content/Vegitations/fir_sapling_medium/prepared/impostors.json b/Content/Vegitations/fir_sapling_medium/prepared/impostors.json new file mode 100644 index 0000000..151295f --- /dev/null +++ b/Content/Vegitations/fir_sapling_medium/prepared/impostors.json @@ -0,0 +1 @@ +{"Views":16,"Elevations":3,"Variants":[{"Center":[0,4.413163,0],"Size":12.756494},{"Center":[0,3.8976264,0],"Size":10.952542},{"Center":[0,2.9533758,0],"Size":9.093285}]} \ No newline at end of file diff --git a/Content/Vegitations/fir_sapling_medium/prepared/model.glb b/Content/Vegitations/fir_sapling_medium/prepared/model.glb new file mode 100644 index 0000000..a4ca2c7 Binary files /dev/null and b/Content/Vegitations/fir_sapling_medium/prepared/model.glb differ diff --git a/Content/Vegitations/fir_sapling_medium/prepared/preparation.json b/Content/Vegitations/fir_sapling_medium/prepared/preparation.json new file mode 100644 index 0000000..eb9e1fb --- /dev/null +++ b/Content/Vegitations/fir_sapling_medium/prepared/preparation.json @@ -0,0 +1,52 @@ +{ + "source": "https://polyhaven.com/a/fir_sapling_medium", + "license": "CC0", + "budgets": [ + 12000, + 2000, + 350 + ], + "triangles": [ + 21842, + 5257, + 1056 + ], + "variants": [ + { + "min": [ + -3.264199733734131, + -1.2817002534866333, + -3.336195707321167 + ], + "max": [ + 3.264199733734131, + 10.108026504516602, + 3.336195707321167 + ] + }, + { + "min": [ + -2.7086594104766846, + -0.9919012784957886, + -2.6309804916381836 + ], + "max": [ + 2.7086594104766846, + 8.787154197692871, + 2.6309804916381836 + ] + }, + { + "min": [ + -2.674987554550171, + -1.1061264276504517, + -2.601681709289551 + ], + "max": [ + 2.674987554550171, + 7.012877941131592, + 2.601681709289551 + ] + } + ] +} \ No newline at end of file diff --git a/Content/Vegitations/fir_sapling_medium/prepared/stamp.json b/Content/Vegitations/fir_sapling_medium/prepared/stamp.json new file mode 100644 index 0000000..a7eefb8 --- /dev/null +++ b/Content/Vegitations/fir_sapling_medium/prepared/stamp.json @@ -0,0 +1 @@ +{"version":5,"cards":true,"files":["ab770a7f4437bb1585c856e9467345cf","5fa93c73d145cacdb258a29e3860a1ae","e9375177560503904ece137474cfde0b","1ffec60c862633f2bb5c47f2102651cf","2072f6b164184f0ca9b41f8623bfec06","f5e2dd9085334845f662fc11c60eb713","2fcac76cebb4f5fdc7fba2f31e02a490","d3976657a03ce2e6eccfa56b378bbcd0","1d93929d869e5539ec245ae3eddb7243"]} \ No newline at end of file diff --git a/Content/Vegitations/fir_tree_01/prepared/cards-0-color.png b/Content/Vegitations/fir_tree_01/prepared/cards-0-color.png new file mode 100644 index 0000000..bdd0d50 Binary files /dev/null and b/Content/Vegitations/fir_tree_01/prepared/cards-0-color.png differ diff --git a/Content/Vegitations/fir_tree_01/prepared/cards-0-normal.png b/Content/Vegitations/fir_tree_01/prepared/cards-0-normal.png new file mode 100644 index 0000000..9d3e25a Binary files /dev/null and b/Content/Vegitations/fir_tree_01/prepared/cards-0-normal.png differ diff --git a/Content/Vegitations/fir_tree_01/prepared/cards-1-color.png b/Content/Vegitations/fir_tree_01/prepared/cards-1-color.png new file mode 100644 index 0000000..73a2ebc Binary files /dev/null and b/Content/Vegitations/fir_tree_01/prepared/cards-1-color.png differ diff --git a/Content/Vegitations/fir_tree_01/prepared/cards-1-normal.png b/Content/Vegitations/fir_tree_01/prepared/cards-1-normal.png new file mode 100644 index 0000000..12eb5f2 Binary files /dev/null and b/Content/Vegitations/fir_tree_01/prepared/cards-1-normal.png differ diff --git a/Content/Vegitations/fir_tree_01/prepared/cards-2-color.png b/Content/Vegitations/fir_tree_01/prepared/cards-2-color.png new file mode 100644 index 0000000..2ac9240 Binary files /dev/null and b/Content/Vegitations/fir_tree_01/prepared/cards-2-color.png differ diff --git a/Content/Vegitations/fir_tree_01/prepared/cards-2-normal.png b/Content/Vegitations/fir_tree_01/prepared/cards-2-normal.png new file mode 100644 index 0000000..ca67953 Binary files /dev/null and b/Content/Vegitations/fir_tree_01/prepared/cards-2-normal.png differ diff --git a/Content/Vegitations/fir_tree_01/prepared/cards.json b/Content/Vegitations/fir_tree_01/prepared/cards.json new file mode 100644 index 0000000..e92aa9e --- /dev/null +++ b/Content/Vegitations/fir_tree_01/prepared/cards.json @@ -0,0 +1 @@ +{"Variants":[[{"Center":[-1.6334448,1.5770925,-1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0,0.8888889,0.125,0.11111111]},{"Center":[-1.6334448,1.5770925,-1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.125,0.8888889,0.125,0.11111111]},{"Center":[-1.6334448,1.5770925,-1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.25,0.8888889,0.125,0.11111111]},{"Center":[1.6334448,1.5770925,-1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.375,0.8888889,0.125,0.11111111]},{"Center":[1.6334448,1.5770925,-1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.5,0.8888889,0.125,0.11111111]},{"Center":[1.6334448,1.5770925,-1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.625,0.8888889,0.125,0.11111111]},{"Center":[-1.6334448,1.5770925,1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.75,0.8888889,0.125,0.11111111]},{"Center":[-1.6334448,1.5770925,1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.875,0.8888889,0.125,0.11111111]},{"Center":[-1.6334448,1.5770925,1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0,0.7777778,0.125,0.11111111]},{"Center":[1.6334448,1.5770925,1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.125,0.7777778,0.125,0.11111111]},{"Center":[1.6334448,1.5770925,1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.25,0.7777778,0.125,0.11111111]},{"Center":[1.6334448,1.5770925,1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.375,0.7777778,0.125,0.11111111]},{"Center":[-1.6334448,4.7312775,-1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.5,0.7777778,0.125,0.11111111]},{"Center":[-1.6334448,4.7312775,-1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.625,0.7777778,0.125,0.11111111]},{"Center":[-1.6334448,4.7312775,-1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.75,0.7777778,0.125,0.11111111]},{"Center":[1.6334448,4.7312775,-1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.875,0.7777778,0.125,0.11111111]},{"Center":[1.6334448,4.7312775,-1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0,0.6666666,0.125,0.11111111]},{"Center":[1.6334448,4.7312775,-1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.125,0.6666666,0.125,0.11111111]},{"Center":[-1.6334448,4.7312775,1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.25,0.6666666,0.125,0.11111111]},{"Center":[-1.6334448,4.7312775,1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.375,0.6666666,0.125,0.11111111]},{"Center":[-1.6334448,4.7312775,1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.5,0.6666666,0.125,0.11111111]},{"Center":[1.6334448,4.7312775,1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.625,0.6666666,0.125,0.11111111]},{"Center":[1.6334448,4.7312775,1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.75,0.6666666,0.125,0.11111111]},{"Center":[1.6334448,4.7312775,1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.875,0.6666666,0.125,0.11111111]},{"Center":[-1.6334448,7.8854628,-1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0,0.5555556,0.125,0.11111111]},{"Center":[-1.6334448,7.8854628,-1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.125,0.5555556,0.125,0.11111111]},{"Center":[-1.6334448,7.8854628,-1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.25,0.5555556,0.125,0.11111111]},{"Center":[1.6334448,7.8854628,-1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.375,0.5555556,0.125,0.11111111]},{"Center":[1.6334448,7.8854628,-1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.5,0.5555556,0.125,0.11111111]},{"Center":[1.6334448,7.8854628,-1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.625,0.5555556,0.125,0.11111111]},{"Center":[-1.6334448,7.8854628,1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.75,0.5555556,0.125,0.11111111]},{"Center":[-1.6334448,7.8854628,1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.875,0.5555556,0.125,0.11111111]},{"Center":[-1.6334448,7.8854628,1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0,0.44444442,0.125,0.11111111]},{"Center":[1.6334448,7.8854628,1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.125,0.44444442,0.125,0.11111111]},{"Center":[1.6334448,7.8854628,1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.25,0.44444442,0.125,0.11111111]},{"Center":[1.6334448,7.8854628,1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.375,0.44444442,0.125,0.11111111]},{"Center":[-1.6334448,11.039648,-1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.5,0.44444442,0.125,0.11111111]},{"Center":[-1.6334448,11.039648,-1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.625,0.44444442,0.125,0.11111111]},{"Center":[-1.6334448,11.039648,-1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.75,0.44444442,0.125,0.11111111]},{"Center":[1.6334448,11.039648,-1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.875,0.44444442,0.125,0.11111111]},{"Center":[1.6334448,11.039648,-1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0,0.3333333,0.125,0.11111111]},{"Center":[1.6334448,11.039648,-1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.125,0.3333333,0.125,0.11111111]},{"Center":[-1.6334448,11.039648,1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.25,0.3333333,0.125,0.11111111]},{"Center":[-1.6334448,11.039648,1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.375,0.3333333,0.125,0.11111111]},{"Center":[-1.6334448,11.039648,1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.5,0.3333333,0.125,0.11111111]},{"Center":[1.6334448,11.039648,1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.625,0.3333333,0.125,0.11111111]},{"Center":[1.6334448,11.039648,1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.75,0.3333333,0.125,0.11111111]},{"Center":[1.6334448,11.039648,1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.875,0.3333333,0.125,0.11111111]},{"Center":[-1.6334448,14.193832,-1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0,0.22222221,0.125,0.11111111]},{"Center":[-1.6334448,14.193832,-1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.125,0.22222221,0.125,0.11111111]},{"Center":[-1.6334448,14.193832,-1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.25,0.22222221,0.125,0.11111111]},{"Center":[1.6334448,14.193832,-1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.375,0.22222221,0.125,0.11111111]},{"Center":[1.6334448,14.193832,-1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.5,0.22222221,0.125,0.11111111]},{"Center":[1.6334448,14.193832,-1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.625,0.22222221,0.125,0.11111111]},{"Center":[-1.6334448,14.193832,1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.75,0.22222221,0.125,0.11111111]},{"Center":[-1.6334448,14.193832,1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.875,0.22222221,0.125,0.11111111]},{"Center":[-1.6334448,14.193832,1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0,0.111111104,0.125,0.11111111]},{"Center":[1.6334448,14.193832,1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.125,0.111111104,0.125,0.11111111]},{"Center":[1.6334448,14.193832,1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.25,0.111111104,0.125,0.11111111]},{"Center":[1.6334448,14.193832,1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.375,0.111111104,0.125,0.11111111]},{"Center":[-1.6334448,17.348017,-1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.5,0.111111104,0.125,0.11111111]},{"Center":[-1.6334448,17.348017,-1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.625,0.111111104,0.125,0.11111111]},{"Center":[-1.6334448,17.348017,-1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.75,0.111111104,0.125,0.11111111]},{"Center":[1.6334448,17.348017,-1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.875,0.111111104,0.125,0.11111111]},{"Center":[1.6334448,17.348017,-1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0,0,0.125,0.11111111]},{"Center":[1.6334448,17.348017,-1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.125,0,0.125,0.11111111]},{"Center":[-1.6334448,17.348017,1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.25,0,0.125,0.11111111]},{"Center":[-1.6334448,17.348017,1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.375,0,0.125,0.11111111]},{"Center":[-1.6334448,17.348017,1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.5,0,0.125,0.11111111]},{"Center":[1.6334448,17.348017,1.6067058],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.7299356,"Uv":[0.625,0,0.125,0.11111111]},{"Center":[1.6334448,17.348017,1.6067058],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.7299356,"Uv":[0.75,0,0.125,0.11111111]},{"Center":[1.6334448,17.348017,1.6067058],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.7299356,"Uv":[0.875,0,0.125,0.11111111]}],[{"Center":[-1.4031467,1.1712672,-1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.889362,"Uv":[0,0.8888889,0.125,0.11111111]},{"Center":[-1.4031467,1.1712672,-1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.889362,"Uv":[0.125,0.8888889,0.125,0.11111111]},{"Center":[-1.4031467,1.1712672,-1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.889362,"Uv":[0.25,0.8888889,0.125,0.11111111]},{"Center":[1.4031467,1.1712672,-1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.889362,"Uv":[0.375,0.8888889,0.125,0.11111111]},{"Center":[1.4031467,1.1712672,-1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.889362,"Uv":[0.5,0.8888889,0.125,0.11111111]},{"Center":[1.4031467,1.1712672,-1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.889362,"Uv":[0.625,0.8888889,0.125,0.11111111]},{"Center":[-1.4031467,1.1712672,1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.889362,"Uv":[0.75,0.8888889,0.125,0.11111111]},{"Center":[-1.4031467,1.1712672,1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.889362,"Uv":[0.875,0.8888889,0.125,0.11111111]},{"Center":[-1.4031467,1.1712672,1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.889362,"Uv":[0,0.7777778,0.125,0.11111111]},{"Center":[1.4031467,1.1712672,1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.889362,"Uv":[0.125,0.7777778,0.125,0.11111111]},{"Center":[1.4031467,1.1712672,1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.889362,"Uv":[0.25,0.7777778,0.125,0.11111111]},{"Center":[1.4031467,1.1712672,1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.889362,"Uv":[0.375,0.7777778,0.125,0.11111111]},{"Center":[-1.4031467,3.5138016,-1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.889362,"Uv":[0.5,0.7777778,0.125,0.11111111]},{"Center":[-1.4031467,3.5138016,-1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.889362,"Uv":[0.625,0.7777778,0.125,0.11111111]},{"Center":[-1.4031467,3.5138016,-1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.889362,"Uv":[0.75,0.7777778,0.125,0.11111111]},{"Center":[1.4031467,3.5138016,-1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.889362,"Uv":[0.875,0.7777778,0.125,0.11111111]},{"Center":[1.4031467,3.5138016,-1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.889362,"Uv":[0,0.6666666,0.125,0.11111111]},{"Center":[1.4031467,3.5138016,-1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.889362,"Uv":[0.125,0.6666666,0.125,0.11111111]},{"Center":[-1.4031467,3.5138016,1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.889362,"Uv":[0.25,0.6666666,0.125,0.11111111]},{"Center":[-1.4031467,3.5138016,1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.889362,"Uv":[0.375,0.6666666,0.125,0.11111111]},{"Center":[-1.4031467,3.5138016,1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.889362,"Uv":[0.5,0.6666666,0.125,0.11111111]},{"Center":[1.4031467,3.5138016,1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.889362,"Uv":[0.625,0.6666666,0.125,0.11111111]},{"Center":[1.4031467,3.5138016,1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.889362,"Uv":[0.75,0.6666666,0.125,0.11111111]},{"Center":[1.4031467,3.5138016,1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.889362,"Uv":[0.875,0.6666666,0.125,0.11111111]},{"Center":[-1.4031467,5.8563356,-1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.889362,"Uv":[0,0.5555556,0.125,0.11111111]},{"Center":[-1.4031467,5.8563356,-1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.889362,"Uv":[0.125,0.5555556,0.125,0.11111111]},{"Center":[-1.4031467,5.8563356,-1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.889362,"Uv":[0.25,0.5555556,0.125,0.11111111]},{"Center":[1.4031467,5.8563356,-1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.889362,"Uv":[0.375,0.5555556,0.125,0.11111111]},{"Center":[1.4031467,5.8563356,-1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.889362,"Uv":[0.5,0.5555556,0.125,0.11111111]},{"Center":[1.4031467,5.8563356,-1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.889362,"Uv":[0.625,0.5555556,0.125,0.11111111]},{"Center":[-1.4031467,5.8563356,1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.889362,"Uv":[0.75,0.5555556,0.125,0.11111111]},{"Center":[-1.4031467,5.8563356,1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.889362,"Uv":[0.875,0.5555556,0.125,0.11111111]},{"Center":[-1.4031467,5.8563356,1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.889362,"Uv":[0,0.44444442,0.125,0.11111111]},{"Center":[1.4031467,5.8563356,1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.889362,"Uv":[0.125,0.44444442,0.125,0.11111111]},{"Center":[1.4031467,5.8563356,1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.889362,"Uv":[0.25,0.44444442,0.125,0.11111111]},{"Center":[1.4031467,5.8563356,1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.889362,"Uv":[0.375,0.44444442,0.125,0.11111111]},{"Center":[-1.4031467,8.19887,-1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.889362,"Uv":[0.5,0.44444442,0.125,0.11111111]},{"Center":[-1.4031467,8.19887,-1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.889362,"Uv":[0.625,0.44444442,0.125,0.11111111]},{"Center":[-1.4031467,8.19887,-1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.889362,"Uv":[0.75,0.44444442,0.125,0.11111111]},{"Center":[1.4031467,8.19887,-1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.889362,"Uv":[0.875,0.44444442,0.125,0.11111111]},{"Center":[1.4031467,8.19887,-1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.889362,"Uv":[0,0.3333333,0.125,0.11111111]},{"Center":[1.4031467,8.19887,-1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.889362,"Uv":[0.125,0.3333333,0.125,0.11111111]},{"Center":[-1.4031467,8.19887,1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.889362,"Uv":[0.25,0.3333333,0.125,0.11111111]},{"Center":[-1.4031467,8.19887,1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.889362,"Uv":[0.375,0.3333333,0.125,0.11111111]},{"Center":[-1.4031467,8.19887,1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.889362,"Uv":[0.5,0.3333333,0.125,0.11111111]},{"Center":[1.4031467,8.19887,1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.889362,"Uv":[0.625,0.3333333,0.125,0.11111111]},{"Center":[1.4031467,8.19887,1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.889362,"Uv":[0.75,0.3333333,0.125,0.11111111]},{"Center":[1.4031467,8.19887,1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.889362,"Uv":[0.875,0.3333333,0.125,0.11111111]},{"Center":[-1.4031467,10.541405,-1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.8893614,"Uv":[0,0.22222221,0.125,0.11111111]},{"Center":[-1.4031467,10.541405,-1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.8893614,"Uv":[0.125,0.22222221,0.125,0.11111111]},{"Center":[-1.4031467,10.541405,-1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.8893614,"Uv":[0.25,0.22222221,0.125,0.11111111]},{"Center":[1.4031467,10.541405,-1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.8893614,"Uv":[0.375,0.22222221,0.125,0.11111111]},{"Center":[1.4031467,10.541405,-1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.8893614,"Uv":[0.5,0.22222221,0.125,0.11111111]},{"Center":[1.4031467,10.541405,-1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.8893614,"Uv":[0.625,0.22222221,0.125,0.11111111]},{"Center":[-1.4031467,10.541405,1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.8893614,"Uv":[0.75,0.22222221,0.125,0.11111111]},{"Center":[-1.4031467,10.541405,1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.8893614,"Uv":[0.875,0.22222221,0.125,0.11111111]},{"Center":[-1.4031467,10.541405,1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.8893614,"Uv":[0,0.111111104,0.125,0.11111111]},{"Center":[1.4031467,10.541405,1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.8893614,"Uv":[0.125,0.111111104,0.125,0.11111111]},{"Center":[1.4031467,10.541405,1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.8893614,"Uv":[0.25,0.111111104,0.125,0.11111111]},{"Center":[1.4031467,10.541405,1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.8893614,"Uv":[0.375,0.111111104,0.125,0.11111111]},{"Center":[-1.4031467,12.883938,-1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.8893614,"Uv":[0.5,0.111111104,0.125,0.11111111]},{"Center":[-1.4031467,12.883938,-1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.8893614,"Uv":[0.625,0.111111104,0.125,0.11111111]},{"Center":[-1.4031467,12.883938,-1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.8893614,"Uv":[0.75,0.111111104,0.125,0.11111111]},{"Center":[1.4031467,12.883938,-1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.8893614,"Uv":[0.875,0.111111104,0.125,0.11111111]},{"Center":[1.4031467,12.883938,-1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.8893614,"Uv":[0,0,0.125,0.11111111]},{"Center":[1.4031467,12.883938,-1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.8893614,"Uv":[0.125,0,0.125,0.11111111]},{"Center":[-1.4031467,12.883938,1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.8893614,"Uv":[0.25,0,0.125,0.11111111]},{"Center":[-1.4031467,12.883938,1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.8893614,"Uv":[0.375,0,0.125,0.11111111]},{"Center":[-1.4031467,12.883938,1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.8893614,"Uv":[0.5,0,0.125,0.11111111]},{"Center":[1.4031467,12.883938,1.5141677],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":4.8893614,"Uv":[0.625,0,0.125,0.11111111]},{"Center":[1.4031467,12.883938,1.5141677],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":4.8893614,"Uv":[0.75,0,0.125,0.11111111]},{"Center":[1.4031467,12.883938,1.5141677],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":4.8893614,"Uv":[0.875,0,0.125,0.11111111]}],[{"Center":[-1.5764847,1.2100484,-1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.1187043,"Uv":[0,0.8888889,0.125,0.11111111]},{"Center":[-1.5764847,1.2100484,-1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.1187043,"Uv":[0.125,0.8888889,0.125,0.11111111]},{"Center":[-1.5764847,1.2100484,-1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.1187043,"Uv":[0.25,0.8888889,0.125,0.11111111]},{"Center":[1.5764847,1.2100484,-1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.1187043,"Uv":[0.375,0.8888889,0.125,0.11111111]},{"Center":[1.5764847,1.2100484,-1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.1187043,"Uv":[0.5,0.8888889,0.125,0.11111111]},{"Center":[1.5764847,1.2100484,-1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.1187043,"Uv":[0.625,0.8888889,0.125,0.11111111]},{"Center":[-1.5764847,1.2100484,1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.1187043,"Uv":[0.75,0.8888889,0.125,0.11111111]},{"Center":[-1.5764847,1.2100484,1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.1187043,"Uv":[0.875,0.8888889,0.125,0.11111111]},{"Center":[-1.5764847,1.2100484,1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.1187043,"Uv":[0,0.7777778,0.125,0.11111111]},{"Center":[1.5764847,1.2100484,1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.1187043,"Uv":[0.125,0.7777778,0.125,0.11111111]},{"Center":[1.5764847,1.2100484,1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.1187043,"Uv":[0.25,0.7777778,0.125,0.11111111]},{"Center":[1.5764847,1.2100484,1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.1187043,"Uv":[0.375,0.7777778,0.125,0.11111111]},{"Center":[-1.5764847,3.6301453,-1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.1187043,"Uv":[0.5,0.7777778,0.125,0.11111111]},{"Center":[-1.5764847,3.6301453,-1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.1187043,"Uv":[0.625,0.7777778,0.125,0.11111111]},{"Center":[-1.5764847,3.6301453,-1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.1187043,"Uv":[0.75,0.7777778,0.125,0.11111111]},{"Center":[1.5764847,3.6301453,-1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.1187043,"Uv":[0.875,0.7777778,0.125,0.11111111]},{"Center":[1.5764847,3.6301453,-1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.1187043,"Uv":[0,0.6666666,0.125,0.11111111]},{"Center":[1.5764847,3.6301453,-1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.1187043,"Uv":[0.125,0.6666666,0.125,0.11111111]},{"Center":[-1.5764847,3.6301453,1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.1187043,"Uv":[0.25,0.6666666,0.125,0.11111111]},{"Center":[-1.5764847,3.6301453,1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.1187043,"Uv":[0.375,0.6666666,0.125,0.11111111]},{"Center":[-1.5764847,3.6301453,1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.1187043,"Uv":[0.5,0.6666666,0.125,0.11111111]},{"Center":[1.5764847,3.6301453,1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.1187043,"Uv":[0.625,0.6666666,0.125,0.11111111]},{"Center":[1.5764847,3.6301453,1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.1187043,"Uv":[0.75,0.6666666,0.125,0.11111111]},{"Center":[1.5764847,3.6301453,1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.1187043,"Uv":[0.875,0.6666666,0.125,0.11111111]},{"Center":[-1.5764847,6.0502424,-1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.1187043,"Uv":[0,0.5555556,0.125,0.11111111]},{"Center":[-1.5764847,6.0502424,-1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.1187043,"Uv":[0.125,0.5555556,0.125,0.11111111]},{"Center":[-1.5764847,6.0502424,-1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.1187043,"Uv":[0.25,0.5555556,0.125,0.11111111]},{"Center":[1.5764847,6.0502424,-1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.1187043,"Uv":[0.375,0.5555556,0.125,0.11111111]},{"Center":[1.5764847,6.0502424,-1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.1187043,"Uv":[0.5,0.5555556,0.125,0.11111111]},{"Center":[1.5764847,6.0502424,-1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.1187043,"Uv":[0.625,0.5555556,0.125,0.11111111]},{"Center":[-1.5764847,6.0502424,1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.1187043,"Uv":[0.75,0.5555556,0.125,0.11111111]},{"Center":[-1.5764847,6.0502424,1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.1187043,"Uv":[0.875,0.5555556,0.125,0.11111111]},{"Center":[-1.5764847,6.0502424,1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.1187043,"Uv":[0,0.44444442,0.125,0.11111111]},{"Center":[1.5764847,6.0502424,1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.1187043,"Uv":[0.125,0.44444442,0.125,0.11111111]},{"Center":[1.5764847,6.0502424,1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.1187043,"Uv":[0.25,0.44444442,0.125,0.11111111]},{"Center":[1.5764847,6.0502424,1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.1187043,"Uv":[0.375,0.44444442,0.125,0.11111111]},{"Center":[-1.5764847,8.470339,-1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.1187043,"Uv":[0.5,0.44444442,0.125,0.11111111]},{"Center":[-1.5764847,8.470339,-1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.1187043,"Uv":[0.625,0.44444442,0.125,0.11111111]},{"Center":[-1.5764847,8.470339,-1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.1187043,"Uv":[0.75,0.44444442,0.125,0.11111111]},{"Center":[1.5764847,8.470339,-1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.1187043,"Uv":[0.875,0.44444442,0.125,0.11111111]},{"Center":[1.5764847,8.470339,-1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.1187043,"Uv":[0,0.3333333,0.125,0.11111111]},{"Center":[1.5764847,8.470339,-1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.1187043,"Uv":[0.125,0.3333333,0.125,0.11111111]},{"Center":[-1.5764847,8.470339,1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.1187043,"Uv":[0.25,0.3333333,0.125,0.11111111]},{"Center":[-1.5764847,8.470339,1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.1187043,"Uv":[0.375,0.3333333,0.125,0.11111111]},{"Center":[-1.5764847,8.470339,1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.1187043,"Uv":[0.5,0.3333333,0.125,0.11111111]},{"Center":[1.5764847,8.470339,1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.1187043,"Uv":[0.625,0.3333333,0.125,0.11111111]},{"Center":[1.5764847,8.470339,1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.1187043,"Uv":[0.75,0.3333333,0.125,0.11111111]},{"Center":[1.5764847,8.470339,1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.1187043,"Uv":[0.875,0.3333333,0.125,0.11111111]},{"Center":[-1.5764847,10.890436,-1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.1187043,"Uv":[0,0.22222221,0.125,0.11111111]},{"Center":[-1.5764847,10.890436,-1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.1187043,"Uv":[0.125,0.22222221,0.125,0.11111111]},{"Center":[-1.5764847,10.890436,-1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.1187043,"Uv":[0.25,0.22222221,0.125,0.11111111]},{"Center":[1.5764847,10.890436,-1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.1187043,"Uv":[0.375,0.22222221,0.125,0.11111111]},{"Center":[1.5764847,10.890436,-1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.1187043,"Uv":[0.5,0.22222221,0.125,0.11111111]},{"Center":[1.5764847,10.890436,-1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.1187043,"Uv":[0.625,0.22222221,0.125,0.11111111]},{"Center":[-1.5764847,10.890436,1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.1187043,"Uv":[0.75,0.22222221,0.125,0.11111111]},{"Center":[-1.5764847,10.890436,1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.1187043,"Uv":[0.875,0.22222221,0.125,0.11111111]},{"Center":[-1.5764847,10.890436,1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.1187043,"Uv":[0,0.111111104,0.125,0.11111111]},{"Center":[1.5764847,10.890436,1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.1187043,"Uv":[0.125,0.111111104,0.125,0.11111111]},{"Center":[1.5764847,10.890436,1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.1187043,"Uv":[0.25,0.111111104,0.125,0.11111111]},{"Center":[1.5764847,10.890436,1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.1187043,"Uv":[0.375,0.111111104,0.125,0.11111111]},{"Center":[-1.5764847,13.310532,-1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.118704,"Uv":[0.5,0.111111104,0.125,0.11111111]},{"Center":[-1.5764847,13.310532,-1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.118704,"Uv":[0.625,0.111111104,0.125,0.11111111]},{"Center":[-1.5764847,13.310532,-1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.118704,"Uv":[0.75,0.111111104,0.125,0.11111111]},{"Center":[1.5764847,13.310532,-1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.118704,"Uv":[0.875,0.111111104,0.125,0.11111111]},{"Center":[1.5764847,13.310532,-1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.118704,"Uv":[0,0,0.125,0.11111111]},{"Center":[1.5764847,13.310532,-1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.118704,"Uv":[0.125,0,0.125,0.11111111]},{"Center":[-1.5764847,13.310532,1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.118704,"Uv":[0.25,0,0.125,0.11111111]},{"Center":[-1.5764847,13.310532,1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.118704,"Uv":[0.375,0,0.125,0.11111111]},{"Center":[-1.5764847,13.310532,1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.118704,"Uv":[0.5,0,0.125,0.11111111]},{"Center":[1.5764847,13.310532,1.4915593],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.118704,"Uv":[0.625,0,0.125,0.11111111]},{"Center":[1.5764847,13.310532,1.4915593],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.118704,"Uv":[0.75,0,0.125,0.11111111]},{"Center":[1.5764847,13.310532,1.4915593],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.118704,"Uv":[0.875,0,0.125,0.11111111]}]]} \ No newline at end of file diff --git a/Content/Vegitations/fir_tree_01/prepared/impostor-0-color.png b/Content/Vegitations/fir_tree_01/prepared/impostor-0-color.png new file mode 100644 index 0000000..9994f31 Binary files /dev/null and b/Content/Vegitations/fir_tree_01/prepared/impostor-0-color.png differ diff --git a/Content/Vegitations/fir_tree_01/prepared/impostor-0-normal.png b/Content/Vegitations/fir_tree_01/prepared/impostor-0-normal.png new file mode 100644 index 0000000..3fe0e22 Binary files /dev/null and b/Content/Vegitations/fir_tree_01/prepared/impostor-0-normal.png differ diff --git a/Content/Vegitations/fir_tree_01/prepared/impostor-1-color.png b/Content/Vegitations/fir_tree_01/prepared/impostor-1-color.png new file mode 100644 index 0000000..803e22a Binary files /dev/null and b/Content/Vegitations/fir_tree_01/prepared/impostor-1-color.png differ diff --git a/Content/Vegitations/fir_tree_01/prepared/impostor-1-normal.png b/Content/Vegitations/fir_tree_01/prepared/impostor-1-normal.png new file mode 100644 index 0000000..76a0a33 Binary files /dev/null and b/Content/Vegitations/fir_tree_01/prepared/impostor-1-normal.png differ diff --git a/Content/Vegitations/fir_tree_01/prepared/impostor-2-color.png b/Content/Vegitations/fir_tree_01/prepared/impostor-2-color.png new file mode 100644 index 0000000..535673b Binary files /dev/null and b/Content/Vegitations/fir_tree_01/prepared/impostor-2-color.png differ diff --git a/Content/Vegitations/fir_tree_01/prepared/impostor-2-normal.png b/Content/Vegitations/fir_tree_01/prepared/impostor-2-normal.png new file mode 100644 index 0000000..d3e69f9 Binary files /dev/null and b/Content/Vegitations/fir_tree_01/prepared/impostor-2-normal.png differ diff --git a/Content/Vegitations/fir_tree_01/prepared/impostors.json b/Content/Vegitations/fir_tree_01/prepared/impostors.json new file mode 100644 index 0000000..5f9496e --- /dev/null +++ b/Content/Vegitations/fir_tree_01/prepared/impostors.json @@ -0,0 +1 @@ +{"Views":16,"Elevations":3,"Variants":[{"Center":[0,9.462554,0],"Size":24.080963},{"Center":[0,7.0276027,0],"Size":18.594278},{"Center":[0,7.26029,0],"Size":19.28549}]} \ No newline at end of file diff --git a/Content/Vegitations/fir_tree_01/prepared/model.glb b/Content/Vegitations/fir_tree_01/prepared/model.glb new file mode 100644 index 0000000..cff6f4f Binary files /dev/null and b/Content/Vegitations/fir_tree_01/prepared/model.glb differ diff --git a/Content/Vegitations/fir_tree_01/prepared/preparation.json b/Content/Vegitations/fir_tree_01/prepared/preparation.json new file mode 100644 index 0000000..d558e47 --- /dev/null +++ b/Content/Vegitations/fir_tree_01/prepared/preparation.json @@ -0,0 +1,52 @@ +{ + "source": "https://polyhaven.com/a/fir_tree_01", + "license": "CC0", + "budgets": [ + 12000, + 2000, + 350 + ], + "triangles": [ + 32895, + 5467, + 1037 + ], + "variants": [ + { + "min": [ + -4.498412609100342, + -1.2878752946853638, + -4.471673488616943 + ], + "max": [ + 4.498412609100342, + 20.212984085083008, + 4.471673488616943 + ] + }, + { + "min": [ + -3.847827672958374, + -1.2734137773513794, + -3.958848714828491 + ], + "max": [ + 3.847827672958374, + 15.328619003295898, + 3.958848714828491 + ] + }, + { + "min": [ + -4.135837078094482, + -1.3493037223815918, + -4.0509114265441895 + ], + "max": [ + 4.135837078094482, + 15.86988353729248, + 4.0509114265441895 + ] + } + ] +} \ No newline at end of file diff --git a/Content/Vegitations/fir_tree_01/prepared/stamp.json b/Content/Vegitations/fir_tree_01/prepared/stamp.json new file mode 100644 index 0000000..baa851a --- /dev/null +++ b/Content/Vegitations/fir_tree_01/prepared/stamp.json @@ -0,0 +1 @@ +{"version":5,"cards":true,"files":["937cc3176a17f40d47d655c54571dbd7","5fa93c73d145cacdb258a29e3860a1ae","e9375177560503904ece137474cfde0b","1ffec60c862633f2bb5c47f2102651cf","97ce92fcba67201ace1c4ed3d6e154f6","44fd71883c0e29551243c8937c47b65c","7f6bfdfd4987848b09b119cc1294e177","2072f6b164184f0ca9b41f8623bfec06","6d023f52dab632b031a2ab8e5a27541f","2fcac76cebb4f5fdc7fba2f31e02a490","007dee669d056d81043ed0d171a731c2","cba156ed09519ca12bea8c7ff90585ee","3d0e1b965f2ac48b894d259e4ec5f10c","26d2bf6d4d2e32d3524e8fa3760f643f","1d93929d869e5539ec245ae3eddb7243"]} \ No newline at end of file diff --git a/Content/Vegitations/grass_medium_01/prepared/dry-impostor-0-color.png b/Content/Vegitations/grass_medium_01/prepared/dry-impostor-0-color.png new file mode 100644 index 0000000..ab20f55 Binary files /dev/null and b/Content/Vegitations/grass_medium_01/prepared/dry-impostor-0-color.png differ diff --git a/Content/Vegitations/grass_medium_01/prepared/dry-impostor-0-normal.png b/Content/Vegitations/grass_medium_01/prepared/dry-impostor-0-normal.png new file mode 100644 index 0000000..18fdb09 Binary files /dev/null and b/Content/Vegitations/grass_medium_01/prepared/dry-impostor-0-normal.png differ diff --git a/Content/Vegitations/grass_medium_01/prepared/dry-impostor-1-color.png b/Content/Vegitations/grass_medium_01/prepared/dry-impostor-1-color.png new file mode 100644 index 0000000..bd5ea07 Binary files /dev/null and b/Content/Vegitations/grass_medium_01/prepared/dry-impostor-1-color.png differ diff --git a/Content/Vegitations/grass_medium_01/prepared/dry-impostor-1-normal.png b/Content/Vegitations/grass_medium_01/prepared/dry-impostor-1-normal.png new file mode 100644 index 0000000..dc45507 Binary files /dev/null and b/Content/Vegitations/grass_medium_01/prepared/dry-impostor-1-normal.png differ diff --git a/Content/Vegitations/grass_medium_01/prepared/dry-impostor-2-color.png b/Content/Vegitations/grass_medium_01/prepared/dry-impostor-2-color.png new file mode 100644 index 0000000..477e8f1 Binary files /dev/null and b/Content/Vegitations/grass_medium_01/prepared/dry-impostor-2-color.png differ diff --git a/Content/Vegitations/grass_medium_01/prepared/dry-impostor-2-normal.png b/Content/Vegitations/grass_medium_01/prepared/dry-impostor-2-normal.png new file mode 100644 index 0000000..55e6cd1 Binary files /dev/null and b/Content/Vegitations/grass_medium_01/prepared/dry-impostor-2-normal.png differ diff --git a/Content/Vegitations/grass_medium_01/prepared/dry-impostors.json b/Content/Vegitations/grass_medium_01/prepared/dry-impostors.json new file mode 100644 index 0000000..c982e14 --- /dev/null +++ b/Content/Vegitations/grass_medium_01/prepared/dry-impostors.json @@ -0,0 +1 @@ +{"Views":16,"Elevations":3,"Variants":[{"Center":[0,0.07021334,0],"Size":0.2755095},{"Center":[0,0.11812809,0],"Size":0.26460692},{"Center":[0,0.04119435,0],"Size":0.092275344}]} \ No newline at end of file diff --git a/Content/Vegitations/grass_medium_01/prepared/dry.glb b/Content/Vegitations/grass_medium_01/prepared/dry.glb new file mode 100644 index 0000000..d4905d4 Binary files /dev/null and b/Content/Vegitations/grass_medium_01/prepared/dry.glb differ diff --git a/Content/Vegitations/grass_medium_01/prepared/impostor-0-color.png b/Content/Vegitations/grass_medium_01/prepared/impostor-0-color.png new file mode 100644 index 0000000..94563fa Binary files /dev/null and b/Content/Vegitations/grass_medium_01/prepared/impostor-0-color.png differ diff --git a/Content/Vegitations/grass_medium_01/prepared/impostor-0-normal.png b/Content/Vegitations/grass_medium_01/prepared/impostor-0-normal.png new file mode 100644 index 0000000..18fdb09 Binary files /dev/null and b/Content/Vegitations/grass_medium_01/prepared/impostor-0-normal.png differ diff --git a/Content/Vegitations/grass_medium_01/prepared/impostor-1-color.png b/Content/Vegitations/grass_medium_01/prepared/impostor-1-color.png new file mode 100644 index 0000000..d87a41a Binary files /dev/null and b/Content/Vegitations/grass_medium_01/prepared/impostor-1-color.png differ diff --git a/Content/Vegitations/grass_medium_01/prepared/impostor-1-normal.png b/Content/Vegitations/grass_medium_01/prepared/impostor-1-normal.png new file mode 100644 index 0000000..dc45507 Binary files /dev/null and b/Content/Vegitations/grass_medium_01/prepared/impostor-1-normal.png differ diff --git a/Content/Vegitations/grass_medium_01/prepared/impostor-2-color.png b/Content/Vegitations/grass_medium_01/prepared/impostor-2-color.png new file mode 100644 index 0000000..cddf6a4 Binary files /dev/null and b/Content/Vegitations/grass_medium_01/prepared/impostor-2-color.png differ diff --git a/Content/Vegitations/grass_medium_01/prepared/impostor-2-normal.png b/Content/Vegitations/grass_medium_01/prepared/impostor-2-normal.png new file mode 100644 index 0000000..55e6cd1 Binary files /dev/null and b/Content/Vegitations/grass_medium_01/prepared/impostor-2-normal.png differ diff --git a/Content/Vegitations/grass_medium_01/prepared/impostors.json b/Content/Vegitations/grass_medium_01/prepared/impostors.json new file mode 100644 index 0000000..c982e14 --- /dev/null +++ b/Content/Vegitations/grass_medium_01/prepared/impostors.json @@ -0,0 +1 @@ +{"Views":16,"Elevations":3,"Variants":[{"Center":[0,0.07021334,0],"Size":0.2755095},{"Center":[0,0.11812809,0],"Size":0.26460692},{"Center":[0,0.04119435,0],"Size":0.092275344}]} \ No newline at end of file diff --git a/Content/Vegitations/grass_medium_01/prepared/model.glb b/Content/Vegitations/grass_medium_01/prepared/model.glb new file mode 100644 index 0000000..4513c78 Binary files /dev/null and b/Content/Vegitations/grass_medium_01/prepared/model.glb differ diff --git a/Content/Vegitations/grass_medium_01/prepared/preparation.json b/Content/Vegitations/grass_medium_01/prepared/preparation.json new file mode 100644 index 0000000..aed8f60 --- /dev/null +++ b/Content/Vegitations/grass_medium_01/prepared/preparation.json @@ -0,0 +1,52 @@ +{ + "source": "https://polyhaven.com/a/grass_medium_01", + "license": "CC0", + "budgets": [ + 2500, + 800, + 200 + ], + "triangles": [ + 1213, + 1213, + 387 + ], + "variants": [ + { + "min": [ + -0.0924200564622879, + 0, + -0.08115652203559875 + ], + "max": [ + 0.0924200564622879, + 0.14042668044567108, + 0.08115652203559875 + ] + }, + { + "min": [ + -0.07651543617248535, + 0, + -0.0640784502029419 + ], + "max": [ + 0.07651543617248535, + 0.2362561821937561, + 0.0640784502029419 + ] + }, + { + "min": [ + -0.008015751838684082, + 0, + -0.014501886442303658 + ], + "max": [ + 0.008015751838684082, + 0.08238869905471802, + 0.014501886442303658 + ] + } + ] +} \ No newline at end of file diff --git a/Content/Vegitations/grass_medium_01/prepared/stamp.json b/Content/Vegitations/grass_medium_01/prepared/stamp.json new file mode 100644 index 0000000..ea03b45 --- /dev/null +++ b/Content/Vegitations/grass_medium_01/prepared/stamp.json @@ -0,0 +1 @@ +{"version":4,"cards":false,"files":["784c287f7828f8e30c4d28ebcbd90f6e","7c75ce52a121de1fc00422672584f757","000814eb2149add70695cf71bfb52c4b","a530c693ba3d08be15fcbce33dc5dabe","e0527a0561c2dae5b7b5c1b478d8c6bf","0810166131307c1094d37e73a8490542","8ea1c1853688113b9e1e0b17c4efda5a"]} \ No newline at end of file diff --git a/Content/Vegitations/grass_medium_02/prepared/dry.glb b/Content/Vegitations/grass_medium_02/prepared/dry.glb new file mode 100644 index 0000000..7a3e3da Binary files /dev/null and b/Content/Vegitations/grass_medium_02/prepared/dry.glb differ diff --git a/Content/Vegitations/grass_medium_02/prepared/impostor-0-color.png b/Content/Vegitations/grass_medium_02/prepared/impostor-0-color.png new file mode 100644 index 0000000..6e40aee Binary files /dev/null and b/Content/Vegitations/grass_medium_02/prepared/impostor-0-color.png differ diff --git a/Content/Vegitations/grass_medium_02/prepared/impostor-0-normal.png b/Content/Vegitations/grass_medium_02/prepared/impostor-0-normal.png new file mode 100644 index 0000000..a2867d8 Binary files /dev/null and b/Content/Vegitations/grass_medium_02/prepared/impostor-0-normal.png differ diff --git a/Content/Vegitations/grass_medium_02/prepared/impostor-1-color.png b/Content/Vegitations/grass_medium_02/prepared/impostor-1-color.png new file mode 100644 index 0000000..4a0e3ce Binary files /dev/null and b/Content/Vegitations/grass_medium_02/prepared/impostor-1-color.png differ diff --git a/Content/Vegitations/grass_medium_02/prepared/impostor-1-normal.png b/Content/Vegitations/grass_medium_02/prepared/impostor-1-normal.png new file mode 100644 index 0000000..864416c Binary files /dev/null and b/Content/Vegitations/grass_medium_02/prepared/impostor-1-normal.png differ diff --git a/Content/Vegitations/grass_medium_02/prepared/impostor-2-color.png b/Content/Vegitations/grass_medium_02/prepared/impostor-2-color.png new file mode 100644 index 0000000..b8de68d Binary files /dev/null and b/Content/Vegitations/grass_medium_02/prepared/impostor-2-color.png differ diff --git a/Content/Vegitations/grass_medium_02/prepared/impostor-2-normal.png b/Content/Vegitations/grass_medium_02/prepared/impostor-2-normal.png new file mode 100644 index 0000000..49d7ddd Binary files /dev/null and b/Content/Vegitations/grass_medium_02/prepared/impostor-2-normal.png differ diff --git a/Content/Vegitations/grass_medium_02/prepared/impostors.json b/Content/Vegitations/grass_medium_02/prepared/impostors.json new file mode 100644 index 0000000..dd89ad9 --- /dev/null +++ b/Content/Vegitations/grass_medium_02/prepared/impostors.json @@ -0,0 +1 @@ +{"Views":16,"Elevations":3,"Variants":[{"Center":[0,0.078465186,0],"Size":0.18918738},{"Center":[0,0.08978201,0],"Size":0.3000981},{"Center":[0,0.13018826,0],"Size":0.5391352}]} \ No newline at end of file diff --git a/Content/Vegitations/grass_medium_02/prepared/model.glb b/Content/Vegitations/grass_medium_02/prepared/model.glb new file mode 100644 index 0000000..cf450d2 Binary files /dev/null and b/Content/Vegitations/grass_medium_02/prepared/model.glb differ diff --git a/Content/Vegitations/grass_medium_02/prepared/preparation.json b/Content/Vegitations/grass_medium_02/prepared/preparation.json new file mode 100644 index 0000000..747bcb7 --- /dev/null +++ b/Content/Vegitations/grass_medium_02/prepared/preparation.json @@ -0,0 +1,52 @@ +{ + "source": "https://polyhaven.com/a/grass_medium_02", + "license": "CC0", + "budgets": [ + 2500, + 800, + 200 + ], + "triangles": [ + 4963, + 1995, + 498 + ], + "variants": [ + { + "min": [ + -0.05956093221902847, + 0, + -0.05988120660185814 + ], + "max": [ + 0.05956093221902847, + 0.1569303721189499, + 0.05988120660185814 + ] + }, + { + "min": [ + -0.0895635113120079, + 0, + -0.09963418543338776 + ], + "max": [ + 0.0895635113120079, + 0.17956401407718658, + 0.09963418543338776 + ] + }, + { + "min": [ + -0.1586880385875702, + 0, + -0.18096286058425903 + ], + "max": [ + 0.1586880385875702, + 0.260376513004303, + 0.18096286058425903 + ] + } + ] +} \ No newline at end of file diff --git a/Content/Vegitations/grass_medium_02/prepared/stamp.json b/Content/Vegitations/grass_medium_02/prepared/stamp.json new file mode 100644 index 0000000..a7e361a --- /dev/null +++ b/Content/Vegitations/grass_medium_02/prepared/stamp.json @@ -0,0 +1 @@ +{"version":4,"cards":false,"files":["fc24d97b602c74c1a9357a7570ae2b87","9ca6e2499ce71446f0cd8a3fc8c54a1b","3431e6de76f40f25bef48755f4c6a76a","66eb24d4e6dbda01faad666a3517dcd1","145041ae03fe02783a0adbd1cdcb4988","3b2ba296de3cffad18e8119e6ca693e5","04f9adb6ab1ae0c8bc207be8da6c5dd5"]} \ No newline at end of file diff --git a/Content/Vegitations/ground/forest_ground_04/diff.jpg b/Content/Vegitations/ground/forest_ground_04/diff.jpg new file mode 100644 index 0000000..1279749 Binary files /dev/null and b/Content/Vegitations/ground/forest_ground_04/diff.jpg differ diff --git a/Content/Vegitations/ground/forest_ground_04/nor_gl.jpg b/Content/Vegitations/ground/forest_ground_04/nor_gl.jpg new file mode 100644 index 0000000..0d39601 Binary files /dev/null and b/Content/Vegitations/ground/forest_ground_04/nor_gl.jpg differ diff --git a/Content/Vegitations/ground/forest_ground_04/rough.jpg b/Content/Vegitations/ground/forest_ground_04/rough.jpg new file mode 100644 index 0000000..89a3357 Binary files /dev/null and b/Content/Vegitations/ground/forest_ground_04/rough.jpg differ diff --git a/Content/Vegitations/ground/snow_02/diff.jpg b/Content/Vegitations/ground/snow_02/diff.jpg new file mode 100644 index 0000000..fe43d94 Binary files /dev/null and b/Content/Vegitations/ground/snow_02/diff.jpg differ diff --git a/Content/Vegitations/ground/snow_02/nor_gl.jpg b/Content/Vegitations/ground/snow_02/nor_gl.jpg new file mode 100644 index 0000000..24eff97 Binary files /dev/null and b/Content/Vegitations/ground/snow_02/nor_gl.jpg differ diff --git a/Content/Vegitations/ground/snow_02/rough.jpg b/Content/Vegitations/ground/snow_02/rough.jpg new file mode 100644 index 0000000..6ba6231 Binary files /dev/null and b/Content/Vegitations/ground/snow_02/rough.jpg differ diff --git a/Content/Vegitations/ground/withered_grass/diff.jpg b/Content/Vegitations/ground/withered_grass/diff.jpg new file mode 100644 index 0000000..04fd7d2 Binary files /dev/null and b/Content/Vegitations/ground/withered_grass/diff.jpg differ diff --git a/Content/Vegitations/ground/withered_grass/nor_gl.jpg b/Content/Vegitations/ground/withered_grass/nor_gl.jpg new file mode 100644 index 0000000..a5916ba Binary files /dev/null and b/Content/Vegitations/ground/withered_grass/nor_gl.jpg differ diff --git a/Content/Vegitations/ground/withered_grass/rough.jpg b/Content/Vegitations/ground/withered_grass/rough.jpg new file mode 100644 index 0000000..52d454d Binary files /dev/null and b/Content/Vegitations/ground/withered_grass/rough.jpg differ diff --git a/Content/Vegitations/pine_tree_01/prepared/cards-0-color.png b/Content/Vegitations/pine_tree_01/prepared/cards-0-color.png new file mode 100644 index 0000000..58773d7 Binary files /dev/null and b/Content/Vegitations/pine_tree_01/prepared/cards-0-color.png differ diff --git a/Content/Vegitations/pine_tree_01/prepared/cards-0-normal.png b/Content/Vegitations/pine_tree_01/prepared/cards-0-normal.png new file mode 100644 index 0000000..d9cac2d Binary files /dev/null and b/Content/Vegitations/pine_tree_01/prepared/cards-0-normal.png differ diff --git a/Content/Vegitations/pine_tree_01/prepared/cards-1-color.png b/Content/Vegitations/pine_tree_01/prepared/cards-1-color.png new file mode 100644 index 0000000..2bb8dbc Binary files /dev/null and b/Content/Vegitations/pine_tree_01/prepared/cards-1-color.png differ diff --git a/Content/Vegitations/pine_tree_01/prepared/cards-1-normal.png b/Content/Vegitations/pine_tree_01/prepared/cards-1-normal.png new file mode 100644 index 0000000..45bea3c Binary files /dev/null and b/Content/Vegitations/pine_tree_01/prepared/cards-1-normal.png differ diff --git a/Content/Vegitations/pine_tree_01/prepared/cards-2-color.png b/Content/Vegitations/pine_tree_01/prepared/cards-2-color.png new file mode 100644 index 0000000..c9ae28b Binary files /dev/null and b/Content/Vegitations/pine_tree_01/prepared/cards-2-color.png differ diff --git a/Content/Vegitations/pine_tree_01/prepared/cards-2-normal.png b/Content/Vegitations/pine_tree_01/prepared/cards-2-normal.png new file mode 100644 index 0000000..2dc24d9 Binary files /dev/null and b/Content/Vegitations/pine_tree_01/prepared/cards-2-normal.png differ diff --git a/Content/Vegitations/pine_tree_01/prepared/cards.json b/Content/Vegitations/pine_tree_01/prepared/cards.json new file mode 100644 index 0000000..f644707 --- /dev/null +++ b/Content/Vegitations/pine_tree_01/prepared/cards.json @@ -0,0 +1 @@ +{"Variants":[[{"Center":[-1.9002533,1.6978993,-2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.774379,"Uv":[0,0.8888889,0.125,0.11111111]},{"Center":[-1.9002533,1.6978993,-2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.774379,"Uv":[0.125,0.8888889,0.125,0.11111111]},{"Center":[-1.9002533,1.6978993,-2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.774379,"Uv":[0.25,0.8888889,0.125,0.11111111]},{"Center":[1.9002533,1.6978993,-2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.774379,"Uv":[0.375,0.8888889,0.125,0.11111111]},{"Center":[1.9002533,1.6978993,-2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.774379,"Uv":[0.5,0.8888889,0.125,0.11111111]},{"Center":[1.9002533,1.6978993,-2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.774379,"Uv":[0.625,0.8888889,0.125,0.11111111]},{"Center":[-1.9002533,1.6978993,2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.774379,"Uv":[0.75,0.8888889,0.125,0.11111111]},{"Center":[-1.9002533,1.6978993,2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.774379,"Uv":[0.875,0.8888889,0.125,0.11111111]},{"Center":[-1.9002533,1.6978993,2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.774379,"Uv":[0,0.7777778,0.125,0.11111111]},{"Center":[1.9002533,1.6978993,2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.774379,"Uv":[0.125,0.7777778,0.125,0.11111111]},{"Center":[1.9002533,1.6978993,2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.774379,"Uv":[0.25,0.7777778,0.125,0.11111111]},{"Center":[1.9002533,1.6978993,2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.774379,"Uv":[0.375,0.7777778,0.125,0.11111111]},{"Center":[-1.9002533,5.093698,-2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.774379,"Uv":[0.5,0.7777778,0.125,0.11111111]},{"Center":[-1.9002533,5.093698,-2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.774379,"Uv":[0.625,0.7777778,0.125,0.11111111]},{"Center":[-1.9002533,5.093698,-2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.774379,"Uv":[0.75,0.7777778,0.125,0.11111111]},{"Center":[1.9002533,5.093698,-2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.774379,"Uv":[0.875,0.7777778,0.125,0.11111111]},{"Center":[1.9002533,5.093698,-2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.774379,"Uv":[0,0.6666666,0.125,0.11111111]},{"Center":[1.9002533,5.093698,-2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.774379,"Uv":[0.125,0.6666666,0.125,0.11111111]},{"Center":[-1.9002533,5.093698,2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.774379,"Uv":[0.25,0.6666666,0.125,0.11111111]},{"Center":[-1.9002533,5.093698,2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.774379,"Uv":[0.375,0.6666666,0.125,0.11111111]},{"Center":[-1.9002533,5.093698,2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.774379,"Uv":[0.5,0.6666666,0.125,0.11111111]},{"Center":[1.9002533,5.093698,2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.774379,"Uv":[0.625,0.6666666,0.125,0.11111111]},{"Center":[1.9002533,5.093698,2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.774379,"Uv":[0.75,0.6666666,0.125,0.11111111]},{"Center":[1.9002533,5.093698,2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.774379,"Uv":[0.875,0.6666666,0.125,0.11111111]},{"Center":[-1.9002533,8.489496,-2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.774379,"Uv":[0,0.5555556,0.125,0.11111111]},{"Center":[-1.9002533,8.489496,-2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.774379,"Uv":[0.125,0.5555556,0.125,0.11111111]},{"Center":[-1.9002533,8.489496,-2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.774379,"Uv":[0.25,0.5555556,0.125,0.11111111]},{"Center":[1.9002533,8.489496,-2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.774379,"Uv":[0.375,0.5555556,0.125,0.11111111]},{"Center":[1.9002533,8.489496,-2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.774379,"Uv":[0.5,0.5555556,0.125,0.11111111]},{"Center":[1.9002533,8.489496,-2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.774379,"Uv":[0.625,0.5555556,0.125,0.11111111]},{"Center":[-1.9002533,8.489496,2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.774379,"Uv":[0.75,0.5555556,0.125,0.11111111]},{"Center":[-1.9002533,8.489496,2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.774379,"Uv":[0.875,0.5555556,0.125,0.11111111]},{"Center":[-1.9002533,8.489496,2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.774379,"Uv":[0,0.44444442,0.125,0.11111111]},{"Center":[1.9002533,8.489496,2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.774379,"Uv":[0.125,0.44444442,0.125,0.11111111]},{"Center":[1.9002533,8.489496,2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.774379,"Uv":[0.25,0.44444442,0.125,0.11111111]},{"Center":[1.9002533,8.489496,2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.774379,"Uv":[0.375,0.44444442,0.125,0.11111111]},{"Center":[-1.9002533,11.885296,-2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.774379,"Uv":[0.5,0.44444442,0.125,0.11111111]},{"Center":[-1.9002533,11.885296,-2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.774379,"Uv":[0.625,0.44444442,0.125,0.11111111]},{"Center":[-1.9002533,11.885296,-2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.774379,"Uv":[0.75,0.44444442,0.125,0.11111111]},{"Center":[1.9002533,11.885296,-2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.774379,"Uv":[0.875,0.44444442,0.125,0.11111111]},{"Center":[1.9002533,11.885296,-2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.774379,"Uv":[0,0.3333333,0.125,0.11111111]},{"Center":[1.9002533,11.885296,-2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.774379,"Uv":[0.125,0.3333333,0.125,0.11111111]},{"Center":[-1.9002533,11.885296,2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.774379,"Uv":[0.25,0.3333333,0.125,0.11111111]},{"Center":[-1.9002533,11.885296,2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.774379,"Uv":[0.375,0.3333333,0.125,0.11111111]},{"Center":[-1.9002533,11.885296,2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.774379,"Uv":[0.5,0.3333333,0.125,0.11111111]},{"Center":[1.9002533,11.885296,2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.774379,"Uv":[0.625,0.3333333,0.125,0.11111111]},{"Center":[1.9002533,11.885296,2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.774379,"Uv":[0.75,0.3333333,0.125,0.11111111]},{"Center":[1.9002533,11.885296,2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.774379,"Uv":[0.875,0.3333333,0.125,0.11111111]},{"Center":[-1.9002533,15.281094,-2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.7743783,"Uv":[0,0.22222221,0.125,0.11111111]},{"Center":[-1.9002533,15.281094,-2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.7743783,"Uv":[0.125,0.22222221,0.125,0.11111111]},{"Center":[-1.9002533,15.281094,-2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.7743783,"Uv":[0.25,0.22222221,0.125,0.11111111]},{"Center":[1.9002533,15.281094,-2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.7743783,"Uv":[0.375,0.22222221,0.125,0.11111111]},{"Center":[1.9002533,15.281094,-2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.7743783,"Uv":[0.5,0.22222221,0.125,0.11111111]},{"Center":[1.9002533,15.281094,-2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.7743783,"Uv":[0.625,0.22222221,0.125,0.11111111]},{"Center":[-1.9002533,15.281094,2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.7743783,"Uv":[0.75,0.22222221,0.125,0.11111111]},{"Center":[-1.9002533,15.281094,2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.7743783,"Uv":[0.875,0.22222221,0.125,0.11111111]},{"Center":[-1.9002533,15.281094,2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.7743783,"Uv":[0,0.111111104,0.125,0.11111111]},{"Center":[1.9002533,15.281094,2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.7743783,"Uv":[0.125,0.111111104,0.125,0.11111111]},{"Center":[1.9002533,15.281094,2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.7743783,"Uv":[0.25,0.111111104,0.125,0.11111111]},{"Center":[1.9002533,15.281094,2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.7743783,"Uv":[0.375,0.111111104,0.125,0.11111111]},{"Center":[-1.9002533,18.676891,-2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.7743783,"Uv":[0.5,0.111111104,0.125,0.11111111]},{"Center":[-1.9002533,18.676891,-2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.7743783,"Uv":[0.625,0.111111104,0.125,0.11111111]},{"Center":[-1.9002533,18.676891,-2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.7743783,"Uv":[0.75,0.111111104,0.125,0.11111111]},{"Center":[1.9002533,18.676891,-2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.7743783,"Uv":[0.875,0.111111104,0.125,0.11111111]},{"Center":[1.9002533,18.676891,-2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.7743783,"Uv":[0,0,0.125,0.11111111]},{"Center":[1.9002533,18.676891,-2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.7743783,"Uv":[0.125,0,0.125,0.11111111]},{"Center":[-1.9002533,18.676891,2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.7743783,"Uv":[0.25,0,0.125,0.11111111]},{"Center":[-1.9002533,18.676891,2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.7743783,"Uv":[0.375,0,0.125,0.11111111]},{"Center":[-1.9002533,18.676891,2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.7743783,"Uv":[0.5,0,0.125,0.11111111]},{"Center":[1.9002533,18.676891,2.0786119],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.7743783,"Uv":[0.625,0,0.125,0.11111111]},{"Center":[1.9002533,18.676891,2.0786119],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.7743783,"Uv":[0.75,0,0.125,0.11111111]},{"Center":[1.9002533,18.676891,2.0786119],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.7743783,"Uv":[0.875,0,0.125,0.11111111]}],[{"Center":[-1.8059375,1.240105,-1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.972738,"Uv":[0,0.8888889,0.125,0.11111111]},{"Center":[-1.8059375,1.240105,-1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.972738,"Uv":[0.125,0.8888889,0.125,0.11111111]},{"Center":[-1.8059375,1.240105,-1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.972738,"Uv":[0.25,0.8888889,0.125,0.11111111]},{"Center":[1.8059375,1.240105,-1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.972738,"Uv":[0.375,0.8888889,0.125,0.11111111]},{"Center":[1.8059375,1.240105,-1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.972738,"Uv":[0.5,0.8888889,0.125,0.11111111]},{"Center":[1.8059375,1.240105,-1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.972738,"Uv":[0.625,0.8888889,0.125,0.11111111]},{"Center":[-1.8059375,1.240105,1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.972738,"Uv":[0.75,0.8888889,0.125,0.11111111]},{"Center":[-1.8059375,1.240105,1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.972738,"Uv":[0.875,0.8888889,0.125,0.11111111]},{"Center":[-1.8059375,1.240105,1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.972738,"Uv":[0,0.7777778,0.125,0.11111111]},{"Center":[1.8059375,1.240105,1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.972738,"Uv":[0.125,0.7777778,0.125,0.11111111]},{"Center":[1.8059375,1.240105,1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.972738,"Uv":[0.25,0.7777778,0.125,0.11111111]},{"Center":[1.8059375,1.240105,1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.972738,"Uv":[0.375,0.7777778,0.125,0.11111111]},{"Center":[-1.8059375,3.720315,-1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.972738,"Uv":[0.5,0.7777778,0.125,0.11111111]},{"Center":[-1.8059375,3.720315,-1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.972738,"Uv":[0.625,0.7777778,0.125,0.11111111]},{"Center":[-1.8059375,3.720315,-1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.972738,"Uv":[0.75,0.7777778,0.125,0.11111111]},{"Center":[1.8059375,3.720315,-1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.972738,"Uv":[0.875,0.7777778,0.125,0.11111111]},{"Center":[1.8059375,3.720315,-1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.972738,"Uv":[0,0.6666666,0.125,0.11111111]},{"Center":[1.8059375,3.720315,-1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.972738,"Uv":[0.125,0.6666666,0.125,0.11111111]},{"Center":[-1.8059375,3.720315,1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.972738,"Uv":[0.25,0.6666666,0.125,0.11111111]},{"Center":[-1.8059375,3.720315,1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.972738,"Uv":[0.375,0.6666666,0.125,0.11111111]},{"Center":[-1.8059375,3.720315,1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.972738,"Uv":[0.5,0.6666666,0.125,0.11111111]},{"Center":[1.8059375,3.720315,1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.972738,"Uv":[0.625,0.6666666,0.125,0.11111111]},{"Center":[1.8059375,3.720315,1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.972738,"Uv":[0.75,0.6666666,0.125,0.11111111]},{"Center":[1.8059375,3.720315,1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.972738,"Uv":[0.875,0.6666666,0.125,0.11111111]},{"Center":[-1.8059375,6.2005253,-1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.9727373,"Uv":[0,0.5555556,0.125,0.11111111]},{"Center":[-1.8059375,6.2005253,-1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.9727373,"Uv":[0.125,0.5555556,0.125,0.11111111]},{"Center":[-1.8059375,6.2005253,-1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.9727373,"Uv":[0.25,0.5555556,0.125,0.11111111]},{"Center":[1.8059375,6.2005253,-1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.9727373,"Uv":[0.375,0.5555556,0.125,0.11111111]},{"Center":[1.8059375,6.2005253,-1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.9727373,"Uv":[0.5,0.5555556,0.125,0.11111111]},{"Center":[1.8059375,6.2005253,-1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.9727373,"Uv":[0.625,0.5555556,0.125,0.11111111]},{"Center":[-1.8059375,6.2005253,1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.9727373,"Uv":[0.75,0.5555556,0.125,0.11111111]},{"Center":[-1.8059375,6.2005253,1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.9727373,"Uv":[0.875,0.5555556,0.125,0.11111111]},{"Center":[-1.8059375,6.2005253,1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.9727373,"Uv":[0,0.44444442,0.125,0.11111111]},{"Center":[1.8059375,6.2005253,1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.9727373,"Uv":[0.125,0.44444442,0.125,0.11111111]},{"Center":[1.8059375,6.2005253,1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.9727373,"Uv":[0.25,0.44444442,0.125,0.11111111]},{"Center":[1.8059375,6.2005253,1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.9727373,"Uv":[0.375,0.44444442,0.125,0.11111111]},{"Center":[-1.8059375,8.680735,-1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.972738,"Uv":[0.5,0.44444442,0.125,0.11111111]},{"Center":[-1.8059375,8.680735,-1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.972738,"Uv":[0.625,0.44444442,0.125,0.11111111]},{"Center":[-1.8059375,8.680735,-1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.972738,"Uv":[0.75,0.44444442,0.125,0.11111111]},{"Center":[1.8059375,8.680735,-1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.972738,"Uv":[0.875,0.44444442,0.125,0.11111111]},{"Center":[1.8059375,8.680735,-1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.972738,"Uv":[0,0.3333333,0.125,0.11111111]},{"Center":[1.8059375,8.680735,-1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.972738,"Uv":[0.125,0.3333333,0.125,0.11111111]},{"Center":[-1.8059375,8.680735,1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.972738,"Uv":[0.25,0.3333333,0.125,0.11111111]},{"Center":[-1.8059375,8.680735,1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.972738,"Uv":[0.375,0.3333333,0.125,0.11111111]},{"Center":[-1.8059375,8.680735,1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.972738,"Uv":[0.5,0.3333333,0.125,0.11111111]},{"Center":[1.8059375,8.680735,1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.972738,"Uv":[0.625,0.3333333,0.125,0.11111111]},{"Center":[1.8059375,8.680735,1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.972738,"Uv":[0.75,0.3333333,0.125,0.11111111]},{"Center":[1.8059375,8.680735,1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.972738,"Uv":[0.875,0.3333333,0.125,0.11111111]},{"Center":[-1.8059375,11.160946,-1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.972738,"Uv":[0,0.22222221,0.125,0.11111111]},{"Center":[-1.8059375,11.160946,-1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.972738,"Uv":[0.125,0.22222221,0.125,0.11111111]},{"Center":[-1.8059375,11.160946,-1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.972738,"Uv":[0.25,0.22222221,0.125,0.11111111]},{"Center":[1.8059375,11.160946,-1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.972738,"Uv":[0.375,0.22222221,0.125,0.11111111]},{"Center":[1.8059375,11.160946,-1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.972738,"Uv":[0.5,0.22222221,0.125,0.11111111]},{"Center":[1.8059375,11.160946,-1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.972738,"Uv":[0.625,0.22222221,0.125,0.11111111]},{"Center":[-1.8059375,11.160946,1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.972738,"Uv":[0.75,0.22222221,0.125,0.11111111]},{"Center":[-1.8059375,11.160946,1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.972738,"Uv":[0.875,0.22222221,0.125,0.11111111]},{"Center":[-1.8059375,11.160946,1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.972738,"Uv":[0,0.111111104,0.125,0.11111111]},{"Center":[1.8059375,11.160946,1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.972738,"Uv":[0.125,0.111111104,0.125,0.11111111]},{"Center":[1.8059375,11.160946,1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.972738,"Uv":[0.25,0.111111104,0.125,0.11111111]},{"Center":[1.8059375,11.160946,1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.972738,"Uv":[0.375,0.111111104,0.125,0.11111111]},{"Center":[-1.8059375,13.641155,-1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.972738,"Uv":[0.5,0.111111104,0.125,0.11111111]},{"Center":[-1.8059375,13.641155,-1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.972738,"Uv":[0.625,0.111111104,0.125,0.11111111]},{"Center":[-1.8059375,13.641155,-1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.972738,"Uv":[0.75,0.111111104,0.125,0.11111111]},{"Center":[1.8059375,13.641155,-1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.972738,"Uv":[0.875,0.111111104,0.125,0.11111111]},{"Center":[1.8059375,13.641155,-1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.972738,"Uv":[0,0,0.125,0.11111111]},{"Center":[1.8059375,13.641155,-1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.972738,"Uv":[0.125,0,0.125,0.11111111]},{"Center":[-1.8059375,13.641155,1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.972738,"Uv":[0.25,0,0.125,0.11111111]},{"Center":[-1.8059375,13.641155,1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.972738,"Uv":[0.375,0,0.125,0.11111111]},{"Center":[-1.8059375,13.641155,1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.972738,"Uv":[0.5,0,0.125,0.11111111]},{"Center":[1.8059375,13.641155,1.8992565],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":5.972738,"Uv":[0.625,0,0.125,0.11111111]},{"Center":[1.8059375,13.641155,1.8992565],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":5.972738,"Uv":[0.75,0,0.125,0.11111111]},{"Center":[1.8059375,13.641155,1.8992565],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":5.972738,"Uv":[0.875,0,0.125,0.11111111]}],[{"Center":[-1.9064286,1.4620003,-1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0,0.8888889,0.125,0.11111111]},{"Center":[-1.9064286,1.4620003,-1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.125,0.8888889,0.125,0.11111111]},{"Center":[-1.9064286,1.4620003,-1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.25,0.8888889,0.125,0.11111111]},{"Center":[1.9064286,1.4620003,-1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.375,0.8888889,0.125,0.11111111]},{"Center":[1.9064286,1.4620003,-1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.5,0.8888889,0.125,0.11111111]},{"Center":[1.9064286,1.4620003,-1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.625,0.8888889,0.125,0.11111111]},{"Center":[-1.9064286,1.4620003,1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.75,0.8888889,0.125,0.11111111]},{"Center":[-1.9064286,1.4620003,1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.875,0.8888889,0.125,0.11111111]},{"Center":[-1.9064286,1.4620003,1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0,0.7777778,0.125,0.11111111]},{"Center":[1.9064286,1.4620003,1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.125,0.7777778,0.125,0.11111111]},{"Center":[1.9064286,1.4620003,1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.25,0.7777778,0.125,0.11111111]},{"Center":[1.9064286,1.4620003,1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.375,0.7777778,0.125,0.11111111]},{"Center":[-1.9064286,4.3860006,-1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.5,0.7777778,0.125,0.11111111]},{"Center":[-1.9064286,4.3860006,-1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.625,0.7777778,0.125,0.11111111]},{"Center":[-1.9064286,4.3860006,-1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.75,0.7777778,0.125,0.11111111]},{"Center":[1.9064286,4.3860006,-1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.875,0.7777778,0.125,0.11111111]},{"Center":[1.9064286,4.3860006,-1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0,0.6666666,0.125,0.11111111]},{"Center":[1.9064286,4.3860006,-1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.125,0.6666666,0.125,0.11111111]},{"Center":[-1.9064286,4.3860006,1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.25,0.6666666,0.125,0.11111111]},{"Center":[-1.9064286,4.3860006,1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.375,0.6666666,0.125,0.11111111]},{"Center":[-1.9064286,4.3860006,1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.5,0.6666666,0.125,0.11111111]},{"Center":[1.9064286,4.3860006,1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.625,0.6666666,0.125,0.11111111]},{"Center":[1.9064286,4.3860006,1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.75,0.6666666,0.125,0.11111111]},{"Center":[1.9064286,4.3860006,1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.875,0.6666666,0.125,0.11111111]},{"Center":[-1.9064286,7.3100014,-1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0,0.5555556,0.125,0.11111111]},{"Center":[-1.9064286,7.3100014,-1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.125,0.5555556,0.125,0.11111111]},{"Center":[-1.9064286,7.3100014,-1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.25,0.5555556,0.125,0.11111111]},{"Center":[1.9064286,7.3100014,-1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.375,0.5555556,0.125,0.11111111]},{"Center":[1.9064286,7.3100014,-1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.5,0.5555556,0.125,0.11111111]},{"Center":[1.9064286,7.3100014,-1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.625,0.5555556,0.125,0.11111111]},{"Center":[-1.9064286,7.3100014,1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.75,0.5555556,0.125,0.11111111]},{"Center":[-1.9064286,7.3100014,1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.875,0.5555556,0.125,0.11111111]},{"Center":[-1.9064286,7.3100014,1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0,0.44444442,0.125,0.11111111]},{"Center":[1.9064286,7.3100014,1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.125,0.44444442,0.125,0.11111111]},{"Center":[1.9064286,7.3100014,1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.25,0.44444442,0.125,0.11111111]},{"Center":[1.9064286,7.3100014,1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.375,0.44444442,0.125,0.11111111]},{"Center":[-1.9064286,10.234001,-1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.5,0.44444442,0.125,0.11111111]},{"Center":[-1.9064286,10.234001,-1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.625,0.44444442,0.125,0.11111111]},{"Center":[-1.9064286,10.234001,-1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.75,0.44444442,0.125,0.11111111]},{"Center":[1.9064286,10.234001,-1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.875,0.44444442,0.125,0.11111111]},{"Center":[1.9064286,10.234001,-1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0,0.3333333,0.125,0.11111111]},{"Center":[1.9064286,10.234001,-1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.125,0.3333333,0.125,0.11111111]},{"Center":[-1.9064286,10.234001,1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.25,0.3333333,0.125,0.11111111]},{"Center":[-1.9064286,10.234001,1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.375,0.3333333,0.125,0.11111111]},{"Center":[-1.9064286,10.234001,1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.5,0.3333333,0.125,0.11111111]},{"Center":[1.9064286,10.234001,1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.625,0.3333333,0.125,0.11111111]},{"Center":[1.9064286,10.234001,1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.75,0.3333333,0.125,0.11111111]},{"Center":[1.9064286,10.234001,1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.875,0.3333333,0.125,0.11111111]},{"Center":[-1.9064286,13.158003,-1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0,0.22222221,0.125,0.11111111]},{"Center":[-1.9064286,13.158003,-1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.125,0.22222221,0.125,0.11111111]},{"Center":[-1.9064286,13.158003,-1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.25,0.22222221,0.125,0.11111111]},{"Center":[1.9064286,13.158003,-1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.375,0.22222221,0.125,0.11111111]},{"Center":[1.9064286,13.158003,-1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.5,0.22222221,0.125,0.11111111]},{"Center":[1.9064286,13.158003,-1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.625,0.22222221,0.125,0.11111111]},{"Center":[-1.9064286,13.158003,1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.75,0.22222221,0.125,0.11111111]},{"Center":[-1.9064286,13.158003,1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.875,0.22222221,0.125,0.11111111]},{"Center":[-1.9064286,13.158003,1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0,0.111111104,0.125,0.11111111]},{"Center":[1.9064286,13.158003,1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.125,0.111111104,0.125,0.11111111]},{"Center":[1.9064286,13.158003,1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.25,0.111111104,0.125,0.11111111]},{"Center":[1.9064286,13.158003,1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.375,0.111111104,0.125,0.11111111]},{"Center":[-1.9064286,16.082003,-1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.5,0.111111104,0.125,0.11111111]},{"Center":[-1.9064286,16.082003,-1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.625,0.111111104,0.125,0.11111111]},{"Center":[-1.9064286,16.082003,-1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.75,0.111111104,0.125,0.11111111]},{"Center":[1.9064286,16.082003,-1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.875,0.111111104,0.125,0.11111111]},{"Center":[1.9064286,16.082003,-1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0,0,0.125,0.11111111]},{"Center":[1.9064286,16.082003,-1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.125,0,0.125,0.11111111]},{"Center":[-1.9064286,16.082003,1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.25,0,0.125,0.11111111]},{"Center":[-1.9064286,16.082003,1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.375,0,0.125,0.11111111]},{"Center":[-1.9064286,16.082003,1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.5,0,0.125,0.11111111]},{"Center":[1.9064286,16.082003,1.9638715],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":6.392212,"Uv":[0.625,0,0.125,0.11111111]},{"Center":[1.9064286,16.082003,1.9638715],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":6.392212,"Uv":[0.75,0,0.125,0.11111111]},{"Center":[1.9064286,16.082003,1.9638715],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":6.392212,"Uv":[0.875,0,0.125,0.11111111]}]]} \ No newline at end of file diff --git a/Content/Vegitations/pine_tree_01/prepared/impostor-0-color.png b/Content/Vegitations/pine_tree_01/prepared/impostor-0-color.png new file mode 100644 index 0000000..45e3365 Binary files /dev/null and b/Content/Vegitations/pine_tree_01/prepared/impostor-0-color.png differ diff --git a/Content/Vegitations/pine_tree_01/prepared/impostor-0-normal.png b/Content/Vegitations/pine_tree_01/prepared/impostor-0-normal.png new file mode 100644 index 0000000..1db51f2 Binary files /dev/null and b/Content/Vegitations/pine_tree_01/prepared/impostor-0-normal.png differ diff --git a/Content/Vegitations/pine_tree_01/prepared/impostor-1-color.png b/Content/Vegitations/pine_tree_01/prepared/impostor-1-color.png new file mode 100644 index 0000000..9d680e1 Binary files /dev/null and b/Content/Vegitations/pine_tree_01/prepared/impostor-1-color.png differ diff --git a/Content/Vegitations/pine_tree_01/prepared/impostor-1-normal.png b/Content/Vegitations/pine_tree_01/prepared/impostor-1-normal.png new file mode 100644 index 0000000..4f0798e Binary files /dev/null and b/Content/Vegitations/pine_tree_01/prepared/impostor-1-normal.png differ diff --git a/Content/Vegitations/pine_tree_01/prepared/impostor-2-color.png b/Content/Vegitations/pine_tree_01/prepared/impostor-2-color.png new file mode 100644 index 0000000..1ec2245 Binary files /dev/null and b/Content/Vegitations/pine_tree_01/prepared/impostor-2-color.png differ diff --git a/Content/Vegitations/pine_tree_01/prepared/impostor-2-normal.png b/Content/Vegitations/pine_tree_01/prepared/impostor-2-normal.png new file mode 100644 index 0000000..db3d584 Binary files /dev/null and b/Content/Vegitations/pine_tree_01/prepared/impostor-2-normal.png differ diff --git a/Content/Vegitations/pine_tree_01/prepared/impostors.json b/Content/Vegitations/pine_tree_01/prepared/impostors.json new file mode 100644 index 0000000..2bffa38 --- /dev/null +++ b/Content/Vegitations/pine_tree_01/prepared/impostors.json @@ -0,0 +1 @@ +{"Views":16,"Elevations":3,"Variants":[{"Center":[0,10.187394,0],"Size":26.603775},{"Center":[0,7.44063,0],"Size":20.578642},{"Center":[0,8.772001,0],"Size":23.53368}]} \ No newline at end of file diff --git a/Content/Vegitations/pine_tree_01/prepared/model.glb b/Content/Vegitations/pine_tree_01/prepared/model.glb new file mode 100644 index 0000000..33f2bb9 Binary files /dev/null and b/Content/Vegitations/pine_tree_01/prepared/model.glb differ diff --git a/Content/Vegitations/pine_tree_01/prepared/preparation.json b/Content/Vegitations/pine_tree_01/prepared/preparation.json new file mode 100644 index 0000000..902f69d --- /dev/null +++ b/Content/Vegitations/pine_tree_01/prepared/preparation.json @@ -0,0 +1,52 @@ +{ + "source": "https://polyhaven.com/a/pine_tree_01", + "license": "CC0", + "budgets": [ + 12000, + 2000, + 350 + ], + "triangles": [ + 33340, + 5610, + 1039 + ], + "variants": [ + { + "min": [ + -5.287442684173584, + -1.689290165901184, + -5.465801239013672 + ], + "max": [ + 5.287442684173584, + 22.06407928466797, + 5.465801239013672 + ] + }, + { + "min": [ + -4.792306423187256, + -1.7462639808654785, + -4.88562536239624 + ], + "max": [ + 4.792306423187256, + 16.62752342224121, + 4.88562536239624 + ] + }, + { + "min": [ + -5.102534770965576, + -1.7341057062149048, + -5.159977436065674 + ], + "max": [ + 5.102534770965576, + 19.278108596801758, + 5.159977436065674 + ] + } + ] +} \ No newline at end of file diff --git a/Content/Vegitations/pine_tree_01/prepared/stamp.json b/Content/Vegitations/pine_tree_01/prepared/stamp.json new file mode 100644 index 0000000..0fd09ea --- /dev/null +++ b/Content/Vegitations/pine_tree_01/prepared/stamp.json @@ -0,0 +1 @@ +{"version":5,"cards":true,"files":["001c1a0eb6748333f000d3fa0db5e278","da6eb7bc3c4eafea4e8ed696f7891a73","f420cb9ca2e845cd988f3bcf03c41a85","218f22567f42e7918afc25c7e3450be8","97ce92fcba67201ace1c4ed3d6e154f6","44fd71883c0e29551243c8937c47b65c","c27af4b3374a4f36780b316b3a10bfd5","843dd3359c958c7a8bfd60f55fb4fb80","45d577b1b780711976147d66234a1f4c","d9110a0de287f6abacb7abe4c50ae9b3","007dee669d056d81043ed0d171a731c2","cba156ed09519ca12bea8c7ff90585ee","36a7440fbb145fc68e99c1b6a5f20047","3079e56cc48c57d8ba530d005e86bae6","85f9f62d98dc35cce642895d75f79fa6"]} \ No newline at end of file diff --git a/Content/Vegitations/shrub_01/prepared/impostor-0-color.png b/Content/Vegitations/shrub_01/prepared/impostor-0-color.png new file mode 100644 index 0000000..232ab8a Binary files /dev/null and b/Content/Vegitations/shrub_01/prepared/impostor-0-color.png differ diff --git a/Content/Vegitations/shrub_01/prepared/impostor-0-normal.png b/Content/Vegitations/shrub_01/prepared/impostor-0-normal.png new file mode 100644 index 0000000..8fc96ce Binary files /dev/null and b/Content/Vegitations/shrub_01/prepared/impostor-0-normal.png differ diff --git a/Content/Vegitations/shrub_01/prepared/impostors.json b/Content/Vegitations/shrub_01/prepared/impostors.json new file mode 100644 index 0000000..3330065 --- /dev/null +++ b/Content/Vegitations/shrub_01/prepared/impostors.json @@ -0,0 +1 @@ +{"Views":16,"Elevations":3,"Variants":[{"Center":[1.0848045E-05,0.19774766,3.1113625E-05],"Size":2.9066532}]} \ No newline at end of file diff --git a/Content/Vegitations/shrub_01/prepared/model.glb b/Content/Vegitations/shrub_01/prepared/model.glb new file mode 100644 index 0000000..b7353bb Binary files /dev/null and b/Content/Vegitations/shrub_01/prepared/model.glb differ diff --git a/Content/Vegitations/shrub_01/prepared/preparation.json b/Content/Vegitations/shrub_01/prepared/preparation.json new file mode 100644 index 0000000..c4f999e --- /dev/null +++ b/Content/Vegitations/shrub_01/prepared/preparation.json @@ -0,0 +1,28 @@ +{ + "source": "https://polyhaven.com/a/shrub_01", + "license": "CC0", + "budgets": [ + 12000, + 3000, + 600 + ], + "triangles": [ + 11897, + 2992, + 600 + ], + "variants": [ + { + "min": [ + -1.2933306694030762, + 0, + -0.10928048193454742 + ], + "max": [ + 1.2933306694030762, + 0.3959742784500122, + 0.10928048193454742 + ] + } + ] +} \ No newline at end of file diff --git a/Content/Vegitations/shrub_01/prepared/stamp.json b/Content/Vegitations/shrub_01/prepared/stamp.json new file mode 100644 index 0000000..88a5f01 --- /dev/null +++ b/Content/Vegitations/shrub_01/prepared/stamp.json @@ -0,0 +1 @@ +{"version":4,"cards":false,"files":["b4dba9051ffc1eae61780af528973499","ba76e790f9346ad2366585b57095c214","94b1eac99e9f949487d49243d19f70e4","baf77d162f397f43fa4737b545d73e1a","cd89f941ba9e5c60cbeee50e0b269e2c","dabea4c2ea5710c231d58cf94b85e6b3"]} \ No newline at end of file diff --git a/Content/Vegitations/shrub_04/prepared/impostor-0-color.png b/Content/Vegitations/shrub_04/prepared/impostor-0-color.png new file mode 100644 index 0000000..62f0bb6 Binary files /dev/null and b/Content/Vegitations/shrub_04/prepared/impostor-0-color.png differ diff --git a/Content/Vegitations/shrub_04/prepared/impostor-0-normal.png b/Content/Vegitations/shrub_04/prepared/impostor-0-normal.png new file mode 100644 index 0000000..7bdd5fa Binary files /dev/null and b/Content/Vegitations/shrub_04/prepared/impostor-0-normal.png differ diff --git a/Content/Vegitations/shrub_04/prepared/impostors.json b/Content/Vegitations/shrub_04/prepared/impostors.json new file mode 100644 index 0000000..19e2cad --- /dev/null +++ b/Content/Vegitations/shrub_04/prepared/impostors.json @@ -0,0 +1 @@ +{"Views":16,"Elevations":3,"Variants":[{"Center":[0,0.10959984,0],"Size":0.67390007}]} \ No newline at end of file diff --git a/Content/Vegitations/shrub_04/prepared/model.glb b/Content/Vegitations/shrub_04/prepared/model.glb new file mode 100644 index 0000000..f85e5ba Binary files /dev/null and b/Content/Vegitations/shrub_04/prepared/model.glb differ diff --git a/Content/Vegitations/shrub_04/prepared/preparation.json b/Content/Vegitations/shrub_04/prepared/preparation.json new file mode 100644 index 0000000..6e3f5a2 --- /dev/null +++ b/Content/Vegitations/shrub_04/prepared/preparation.json @@ -0,0 +1,28 @@ +{ + "source": "https://polyhaven.com/a/shrub_04", + "license": "CC0", + "budgets": [ + 12000, + 3000, + 600 + ], + "triangles": [ + 11969, + 2894, + 591 + ], + "variants": [ + { + "min": [ + -0.29184508323669434, + 0, + -0.07304864376783371 + ], + "max": [ + 0.29184508323669434, + 0.21919968724250793, + 0.07304864376783371 + ] + } + ] +} \ No newline at end of file diff --git a/Content/Vegitations/shrub_04/prepared/stamp.json b/Content/Vegitations/shrub_04/prepared/stamp.json new file mode 100644 index 0000000..7148f30 --- /dev/null +++ b/Content/Vegitations/shrub_04/prepared/stamp.json @@ -0,0 +1 @@ +{"version":4,"cards":false,"files":["7a6f451aea5cc9bae36ce9b027dc25cf","f37749dbbfe57024799df7b96f896643","987a672e598f9dc04b6d23cb8f36d697","ca4ecc50015a85e38675a4967e843485","76f10cb443bc5964ec9484cfea474196","c580b41967ed8e43358c2b8621fd07b1"]} \ No newline at end of file diff --git a/Content/Vegitations/sources.json b/Content/Vegitations/sources.json new file mode 100644 index 0000000..59de437 --- /dev/null +++ b/Content/Vegitations/sources.json @@ -0,0 +1,596 @@ +{ + "provider": "Poly Haven", + "license": "CC0", + "source": "https://polyhaven.com", + "files": [ + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/gltf/2k/pine_tree_01/pine_tree_01_2k.gltf", + "size": 30596, + "md5": "001c1a0eb6748333f000d3fa0db5e278", + "path": "pine_tree_01/source/model.gltf" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/pine_tree_01/pine_tree_01_bark_nor_gl_2k.jpg", + "size": 3373886, + "md5": "da6eb7bc3c4eafea4e8ed696f7891a73", + "path": "pine_tree_01/source/textures/pine_tree_01_bark_nor_gl_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/pine_tree_01/pine_tree_01_bark_diff_2k.jpg", + "size": 2700448, + "md5": "f420cb9ca2e845cd988f3bcf03c41a85", + "path": "pine_tree_01/source/textures/pine_tree_01_bark_diff_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/pine_tree_01/pine_tree_01_bark_arm_2k.jpg", + "size": 2941366, + "md5": "218f22567f42e7918afc25c7e3450be8", + "path": "pine_tree_01/source/textures/pine_tree_01_bark_arm_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/pine_tree_01/pine_tree_01_trunk_a_nor_gl_2k.jpg", + "size": 3642074, + "md5": "97ce92fcba67201ace1c4ed3d6e154f6", + "path": "pine_tree_01/source/textures/pine_tree_01_trunk_a_nor_gl_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/pine_tree_01/pine_tree_01_trunk_a_diff_2k.jpg", + "size": 3028704, + "md5": "44fd71883c0e29551243c8937c47b65c", + "path": "pine_tree_01/source/textures/pine_tree_01_trunk_a_diff_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/pine_tree_01/pine_tree_01_trunk_a_arm_2k.jpg", + "size": 2974686, + "md5": "c27af4b3374a4f36780b316b3a10bfd5", + "path": "pine_tree_01/source/textures/pine_tree_01_trunk_a_arm_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/pine_tree_01/pine_tree_01_twig_nor_gl_2k.jpg", + "size": 2924424, + "md5": "843dd3359c958c7a8bfd60f55fb4fb80", + "path": "pine_tree_01/source/textures/pine_tree_01_twig_nor_gl_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/pine_tree_01/pine_tree_01_twig_diff_2k.jpg", + "size": 1623201, + "md5": "45d577b1b780711976147d66234a1f4c", + "path": "pine_tree_01/source/textures/pine_tree_01_twig_diff_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/pine_tree_01/pine_tree_01_twig_arm_2k.jpg", + "size": 1524195, + "md5": "d9110a0de287f6abacb7abe4c50ae9b3", + "path": "pine_tree_01/source/textures/pine_tree_01_twig_arm_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/pine_tree_01/pine_tree_01_trunk_b_nor_gl_2k.jpg", + "size": 3269280, + "md5": "007dee669d056d81043ed0d171a731c2", + "path": "pine_tree_01/source/textures/pine_tree_01_trunk_b_nor_gl_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/pine_tree_01/pine_tree_01_trunk_b_diff_2k.jpg", + "size": 2548347, + "md5": "cba156ed09519ca12bea8c7ff90585ee", + "path": "pine_tree_01/source/textures/pine_tree_01_trunk_b_diff_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/pine_tree_01/pine_tree_01_trunk_b_arm_2k.jpg", + "size": 2718246, + "md5": "36a7440fbb145fc68e99c1b6a5f20047", + "path": "pine_tree_01/source/textures/pine_tree_01_trunk_b_arm_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/gltf/8k/pine_tree_01/pine_tree_01.bin", + "size": 948849556, + "md5": "3079e56cc48c57d8ba530d005e86bae6", + "path": "pine_tree_01/source/pine_tree_01.bin" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/gltf/2k/fir_tree_01/fir_tree_01_2k.gltf", + "size": 30279, + "md5": "937cc3176a17f40d47d655c54571dbd7", + "path": "fir_tree_01/source/model.gltf" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fir_tree_01/fir_tree_01_bark_nor_gl_2k.jpg", + "size": 3920997, + "md5": "5fa93c73d145cacdb258a29e3860a1ae", + "path": "fir_tree_01/source/textures/fir_tree_01_bark_nor_gl_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fir_tree_01/fir_tree_01_bark_diff_2k.jpg", + "size": 2839282, + "md5": "e9375177560503904ece137474cfde0b", + "path": "fir_tree_01/source/textures/fir_tree_01_bark_diff_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fir_tree_01/fir_tree_01_bark_arm_2k.jpg", + "size": 2783615, + "md5": "1ffec60c862633f2bb5c47f2102651cf", + "path": "fir_tree_01/source/textures/fir_tree_01_bark_arm_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fir_tree_01/fir_tree_01_trunk_a_nor_gl_2k.jpg", + "size": 3642074, + "md5": "97ce92fcba67201ace1c4ed3d6e154f6", + "path": "fir_tree_01/source/textures/fir_tree_01_trunk_a_nor_gl_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fir_tree_01/fir_tree_01_trunk_a_diff_2k.jpg", + "size": 3028704, + "md5": "44fd71883c0e29551243c8937c47b65c", + "path": "fir_tree_01/source/textures/fir_tree_01_trunk_a_diff_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fir_tree_01/fir_tree_01_trunk_a_arm_2k.jpg", + "size": 2974686, + "md5": "7f6bfdfd4987848b09b119cc1294e177", + "path": "fir_tree_01/source/textures/fir_tree_01_trunk_a_arm_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fir_tree_01/fir_tree_01_twig_nor_gl_2k.jpg", + "size": 1611490, + "md5": "2072f6b164184f0ca9b41f8623bfec06", + "path": "fir_tree_01/source/textures/fir_tree_01_twig_nor_gl_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fir_tree_01/fir_tree_01_twig_diff_2k.jpg", + "size": 1141996, + "md5": "6d023f52dab632b031a2ab8e5a27541f", + "path": "fir_tree_01/source/textures/fir_tree_01_twig_diff_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fir_tree_01/fir_tree_01_twig_arm_2k.jpg", + "size": 1271524, + "md5": "2fcac76cebb4f5fdc7fba2f31e02a490", + "path": "fir_tree_01/source/textures/fir_tree_01_twig_arm_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fir_tree_01/fir_tree_01_trunk_b_nor_gl_2k.jpg", + "size": 3269280, + "md5": "007dee669d056d81043ed0d171a731c2", + "path": "fir_tree_01/source/textures/fir_tree_01_trunk_b_nor_gl_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fir_tree_01/fir_tree_01_trunk_b_diff_2k.jpg", + "size": 2548347, + "md5": "cba156ed09519ca12bea8c7ff90585ee", + "path": "fir_tree_01/source/textures/fir_tree_01_trunk_b_diff_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fir_tree_01/fir_tree_01_trunk_b_arm_2k.jpg", + "size": 2718246, + "md5": "3d0e1b965f2ac48b894d259e4ec5f10c", + "path": "fir_tree_01/source/textures/fir_tree_01_trunk_b_arm_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/gltf/8k/fir_tree_01/fir_tree_01.bin", + "size": 478462204, + "md5": "26d2bf6d4d2e32d3524e8fa3760f643f", + "path": "fir_tree_01/source/fir_tree_01.bin" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/gltf/2k/tree_small_02/tree_small_02_2k.gltf", + "size": 9075, + "md5": "7b77f2ca02edf5d7964ca3514e66c6b7", + "path": "tree_small_02/source/model.gltf" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/tree_small_02/tree_small_02_branch_nor_gl_2k.jpg", + "size": 1781975, + "md5": "12286d5b329b4fb4203fc155a01b0a04", + "path": "tree_small_02/source/textures/tree_small_02_branch_nor_gl_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/tree_small_02/tree_small_02_branch_diff_2k.jpg", + "size": 1425692, + "md5": "5ba327fc166360fcbb44f2eb04fbd8d8", + "path": "tree_small_02/source/textures/tree_small_02_branch_diff_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/tree_small_02/tree_small_02_branch_arm_2k.jpg", + "size": 1147503, + "md5": "d6a0921ec6dbeaaf22a4b91d68fe16d1", + "path": "tree_small_02/source/textures/tree_small_02_branch_arm_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/tree_small_02/tree_small_02_leaves_nor_gl_2k.jpg", + "size": 2395771, + "md5": "ab509e55b5d763f68195e0eb257bb5e3", + "path": "tree_small_02/source/textures/tree_small_02_leaves_nor_gl_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/tree_small_02/tree_small_02_leaves_diff_2k.jpg", + "size": 961870, + "md5": "021ddc0cf1d94b99716a106cede576c7", + "path": "tree_small_02/source/textures/tree_small_02_leaves_diff_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/tree_small_02/tree_small_02_leaves_arm_2k.jpg", + "size": 2191542, + "md5": "4b88d67947fe214f2afc65beb83bee02", + "path": "tree_small_02/source/textures/tree_small_02_leaves_arm_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/tree_small_02/tree_small_02_nor_gl_2k.jpg", + "size": 4555311, + "md5": "064c3e1d08f010c732e7af6395a66769", + "path": "tree_small_02/source/textures/tree_small_02_nor_gl_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/tree_small_02/tree_small_02_diff_2k.jpg", + "size": 3170612, + "md5": "ff4d82640b7e0daaec0b71bfc1cef76c", + "path": "tree_small_02/source/textures/tree_small_02_diff_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/tree_small_02/tree_small_02_arm_2k.jpg", + "size": 2586815, + "md5": "038ad0c1968be6802b20d084cdf30603", + "path": "tree_small_02/source/textures/tree_small_02_arm_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/gltf/8k/tree_small_02/tree_small_02.bin", + "size": 95102324, + "md5": "7a7019a87e6fe0d7475c0fa2e08d8962", + "path": "tree_small_02/source/tree_small_02.bin" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/gltf/2k/fir_sapling_medium/fir_sapling_medium_2k.gltf", + "size": 17059, + "md5": "ab770a7f4437bb1585c856e9467345cf", + "path": "fir_sapling_medium/source/model.gltf" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fir_sapling_medium/fir_sapling_medium_branches_nor_gl_2k.jpg", + "size": 3920997, + "md5": "5fa93c73d145cacdb258a29e3860a1ae", + "path": "fir_sapling_medium/source/textures/fir_sapling_medium_branches_nor_gl_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fir_sapling_medium/fir_sapling_medium_branches_diff_2k.jpg", + "size": 2839282, + "md5": "e9375177560503904ece137474cfde0b", + "path": "fir_sapling_medium/source/textures/fir_sapling_medium_branches_diff_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fir_sapling_medium/fir_sapling_medium_branches_arm_2k.jpg", + "size": 2783615, + "md5": "1ffec60c862633f2bb5c47f2102651cf", + "path": "fir_sapling_medium/source/textures/fir_sapling_medium_branches_arm_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fir_sapling_medium/fir_sapling_medium_twigs_nor_gl_2k.jpg", + "size": 1611490, + "md5": "2072f6b164184f0ca9b41f8623bfec06", + "path": "fir_sapling_medium/source/textures/fir_sapling_medium_twigs_nor_gl_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fir_sapling_medium/fir_sapling_medium_twigs_diff_2k.jpg", + "size": 908300, + "md5": "f5e2dd9085334845f662fc11c60eb713", + "path": "fir_sapling_medium/source/textures/fir_sapling_medium_twigs_diff_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fir_sapling_medium/fir_sapling_medium_twigs_arm_2k.jpg", + "size": 1271524, + "md5": "2fcac76cebb4f5fdc7fba2f31e02a490", + "path": "fir_sapling_medium/source/textures/fir_sapling_medium_twigs_arm_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/gltf/8k/fir_sapling_medium/fir_sapling_medium.bin", + "size": 74159400, + "md5": "d3976657a03ce2e6eccfa56b378bbcd0", + "path": "fir_sapling_medium/source/fir_sapling_medium.bin" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/gltf/2k/shrub_01/shrub_01_2k.gltf", + "size": 2895, + "md5": "b4dba9051ffc1eae61780af528973499", + "path": "shrub_01/source/model.gltf" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/shrub_01/shrub_01_nor_gl_2k.jpg", + "size": 2628947, + "md5": "ba76e790f9346ad2366585b57095c214", + "path": "shrub_01/source/textures/shrub_01_nor_gl_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/shrub_01/shrub_01_arm_2k.jpg", + "size": 1232008, + "md5": "94b1eac99e9f949487d49243d19f70e4", + "path": "shrub_01/source/textures/shrub_01_arm_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/gltf/4k/shrub_01/shrub_01.bin", + "size": 5239664, + "md5": "baf77d162f397f43fa4737b545d73e1a", + "path": "shrub_01/source/shrub_01.bin" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/shrub_01/shrub_01_diff_2k.jpg", + "size": 1651989, + "md5": "cd89f941ba9e5c60cbeee50e0b269e2c", + "path": "shrub_01/source/textures/shrub_01_diff_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/gltf/2k/shrub_04/shrub_04_2k.gltf", + "size": 2882, + "md5": "7a6f451aea5cc9bae36ce9b027dc25cf", + "path": "shrub_04/source/model.gltf" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/shrub_04/shrub_04_arm_2k.jpg", + "size": 1005689, + "md5": "f37749dbbfe57024799df7b96f896643", + "path": "shrub_04/source/textures/shrub_04_arm_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/gltf/4k/shrub_04/shrub_04.bin", + "size": 749084, + "md5": "987a672e598f9dc04b6d23cb8f36d697", + "path": "shrub_04/source/shrub_04.bin" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/shrub_04/shrub_04_nor_gl_2k.jpg", + "size": 1428577, + "md5": "ca4ecc50015a85e38675a4967e843485", + "path": "shrub_04/source/textures/shrub_04_nor_gl_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/shrub_04/shrub_04_diff_2k.jpg", + "size": 1202317, + "md5": "76f10cb443bc5964ec9484cfea474196", + "path": "shrub_04/source/textures/shrub_04_diff_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/gltf/2k/fern_02/fern_02_2k.gltf", + "size": 6899, + "md5": "904c73900125e017f5e76ac1cdd906fe", + "path": "fern_02/source/model.gltf" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fern_02/fern_02_diff_2k.jpg", + "size": 993703, + "md5": "ba8bbb9dc9ff55bdc081565cd926d776", + "path": "fern_02/source/textures/fern_02_diff_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fern_02/fern_02_arm_2k.jpg", + "size": 850856, + "md5": "7f7b46e813a3cb4811405191de0ecdce", + "path": "fern_02/source/textures/fern_02_arm_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/gltf/8k/fern_02/fern_02.bin", + "size": 176176, + "md5": "c061e9255f7d36ae9ce0e92ef3e100b2", + "path": "fern_02/source/fern_02.bin" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/fern_02/fern_02_nor_gl_2k.jpg", + "size": 1365615, + "md5": "00936dff4661dc9fe1f2fbe6c000615a", + "path": "fern_02/source/textures/fern_02_nor_gl_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/gltf/2k/grass_medium_01/grass_medium_01_2k.gltf", + "size": 27110, + "md5": "784c287f7828f8e30c4d28ebcbd90f6e", + "path": "grass_medium_01/source/model.gltf" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/grass_medium_01/grass_medium_01_nor_gl_2k.jpg", + "size": 3530394, + "md5": "7c75ce52a121de1fc00422672584f757", + "path": "grass_medium_01/source/textures/grass_medium_01_nor_gl_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/grass_medium_01/grass_medium_01_diff_2k.jpg", + "size": 1166527, + "md5": "000814eb2149add70695cf71bfb52c4b", + "path": "grass_medium_01/source/textures/grass_medium_01_diff_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/grass_medium_01/grass_medium_01_arm_2k.jpg", + "size": 2256355, + "md5": "a530c693ba3d08be15fcbce33dc5dabe", + "path": "grass_medium_01/source/textures/grass_medium_01_arm_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/gltf/8k/grass_medium_01/grass_medium_01.bin", + "size": 1006632, + "md5": "e0527a0561c2dae5b7b5c1b478d8c6bf", + "path": "grass_medium_01/source/grass_medium_01.bin" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/gltf/2k/grass_medium_02/grass_medium_02_2k.gltf", + "size": 8864, + "md5": "fc24d97b602c74c1a9357a7570ae2b87", + "path": "grass_medium_02/source/model.gltf" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/grass_medium_02/grass_medium_02_nor_gl_2k.jpg", + "size": 353362, + "md5": "9ca6e2499ce71446f0cd8a3fc8c54a1b", + "path": "grass_medium_02/source/textures/grass_medium_02_nor_gl_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/grass_medium_02/grass_medium_02_diff_2k.jpg", + "size": 456989, + "md5": "3431e6de76f40f25bef48755f4c6a76a", + "path": "grass_medium_02/source/textures/grass_medium_02_diff_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/grass_medium_02/grass_medium_02_arm_2k.jpg", + "size": 698115, + "md5": "66eb24d4e6dbda01faad666a3517dcd1", + "path": "grass_medium_02/source/textures/grass_medium_02_arm_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/gltf/4k/grass_medium_02/grass_medium_02.bin", + "size": 272048, + "md5": "145041ae03fe02783a0adbd1cdcb4988", + "path": "grass_medium_02/source/grass_medium_02.bin" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/gltf/2k/weed_plant_02/weed_plant_02_2k.gltf", + "size": 9128, + "md5": "d8a430593da2222596743f19868a384f", + "path": "weed_plant_02/source/model.gltf" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/weed_plant_02/weed_plant_02_nor_gl_2k.jpg", + "size": 2569183, + "md5": "c9141036671d1b13841a17df18945359", + "path": "weed_plant_02/source/textures/weed_plant_02_nor_gl_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/weed_plant_02/weed_plant_02_arm_2k.jpg", + "size": 2042103, + "md5": "0849fef5384b7b0d6d1aaadec40eb459", + "path": "weed_plant_02/source/textures/weed_plant_02_arm_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/jpg/2k/weed_plant_02/weed_plant_02_diff_2k.jpg", + "size": 1566453, + "md5": "97165d40d029a652ada297f06f382c4f", + "path": "weed_plant_02/source/textures/weed_plant_02_diff_2k.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/gltf/8k/weed_plant_02/weed_plant_02.bin", + "size": 330884, + "md5": "31440e5d2ad17eff90fcf83f3dbe989e", + "path": "weed_plant_02/source/weed_plant_02.bin" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/2k/forest_ground_04/forest_ground_04_diff_2k.jpg", + "size": 4529321, + "md5": "f6ce9c8af4316c2627a4b3fbab8ad944", + "path": "ground/forest_ground_04/diff.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/2k/forest_ground_04/forest_ground_04_nor_gl_2k.jpg", + "size": 5288849, + "md5": "5fa9256955e7bfa40a0269b4965ce646", + "path": "ground/forest_ground_04/nor_gl.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/2k/forest_ground_04/forest_ground_04_rough_2k.jpg", + "size": 2509805, + "md5": "81daa9a1d8b35dd9ffdff3aa7a74fbd9", + "path": "ground/forest_ground_04/rough.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/2k/withered_grass/withered_grass_diff_2k.jpg", + "size": 4632786, + "md5": "503d5c7262c46351e5b0acdcc90dd30b", + "path": "ground/withered_grass/diff.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/2k/withered_grass/withered_grass_nor_gl_2k.jpg", + "size": 6194953, + "md5": "dd239e55b44c9b9bb0e9f1e145bf3a22", + "path": "ground/withered_grass/nor_gl.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/2k/withered_grass/withered_grass_rough_2k.jpg", + "size": 3666367, + "md5": "5c065d1438f6d74ec9c67bf8051430fd", + "path": "ground/withered_grass/rough.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/2k/snow_02/snow_02_diff_2k.jpg", + "size": 1314409, + "md5": "5541e5601951b071691238ddc70632c0", + "path": "ground/snow_02/diff.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/2k/snow_02/snow_02_nor_gl_2k.jpg", + "size": 4761595, + "md5": "ef128fbdf1b31932c792eb6472c84574", + "path": "ground/snow_02/nor_gl.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/2k/snow_02/snow_02_rough_2k.jpg", + "size": 628467, + "md5": "8b9787b4c7b1d7390a4b4a1ae67728b1", + "path": "ground/snow_02/rough.jpg" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/png/2k/pine_tree_01/pine_tree_01_twig_alpha_2k.png", + "size": 814656, + "md5": "85f9f62d98dc35cce642895d75f79fa6", + "path": "pine_tree_01/source/masks/twig_alpha.png" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/png/2k/fir_tree_01/fir_tree_01_twig_alpha_2k.png", + "size": 996187, + "md5": "1d93929d869e5539ec245ae3eddb7243", + "path": "fir_tree_01/source/masks/twig_alpha.png" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/png/2k/tree_small_02/tree_small_02_leaves_alpha_2k.png", + "size": 636661, + "md5": "4d94de8e4677e92ce6d812871f9cf541", + "path": "tree_small_02/source/masks/leaves_alpha.png" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/png/2k/fir_sapling_medium/fir_sapling_medium_twigs_alpha_2k.png", + "size": 996187, + "md5": "1d93929d869e5539ec245ae3eddb7243", + "path": "fir_sapling_medium/source/masks/twigs_alpha.png" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/png/2k/shrub_01/shrub_01_alpha_2k.png", + "size": 297762, + "md5": "dabea4c2ea5710c231d58cf94b85e6b3", + "path": "shrub_01/source/masks/Alpha.png" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/png/2k/shrub_04/shrub_04_alpha_2k.png", + "size": 91048, + "md5": "c580b41967ed8e43358c2b8621fd07b1", + "path": "shrub_04/source/masks/Alpha.png" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/png/2k/fern_02/fern_02_alpha_2k.png", + "size": 230089, + "md5": "ff18f7e8e869c58eac8f9c7d7c519f4a", + "path": "fern_02/source/masks/Alpha.png" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/png/2k/grass_medium_01/grass_medium_01_alpha_2k.png", + "size": 1580653, + "md5": "0810166131307c1094d37e73a8490542", + "path": "grass_medium_01/source/masks/Alpha.png" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/png/2k/grass_medium_01/grass_medium_01_dry_diff_2k.png", + "size": 6652964, + "md5": "8ea1c1853688113b9e1e0b17c4efda5a", + "path": "grass_medium_01/source/masks/dry_diff.png" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/png/2k/grass_medium_02/grass_medium_02_alpha_2k.png", + "size": 303260, + "md5": "3b2ba296de3cffad18e8119e6ca693e5", + "path": "grass_medium_02/source/masks/Alpha.png" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/png/2k/grass_medium_02/grass_medium_02_dry_diff_2k.png", + "size": 3011488, + "md5": "04f9adb6ab1ae0c8bc207be8da6c5dd5", + "path": "grass_medium_02/source/masks/dry_diff.png" + }, + { + "url": "https://dl.polyhaven.org/file/ph-assets/Models/png/2k/weed_plant_02/weed_plant_02_alpha_2k.png", + "size": 420666, + "md5": "47efb600ae3dc785c2978bf65f84c15b", + "path": "weed_plant_02/source/masks/Alpha.png" + } + ], + "foliageMasksIncluded": true +} diff --git a/Content/Vegitations/tree_small_02/prepared/cards-0-color.png b/Content/Vegitations/tree_small_02/prepared/cards-0-color.png new file mode 100644 index 0000000..63533a8 Binary files /dev/null and b/Content/Vegitations/tree_small_02/prepared/cards-0-color.png differ diff --git a/Content/Vegitations/tree_small_02/prepared/cards-0-normal.png b/Content/Vegitations/tree_small_02/prepared/cards-0-normal.png new file mode 100644 index 0000000..6607f8b Binary files /dev/null and b/Content/Vegitations/tree_small_02/prepared/cards-0-normal.png differ diff --git a/Content/Vegitations/tree_small_02/prepared/cards.json b/Content/Vegitations/tree_small_02/prepared/cards.json new file mode 100644 index 0000000..2b483b2 --- /dev/null +++ b/Content/Vegitations/tree_small_02/prepared/cards.json @@ -0,0 +1 @@ +{"Variants":[[{"Center":[-0.729175,0.3797284,-1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0,0.8888889,0.125,0.11111111]},{"Center":[-0.729175,0.3797284,-1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.125,0.8888889,0.125,0.11111111]},{"Center":[-0.729175,0.3797284,-1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.25,0.8888889,0.125,0.11111111]},{"Center":[0.729175,0.3797284,-1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.375,0.8888889,0.125,0.11111111]},{"Center":[0.729175,0.3797284,-1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.5,0.8888889,0.125,0.11111111]},{"Center":[0.729175,0.3797284,-1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.625,0.8888889,0.125,0.11111111]},{"Center":[-0.729175,0.3797284,1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.75,0.8888889,0.125,0.11111111]},{"Center":[-0.729175,0.3797284,1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.875,0.8888889,0.125,0.11111111]},{"Center":[-0.729175,0.3797284,1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0,0.7777778,0.125,0.11111111]},{"Center":[0.729175,0.3797284,1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.125,0.7777778,0.125,0.11111111]},{"Center":[0.729175,0.3797284,1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.25,0.7777778,0.125,0.11111111]},{"Center":[0.729175,0.3797284,1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.375,0.7777778,0.125,0.11111111]},{"Center":[-0.729175,1.1391852,-1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.5,0.7777778,0.125,0.11111111]},{"Center":[-0.729175,1.1391852,-1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.625,0.7777778,0.125,0.11111111]},{"Center":[-0.729175,1.1391852,-1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.75,0.7777778,0.125,0.11111111]},{"Center":[0.729175,1.1391852,-1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.875,0.7777778,0.125,0.11111111]},{"Center":[0.729175,1.1391852,-1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0,0.6666666,0.125,0.11111111]},{"Center":[0.729175,1.1391852,-1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.125,0.6666666,0.125,0.11111111]},{"Center":[-0.729175,1.1391852,1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.25,0.6666666,0.125,0.11111111]},{"Center":[-0.729175,1.1391852,1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.375,0.6666666,0.125,0.11111111]},{"Center":[-0.729175,1.1391852,1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.5,0.6666666,0.125,0.11111111]},{"Center":[0.729175,1.1391852,1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.625,0.6666666,0.125,0.11111111]},{"Center":[0.729175,1.1391852,1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.75,0.6666666,0.125,0.11111111]},{"Center":[0.729175,1.1391852,1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.875,0.6666666,0.125,0.11111111]},{"Center":[-0.729175,1.8986421,-1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0,0.5555556,0.125,0.11111111]},{"Center":[-0.729175,1.8986421,-1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.125,0.5555556,0.125,0.11111111]},{"Center":[-0.729175,1.8986421,-1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.25,0.5555556,0.125,0.11111111]},{"Center":[0.729175,1.8986421,-1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.375,0.5555556,0.125,0.11111111]},{"Center":[0.729175,1.8986421,-1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.5,0.5555556,0.125,0.11111111]},{"Center":[0.729175,1.8986421,-1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.625,0.5555556,0.125,0.11111111]},{"Center":[-0.729175,1.8986421,1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.75,0.5555556,0.125,0.11111111]},{"Center":[-0.729175,1.8986421,1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.875,0.5555556,0.125,0.11111111]},{"Center":[-0.729175,1.8986421,1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0,0.44444442,0.125,0.11111111]},{"Center":[0.729175,1.8986421,1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.125,0.44444442,0.125,0.11111111]},{"Center":[0.729175,1.8986421,1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.25,0.44444442,0.125,0.11111111]},{"Center":[0.729175,1.8986421,1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.375,0.44444442,0.125,0.11111111]},{"Center":[-0.729175,2.6580987,-1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.5,0.44444442,0.125,0.11111111]},{"Center":[-0.729175,2.6580987,-1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.625,0.44444442,0.125,0.11111111]},{"Center":[-0.729175,2.6580987,-1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.75,0.44444442,0.125,0.11111111]},{"Center":[0.729175,2.6580987,-1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.875,0.44444442,0.125,0.11111111]},{"Center":[0.729175,2.6580987,-1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0,0.3333333,0.125,0.11111111]},{"Center":[0.729175,2.6580987,-1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.125,0.3333333,0.125,0.11111111]},{"Center":[-0.729175,2.6580987,1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.25,0.3333333,0.125,0.11111111]},{"Center":[-0.729175,2.6580987,1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.375,0.3333333,0.125,0.11111111]},{"Center":[-0.729175,2.6580987,1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.5,0.3333333,0.125,0.11111111]},{"Center":[0.729175,2.6580987,1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.625,0.3333333,0.125,0.11111111]},{"Center":[0.729175,2.6580987,1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.75,0.3333333,0.125,0.11111111]},{"Center":[0.729175,2.6580987,1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.875,0.3333333,0.125,0.11111111]},{"Center":[-0.729175,3.4175558,-1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0,0.22222221,0.125,0.11111111]},{"Center":[-0.729175,3.4175558,-1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.125,0.22222221,0.125,0.11111111]},{"Center":[-0.729175,3.4175558,-1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.25,0.22222221,0.125,0.11111111]},{"Center":[0.729175,3.4175558,-1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.375,0.22222221,0.125,0.11111111]},{"Center":[0.729175,3.4175558,-1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.5,0.22222221,0.125,0.11111111]},{"Center":[0.729175,3.4175558,-1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.625,0.22222221,0.125,0.11111111]},{"Center":[-0.729175,3.4175558,1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.75,0.22222221,0.125,0.11111111]},{"Center":[-0.729175,3.4175558,1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.875,0.22222221,0.125,0.11111111]},{"Center":[-0.729175,3.4175558,1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0,0.111111104,0.125,0.11111111]},{"Center":[0.729175,3.4175558,1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.125,0.111111104,0.125,0.11111111]},{"Center":[0.729175,3.4175558,1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.25,0.111111104,0.125,0.11111111]},{"Center":[0.729175,3.4175558,1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.375,0.111111104,0.125,0.11111111]},{"Center":[-0.729175,4.1770124,-1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.5,0.111111104,0.125,0.11111111]},{"Center":[-0.729175,4.1770124,-1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.625,0.111111104,0.125,0.11111111]},{"Center":[-0.729175,4.1770124,-1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.75,0.111111104,0.125,0.11111111]},{"Center":[0.729175,4.1770124,-1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.875,0.111111104,0.125,0.11111111]},{"Center":[0.729175,4.1770124,-1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0,0,0.125,0.11111111]},{"Center":[0.729175,4.1770124,-1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.125,0,0.125,0.11111111]},{"Center":[-0.729175,4.1770124,1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.25,0,0.125,0.11111111]},{"Center":[-0.729175,4.1770124,1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.375,0,0.125,0.11111111]},{"Center":[-0.729175,4.1770124,1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.5,0,0.125,0.11111111]},{"Center":[0.729175,4.1770124,1.0731193],"Right":[1,0,0],"Up":[0,1,0],"Normal":[0,0,1],"Size":2.7847931,"Uv":[0.625,0,0.125,0.11111111]},{"Center":[0.729175,4.1770124,1.0731193],"Right":[0,0,-1],"Up":[-0,1,0],"Normal":[1,0,0],"Size":2.7847931,"Uv":[0.75,0,0.125,0.11111111]},{"Center":[0.729175,4.1770124,1.0731193],"Right":[-1,0,0],"Up":[0,-0,1],"Normal":[0,1,0],"Size":2.7847931,"Uv":[0.875,0,0.125,0.11111111]}]]} \ No newline at end of file diff --git a/Content/Vegitations/tree_small_02/prepared/impostor-0-color.png b/Content/Vegitations/tree_small_02/prepared/impostor-0-color.png new file mode 100644 index 0000000..dd6a776 Binary files /dev/null and b/Content/Vegitations/tree_small_02/prepared/impostor-0-color.png differ diff --git a/Content/Vegitations/tree_small_02/prepared/impostor-0-normal.png b/Content/Vegitations/tree_small_02/prepared/impostor-0-normal.png new file mode 100644 index 0000000..54b7b59 Binary files /dev/null and b/Content/Vegitations/tree_small_02/prepared/impostor-0-normal.png differ diff --git a/Content/Vegitations/tree_small_02/prepared/impostors.json b/Content/Vegitations/tree_small_02/prepared/impostors.json new file mode 100644 index 0000000..0869ff5 --- /dev/null +++ b/Content/Vegitations/tree_small_02/prepared/impostors.json @@ -0,0 +1 @@ +{"Views":16,"Elevations":3,"Variants":[{"Center":[0,2.2783704,0],"Size":7.3719263}]} \ No newline at end of file diff --git a/Content/Vegitations/tree_small_02/prepared/model.glb b/Content/Vegitations/tree_small_02/prepared/model.glb new file mode 100644 index 0000000..8cfc9d7 Binary files /dev/null and b/Content/Vegitations/tree_small_02/prepared/model.glb differ diff --git a/Content/Vegitations/tree_small_02/prepared/preparation.json b/Content/Vegitations/tree_small_02/prepared/preparation.json new file mode 100644 index 0000000..adc2864 --- /dev/null +++ b/Content/Vegitations/tree_small_02/prepared/preparation.json @@ -0,0 +1,28 @@ +{ + "source": "https://polyhaven.com/a/tree_small_02", + "license": "CC0", + "budgets": [ + 12000, + 2000, + 350 + ], + "triangles": [ + 11065, + 2016, + 339 + ], + "variants": [ + { + "min": [ + -2.1215715408325195, + -1.0126681327819824, + -2.4655158519744873 + ], + "max": [ + 2.1215715408325195, + 5.569408893585205, + 2.4655158519744873 + ] + } + ] +} \ No newline at end of file diff --git a/Content/Vegitations/tree_small_02/prepared/stamp.json b/Content/Vegitations/tree_small_02/prepared/stamp.json new file mode 100644 index 0000000..fb748e0 --- /dev/null +++ b/Content/Vegitations/tree_small_02/prepared/stamp.json @@ -0,0 +1 @@ +{"version":5,"cards":true,"files":["7b77f2ca02edf5d7964ca3514e66c6b7","12286d5b329b4fb4203fc155a01b0a04","5ba327fc166360fcbb44f2eb04fbd8d8","d6a0921ec6dbeaaf22a4b91d68fe16d1","ab509e55b5d763f68195e0eb257bb5e3","021ddc0cf1d94b99716a106cede576c7","4b88d67947fe214f2afc65beb83bee02","064c3e1d08f010c732e7af6395a66769","ff4d82640b7e0daaec0b71bfc1cef76c","038ad0c1968be6802b20d084cdf30603","7a7019a87e6fe0d7475c0fa2e08d8962","4d94de8e4677e92ce6d812871f9cf541"]} \ No newline at end of file diff --git a/Content/Vegitations/vegetation.json b/Content/Vegitations/vegetation.json new file mode 100644 index 0000000..a9f671a --- /dev/null +++ b/Content/Vegitations/vegetation.json @@ -0,0 +1,360 @@ +{ + "schemaVersion": 1, + "biomes": [ + { + "id": "forest", + "colorRgb": [ + 45, + 80, + 35 + ], + "tolerancePercent": 24, + "material": "forest-ground" + }, + { + "id": "dry", + "colorRgb": [ + 145, + 150, + 80 + ], + "tolerancePercent": 18, + "material": "dry-grass" + }, + { + "id": "sand", + "colorRgb": [ + 205, + 181, + 120 + ], + "tolerancePercent": 15, + "material": "sand" + }, + { + "id": "snow", + "colorRgb": [ + 240, + 242, + 245 + ], + "tolerancePercent": 12, + "material": "snow" + } + ], + "plants": [ + { + "id": "pine_tree_01", + "type": "tree", + "model": "pine_tree_01/prepared/model.glb", + "matchColorRgb": [ + 45, + 80, + 35 + ], + "colorTolerancePercent": 25, + "spawnWeight": 1, + "scaleRange": [ + 0.8, + 1.25 + ], + "altitudeMeters": [ + -100, + 6000 + ], + "absoluteLatitudeDegrees": [ + 0, + 80 + ], + "maxSlopeDegrees": 32, + "minimumSpacingMeters": 2.4, + "windStrength": 0.15, + "trunkCollision": { + "radiusMeters": 0.36, + "heightMeters": 10 + } + }, + { + "id": "fir_tree_01", + "type": "tree", + "model": "fir_tree_01/prepared/model.glb", + "matchColorRgb": [ + 45, + 80, + 35 + ], + "colorTolerancePercent": 25, + "spawnWeight": 1, + "scaleRange": [ + 0.8, + 1.25 + ], + "altitudeMeters": [ + -100, + 6000 + ], + "absoluteLatitudeDegrees": [ + 0, + 80 + ], + "maxSlopeDegrees": 32, + "minimumSpacingMeters": 2.4, + "windStrength": 0.15, + "trunkCollision": { + "radiusMeters": 0.36, + "heightMeters": 10 + } + }, + { + "id": "tree_small_02", + "type": "tree", + "model": "tree_small_02/prepared/model.glb", + "matchColorRgb": [ + 45, + 80, + 35 + ], + "colorTolerancePercent": 25, + "spawnWeight": 1, + "scaleRange": [ + 0.8, + 1.25 + ], + "altitudeMeters": [ + -100, + 6000 + ], + "absoluteLatitudeDegrees": [ + 0, + 80 + ], + "maxSlopeDegrees": 32, + "minimumSpacingMeters": 2.4, + "windStrength": 0.15, + "trunkCollision": { + "radiusMeters": 0.36, + "heightMeters": 10 + } + }, + { + "id": "fir_sapling_medium", + "type": "tree", + "model": "fir_sapling_medium/prepared/model.glb", + "matchColorRgb": [ + 45, + 80, + 35 + ], + "colorTolerancePercent": 25, + "spawnWeight": 1, + "scaleRange": [ + 0.8, + 1.25 + ], + "altitudeMeters": [ + -100, + 6000 + ], + "absoluteLatitudeDegrees": [ + 0, + 80 + ], + "maxSlopeDegrees": 32, + "minimumSpacingMeters": 2.4, + "windStrength": 0.15, + "trunkCollision": { + "radiusMeters": 0.22, + "heightMeters": 5 + } + }, + { + "id": "shrub_01", + "type": "shrub", + "model": "shrub_01/prepared/model.glb", + "matchColorRgb": [ + 100, + 125, + 60 + ], + "colorTolerancePercent": 25, + "spawnWeight": 1, + "scaleRange": [ + 0.8, + 1.25 + ], + "altitudeMeters": [ + -100, + 6500 + ], + "absoluteLatitudeDegrees": [ + 0, + 80 + ], + "maxSlopeDegrees": 40, + "minimumSpacingMeters": 0.6, + "windStrength": 0.3 + }, + { + "id": "shrub_04", + "type": "shrub", + "model": "shrub_04/prepared/model.glb", + "matchColorRgb": [ + 100, + 125, + 60 + ], + "colorTolerancePercent": 25, + "spawnWeight": 1, + "scaleRange": [ + 0.8, + 1.25 + ], + "altitudeMeters": [ + -100, + 6500 + ], + "absoluteLatitudeDegrees": [ + 0, + 80 + ], + "maxSlopeDegrees": 40, + "minimumSpacingMeters": 0.6, + "windStrength": 0.3 + }, + { + "id": "fern_02", + "type": "fern", + "model": "fern_02/prepared/model.glb", + "matchColorRgb": [ + 45, + 80, + 35 + ], + "colorTolerancePercent": 25, + "spawnWeight": 1, + "scaleRange": [ + 0.8, + 1.25 + ], + "altitudeMeters": [ + -100, + 6500 + ], + "absoluteLatitudeDegrees": [ + 0, + 80 + ], + "maxSlopeDegrees": 40, + "minimumSpacingMeters": 0.2, + "windStrength": 0.3 + }, + { + "id": "grass_medium_01", + "type": "grass", + "model": "grass_medium_01/prepared/model.glb", + "matchColorRgb": [ + 100, + 125, + 60 + ], + "colorTolerancePercent": 27, + "spawnWeight": 1, + "scaleRange": [ + 0.8, + 1.25 + ], + "altitudeMeters": [ + -100, + 6500 + ], + "absoluteLatitudeDegrees": [ + 0, + 80 + ], + "maxSlopeDegrees": 40, + "minimumSpacingMeters": 0.2, + "windStrength": 0.3 + }, + { + "id": "grass_medium_02", + "type": "grass", + "model": "grass_medium_02/prepared/model.glb", + "matchColorRgb": [ + 100, + 125, + 60 + ], + "colorTolerancePercent": 27, + "spawnWeight": 1, + "scaleRange": [ + 0.8, + 1.25 + ], + "altitudeMeters": [ + -100, + 6500 + ], + "absoluteLatitudeDegrees": [ + 0, + 80 + ], + "maxSlopeDegrees": 40, + "minimumSpacingMeters": 0.2, + "windStrength": 0.3 + }, + { + "id": "weed_plant_02", + "type": "grass", + "model": "weed_plant_02/prepared/model.glb", + "matchColorRgb": [ + 100, + 125, + 60 + ], + "colorTolerancePercent": 27, + "spawnWeight": 1, + "scaleRange": [ + 0.8, + 1.25 + ], + "altitudeMeters": [ + -100, + 6500 + ], + "absoluteLatitudeDegrees": [ + 0, + 80 + ], + "maxSlopeDegrees": 40, + "minimumSpacingMeters": 0.2, + "windStrength": 0.3 + }, + { + "id": "dry_grass", + "type": "grass", + "model": "grass_medium_01/prepared/dry.glb", + "matchColorRgb": [ + 155, + 150, + 85 + ], + "colorTolerancePercent": 21, + "spawnWeight": 1, + "scaleRange": [ + 0.8, + 1.25 + ], + "altitudeMeters": [ + -100, + 6500 + ], + "absoluteLatitudeDegrees": [ + 0, + 80 + ], + "maxSlopeDegrees": 40, + "minimumSpacingMeters": 0.2, + "windStrength": 0.3 + } + ] +} diff --git a/Content/Vegitations/weed_plant_02/prepared/impostor-0-color.png b/Content/Vegitations/weed_plant_02/prepared/impostor-0-color.png new file mode 100644 index 0000000..d63eacf Binary files /dev/null and b/Content/Vegitations/weed_plant_02/prepared/impostor-0-color.png differ diff --git a/Content/Vegitations/weed_plant_02/prepared/impostor-0-normal.png b/Content/Vegitations/weed_plant_02/prepared/impostor-0-normal.png new file mode 100644 index 0000000..d0f4140 Binary files /dev/null and b/Content/Vegitations/weed_plant_02/prepared/impostor-0-normal.png differ diff --git a/Content/Vegitations/weed_plant_02/prepared/impostor-1-color.png b/Content/Vegitations/weed_plant_02/prepared/impostor-1-color.png new file mode 100644 index 0000000..5f4316b Binary files /dev/null and b/Content/Vegitations/weed_plant_02/prepared/impostor-1-color.png differ diff --git a/Content/Vegitations/weed_plant_02/prepared/impostor-1-normal.png b/Content/Vegitations/weed_plant_02/prepared/impostor-1-normal.png new file mode 100644 index 0000000..1adc839 Binary files /dev/null and b/Content/Vegitations/weed_plant_02/prepared/impostor-1-normal.png differ diff --git a/Content/Vegitations/weed_plant_02/prepared/impostor-2-color.png b/Content/Vegitations/weed_plant_02/prepared/impostor-2-color.png new file mode 100644 index 0000000..5439dc9 Binary files /dev/null and b/Content/Vegitations/weed_plant_02/prepared/impostor-2-color.png differ diff --git a/Content/Vegitations/weed_plant_02/prepared/impostor-2-normal.png b/Content/Vegitations/weed_plant_02/prepared/impostor-2-normal.png new file mode 100644 index 0000000..616c1a2 Binary files /dev/null and b/Content/Vegitations/weed_plant_02/prepared/impostor-2-normal.png differ diff --git a/Content/Vegitations/weed_plant_02/prepared/impostors.json b/Content/Vegitations/weed_plant_02/prepared/impostors.json new file mode 100644 index 0000000..511c0ab --- /dev/null +++ b/Content/Vegitations/weed_plant_02/prepared/impostors.json @@ -0,0 +1 @@ +{"Views":16,"Elevations":3,"Variants":[{"Center":[0,0.03622882,0],"Size":0.40810227},{"Center":[0,0.033846423,0],"Size":0.3058868},{"Center":[0,0.019473067,0],"Size":0.210797}]} \ No newline at end of file diff --git a/Content/Vegitations/weed_plant_02/prepared/model.glb b/Content/Vegitations/weed_plant_02/prepared/model.glb new file mode 100644 index 0000000..cfa3144 Binary files /dev/null and b/Content/Vegitations/weed_plant_02/prepared/model.glb differ diff --git a/Content/Vegitations/weed_plant_02/prepared/preparation.json b/Content/Vegitations/weed_plant_02/prepared/preparation.json new file mode 100644 index 0000000..99b29bc --- /dev/null +++ b/Content/Vegitations/weed_plant_02/prepared/preparation.json @@ -0,0 +1,52 @@ +{ + "source": "https://polyhaven.com/a/weed_plant_02", + "license": "CC0", + "budgets": [ + 2500, + 800, + 200 + ], + "triangles": [ + 6700, + 2205, + 583 + ], + "variants": [ + { + "min": [ + -0.10970016568899155, + 0, + -0.14545971155166626 + ], + "max": [ + 0.10970016568899155, + 0.07245764136314392, + 0.14545971155166626 + ] + }, + { + "min": [ + -0.09377400577068329, + 0, + -0.09926804900169373 + ], + "max": [ + 0.09377400577068329, + 0.0676928460597992, + 0.09926804900169373 + ] + }, + { + "min": [ + -0.07703268527984619, + 0, + -0.05405430123209953 + ], + "max": [ + 0.07703268527984619, + 0.038946133106946945, + 0.05405430123209953 + ] + } + ] +} \ No newline at end of file diff --git a/Content/Vegitations/weed_plant_02/prepared/stamp.json b/Content/Vegitations/weed_plant_02/prepared/stamp.json new file mode 100644 index 0000000..4b93cfc --- /dev/null +++ b/Content/Vegitations/weed_plant_02/prepared/stamp.json @@ -0,0 +1 @@ +{"version":4,"cards":false,"files":["d8a430593da2222596743f19868a384f","c9141036671d1b13841a17df18945359","0849fef5384b7b0d6d1aaadec40eb459","97165d40d029a652ada297f06f382c4f","31440e5d2ad17eff90fcf83f3dbe989e","47efb600ae3dc785c2978bf65f84c15b"]} \ No newline at end of file diff --git a/Content/materials.json b/Content/materials.json index 8ff4429..36956ac 100644 --- a/Content/materials.json +++ b/Content/materials.json @@ -1,11 +1,58 @@ { "materials": [ - { "id": "leafy-grass", "color": "Vegetations/leafy_grass/Diffuse.jpg", "normal": "Vegetations/leafy_grass/nor_gl.jpg", "roughness": "Vegetations/leafy_grass/Rough.jpg" }, - { "id": "rock-ground", "color": "Rocks/rock_ground/Diffuse.jpg", "normal": "Rocks/rock_ground/nor_gl.jpg", "roughness": "Rocks/rock_ground/Rough.jpg" }, - { "id": "gravel", "color": "Gravel/rocks_ground_05/Diffuse.jpg", "normal": "Gravel/rocks_ground_05/nor_gl.jpg", "roughness": "Gravel/rocks_ground_05/Rough.jpg" }, - { "id": "sand", "color": "Sand/coast_sand_02/Diffuse.jpg", "normal": "Sand/coast_sand_02/nor_gl.jpg", "roughness": "Sand/coast_sand_02/Rough.jpg" } + { + "id": "leafy-grass", + "color": "Vegetations/leafy_grass/Diffuse.jpg", + "normal": "Vegetations/leafy_grass/nor_gl.jpg", + "roughness": "Vegetations/leafy_grass/Rough.jpg" + }, + { + "id": "rock-ground", + "color": "Rocks/rock_ground/Diffuse.jpg", + "normal": "Rocks/rock_ground/nor_gl.jpg", + "roughness": "Rocks/rock_ground/Rough.jpg" + }, + { + "id": "gravel", + "color": "Gravel/rocks_ground_05/Diffuse.jpg", + "normal": "Gravel/rocks_ground_05/nor_gl.jpg", + "roughness": "Gravel/rocks_ground_05/Rough.jpg" + }, + { + "id": "sand", + "color": "Sand/coast_sand_02/Diffuse.jpg", + "normal": "Sand/coast_sand_02/nor_gl.jpg", + "roughness": "Sand/coast_sand_02/Rough.jpg" + }, + { + "id": "forest-ground", + "color": "Vegitations/ground/forest_ground_04/diff.jpg", + "normal": "Vegitations/ground/forest_ground_04/nor_gl.jpg", + "roughness": "Vegitations/ground/forest_ground_04/rough.jpg" + }, + { + "id": "dry-grass", + "color": "Vegitations/ground/withered_grass/diff.jpg", + "normal": "Vegitations/ground/withered_grass/nor_gl.jpg", + "roughness": "Vegitations/ground/withered_grass/rough.jpg" + }, + { + "id": "snow", + "color": "Vegitations/ground/snow_02/diff.jpg", + "normal": "Vegitations/ground/snow_02/nor_gl.jpg", + "roughness": "Vegitations/ground/snow_02/rough.jpg" + } ], - "terrestrialFlat": ["leafy-grass"], - "rockyFlat": ["gravel", "sand", "rock-ground"], - "slopes": ["rock-ground", "gravel"] + "terrestrialFlat": [ + "leafy-grass" + ], + "rockyFlat": [ + "gravel", + "sand", + "rock-ground" + ], + "slopes": [ + "rock-ground", + "gravel" + ] } diff --git a/KnownSolarSystem/README.md b/KnownSolarSystem/README.md index bf5f22a..ac49b1f 100644 --- a/KnownSolarSystem/README.md +++ b/KnownSolarSystem/README.md @@ -121,13 +121,18 @@ Jordens `ocean`-block aktiverar ett eget vattenpass på planetens havsnivå. Try | `enabled` | Aktivera havet; utelämnat block ger inget vatten | | `maskFromSurfaceTexture` | Använd färgkartans blå vattenområden som kustmask; vita isområden och land förblir torra. `false` fyller terräng under havsnivån utan färgmask | | `seaLevelMeters`, `depthMeters` | Havsnivå relativt planetradien och bottennivå i maskerade hav | -| `waveAmplitudeMeters`, `waveLengthMeters` | Den största vågens amplitud och våglängd; ytterligare elva komponenter ger finare vågor | -| `windDirectionDegrees`, `waveSpeed`, `choppiness` | Vågriktning, animationshastighet och hur spetsiga vågkammarna är | +| `waveAmplitudeMeters`, `waveLengthMeters` | Amplitudbudget och våglängd för det längsta vågbandet. Åtta band med fyra korsande komponenter vardera ger mindre vågor och krusningar | +| `windDirectionDegrees`, `windSpreadDegrees`, `waveSpeed`, `choppiness` | Vindriktning, riktningsspridning (0–180°), animationshastighet och vågkammarnas spetsighet. Ett svagare korssvall bryter upp parallella vågor | +| `rippleStrength` | Styrka på tre skalor av planetförankrade normaldetaljer, 0–3. Standard 0,8 | +| `waveGeometryDistanceMeters` | Avstånd där geometriska vågor har tonats ut helt, 500–10000 m; standard 4000. Normaler och filtrerad reflektionssträvhet fortsätter längre bort | | `roughness`, `reflectionStrength`, `refractionStrength` | Solglittrets bredd, reflektionernas styrka och brytningens skärmförskjutning | | `deepColor`, `shallowColor`, `absorption` | Linjära RGB-färger för djup/grunt vatten och absorption per meter för rött, grönt och blått ljus | | `foamStrength` | Skum på vågkammar och vid grunda stränder | +| `whitecapThreshold` | Minsta vågkompression för vita vågkammar, 0,05–1; standard 0,22. Högre värde ger färre vita kammar. Strandens skum beräknas separat | -Vågspektrumet består av Gerstner-komponenter som filtreras efter pixelstorleken. Ytan söks med strålar i ett lokalt koordinatsystem förankrat i dubbelprecision, så samma hav fungerar från rymden till våghöjd. Refraktionen använder scenens färg och djup från ett separat framebuffer. Havet simulerar ingen fluiddynamik, fartygskölvatten eller bestående skumtransport. +Vågspektrumet består av 32 seedade komponenter med oberoende faser, varierade våglängder och korsande riktningar. Fasmodulering bryter upp långa vågkammar. De 16 längsta komponenterna förskjuter ytan; alla bidrar till normalerna. Vågor filtreras med ett mjukt, riktningsberoende pixelfilter. Detaljer som blir mindre än en pixel övergår till statistisk ytsträvhet, så vatten på håll behåller ett krusigt reflektionsmaterial i stället för att bli en blank spegel. Samma strävhet filtrerar både molnreserv och SSR. Diffust himmelsljus bidrar till vattnets blå färg även i molnskugga. + +Ytan söks med strålar i ett lokalt koordinatsystem förankrat i dubbelprecision, så samma hav fungerar från rymden till våghöjd. Refraktionen använder scenens färg och djup från ett separat framebuffer. Havet simulerar ingen fluiddynamik, fartygskölvatten eller bestående skumtransport. Jordens profil använder ett lugnare hav med 0,6 m amplitudbudget, 36 m längsta vågband och sparsamt skum; parametrarna ändras i `solar-systems.json`. ### Skärmrymdsreflektioner på vatten @@ -151,7 +156,7 @@ Upplösning, raymarch, kantutfasning och uppskalning mäts tillsammans som **WAT SSR kan bara träffa geometri som finns i kamerans färg- och djupbild. Dolda objekt och objekt utanför bilden får därför en analytisk himmel/molnreserv, och övergången tonas ut. Det är inte RTX-strålspårning eller en fullständig reflektionskarta av omgivningen. Metoden bygger på perspektivkorrekt skärmsökning enligt [McGuire och Mara, Efficient GPU Screen-Space Ray Tracing](https://jcgt.org/published/0003/04/04/). -`--ocean-report` kör ett kontrollerat väggtest (även att mark inte ändras), sparar dag-/nattbilder med reflektionen av/på samt undervattens- och omloppsvyer. Jämförelser och GPU-tid skrivs till konsolen; bilder hamnar i appens `graphics-report/`. +`--ocean-report` kör ett kontrollerat väggtest (även att mark inte ändras), sparar dag-/nattbilder med reflektionen av/på, vyer från 10/100/1000/10000 meter samt undervattens- och omloppsvyer. Ett GPU-test läser ytsträvheten ur materialbufferten nära och långt bort och kontrollerar små kamerasteg genom detaljgränserna. Värdena sparas i `graphics-report/ocean-detail.json`. Jämförelser och reflektionspassets tid skrivs till konsolen; bilder hamnar i appens `graphics-report/`. Tekniken är inspirerad av principerna i [Crests beskrivning](https://github.com/JJarvis89/crest-oceanrender#the-tech), med en egen implementation för OpenGL 3.3. Unity-paketet ingår inte som ett beroende. @@ -340,7 +345,11 @@ Styrkan beräknas som `intensity × sqrt(luminositySolar) / (1 + (distanceAu / r Strålar och reflexer beräknas separat för varje sol och läggs till i HDR före bloom och tonmappning. Djupbufferten samplas över solskivan: objekt framför solen dämpar eller släcker glöden. Reflexerna flyttas med solens läge på skärmen och är tydligast när solen ligger vid sidan av bildcentrum. -## Automatiska grafikrapporter +## Vegetation + +Jordens `vegetation`-block aktiverar färgstyrd vegetation. Samtliga planetinställningar och växtdefinitioner beskrivs i `Content/Vegitations/README.md`. Standardkartan är `surfaceTexture`; valfri `vegetation.biomeTexture` anges relativt `KnownSolarSystem/` och använder samma UV-riktning. `enabled: false` stänger av växterna och deras biommarkmaterial. Atmosfäriska `terrestrial`-planeter kan få standardprofilen; andra fasta planeter aktiveras explicit. Gasplaneter kan inte ha vegetation. + +## Grafikrapporter Rapporterna använder appens riktiga OpenGL-shaders i ett dolt fönster och avslutas automatiskt utan att fånga musen: diff --git a/KnownSolarSystem/solar-systems.json b/KnownSolarSystem/solar-systems.json index e24d3b7..0326743 100644 --- a/KnownSolarSystem/solar-systems.json +++ b/KnownSolarSystem/solar-systems.json @@ -217,19 +217,23 @@ "maskFromSurfaceTexture": true, "seaLevelMeters": 0, "depthMeters": 4000, - "waveAmplitudeMeters": 1.6, - "waveLengthMeters": 72, + "waveAmplitudeMeters": 0.6, + "waveLengthMeters": 36, "windDirectionDegrees": 35, + "windSpreadDegrees": 110, "waveSpeed": 1.0, - "choppiness": 0.65, - "roughness": 0.14, + "choppiness": 0.4, + "rippleStrength": 0.65, + "waveGeometryDistanceMeters": 4000, + "whitecapThreshold": 0.22, + "roughness": 0.26, "reflectionStrength": 1.0, "refractionStrength": 0.018, - "foamStrength": 0.65, + "foamStrength": 0.3, "deepColor": [ - 0.006, - 0.045, - 0.075 + 0.008, + 0.07, + 0.14 ], "shallowColor": [ 0.025, @@ -318,7 +322,29 @@ ], "rotationPeriodHours": 23.93447232, "massKg": 5.97217e+24, - "poleLongitudeDegrees": 0 + "poleLongitudeDegrees": 0, + "vegetation": { + "enabled": true, + "densityMultiplier": 0.7, + "treeDistanceMeters": 2000, + "treeLodMeters": [ + 40, + 150, + 400 + ], + "grassDistanceMeters": 80, + "shrubDistanceMeters": 150, + "affectGroundMaterials": true, + "uploadBudgetMilliseconds": 1, + "shadowResolution": 2048, + "shadowSoftness": 1.0, + "shadowCascadeBlend": 0.2, + "shadowBiasMeters": 0.035, + "shadowDistancesMeters": [ + 100, + 400 + ] + } }, { "id": "mars", diff --git a/README.md b/README.md index 0b87f08..91aba4a 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,13 @@ Renderingen integrerar böjda ljusbanor i en förenklad Schwarzschildmodell och Debug-panelen visar `BLACK HOLE / LENSING` separat. Kör `Universe.App.exe --black-hole-report` för att rendera närmaste hål till `graphics-report/black-hole.png` och kontrollera shaderkompilering, OpenGL-fel och giltiga bildvärden. -## Projekt +## Färgstyrd skog + +Jorden har strömmad skog och undervegetation från `Content/Vegitations/vegetation.json`. Planetens färgkarta och samma biomfält i markshadern styr miljön; höjd, lutning, vatten och seedat täthetsbrus begränsar placeringen. Trädrötter och stamkollision följer planetens rotation. Fri kamera passerar vegetationen, FPS-spelaren glider längs stammar. + +**BENCHMARK FOREST** i P-menyn flyger ned till 1,8 m, går genom skog och återställer Jordvyn. CSV/JSON sparas bredvid appen. `--vegetation-report` mäter vegetation av/på med kall/varm cache vid 2560 × 1440 och redovisar prestandagränserna. Se `Content/Vegitations/README.md` för tillgångar, offlineimport, JSON-fält och diagnostik. FPS-målet måste bedömas från rapporten på aktuell maskin, inte antas utifrån grafikkortsmodell. + +## Projektets delar - `src/Universe.Core`: koordinater, systemgenerering, noise, terräng, streaming och svept kollision. - `src/Universe.App`: fönster, kamera och OpenGL. Shaderkoden finns under `Graphics/Shaders/` och bäddas in i bygget. diff --git a/src/Universe.App/Graphics/AdaptivePlanetMesh.cs b/src/Universe.App/Graphics/AdaptivePlanetMesh.cs index d383946..649c606 100644 --- a/src/Universe.App/Graphics/AdaptivePlanetMesh.cs +++ b/src/Universe.App/Graphics/AdaptivePlanetMesh.cs @@ -7,10 +7,10 @@ namespace Universe.App.Graphics; /// Builds tiles on worker threads. A parent remains visible until all four children are ready. internal sealed class AdaptivePlanetMesh : IDisposable { - private static readonly SemaphoreSlim Workers = new(System.Math.Clamp(Environment.ProcessorCount - 2, 1, 4)); private readonly GL _gl; private readonly PlanetData _planet; private readonly Dictionary _tiles = []; + private readonly Queue _retire = new(); private readonly CancellationTokenSource _cancel = new(); private int _frame; private int _uploads; @@ -28,6 +28,7 @@ internal sealed class AdaptivePlanetMesh : IDisposable public int DrawnTiles { get; private set; } public int DeepestLevel { get; private set; } public int PendingTiles => _pending; + public int UploadedTiles=>_uploads; public int NearestLevel { get; private set; } public double NearestDistance { get; private set; } @@ -69,11 +70,24 @@ internal sealed class AdaptivePlanetMesh : IDisposable for (var face = 0; face < roots.Length; face++) roots[face] = new(face, 0, 0, 0); SortNearest(roots, cameraLocal); foreach (var root in roots) Visit(root, cameraLocal, screenHeight, draw); - if (_frame % 120 != 0) return; - foreach (var pair in _tiles.Where(pair => pair.Key.Level > 0 && pair.Value.Task is null && _frame - pair.Value.LastFrame > 360).ToArray()) + // Frame-count expiry discarded nearby geometry during short orbital trips, especially at high FPS. + // Retain a minute of reuse, retire the oldest excess only, and spread driver deletes over frames. + if(_frame%120==0) { - pair.Value.Mesh?.Dispose(); - _tiles.Remove(pair.Key); + var excess=System.Math.Max(0,_tiles.Count-16384-_retire.Count); + foreach(var pair in _tiles.Where(p=>p.Key.Level>0&&p.Value.Task is null&&_frame-p.Value.LastFrame>2&&!p.Value.Retiring).OrderBy(p=>p.Value.LastUsedTicks)) + { + if(System.Diagnostics.Stopwatch.GetElapsedTime(pair.Value.LastUsedTicks).TotalSeconds<=60&&excess<=0)break; + pair.Value.Retiring=true;_retire.Enqueue(pair.Key);excess--; + } + } + for(var i=0;i<16&&_retire.TryDequeue(out var key);i++) + { + if(!_tiles.TryGetValue(key,out var tile))continue; + tile.Retiring=false; + if(tile.Task is not null||_frame-tile.LastFrame<=2)continue; + if(_tiles.Count<=16384&&System.Diagnostics.Stopwatch.GetElapsedTime(tile.LastUsedTicks).TotalSeconds<=60)continue; + tile.Mesh?.Dispose();_tiles.Remove(key); } } @@ -83,6 +97,7 @@ internal sealed class AdaptivePlanetMesh : IDisposable var radius = _planet.RadiusMeters; var tile = _tiles[key]; tile.LastFrame = _frame; + tile.LastUsedTicks=System.Diagnostics.Stopwatch.GetTimestamp(); var distance = Distance(key, camera); var spacingPixels = radius * key.Width / TerrainPatchBuilder.Segments / System.Math.Max(distance, 1) * screenHeight / (2 * _tanHalfFov); if (spacingPixels > TerrainVisibility.PixelTarget(distance) && key.Level < 19) @@ -159,14 +174,10 @@ internal sealed class AdaptivePlanetMesh : IDisposable _tiles.Add(key, tile); _pending++; var token = _cancel.Token; - tile.Task = Task.Run(async () => - { - await Workers.WaitAsync(token); - try { token.ThrowIfCancellationRequested(); return TerrainPatchBuilder.Build(_planet, key); } - finally { Workers.Release(); } - }); + tile.Task = GenerationJobs.Run(()=>TerrainPatchBuilder.Build(_planet,key),token); } tile.LastFrame = _frame; + tile.LastUsedTicks=System.Diagnostics.Stopwatch.GetTimestamp(); if (tile.Mesh is not null) return true; if (tile.Data is null || _uploads >= 6) return false; tile.Mesh = new(_gl, 9, tile.Data.Vertices, tile.Data.Indices); @@ -188,5 +199,7 @@ internal sealed class AdaptivePlanetMesh : IDisposable public Task? Task; public GpuMesh? Mesh; public int LastFrame; + public long LastUsedTicks; + public bool Retiring; } } diff --git a/src/Universe.App/Graphics/BenchmarkOverlay.cs b/src/Universe.App/Graphics/BenchmarkOverlay.cs index 3c555e0..f93fcee 100644 --- a/src/Universe.App/Graphics/BenchmarkOverlay.cs +++ b/src/Universe.App/Graphics/BenchmarkOverlay.cs @@ -65,6 +65,7 @@ internal sealed class BenchmarkOverlay : IDisposable Rect(x, y, 3 * scale, panelHeight, .13f, .55f, .8f); var title=benchmark is GalaxyJourneyBenchmark journey ? journey.IsComplete ? journey.PerformancePassed ? "GALAXY JOURNEY PASS" : "GALAXY JOURNEY FAIL" : journey.IsRunning ? "GALAXY JOURNEY LIVE" : "GALAXY JOURNEY STOPPED" + : benchmark is ForestBenchmark forest?forest.IsComplete?forest.PerformancePassed?"FOREST PASS":"FOREST FAIL":forest.IsRunning?"FOREST LIVE":"FOREST STOPPED" : benchmark.IsRunning ? "BENCHMARK LIVE" : benchmark.Phase == "STOPPED" ? "BENCHMARK STOPPED" : "BENCHMARK COMPLETE"; Text(title, x + 16 * scale, y + 11 * scale, 1.75 * scale, .76f, .91f, 1f); diff --git a/src/Universe.App/Graphics/GraphicsReport.Lookaround.cs b/src/Universe.App/Graphics/GraphicsReport.Lookaround.cs new file mode 100644 index 0000000..d24e9ba --- /dev/null +++ b/src/Universe.App/Graphics/GraphicsReport.Lookaround.cs @@ -0,0 +1,91 @@ +using System.Diagnostics; +using System.Text.Json; +using Silk.NET.OpenGL; +using Universe.App.Runtime; +using Universe.Core.Generation; +using Universe.Core.Math; +using Universe.Core.World; + +namespace Universe.App.Graphics; + +internal static partial class GraphicsReport +{ + public static void RunLookaround(GL gl,UniverseRenderer renderer,FlyCamera camera,UniverseNeighborhood world,int width,int height) + { + var directory=Path.Combine(AppContext.BaseDirectory,"graphics-report","lookaround");Directory.CreateDirectory(directory); + var planet=world.ActiveSystem.Planets.Single(p=>p.Id=="earth"); + var body=UniversePosition.AtCellCenter(world.ActiveSystem.Coordinate,camera.CellSizeMeters).Add(planet.PositionMeters,camera.CellSizeMeters); + var sun=(world.ActiveSystem.Stars[planet.HostStarIndex].PositionMeters-planet.PositionMeters).Normalized(); + var direction=VegetationSiteFinder.FindForest(new(planet,renderer.Vegetation.Catalog),planet.Orientation.ToBody(sun)); + var radial=planet.Orientation.ToWorld(direction); + var tangent=DVector3.Cross(planet.Orientation.ToWorld(new(0,1,0)),radial).Normalized(); + var across=DVector3.Cross(radial,tangent).Normalized(); + var position=body.Add(radial*(TerrainHeight.Sample(planet,radial)+1.8),camera.CellSizeMeters); + void Pose(double angle) + { + var forward=(tangent*System.Math.Cos(angle)+across*System.Math.Sin(angle)+radial*(-.08+.2*System.Math.Sin(angle*2))).Normalized(); + camera.SetPlayerPose(position,forward,(radial-forward*DVector3.Dot(radial,forward)).Normalized()); + } + Pose(0);renderer.SimulationTimeSeconds=12;renderer.BenchmarkProfiling=true; + var warmup=Stopwatch.StartNew();var stable=0;var warmupFrames=0; + while(warmup.Elapsed.TotalSeconds<90) + { + renderer.Render(camera,world,1d/60);gl.Finish();warmupFrames++; + var vegetation=renderer.Vegetation; + stable=vegetation.ReadyModels==vegetation.RequiredModels&&vegetation.Stream?.PendingTiles==0 + &&renderer.TerrainPendingTiles==0&&renderer.TerrainUploads==0?stable+1:0; + if(stable>=30&&warmup.Elapsed.TotalSeconds>=10)break; + if(warmupFrames%300==0)Console.WriteLine($"Lookaround warmup {warmup.Elapsed.TotalSeconds:F1}s: models {vegetation.ReadyModels}/{vegetation.RequiredModels}, tiles {vegetation.Stream?.Tiles.Count()}, pending {vegetation.Stream?.PendingTiles}/{renderer.TerrainPendingTiles}."); + } + if(stable<30)throw new InvalidOperationException("Lookaround scene did not settle before measurement."); + warmup.Stop(); + var profile=Environment.GetCommandLineArgs().Contains("--profile-passes");renderer.ProfileFrame=profile; + var frames=new List(840); + using var process=Process.GetCurrentProcess();var cpuStart=process.TotalProcessorTime; + var allocatedStart=GC.GetTotalAllocatedBytes();var collections=Enumerable.Range(0,3).Select(GC.CollectionCount).ToArray(); + var run=Stopwatch.StartNew(); + for(var frame=0;frame<840;frame++) + { + var phase=frame<120?"stationary":frame<480?"first-turn":"repeat-turn"; + Pose(frame<120?0:(frame-120)*System.Math.PI/180); + renderer.SimulationTimeSeconds=12+frame/60d; + var start=Stopwatch.GetTimestamp();renderer.Render(camera,world,1d/60);gl.Finish(); + var ms=Stopwatch.GetElapsedTime(start).TotalMilliseconds; + if(profile&&ms>50)Console.WriteLine($"Slow lookaround frame {frame}: "+JsonSerializer.Serialize(renderer.FrameTimings)); + if(gl.GetError()!=GLEnum.NoError)throw new InvalidOperationException("Lookaround OpenGL error."); + var vegetation=renderer.Vegetation; + frames.Add(new(phase,ms,vegetation.CpuMilliseconds,vegetation.UploadMilliseconds,vegetation.Stream!.CpuMilliseconds, + vegetation.Stream.GenerationMilliseconds,vegetation.Stream.Revision,vegetation.Stream.PendingTiles,renderer.TerrainPendingTiles,renderer.TerrainUploads)); + if(frame%120==119)Console.WriteLine($"Lookaround {phase}: {frame+1}/840 frames, {ms:F2} ms, vegetation CPU {vegetation.CpuMilliseconds:F2} ms."); + } + var elapsed=run.Elapsed.TotalSeconds;var cpu=(process.TotalProcessorTime-cpuStart).TotalSeconds; + renderer.ProfileFrame=false; + var allocated=GC.GetTotalAllocatedBytes()-allocatedStart; + var gc=Enumerable.Range(0,3).Select(i=>GC.CollectionCount(i)-collections[i]).ToArray(); + using(var csv=new StreamWriter(Path.Combine(directory,"frames.csv"))) + { + csv.WriteLine("phase,frame_ms,vegetation_cpu_ms,upload_ms,stream_cpu_ms,generation_worker_ms,revision,vegetation_pending,terrain_pending,terrain_uploads"); + foreach(var f in frames)csv.WriteLine(FormattableString.Invariant($"{f.Phase},{f.Milliseconds},{f.VegetationCpuMs},{f.UploadMs},{f.StreamCpuMs},{f.GenerationMs},{f.Revision},{f.VegetationPending},{f.TerrainPending},{f.TerrainUploads}")); + } + var summary=new + { + Width=width,Height=height,Gpu=renderer.HardwareName,SyncGpuEachFrame=true,ProfilePasses=profile,WarmupSeconds=warmup.Elapsed.TotalSeconds, + CpuSeconds=cpu,CpuPercentOfAllProcessors=cpu/elapsed/Environment.ProcessorCount*100,AllocatedBytes=allocated,Collections=gc, + Phases=frames.GroupBy(f=>f.Phase).Select(g=>new + { + Phase=g.Key,AverageMs=g.Average(f=>f.Milliseconds),P99Ms=Percentile(g.Select(f=>f.Milliseconds),.99),MaxMs=g.Max(f=>f.Milliseconds), + VegetationCpuAverageMs=g.Average(f=>f.VegetationCpuMs),VegetationCpuP99Ms=Percentile(g.Select(f=>f.VegetationCpuMs),.99), + GenerationWorkerMs=g.Sum(f=>f.GenerationMs),VegetationRevisions=g.Last().Revision-g.First().Revision, + TerrainUploads=g.Sum(f=>f.TerrainUploads),FramesOver50Ms=g.Count(f=>f.Milliseconds>50) + }).ToArray() + }; + var json=JsonSerializer.Serialize(summary,new JsonSerializerOptions{WriteIndented=true}); + File.WriteAllText(Path.Combine(directory,"summary.json"),json);Console.WriteLine(json); + WritePng(Path.Combine(directory,"final.png"),width,height,Read(gl,width,height),false); + Console.WriteLine("Lookaround report: "+directory); + } + private static double Percentile(IEnumerable values,double percentile) + {var sorted=values.Order().ToArray();return sorted[(int)((sorted.Length-1)*percentile)];} + private sealed record LookaroundFrame(string Phase,double Milliseconds,double VegetationCpuMs,double UploadMs,double StreamCpuMs, + double GenerationMs,long Revision,int VegetationPending,int TerrainPending,int TerrainUploads); +} diff --git a/src/Universe.App/Graphics/GraphicsReport.Ocean.cs b/src/Universe.App/Graphics/GraphicsReport.Ocean.cs new file mode 100644 index 0000000..cfd2be5 --- /dev/null +++ b/src/Universe.App/Graphics/GraphicsReport.Ocean.cs @@ -0,0 +1,81 @@ +using System.Text.Json; +using Silk.NET.OpenGL; +using Universe.App.Runtime; +using Universe.Core.Generation; +using Universe.Core.Math; +using Universe.Core.World; + +namespace Universe.App.Graphics; + +internal static partial class GraphicsReport +{ + private static unsafe void ValidateOceanDetail(GL gl, UniverseRenderer renderer, FlyCamera camera, + UniverseNeighborhood world, PlanetData planet, UniversePosition body, DVector3 radial, + DVector3 heading, int width, int height, string directory) + { + void Pose(double altitude, DVector3 forward, DVector3 up) => camera.SetPlayerPose( + body.Add(radial * (planet.RadiusMeters + planet.Ocean!.SeaLevelMeters + altitude), camera.CellSizeMeters), + forward, up, resetMotionHistory: true); + + double Roughness(double altitude) + { + Pose(altitude, -radial, heading); + renderer.Render(camera, world, 0); gl.Finish(); + var framebuffer = gl.GenFramebuffer(); + try + { + gl.BindFramebuffer(GLEnum.ReadFramebuffer, framebuffer); + gl.FramebufferTexture2D(GLEnum.ReadFramebuffer, GLEnum.ColorAttachment0, GLEnum.Texture2D, renderer.OceanMaterialTexture, 0); + gl.ReadBuffer(GLEnum.ColorAttachment0); + if (gl.CheckFramebufferStatus(GLEnum.ReadFramebuffer) != GLEnum.FramebufferComplete) + throw new InvalidOperationException("Incomplete ocean material diagnostic framebuffer."); + var pixels = new float[16 * 16 * 4]; + fixed (float* p = pixels) gl.ReadPixels(width / 2 - 8, height / 2 - 8, 16, 16, PixelFormat.Rgba, PixelType.Float, p); + return Enumerable.Range(0, 256).Average(i => (double)pixels[i * 4 + 3]); + } + finally { gl.BindFramebuffer(GLEnum.ReadFramebuffer, 0); gl.DeleteFramebuffer(framebuffer); } + } + + // Read the production material buffer: lost normal detail must become optical roughness. + var near = Roughness(8); + var far = Roughness(8000); + if (!double.IsFinite(near + far) || near < .04 || far > 1 || far <= near + .015) + throw new InvalidOperationException($"Subpixel ocean slopes lost their roughness: near {near}, far {far}."); + + // Freeze the environment and cross the old and current geometric detail ranges in small steps. + var stableWorld = world with { ActiveSystem = world.ActiveSystem with + { + Planets = world.ActiveSystem.Planets.Select(p => p with { Clouds = null }).ToArray() + } }; + var maxDelta = 0d; + foreach (var range in new[] { 800d, 2000d, planet.Ocean!.WaveGeometryDistanceMeters }.Distinct()) + { + float[]? previous = null; + for (var step = -3; step <= 3; step++) + { + Pose((range + step * .5) / System.Math.Sqrt(2), (heading - radial).Normalized(), radial); + renderer.Render(camera, stableWorld, 0); gl.Finish(); + var pixels = Read(gl, width, height); + if (pixels.Any(v => !float.IsFinite(v))) throw new InvalidOperationException("Ocean LOD produced non-finite pixels."); + if (previous is not null) + { + double sum = 0; + for (var i = 0; i < width * height / 2; i++) + for (var c = 0; c < 3; c++) sum += System.Math.Abs(pixels[i * 4 + c] - previous[i * 4 + c]); + maxDelta = System.Math.Max(maxDelta, sum / (width * height / 2 * 3)); + } + previous = pixels; + } + } + if (maxDelta > .02 || gl.GetError() != GLEnum.NoError) + throw new InvalidOperationException($"Ocean detail transition popped: mean RGB step {maxDelta}."); + File.WriteAllText(Path.Combine(directory, "ocean-detail.json"), JsonSerializer.Serialize(new + { + Gpu = renderer.HardwareName, Width = width, Height = height, + NearRoughness = near, FarRoughness = far, MaximumTransitionRgbDelta = maxDelta, + TransitionStepMeters = .5, TransitionDeltaLimit = .02, WaveComponents = OceanSurface.WaveCount, + planet.Ocean.WaveGeometryDistanceMeters + }, new JsonSerializerOptions { WriteIndented = true })); + Console.WriteLine($"Ocean LOD passed: roughness {near:F3} -> {far:F3}, maximum transition RGB step {maxDelta:G4}."); + } +} diff --git a/src/Universe.App/Graphics/GraphicsReport.Shadows.cs b/src/Universe.App/Graphics/GraphicsReport.Shadows.cs new file mode 100644 index 0000000..02141e4 --- /dev/null +++ b/src/Universe.App/Graphics/GraphicsReport.Shadows.cs @@ -0,0 +1,86 @@ +using System.Numerics; +using Silk.NET.OpenGL; + +namespace Universe.App.Graphics; + +internal static partial class GraphicsReport +{ + public static unsafe void RunShadows(GL gl) + { + const int size=32,width=512,height=256; + var directory=Path.Combine(AppContext.BaseDirectory,"graphics-report","shadows");Directory.CreateDirectory(directory); + var depths=new float[size*size*4]; + for(var layer=0;layer<4;layer++)for(var y=0;y=2)shadow=!shadow; + depths[(layer*size+y)*size+x]=shadow?.3f:.7f; + } + var texture=gl.GenTexture(); + gl.ActiveTexture(GLEnum.Texture2);gl.BindTexture(GLEnum.Texture2DArray,texture); + fixed(float* p=depths)gl.TexImage3D(GLEnum.Texture2DArray,0,InternalFormat.DepthComponent24,size,size,4,0,PixelFormat.DepthComponent,PixelType.Float,p); + gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureMinFilter,(int)GLEnum.Linear); + gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureMagFilter,(int)GLEnum.Linear); + gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureCompareMode,(int)GLEnum.CompareRefToTexture); + gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureCompareFunc,(int)GLEnum.Lequal); + gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureWrapS,(int)GLEnum.ClampToEdge); + gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureWrapT,(int)GLEnum.ClampToEdge); + try + { + using var target=new RenderTarget(gl,width,height,fullPrecision:true); + using var quad=new GpuMesh(gl,3,[-1,-1,0,3,-1,0,-1,3,0]); + ShaderProgram Program(bool foliage)=>ShaderProgram.Create(gl,ShaderSources.Load("fullscreen.vert"), + "#version 330 core\n"+(foliage?"#define VEGETATION_FOLIAGE\n":"")+ + "in vec2 vUv;\nuniform int uFixtureMode;\nuniform int uFixtureLight;\nout vec4 fragColor;\n"+ + ShaderSources.Load("vegetation-shadow")+"\nvoid main(){vec3 p=uFixtureMode==0?vec3(vUv*2.0-1.0,0):vec3(70.0+vUv.x*40.0,0,0);fragColor=vec4(vec3(vegetationShadow(p,uFixtureLight)),1);}"); + using var ground=Program(false);using var foliage=Program(true); + float[] Draw(ShaderProgram program,float softness=1,int light=0,bool cascade=false,bool enabled=true) + { + target.Bind();gl.Disable(GLEnum.DepthTest);gl.Disable(GLEnum.Blend);program.Use(); + program.Set("uVegetationShadowMap",2);program.Set("uVegetationShadows",enabled?1:0); + program.Set("uVegetationShadowDistances",new Vector3(100,400,0)); + program.Set("uVegetationShadowSettings",new Vector3(softness,.2f,.035f)); + program.Set("uFixtureMode",cascade?1:0);program.Set("uFixtureLight",light); + for(var i=0;i<4;i++)program.SetMatrix($"uVegetationShadowMatrices[{i}]",cascade?Matrix4x4.CreateScale(.005f):Matrix4x4.Identity); + gl.ActiveTexture(GLEnum.Texture2);gl.BindTexture(GLEnum.Texture2DArray,texture); + quad.Draw();gl.Finish();return Read(gl,width,height); + } + double Sample(double u,double v,int layer) + { + var px=u*size-.5;var py=v*size-.5;var x=(int)System.Math.Floor(px);var y=(int)System.Math.Floor(py); + double Tap(int xx,int yy)=>depths[(layer*size+System.Math.Clamp(yy,0,size-1))*size+System.Math.Clamp(xx,0,size-1)]>.5?1:0; + var fx=px-x;var fy=py-y; + return (Tap(x,y)*(1-fx)+Tap(x+1,y)*fx)*(1-fy)+(Tap(x,y+1)*(1-fx)+Tap(x+1,y+1)*fx)*fy; + } + double Validate(float[] pixels,int radius,int light) + { + double error=0;var denominator=radius==2?81d:16d; + for(var y=3;y.003)throw new InvalidOperationException($"Shadow tent filter mismatch: {error}."); + return error; + } + var soft=Draw(ground);var hard=Draw(ground,0); + var groundError=Validate(soft,2,0);var leafError=Validate(Draw(foliage),1,0);var secondSunError=Validate(Draw(ground,light:1),2,1); + double EdgeStep(float[] pixels)=>Enumerable.Range(1,width-1).Max(x=>System.Math.Abs(pixels[(height/2*width+x)*4]-pixels[(height/2*width+x-1)*4])); + var hardStep=EdgeStep(hard);var softStep=EdgeStep(soft); + if(softStep>=hardStep*.6)throw new InvalidOperationException("Soft shadows did not smooth the edge."); + WritePng(Path.Combine(directory,"soft.png"),width,height,soft,false);WritePng(Path.Combine(directory,"hard.png"),width,height,hard,false); + Array.Fill(depths,.7f);Array.Fill(depths,.3f,0,size*size); + fixed(float* p=depths)gl.TexSubImage3D(GLEnum.Texture2DArray,0,0,0,0,size,size,4,PixelFormat.DepthComponent,PixelType.Float,p); + var blended=Draw(ground,cascade:true);var blendStep=EdgeStep(blended); + if(blendStep>.006||blended[0]>.181||blended[(width-1)*4]<.999) + throw new InvalidOperationException($"Shadow cascade transition is discontinuous: {blendStep}."); + if(Draw(ground,enabled:false).Any(v=>System.Math.Abs(v-1)>.0001)||gl.GetError()!=GLEnum.NoError) + throw new InvalidOperationException("Disabled shadows or shadow sampler state is invalid."); + WritePng(Path.Combine(directory,"cascades.png"),width,height,blended,false); + Console.WriteLine($"Shadow GPU test passed: ground/foliage/two-sun reference errors {groundError:G3}/{leafError:G3}/{secondSunError:G3}; edge step {hardStep:G3} -> {softStep:G3}; cascade step {blendStep:G3}."); + } + finally {gl.DeleteTexture(texture);gl.BindFramebuffer(GLEnum.Framebuffer,0);gl.Enable(GLEnum.DepthTest);gl.DepthMask(true);} + } +} diff --git a/src/Universe.App/Graphics/GraphicsReport.cs b/src/Universe.App/Graphics/GraphicsReport.cs index b24085f..7132d36 100644 --- a/src/Universe.App/Graphics/GraphicsReport.cs +++ b/src/Universe.App/Graphics/GraphicsReport.cs @@ -11,8 +11,107 @@ using Universe.App.Runtime; namespace Universe.App.Graphics; /// Offscreen tests of the production shaders; no desktop capture or mouse control. -internal static class GraphicsReport +internal static partial class GraphicsReport { + public static void RunVegetation(GL gl,UniverseRenderer renderer,FlyCamera camera,UniverseNeighborhood world,int width,int height) + { + var dir=Path.Combine(AppContext.BaseDirectory,"graphics-report","vegetation");Directory.CreateDirectory(dir); + var planet=world.ActiveSystem.Planets.Single(p=>p.Id=="earth"); + var origin=UniversePosition.AtCellCenter(world.ActiveSystem.Coordinate,camera.CellSizeMeters); + var body=origin.Add(planet.PositionMeters,camera.CellSizeMeters); + var field=new BiomeField(planet,renderer.Vegetation.Catalog); + var sun=(world.ActiveSystem.Stars[planet.HostStarIndex].PositionMeters-planet.PositionMeters).Normalized(); + var direction=VegetationSiteFinder.FindForest(field,planet.Orientation.ToBody(sun)); + var radial=planet.Orientation.ToWorld(direction);var tangent=DVector3.Cross(planet.Orientation.ToWorld(new(0,1,0)),radial).Normalized(); + var ground=TerrainHeight.Sample(planet,radial);var home=camera.Position;var homeForward=camera.Forward;var homeUp=camera.Up; + void Pose(double height,double pitch=0) + {camera.SetPlayerPose(body.Add(radial*(ground+height),camera.CellSizeMeters),(tangent+radial*pitch).Normalized(),radial,true);} + Pose(1.8,-.03);renderer.SimulationTimeSeconds=12;renderer.BenchmarkProfiling=true; + var load=System.Diagnostics.Stopwatch.StartNew();var frames=0;var settledAt=0d; + while(load.Elapsed.TotalSeconds<120) + { + renderer.Render(camera,world,1d/60);frames++; + if(frames%180==0)Console.WriteLine($"Vegetation warmup {load.Elapsed.TotalSeconds:F1}s: models {renderer.Vegetation.ReadyModels}, instances {renderer.Vegetation.Stream?.InstanceCount}, tiles {renderer.Vegetation.Stream?.Tiles.Count()}, pending {renderer.Vegetation.Stream?.PendingTiles}."); + if(renderer.Vegetation.ReadyModels>=renderer.Vegetation.RequiredModels + &&renderer.Vegetation.DistantInstances>1000&&renderer.TerrainPendingTiles==0&&renderer.TerrainUploads==0&&renderer.Vegetation.Stream?.PendingTiles==0) + {if(settledAt==0)settledAt=load.Elapsed.TotalSeconds;else if(load.Elapsed.TotalSeconds-settledAt>.5&&load.Elapsed.TotalSeconds>12)break;} + else settledAt=0; + Thread.Sleep(1); + } + if(renderer.Vegetation.ReadyModels!float.IsFinite(p)))throw new InvalidOperationException("Vegetation produced non-finite output."); + WritePng(Path.Combine(dir,name),width,height,pixels,false); + } + Capture("forest-ground.png"); + Pose(1.8,-.65);for(var i=0;i<120;i++)renderer.Render(camera,world,1d/60);gl.Finish(); + var depth=new float[width*height]; + gl.BindTexture(GLEnum.Texture2D,renderer.SceneDepthTexture); + unsafe{fixed(float* p=depth)gl.GetTexImage(GLEnum.Texture2D,0,PixelFormat.DepthComponent,PixelType.Float,p);} + var threshold=System.Math.Log2(101)/System.Math.Log2(camera.CellSizeMeters*4.5+1); + var covered=depth.Take(width*height/4).Count(d=>float.IsFinite(d)&&d();var allPass=true; + foreach(var enabled in new[]{false,true})foreach(var cold in new[]{true,false}) + { + renderer.Vegetation.Enabled=enabled;if(cold)renderer.Vegetation.ClearTiles(); + // Identical camera poses in all A/B runs; trunk collision is verified separately. + var benchmark=new ForestBenchmark(world.ActiveSystem,planet,renderer.Vegetation.Catalog,camera,home,homeForward,homeUp); + var runName=$"{(enabled?"on":"off")}-{(cold?"cold":"warm")}"; + var measured=new List();var vegetation=new List();var shadows=new List(); + var watch=System.Diagnostics.Stopwatch.StartNew(); + while(benchmark.IsRunning) + { + benchmark.Update(1d/30,camera); + renderer.SimulationTimeSeconds=12+benchmark.ElapsedSeconds; + var start=System.Diagnostics.Stopwatch.GetTimestamp();renderer.Render(camera,world,1d/30);gl.Finish(); + var seconds=System.Diagnostics.Stopwatch.GetElapsedTime(start).TotalSeconds; + benchmark.Metrics(renderer.Vegetation.CpuMilliseconds,renderer.Vegetation.UploadMilliseconds,renderer.VegetationGpuMilliseconds,renderer.VegetationShadowMilliseconds, + renderer.Vegetation.Instances,renderer.Vegetation.Triangles,renderer.Vegetation.Stream?.Tiles.Count()??0,renderer.Vegetation.Stream?.PendingTiles??0,renderer.Vegetation.ReadyModels,renderer.Vegetation.LodCounts,renderer.Vegetation.Stream?.GenerationMilliseconds??0); + benchmark.RecordFrame(seconds);measured.Add(seconds*1000); + vegetation.Add(renderer.VegetationGpuMilliseconds);shadows.Add(renderer.VegetationShadowMilliseconds); + if(measured.Count%720==0)Console.WriteLine($"Forest {runName}: {benchmark.ElapsedSeconds:F0}/{benchmark.DurationSeconds:F0}s, {benchmark.AverageFps:F1} FPS, {renderer.Vegetation.Instances} instances."); + } + benchmark.SaveReport(Path.Combine(dir,runName)); + renderer.DrawBenchmarkOverlay(benchmark);gl.Finish(); + WritePng(Path.Combine(dir,runName,"fps-graph.png"),width,height,Read(gl,width,height),false); + var slowest=measured.Max();var gpu=vegetation.Zip(shadows,(a,b)=>a+b).Order().ToArray();var gpu99=gpu[(int)((gpu.Length-1)*.99)]; + var pass=benchmark.AverageFps>=60&&benchmark.OnePercentLowFps>=55&&slowest<=50&&(!enabled||gpu99<=5);allPass&=pass; + samples.Add(new{Name=runName,benchmark.AverageFps,benchmark.OnePercentLowFps,MaximumFrameMilliseconds=slowest,VegetationGpuP99Milliseconds=gpu99,Passed=pass,Seconds=watch.Elapsed.TotalSeconds}); + Console.WriteLine($"Forest {runName}: {benchmark.AverageFps:F1} FPS, 1% {benchmark.OnePercentLowFps:F1}, max {slowest:F2} ms, vegetation GPU p99 {gpu99:F2} ms; {(pass?"PASS":"FAIL")}."); + } + renderer.Vegetation.Enabled=true;renderer.ProfileFrame=true;Pose(1.8,-.03);renderer.Render(camera,world,0);renderer.ProfileFrame=false; + File.WriteAllText(Path.Combine(dir,"summary.json"),System.Text.Json.JsonSerializer.Serialize(new{Width=width,Height=height,Gpu=renderer.HardwareName,SourceLoadSeconds=sourceLoadSeconds,Passed=allPass,Runs=samples,FrameTimings=renderer.FrameTimings},new System.Text.Json.JsonSerializerOptions{WriteIndented=true})); + Capture("forest-final.png");Console.WriteLine("Vegetation report: "+dir); + if(!allPass)Environment.ExitCode=1; + } + public static void RunSimulation(GL gl, UniverseRenderer renderer, UniverseNeighborhood initial, double cellSize, int width, int height) { var directory = Path.Combine(AppContext.BaseDirectory, "graphics-report"); Directory.CreateDirectory(directory); @@ -227,7 +326,7 @@ internal static class GraphicsReport var dir = Path.Combine(AppContext.BaseDirectory, "graphics-report"); Directory.CreateDirectory(dir); WritePng(Path.Combine(dir, "pause-menu.png"), width, height, pixels, false); var scale = System.Math.Min(width / 1440d, height / 900d); - if (Enumerable.Range(0,7).Any(i => renderer.MenuHit(width*.5,height*.5+(-182+i*57)*scale,width,height)!=i) + if (Enumerable.Range(0,PauseMenu.Count).Any(i => renderer.MenuHit(width*.5,height*.5+(-182+i*57)*scale,width,height)!=i) || renderer.MenuHit(0, 0, width, height) != -1) throw new InvalidOperationException("Menu hit testing failed."); Console.WriteLine($"Pause menu rendered and hit testing passed. Image: {dir}"); @@ -394,8 +493,10 @@ internal static class GraphicsReport var withoutReflections=Snapshot(WithoutSsr(neighborhood),"earth-ocean-reflections-off.png"); var reflectionDifference=withReflections.Zip(withoutReflections,(a,b)=>System.Math.Abs(a-b)).Average(); if(reflectionDifference<.0001)throw new InvalidOperationException($"Water reflections did not change the production image: {reflectionDifference}."); - renderer.ProfileFrame=true;Snapshot(neighborhood,"earth-ocean-reflections-on.png");renderer.ProfileFrame=false; + renderer.ProfileFrame=true; + for(var i=0;i<8;i++)renderer.Render(camera,neighborhood,0); var reflectionMs=renderer.FrameTimings.GetValueOrDefault("water reflections"); + renderer.ProfileFrame=false; var earth=neighborhood.ActiveSystem.Planets.Single(p=>p.Id=="earth"); var up=(camera.Position.RelativeTo(origin,cellSize)-earth.PositionMeters).Normalized(); var night=neighborhood with { ActiveSystem=neighborhood.ActiveSystem with @@ -405,6 +506,20 @@ internal static class GraphicsReport var nightOff=Snapshot(WithoutSsr(night),"earth-ocean-night-reflections-off.png"); var nightDifference=nightOn.Zip(nightOff,(a,b)=>System.Math.Abs(a-b)).Average(); if(nightDifference<.00001)throw new InvalidOperationException("The night sky did not reflect in the water."); + var oceanPosition=camera.Position; var oceanForward=camera.Forward; var oceanUp=camera.Up; + var seaBody=origin.Add(earth.PositionMeters,cellSize); + var tangent=(oceanForward-up*DVector3.Dot(oceanForward,up)).Normalized(); + var side=DVector3.Cross(up,tangent).Normalized(); + var surveyHeading=(tangent*.35+side*.94).Normalized(); + foreach(var altitude in new[]{10d,100d,1000d,10000d}) + { + var pitch=altitude<100?.12:altitude<1000?.18:altitude<10000?.28:.5; + camera.SetPlayerPose(seaBody.Add(up*(earth.RadiusMeters+earth.Ocean!.SeaLevelMeters+altitude),cellSize), + (surveyHeading-up*pitch).Normalized(),up,resetMotionHistory:true); + Snapshot(neighborhood,$"earth-ocean-{altitude:0}m.png"); + } + ValidateOceanDetail(gl,renderer,camera,neighborhood,earth,seaBody,up,surveyHeading,width,height,directory); + camera.SetPlayerPose(oceanPosition,oceanForward,oceanUp,resetMotionHistory:true); renderer.SimulationTimeSeconds = null; for (var i = 0; i < 18; i++) camera.Update(.1, -camera.Up, neighborhood); Capture("earth-underwater.png"); @@ -436,7 +551,7 @@ internal static class GraphicsReport float t=object?wall:1e8; color=vec4(.08,.2,.4,1);normal=vec4(0);fallback=vec4(0); if(object)color=vec4(2,.04,.02,1); - if(water _mesh = new(gl, 6, [], dynamic: true); - private static readonly string[] Labels = ["PLAY", "FREE CAMERA", "DEBUG OFF", "BENCHMARK EARTH", "BENCHMARK UNKNOWN", "GALAXY JOURNEY", "QUIT"]; + private static readonly string[] Labels = ["PLAY", "FREE CAMERA", "DEBUG OFF", "BENCHMARK EARTH", "BENCHMARK UNKNOWN", "GALAXY JOURNEY", "BENCHMARK FOREST", "QUIT"]; + public static int Count=>Labels.Length; internal static readonly Dictionary Font = new() { ['A']=["01110","10001","10001","11111","10001","10001","10001"], ['C']=["01111","10000","10000","10000","10000","10000","01111"], diff --git a/src/Universe.App/Graphics/PerformanceOverlay.cs b/src/Universe.App/Graphics/PerformanceOverlay.cs index 04a6b36..214f1ea 100644 --- a/src/Universe.App/Graphics/PerformanceOverlay.cs +++ b/src/Universe.App/Graphics/PerformanceOverlay.cs @@ -11,7 +11,7 @@ internal sealed class PerformanceOverlay : IDisposable public void Draw(int width, int height, IReadOnlyList> samples, double cpuMilliseconds, double fps, int terrainTiles, int terrainLod, int pendingTiles, string? probePlanetId, - double timeScale = 1, DateTimeOffset? simulationUtc = null) + double timeScale = 1, DateTimeOffset? simulationUtc = null,VegetationRenderer? vegetation=null) { var values = samples.ToDictionary(p => p.Key, p => p.Value, StringComparer.Ordinal); var rows = new List<(string Label, string Value, bool Emphasis)>(); @@ -19,6 +19,9 @@ internal sealed class PerformanceOverlay : IDisposable AddMetric("SKY / STARFIELD", "SKY / STARFIELD"); AddMetric("GALAXIES / NEBULAE", "GALAXIES / NEBULAE"); AddMetric("PLANETS TOTAL", "PLANETS TOTAL"); + AddMetric("VEGETATION", "VEGETATION"); + AddMetric("VEGETATION DEPTH", "VEGETATION DEPTH"); + AddMetric("VEGETATION SHADOWS", "VEGETATION SHADOWS"); foreach (var key in values.Keys.Where(k => k.StartsWith("PLANET ", StringComparison.Ordinal) && k != "PLANETS TOTAL" && k != "GPU FRAME") .OrderBy(k => k, StringComparer.Ordinal)) AddMetric(key, key); @@ -46,6 +49,16 @@ internal sealed class PerformanceOverlay : IDisposable rows.Add(("FPS", $"{Number(fps, "F1")}", false)); rows.Add(("TERRAIN TILES", terrainTiles.ToString(CultureInfo.InvariantCulture), false)); rows.Add(("LOD / PENDING", $"{terrainLod} / {pendingTiles}", false)); + if(vegetation is {} veg) + { + rows.Add(("VEG CPU / UPLOAD",$"{Number(veg.CpuMilliseconds)} / {Number(veg.UploadMilliseconds)} MS",false)); + rows.Add(("VEG SUBMITTED / TRIS",$"{veg.Instances} / {veg.Triangles}",false)); + rows.Add(("VEG WORKER RESULTS",$"{Number(veg.Stream?.GenerationMilliseconds??0)} MS",false)); + rows.Add(("VEG LOD 0 / 1",$"{veg.LodCounts[0]} / {veg.LodCounts[1]}",false)); + rows.Add(("VEG LOD 2 / BILLBOARDS",$"{veg.LodCounts[2]} / {veg.LodCounts[3]}",false)); + rows.Add(("VEG TILES / PENDING",$"{veg.Stream?.Tiles.Count()??0} / {veg.Stream?.PendingTiles??0}",false)); + rows.Add(("VEG MODELS / DRAWS",$"{veg.ReadyModels} / {veg.DrawCalls}",false)); + } rows.Add(("TIME SCALE", $"{Number(timeScale, "F1")} X", true)); if (simulationUtc is { } date) rows.Add(("WORLD UTC", date.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), false)); diff --git a/src/Universe.App/Graphics/PlanetTextures.cs b/src/Universe.App/Graphics/PlanetTextures.cs index 99cba0f..a156bd3 100644 --- a/src/Universe.App/Graphics/PlanetTextures.cs +++ b/src/Universe.App/Graphics/PlanetTextures.cs @@ -10,6 +10,7 @@ internal sealed unsafe class PlanetTextures : IDisposable public uint Height { get; } public uint OceanMask { get; } public uint RegionalHeight { get; } + public uint Biome {get;} public PlanetTextures(GL gl, PlanetData planet) { @@ -22,6 +23,11 @@ internal sealed unsafe class PlanetTextures : IDisposable gl.TexImage2D(GLEnum.Texture2D, 0, InternalFormat.Srgb8Alpha8, (uint)(image?.Width ?? 1), (uint)(image?.Height ?? 1), 0, PixelFormat.Rgba, PixelType.UnsignedByte, data); SetParameters(gl, true); gl.GenerateMipmap(GLEnum.Texture2D); + Biome=gl.GenTexture();gl.BindTexture(GLEnum.Texture2D,Biome); + var biome=planet.VegetationBiomeTexture??planet.SurfaceTexture; + fixed(byte* data=biome?.Rgba??[45,80,35,255]) + gl.TexImage2D(GLEnum.Texture2D,0,InternalFormat.Rgba8,(uint)(biome?.Width??1),(uint)(biome?.Height??1),0,PixelFormat.Rgba,PixelType.UnsignedByte,data); + SetParameters(gl,true);gl.GenerateMipmap(GLEnum.Texture2D); Height = gl.GenTexture(); gl.BindTexture(GLEnum.Texture2D, Height); var map = planet.Terrain.HeightMap; @@ -54,5 +60,5 @@ internal sealed unsafe class PlanetTextures : IDisposable gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureWrapT, (int)GLEnum.ClampToEdge); } - public void Dispose() { _gl.DeleteTexture(Color); _gl.DeleteTexture(Height); _gl.DeleteTexture(OceanMask); _gl.DeleteTexture(RegionalHeight); } + public void Dispose() { _gl.DeleteTexture(Color); _gl.DeleteTexture(Biome); _gl.DeleteTexture(Height); _gl.DeleteTexture(OceanMask); _gl.DeleteTexture(RegionalHeight); } } diff --git a/src/Universe.App/Graphics/PlantGpuModel.cs b/src/Universe.App/Graphics/PlantGpuModel.cs new file mode 100644 index 0000000..e826173 --- /dev/null +++ b/src/Universe.App/Graphics/PlantGpuModel.cs @@ -0,0 +1,61 @@ +using System.Numerics; +using Silk.NET.OpenGL; + +namespace Universe.App.Graphics; + +internal sealed class PlantGpuModel : IDisposable +{ + public VegetationModelData Data {get;} + private readonly PlantTexture[][] _textures; + private readonly VegetationMesh[][][] _meshes; + public PlantGpuModel(VegetationModelData data,bool singleLod=false) + { + Data=data;_textures=new PlantTexture[data.Materials.Length][]; + _meshes=data.Lods.Take(singleLod?1:3).Select(l=>l.Select(v=>new VegetationMesh[v.Primitives.Length]).ToArray()).ToArray(); + } + public IEnumerable Upload(GL gl) + { + for(var i=0;i<_textures.Length;i++) + { + var index=i;var material=Data.Materials[i]; + _textures[i]=new PlantTexture[3]; + _textures[index][0]=new(gl,material.Color,true,material.Foliage,true); + foreach(var step in _textures[index][0].UploadSteps())yield return step; + _textures[index][1]=new(gl,material.Normal,false,deferred:true); + foreach(var step in _textures[index][1].UploadSteps())yield return step; + _textures[index][2]=new(gl,material.Arm,false,deferred:true); + foreach(var step in _textures[index][2].UploadSteps())yield return step; + } + for(var l=0;l<_meshes.Length;l++)for(var v=0;v<_meshes[l].Length;v++)for(var p=0;p<_meshes[l][v].Length;p++) + { + var lod=l;var variant=v;var primitive=p; + yield return ()=>_meshes[lod][variant][primitive]=new(gl,Data.Lods[lod][variant].Primitives[primitive]); + } + } + public int Draw(GL gl,ShaderProgram program,int lod,int variant,uint instances,int count,bool shadow=false,bool canopyOnly=false) + { + var primitives=Data.Lods[lod][variant].Primitives;var triangles=0; + program.Set("uHeight",System.Math.Max(.1f,Data.Lods[lod][variant].Maximum.Y)); + program.Set("uDetailedLighting",lod==0?1:0); + for(var i=0;i=0;y--) + { + z.WriteByte(0);for(var x=0;x>1)^0xedb88320:crc>>1;} + BinaryPrimitives.WriteUInt32BigEndian(length,~crc);output.Write(length); + } +} diff --git a/src/Universe.App/Graphics/ShaderSources.cs b/src/Universe.App/Graphics/ShaderSources.cs index b81c606..d1fad77 100644 --- a/src/Universe.App/Graphics/ShaderSources.cs +++ b/src/Universe.App/Graphics/ShaderSources.cs @@ -17,7 +17,7 @@ internal static class ShaderSources } private static string Expand(string name, string source) { - foreach (var include in new[] { "noise", "planet-height", "camera-ray", "ring-optics", "rock-common", "asteroid-shape", "cloud-common", "screen-space-reflection" }) + foreach (var include in new[] { "noise", "planet-height", "camera-ray", "ring-optics", "rock-common", "asteroid-shape", "cloud-common", "screen-space-reflection", "vegetation-shadow", "biomes" }) { var directive = $"#include \"{include}.glsl\""; if (source.Contains(directive, StringComparison.Ordinal)) diff --git a/src/Universe.App/Graphics/Shaders/biomes.glsl b/src/Universe.App/Graphics/Shaders/biomes.glsl new file mode 100644 index 0000000..61c9eea --- /dev/null +++ b/src/Universe.App/Graphics/Shaders/biomes.glsl @@ -0,0 +1,31 @@ +uniform int uBiomesEnabled; +uniform int uBiomeHasTexture; +uniform sampler2D uBiomeTexture; +uniform sampler2DArray uBiomeColors; +uniform vec3 uBiomeTargets[4]; +uniform float uBiomeTolerances[4]; +uniform float uForestScale; +vec4 biomeWeights(vec3 direction){ + vec3 color; + if(uBiomeHasTexture!=0)color=texture(uBiomeTexture,planetUv(direction)).rgb; + else { + float n=noise3(direction*5.0+uNoiseOffset); + color=abs(direction.y)>.91?vec3(235,238,240)/255.0:n<-.2&&abs(direction.y)<.6?vec3(205,181,120)/255.0:n<.1?vec3(145,150,80)/255.0:vec3(45,80,35)/255.0; + } + vec4 weights=vec4(0); + for(int i=0;i<4;i++){ + float d=length(color-uBiomeTargets[i])*57.7350269; + float t=uBiomeTolerances[i];weights[i]=t<=0.0?(d<.00001?1.0:0.0):1.0-smoothstep(t*.8,t,d); + } + return weights/max(1.0,dot(weights,vec4(1))); +} +vec4 biomeTriColor(float layer,vec3 p,vec3 weights){ + vec4 color=texture(uBiomeColors,vec3(p.yz,layer))*weights.x+texture(uBiomeColors,vec3(p.xz,layer))*weights.y+texture(uBiomeColors,vec3(p.xy,layer))*weights.z; + color.rgb=pow(color.rgb,vec3(2.2));return color; +} +vec3 biomeTriNormal(float layer,vec3 p,vec3 n,vec3 weights){ + vec3 x=texture(uBiomeColors,vec3(p.yz,layer+4.0)).xyz*2.0-1.0; + vec3 y=texture(uBiomeColors,vec3(p.xz,layer+4.0)).xyz*2.0-1.0; + vec3 z=texture(uBiomeColors,vec3(p.xy,layer+4.0)).xyz*2.0-1.0; + return normalize(vec3(sign(n.x)*x.z,x.x,x.y)*weights.x+vec3(y.x,sign(n.y)*y.z,y.y)*weights.y+vec3(z.x,z.y,sign(n.z)*z.z)*weights.z); +} diff --git a/src/Universe.App/Graphics/Shaders/cloud-common.glsl b/src/Universe.App/Graphics/Shaders/cloud-common.glsl index 94b6f5f..7e39e82 100644 --- a/src/Universe.App/Graphics/Shaders/cloud-common.glsl +++ b/src/Universe.App/Graphics/Shaders/cloud-common.glsl @@ -92,7 +92,7 @@ float cloudShadow(vec3 worldPoint,vec3 worldLight) { for(int i=0;i<6;i++)optical+=cloudDensity(ro+rd*(start+(float(i)+.5)*ds),lod,false)*ds; return mix(1.0,exp(-min(20.0,optical*uCloudOptics.y)),uCloudOptics.z); } -vec3 cloudReflection(vec3 worldPoint,vec3 worldDirection,vec3 sky) { +vec3 cloudReflection(vec3 worldPoint,vec3 worldDirection,vec3 sky,float coneAngle) { if(uCloudEnabled==0)return sky; vec3 ro=uCloudCamera+cloudToLocal(worldPoint),rd=cloudToLocal(worldDirection); vec2 outer=cloudSphere(ro,rd,uCloudLayer.x+uCloudLayer.y),inner=cloudSphere(ro,rd,uCloudLayer.x); @@ -104,7 +104,9 @@ vec3 cloudReflection(vec3 worldPoint,vec3 worldDirection,vec3 sky) { vec3 light=vec3(0.0); for(int i=0;i<10;i++) { vec3 p=ro+rd*(start+(float(i)+.5)*ds); - float lod=clamp(log2(max(1.0,(length(worldPoint)+start+(float(i)+.5)*ds)*uCloudPixelAngle*128.0/uCloudLayer.z)),1.0,7.0); + float travel=start+(float(i)+.5)*ds; + float footprint=max((length(worldPoint)+travel)*uCloudPixelAngle,travel*coneAngle); + float lod=clamp(log2(max(1.0,footprint*128.0/uCloudLayer.z)),1.0,7.0); float a=1.0-exp(-cloudDensity(p,lod,false)*ds*uCloudOptics.y); float day=smoothstep(-.05,.12,dot(normalize(p+vec3(0,uCloudPlanetRadius,0)),uCloudPrimarySun)); vec3 color=(vec3(.15,.2,.3)*.2+uCloudPrimaryColor*cloudSunTint(p,uCloudPrimarySun)*.8)*day+vec3(.001); diff --git a/src/Universe.App/Graphics/Shaders/ocean-reflection.frag.glsl b/src/Universe.App/Graphics/Shaders/ocean-reflection.frag.glsl index 085dd16..866a250 100644 --- a/src/Universe.App/Graphics/Shaders/ocean-reflection.frag.glsl +++ b/src/Universe.App/Graphics/Shaders/ocean-reflection.frag.glsl @@ -2,6 +2,7 @@ #include "camera-ray.glsl" #include "screen-space-reflection.glsl" in vec2 vUv; +uniform sampler2D uWaterFallback; // alpha contains the pixel's integrated water roughness out vec4 fragColor; void main() { fragColor=vec4(0); @@ -10,7 +11,8 @@ void main() { vec3 ray=cameraRay(vUv); float distance=linearDepth(vUv)/max(.001,dot(ray,uForward)); if(distance<=.03 || distance>uMaxSurfaceDistance)return; - vec4 hit=traceReflection(ray*distance,normalize(surface.rgb)); + float roughness=max(uReflectionSettings.z,textureLod(uWaterFallback,vUv,0).a); + vec4 hit=traceReflection(ray*distance,normalize(surface.rgb),roughness); float fade=1.0-smoothstep(uMaxSurfaceDistance*.6,uMaxSurfaceDistance,distance); fade*=1.0-smoothstep(.6,1.0,uReflectionSettings.z); fragColor=hit*(fade*uReflectionStrength); diff --git a/src/Universe.App/Graphics/Shaders/ocean.frag.glsl b/src/Universe.App/Graphics/Shaders/ocean.frag.glsl index c88fee7..125369e 100644 --- a/src/Universe.App/Graphics/Shaders/ocean.frag.glsl +++ b/src/Universe.App/Graphics/Shaders/ocean.frag.glsl @@ -12,11 +12,12 @@ uniform float uCameraHeight; uniform vec3 uAxisX; uniform vec3 uAxisY; uniform vec3 uAxisZ; -uniform vec3 uWaveVectors[12]; -uniform float uWavePhases[12]; -uniform float uWaveAmplitudes[12]; -uniform vec3 uDetailOrigins[2]; +uniform vec3 uWaveVectors[32]; +uniform float uWavePhases[32]; +uniform float uWaveAmplitudes[32]; +uniform vec3 uDetailOrigins[3]; uniform vec3 uWaveSettings; // maximum displacement, choppiness, pixel angular size +uniform vec3 uSurfaceDetail; // ripple strength, geometry range (metres), whitecap threshold uniform vec3 uWaterOptics; // roughness, reflection, refraction uniform vec3 uDeepColor; uniform vec3 uShallowColor; @@ -49,43 +50,64 @@ vec2 oceanSphere(vec3 ro,vec3 rd,float expansion) { float radialHeight(vec3 p) { return (dot(p,p)+2.0*uSeaRadius*p.y)/(length(p+vec3(0,uSeaRadius,0))+uSeaRadius); } +float wavePhase(int i,vec3 p,out vec3 phaseGradient) { + int partner=(i/4)*4+(i+1)%4; + float crossing=dot(p,uWaveVectors[partner])+uWavePhases[partner]; + // Crossing waves bend and break up the crests without a tiled height texture. + phaseGradient=uWaveVectors[i]+uWaveVectors[partner]*(.55*cos(crossing)); + return dot(p,uWaveVectors[i])+uWavePhases[i]+.55*sin(crossing); +} float waveHeight(vec3 p,float footprint) { float height=0.0; - for(int i=0;i<8;i++) { - float weight=1.0-smoothstep(.35,1.0,length(uWaveVectors[i])*footprint); - float base=dot(p,uWaveVectors[i])+uWavePhases[i]; + for(int i=0;i<16;i++) { + float width=length(uWaveVectors[i])*footprint; + float weight=exp(-.5*width*width); + vec3 phaseGradient; + float base=wavePhase(i,p,phaseGradient); float phase=base; for(int j=0;j<2;j++)phase=base-uWaveSettings.y*.48*cos(phase); height+=sin(phase)*uWaveAmplitudes[i]*weight; } return height; } -vec3 waveNormal(vec3 p,float footprint,out float compression) { +vec3 waveNormal(vec3 p,vec3 dx,vec3 dy,float footprint,out float compression,out float variance) { vec3 gradient=vec3(0); compression=0.0; - float total=0.0; - for(int i=0;i<12;i++) { - float weight=1.0-smoothstep(.35,1.0,length(uWaveVectors[i])*footprint); - float base=dot(p,uWaveVectors[i])+uWavePhases[i], phase=base; + // Waves below the smallest normal band still contribute microscopic surface roughness. + variance=.004*uSurfaceDetail.x*uSurfaceDetail.x; + vec3 up=normalize(p+vec3(0,uSeaRadius,0)); + for(int i=0;i<32;i++) { + // Integrate each direction over the actual pixel footprint, including grazing views. + vec3 phaseGradient; + float base=wavePhase(i,p,phaseGradient); + float width=length(vec2(dot(phaseGradient,dx),dot(phaseGradient,dy))); + float weight=exp(-.5*width*width); + float phase=base; float q=uWaveSettings.y*.48; for(int j=0;j<3;j++)phase=base-q*cos(phase); float a=uWaveAmplitudes[i]*weight; - gradient+=uWaveVectors[i]*(a*cos(phase)/max(.15,1.0-q*sin(phase))); - compression+=max(0.0,sin(phase))*a; - total+=a; + gradient+=phaseGradient*(a*cos(phase)/max(.15,1.0-q*sin(phase))); + vec3 tangent=uWaveVectors[i]-up*dot(up,uWaveVectors[i]); + float slope=uWaveAmplitudes[i]*length(tangent); + // Unresolved waves become BRDF roughness instead of disappearing into a flat mirror. + variance+=.5*slope*slope*(1.0-weight*weight); + compression+=sin(phase)*slope*weight*uWaveSettings.y; } - compression/=max(.001,total); - vec3 up=normalize(p+vec3(0,uSeaRadius,0)); gradient-=up*dot(gradient,up); // Capillary ripples use a planet-anchored noise field and disappear below the pixel footprint. vec3 bodyPoint=mat3(uWorldToBody)*toWorld(p); vec3 bodyX=mat3(uWorldToBody)*uAxisX,bodyZ=mat3(uWorldToBody)*uAxisZ; - for(int detail=0;detail<2;detail++) { - float frequency=detail==0?.35:1.4; - float weight=1.0-smoothstep(.25,.8,footprint*frequency); - vec3 q=bodyPoint*frequency+uDetailOrigins[detail]; + for(int detail=0;detail<3;detail++) { + float frequency=detail==0?.045:(detail==1?.35:1.4); + float width=footprint*frequency*2.0; + float weight=exp(-.5*width*width); + // Modulo origins on the CPU preserve centimetre detail without planet-sized floats. + vec3 drift=vec3(uTime*.018,0,uTime*.011)*sqrt(frequency); + vec3 q=bodyPoint*frequency+uDetailOrigins[detail]+drift; float gx=(noise3(q+bodyX*.04)-noise3(q-bodyX*.04))/.08; float gz=(noise3(q+bodyZ*.04)-noise3(q-bodyZ*.04))/.08; - gradient+=vec3(gx,0,gz)*weight*(detail==0?.13:.075); + float strength=(detail==0?.055:(detail==1?.14:.08))*uSurfaceDetail.x; + gradient+=vec3(gx,0,gz)*weight*strength; + variance+=strength*strength*.35*(1.0-weight*weight); } return normalize(up-gradient); } @@ -101,7 +123,7 @@ vec3 skyReflection(vec3 direction,vec3 up) { return sky*(.006+daylight*1.35); } void main() { - // Premultiplied-alpha MRT blending preserves earlier ocean planets outside this one's silhouette. + // Misses discard, preserving earlier oceans and their reflection metadata. reflectionNormal=vec4(0); reflectionFallback=vec4(0); vec3 scene=texture(uScene,vUv).rgb; float originalDepth=texture(uDepth,vUv).r; @@ -114,7 +136,8 @@ void main() { vec2 base=oceanSphere(ro,rd,0.0); float t=base.x>0.0?base.x:base.y; bool hit=envelope.y>0.0 && t>0.0; - if(hit && abs(uCameraHeight)<2500.0 && t<2000.0) { + float geometryRange=uSurfaceDetail.y; + if(hit && abs(uCameraHeight)0.0?inner.x:envelope.y; if(underwater) {low=max(.015,inner.y);high=envelope.y;} @@ -128,10 +151,14 @@ void main() { if(height*reference>0.0)low=mid;else high=mid; } float solved=(low+high)*.5; - t=mix(solved,t,smoothstep(800.0,2000.0,t)); + float geometry=1.0-smoothstep(geometryRange*.35,geometryRange,t); + geometry*=1.0-smoothstep(geometryRange*.5,geometryRange,abs(uCameraHeight)); + t=mix(t,solved,geometry); } } vec3 p=ro+rd*max(0.0,t); + // Evaluate derivatives before any divergent discard at the shoreline/horizon. + vec3 dx=dFdx(p),dy=dFdy(p); vec3 radial=normalize(uAxisY+toWorld(p)/uSeaRadius); float waterMask=uHasOceanMask!=0?oceanMaskValue(bodyDirection(radial)):1.0; hit=hit && waterMask>.01 && t.5) { vec3 transmission=exp(-uAbsorption*min(opaque,400.0)); scene=scene*transmission+uShallowColor*(1.0-transmission)*.5; + fragColor=vec4(scene,1);return; } - fragColor=vec4(scene,1);return; + discard; } - float footprint=max(.02,t*uWaveSettings.z/max(.12,abs(dot(rd,normalize(p+vec3(0,uSeaRadius,0)))))); - float compression; - vec3 n=waveNormal(p,footprint,compression), up=normalize(p+vec3(0,uSeaRadius,0)); + float footprint=max(.02,max(length(dx),length(dy))); + float compression,variance; + vec3 n=waveNormal(p,dx,dy,footprint,compression,variance), up=normalize(p+vec3(0,uSeaRadius,0)); + float alpha=clamp(sqrt(pow(uWaterOptics.x,4.0)+variance*.7),.001,1.0); + float roughness=clamp(sqrt(alpha),.04,1.0); vec3 view=-rd; if(dot(n,view)<0.0)n=-n; float ndv=max(.02,dot(n,view)); - float fresnel=.0204+.9796*pow(1.0-ndv,5.0); + // Average Fresnel over unresolved slopes, rather than applying the flat-surface grazing spike. + float environmentNdv=min(1.0,sqrt(ndv*ndv+2.0*variance)); + float fresnel=.0204+(max(.0204,1.0-roughness*.65)-.0204)*pow(1.0-environmentNdv,5.0); vec3 reflectionDirection=reflect(rd,n); - vec3 reflected=skyReflection(reflectionDirection,up); - reflected=cloudReflection(toWorld(p-ro),toWorld(reflectionDirection),reflected); + vec3 reflected=skyReflection(normalize(mix(reflectionDirection,up,roughness*.3)),up); + reflected=cloudReflection(toWorld(p-ro),toWorld(reflectionDirection),reflected,roughness*roughness); vec2 refractUv=clamp(vUv+vec2(dot(toWorld(n),uRight),dot(toWorld(n),uUp))*uWaterOptics.z/(1.0+t*.012),vec2(.001),vec2(.999)); float floorDistance=sceneDistance(refractUv,cameraRay(refractUv)); float thickness=max(.0,min(400.0,floorDistance-t)); vec3 transmission=exp(-uAbsorption*thickness); vec3 scatter=mix(uShallowColor,uDeepColor,1.0-exp(-thickness*.075)); - vec3 lighting=vec3(.04), highlights=vec3(0); + float skyDaylight=0.0; + for(int l=0;l<2;l++){if(l>=uLightCount)break;skyDaylight+=sqrt(uLightFlux[l])*smoothstep(-.08,.16,dot(up,uLightDirections[l]));} + // Diffuse skylight keeps deep water blue even when a cloud blocks the direct sun. + vec3 lighting=vec3(.004)+vec3(.12,.2,.3)*skyDaylight, highlights=vec3(0); for(int l=0;l<2;l++) { if(l>=uLightCount)break; vec3 light=uLightDirections[l]; @@ -168,21 +203,22 @@ void main() { lighting+=uLightColors[l]*uLightFlux[l]*visible*(.25+.75*max(0.0,sunHeight)); vec3 halfVector=normalize(view+light); float ndh=max(0.0,dot(n,halfVector)); - float alpha=max(.045,uWaterOptics.x*uWaterOptics.x+min(.14,footprint*.001)); - float a2=alpha*alpha, den=ndh*ndh*(a2-1.0)+1.0; + float brdfAlpha=max(.025,alpha); + float a2=brdfAlpha*brdfAlpha, den=ndh*ndh*(a2-1.0)+1.0; float distribution=a2/(PI*den*den); - float k=(alpha+1.0)*(alpha+1.0)/8.0; + float k=(brdfAlpha+1.0)*(brdfAlpha+1.0)/8.0; float geometry=ndv/(ndv*(1.0-k)+k)*ndl/(ndl*(1.0-k)+k); float f=.0204+.9796*pow(1.0-max(0.0,dot(view,halfVector)),5.0); - highlights+=uLightColors[l]*uLightFlux[l]*visible*min(25.0,distribution*geometry*f/(4.0*ndv))*1.8; - scatter+=uShallowColor*pow(max(0.0,dot(view,-light)),4.0)*compression*.28*visible; + highlights+=uLightColors[l]*uLightFlux[l]*visible*min(12.0,distribution*geometry*f/(4.0*ndv)); + scatter+=uShallowColor*pow(max(0.0,dot(view,-light)),4.0)*max(0.0,compression)*.28*visible; } vec3 refracted=texture(uScene,refractUv).rgb*transmission+scatter*(1.0-transmission)*lighting; vec3 color=mix(refracted,reflected*uWaterOptics.y,fresnel)+highlights; float shore=(1.0-smoothstep(.2,3.5,thickness))*(.5+.5*sin(uTime*1.3+t*.03)); - vec3 bodyDetail=mat3(uWorldToBody)*toWorld(p)*.35+uDetailOrigins[0]; - float breakup=noise3(bodyDetail+vec3(uTime*.09,0,uTime*.06))*.5+.5; - float foam=clamp((smoothstep(.54,.78,compression)+shore*.7)*uFoamStrength*smoothstep(.22,.72,breakup),0.0,.9); + vec3 bodyDetail=mat3(uWorldToBody)*toWorld(p)*.045+uDetailOrigins[0]; + float breakup=noise3(bodyDetail+vec3(uTime*.025,0,uTime*.016))*.5+.5; + float breaking=smoothstep(uSurfaceDetail.z,uSurfaceDetail.z+.12,compression); + float foam=clamp((breaking*smoothstep(.25,.75,breakup)+shore*.7)*uFoamStrength,0.0,.9); color=mix(color,vec3(.72,.86,.88)*lighting,foam); if(underwater) { vec3 through=exp(-uAbsorption*min(t,400.0)); @@ -191,6 +227,6 @@ void main() { fragColor=vec4(max(color,vec3(0)),1); float reflectance=underwater?0.0:fresnel*uWaterOptics.y*(1.0-foam); reflectionNormal=vec4(toWorld(n)*reflectance,1); - reflectionFallback=vec4(reflected*reflectance,1); + reflectionFallback=vec4(reflected*reflectance,roughness); gl_FragDepth=clamp(log2(1.0+t*max(.001,dot(worldRay,uForward)))/log2(uFar+1.0),0.0,1.0); } diff --git a/src/Universe.App/Graphics/Shaders/planet-height.glsl b/src/Universe.App/Graphics/Shaders/planet-height.glsl index be9729a..951eba6 100644 --- a/src/Universe.App/Graphics/Shaders/planet-height.glsl +++ b/src/Universe.App/Graphics/Shaders/planet-height.glsl @@ -32,9 +32,8 @@ float rasterValue(sampler2D raster,vec2 uv) { } float heightMapValue(vec3 localDirection) { return rasterValue(uHeightMap,planetUv(localDirection)); } float oceanMaskValue(vec3 localDirection) { return rasterValue(uOceanMask,planetUv(localDirection)); } -float surfaceElevation(vec3 worldDirection) { - vec3 d=bodyDirection(worldDirection); - float mapValue=uHasHeightMap!=0 ? heightMapValue(d) : 0.0; +float mappedElevation(vec3 d,out float mapValue) { + mapValue=uHasHeightMap!=0 ? heightMapValue(d) : 0.0; float mapped=uHasHeightMap!=0 ? mix(uHeightRange.x,uHeightRange.y,pow(mapValue,uHeightRange.z)) : 0.0; if(uHasRegionalHeight!=0) { // Avoid subtracting nearby global UVs: the regional tile is only one degree wide. @@ -46,6 +45,18 @@ float surfaceElevation(vec3 worldDirection) { mapped=mix(mapped,texture(uRegionalHeight,clamp(pos,0.0,1.0)).r/uRadius,smoothstep(0.0,uRegionOther.y,edge)); } } + return mapped; +} +float surfaceElevationUpperBound(vec3 worldDirection){ + vec3 d=bodyDirection(worldDirection);float mapValue; + float mapped=mappedElevation(d,mapValue); + float water=uHasOceanMask!=0?oceanMaskValue(d):0.0; + // The ridged terrain is bounded by 1.2 amplitudes, including its land mask. + return mix(mapped+1.2*uAmplitude,uOceanTerrain.x-uOceanTerrain.y,water*uOceanTerrain.z); +} +float surfaceElevation(vec3 worldDirection) { + vec3 d=bodyDirection(worldDirection);float mapValue; + float mapped=mappedElevation(d,mapValue); float mask=(uHasHeightMap!=0 && uLandMask.x>0.5) ? smoothstep(uLandMask.y,uLandMask.y+uLandMask.z,mapValue) : 1.0; float detail=uAmplitude>0.0 ? terrainHeight(d*uNoiseFrequency,uNoiseOffset,9)*uAmplitude*mask : 0.0; float water=uHasOceanMask!=0 ? oceanMaskValue(d) : 0.0; diff --git a/src/Universe.App/Graphics/Shaders/screen-space-reflection.glsl b/src/Universe.App/Graphics/Shaders/screen-space-reflection.glsl index af43ee6..1e0143d 100644 --- a/src/Universe.App/Graphics/Shaders/screen-space-reflection.glsl +++ b/src/Universe.App/Graphics/Shaders/screen-space-reflection.glsl @@ -18,15 +18,14 @@ float edgeConfidence(vec2 uv) { float edge=min(min(uv.x,uv.y),min(1.0-uv.x,1.0-uv.y)); return smoothstep(0.0,uReflectionSettings.x,edge); } -vec4 traceReflection(vec3 world,vec3 normal) { +vec4 traceReflection(vec3 world,vec3 normal,float roughness) { vec3 reflection=reflect(normalize(world),normal); vec3 origin=viewPoint(world+normal*uTraceSettings.z),direction=viewPoint(reflection); if(origin.z<=.02)return vec4(0); float distanceLimit=uTraceSettings.x; if(direction.z<0.0)distanceLimit=min(distanceLimit,(.05-origin.z)/direction.z); vec4 result=vec4(0); - float roughness=uReflectionSettings.z; - float mip=roughness*5.0; + float mip=clamp(log2(max(1.0,roughness*roughness*float(textureSize(uScene,0).y)*.25)),0.0,7.0); // Infinite/distant sky lookup includes the actual stars, atmospheric colour and cloud texture. // Opaque foreground pixels are never treated as sky or used to fill missing geometry. if(uReflectSky!=0 && direction.z>.001) { diff --git a/src/Universe.App/Graphics/Shaders/terrain.frag.glsl b/src/Universe.App/Graphics/Shaders/terrain.frag.glsl index b5f18a2..927cbc8 100644 --- a/src/Universe.App/Graphics/Shaders/terrain.frag.glsl +++ b/src/Universe.App/Graphics/Shaders/terrain.frag.glsl @@ -2,6 +2,8 @@ #include "noise.glsl" #include "planet-height.glsl" #include "cloud-common.glsl" +#include "vegetation-shadow.glsl" +#include "biomes.glsl" in vec3 vWorldPosition; in vec3 vPlanetLocal; in vec3 vNormal; @@ -14,6 +16,8 @@ uniform sampler2D uSlopeColor; uniform sampler2D uSlopeNormal; uniform vec3 uMaterialSettings; uniform float uFar; +uniform sampler2D uVegetationDepth; +uniform int uVegetationDepthCullEnabled; uniform sampler2D uSurfaceTexture; uniform int uHasSurfaceTexture; uniform int uSurfaceType; @@ -30,6 +34,7 @@ uniform int uOccluderCount; uniform vec3 uOccluderCenters[16]; uniform float uOccluderRadii[16]; uniform int uTerrainShadows; +uniform float uMaximumTerrainRadius; uniform int uDebugSkipSurfaceNoise; uniform int uDebugDisableBodyShadows; out vec4 fragColor; @@ -52,6 +57,11 @@ float mountainShadow(vec3 p, vec3 lightDir, float sunHeight) { float t=0.000055; for(int i=0;i<6;i++) { vec3 q=p+lightDir*t; + // Once an outward ray leaves the conservative terrain sphere it cannot hit terrain again. + if(length(q)>uMaximumTerrainRadius&&dot(q,lightDir)>0.0)break; + // Skip procedural octaves only when even the upper height bound yields full visibility. + float clearanceLower=length(q)-(1.0+surfaceElevationUpperBound(normalize(q)))+.000055; + if(18.0*clearanceLower>=t){t*=2.75;continue;} float ground=1.0+surfaceElevation(normalize(q)); float clearance=length(q)-ground+0.000055; visibility=min(visibility,clamp(18.0*clearance/t,0.0,1.0)); @@ -82,6 +92,7 @@ vec3 cloudMaterial(vec3 d) { void main() { + if(uVegetationDepthCullEnabled!=0&&clamp(log2(max(.0001,vFragDepth))/log2(uFar+1.0),0.0,1.0)>texelFetch(uVegetationDepth,ivec2(gl_FragCoord.xy),0).r+.0000002)discard; vec3 radial=normalize(vPlanetLocal); vec3 localDirection=bodyDirection(radial); vec3 textureColor=uHasSurfaceTexture!=0 ? texture(uSurfaceTexture,planetUv(localDirection)).rgb : vec3(0.0); @@ -94,11 +105,13 @@ void main() { // Small rock relief is filtered by the pixel footprint to avoid distant sparkling. vec3 rockPosition=radial*18000.0+uNoiseOffset; float grainFootprint=length(fwidth(rockPosition)); + float materialCoverage=uMaterialsEnabled!=0?uMaterialSettings.z*(1.0-smoothstep(uMaterialSettings.y*.35,uMaterialSettings.y,length(vWorldPosition))):0.0; float grain=0.0,erosion=0.0; if(uDebugSkipSurfaceNoise==0) { grain=noise3(rockPosition)*(1.0-smoothstep(0.55,1.0,grainFootprint)) +0.25*noise3(rockPosition*3.1)*(1.0-smoothstep(0.55,1.0,grainFootprint*3.1)); - erosion=fbm(radial*2400.0+uNoiseOffset)*0.8; + // Detailed PBR ground already supplies small relief; avoid a second rock-noise layer over it. + if(materialCoverage<.5)erosion=fbm(radial*2400.0+uNoiseOffset)*0.8; } float detail=grain*0.10+erosion; vec3 sigmaX=dFdx(vWorldPosition), sigmaY=dFdy(vWorldPosition); @@ -111,7 +124,7 @@ void main() { if(uSurfaceType>=2 || ocean>0.5)normal=radial; float slope=clamp(1.0-dot(normal,radial),0.0,1.0); float geology=0.0,deposits=0.5,strata=0.5; - if(uDebugSkipSurfaceNoise==0) { + if(uDebugSkipSurfaceNoise==0&&uHasSurfaceTexture==0) { geology=fbm(radial*31.0+uNoiseOffset); deposits=noise3(radial*170.0+uNoiseOffset); strata=sin(height*150.0+noise3(radial*76.0)*2.2)*0.5+0.5; @@ -127,17 +140,43 @@ void main() { if(uHasSurfaceTexture!=0) { mineral=textureColor; ao=1.0; snow=0.0; } if(uSurfaceType>=2) { mineral=cloudMaterial(localDirection); ao=1.0; snow=0.0; } float materialRoughness=.8; + vec4 environment=uBiomesEnabled!=0?biomeWeights(localDirection):vec4(0); + if(uBiomesEnabled!=0&&uBiomeHasTexture==0){ + vec3 biomeColor=vec3(0);for(int b=0;b<4;b++)biomeColor+=pow(uBiomeTargets[b],vec3(2.2))*environment[b]; + float total=dot(environment,vec4(1));if(total>.001)mineral=mix(mineral,biomeColor/total,min(1.0,total)); + } + if(uBiomesEnabled!=0&&ocean<.5){ + vec3 q=localDirection*uRadius/uForestScale+uNoiseOffset; + float n=noise3(q)*.7+noise3(q*2.17)*.3; + float cover=smoothstep(-.26,.39,n); + mineral*=1.0-environment.x*cover*.16*smoothstep(350.0,1600.0,length(vWorldPosition)); + } if(uMaterialsEnabled!=0 && ocean<.5) { - float strength=uMaterialSettings.z*(1.0-smoothstep(uMaterialSettings.y*.35,uMaterialSettings.y,length(vWorldPosition))); + float strength=materialCoverage; if(strength>.001) { vec3 bodyNormal=normalize(mat3(uWorldToBody)*normal); vec3 weights=pow(abs(bodyNormal),vec3(4.0)); weights/=max(.0001,weights.x+weights.y+weights.z); vec3 position=vMaterialPosition/uMaterialSettings.x; float rock=smoothstep(.04,.23,slope); - vec4 material=mix(triColor(uFlatColor,position,weights),triColor(uSlopeColor,position,weights),rock); + vec4 flatMaterial; + vec3 flatDetail; + if(uBiomesEnabled!=0&&dot(environment,vec4(1))>.01){ + int dominant=0;for(int b=1;b<4;b++)if(environment[b]>environment[dominant])dominant=b; + vec4 mixed=biomeTriColor(float(dominant),position,weights)*environment[dominant]; + vec3 mixedNormal=biomeTriNormal(float(dominant),position,bodyNormal,weights)*environment[dominant]; + float sum=environment[dominant]; + for(int b=0;b<4;b++)if(b!=dominant&&environment[b]>.05){ + mixed+=biomeTriColor(float(b),position,weights)*environment[b]; + mixedNormal+=biomeTriNormal(float(b),position,bodyNormal,weights)*environment[b];sum+=environment[b]; + } + flatMaterial=mixed/max(.0001,sum);flatDetail=normalize(mixedNormal); + } + else{flatMaterial=triColor(uFlatColor,position,weights);flatDetail=triNormal(uFlatNormal,position,bodyNormal,weights);} + vec4 material=flatMaterial;vec3 detail=flatDetail; + // Do not fetch the legacy flat material beneath a biome or rock maps on flat ground. + if(rock>.001){material=mix(flatMaterial,triColor(uSlopeColor,position,weights),rock);detail=mix(flatDetail,triNormal(uSlopeNormal,position,bodyNormal,weights),rock);} mineral=mix(mineral,material.rgb,strength); materialRoughness=mix(.8,material.a,strength); - vec3 detail=mix(triNormal(uFlatNormal,position,bodyNormal,weights),triNormal(uSlopeNormal,position,bodyNormal,weights),rock); normal=normalize(mix(normal,transpose(mat3(uWorldToBody))*detail,strength*.4)); } } @@ -149,7 +188,8 @@ void main() { float distanceToLight=length(toLight); vec3 ld=toLight/max(distanceToLight,1.0); float sunHeight=dot(radial,ld); - float visibility=smoothstep(-0.025,0.018,sunHeight); + float vegetationVisibility=vegetationShadow(vWorldPosition,l); + float visibility=smoothstep(-0.025,0.018,sunHeight)*vegetationVisibility; if(visibility>0.001) { visibility*=cloudShadow(vWorldPosition,ld); if(uDebugDisableBodyShadows==0) { diff --git a/src/Universe.App/Graphics/Shaders/vegetation-depth.frag.glsl b/src/Universe.App/Graphics/Shaders/vegetation-depth.frag.glsl new file mode 100644 index 0000000..064959e --- /dev/null +++ b/src/Universe.App/Graphics/Shaders/vegetation-depth.frag.glsl @@ -0,0 +1,5 @@ +#version 330 core +in vec2 vUv; +uniform sampler2D uColor; +uniform float uAlphaCutoff; +void main(){if(uAlphaCutoff>0.0&&texture(uColor,vUv).atexelFetch(uNearestDepth,ivec2(gl_FragCoord.xy),0).r+0.0000002)discard; + float angle=atan(vView.x,vView.z)*16.0/6.28318530718;float first=floor(angle),blend=fract(angle); + float row=clamp(round(asin(clamp(vView.y,0.0,1.0))*6.0/3.14159265359),0.0,2.0); + vec4 a=texture(uColor,atlas(first,row)),b=texture(uColor,atlas(first+1.0,row)); + vec4 tex=mix(a,b,blend); + tex.rgb=mix(a.rgb*a.a,b.rgb*b.a,blend)/max(tex.a,.0001); + if(tex.a<.35)discard; + if(vVariation.z<.9999){ + float dither=1.0-fract(sin(dot(floor((vStablePosition+vVariation.w*71.0)*47.0),vec3(12.9898,78.233,37.719)))*43758.5453); + if(dither>vVariation.z)discard; + } + if(uDepthOnly>0){fragColor=vec4(0);return;} + vec3 na=texture(uNormal,atlas(first,row)).rgb*2.0-1.0,nb=texture(uNormal,atlas(first+1.0,row)).rgb*2.0-1.0; + vec3 normal=normalize(vBasis*mix(na*a.a,nb*b.a,blend)); + vec3 light=vec3(.04,.055,.07); + for(int i=0;i<2;i++){ + if(i>=uLightCount)break;float sunHeight=dot(uPlanetUp,uLightDirections[i]); + vec3 extinction=exp(-vec3(.035,.07,.16)/(max(sunHeight,.04)+.08)); + light+=uLightColors[i]*extinction*uLightFlux[i]*smoothstep(-.025,.018,sunHeight)*max(.12,dot(normal,uLightDirections[i]))*vegetationShadow(vWorldPosition,i); + } + fragColor=vec4(tex.rgb*light*(.9+vVariation.x*.2),1); + gl_FragDepth=clamp(log2(max(.0001,vFragDepth))/log2(uFar+1.0),0.0,1.0); +} diff --git a/src/Universe.App/Graphics/Shaders/vegetation-impostor.vert.glsl b/src/Universe.App/Graphics/Shaders/vegetation-impostor.vert.glsl new file mode 100644 index 0000000..00ce58b --- /dev/null +++ b/src/Universe.App/Graphics/Shaders/vegetation-impostor.vert.glsl @@ -0,0 +1,42 @@ +#version 330 core +layout(location=0)in vec3 aPosition; +layout(location=2)in vec2 aUv; +layout(location=3)in vec4 aOffsetScale; +layout(location=4)in vec4 aUpYaw; +layout(location=5)in vec4 aVariation; +uniform mat4 uViewProjection; +uniform mat4 uBodyToWorld; +uniform vec3 uAnchor; +uniform vec3 uCameraBody; +uniform vec3 uCameraOffset; +uniform vec3 uBatchBodyOrigin; +uniform vec3 uModelCenter; +uniform float uBillboardSize; +uniform float uFar; +uniform float uTime; +uniform vec3 uImpostorRange; +out vec3 vWorldPosition; +out vec3 vStablePosition; +out vec2 vUv; +out float vFragDepth; +flat out vec4 vVariation; +flat out vec3 vView; +flat out mat3 vBasis; +void main(){ + vec3 up=normalize(aUpYaw.xyz); + vec3 right=normalize(cross(abs(up.y)<.95?vec3(0,1,0):vec3(1,0,0),up)),forward=cross(right,up); + float c=cos(aUpYaw.w),s=sin(aUpYaw.w);mat3 basis=mat3(right*c+forward*s,up,forward*c-right*s); + vec3 center=aOffsetScale.xyz+basis*uModelCenter*aOffsetScale.w; + vec3 eye=uCameraOffset-center; + float distance=length(eye); + vec3 view=normalize(eye),bx=normalize(cross(up,view)),by=cross(view,bx); + vec3 p=center+(bx*aPosition.x+by*aPosition.y)*uBillboardSize*aOffsetScale.w; + p+=right*sin(uTime*1.3+aVariation.x*28.0)*aVariation.y*max(0.0,aPosition.y); + vWorldPosition=uAnchor+mat3(uBodyToWorld)*p;vUv=aUv; + vStablePosition=transpose(basis)*(p-aOffsetScale.xyz)/aOffsetScale.w; + vView=transpose(basis)*view;vBasis=mat3(uBodyToWorld)*basis;vVariation=aVariation; + vVariation.z*=smoothstep(uImpostorRange.x,uImpostorRange.y,distance)*(1.0-smoothstep(uImpostorRange.z*.9,uImpostorRange.z,distance)); + vec4 clip=uViewProjection*vec4(vWorldPosition,1);vFragDepth=clip.w+1.0;gl_Position=clip; + gl_Position.z=(2.0*log2(max(.0001,vFragDepth))/log2(uFar+1.0)-1.0)*clip.w; + if(distance>uImpostorRange.z||vVariation.z<=0.0)gl_Position=vec4(2,2,2,1); +} diff --git a/src/Universe.App/Graphics/Shaders/vegetation-shadow.glsl b/src/Universe.App/Graphics/Shaders/vegetation-shadow.glsl new file mode 100644 index 0000000..9d83c48 --- /dev/null +++ b/src/Universe.App/Graphics/Shaders/vegetation-shadow.glsl @@ -0,0 +1,56 @@ +uniform sampler2DArrayShadow uVegetationShadowMap; +uniform mat4 uVegetationShadowMatrices[4]; +uniform int uVegetationShadows; +uniform vec3 uVegetationShadowDistances; +uniform vec3 uVegetationShadowSettings; // softness [0,1], cascade blend fraction, bias in metres + +float vegetationCascadeShadow(vec3 world,int light,int cascade) { + int layer=light*2+cascade; + vec4 p=uVegetationShadowMatrices[layer]*vec4(world,1); + vec3 q=p.xyz*.5+.5; + if(any(lessThan(q,vec3(0)))||any(greaterThan(q,vec3(1))))return 1.0; + // Convert metres using the actual orthographic depth gradient, including the GL depth remap. + mat4 matrix=uVegetationShadowMatrices[layer]; + float depthPerMeter=.5*length(vec3(matrix[0].z,matrix[1].z,matrix[2].z)); + float reference=q.z-uVegetationShadowSettings.z*depthPerMeter; + vec2 size=vec2(textureSize(uVegetationShadowMap,0).xy); + vec2 pixel=q.xy*size-.5,f=fract(pixel),anchor=(floor(pixel)+.5)/size; + float softness=uVegetationShadowSettings.x; + if(softness<.001)return texture(uVegetationShadowMap,vec4(q.xy,float(layer),reference)); + float sum=0.0; +#ifdef VEGETATION_FOLIAGE + // Separable 3x3 tent: four bilinear comparison fetches instead of nine point fetches. + vec2 wx=vec2(3.0-2.0*f.x,1.0+2.0*f.x),wy=vec2(3.0-2.0*f.y,1.0+2.0*f.y); + vec2 ox=vec2(-1.0+(2.0-f.x)/wx.x,1.0+f.x/wx.y); + vec2 oy=vec2(-1.0+(2.0-f.y)/wy.x,1.0+f.y/wy.y); + for(int y=0;y<2;y++)for(int x=0;x<2;x++) + sum+=wx[x]*wy[y]*texture(uVegetationShadowMap,vec4(anchor+vec2(ox[x],oy[y])/size,float(layer),reference)); + sum/=16.0; +#else + // Exact, subtexel-continuous 5x5 tent, paired into nine hardware PCF fetches. + // The weights sum to 9 on each axis, including between shadow-map texels. + vec3 wx=vec3(3.0-2.0*f.x,5.0,1.0+2.0*f.x),wy=vec3(3.0-2.0*f.y,5.0,1.0+2.0*f.y); + vec3 ox=vec3(-2.0+(2.0-f.x)/wx.x,(2.0+f.x)/5.0,2.0+f.x/wx.z); + vec3 oy=vec3(-2.0+(2.0-f.y)/wy.x,(2.0+f.y)/5.0,2.0+f.y/wy.z); + for(int y=0;y<3;y++)for(int x=0;x<3;x++) + sum+=wx[x]*wy[y]*texture(uVegetationShadowMap,vec4(anchor+vec2(ox[x],oy[y])/size,float(layer),reference)); + sum/=81.0; +#endif + if(softness<.999) + sum=mix(texture(uVegetationShadowMap,vec4(q.xy,float(layer),reference)),sum,softness); + return sum; +} + +float vegetationShadow(vec3 world,int light) { + if(uVegetationShadows==0)return 1.0; + float distance=length(world); + if(distance>uVegetationShadowDistances.y)return 1.0; + int cascade=distance0.0&&distance>start) + visibility=mix(visibility,vegetationCascadeShadow(world,light,1),smoothstep(start,uVegetationShadowDistances.x,distance)); + float fade=1.0-smoothstep(uVegetationShadowDistances.y*.8,uVegetationShadowDistances.y,distance); + return mix(1.0,.18+.82*visibility,fade); +} diff --git a/src/Universe.App/Graphics/Shaders/vegetation.frag.glsl b/src/Universe.App/Graphics/Shaders/vegetation.frag.glsl new file mode 100644 index 0000000..9c0d01a --- /dev/null +++ b/src/Universe.App/Graphics/Shaders/vegetation.frag.glsl @@ -0,0 +1,80 @@ +#version 330 core +#define VEGETATION_FOLIAGE +#include "vegetation-shadow.glsl" +in vec3 vWorldPosition; +in vec3 vNormal; +in vec3 vStablePosition; +in vec2 vUv; +in float vFragDepth; +flat in vec4 vVariation; +uniform sampler2D uColor; +uniform sampler2D uNormal; +uniform sampler2D uArm; +uniform vec3 uFactor; +uniform float uAlphaCutoff; +uniform int uFoliage; +uniform float uFar; +uniform int uBake; +uniform int uDitherInvert; +uniform int uDepthOnly; +uniform sampler2D uNearestDepth; +uniform int uBakeCluster; +uniform vec3 uBakeMinimum; +uniform vec3 uBakeMaximum; +uniform mat4 uBakeNormalBasis; +uniform int uLightCount; +uniform vec3 uLightDirections[2]; +uniform vec3 uLightColors[2]; +uniform float uLightFlux[2]; +uniform vec3 uPlanetUp; +uniform int uDetailedLighting; +out vec4 fragColor; +float hash(vec3 p){return fract(sin(dot(floor(p*47.0),vec3(12.9898,78.233,37.719)))*43758.5453);} +void main(){ + gl_FragDepth=uBake!=0?gl_FragCoord.z:clamp(log2(max(.0001,vFragDepth))/log2(uFar+1.0),0.0,1.0); + if(uBake==0&&uDepthOnly==0&&gl_FragDepth>texelFetch(uNearestDepth,ivec2(gl_FragCoord.xy),0).r+0.0000002)discard; + if(uBakeCluster!=0&&(any(lessThan(vWorldPosition,uBakeMinimum))||any(greaterThan(vWorldPosition,uBakeMaximum))))discard; + vec4 tex=texture(uColor,vUv); + if(tex.avVariation.z)discard; + } + if(uDepthOnly>0){fragColor=vec4(0);return;} + vec3 normal=normalize(vNormal);if(!gl_FrontFacing)normal=-normal; + vec3 dp1=dFdx(vWorldPosition),dp2=dFdy(vWorldPosition);vec2 du1=dFdx(vUv),du2=dFdy(vUv); + vec3 tangent=dp1*du2.y-dp2*du1.y;vec3 bitangent=-dp1*du2.x+dp2*du1.x; + float inv= inversesqrt(max(dot(tangent,tangent),dot(bitangent,bitangent))+.00001); + vec3 detail=texture(uNormal,vUv).xyz*2.0-1.0; + normal=normalize(mat3(tangent*inv,bitangent*inv,normal)*detail); + vec3 color=tex.rgb*uFactor*(.9+vVariation.x*.2); + if(uBake!=0){ + if(uBake==2)fragColor=vec4((uBakeCluster!=0?mat3(uBakeNormalBasis)*normal:normal)*.5+.5,1); + else fragColor=vec4(pow(max(color,vec3(0)),vec3(1.0/2.2)),tex.a); + return; + } + vec3 arm=texture(uArm,vUv).rgb; + vec3 light=vec3(.035,.05,.065)*(.35+.65*arm.r); + vec3 highlights=vec3(0); + vec3 view=normalize(-vWorldPosition); + for(int i=0;i<2;i++){ + if(i>=uLightCount)break; + vec3 ld=uLightDirections[i];float ndl=max(0.0,dot(normal,ld)); + float back=uFoliage!=0?pow(max(0.0,dot(view,-ld)),3.0)*.32:0.0; + float visibility=vegetationShadow(vWorldPosition,i); + float sunHeight=dot(uPlanetUp,ld); + vec3 extinction=exp(-vec3(.035,.07,.16)/(max(sunHeight,.04)+.08)); + vec3 sunlight=uLightColors[i]*extinction*uLightFlux[i]*visibility*smoothstep(-.025,.018,sunHeight); + light+=sunlight*(ndl+back); + if(uDetailedLighting!=0){ + vec3 halfway=normalize(view+ld);float gloss=1.0-clamp(arm.g,.35,1.0); + float nh=max(0.0,dot(normal,halfway));float nh2=nh*nh,nh4=nh2*nh2,nh8=nh4*nh4,nh16=nh8*nh8,nh32=nh16*nh16; + float f=1.0-max(0.0,dot(view,halfway));float f2=f*f; + float fresnel=.04+.96*f2*f2*f; + highlights+=sunlight*ndl*fresnel*mix(nh8*10.0,nh32*34.0,gloss*gloss)/25.1327412; + }else highlights+=sunlight*ndl*.003; + } + fragColor=vec4(color*light+highlights,1); + gl_FragDepth=clamp(log2(max(.0001,vFragDepth))/log2(uFar+1.0),0.0,1.0); +} diff --git a/src/Universe.App/Graphics/Shaders/vegetation.vert.glsl b/src/Universe.App/Graphics/Shaders/vegetation.vert.glsl new file mode 100644 index 0000000..edf50a7 --- /dev/null +++ b/src/Universe.App/Graphics/Shaders/vegetation.vert.glsl @@ -0,0 +1,46 @@ +#version 330 core +layout(location=0)in vec3 aPosition; +layout(location=1)in vec3 aNormal; +layout(location=2)in vec2 aUv; +layout(location=3)in vec4 aOffsetScale; +layout(location=4)in vec4 aUpYaw; +layout(location=5)in vec4 aVariation; +uniform mat4 uViewProjection; +uniform mat4 uBodyToWorld; +uniform vec3 uAnchor; +uniform float uFar; +uniform float uTime; +uniform float uHeight; +uniform int uShadowPass; +uniform vec3 uLodNearFar; +out vec3 vWorldPosition; +out vec3 vNormal; +out vec3 vStablePosition; +out vec2 vUv; +out float vFragDepth; +flat out vec4 vVariation; +void main(){ + vec3 up=normalize(aUpYaw.xyz); + vec3 right=normalize(cross(abs(up.y)<.95?vec3(0,1,0):vec3(1,0,0),up)); + vec3 forward=cross(right,up); + float c=cos(aUpYaw.w),s=sin(aUpYaw.w); + mat3 basis=mat3(right*c+forward*s,up,forward*c-right*s); + vec3 local=aPosition; + float bend=pow(clamp(aPosition.y/max(.1,uHeight),0.0,1.0),2.0); + local.x+=sin(uTime*1.3+aVariation.x*28.0+aPosition.y*.5)*aVariation.y*bend; + local.z+=cos(uTime*.93+aVariation.x*19.0)*aVariation.y*bend*.6; + vWorldPosition=uAnchor+mat3(uBodyToWorld)*(aOffsetScale.xyz+basis*local*aOffsetScale.w); + vNormal=mat3(uBodyToWorld)*basis*aNormal;vUv=aUv;vVariation=aVariation; + if(uShadowPass==0){ + float distance=length(uAnchor+mat3(uBodyToWorld)*aOffsetScale.xyz); + float fade=uLodNearFar.x<=0.0?1.0:smoothstep(uLodNearFar.x*.9,uLodNearFar.x*1.1,distance); + fade*=1.0-smoothstep(uLodNearFar.y*.9,uLodNearFar.y*1.1,distance); + float range=abs(uLodNearFar.z); + fade*=1.0-smoothstep(range*(uLodNearFar.z<0.0?.8:.9),range,distance); + vVariation.z*=fade; + } + vStablePosition=aPosition; + vec4 clip=uViewProjection*vec4(vWorldPosition,1); + vFragDepth=clip.w+1.0;gl_Position=clip; + if(uShadowPass==0)gl_Position.z=(2.0*log2(max(.0001,vFragDepth))/log2(uFar+1.0)-1.0)*clip.w; +} diff --git a/src/Universe.App/Graphics/SurfaceMaterials.cs b/src/Universe.App/Graphics/SurfaceMaterials.cs index b104fd3..47bae13 100644 --- a/src/Universe.App/Graphics/SurfaceMaterials.cs +++ b/src/Universe.App/Graphics/SurfaceMaterials.cs @@ -14,6 +14,7 @@ internal sealed unsafe class SurfaceMaterials : IDisposable private readonly Catalog _catalog; private readonly Dictionary _entries; private readonly Dictionary _textures = new(StringComparer.OrdinalIgnoreCase); + private uint _biomeColor,_biomeNormal; public SurfaceMaterials(GL gl, string directory) { @@ -45,6 +46,37 @@ internal sealed unsafe class SurfaceMaterials : IDisposable return full; } + public (uint Color,uint Normal) Biomes(VegetationCatalog catalog) + { + if(_biomeColor!=0)return(_biomeColor,_biomeNormal); + const int size=2048; + _biomeColor=_gl.GenTexture();_biomeNormal=_biomeColor; + _gl.BindTexture(GLEnum.Texture2DArray,_biomeColor); + _gl.TexImage3D(GLEnum.Texture2DArray,0,InternalFormat.Rgba8,size,size,8,0,PixelFormat.Rgba,PixelType.UnsignedByte,null); + for(var kind=0;kind<2;kind++) + { + for(var layer=0;layer<4;layer++) + { + var entry=_entries[catalog.Biomes[layer].Material]; + var image=RasterImage.Load(Resolve(kind==0?entry.Color:entry.Normal)); + var rough=kind==0?RasterImage.Load(Resolve(entry.Roughness)):null; + var data=new byte[size*size*4]; + for(var y=0;y_scene.Depth; + internal uint OceanMaterialTexture=>_scene.AuxiliaryColors[1]; + internal int TerrainUploads {get;private set;} + public double VegetationGpuMilliseconds=>_gpuProfiler.Results().Where(p=>p.Key is "VEGETATION" or "VEGETATION DEPTH").Sum(p=>p.Value); + public double VegetationShadowMilliseconds=>_gpuProfiler.Results().FirstOrDefault(p=>p.Key=="VEGETATION SHADOWS").Value; public bool DebugProbeEnabled { get; set; } public double CpuRenderMilliseconds { get; private set; } public float ExposureCompensation { get; set; } = 1f; @@ -98,6 +105,8 @@ internal sealed class UniverseRenderer : IDisposable _cloudNoise = new(gl); _emptyCloudDensity = new(gl, null); _materials = new(gl, Path.Combine(AppContext.BaseDirectory, "Content")); + Vegetation=new(gl,Path.Combine(AppContext.BaseDirectory,"Content","Vegitations")); + _materials.Biomes(Vegetation.Catalog); _emptyRing = new(gl, null); var (vertices, indices) = PrimitiveMeshes.UnitSphere(96, 144); _sphere = new(gl, 6, vertices, indices); @@ -190,6 +199,7 @@ internal sealed class UniverseRenderer : IDisposable ("fullscreen.vert","ocean-reflection.frag"),("fullscreen.vert","ocean-reflection-composite.frag") }) replacements.Add(Program(pair.Item1, pair.Item2)); _starTrails.ReloadShaders(); + Vegetation.ReloadShaders(); } catch (Exception error) { @@ -266,7 +276,7 @@ internal sealed class UniverseRenderer : IDisposable { if (_disposed) return; _starTrails.BeginFrame(camera,_width,_height,deltaSeconds); - var cpuTimer = DebugMode ? System.Diagnostics.Stopwatch.StartNew() : null; + var cpuTimer = DebugMode||BenchmarkProfiling ? System.Diagnostics.Stopwatch.StartNew() : null; var timer = ProfileFrame ? System.Diagnostics.Stopwatch.StartNew() : null; if (timer is not null) { _gl.Finish(); timer.Restart(); FrameTimings.Clear(); } void Mark(string name) @@ -275,7 +285,7 @@ internal sealed class UniverseRenderer : IDisposable _gl.Finish(); FrameTimings[name] = timer.Elapsed.TotalMilliseconds; timer.Restart(); } _time = SimulationTimeSeconds ?? (_time + System.Math.Min(deltaSeconds, 0.1)); - TerrainTiles = 0; + TerrainTiles = 0;TerrainUploads=0; TerrainDeepestLevel = 0; TerrainPendingTiles = 0; TerrainNearestLevel = 0; TerrainNearestDistance = double.PositiveInfinity; SetActiveSystem(neighborhood.ActiveSystem.Coordinate); @@ -284,11 +294,17 @@ internal sealed class UniverseRenderer : IDisposable var projection = Matrix4x4.CreatePerspectiveFieldOfView((float)camera.FieldOfViewRadians, (float)_width / _height, 0.02f, far); var vp = view * projection; var system = neighborhood.ActiveSystem; + Vegetation.Aspect=(double)_width/_height;Vegetation.Update(camera,system,_time); + Mark("vegetation streaming"); RetainSystem(system); var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters); var starPositions = system.Stars.Select(s => center.Add(s.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters)).ToArray(); BindNoise(); - _gpuProfiler.BeginFrame(DebugMode); + _gpuProfiler.BeginFrame(DebugMode||BenchmarkProfiling); + _gpuProfiler.BeginPass("VEGETATION SHADOWS"); + Vegetation.DrawShadows(camera,system); + _gpuProfiler.EndPass(); + Mark("vegetation shadows"); _oceanRendered = false; _cloudLayerPlanetId = null; _scene.Bind(allColors: true); _gl.DepthMask(true); @@ -318,6 +334,14 @@ internal sealed class UniverseRenderer : IDisposable Mark("galaxies and nebulae"); _gl.DepthMask(true); _gl.Disable(GLEnum.Blend); + _gpuProfiler.BeginPass("VEGETATION DEPTH"); + Vegetation.PrepareDepth(camera,system,vp,far,_width,_height); + _gpuProfiler.EndPass(); + Mark("vegetation depth"); + _gpuProfiler.BeginPass("VEGETATION"); + Vegetation.Draw(camera,system,vp,far,_width,_height); + _gpuProfiler.EndPass(); + Mark("vegetation"); _gpuProfiler.BeginPass("PLANETS TOTAL"); DrawPlanets(camera, system, vp, far, starPositions); _gpuProfiler.EndPass(); @@ -389,7 +413,7 @@ internal sealed class UniverseRenderer : IDisposable _gl.Disable(GLEnum.DepthTest); _gl.Disable(GLEnum.Blend); _gl.DepthMask(false); _overlay.Use(); _performanceOverlay.Draw(_width, _height, DebugSamples(), CpuRenderMilliseconds, fps, - TerrainTiles, TerrainDeepestLevel, TerrainPendingTiles, _debugProbePlanetId, timeScale, simulationUtc); + TerrainTiles, TerrainDeepestLevel, TerrainPendingTiles, _debugProbePlanetId, timeScale, simulationUtc,Vegetation); _gl.DepthMask(true); _gl.Enable(GLEnum.DepthTest); } @@ -468,6 +492,16 @@ internal sealed class UniverseRenderer : IDisposable if (onlyPlanetId is not null && planet.Id != onlyPlanetId) continue; if (planet.IsGasPlanet) continue; var relative = relativePlanets[i]; + var blockers=system.Planets.Select((body,index)=>(Body:body,Index:index)).Where(b=>b.Index!=i&&system.Stars.Any(star=> + { + var light=star.PositionMeters-planet.PositionMeters;var distance=light.Length;var direction=light/distance; + var offset=b.Body.PositionMeters-planet.PositionMeters;var along=DVector3.Dot(offset,direction); + return along>0&&alongrelativePlanets[b.Index].ToVector3()).ToArray()); + _surface.SetFloatArray("uOccluderRadii",blockers.Select(b=>(float)b.Body.RadiusMeters).ToArray()); + _surface.Set("uMaximumTerrainRadius",(float)(planet.MaximumTerrainRadiusMeters/planet.RadiusMeters)); if (DVector3.Dot(relative, camera.Forward) < -planet.MaximumTerrainRadiusMeters) continue; if (planet.RadiusMeters / System.Math.Max(1,relative.Length) * _height / (2 * System.Math.Tan(camera.FieldOfViewRadians*.5)) < .5) continue; if (!_planetMeshes.TryGetValue(planet.Id, out var mesh)) @@ -485,20 +519,39 @@ internal sealed class UniverseRenderer : IDisposable } } BindPlanetTerrain(_surface, planet, textures); + Vegetation.BindDepthCulling(_surface,onlyPlanetId is null); + Vegetation.BindShadows(_surface,planet.Id,2); + var biomesEnabled=planet.Vegetation is {Enabled:true,AffectGroundMaterials:true}; + _surface.Set("uBiomesEnabled",biomesEnabled?1:0);_surface.Set("uBiomeColors",0);_surface.Set("uBiomeTexture",1); + _surface.Set("uFlatColor",11);_surface.Set("uFlatNormal",12);_surface.Set("uSlopeColor",13);_surface.Set("uSlopeNormal",14); + if(biomesEnabled) + { + var biomeTextures=_materials.Biomes(Vegetation.Catalog); + _gl.ActiveTexture(GLEnum.Texture0);_gl.BindTexture(GLEnum.Texture2DArray,biomeTextures.Color); + BindTexture(1,textures.Biome); + _surface.Set("uBiomeHasTexture",planet.VegetationBiomeTexture is not null||planet.SurfaceTexture is not null?1:0); + _surface.Set("uForestScale",(float)planet.Vegetation!.ForestScaleMeters); + _surface.SetVectorArray("uBiomeTargets",Vegetation.Catalog.Biomes.Select(b=>new Vector3(b.ColorRgb[0],b.ColorRgb[1],b.ColorRgb[2])/255).ToArray()); + _surface.SetFloatArray("uBiomeTolerances",Vegetation.Catalog.Biomes.Select(b=>(float)b.TolerancePercent).ToArray()); + } BindCloudSettings(_surface, camera, system, planet, relative); var materialSettings = planet.Materials; - _surface.Set("uMaterialsEnabled", materialSettings.Enabled && planet.SurfaceType != PlanetSurfaceType.CloudCovered ? 1 : 0); + // The shader's detail contribution is zero beyond this range. In particular, + // first seeing the Moon from Earth must not decode/upload its ground maps. + var materialsEnabled=materialSettings.Enabled&&materialSettings.Strength>0&&planet.SurfaceType!=PlanetSurfaceType.CloudCovered + &&relative.Length-planet.MaximumTerrainRadiusMeters 0 ? planet.AtmosphereColor : Vector3.Zero); _surface.Set("uSurfaceTexture", 5); @@ -527,6 +580,7 @@ internal sealed class UniverseRenderer : IDisposable { _gpuProfiler.EndPass(); TerrainTiles += mesh.DrawnTiles; + TerrainUploads+=mesh.UploadedTiles; TerrainDeepestLevel = System.Math.Max(TerrainDeepestLevel, mesh.DeepestLevel); TerrainPendingTiles += mesh.PendingTiles; if (mesh.NearestDistance < TerrainNearestDistance) @@ -694,7 +748,7 @@ internal sealed class UniverseRenderer : IDisposable _gl.BlitFramebuffer(0, 0, _width, _height, 0, 0, _width, _height, ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit, BlitFramebufferFilter.Nearest); _scene.Bind(allColors: true); _gl.Enable(GLEnum.DepthTest); _gl.DepthFunc(GLEnum.Always); _gl.DepthMask(true); - _gl.Enable(GLEnum.Blend); _gl.BlendFunc(GLEnum.One, GLEnum.OneMinusSrcAlpha); + _gl.Disable(GLEnum.Blend); _oceanRendered = true; _ocean.Use(); SetCameraRay(_ocean, camera); BindPlanetTerrain(_ocean, planet, textures); BindCloudSettings(_ocean, camera, system, planet, relative); @@ -710,9 +764,9 @@ internal sealed class UniverseRenderer : IDisposable _ocean.Set("uAxisX", east.ToVector3()); _ocean.Set("uAxisY", up.ToVector3()); _ocean.Set("uAxisZ", north.ToVector3()); _ocean.Set("uSeaRadius", (float)radius); _ocean.Set("uCameraHeight", (float)(relative.Length - radius)); var bodyAnchor = planet.Orientation.ToBody(up * radius); - for (var detail = 0; detail < 2; detail++) + for (var detail = 0; detail < OceanSurface.DetailFrequencies.Length; detail++) { - var q = bodyAnchor * (detail == 0 ? .35 : 1.4); + var q = bodyAnchor * OceanSurface.DetailFrequencies[detail]; _ocean.Set($"uDetailOrigins[{detail}]", new Vector3((float)System.Math.IEEERemainder(q.X, 64), (float)System.Math.IEEERemainder(q.Y, 64), (float)System.Math.IEEERemainder(q.Z, 64))); } @@ -721,11 +775,12 @@ internal sealed class UniverseRenderer : IDisposable { var wave = waves[i]; var direction = planet.Orientation.ToWorld(wave.Direction); _ocean.Set($"uWaveVectors[{i}]", (Local(direction) * wave.WaveNumber).ToVector3()); - _ocean.Set($"uWavePhases[{i}]", (float)System.Math.IEEERemainder(DVector3.Dot(up * radius, direction) * wave.WaveNumber - _time * wave.AngularFrequency, System.Math.Tau)); + _ocean.Set($"uWavePhases[{i}]", (float)System.Math.IEEERemainder(DVector3.Dot(up * radius, direction) * wave.WaveNumber - _time * wave.AngularFrequency + wave.Phase, System.Math.Tau)); _ocean.Set($"uWaveAmplitudes[{i}]", (float)wave.Amplitude); } - _ocean.Set("uWaveSettings", new Vector3((float)(waves.Take(8).Sum(w => w.Amplitude) + .1), (float)settings.Choppiness, + _ocean.Set("uWaveSettings", new Vector3((float)(waves.Take(OceanSurface.GeometryWaveCount).Sum(w => w.Amplitude) + .1), (float)settings.Choppiness, (float)(2 * System.Math.Tan(camera.FieldOfViewRadians * .5) / _height))); + _ocean.Set("uSurfaceDetail", new Vector3((float)settings.RippleStrength, (float)settings.WaveGeometryDistanceMeters, (float)settings.WhitecapThreshold)); _ocean.Set("uWaterOptics", new Vector3((float)settings.Roughness, (float)settings.ReflectionStrength, (float)settings.RefractionStrength)); _ocean.Set("uDeepColor", new Vector3(settings.DeepColor[0], settings.DeepColor[1], settings.DeepColor[2])); _ocean.Set("uShallowColor", new Vector3(settings.ShallowColor[0], settings.ShallowColor[1], settings.ShallowColor[2])); @@ -761,7 +816,9 @@ internal sealed class UniverseRenderer : IDisposable _gl.Disable(GLEnum.Blend); _gl.Disable(GLEnum.DepthTest); _gl.DepthMask(false); _waterReflectionRaw.Bind(); _oceanReflection.Use(); SetCameraRay(_oceanReflection, camera); BindTexture(0, source.Color); BindTexture(1, _scene.Depth); BindTexture(2, _scene.AuxiliaryColors[0]); + BindTexture(3, _scene.AuxiliaryColors[1]); _oceanReflection.Set("uScene", 0); _oceanReflection.Set("uDepth", 1); _oceanReflection.Set("uWaterNormals", 2); + _oceanReflection.Set("uWaterFallback", 3); _oceanReflection.Set("uFar", far); _oceanReflection.Set("uMaxSteps", r.MaxSteps); _oceanReflection.Set("uTraceSettings", new Vector3((float)r.MaxDistanceMeters, (float)r.ThicknessMeters, (float)r.NormalBiasMeters)); _oceanReflection.Set("uReflectionSettings", new Vector3((float)r.EdgeFade, (float)r.StridePixels, (float)planet.Ocean.Roughness)); @@ -1183,6 +1240,7 @@ internal sealed class UniverseRenderer : IDisposable _ocean.Dispose(); _oceanReflection.Dispose(); _oceanReflectionComposite.Dispose(); _waterReflectionRaw?.Dispose(); _waterReflectionTarget.Dispose(); _clouds.Dispose(); _cloudComposite.Dispose(); _cloudNoise.Dispose(); _emptyCloudDensity.Dispose(); _materials.Dispose(); _cloudRaw?.Dispose(); + Vegetation.Dispose(); _scene.Dispose(); _scatterA.Dispose(); _scatterB.Dispose(); _bloomA.Dispose(); _bloomB.Dispose(); _noise.Dispose(); } } diff --git a/src/Universe.App/Graphics/VegetationMesh.cs b/src/Universe.App/Graphics/VegetationMesh.cs new file mode 100644 index 0000000..efa95ec --- /dev/null +++ b/src/Universe.App/Graphics/VegetationMesh.cs @@ -0,0 +1,126 @@ +using Silk.NET.OpenGL; + +namespace Universe.App.Graphics; + +internal sealed unsafe class VegetationMesh : IDisposable +{ + private readonly GL _gl; + private readonly uint _vao,_vertices,_indices; + private readonly Dictionary _instanceVaos=[]; + public int IndexCount {get;} + public VegetationMesh(GL gl,PlantPrimitive mesh) + { + _gl=gl;IndexCount=mesh.Indices.Length;_vao=gl.GenVertexArray();_vertices=gl.GenBuffer();_indices=gl.GenBuffer(); + gl.BindVertexArray(_vao);gl.BindBuffer(GLEnum.ArrayBuffer,_vertices); + fixed(float* p=mesh.Vertices)gl.BufferData(GLEnum.ArrayBuffer,(nuint)(mesh.Vertices.Length*4),p,GLEnum.StaticDraw); + Attribute(0,3,32,0);Attribute(1,3,32,12);Attribute(2,2,32,24); + gl.BindBuffer(GLEnum.ElementArrayBuffer,_indices); + fixed(uint* p=mesh.Indices)gl.BufferData(GLEnum.ElementArrayBuffer,(nuint)(mesh.Indices.Length*4),p,GLEnum.StaticDraw); + gl.BindVertexArray(0); + } + private void Attribute(uint id,int count,uint stride,int offset) + {_gl.EnableVertexAttribArray(id);_gl.VertexAttribPointer(id,count,VertexAttribPointerType.Float,false,stride,(void*)offset);} + public void Draw(uint instances,int count) + { + if(count==0)return; + if(!_instanceVaos.TryGetValue(instances,out var vao)) + { + vao=_gl.GenVertexArray();_instanceVaos.Add(instances,vao);_gl.BindVertexArray(vao); + _gl.BindBuffer(GLEnum.ArrayBuffer,_vertices);Attribute(0,3,32,0);Attribute(1,3,32,12);Attribute(2,2,32,24); + _gl.BindBuffer(GLEnum.ElementArrayBuffer,_indices);_gl.BindBuffer(GLEnum.ArrayBuffer,instances); + for(uint i=3;i<=5;i++){Attribute(i,4,48,(int)(i-3)*16);_gl.VertexAttribDivisor(i,1);} + } + else _gl.BindVertexArray(vao); + _gl.DrawElementsInstanced(GLEnum.Triangles,(uint)IndexCount,DrawElementsType.UnsignedInt,null,(uint)count); + _gl.BindVertexArray(0); + } + public void ForgetInstances(uint buffer){if(_instanceVaos.Remove(buffer,out var vao))_gl.DeleteVertexArray(vao);} + public void Dispose(){foreach(var vao in _instanceVaos.Values)_gl.DeleteVertexArray(vao);_gl.DeleteVertexArray(_vao);_gl.DeleteBuffer(_vertices);_gl.DeleteBuffer(_indices);} +} + +internal sealed unsafe class PlantTexture : IDisposable +{ + private readonly GL _gl; + private readonly bool _srgb; + private readonly Universe.Core.Generation.RasterImage[] _mips; + public uint Handle {get;} + public PlantTexture(GL gl,Universe.Core.Generation.RasterImage image,bool srgb,bool preserveCoverage=false,bool deferred=false) + { + _gl=gl;_srgb=srgb;_mips=PlantMipmaps.Get(image,preserveCoverage);Handle=gl.GenTexture(); + gl.BindTexture(GLEnum.Texture2D,Handle); + gl.TexParameter(GLEnum.Texture2D,GLEnum.TextureMinFilter,(int)GLEnum.LinearMipmapLinear); + gl.TexParameter(GLEnum.Texture2D,GLEnum.TextureMagFilter,(int)GLEnum.Linear); + gl.TexParameter(GLEnum.Texture2D,GLEnum.TextureWrapS,(int)GLEnum.Repeat);gl.TexParameter(GLEnum.Texture2D,GLEnum.TextureWrapT,(int)GLEnum.Repeat); + if(!deferred)foreach(var step in UploadSteps())step(); + } + public IEnumerable UploadSteps() + { + for(var level=0;level<_mips.Length;level++) + { + var mip=_mips[level];var index=level; + yield return ()=>Allocate(index,mip); + var rows=System.Math.Max(1,262144/(mip.Width*4)); + for(var y=0;yUpload(index,mip,row,count); + } + } + } + private void Allocate(int level,Universe.Core.Generation.RasterImage mip) + { + _gl.BindTexture(GLEnum.Texture2D,Handle); + _gl.TexImage2D(GLEnum.Texture2D,level,_srgb?InternalFormat.Srgb8Alpha8:InternalFormat.Rgba8,(uint)mip.Width,(uint)mip.Height,0,PixelFormat.Rgba,PixelType.UnsignedByte,null); + } + private void Upload(int level,Universe.Core.Generation.RasterImage mip,int row,int count) + { + _gl.BindTexture(GLEnum.Texture2D,Handle); + fixed(byte* p=mip.Rgba)_gl.TexSubImage2D(GLEnum.Texture2D,level,0,row,(uint)mip.Width,(uint)count,PixelFormat.Rgba,PixelType.UnsignedByte,p+row*mip.Width*4); + } + public void Dispose()=>_gl.DeleteTexture(Handle); +} + +internal static class PlantMipmaps +{ + private sealed class Cache {public Universe.Core.Generation.RasterImage[]? Plain,Alpha;} + private static readonly System.Runtime.CompilerServices.ConditionalWeakTable Caches=new(); + public static Universe.Core.Generation.RasterImage[] Get(Universe.Core.Generation.RasterImage image,bool preserveCoverage) + { + var cache=Caches.GetOrCreateValue(image); + lock(cache) + { + var existing=preserveCoverage?cache.Alpha:cache.Plain;if(existing is not null)return existing; + var result=new List{image}; + var width=image.Width;var height=image.Height;var pixels=image.Rgba; + var target=preserveCoverage?(double)Enumerable.Range(0,width*height).Count(i=>pixels[i*4+3]>=102)/(width*height):0; + while(width>1||height>1) + { + var w=System.Math.Max(1,width/2);var h=System.Math.Max(1,height/2);var next=new byte[w*h*4]; + for(var y=0;y0&&target<1) + { + var lo=.25;var hi=4d; + for(var i=0;i<10;i++) + { + var scale=(lo+hi)*.5;var covered=0;for(var j=3;j=102)covered++; + if((double)covered/(w*h)(); + RasterImage Image(Texture? texture,byte[] fallback) + { + if(texture is null)return new(1,1,fallback); + var source=texture.PrimaryImage; + if(images.TryGetValue(source.LogicalIndex,out var cached))return cached; + var decoded=ImageResult.FromMemory(source.Content.Content.ToArray(),ColorComponents.RedGreenBlueAlpha); + var image=new RasterImage(decoded.Width,decoded.Height,decoded.Data);images[source.LogicalIndex]=image;return image; + } + var materials=root.LogicalMaterials.Select(m=> + { + var color=m.FindChannel("BaseColor");var normal=m.FindChannel("Normal");var arm=m.FindChannel("MetallicRoughness"); + var foliage=m.DoubleSided||m.Alpha!=AlphaMode.OPAQUE; + return new PlantMaterial(Image(color?.Texture,[255,255,255,255]),Image(normal?.Texture,[128,128,255,255]), + Image(arm?.Texture,[255,190,0,255]),color is {} c?new(c.Color.X,c.Color.Y,c.Color.Z):Vector3.One, + foliage?System.Math.Max(.25f,m.AlphaCutoff):0,foliage,m.Name??""); + }).ToArray(); + foreach(var m in materials){PlantMipmaps.Get(m.Color,m.Foliage);PlantMipmaps.Get(m.Normal,false);PlantMipmaps.Get(m.Arm,false);} + var lods=new PlantVariant[3][]; + for(var lod=0;lod<3;lod++) + { + if(lod>0&&!root.LogicalScenes.Any(s=>s.Name=="LOD"+lod)){lods[lod]=lods[0];continue;} + var scene=root.LogicalScenes.FirstOrDefault(s=>s.Name=="LOD"+lod)??root.DefaultScene; + lods[lod]=scene.VisualChildren.Select(node=> + { + var primitives=new List();var minimum=new Vector3(float.PositiveInfinity);var maximum=new Vector3(float.NegativeInfinity); + void Visit(Node n) + { + if(n.Mesh is {} mesh)foreach(var p in mesh.Primitives) + { + var positions=p.GetVertexAccessor("POSITION").AsVector3Array(); + var normals=p.GetVertexAccessor("NORMAL")?.AsVector3Array(); + var uv=p.GetVertexAccessor("TEXCOORD_0")?.AsVector2Array(); + var data=new float[positions.Count*8];var matrix=n.WorldMatrix; + Matrix4x4.Invert(matrix,out var inverse);var normalMatrix=Matrix4x4.Transpose(inverse); + for(var i=0;i(uint)i).ToArray(),p.Material.LogicalIndex)); + } + foreach(var child in n.VisualChildren)Visit(child); + } + Visit(node);return new PlantVariant(primitives.ToArray(),minimum,maximum); + }).ToArray(); + } + return new(lods,materials); + } +} diff --git a/src/Universe.App/Graphics/VegetationPreparation.cs b/src/Universe.App/Graphics/VegetationPreparation.cs new file mode 100644 index 0000000..adf6910 --- /dev/null +++ b/src/Universe.App/Graphics/VegetationPreparation.cs @@ -0,0 +1,120 @@ +using System.Numerics; +using System.Text.Json; +using Silk.NET.OpenGL; +using Universe.Core.Generation; + +namespace Universe.App.Graphics; + +internal static unsafe class VegetationPreparation +{ + private sealed record Card(float[] Center,float[] Right,float[] Up,float[] Normal,float Size,float[] Uv); + private static void BakeFoliage(GL gl,string directory,VegetationCatalog catalog,ShaderProgram program,uint instance) + { + const int columns=8,rows=9,cell=192; + using var target=new RenderTarget(gl,columns*cell,rows*cell,true); + foreach(var definition in catalog.Plants.Where(p=>p.Type==VegetationType.Tree)) + { + var prepared=Path.GetDirectoryName(Path.Combine(directory,definition.Model))!; + var full=Path.Combine(prepared,"../source/bake-source.glb"); + if(!File.Exists(full)||File.Exists(Path.Combine(prepared,"cards.json")))continue; + Console.WriteLine("Baking clustered full-detail foliage: "+definition.Id); + var source=VegetationModelData.Load(full);using var model=new PlantGpuModel(source,true); + foreach(var upload in model.Upload(gl))upload(); + var variants=new List(); + for(var variant=0;variant();var minima=new List();var maxima=new List(); + for(var y=0;y<6;y++)for(var z=0;z<2;z++)for(var x=0;x<2;x++) + { + var min=bounds.Minimum+extent*new Vector3(x*.5f,y/6f,z*.5f); + var max=min+extent*new Vector3(.5f,1/6f,.5f);var center=(min+max)*.5f; + foreach(var direction in new[]{Vector3.UnitZ,Vector3.UnitX,Vector3.UnitY}) + { + var right=Vector3.Normalize(Vector3.Cross(direction==Vector3.UnitY?Vector3.UnitZ:Vector3.UnitY,direction)); + var up=Vector3.Cross(direction,right);var size=Vector3.Distance(min,max)*1.03f;var n=cards.Count; + cards.Add(new([center.X,center.Y,center.Z],[right.X,right.Y,right.Z],[up.X,up.Y,up.Z],[direction.X,direction.Y,direction.Z],size, + [(n%columns)/(float)columns,1-(n/columns+1)/(float)rows,1f/columns,1f/rows])); + minima.Add(min);maxima.Add(max); + } + } + variants.Add(cards.ToArray()); + for(var mode=1;mode<=2;mode++) + { + target.Bind();gl.ClearColor(0,0,0,0);gl.ClearDepth(1);gl.Clear(ClearBufferMask.ColorBufferBit|ClearBufferMask.DepthBufferBit); + gl.Disable(GLEnum.Blend);gl.Disable(GLEnum.CullFace);gl.Enable(GLEnum.DepthTest);gl.DepthMask(true);gl.DepthFunc(GLEnum.Lequal); + program.Use();program.SetMatrix("uBodyToWorld",Matrix4x4.Identity);program.Set("uAnchor",Vector3.Zero); + program.Set("uTime",0f);program.Set("uFar",1000f);program.Set("uShadowPass",1);program.Set("uBake",mode);program.Set("uBakeCluster",1); + program.Set("uVegetationShadows",0);program.Set("uVegetationShadowMap",15); + for(var i=0;i(); + for(var variant=0;variant Task; + public PlantGpuModel? Gpu; + public IEnumerator? Upload; + public PlantTexture[] Color=[],Normal=[]; + public bool Ready; + public double ReadyAt; + public double ReadyFade; + } + private sealed class Batch {public readonly List Data=[];public uint Buffer;public int CapacityBytes;public int Count=>Data.Count/12;} + private readonly GL _gl;private readonly string _root; + public VegetationCatalog Catalog {get;} + public VegetationStream? Stream {get;private set;} + public string? PlanetId {get;private set;} + private PlanetData? _planet; + private string? _systemId; + private DVector3 _cameraBody,_origin; + private readonly Dictionary _models=[]; + private readonly HashSet _requiredPaths=[]; + private readonly Dictionary<(int Plant,int Lod,int Variant),Batch> _near=[]; + private readonly Dictionary<(int Plant,int Variant),Batch> _far=[]; + private readonly Dictionary<(int Plant,int Cascade,int Variant),Batch> _shadowBatches=[]; + private ShaderProgram _surface,_impostor,_depth; + private readonly VegetationMesh _quad; + private uint _shadowMap,_shadowFramebuffer;private int _shadowSize; + private readonly Matrix4x4[] _shadowMatrices=new Matrix4x4[4]; + private int _shadowLights; + private uint _nearestDepth;private int _depthWidth,_depthHeight; + private double _time; + private double _surfaceAltitude; + private long _farRevision=-1;private DVector3 _farOrigin; + private long _nearRevision=-1;private int _nearModels;private DVector3 _nearCamera; + private VegetationView? _nearView; + private readonly List _nearCandidates=[]; + private readonly record struct NearInstance(VegetationInstance Instance,int Variant,double Radius,double Distance,double Fade); + private sealed record FarSource(Dictionary<(int Plant,int Variant),Batch> Batches,DVector3 Origin,long Revision); + private Task? _farTask;private FarSource? _farBack;private IEnumerator? _farUpload; + private Dictionary<(int Plant,int Variant),Batch>? _farSpare;private uint[] _farBuildingBuffers=[]; + private CancellationTokenSource _farCancellation=new();private long _lastFarBuild; + public bool Enabled {get;set;}=true; + public bool DepthReady {get;private set;} + public double Aspect {get;set;}=1.8; + public int Instances {get;private set;} + public long Triangles {get;private set;} + public int DrawCalls {get;private set;} + public int[] LodCounts {get;}=new int[4]; + public double UploadMilliseconds {get;private set;} + public double CpuMilliseconds {get;private set;} + public int ReadyModels=>_requiredPaths.Count(path=>_models.TryGetValue(path,out var model)&&model.Ready); + public int RequiredModels=>_requiredPaths.Count; + public int DistantInstances=>_far.Values.Sum(b=>b.Count); + public VegetationRenderer(GL gl,string root) + { + _gl=gl;_root=root;Catalog=VegetationCatalog.Load(Path.Combine(root,"vegetation.json")); + _surface=Program("vegetation.vert","vegetation.frag");_impostor=Program("vegetation-impostor.vert","vegetation-impostor.frag");_depth=Program("vegetation.vert","vegetation-depth.frag"); + _quad=new(gl,new([- .5f,-.5f,0,0,0,1,0,0,.5f,-.5f,0,0,0,1,1,0,.5f,.5f,0,0,0,1,1,1,-.5f,.5f,0,0,0,1,0,1],[0,1,2,0,2,3],0)); + } + private ShaderProgram Program(string v,string f)=>ShaderProgram.Create(_gl,ShaderSources.Load(v),ShaderSources.Load(f)); + public void ReloadShaders() + { + var surface=Program("vegetation.vert","vegetation.frag");var impostor=Program("vegetation-impostor.vert","vegetation-impostor.frag");var depth=Program("vegetation.vert","vegetation-depth.frag"); + _surface.Dispose();_impostor.Dispose();_depth.Dispose();_surface=surface;_impostor=impostor;_depth=depth; + } + public void Update(FlyCamera camera,StarSystemData system,double time) + { + var watch=Stopwatch.StartNew();_time=time;Instances=0;Triangles=0;DrawCalls=0;DepthReady=false;CpuMilliseconds=0;UploadMilliseconds=0;Array.Clear(LodCounts); + Stream?.ResetFrameDiagnostics(); + if(!Enabled){CpuMilliseconds=0;UploadMilliseconds=0;return;} + var center=UniversePosition.AtCellCenter(system.Coordinate,camera.CellSizeMeters); + var planet=system.Planets.Where(p=>p.Vegetation is {Enabled:true}&&!p.IsGasPlanet) + .MinBy(p=>center.Add(p.PositionMeters,camera.CellSizeMeters).RelativeTo(camera.Position,camera.CellSizeMeters).Length-p.RadiusMeters); + if(planet is null){if(Stream is not null){Stream.Dispose();ResetFar();}Stream=null;PlanetId=null;_planet=null;_requiredPaths.Clear();return;} + if(PlanetId!=planet.Id||_planet?.Seed!=planet.Seed||_systemId!=system.Id) + { + Stream?.Dispose();Stream=new(new(planet,Catalog));PlanetId=planet.Id;_farRevision=-1; + ResetFar();_systemId=system.Id; + _requiredPaths.Clear();foreach(var p in Catalog.Plants)if(planet.Vegetation!.AllowedPlants is null||planet.Vegetation.AllowedPlants.Contains(p.Id,StringComparer.Ordinal))_requiredPaths.Add(p.Model); + } + _planet=planet;_cameraBody=planet.Orientation.ToBody(-center.Add(planet.PositionMeters,camera.CellSizeMeters).RelativeTo(camera.Position,camera.CellSizeMeters)); + _surfaceAltitude=_cameraBody.Length-TerrainHeight.Sample(Stream!.Generator.Field.Planet,_cameraBody.Normalized()); + if(_surfaceAltitude>10000){CpuMilliseconds=watch.Elapsed.TotalMilliseconds;return;} + Stream!.Update(_cameraBody); + var biome=Stream.Generator.Field.Weights(Stream.Generator.Field.Color(_cameraBody.Normalized())); + if(Stream.InstanceCount>0||biome.X+biome.Y>.1) + foreach(var definition in Catalog.Plants) + if(_requiredPaths.Contains(definition.Model)&&!_models.ContainsKey(definition.Model)&&!_models.Values.Any(m=>!m.Task.IsCompleted)) + { _models[definition.Model]=new(){Task=GenerationJobs.Run(()=>Load(definition.Model),CancellationToken.None)};break; } + PumpUploads(planet.Vegetation!.UploadBudgetMilliseconds*.45); + PumpFar(planet.Vegetation.UploadBudgetMilliseconds*.75); + foreach(var model in _models.Values)if(model.Ready)model.ReadyFade=System.Math.Clamp(Stopwatch.GetElapsedTime((long)model.ReadyAt).TotalSeconds/.5,0,1); + var ready=ReadyModels; + var view=new VegetationView(planet.Orientation.ToBody(camera.Forward),planet.Orientation.ToBody(camera.Right), + planet.Orientation.ToBody(camera.Up),System.Math.Tan(camera.FieldOfViewRadians*.5),Aspect); + if(_surfaceAltitude.5||_models.Values.Any(m=>m.Ready&&m.ReadyFade<1))) + { + BuildSpatialBatches();_nearRevision=Stream.Revision;_nearModels=ready;_nearCamera=_cameraBody;_nearView=null; + } + if(_nearView is null||!_nearView.Value.CanReuse(view))BuildViewBatches(view); + CpuMilliseconds=watch.Elapsed.TotalMilliseconds; + } + private ModelSource Load(string relative) + { + var path=Path.GetFullPath(Path.Combine(_root,relative)); + if(!path.StartsWith(Path.GetFullPath(_root)+Path.DirectorySeparatorChar,StringComparison.OrdinalIgnoreCase))throw new InvalidDataException("Vegetation model must stay inside its catalog folder."); + var data=VegetationModelData.Load(path);var directory=Path.GetDirectoryName(path)!;var prefix=Path.GetFileNameWithoutExtension(path)=="dry"?"dry-":""; + var metadata=JsonSerializer.Deserialize(File.ReadAllText(Path.Combine(directory,prefix+"impostors.json")))!; + var colors=Enumerable.Range(0,metadata.Variants.Length).Select(i=>RasterImage.Load(Path.Combine(directory,$"{prefix}impostor-{i}-color.png"))).ToArray(); + var normals=Enumerable.Range(0,metadata.Variants.Length).Select(i=>RasterImage.Load(Path.Combine(directory,$"{prefix}impostor-{i}-normal.png"))).ToArray(); + foreach(var image in colors)PlantMipmaps.Get(image,true);foreach(var image in normals)PlantMipmaps.Get(image,false); + return new(data,metadata,colors,normals); + } + private IEnumerable UploadModel(Model model) + { + var source=model.Task.Result;model.Gpu=new(source.Data); + foreach(var action in model.Gpu.Upload(_gl))yield return action; + model.Color=new PlantTexture[source.Colors.Length];model.Normal=new PlantTexture[source.Normals.Length]; + for(var i=0;i=budget)break; + } + UploadMilliseconds=watch.Elapsed.TotalMilliseconds; + } + private static void Add(Batch batch,VegetationInstance i,DVector3 origin,VegetationDefinition definition,double fade) + { + var p=i.BodyPosition-origin; + var data=batch.Data;var start=data.Count; + System.Runtime.InteropServices.CollectionsMarshal.SetCount(data,start+12); + var span=System.Runtime.InteropServices.CollectionsMarshal.AsSpan(data)[start..]; + span[0]=(float)p.X;span[1]=(float)p.Y;span[2]=(float)p.Z;span[3]=(float)i.Scale; + span[4]=(float)i.Up.X;span[5]=(float)i.Up.Y;span[6]=(float)i.Up.Z;span[7]=(float)i.Yaw; + span[8]=(float)i.Variation;span[9]=(float)definition.WindStrength;span[10]=(float)fade;span[11]=(float)(i.Id%65536)/65536; + } + private Batch Get(Dictionary dictionary,TKey key) where TKey:notnull + {if(!dictionary.TryGetValue(key,out var b)){b=new(){Buffer=_gl.GenBuffer()};dictionary[key]=b;}return b;} + private void UploadBatch(Batch batch) + { + var started=Stopwatch.GetTimestamp(); + _gl.BindBuffer(GLEnum.ArrayBuffer,batch.Buffer); + var span=System.Runtime.InteropServices.CollectionsMarshal.AsSpan(batch.Data); + fixed(float* p=span)_gl.BufferData(GLEnum.ArrayBuffer,(nuint)(span.Length*4),p,GLEnum.StreamDraw); + UploadMilliseconds+=Stopwatch.GetElapsedTime(started).TotalMilliseconds; + } + private void PumpFar(double budget) + { + var watch=Stopwatch.StartNew(); + if(_farTask is {IsCompleted:true} task&&_farBack is null) + { + _farTask=null;_farBuildingBuffers=[]; + if(task.IsFaulted)throw new InvalidOperationException("Distant forest generation failed.",task.Exception); + if(task.IsCompletedSuccessfully){_farBack=task.Result;_farUpload=UploadFar(_farBack).GetEnumerator();} + } + while(_farUpload is not null&&watch.Elapsed.TotalMilliseconds+UploadMillisecondsp.Key,p=>p.Value);_far.Clear(); + foreach(var pair in _farBack!.Batches)_far.Add(pair.Key,pair.Value); + _farOrigin=_farBack.Origin;_farRevision=_farBack.Revision;_farBack=null; + } + } + UploadMilliseconds+=watch.Elapsed.TotalMilliseconds; + var stream=Stream!;var revision=stream.TreeRevision*256+ReadyModels; + var origin=_cameraBody.Normalized()*_planet!.RadiusMeters; + if(_farTask is null&&_farBack is null&&(_farRevision!=revision||(_farOrigin-origin).Length>150) + &&Stopwatch.GetElapsedTime(_lastFarBuild).TotalSeconds>.12) + { + _lastFarBuild=Stopwatch.GetTimestamp(); + var tiles=stream.Tiles.Where(t=>t.Key.Layer==0).ToArray();var cameraBody=_cameraBody; + var range=_planet.Vegetation!.TreeDistanceMeters+200;var token=_farCancellation.Token; + var variants=Catalog.Plants.Select(p=>_models.TryGetValue(p.Model,out var m)&&m.Ready?m.Task.Result.Data.Lods[0].Length:0).ToArray(); + var batches=_farSpare??new Dictionary<(int Plant,int Variant),Batch>();_farSpare=null; + _farBuildingBuffers=batches.Values.Where(b=>b.Buffer!=0).Select(b=>b.Buffer).ToArray(); + _farTask=GenerationJobs.Run(()=> + { + foreach(var batch in batches.Values)batch.Data.Clear(); + foreach(var tile in tiles) + { + token.ThrowIfCancellationRequested(); + foreach(var instance in tile.Instances) + { + var count=variants[instance.PlantIndex];if(count==0||(instance.BodyPosition-cameraBody).Length>range)continue; + var key=(instance.PlantIndex,(int)(instance.Id%(uint)count)); + if(!batches.TryGetValue(key,out var batch)){batch=new();batches.Add(key,batch);} + Add(batch,instance,origin,Catalog.Plants[instance.PlantIndex],1); + } + } + return new FarSource(batches,origin,revision); + },token); + } + } + private IEnumerable UploadFar(FarSource source) + { + foreach(var batch in source.Batches.Values) + { + if(batch.Count==0)continue; + yield return ()=>{unsafe{ + if(batch.Buffer==0)batch.Buffer=_gl.GenBuffer();_gl.BindBuffer(GLEnum.ArrayBuffer,batch.Buffer); + if(batch.Data.Count*4>batch.CapacityBytes){batch.CapacityBytes=(int)System.Numerics.BitOperations.RoundUpToPowerOf2((uint)(batch.Data.Count*4));_gl.BufferData(GLEnum.ArrayBuffer,(nuint)batch.CapacityBytes,null,GLEnum.StaticDraw);} + }}; + for(var offset=0;offset + { + _gl.BindBuffer(GLEnum.ArrayBuffer,batch.Buffer); + var span=System.Runtime.InteropServices.CollectionsMarshal.AsSpan(batch.Data).Slice(start,count); + unsafe{fixed(float* p=span)_gl.BufferSubData(GLEnum.ArrayBuffer,(nint)(start*4),(nuint)(count*4),p);} + }; + } + } + } + private void ResetFar() + { + _farCancellation.Cancel();_farCancellation.Dispose();_farCancellation=new();_farTask=null; + _farUpload?.Dispose();_farUpload=null; + var buffers=_far.Values.Concat(_farBack?.Batches.Values.AsEnumerable()??Enumerable.Empty()).Concat(_farSpare?.Values.AsEnumerable()??Enumerable.Empty()).Select(b=>b.Buffer).Concat(_farBuildingBuffers).Where(b=>b!=0).Distinct(); + foreach(var buffer in buffers){_quad.ForgetInstances(buffer);_gl.DeleteBuffer(buffer);} + _farBack=null;_farSpare=null;_farBuildingBuffers=[];_far.Clear();_farRevision=-1; + _nearRevision=-1; + _nearCandidates.Clear();_nearView=null; + foreach(var b in _near.Values)b.Data.Clear();foreach(var b in _shadowBatches.Values)b.Data.Clear(); + } + private void BuildSpatialBatches() + { + var stream=Stream!;var settings=_planet!.Vegetation!; + _nearCandidates.Clear();foreach(var batch in _shadowBatches.Values)batch.Data.Clear(); + _origin=_cameraBody.Normalized()*_planet.RadiusMeters; + var nearRange=System.Math.Max(settings.ShadowDistancesMeters[1]+60,Catalog.Plants.Where(p=>p.Type==VegetationType.Tree) + .Select(p=>(p.LodMeters??settings.TreeLodMeters)[2]*1.1).DefaultIfEmpty(0).Max()); + foreach(var tile in stream.Tiles) + { + // Tile anchors are on the reference sphere; mountains can be kilometres above it. + var tileDistance=(tile.Origin.Normalized()*_cameraBody.Length-_cameraBody).Length; + if(tileDistance>nearRange+256)continue; + foreach(var instance in tile.Instances) + { + var definition=Catalog.Plants[instance.PlantIndex]; + if(!_models.TryGetValue(definition.Model,out var model)||!model.Ready)continue; + var source=model.Task.Result;var variant=(int)(instance.Id%(uint)source.Data.Lods[0].Length); + var delta=instance.BodyPosition-_cameraBody;var distance=delta.Length; + if(distance>System.Math.Min(stream.Generator.Range(tile.Key.Layer),definition.DistanceMeters??double.PositiveInfinity)+20)continue; + if(distance>nearRange&&definition.Type==VegetationType.Tree)continue; + if(distance>stream.Generator.Range(tile.Key.Layer)+20)continue; + for(var cascade=0;cascade<2;cascade++) + if(distance0)UploadBatch(b); + } + private void BuildViewBatches(VegetationView view) + { + var stream=Stream!;var settings=_planet!.Vegetation!; + foreach(var batch in _near.Values)batch.Data.Clear(); + foreach(var candidate in _nearCandidates) + { + var instance=candidate.Instance;var definition=Catalog.Plants[instance.PlantIndex]; + if(!view.Intersects(instance.BodyPosition-_cameraBody,candidate.Radius))continue; + var distance=candidate.Distance;var variant=candidate.Variant;var readyFade=candidate.Fade; + if(definition.Type==VegetationType.Tree) + { + var thresholds=definition.LodMeters??settings.TreeLodMeters; + for(var lod=0;lod<3;lod++) + { + var begin=lod==0?0:thresholds[lod-1];var end=thresholds[lod]; + if((lod==0||distance>begin*.9-1)&&distance0)UploadBatch(b); + _nearView=view; + } + private static double Smooth(double a,double b,double x){var t=System.Math.Clamp((x-a)/(b-a),0,1);return t*t*(3-2*t);} + public void DrawShadows(FlyCamera camera,StarSystemData system) + { + _shadowLights=0;if(!Enabled||_planet is null||Stream is null||_surfaceAltitude>1000||!_shadowBatches.Values.Any(b=>b.Count>0))return; + var settings=_planet.Vegetation!; + if(_shadowSize!=settings.ShadowResolution) + { + if(_shadowMap!=0){_gl.DeleteTexture(_shadowMap);_gl.DeleteFramebuffer(_shadowFramebuffer);} + _shadowSize=settings.ShadowResolution;_shadowMap=_gl.GenTexture();_shadowFramebuffer=_gl.GenFramebuffer(); + _gl.BindTexture(GLEnum.Texture2DArray,_shadowMap); + _gl.TexImage3D(GLEnum.Texture2DArray,0,InternalFormat.DepthComponent24,(uint)_shadowSize,(uint)_shadowSize,4,0,PixelFormat.DepthComponent,PixelType.Float,null); + _gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureMinFilter,(int)GLEnum.Linear);_gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureMagFilter,(int)GLEnum.Linear); + _gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureCompareMode,(int)GLEnum.CompareRefToTexture); + _gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureCompareFunc,(int)GLEnum.Lequal); + _gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureWrapS,(int)GLEnum.ClampToEdge);_gl.TexParameter(GLEnum.Texture2DArray,GLEnum.TextureWrapT,(int)GLEnum.ClampToEdge); + } + _gl.BindFramebuffer(GLEnum.Framebuffer,_shadowFramebuffer);_gl.DrawBuffer(GLEnum.None);_gl.ReadBuffer(GLEnum.None); + _gl.Enable(GLEnum.DepthTest);_gl.DepthMask(true);_gl.DepthFunc(GLEnum.Lequal);_gl.Disable(GLEnum.Blend);_gl.Disable(GLEnum.CullFace); + _gl.Viewport(0,0,(uint)_shadowSize,(uint)_shadowSize); + _depth.Use();_depth.Set("uTime",(float)_time);_depth.Set("uShadowPass",1);_depth.Set("uAnchor",_planet.Orientation.ToWorld(_origin-_cameraBody).ToVector3());_depth.SetMatrix("uBodyToWorld",_planet.Orientation.BodyToWorld); + for(var light=0;light0)_models[Catalog.Plants[key.Plant].Model].Gpu!.Draw(_gl,_depth,2,key.Variant,batch.Buffer,batch.Count,true); + } + _shadowLights=system.Stars.Count; + } + public void BindShadows(ShaderProgram program,string? planetId,int textureUnit=15) + { + program.Set("uVegetationShadowMap",textureUnit);program.Set("uVegetationShadows",Enabled&&planetId==PlanetId&&_shadowLights>0?1:0); + _gl.ActiveTexture((GLEnum)((int)GLEnum.Texture0+textureUnit));_gl.BindTexture(GLEnum.Texture2DArray,_shadowMap); + var distances=_planet?.Vegetation?.ShadowDistancesMeters??[100d,400d]; + program.Set("uVegetationShadowDistances",new Vector3((float)distances[0],(float)distances[1],0)); + var settings=_planet?.Vegetation??VegetationSettings.Default; + program.Set("uVegetationShadowSettings",new Vector3((float)settings.ShadowSoftness,(float)settings.ShadowCascadeBlend,(float)settings.ShadowBiasMeters)); + for(var i=0;i<4;i++)program.SetMatrix($"uVegetationShadowMatrices[{i}]",_shadowMatrices[i]); + } + private void Lighting(ShaderProgram program,StarSystemData system) + { + program.Set("uPlanetUp",_planet!.Orientation.ToWorld(_cameraBody.Normalized()).ToVector3()); + program.Set("uLightCount",system.Stars.Count); + program.SetVectorArray("uLightDirections",system.Stars.Select(s=>(s.PositionMeters-_planet!.PositionMeters).Normalized().ToVector3()).ToArray()); + program.SetVectorArray("uLightColors",system.Stars.Select(s=>s.Color).ToArray()); + program.SetFloatArray("uLightFlux",system.Stars.Select(s=>(float)System.Math.Clamp(s.LuminositySolar/System.Math.Pow((s.PositionMeters-_planet!.PositionMeters).Length/UniverseScale.AstronomicalUnitMeters,2),.02,7)).ToArray()); + BindShadows(program,PlanetId); + } + public void PrepareDepth(FlyCamera camera,StarSystemData system,Matrix4x4 vp,float far,int width,int height) + { + if(!Enabled||_planet is null||Stream is null||_surfaceAltitude>_planet.Vegetation!.TreeDistanceMeters*1.2)return; + if(!_planet.Vegetation.DepthPrepass)return; + _gl.ColorMask(false,false,false,false);DrawPass(camera,system,vp,far,true);_gl.ColorMask(true,true,true,true); + CaptureDepth(width,height); + } + private void CaptureDepth(int width,int height) + { + _gl.ActiveTexture(GLEnum.Texture3); + if(_nearestDepth==0)_nearestDepth=_gl.GenTexture();_gl.BindTexture(GLEnum.Texture2D,_nearestDepth); + if(width!=_depthWidth||height!=_depthHeight) + { + _depthWidth=width;_depthHeight=height; + _gl.TexImage2D(GLEnum.Texture2D,0,InternalFormat.DepthComponent32f,(uint)width,(uint)height,0,PixelFormat.DepthComponent,PixelType.Float,null); + _gl.TexParameter(GLEnum.Texture2D,GLEnum.TextureMinFilter,(int)GLEnum.Nearest);_gl.TexParameter(GLEnum.Texture2D,GLEnum.TextureMagFilter,(int)GLEnum.Nearest); + } + _gl.CopyTexSubImage2D(GLEnum.Texture2D,0,0,0,0,0,(uint)width,(uint)height); + DepthReady=true; + } + public void BindDepthCulling(ShaderProgram program,bool enabled) + { + program.Set("uVegetationDepthCullEnabled",DepthReady&&enabled?1:0);program.Set("uVegetationDepth",3); + _gl.ActiveTexture(GLEnum.Texture3);_gl.BindTexture(GLEnum.Texture2D,_nearestDepth); + } + public void Draw(FlyCamera camera,StarSystemData system,Matrix4x4 vp,float far,int width,int height) + { + if(!Enabled||_planet is null||Stream is null||_surfaceAltitude>_planet.Vegetation!.TreeDistanceMeters*1.2)return; + if(_planet.Vegetation.DepthPrepass&&!DepthReady)PrepareDepth(camera,system,vp,far,width,height); + DrawPass(camera,system,vp,far,false); + // This pass runs before terrain: reuse its exact alpha/log-depth coverage to reject hidden ground. + CaptureDepth(width,height); + } + private void DrawPass(FlyCamera camera,StarSystemData system,Matrix4x4 vp,float far,bool depthOnly) + { + if(!Enabled||_planet is null||Stream is null||_surfaceAltitude>_planet.Vegetation!.TreeDistanceMeters*1.2)return; + _gl.Enable(GLEnum.DepthTest);_gl.DepthMask(true);_gl.Disable(GLEnum.Blend);_gl.Disable(GLEnum.CullFace); + _surface.Use();_surface.SetMatrix("uViewProjection",vp);_surface.SetMatrix("uBodyToWorld",_planet.Orientation.BodyToWorld); + _surface.Set("uDepthOnly",depthOnly?1:_planet.Vegetation!.DepthPrepass?0:-1);_surface.Set("uNearestDepth",3); + _surface.Set("uAnchor",_planet.Orientation.ToWorld(_origin-_cameraBody).ToVector3());_surface.Set("uFar",far);_surface.Set("uTime",(float)_time);_surface.Set("uShadowPass",0);_surface.Set("uBake",0);Lighting(_surface,system); + foreach(var (key,batch) in _near) + { + if(batch.Count==0)continue;var definition=Catalog.Plants[key.Plant]; + _surface.Set("uDitherInvert",key.Lod%2); + var range=System.Math.Min(Stream!.Generator.Range(VegetationTileGenerator.Layer(definition.Type)),definition.DistanceMeters??double.PositiveInfinity); + var thresholds=definition.LodMeters??_planet.Vegetation!.TreeLodMeters; + _surface.Set("uLodNearFar",definition.Type==VegetationType.Tree?new Vector3(key.Lod==0?0:(float)thresholds[key.Lod-1],(float)thresholds[key.Lod],(float)range):new Vector3(0,(float)(range*4),(float)-range)); + var triangles=_models[definition.Model].Gpu!.Draw(_gl,_surface,key.Lod,key.Variant,batch.Buffer,batch.Count,shadow:depthOnly); + if(!depthOnly){Triangles+=triangles;Instances+=batch.Count;LodCounts[key.Lod]+=batch.Count;}DrawCalls+=_models[definition.Model].Gpu!.Data.Lods[key.Lod][key.Variant].Primitives.Length; + } + if(depthOnly)return; // Nearby canopy is enough for terrain occlusion; distant impostors are drawn once. + _impostor.Use();_impostor.SetMatrix("uViewProjection",vp);_impostor.SetMatrix("uBodyToWorld",_planet.Orientation.BodyToWorld); + _impostor.Set("uDepthOnly",depthOnly?1:_planet.Vegetation!.DepthPrepass?0:-1);_impostor.Set("uNearestDepth",3); + _impostor.Set("uAnchor",_planet.Orientation.ToWorld(_farOrigin-_cameraBody).ToVector3());_impostor.Set("uBatchBodyOrigin",_farOrigin.ToVector3());_impostor.Set("uCameraBody",_cameraBody.ToVector3()); + _impostor.Set("uCameraOffset",(_cameraBody-_farOrigin).ToVector3()); + _impostor.Set("uFar",far);_impostor.Set("uTime",(float)_time);Lighting(_impostor,system); + var lod=_planet.Vegetation.TreeLodMeters[2];_impostor.Set("uImpostorRange",new Vector3((float)(lod*.9),(float)(lod*1.1),(float)_planet.Vegetation.TreeDistanceMeters)); + foreach(var (key,batch) in _far) + { + if(batch.Count==0)continue;var model=_models[Catalog.Plants[key.Plant].Model];if(!model.Ready)continue; + var definition=Catalog.Plants[key.Plant];var threshold=(definition.LodMeters??_planet.Vegetation.TreeLodMeters)[2]; + _impostor.Set("uImpostorRange",new Vector3((float)(threshold*.9),(float)(threshold*1.1),(float)System.Math.Min(_planet.Vegetation.TreeDistanceMeters,definition.DistanceMeters??double.PositiveInfinity))); + var bounds=model.Task.Result.Metadata.Variants[key.Variant];_impostor.Set("uModelCenter",new Vector3(bounds.Center[0],bounds.Center[1],bounds.Center[2]));_impostor.Set("uBillboardSize",bounds.Size); + _gl.ActiveTexture(GLEnum.Texture0);_gl.BindTexture(GLEnum.Texture2D,model.Color[key.Variant].Handle);_impostor.Set("uColor",0); + _gl.ActiveTexture(GLEnum.Texture1);_gl.BindTexture(GLEnum.Texture2D,model.Normal[key.Variant].Handle);_impostor.Set("uNormal",1); + _quad.Draw(batch.Buffer,batch.Count);if(!depthOnly){Instances+=batch.Count;LodCounts[3]+=batch.Count;Triangles+=batch.Count*2;}DrawCalls++; + } + } + public TrunkCapsule[] Trunks(string planetId,DVector3 bodyPosition) + { + if(!Enabled||Stream is null||PlanetId!=planetId)return []; + var range=_planet!.Vegetation!.CollisionDistanceMeters; + return Stream.Tiles.Where(t=>t.Key.Layer==0&&(t.Origin.Normalized()*bodyPosition.Length-bodyPosition).Lengtht.Instances) + .Where(i=>(i.BodyPosition-bodyPosition).Length(i,definition:Catalog.Plants[i.PlantIndex])).Where(x=>x.definition.TrunkCollision is not null) + .Select(x=>new TrunkCapsule(x.i.BodyPosition,x.i.Up,x.definition.TrunkCollision!.RadiusMeters*x.i.Scale,x.definition.TrunkCollision.HeightMeters*x.i.Scale)).ToArray(); + } + public void ClearTiles(){Stream?.Clear();ResetFar();} + public void Dispose() + { + ResetFar();_farCancellation.Cancel();_farCancellation.Dispose();Stream?.Dispose();_surface.Dispose();_impostor.Dispose();_depth.Dispose();_quad.Dispose(); + foreach(var m in _models.Values){m.Upload?.Dispose();m.Gpu?.Dispose();foreach(var t in m.Color)t?.Dispose();foreach(var t in m.Normal)t?.Dispose();} + foreach(var b in _near.Values)_gl.DeleteBuffer(b.Buffer);foreach(var b in _far.Values)_gl.DeleteBuffer(b.Buffer);foreach(var b in _shadowBatches.Values)_gl.DeleteBuffer(b.Buffer); + if(_shadowMap!=0){_gl.DeleteTexture(_shadowMap);_gl.DeleteFramebuffer(_shadowFramebuffer);} + if(_nearestDepth!=0)_gl.DeleteTexture(_nearestDepth); + } +} diff --git a/src/Universe.App/Runtime/ForestBenchmark.cs b/src/Universe.App/Runtime/ForestBenchmark.cs new file mode 100644 index 0000000..a615076 --- /dev/null +++ b/src/Universe.App/Runtime/ForestBenchmark.cs @@ -0,0 +1,113 @@ +using System.Globalization; +using System.Text.Json; +using Universe.Core.Generation; +using Universe.Core.Math; +using Universe.Core.Physics; +using Universe.Core.World; + +namespace Universe.App.Runtime; + +internal sealed class ForestBenchmark : IBenchmarkSession +{ + private readonly PlanetData _planet;private readonly UniversePosition _body,_home;private readonly DVector3 _homeForward,_homeUp; + private readonly double _cell;private readonly DVector3 _radial,_tangent,_across; + private readonly List _frames=[];private readonly List _graph=[]; + private double _elapsed,_rendered,_graphTime;private int _graphFrames;private bool _stopped; + private double _cpu,_upload,_vegetationGpu,_shadowGpu,_generation;private int _instances,_tiles,_pending,_models;private long _triangles;private int[] _lod=[0,0,0,0]; + private readonly Func? _trunks; + private sealed record Frame(double Time,double Milliseconds,string Phase,double CpuMs,double UploadMs,double VegetationMs,double ShadowMs,int Instances,long Triangles,int Tiles,int Pending,int Models,double GenerationMs,int[] Lod); + public const double Duration=96; + public DVector3 LandingBodyDirection {get;} + public ForestBenchmark(StarSystemData system,PlanetData planet,VegetationCatalog catalog,FlyCamera camera, + UniversePosition home,DVector3 homeForward,DVector3 homeUp,Func? trunks=null) + { + _planet=planet;_cell=camera.CellSizeMeters;_home=home;_homeForward=homeForward;_homeUp=homeUp;_trunks=trunks; + _body=UniversePosition.AtCellCenter(system.Coordinate,_cell).Add(planet.PositionMeters,_cell); + var field=new BiomeField(planet,catalog); + LandingBodyDirection=VegetationSiteFinder.FindForest(field,planet.Orientation.ToBody((system.Stars[planet.HostStarIndex].PositionMeters-planet.PositionMeters).Normalized())); + _radial=planet.Orientation.ToWorld(LandingBodyDirection); + _tangent=DVector3.Cross(planet.Orientation.ToWorld(new(0,1,0)),_radial).Normalized();_across=DVector3.Cross(_radial,_tangent).Normalized(); + camera.SetPlayerPose(_body.Add(_radial*(planet.RadiusMeters*4.8),_cell),-_radial,_across,true); + } + public bool IsRunning=>!_stopped&&_elapsed!_stopped&&_elapsed>=Duration; + public bool HasRun=>true; + public double ElapsedSeconds=>_elapsed; + public double DurationSeconds=>Duration; + public long GraphRevision {get;private set;} + public IReadOnlyList GraphSamples=>_graph; + public string TargetLabel=>"EARTH FOREST"; + public double TargetEarthRadii=>_planet.RadiusMeters/UniverseScale.EarthRadiusMeters; + public double CurrentFps {get;private set;} + public double AverageFps=>_rendered>0?_frames.Count/_rendered:0; + public double MinimumFps=>_frames.Count>0?1000/_frames.Max(f=>f.Milliseconds):0; + public double OnePercentLowFps=>_frames.Count>0?1000/_frames.OrderByDescending(f=>f.Milliseconds).Take(System.Math.Max(1,(int)System.Math.Ceiling(_frames.Count*.01))).Average(f=>f.Milliseconds):0; + public double MaximumFrameMilliseconds=>_frames.Count>0?_frames.Max(f=>f.Milliseconds):0; + public double VegetationGpuP99Milliseconds + {get{if(_frames.Count==0)return 0;var values=_frames.Select(f=>f.VegetationMs+f.ShadowMs).Order().ToArray();return values[(int)((values.Length-1)*.99)];}} + public bool PerformancePassed=>AverageFps>=60-1e-9&&OnePercentLowFps>=55-1e-9&&MaximumFrameMilliseconds<=50+1e-9&&VegetationGpuP99Milliseconds<=5+1e-9; + public string Phase=>_stopped?"STOPPED":IsComplete?"COMPLETE":_elapsed<8?"SPACE VIEW":_elapsed<40?"FOREST DESCENT":_elapsed<64?"DENSE FOREST":_elapsed<80?"FOREST EDGE":"ASCENT"; + public string? ReportDirectory {get;private set;} + public void Stop()=>_stopped=true; + private static double Smooth(double t){t=System.Math.Clamp(t,0,1);return t*t*t*(10+t*(-15+6*t));} + public void Update(double seconds,FlyCamera camera) + { + if(!IsRunning||!double.IsFinite(seconds)||seconds<=0)return; + _elapsed=System.Math.Min(Duration,_elapsed+System.Math.Min(seconds,.1)); + var radial=_radial;var ground=TerrainHeight.Sample(_planet,radial)+1.8;double height; + var forward=-radial;var up=_across; + if(_elapsed<8)height=_planet.RadiusMeters*4.8; + else if(_elapsed<40) + { + var t=(_elapsed-8)/32; + height=t<.5?_planet.RadiusMeters*4.8+(ground+10000-_planet.RadiusMeters*4.8)*Smooth(t*2):ground+10000*(1-Smooth((t-.5)*2)); + var look=Smooth((t-.25)/.75);forward=(-radial*(1-look)+(_tangent-radial*.12)*look).Normalized();up=radial; + } + else + { + var walk=System.Math.Min(40,_elapsed-40);var distance=walk*4.5*Smooth(walk/2); + var direction=(_tangent+_across*.15*System.Math.Sin(walk*.09)).Normalized(); + radial=(_radial*System.Math.Cos(distance/_planet.RadiusMeters)+direction*System.Math.Sin(distance/_planet.RadiusMeters)).Normalized(); + height=TerrainHeight.Sample(_planet,radial)+1.8; + var head=.55*System.Math.Sin(walk*.18);forward=(_tangent*System.Math.Cos(head)+_across*System.Math.Sin(head)-radial*.08).Normalized();up=radial; + if(_elapsed>=80) + { + var t=Smooth((_elapsed-80)/16);height+=15000*t; + forward=(forward*(1-t)-radial*t).Normalized(); + } + } + var position=_body.Add(radial*height,_cell); + if(_elapsed>=40&&_elapsed<80&&_trunks is not null) + { + var start=_planet.Orientation.ToBody(camera.Position.RelativeTo(_body,_cell));var end=_planet.Orientation.ToBody(radial*height); + var resolved=VegetationCollision.Move(start,end,_planet.Orientation.ToBody(radial),1.8,_trunks(_planet.Id,start)); + var world=_planet.Orientation.ToWorld(resolved);var r=world.Normalized(); + position=_body.Add(r*(TerrainHeight.Sample(_planet,r)+1.8),_cell); + } + if(_elapsed>=Duration){camera.SetPlayerPose(_home,_homeForward,_homeUp,true);return;} + up=(up-forward*DVector3.Dot(up,forward)).Normalized();if(up==DVector3.Zero)up=_across; + camera.SetPlayerPose(position,forward,up); + } + public void Metrics(double cpu,double upload,double vegetationGpu,double shadows,int instances,long triangles,int tiles,int pending,int models,int[] lod,double generation=0) + {_cpu=cpu;_upload=upload;_vegetationGpu=vegetationGpu;_shadowGpu=shadows;_instances=instances;_triangles=triangles;_tiles=tiles;_pending=pending;_models=models;_generation=generation;_lod=(int[])lod.Clone();} + public void RecordFrame(double seconds) + { + if(_stopped||(_frames.Count>0&&_frames[^1].Time>=Duration)||!double.IsFinite(seconds)||seconds<=0)return; + CurrentFps=1/seconds;_rendered+=seconds;_graphTime+=seconds;_graphFrames++; + _frames.Add(new(_elapsed,seconds*1000,Phase,_cpu,_upload,_vegetationGpu,_shadowGpu,_instances,_triangles,_tiles,_pending,_models,_generation,_lod)); + if(_graphTime>=.2){_graph.Add(_graphFrames/_graphTime);_graphFrames=0;_graphTime=0;GraphRevision++;} + } + public void SaveReport(string? directory=null) + { + if(ReportDirectory is not null)return; + ReportDirectory=directory??Path.Combine(AppContext.BaseDirectory,"benchmark-results","forest-"+DateTime.UtcNow.ToString("yyyyMMdd-HHmmss"));Directory.CreateDirectory(ReportDirectory); + using(var csv=new StreamWriter(Path.Combine(ReportDirectory,"frames.csv"))) + { + csv.WriteLine("time_seconds,frame_ms,phase,cpu_ms,upload_ms,vegetation_gpu_ms,shadow_gpu_ms,instances,triangles,tiles,pending,models,generation_worker_ms,lod0,lod1,lod2,impostors"); + foreach(var f in _frames)csv.WriteLine(string.Join(",",new object[]{f.Time,f.Milliseconds,f.Phase,f.CpuMs,f.UploadMs,f.VegetationMs,f.ShadowMs,f.Instances,f.Triangles,f.Tiles,f.Pending,f.Models,f.GenerationMs}.Concat(f.Lod.Cast()).Select(v=>v is IFormattable n?n.ToString(null,CultureInfo.InvariantCulture):v.ToString()))); + } + File.WriteAllText(Path.Combine(ReportDirectory,"summary.json"),JsonSerializer.Serialize(new{AverageFps,OnePercentLowFps,MinimumFps,MaximumFrameMilliseconds,VegetationGpuP99Milliseconds, + Passed=PerformancePassed,Complete=IsComplete,Landing=new[]{LandingBodyDirection.X,LandingBodyDirection.Y,LandingBodyDirection.Z}, + Phases=_frames.GroupBy(f=>f.Phase).Select(g=>new{Phase=g.Key,AverageFps=1000/g.Average(f=>f.Milliseconds),MaxMs=g.Max(f=>f.Milliseconds)})},new JsonSerializerOptions{WriteIndented=true})); + } +} diff --git a/src/Universe.App/Runtime/PlayerController.cs b/src/Universe.App/Runtime/PlayerController.cs index 9ca7859..7f88de2 100644 --- a/src/Universe.App/Runtime/PlayerController.cs +++ b/src/Universe.App/Runtime/PlayerController.cs @@ -20,6 +20,7 @@ public sealed class PlayerController public DVector3 Forward => (_heading * System.Math.Cos(_pitch) + Up * System.Math.Sin(_pitch)).Normalized(); public double EyeHeight => Crouching ? CrouchHeight : StandHeight; public double Speed => _velocity.Length; + public Func? TrunksProvider {get;set;} public PlayerController(UniversePosition position, DVector3 forward, DVector3 fallbackUp, double cellSize, UniverseNeighborhood neighborhood) { @@ -90,6 +91,7 @@ public sealed class PlayerController if (Grounded) { var proposed = relative + horizontal * dt; + proposed=MoveThroughTrees(t.Planet,relative,proposed,radial); var nextRadial = proposed.Normalized(); var nextSurface = TerrainHeight.Sample(t.Planet, nextRadial); var rise = nextSurface - surface; @@ -115,6 +117,8 @@ public sealed class PlayerController else vertical = System.Math.Max(vertical - gravity * dt, -180); _velocity = horizontal + radial * vertical; var destination = Position.Add(_velocity * dt, _cellSize); + var treeMove=MoveThroughTrees(t.Planet,relative,destination.RelativeTo(t.Body,_cellSize),radial); + destination=t.Body.Add(treeMove,_cellSize); var collision = PlanetCollisionSolver.Sweep(Position, destination, UniversePosition.AtCellCenter(t.System.Coordinate, _cellSize), [t.Planet], _cellSize, eye, _velocity.Length * dt < 100 ? 16 : 128); @@ -135,6 +139,13 @@ public sealed class PlayerController _heading = heading.LengthSquared > .0001 ? heading.Normalized() : Tangent(up); Up = up; } + private DVector3 MoveThroughTrees(PlanetData planet,DVector3 start,DVector3 end,DVector3 up) + { + if(TrunksProvider is null)return end; + var bodyStart=planet.Orientation.ToBody(start);var trunks=TrunksProvider(planet.Id,bodyStart); + if(trunks.Length==0)return end; + return planet.Orientation.ToWorld(VegetationCollision.Move(bodyStart,planet.Orientation.ToBody(end),planet.Orientation.ToBody(up),EyeHeight,trunks)); + } private static DVector3 Tangent(DVector3 up) => DVector3.Cross(System.Math.Abs(up.Y) < .9 ? new(0, 1, 0) : new DVector3(1, 0, 0), up).Normalized(); private (StarSystemData System, PlanetData Planet, UniversePosition Body)? Nearest(UniverseNeighborhood neighborhood) { diff --git a/src/Universe.App/Universe.App.csproj b/src/Universe.App/Universe.App.csproj index 7c86055..c343fa2 100644 --- a/src/Universe.App/Universe.App.csproj +++ b/src/Universe.App/Universe.App.csproj @@ -6,7 +6,8 @@ - + + @@ -18,6 +19,7 @@ + diff --git a/src/Universe.App/UniverseApplication.cs b/src/Universe.App/UniverseApplication.cs index e872c14..35daba3 100644 --- a/src/Universe.App/UniverseApplication.cs +++ b/src/Universe.App/UniverseApplication.cs @@ -32,6 +32,7 @@ public sealed class UniverseApplication : IDisposable private PlayerController? _player; private IBenchmarkSession? _benchmark; private bool _benchmarkNeedsInitialFrame; + private bool? _benchmarkVsync; private bool _menuOpen; private bool _debugMode; private int _menuSelection; @@ -83,8 +84,9 @@ public sealed class UniverseApplication : IDisposable var options = WindowOptions.Default with { Title = "Universe | loading", - IsVisible = !Environment.GetCommandLineArgs().Any(a => a is "--simulation-report" or "--graphics-report" or "--scene-report" or "--ocean-report" or "--cloud-report" or "--terrain-report" or "--menu-report" or "--terrain-perf-report" or "--black-hole-report" or "--galaxy-journey-report" or "--star-trails-report"), - Size = new Vector2D(1440, 900), + IsVisible = !Environment.GetCommandLineArgs().Any(a => a is "--shadow-report" or "--lookaround-report" or "--prepare-vegetation" or "--vegetation-report" or "--simulation-report" or "--graphics-report" or "--scene-report" or "--ocean-report" or "--cloud-report" or "--terrain-report" or "--menu-report" or "--terrain-perf-report" or "--black-hole-report" or "--galaxy-journey-report" or "--star-trails-report"), + Size = Environment.GetCommandLineArgs().Contains("--vegetation-report")?new Vector2D(2560,1440):new Vector2D(1440, 900), + WindowBorder = Environment.GetCommandLineArgs().Contains("--vegetation-report")?WindowBorder.Hidden:WindowBorder.Resizable, VSync = true, PreferredDepthBufferBits = 24, API = new GraphicsAPI(ContextAPI.OpenGL, new APIVersion(3, 3)) @@ -102,8 +104,14 @@ public sealed class UniverseApplication : IDisposable private void OnLoad() { var gl = _window.CreateOpenGL(); + if(Environment.GetCommandLineArgs().Contains("--prepare-vegetation")) + {VegetationPreparation.Run(gl,Path.Combine(AppContext.BaseDirectory,"../../../../..","Content","Vegitations"));_window.Close();return;} _input = _window.CreateInput(); _renderer = new(gl, _window.FramebufferSize.X, _window.FramebufferSize.Y, _galaxies, _starMotionBlur); + if(Environment.GetCommandLineArgs().Contains("--lookaround-report")) + {GraphicsReport.RunLookaround(gl,_renderer,_camera,_neighborhood,_window.FramebufferSize.X,_window.FramebufferSize.Y);_window.Close();return;} + if(Environment.GetCommandLineArgs().Contains("--vegetation-report")) + {GraphicsReport.RunVegetation(gl,_renderer,_camera,_neighborhood,_window.FramebufferSize.X,_window.FramebufferSize.Y);_window.Close();return;} if (Environment.GetCommandLineArgs().Contains("--simulation-report")) { GraphicsReport.RunSimulation(gl, _renderer, _neighborhood, _generator.CellSizeMeters, _window.FramebufferSize.X, _window.FramebufferSize.Y); @@ -132,6 +140,8 @@ public sealed class UniverseApplication : IDisposable { GraphicsReport.RunTerrainPerformance(gl, _renderer, _neighborhood, _generator.CellSizeMeters, _window.FramebufferSize.X, _window.FramebufferSize.Y); _window.Close(); return; } if (Environment.GetCommandLineArgs().Contains("--menu-report")) { GraphicsReport.RunMenu(gl, _renderer, _camera, _neighborhood, _window.FramebufferSize.X, _window.FramebufferSize.Y); _window.Close(); return; } + if (Environment.GetCommandLineArgs().Contains("--shadow-report")) + { GraphicsReport.RunShadows(gl); _window.Close(); return; } if (Environment.GetCommandLineArgs().Contains("--terrain-report")) { GraphicsReport.RunTerrain(gl, _renderer, _neighborhood, _generator.CellSizeMeters, _window.FramebufferSize.X, _window.FramebufferSize.Y); _window.Close(); return; } if (Environment.GetCommandLineArgs().Contains("--cloud-report")) @@ -232,13 +242,19 @@ public sealed class UniverseApplication : IDisposable // Render one initial frame before starting the measurement clock. if (!_benchmarkNeedsInitialFrame) _benchmark?.RecordFrame(deltaSeconds); if (_benchmark is GalaxyJourneyBenchmark { IsComplete: true, ReportDirectory: null } journey) journey.SaveReport(); + if(_benchmark is ForestBenchmark {IsRunning:false,ReportDirectory:null} forestFinished)forestFinished.SaveReport(); + if(_benchmark?.IsRunning!=true&&_benchmarkVsync is {} vsync){_window.VSync=vsync;_benchmarkVsync=null;} if (_renderer is not null) { _renderer.DebugMode = _debugMode; - _renderer.SimulationTimeSeconds = _clock.ElapsedSeconds; + _renderer.SimulationTimeSeconds = _clock.ElapsedSeconds+(_benchmark is ForestBenchmark {IsRunning:true} activeForest?activeForest.ElapsedSeconds*_clock.TimeScale:0); _renderer.DebugProbeEnabled = _debugMode && !_menuOpen; + _renderer.BenchmarkProfiling=_benchmark is ForestBenchmark {IsRunning:true}; } _renderer?.Render(_camera, _neighborhood, _menuOpen ? 0 : deltaSeconds); + if(_benchmark is ForestBenchmark forest&&_renderer is {} renderer) + forest.Metrics(renderer.Vegetation.CpuMilliseconds,renderer.Vegetation.UploadMilliseconds,renderer.VegetationGpuMilliseconds,renderer.VegetationShadowMilliseconds, + renderer.Vegetation.Instances,renderer.Vegetation.Triangles,renderer.Vegetation.Stream?.Tiles.Count()??0,renderer.Vegetation.Stream?.PendingTiles??0,renderer.Vegetation.ReadyModels,renderer.Vegetation.LodCounts,renderer.Vegetation.Stream?.GenerationMilliseconds??0); _renderer?.DrawDebugOverlay(1 / _frameTime, _clock.TimeScale, _clock.Utc); if (_benchmark is { HasRun: true } benchmark) _renderer?.DrawBenchmarkOverlay(benchmark); if (_menuOpen) _renderer?.DrawMenu(_menuSelection, _debugMode, _clock.TimeScale, _clock.Utc); @@ -274,8 +290,8 @@ public sealed class UniverseApplication : IDisposable if (key == Key.Escape) SetMenu(false); else if (_debugMode && key is Key.Left or Key.Right) _clock.TimeScale += (key == Key.Right ? 1 : -1) * (IsDown(Key.ShiftLeft) || IsDown(Key.ShiftRight) ? 10 : 1); - else if (key == Key.Up) _menuSelection = (_menuSelection + 6) % 7; - else if (key == Key.Down) _menuSelection = (_menuSelection + 1) % 7; + else if (key == Key.Up) _menuSelection = (_menuSelection + PauseMenu.Count-1) % PauseMenu.Count; + else if (key == Key.Down) _menuSelection = (_menuSelection + 1) % PauseMenu.Count; else if (key is Key.Enter or Key.KeypadEnter) ActivateMenu(); return; } @@ -410,7 +426,15 @@ public sealed class UniverseApplication : IDisposable private void ActivateMenu() { - if (_menuSelection == 6) { _window.Close(); return; } + if (_menuSelection == 7) { _window.Close(); return; } + if(_menuSelection==6) + { + var home=CurrentHomeView(); + try{_benchmark=new ForestBenchmark(home.System,home.Earth,_renderer!.Vegetation.Catalog,_camera,home.Position,home.Forward,home.Up,_renderer.Vegetation.Trunks);} + catch(InvalidOperationException error){_window.Title="Universe | Forest benchmark: "+error.Message;return;} + _player=null;_debugMode=false;_benchmarkVsync=_window.VSync;_window.VSync=false; + _benchmarkNeedsInitialFrame=true;UpdateNeighborhood();SetMenu(false);return; + } if (_menuSelection == 2) { _debugMode = !_debugMode; return; } if (_menuSelection == 5) { @@ -436,6 +460,7 @@ public sealed class UniverseApplication : IDisposable if (_menuSelection == 0) { _player ??= new PlayerController(_camera.Position, _camera.Forward, _camera.Up, _generator.CellSizeMeters, _neighborhood); + _player.TrunksProvider=(id,position)=>_renderer?.Vegetation.Trunks(id,position)??[]; _camera.SetPlayerPose(_player.Position, _player.Forward, _player.Up); } else _player = null; diff --git a/src/Universe.Core/Configuration/KnownSolarSystemCatalog.cs b/src/Universe.Core/Configuration/KnownSolarSystemCatalog.cs index 11a47be..7e192cf 100644 --- a/src/Universe.Core/Configuration/KnownSolarSystemCatalog.cs +++ b/src/Universe.Core/Configuration/KnownSolarSystemCatalog.cs @@ -128,6 +128,10 @@ public sealed class KnownSolarSystemCatalog var orbit = BuildOrbit(p.Orbit, (parent?.MassKg ?? stars[host].MassKg) + p.MassKg); var position = (parent?.PositionMeters ?? stars[host].PositionMeters) + orbit.Position(0); var surfaceTexture = p.SurfaceTexture is null ? null : LoadColor(p.SurfaceTexture); + var vegetation=p.Vegetation??(p.SurfaceType==PlanetSurfaceType.Terrestrial&&air.HeightMeters>0?new VegetationSettings():null); + vegetation?.Validate(); + if(vegetation is {Enabled:true}&&p.SurfaceType is PlanetSurfaceType.GasGiant or PlanetSurfaceType.IceGiant) + throw new InvalidDataException($"'{p.Id}': vegetation requires solid ground."); var planetSeed = p.Seed ?? StableSeed(systemId + "/" + p.Id); var clouds = p.Clouds ?? CloudSettings.ForAtmosphere(planetSeed, air.HeightMeters, air.Density, p.SurfaceType); if (clouds is not null) VisualSettingsValidator.Clouds(clouds, p.Id, air.HeightMeters, air.Density, p.SurfaceType); @@ -158,6 +162,8 @@ public sealed class KnownSolarSystemCatalog StormStrength = p.StormStrength, SurfaceTexture = surfaceTexture, Materials = materials, + Vegetation = vegetation, + VegetationBiomeTexture = vegetation?.BiomeTexture is {} biome?LoadColor(biome):null, Ocean = p.Ocean, Clouds = clouds, CloudDensityMap = cloudDensityMap, diff --git a/src/Universe.Core/Configuration/SolarSystemDefinitions.cs b/src/Universe.Core/Configuration/SolarSystemDefinitions.cs index 184cf15..e26e84d 100644 --- a/src/Universe.Core/Configuration/SolarSystemDefinitions.cs +++ b/src/Universe.Core/Configuration/SolarSystemDefinitions.cs @@ -121,6 +121,7 @@ public sealed record PlanetDefinition public float[] SurfaceColor { get; init; } = [0.38f, 0.30f, 0.22f]; public string? SurfaceTexture { get; init; } public TerrainMaterialSettings Materials { get; init; } = new(); + public VegetationSettings? Vegetation { get; init; } public OceanSettings? Ocean { get; init; } public CloudSettings? Clouds { get; init; } public TerrainDefinition Terrain { get; init; } = new(); diff --git a/src/Universe.Core/Configuration/VisualSettingsValidator.cs b/src/Universe.Core/Configuration/VisualSettingsValidator.cs index 35633d9..20ae1ea 100644 --- a/src/Universe.Core/Configuration/VisualSettingsValidator.cs +++ b/src/Universe.Core/Configuration/VisualSettingsValidator.cs @@ -46,7 +46,11 @@ internal static class VisualSettingsValidator Range(o.WaveAmplitudeMeters, 0, 30, id, "ocean.waveAmplitudeMeters"); Range(o.WaveLengthMeters, 10, 2000, id, "ocean.waveLengthMeters"); Range(o.WindDirectionDegrees, -360, 360, id, "ocean.windDirectionDegrees"); + Range(o.WindSpreadDegrees, 0, 180, id, "ocean.windSpreadDegrees"); Range(o.WaveSpeed, 0, 10, id, "ocean.waveSpeed"); Range(o.Choppiness, 0, .9, id, "ocean.choppiness"); + Range(o.RippleStrength, 0, 3, id, "ocean.rippleStrength"); + Range(o.WaveGeometryDistanceMeters, 500, 10000, id, "ocean.waveGeometryDistanceMeters"); + Range(o.WhitecapThreshold, .05, 1, id, "ocean.whitecapThreshold"); Range(o.Roughness, .04, 1, id, "ocean.roughness"); Range(o.ReflectionStrength, 0, 3, id, "ocean.reflectionStrength"); Range(o.RefractionStrength, 0, .1, id, "ocean.refractionStrength"); Range(o.FoamStrength, 0, 3, id, "ocean.foamStrength"); Color(o.DeepColor, id); Color(o.ShallowColor, id); Color(o.Absorption, id); diff --git a/src/Universe.Core/Generation/CelestialBodies.cs b/src/Universe.Core/Generation/CelestialBodies.cs index 36af1a0..3342147 100644 --- a/src/Universe.Core/Generation/CelestialBodies.cs +++ b/src/Universe.Core/Generation/CelestialBodies.cs @@ -46,6 +46,8 @@ public sealed record PlanetData( public double StormStrength { get; init; } public RasterImage? SurfaceTexture { get; init; } public TerrainMaterialSettings Materials { get; init; } = new(); + public VegetationSettings? Vegetation { get; init; } + public RasterImage? VegetationBiomeTexture { get; init; } public OceanSettings? Ocean { get; init; } public CloudSettings? Clouds { get; init; } public CloudDensityMap? CloudDensityMap { get; init; } diff --git a/src/Universe.Core/Generation/GenerationJobs.cs b/src/Universe.Core/Generation/GenerationJobs.cs new file mode 100644 index 0000000..f2c915d --- /dev/null +++ b/src/Universe.Core/Generation/GenerationJobs.cs @@ -0,0 +1,13 @@ +namespace Universe.Core.Generation; + +/// One bounded pool for terrain and vegetation; GL work stays on the render thread. +public static class GenerationJobs +{ + private static readonly SemaphoreSlim Slots=new(System.Math.Clamp(Environment.ProcessorCount-2,1,4)); + public static Task Run(Func work,CancellationToken token)=>Task.Run(async()=> + { + await Slots.WaitAsync(token).ConfigureAwait(false); + try {token.ThrowIfCancellationRequested();return work();} + finally {Slots.Release();} + },token); +} diff --git a/src/Universe.Core/Generation/OceanSettings.cs b/src/Universe.Core/Generation/OceanSettings.cs index 40ab5f5..f4605d5 100644 --- a/src/Universe.Core/Generation/OceanSettings.cs +++ b/src/Universe.Core/Generation/OceanSettings.cs @@ -13,8 +13,12 @@ public sealed record OceanSettings public double WaveAmplitudeMeters { get; init; } = 1.6; public double WaveLengthMeters { get; init; } = 72; public double WindDirectionDegrees { get; init; } = 35; + public double WindSpreadDegrees { get; init; } = 110; public double WaveSpeed { get; init; } = 1; public double Choppiness { get; init; } = .65; + public double RippleStrength { get; init; } = .8; + public double WaveGeometryDistanceMeters { get; init; } = 4000; + public double WhitecapThreshold { get; init; } = .22; public double Roughness { get; init; } = .14; public double ReflectionStrength { get; init; } = 1; public double RefractionStrength { get; init; } = .018; @@ -39,11 +43,14 @@ public sealed record OceanReflectionSettings public double EdgeFade { get; init; } = .07; } -public readonly record struct OceanWave(DVector3 Direction, double WaveNumber, double Amplitude, double AngularFrequency); +public readonly record struct OceanWave(DVector3 Direction, double WaveNumber, double Amplitude, double AngularFrequency, double Phase); public static class OceanSurface { - public const int WaveCount = 12; + public const int WaveCount = 32; + public const int GeometryWaveCount = 16; + // Shared with the shader; origins are reduced in double precision before conversion to floats. + public static ReadOnlySpan DetailFrequencies => [.045, .35, 1.4]; public static OceanWave[] Spectrum(OceanSettings settings, ulong seed) { @@ -51,12 +58,19 @@ public static class OceanSurface var waves = new OceanWave[WaveCount]; for (var i = 0; i < waves.Length; i++) { - var angle = (settings.WindDirectionDegrees + (random.NextDouble() - .5) * 115 + i * 23) * System.Math.PI / 180; - var direction = new DVector3(System.Math.Cos(angle), .23 * System.Math.Sin(angle * 1.7 + i), System.Math.Sin(angle)).Normalized(); - var wavelength = settings.WaveLengthMeters * System.Math.Pow(.63, i); + var band = i / 4; + var lobe = i % 4; + // Several independently phased directions per octave avoid long, repeating foam rows. + var spread = ((lobe + random.NextDouble()) / 4 - .5) * settings.WindSpreadDegrees; + if (lobe == 3) spread += 70; // weaker cross swell, rather than one set of parallel waves + var angle = (settings.WindDirectionDegrees + spread) * System.Math.PI / 180; + var direction = new DVector3(System.Math.Cos(angle), (random.NextDouble() - .5) * .4, System.Math.Sin(angle)).Normalized(); + var wavelength = settings.WaveLengthMeters * System.Math.Pow(.55, band) * (.82 + .36 * random.NextDouble()); var k = System.Math.Tau / wavelength; - var amplitude = settings.WaveAmplitudeMeters * System.Math.Pow(.62, i); - waves[i] = new(direction, k, amplitude, System.Math.Sqrt(9.81 * k) * settings.WaveSpeed); + var weight = lobe switch { 0 => .38, 1 => .29, 2 => .21, _ => .12 }; + // Limit steepness even for unusually short/high waves configured in JSON. + var amplitude = System.Math.Min(settings.WaveAmplitudeMeters * System.Math.Pow(.51, band) * weight, .18 / k); + waves[i] = new(direction, k, amplitude, System.Math.Sqrt(9.81 * k) * settings.WaveSpeed, random.NextDouble() * System.Math.Tau); } return waves; } diff --git a/src/Universe.Core/Generation/RasterImage.cs b/src/Universe.Core/Generation/RasterImage.cs index cc338db..a7ee2ab 100644 --- a/src/Universe.Core/Generation/RasterImage.cs +++ b/src/Universe.Core/Generation/RasterImage.cs @@ -6,6 +6,19 @@ namespace Universe.Core.Generation; /// Unmodified image rows, north at the top. Color and height maps share equirectangular UVs. public sealed record RasterImage(int Width, int Height, byte[] Rgba, string? SourcePath = null) { + public System.Numerics.Vector3 SampleColor(DVector3 direction) + { + var (u,v)=HeightMap.Coordinates(direction); + var x=(u-System.Math.Floor(u))*Width-.5;var y=System.Math.Clamp(v*Height-.5,0,Height-1); + var ix=(int)System.Math.Floor(x);var iy=(int)System.Math.Floor(y); + System.Numerics.Vector3 At(int a,int b) + { + var offset=(System.Math.Clamp(b,0,Height-1)*Width+((a%Width+Width)%Width))*4; + return new(Rgba[offset],Rgba[offset+1],Rgba[offset+2]); + } + return System.Numerics.Vector3.Lerp(System.Numerics.Vector3.Lerp(At(ix,iy),At(ix+1,iy),(float)(x-ix)), + System.Numerics.Vector3.Lerp(At(ix,iy+1),At(ix+1,iy+1),(float)(x-ix)),(float)(y-iy)); + } public static RasterImage Load(string path) { using var stream = File.OpenRead(path); diff --git a/src/Universe.Core/Generation/TerrainPatchBuilder.cs b/src/Universe.Core/Generation/TerrainPatchBuilder.cs index d308571..6a73f4a 100644 --- a/src/Universe.Core/Generation/TerrainPatchBuilder.cs +++ b/src/Universe.Core/Generation/TerrainPatchBuilder.cs @@ -19,6 +19,9 @@ public sealed record TerrainPatchData(DVector3 Origin, float[] Vertices, uint[] public static class TerrainPatchBuilder { public const int Segments = 24; + // Every patch has identical topology. Keep it once instead of allocating + // hundreds of tiny index arrays for every worker result during a camera turn. + private static readonly uint[] SharedIndices=BuildIndices(); public static TerrainPatchData Build(PlanetData planet, TerrainPatchKey key) { @@ -34,7 +37,8 @@ public static class TerrainPatchBuilder var d = TerrainChunkBuilder.FaceDirection(key.Face, startU + key.Width * x / Segments, startV + key.Width * y / Segments).Normalized(); positions[(y + 1) * expanded + x + 1] = d * TerrainHeight.Sample(planet, d); } - var vertices = new List((side * side + side * 4) * 9); + var vertices = new float[(side * side + side * 4) * 9]; + var vertexOffset=0; for (var y = 0; y < side; y++) for (var x = 0; x < side; x++) { @@ -42,14 +46,7 @@ public static class TerrainPatchBuilder var p = positions[index]; var n = DVector3.Cross(positions[index + 1] - positions[index - 1], positions[index + expanded] - positions[index - expanded]).Normalized(); if (DVector3.Dot(n, p) < 0) n = -n; - AddVertex(vertices, p - origin, n, p.Normalized()); - } - var indices = new List(Segments * Segments * 6 + Segments * 24); - for (var y = 0; y < Segments; y++) - for (var x = 0; x < Segments; x++) - { - var a = (uint)(y * side + x); - indices.AddRange([a, a + (uint)side, a + 1, a + 1, a + (uint)side, a + (uint)side + 1]); + AddVertex(vertices, ref vertexOffset, p - origin, n, p.Normalized()); } var spacing = planet.RadiusMeters * key.Width / Segments; // A fixed fraction of mountain amplitude creates hundred-metre walls around @@ -57,21 +54,16 @@ public static class TerrainPatchBuilder var skirtDepth = spacing * 2 + System.Math.Min(planet.TerrainAmplitudeMeters * .012, spacing * 2); for (var edge = 0; edge < 4; edge++) { - var skirtStart = (uint)(vertices.Count / 9); for (var t = 0; t < side; t++) { var source = edge switch { 0 => t, 1 => t * side + Segments, 2 => Segments * side + t, _ => t * side }; var p = new DVector3(vertices[source * 9], vertices[source * 9 + 1], vertices[source * 9 + 2]) + origin; var n = new DVector3(vertices[source * 9 + 3], vertices[source * 9 + 4], vertices[source * 9 + 5]); var d = p.Normalized(); - AddVertex(vertices, p - d * skirtDepth - origin, n, d); - if (t == Segments) continue; - var next = source + (edge is 0 or 2 ? 1 : side); - var b = skirtStart + (uint)t; - indices.AddRange([(uint)source, b, (uint)next, (uint)next, b, b + 1]); + AddVertex(vertices, ref vertexOffset, p - d * skirtDepth - origin, n, d); } } - var vertexData = vertices.ToArray(); + var vertexData = vertices; var min = new DVector3(double.PositiveInfinity, double.PositiveInfinity, double.PositiveInfinity); var max = new DVector3(double.NegativeInfinity, double.NegativeInfinity, double.NegativeInfinity); for (var i = 0; i < vertexData.Length; i += 9) @@ -83,11 +75,33 @@ public static class TerrainPatchBuilder var radius = 0d; for (var i = 0; i < vertexData.Length; i += 9) radius = System.Math.Max(radius, (new DVector3(vertexData[i], vertexData[i + 1], vertexData[i + 2]) - center).Length); - return new(origin, vertexData, indices.ToArray()) { BoundsCenter = origin + center, BoundsRadius = radius + .01 }; + return new(origin, vertexData, SharedIndices) { BoundsCenter = origin + center, BoundsRadius = radius + .01 }; } - private static void AddVertex(List target, DVector3 p, DVector3 n, DVector3 d) + private static void AddVertex(float[] target, ref int offset, DVector3 p, DVector3 n, DVector3 d) { - target.AddRange([(float)p.X, (float)p.Y, (float)p.Z, (float)n.X, (float)n.Y, (float)n.Z, (float)d.X, (float)d.Y, (float)d.Z]); + target[offset++]=(float)p.X;target[offset++]=(float)p.Y;target[offset++]=(float)p.Z; + target[offset++]=(float)n.X;target[offset++]=(float)n.Y;target[offset++]=(float)n.Z; + target[offset++]=(float)d.X;target[offset++]=(float)d.Y;target[offset++]=(float)d.Z; + } + + private static uint[] BuildIndices() + { + const int side=Segments+1; + var indices=new uint[Segments*Segments*6+Segments*24];var offset=0; + void TrianglePair(uint a,uint b,uint c,uint d,uint e,uint f) + {indices[offset++]=a;indices[offset++]=b;indices[offset++]=c;indices[offset++]=d;indices[offset++]=e;indices[offset++]=f;} + for(var y=0;yt,1=>t*side+Segments,2=>Segments*side+t,_=>t*side}; + var next=source+(edge is 0 or 2?1:side);var b=(uint)(side*side+edge*side+t); + TrianglePair((uint)source,b,(uint)next,(uint)next,b,b+1); + } + return indices; } } diff --git a/src/Universe.Core/Generation/UniverseGenerator.cs b/src/Universe.Core/Generation/UniverseGenerator.cs index 4f42e79..2b8546e 100644 --- a/src/Universe.Core/Generation/UniverseGenerator.cs +++ b/src/Universe.Core/Generation/UniverseGenerator.cs @@ -118,6 +118,8 @@ public sealed class UniverseGenerator Orbit = MovingOrbit(position - stars[0].PositionMeters, orbitNormal, eccentricity, phase, stars[0].MassKg), Rotation = new((10 + weather.NextDouble() * 40) * 3600, weather.NextDouble() * 60, weather.NextDouble() * 360), AtmosphereDensity = atmosphereDensity, + SurfaceType = atmosphereDensity>.6&&orbitAu is >.7 and <2.5?PlanetSurfaceType.Terrestrial:PlanetSurfaceType.Rocky, + Vegetation = atmosphereDensity>.6&&orbitAu is >.7 and <2.5?VegetationSettings.Default:null, Clouds = CloudSettings.ForAtmosphere(seed, radius * .018, atmosphereDensity, PlanetSurfaceType.Rocky) }); } diff --git a/src/Universe.Core/Generation/Vegetation.cs b/src/Universe.Core/Generation/Vegetation.cs new file mode 100644 index 0000000..3802035 --- /dev/null +++ b/src/Universe.Core/Generation/Vegetation.cs @@ -0,0 +1,163 @@ +using System.Numerics; +using System.Text.Json; +using System.Text.Json.Serialization; +using Universe.Core.Math; + +namespace Universe.Core.Generation; + +public enum VegetationType { Tree, Shrub, Grass, Fern } + +public sealed record VegetationSettings +{ + public static VegetationSettings Default {get;}=new(); + public bool Enabled { get; init; } = true; + public double DensityMultiplier { get; init; } = 1; + public string[]? AllowedPlants { get; init; } + public string? BiomeTexture { get; init; } + public bool AffectGroundMaterials { get; init; } = true; + public double TreeDistanceMeters { get; init; } = 2000; + public double ShrubDistanceMeters { get; init; } = 150; + public double GrassDistanceMeters { get; init; } = 80; + public double[] TreeLodMeters { get; init; } = [40, 150, 400]; + public double TreeDensityPerHectare { get; init; } = 600; + public double ShrubDensityPerHectare { get; init; } = 350; + public double GroundDensityPerHectare { get; init; } = 20000; + public double ForestScaleMeters { get; init; } = 450; + public double UploadBudgetMilliseconds { get; init; } = 1; + public bool DepthPrepass {get;init;} + // Space for the full 2 km neighbourhood, including trees behind the player. + public int MaximumCachedTiles { get; init; } = 2048; + public int MaximumInstances { get; init; } = 650000; + public int ShadowResolution { get; init; } = 2048; + public double[] ShadowDistancesMeters { get; init; } = [100, 400]; + public double ShadowSoftness { get; init; } = 1; + public double ShadowCascadeBlend { get; init; } = .2; + public double ShadowBiasMeters { get; init; } = .035; + public double CollisionDistanceMeters { get; init; } = 64; + public void Validate() + { + static bool Range(double n,double min,double max)=>double.IsFinite(n)&&n>=min&&n<=max; + if(!Range(DensityMultiplier,0,10)||!Range(TreeDistanceMeters,50,5000)||!Range(ShrubDistanceMeters,10,500) + ||!Range(GrassDistanceMeters,5,250)||!Range(ForestScaleMeters,20,10000)||!Range(UploadBudgetMilliseconds,.1,5) + ||!Range(TreeDensityPerHectare,0,2000)||!Range(ShrubDensityPerHectare,0,5000)||!Range(GroundDensityPerHectare,0,50000) + ||TreeLodMeters is not {Length:3}||TreeLodMeters.Any(n=>!Range(n,1,TreeDistanceMeters)) + ||TreeLodMeters[0]>=TreeLodMeters[1]||TreeLodMeters[1]>=TreeLodMeters[2] + ||ShadowDistancesMeters is not {Length:2}||ShadowDistancesMeters.Any(n=>!Range(n,10,1000)) + ||ShadowDistancesMeters[0]>=ShadowDistancesMeters[1]||ShadowResolution is <256 or >4096 + ||!Range(ShadowSoftness,0,1)||!Range(ShadowCascadeBlend,0,.4)||!Range(ShadowBiasMeters,.001,.25) + ||(ShadowResolution&(ShadowResolution-1))!=0||MaximumCachedTiles is <16 or >4096 + ||MaximumInstances is <1000 or >2000000||!Range(CollisionDistanceMeters,10,256) + ||AllowedPlants?.Any(string.IsNullOrWhiteSpace)==true) + throw new InvalidDataException("Invalid vegetation density, LOD, streaming or shadow settings."); + } +} + +public sealed record TrunkCollision(double RadiusMeters = .3, double HeightMeters = 8); +public sealed record VegetationDefinition +{ + public required string Id { get; init; } + public required string Model { get; init; } + public VegetationType Type { get; init; } + public int[] MatchColorRgb { get; init; } = [45,80,35]; + public double ColorTolerancePercent { get; init; } = 22; + public double SpawnWeight { get; init; } = 1; + public double[] ScaleRange { get; init; } = [.8,1.25]; + public double[] AltitudeMeters { get; init; } = [-100,2200]; + public double[] AbsoluteLatitudeDegrees { get; init; } = [0,80]; + public double MaxSlopeDegrees { get; init; } = 32; + public double MinimumSpacingMeters { get; init; } = 2; + public double WindStrength { get; init; } = .2; + public double[]? LodMeters {get;init;} + public double? DistanceMeters {get;init;} + public TrunkCollision? TrunkCollision { get; init; } +} +public sealed record BiomeDefinition(string Id, int[] ColorRgb, double TolerancePercent, string Material); + +public sealed record VegetationCatalog +{ + public int SchemaVersion { get; init; } = 1; + public BiomeDefinition[] Biomes { get; init; } = []; + public VegetationDefinition[] Plants { get; init; } = []; + public static VegetationCatalog Load(string path) + { + var options=new JsonSerializerOptions {PropertyNameCaseInsensitive=true,UnmappedMemberHandling=JsonUnmappedMemberHandling.Disallow}; + options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); + var catalog=JsonSerializer.Deserialize(File.ReadAllText(path),options) + ??throw new InvalidDataException("Empty vegetation catalog."); + string[] biomeIds=["forest","dry","sand","snow"]; + if(catalog.SchemaVersion!=1||catalog.Biomes.Length!=4||biomeIds.Any(id=>catalog.Biomes.Count(b=>b.Id==id)!=1)||catalog.Plants.Length==0 + ||catalog.Plants.Select(p=>p.Id).Distinct(StringComparer.Ordinal).Count()!=catalog.Plants.Length) + throw new InvalidDataException("Vegetation catalog needs four biomes and unique plant IDs."); + catalog=catalog with {Biomes=biomeIds.Select(id=>catalog.Biomes.Single(b=>b.Id==id)).ToArray()}; + foreach(var b in catalog.Biomes){ValidateColor(b.ColorRgb,b.TolerancePercent);if(string.IsNullOrWhiteSpace(b.Material))throw new InvalidDataException("A biome needs a ground material ID.");} + foreach(var p in catalog.Plants) + { + ValidateColor(p.MatchColorRgb,p.ColorTolerancePercent); + if(string.IsNullOrWhiteSpace(p.Id)||string.IsNullOrWhiteSpace(p.Model)||!Enum.IsDefined(p.Type) + ||Path.IsPathRooted(p.Model)||p.Model.Replace('\\','/').Split('/').Contains("..")||!p.Model.EndsWith(".glb",StringComparison.OrdinalIgnoreCase) + ||!double.IsFinite(p.SpawnWeight)||p.SpawnWeight<=0||p.ScaleRange is not {Length:2} + ||p.ScaleRange.Any(n=>!double.IsFinite(n)||n<=0)||p.ScaleRange[0]>p.ScaleRange[1] + ||p.AltitudeMeters is not {Length:2}||p.AltitudeMeters.Any(n=>!double.IsFinite(n))||p.AltitudeMeters[0]>p.AltitudeMeters[1] + ||p.AbsoluteLatitudeDegrees is not {Length:2}||p.AbsoluteLatitudeDegrees.Any(n=>!double.IsFinite(n)||n<0||n>90) + ||p.AbsoluteLatitudeDegrees[0]>p.AbsoluteLatitudeDegrees[1]||!double.IsFinite(p.MaxSlopeDegrees)||p.MaxSlopeDegrees is <0 or >70 + ||!double.IsFinite(p.MinimumSpacingMeters)||p.MinimumSpacingMeters is <0 or >20 + ||!double.IsFinite(p.WindStrength)||p.WindStrength is <0 or >2 + ||p.DistanceMeters is {} distance&&(!double.IsFinite(distance)||distance is <1 or >5000) + ||p.LodMeters is {} lod&&(lod.Length!=3||lod.Any(n=>!double.IsFinite(n)||n is <1 or >5000)||lod[0]>=lod[1]||lod[1]>=lod[2]) + ||p.LodMeters is not null&&p.Type!=VegetationType.Tree + ||p.TrunkCollision is {} c && (!double.IsFinite(c.RadiusMeters)||c.RadiusMeters<=0||!double.IsFinite(c.HeightMeters)||c.HeightMeters<=0)) + throw new InvalidDataException($"Invalid vegetation definition '{p.Id}'."); + } + return catalog; + } + private static void ValidateColor(int[] color,double tolerance) + { + if(color is not {Length:3}||color.Any(c=>c is <0 or >255)||!double.IsFinite(tolerance)||tolerance is <0 or >100) + throw new InvalidDataException("Vegetation RGB must be 0..255 and tolerance 0..100 percent."); + } + public static double Match(Vector3 color,int[] target,double tolerance) + { + var distance=Vector3.Distance(color,new(target[0],target[1],target[2]))*(100/(255*System.Math.Sqrt(3))); + if(tolerance==0)return distance<1e-6?1:0; + var t=System.Math.Clamp((distance-tolerance*.8)/(tolerance*.2),0,1); + return 1-t*t*(3-2*t); + } +} + +/// The same sRGB source and equirectangular mapping drive plants and terrain materials. +public sealed class BiomeField +{ + public PlanetData Planet {get;} + public VegetationCatalog Catalog {get;} + public RasterImage? Image {get;} + public BiomeField(PlanetData planet,VegetationCatalog catalog) + { + Planet=planet with {Orientation=PlanetOrientation.Identity}; Catalog=catalog; + Image=planet.VegetationBiomeTexture??planet.SurfaceTexture; + } + public Vector3 Color(DVector3 direction) + { + if(Image is not null)return Image.SampleColor(direction); + var latitude=System.Math.Abs(direction.Normalized().Y); + if(latitude>.91)return new(235,238,240); + var n=ProceduralNoise.Sample(direction*5+ProceduralNoise.SeedOffset(Planet.Seed)); + return n<-.2&&latitude<.6?new(205,181,120):n<.1?new(145,150,80):new(45,80,35); + } + public Vector4 Weights(Vector3 color) + { + var b=Catalog.Biomes; + var v=new Vector4((float)VegetationCatalog.Match(color,b[0].ColorRgb,b[0].TolerancePercent), + (float)VegetationCatalog.Match(color,b[1].ColorRgb,b[1].TolerancePercent), + (float)VegetationCatalog.Match(color,b[2].ColorRgb,b[2].TolerancePercent), + (float)VegetationCatalog.Match(color,b[3].ColorRgb,b[3].TolerancePercent)); + var sum=v.X+v.Y+v.Z+v.W; + return sum>1?v/sum:v; + } + public double ForestCover(DVector3 direction) + { + var p=direction*Planet.RadiusMeters/(Planet.Vegetation?.ForestScaleMeters??450)+ProceduralNoise.SeedOffset(Planet.Seed); + var n=ProceduralNoise.Sample(p)*.7+ProceduralNoise.Sample(p*2.17)*.3; + var x=System.Math.Clamp((n+.26)/.65,0,1); + return x*x*(3-2*x); + } +} diff --git a/src/Universe.Core/Generation/VegetationSiteFinder.cs b/src/Universe.Core/Generation/VegetationSiteFinder.cs new file mode 100644 index 0000000..8737fc3 --- /dev/null +++ b/src/Universe.Core/Generation/VegetationSiteFinder.cs @@ -0,0 +1,34 @@ +using Universe.Core.Math; + +namespace Universe.Core.Generation; + +public static class VegetationSiteFinder +{ + public static DVector3 FindForest(BiomeField field,DVector3 sunBody) + { + var best=double.NegativeInfinity;var result=DVector3.Zero;var candidates=new int[6];var minimumHeight=double.PositiveInfinity;var maximumHeight=double.NegativeInfinity; + // Scan the actual planet data, not an assumed geographic forest or a hard-coded texture colour. + for(var latitude=-64;latitude<=64;latitude+=2)for(var longitude=-180;longitude<180;longitude+=2) + { + if(System.Math.Abs(latitude)<26)continue; + var lat=latitude*System.Math.PI/180;var lon=longitude*System.Math.PI/180; + var direction=new DVector3(System.Math.Cos(lat)*System.Math.Cos(lon),System.Math.Sin(lat),System.Math.Cos(lat)*System.Math.Sin(lon)); + var weight=field.Weights(field.Color(direction)).X;if(weight<.6)continue;candidates[0]++; + if(field.Planet.OceanMask?.Sample(direction)>.05)continue;candidates[1]++; + var sun=DVector3.Dot(direction,sunBody);if(sun<.2)continue;candidates[2]++; + var height=TerrainHeight.Sample(field.Planet,direction)-field.Planet.RadiusMeters; + minimumHeight=System.Math.Min(minimumHeight,height);maximumHeight=System.Math.Max(maximumHeight,height); + if(height<1||!field.Catalog.Plants.Any(p=>p.Type==VegetationType.Tree + &&(field.Planet.Vegetation?.AllowedPlants is null||field.Planet.Vegetation.AllowedPlants.Contains(p.Id,StringComparer.Ordinal)) + &&height>=p.AltitudeMeters[0]&&height<=p.AltitudeMeters[1] + &&System.Math.Abs(latitude)>=p.AbsoluteLatitudeDegrees[0]&&System.Math.Abs(latitude)<=p.AbsoluteLatitudeDegrees[1] + &&VegetationCatalog.Match(field.Color(direction),p.MatchColorRgb,p.ColorTolerancePercent)>.5))continue;candidates[3]++; + var cover=field.ForestCover(direction);if(cover<.3)continue;candidates[4]++; + var slope=DVector3.Dot(TerrainHeight.Normal(field.Planet,direction,1e-6),direction);if(slope<.95)continue;candidates[5]++; + var score=weight*cover*(.4+sun)*slope; + if(score>best){best=score;result=direction;} + } + if(result==DVector3.Zero)throw new InvalidOperationException($"No lit, dry forest site satisfies the vegetation/terrain rules. Filters: {string.Join('/',candidates)}, elevations {minimumHeight:F1}..{maximumHeight:F1} m."); + return result; + } +} diff --git a/src/Universe.Core/Generation/VegetationStream.cs b/src/Universe.Core/Generation/VegetationStream.cs new file mode 100644 index 0000000..0f518d2 --- /dev/null +++ b/src/Universe.Core/Generation/VegetationStream.cs @@ -0,0 +1,89 @@ +using System.Diagnostics; +using Universe.Core.Math; + +namespace Universe.Core.Generation; + +/// Bounded asynchronous body-space tiles, shared by rendering and trunk collision. +public sealed class VegetationStream : IDisposable +{ + private sealed class Entry {public Task? Task;public VegetationTile? Tile;public long LastUsed;public CancellationTokenSource? Cancellation;} + private readonly Dictionary _tiles=[]; + private readonly CancellationTokenSource _cancel=new(); + private long _frame; + private DVector3 _lastCamera; + private List<(VegetationTileKey Key,double Priority)>? _wanted; + private HashSet _requested=[]; + public VegetationTileGenerator Generator {get;} + public long Revision {get;private set;} + public long TreeRevision {get;private set;} + public int PendingTiles=>_tiles.Values.Count(e=>e.Task is not null); + public int InstanceCount {get;private set;} + public double CpuMilliseconds {get;private set;} + public double GenerationMilliseconds {get;private set;} + public IEnumerable Tiles=>_tiles.Values.Where(e=>e.Tile is not null).Select(e=>e.Tile!); + public VegetationStream(BiomeField field)=>Generator=new(field); + public void ResetFrameDiagnostics(){CpuMilliseconds=0;GenerationMilliseconds=0;} + public void Update(DVector3 camera) + { + var timer=Stopwatch.StartNew();_frame++;GenerationMilliseconds=0; + foreach(var entry in _tiles.Values) + if(entry.Task is {IsCompleted:true} task) + { + entry.Task=null; + entry.Cancellation?.Dispose();entry.Cancellation=null; + if(task.IsCompletedSuccessfully){entry.Tile=task.Result;InstanceCount+=entry.Tile.Instances.Length;GenerationMilliseconds+=entry.Tile.GenerationMilliseconds;Revision++;if(entry.Tile.Key.Layer==0)TreeRevision++;} + else if(task.IsFaulted)throw new InvalidOperationException("Vegetation generation failed.",task.Exception); + } + foreach(var key in _tiles.Where(p=>p.Value.Task is null&&p.Value.Tile is null).Select(p=>p.Key).ToArray())_tiles.Remove(key); + var altitude=System.Math.Max(0,camera.Length-Generator.Field.Planet.MaximumTerrainRadiusMeters); + if(altitude>Generator.Settings.TreeDistanceMeters*1.2){CpuMilliseconds=timer.Elapsed.TotalMilliseconds;return;} + // Retain a full neighbourhood. Turning the camera must never invalidate tiles + // or cancel generation behind the player, which is also needed by collision. + if(_wanted is null||(camera-_lastCamera).Length>6) + { + var wanted=new List<(VegetationTileKey Key,double Priority)>(); + for(var layer=0;layer<3;layer++) + { + if(altitude>Generator.Range(layer))continue; + foreach(var key in Generator.Around(camera,layer)) + { + var offset=key.Direction*camera.Length-camera; + var distance=offset.Length; + wanted.Add((key,distance+(layer==0?0:layer==1?40:60))); + } + } + _wanted=wanted.OrderBy(w=>w.Priority).Take(Generator.Settings.MaximumCachedTiles).ToList(); + _requested=_wanted.Select(w=>w.Key).ToHashSet();_lastCamera=camera; + } + var pending=PendingTiles; + foreach(var (key,_) in _wanted) + { + if(_tiles.TryGetValue(key,out var entry)){entry.LastUsed=_frame;continue;} + if(pending>=8||InstanceCount>=Generator.Settings.MaximumInstances)continue; + if(_tiles.Count>=Generator.Settings.MaximumCachedTiles) + { + var victim=_tiles.Where(p=>p.Value.Task is null&&!_requested.Contains(p.Key)).MinBy(p=>p.Value.LastUsed); + if(victim.Value is null)continue; + InstanceCount-=victim.Value.Tile?.Instances.Length??0;_tiles.Remove(victim.Key);Revision++;if(victim.Key.Layer==0)TreeRevision++; + } + var cancellation=CancellationTokenSource.CreateLinkedTokenSource(_cancel.Token);var token=cancellation.Token; + _tiles[key]=new(){LastUsed=_frame,Cancellation=cancellation,Task=GenerationJobs.Run(()=>Generator.Generate(key,token),token)}; + pending++; + } + foreach(var entry in _tiles.Values)if(entry.Task is not null&&_frame-entry.LastUsed>2)entry.Cancellation?.Cancel(); + foreach(var key in _tiles.Where(p=>p.Value.Task is null&&(_frame-p.Value.LastUsed>120 + ||_tiles.Count>Generator.Settings.MaximumCachedTiles&&_frame-p.Value.LastUsed>2)).Select(p=>p.Key).ToArray()) + { + InstanceCount-=_tiles[key].Tile?.Instances.Length??0;_tiles.Remove(key);Revision++; + if(key.Layer==0)TreeRevision++; + } + CpuMilliseconds=timer.Elapsed.TotalMilliseconds; + } + public void Clear() + { + // In-flight tasks are harmless and bounded; disposal cancels them on a system switch. + foreach(var key in _tiles.Where(p=>p.Value.Task is null).Select(p=>p.Key).ToArray())_tiles.Remove(key); + InstanceCount=0;Revision++;TreeRevision++; + } + public void Dispose(){_cancel.Cancel();foreach(var entry in _tiles.Values)entry.Cancellation?.Dispose();_cancel.Dispose();} +} diff --git a/src/Universe.Core/Generation/VegetationTiles.cs b/src/Universe.Core/Generation/VegetationTiles.cs new file mode 100644 index 0000000..56a9f06 --- /dev/null +++ b/src/Universe.Core/Generation/VegetationTiles.cs @@ -0,0 +1,139 @@ +using Universe.Core.Math; + +namespace Universe.Core.Generation; + +public readonly record struct VegetationTileKey(int Face,int Level,int X,int Y,int Layer) +{ + public double Width=>2d/(1L<TerrainChunkBuilder.FaceDirection(Face,-1+(X+.5)*Width,-1+(Y+.5)*Width).Normalized(); + public static VegetationTileKey At(DVector3 direction,int level,int layer) + { + var d=direction.Normalized();var ax=System.Math.Abs(d.X);var ay=System.Math.Abs(d.Y);var az=System.Math.Abs(d.Z); + int face;double u,v; + if(ax>=ay&&ax>=az){face=d.X>=0?0:1;u=(d.X>=0?-d.Z:d.Z)/ax;v=d.Y/ax;} + else if(ay>=az){face=d.Y>=0?2:3;u=d.X/ay;v=(d.Y>=0?-d.Z:d.Z)/ay;} + else {face=d.Z>=0?4:5;u=(d.Z>=0?d.X:-d.X)/az;v=d.Y/az;} + var count=1<At(TerrainChunkBuilder.FaceDirection(Face, + -1+(X+x+.5)*Width,-1+(Y+y+.5)*Width),Level,Layer); +} + +public readonly record struct VegetationInstance(ulong Id,int PlantIndex,DVector3 BodyPosition,DVector3 Up,double Scale,double Yaw,double Variation); +public sealed record VegetationTile(VegetationTileKey Key,DVector3 Origin,VegetationInstance[] Instances) +{public double GenerationMilliseconds {get;init;}} + +public sealed class VegetationTileGenerator +{ + public BiomeField Field {get;} + public VegetationSettings Settings {get;} + private readonly int[][] _plants; + private readonly ulong _seed; + public VegetationTileGenerator(BiomeField field) + { + Field=field;Settings=field.Planet.Vegetation??new();Settings.Validate(); + _seed=field.Planet.Seed;foreach(var c in field.Planet.Id)_seed=DeterministicRandom.Mix(_seed^c); + _plants=Enumerable.Range(0,3).Select(layer=>field.Catalog.Plants.Select((p,i)=>(p,i)).Where(x=>Layer(x.p.Type)==layer + &&(Settings.AllowedPlants is null||Settings.AllowedPlants.Contains(x.p.Id,StringComparer.Ordinal))).Select(x=>x.i).ToArray()).ToArray(); + if(Settings.AllowedPlants?.Any(id=>!field.Catalog.Plants.Any(p=>p.Id==id))==true)throw new InvalidDataException("Unknown vegetation plant ID."); + } + public static int Layer(VegetationType type)=>type==VegetationType.Tree?0:type==VegetationType.Shrub?1:2; + public int Level(int layer)=>(int)System.Math.Clamp(System.Math.Ceiling(System.Math.Log2(2*Field.Planet.RadiusMeters/(layer==0?256:layer==1?128:32))),0,24); + public double Range(int layer)=>layer==0?Settings.TreeDistanceMeters:layer==1?Settings.ShrubDistanceMeters:Settings.GrassDistanceMeters; + public IEnumerable Around(DVector3 bodyCamera,int layer,double? range=null) + { + var level=Level(layer);var key=VegetationTileKey.At(bodyCamera,level,layer); + var width=key.Width*Field.Planet.RadiusMeters; + // Cube-face compression is strongest at the corners (sqrt(3)). + var cells=(int)System.Math.Ceiling((range??Range(layer))/width*1.75)+2; + var keys=new HashSet(); + var radial=bodyCamera.Normalized(); + for(var y=-cells;y<=cells;y++)for(var x=-cells;x<=cells;x++) + { + var next=key.Offset(x,y); + if((next.Direction-radial).Length*Field.Planet.RadiusMeters<=(range??Range(layer))+width*1.5)keys.Add(next); + } + return keys.OrderBy(k=>(k.Direction-radial).LengthSquared); + } + public VegetationTile Generate(VegetationTileKey key,CancellationToken token=default) + { + var watch=System.Diagnostics.Stopwatch.StartNew(); + var planet=Field.Planet;var origin=key.Direction*planet.RadiusMeters; + if(_plants[key.Layer].Length==0||Settings.DensityMultiplier<=0)return new(key,origin,[]); + var density=(key.Layer==0?Settings.TreeDensityPerHectare:key.Layer==1?Settings.ShrubDensityPerHectare:Settings.GroundDensityPerHectare)/10000*Settings.DensityMultiplier; + var side=System.Math.Max(1,(int)System.Math.Ceiling(key.Width*planet.RadiusMeters*System.Math.Sqrt(density))); + var grid=(1L<Field.Catalog.Plants[i].MinimumSpacingMeters); + var neighbors=(int)System.Math.Ceiling(spacing/(2*planet.RadiusMeters/grid/3)); + var output=new List(); + Span weights=stackalloc double[_plants[key.Layer].Length]; + for(var y=0;ySystem.Math.Pow(1+u*u+v*v,-1.5))continue; + var d=TerrainChunkBuilder.FaceDirection(key.Face,u,v).Normalized(); + if(planet.OceanMask?.Sample(d)>.2)continue; + var color=Field.Color(d);var biome=Field.Weights(color); + if(biome.W>.5)continue; + var cover=Field.ForestCover(d); + var probability=key.Layer==0?(biome.X+biome.Y*.25)*cover + :key.Layer==1?(biome.X*.65+biome.Y*.3)*(.25+cover*.75) + :(biome.X*(.18+.4*(1-cover))+biome.Y+biome.Z*.035)*(.4+cover*.6); + if(random.NextDouble()>probability)continue; + var height=TerrainHeight.Sample(planet,d); + if(planet.Ocean is {Enabled:true} water&&height=p.AltitudeMeters[0]&&altitude<=p.AltitudeMeters[1] + &&latitude>=p.AbsoluteLatitudeDegrees[0]&&latitude<=p.AbsoluteLatitudeDegrees[1]; + weights[j]=eligible?VegetationCatalog.Match(color,p.MatchColorRgb,p.ColorTolerancePercent)*p.SpawnWeight:0; + total+=weights[j]; + } + if(total<=0)continue; + var pick=random.NextDouble()*total;var selected=0; + for(;selected=hash)continue; + var nr=new DeterministicRandom(nh); + var nd=TerrainChunkBuilder.FaceDirection(neighbor.Face,-1+2*(nx+.2+nr.NextDouble()*.6)/grid, + -1+2*(ny+.2+nr.NextDouble()*.6)/grid).Normalized(); + if((nd-d).Length*planet.RadiusMetersface switch + { + 0=>(-d.Z/d.X,d.Y/d.X),1=>(d.Z/-d.X,d.Y/-d.X),2=>(d.X/d.Y,-d.Z/d.Y), + 3=>(d.X/-d.Y,d.Z/-d.Y),4=>(d.X/d.Z,d.Y/d.Z),_=>(-d.X/-d.Z,d.Y/-d.Z) + }; + private static ulong Hash(int face,long x,long y,int layer,ulong seed)=>DeterministicRandom.Mix(seed + ^DeterministicRandom.Mix(unchecked((ulong)x))^DeterministicRandom.Mix(unchecked((ulong)y)^0x535041574EUL) + ^((ulong)face<<52)^((ulong)layer<<58)); +} diff --git a/src/Universe.Core/Generation/VegetationView.cs b/src/Universe.Core/Generation/VegetationView.cs new file mode 100644 index 0000000..d09963c --- /dev/null +++ b/src/Universe.Core/Generation/VegetationView.cs @@ -0,0 +1,24 @@ +using Universe.Core.Math; + +namespace Universe.Core.Generation; + +/// A padded frustum lets small head movements reuse the same instance buffers. +public readonly record struct VegetationView(DVector3 Forward,DVector3 Right,DVector3 Up,double TanHalfFov,double Aspect) +{ + private const double ReuseAngle=4*System.Math.PI/180; + private const double PaddingAngle=6*System.Math.PI/180; + + public bool CanReuse(VegetationView next) + => TanHalfFov==next.TanHalfFov&&Aspect==next.Aspect + // The trace of the relative basis gives the total rotation, including roll. + &&DVector3.Dot(Forward,next.Forward)+DVector3.Dot(Right,next.Right)+DVector3.Dot(Up,next.Up)>=1+2*System.Math.Cos(ReuseAngle); + + public bool Intersects(DVector3 offset,double radius) + { + radius+=offset.Length*2*System.Math.Sin(PaddingAngle*.5); + var z=DVector3.Dot(offset,Forward);var horizontal=TanHalfFov*Aspect; + return z>=-radius + &&System.Math.Abs(DVector3.Dot(offset,Right))<=z*horizontal+radius*System.Math.Sqrt(1+horizontal*horizontal) + &&System.Math.Abs(DVector3.Dot(offset,Up))<=z*TanHalfFov+radius*System.Math.Sqrt(1+TanHalfFov*TanHalfFov); + } +} diff --git a/src/Universe.Core/Physics/VegetationCollision.cs b/src/Universe.Core/Physics/VegetationCollision.cs new file mode 100644 index 0000000..b77e6ce --- /dev/null +++ b/src/Universe.Core/Physics/VegetationCollision.cs @@ -0,0 +1,82 @@ +using Universe.Core.Generation; +using Universe.Core.Math; + +namespace Universe.Core.Physics; + +public readonly record struct TrunkCapsule(DVector3 Base,DVector3 Up,double Radius,double Height); + +public static class VegetationCollision +{ + /// Conservative continuous capsule sweep with tangent sliding. Coordinates are body-local metres. + public static DVector3 Move(DVector3 start,DVector3 end,DVector3 up,double eyeHeight,IReadOnlyList trunks,double playerRadius=.28) + { + var position=start; + foreach(var trunk in trunks) + { + var separation=Closest(position-up*(eyeHeight-playerRadius),position-up*playerRadius, + trunk.Base+trunk.Up*trunk.Radius,trunk.Base+trunk.Up*System.Math.Max(trunk.Radius,trunk.Height-trunk.Radius)); + var penetration=playerRadius+trunk.Radius-separation.Length; + if(penetration>0) + { + var normal=separation.Normalized(); + if(normal==DVector3.Zero)normal=DVector3.Cross(up,System.Math.Abs(up.Z)<.9?new(0,0,1):new DVector3(1,0,0)).Normalized(); + var push=normal*(penetration+.001);position+=push;end+=push; + } + } + // A player's body is a vertical capsule below the camera; crouching changes its segment length. + for(var iteration=0;iteration<3;iteration++) + { + var delta=end-position;var length=delta.Length;if(length<1e-8)break; + var best=1d;var normal=DVector3.Zero; + foreach(var trunk in trunks) + { + var radius=playerRadius+trunk.Radius; + var t=0d; + for(var step=0;step<64&&t<=best;step++) + { + var point=position+delta*t; + var a=point-up*(eyeHeight-playerRadius);var b=point-up*playerRadius; + var c=trunk.Base+trunk.Up*trunk.Radius;var d=trunk.Base+trunk.Up*System.Math.Max(trunk.Radius,trunk.Height-trunk.Radius); + var separation=Closest(a,b,c,d); + var clearance=separation.Length-radius; + if(clearance<.0002) + { + var n=separation.Normalized();if(n==DVector3.Zero)n=(point-trunk.Base-up*DVector3.Dot(point-trunk.Base,up)).Normalized(); + if(n==DVector3.Zero)n=DVector3.Cross(up,new DVector3(0,0,1)).Normalized(); + if(t=1){position=end;break;} + position+=delta*System.Math.Max(0,best-.0001/length); + var remaining=end-position; + remaining-=normal*System.Math.Min(0,DVector3.Dot(remaining,normal)); + end=position+remaining; + } + return position; + } + private static DVector3 Closest(DVector3 p1,DVector3 q1,DVector3 p2,DVector3 q2) + { + var d1=q1-p1;var d2=q2-p2;var r=p1-p2; + var a=d1.LengthSquared;var e=d2.LengthSquared;var f=DVector3.Dot(d2,r); + double s,t; + if(a<=1e-12&&e<=1e-12)return r; + if(a<=1e-12){s=0;t=System.Math.Clamp(f/e,0,1);} + else + { + var c=DVector3.Dot(d1,r); + if(e<=1e-12){t=0;s=System.Math.Clamp(-c/a,0,1);} + else + { + var b=DVector3.Dot(d1,d2);var den=a*e-b*b; + s=den>1e-12?System.Math.Clamp((b*f-c*e)/den,0,1):0; + t=(b*s+f)/e; + if(t<0){t=0;s=System.Math.Clamp(-c/a,0,1);} + else if(t>1){t=1;s=System.Math.Clamp((b-c)/a,0,1);} + } + } + return p1+d1*s-p2-d2*t; + } +} diff --git a/tests/Universe.Tests/OceanTests.cs b/tests/Universe.Tests/OceanTests.cs index f5ff4e4..1962fcc 100644 --- a/tests/Universe.Tests/OceanTests.cs +++ b/tests/Universe.Tests/OceanTests.cs @@ -91,4 +91,62 @@ public sealed class OceanTests Assert.InRange(a.Sum(w => w.Amplitude), settings.WaveAmplitudeMeters, settings.WaveAmplitudeMeters * 3); Assert.All(a, w => Assert.InRange(w.Amplitude * w.WaveNumber, 0, .2)); } + + [Fact] + public void SpectrumSpreadsEnergyAcrossDirectionsAndIndependentPhases() + { + var settings = new OceanSettings(); + var waves = OceanSurface.Spectrum(settings, 7); + Assert.Equal(OceanSurface.WaveCount, waves.Length); + Assert.All(waves, wave => + { + Assert.Equal(1, wave.Direction.Length, 10); + Assert.Equal(9.81 * wave.WaveNumber, wave.AngularFrequency * wave.AngularFrequency, 9); + Assert.InRange(wave.Phase, 0, System.Math.Tau); + }); + Assert.True(waves.Select(w => w.Phase).Distinct().Count() > 24); + for (var band = 0; band < waves.Length / 4; band++) + { + var group = waves.Skip(band * 4).Take(4).ToArray(); + Assert.True(group.Select(w => w.WaveNumber).Distinct().Count() == 4); + Assert.Contains(group, w => DVector3.Dot(group[0].Direction, w.Direction) < .8); + } + var still = OceanSurface.Spectrum(settings with { WaveSpeed = 0 }, 7); + Assert.All(still, wave => Assert.Equal(0, wave.AngularFrequency)); + Assert.Equal(waves.Select(w => (w.Direction, w.Phase, w.Amplitude)), still.Select(w => (w.Direction, w.Phase, w.Amplitude))); + var storm = OceanSurface.Spectrum(settings with { WaveAmplitudeMeters = 30, WaveLengthMeters = 10 }, 7); + Assert.All(storm, wave => Assert.InRange(wave.Amplitude * wave.WaveNumber, 0, .18000001)); + } + + [Fact] + public void PlanetJsonLoadsWaterDetailControlsAndRejectsInvalidValues() + { + var settings = new OceanSettings { WindSpreadDegrees = 75, RippleStrength = 1.2, + WaveGeometryDistanceMeters = 6000, WhitecapThreshold = .3 }; + var path = Path.Combine(Path.GetTempPath(), $"universe-water-detail-{Guid.NewGuid():N}.json"); + OceanSettings Load(OceanSettings ocean) + { + var definition = new SolarSystemDocument { Systems = [new SolarSystemDefinition + { + Id = "water-system", Name = "Water test", Sector = new(0, 0, 0), + Stars = [new StarDefinition { Id = "sun", Name = "Sun", RadiusMeters = 695700000 }], + Planets = [new PlanetDefinition { Id = "ocean", Name = "Ocean", RadiusMeters = 6371000, + Orbit = new() { SemiMajorAxisAu = 1 }, Ocean = ocean with { MaskFromSurfaceTexture = false } }] + }] }; + File.WriteAllText(path, JsonSerializer.Serialize(definition)); + return KnownSolarSystemCatalog.Load(path).GetById("water-system").Planets.Single().Ocean!; + } + try + { + var parsed = Load(settings); + Assert.Equal(settings.WindSpreadDegrees, parsed.WindSpreadDegrees); + Assert.Equal(settings.RippleStrength, parsed.RippleStrength); + Assert.Equal(settings.WaveGeometryDistanceMeters, parsed.WaveGeometryDistanceMeters); + Assert.Equal(settings.WhitecapThreshold, parsed.WhitecapThreshold); + foreach (var invalid in new[] { settings with { WindSpreadDegrees = 181 }, settings with { RippleStrength = -1 }, + settings with { WaveGeometryDistanceMeters = 0 }, settings with { WhitecapThreshold = .01 } }) + Assert.Throws(() => Load(invalid)); + } + finally { File.Delete(path); } + } } diff --git a/tests/Universe.Tests/TerrainAndCollisionTests.cs b/tests/Universe.Tests/TerrainAndCollisionTests.cs index afa1f90..b9309b1 100644 --- a/tests/Universe.Tests/TerrainAndCollisionTests.cs +++ b/tests/Universe.Tests/TerrainAndCollisionTests.cs @@ -69,6 +69,19 @@ public sealed class TerrainAndCollisionTests } } + [Fact] + public void TerrainStreamingKeepsPerPatchAllocationsBelow64KiB() + { + var planet=new UniverseGenerator(55).Generate(new(0,0,0)).Planets[0]; + var key=new TerrainPatchKey(0,10,512,512); + TerrainPatchBuilder.Build(planet,key); + var started=GC.GetAllocatedBytesForCurrentThread(); + var patch=TerrainPatchBuilder.Build(planet,key); + var allocated=GC.GetAllocatedBytesForCurrentThread()-started; + Assert.InRange(allocated,1,64*1024); + Assert.Equal(TerrainPatchBuilder.Segments*TerrainPatchBuilder.Segments*6+TerrainPatchBuilder.Segments*24,patch.Indices.Length); + Assert.All(patch.Indices,index=>Assert.True(index(System.Text.Json.JsonSerializer.Serialize(settings,options),options)!; + loaded.Validate(); + Assert.Equal(settings.ShadowSoftness,loaded.ShadowSoftness); + Assert.Equal(settings.ShadowCascadeBlend,loaded.ShadowCascadeBlend); + Assert.Equal(settings.ShadowBiasMeters,loaded.ShadowBiasMeters); + foreach(var invalid in new[]{settings with {ShadowSoftness=-.1},settings with {ShadowSoftness=1.1},settings with {ShadowSoftness=double.NaN}, + settings with {ShadowCascadeBlend=-.1},settings with {ShadowCascadeBlend=.5},settings with {ShadowBiasMeters=0}}) + Assert.Throws(invalid.Validate); + } + private static BiomeField Field(byte[]? color=null,bool ocean=false,PlanetOrientation? orientation=null) + { + var rgba=color??[45,80,35,255]; + var planet=new PlanetData(0,"Test",DVector3.Zero,UniverseScale.EarthRadiusMeters,0,0,Vector3.One,Vector3.Zero,0,0,0,77) + {Id="test",SurfaceTexture=new(2,2,rgba.Concat(rgba).Concat(rgba).Concat(rgba).ToArray()), + Vegetation=new(){TreeDistanceMeters=500},Orientation=orientation??PlanetOrientation.Identity,OceanMask=ocean?new(2,2,[1,1,1,1]):null}; + var catalog=new VegetationCatalog + { + Biomes=[new("forest",[45,80,35],24,"forest"),new("dry",[145,150,80],18,"dry"),new("sand",[205,181,120],15,"sand"),new("snow",[240,242,245],12,"snow")], + Plants=[new(){Id="tree",Model="tree.glb",Type=VegetationType.Tree,MatchColorRgb=[45,80,35],TrunkCollision=new()}] + }; + return new(planet,catalog); + } + [Fact] + public void RgbToleranceIsNormalizedAndHasASoftOuterEdge() + { + int[] color=[45,80,35];var target=new Vector3(45,80,35);var diagonal=255*System.Math.Sqrt(3); + Assert.Equal(1,VegetationCatalog.Match(target,color,0)); + Assert.Equal(0,VegetationCatalog.Match(target+Vector3.One,color,0)); + Assert.InRange(VegetationCatalog.Match(target+new Vector3((float)(diagonal*.09),0,0),color,10),.4999,.5001); + Assert.InRange(VegetationCatalog.Match(target+new Vector3((float)(diagonal*.1),0,0),color,10),0,.00001); + } + [Fact] + public void TextureSamplingWrapsAndUsesTheSharedPlanetCoordinates() + { + var image=new RasterImage(4,2,[255,0,0,255,0,255,0,255,0,0,255,255,255,255,255,255,255,0,0,255,0,255,0,255,0,0,255,255,255,255,255,255]); + var green=new DVector3(System.Math.Cos(System.Math.PI*.25),0,System.Math.Sin(System.Math.PI*.25)); + Assert.InRange(Vector3.Distance(image.SampleColor(green),new(0,255,0)),0,.0001); + var a=image.SampleColor(new(-1,0,.00000001));var b=image.SampleColor(new(-1,0,-.00000001)); + Assert.InRange(Vector3.Distance(a,b),0,.0001); + Assert.True(float.IsFinite(image.SampleColor(new(0,1,0)).X)); + } + [Fact] + public void BiomesDistinguishForestDrySandAndSnow() + { + var f=Field(); + Assert.InRange(f.Weights(new(45,80,35)).X,.99,1); + Assert.True(f.Weights(new(145,150,80)).Y>.4); + Assert.True(f.Weights(new(205,181,120)).Z>.4); + Assert.Equal(1,f.Weights(new(240,242,245)).W); + } + [Fact] + public void TilesAreDeterministicAndDoNotDependOnPlanetRotationOrTerrainLod() + { + var a=new VegetationTileGenerator(Field());var b=new VegetationTileGenerator(Field(orientation:new(23.4,179,45))); + var key=VegetationTileKey.At(new(1,.3,.2),a.Level(0),0); + var first=a.Generate(key);Assert.NotEmpty(first.Instances); + Assert.Equal(first.Instances,a.Generate(key).Instances);Assert.Equal(first.Instances,b.Generate(key).Instances); + Assert.All(first.Instances,i=>Assert.InRange(System.Math.Abs(i.BodyPosition.Length-a.Field.Planet.RadiusMeters+.22),0,.00001)); + } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SnowAndOceanDoNotGrowTrees(bool ocean) + { + var g=new VegetationTileGenerator(Field(ocean?null:[240,242,245,255],ocean)); + var key=VegetationTileKey.At(new(1,.3,.2),g.Level(0),0); + Assert.Empty(g.Generate(key).Instances); + } + [Fact] + public void CubeEdgesAndPolesProduceValidStableTiles() + { + var g=new VegetationTileGenerator(Field()); + foreach(var d in new[]{new DVector3(1,1,0),new(0,1,0),new(-1,.2,-1)}) + { + var key=VegetationTileKey.At(d,g.Level(0),0); + var neighbors=new[]{key,key.Offset(1,0),key.Offset(-1,0),key.Offset(0,1)}.Distinct().SelectMany(k=>g.Generate(k).Instances).ToArray(); + Assert.Equal(neighbors.Length,neighbors.Select(i=>i.Id).Distinct().Count()); + Assert.All(neighbors,i=>Assert.True(double.IsFinite(i.BodyPosition.Length))); + } + } + [Fact] + public void AddingSpeciesDoesNotMultiplyLayerDensity() + { + var field=Field();var generator=new VegetationTileGenerator(field);var key=VegetationTileKey.At(new(1,.3,.2),generator.Level(0),0); + var one=generator.Generate(key); + var twoField=new BiomeField(field.Planet,field.Catalog with {Plants=[field.Catalog.Plants[0],field.Catalog.Plants[0] with {Id="second"}]}); + var two=new VegetationTileGenerator(twoField).Generate(key); + Assert.Equal(one.Instances.Length,two.Instances.Length); + Assert.Equal(one.Instances.Select(i=>i.Id),two.Instances.Select(i=>i.Id)); + } + [Fact] + public void SweptPlayerStopsAtTrunkAndSlidesAlongIt() + { + TrunkCapsule[] trunks=[new(DVector3.Zero,new(0,1,0),.36,10)]; + var stopped=VegetationCollision.Move(new(-1000,1.8,0),new(1000,1.8,0),new(0,1,0),1.8,trunks); + Assert.InRange(stopped.X,-.642,-.639); + var slid=VegetationCollision.Move(new(-2,1.8,0),new(2,1.8,3),new(0,1,0),1.8,trunks); + Assert.True(slid.Z>2); + var crouched=VegetationCollision.Move(new(-2,1,0),new(2,1,0),new(0,1,0),1,trunks); + Assert.True(crouched.X<-.63); + var above=VegetationCollision.Move(new(-2,12,0),new(2,12,0),new(0,1,0),1.8,trunks); + Assert.Equal(2,above.X,6); + var inside=VegetationCollision.Move(new(0,1.8,0),new(0,1.8,0),new(0,1,0),1.8,trunks); + Assert.True(System.Math.Sqrt(inside.X*inside.X+inside.Z*inside.Z)>.64); + } + [Fact] + public void ProceduralBiomesAndStreamingSettingsWorkWithoutATexture() + { + var field=Field();var procedural=new BiomeField(field.Planet with {SurfaceTexture=null},field.Catalog); + Assert.Equal(procedural.Color(new(1,0,0)),new BiomeField(procedural.Planet,field.Catalog).Color(new(1,0,0))); + Assert.True(procedural.Weights(procedural.Color(new(0,1,0))).W>.8); + Assert.Throws(()=>new VegetationSettings {UploadBudgetMilliseconds=0}.Validate()); + Assert.Throws(()=>new VegetationSettings {TreeLodMeters=[100,50,200]}.Validate()); + } + [Fact] + public void PlanetIdentityAndPlacementFiltersAreRespected() + { + var field=Field();var generator=new VegetationTileGenerator(field);var key=VegetationTileKey.At(new(1,.3,.2),generator.Level(0),0); + var first=generator.Generate(key).Instances; + var other=new VegetationTileGenerator(new(field.Planet with {Id="another-planet"},field.Catalog)).Generate(key).Instances; + Assert.NotEqual(first.Select(i=>i.Id),other.Select(i=>i.Id)); + foreach(var plant in new[]{field.Catalog.Plants[0] with {AltitudeMeters=[100,200]},field.Catalog.Plants[0] with {AbsoluteLatitudeDegrees=[70,90]}}) + Assert.Empty(new VegetationTileGenerator(new(field.Planet,field.Catalog with {Plants=[plant]})).Generate(key).Instances); + Assert.NotEmpty(new VegetationTileGenerator(new(field.Planet,field.Catalog with {Plants=[field.Catalog.Plants[0] with {MaxSlopeDegrees=0}]})).Generate(key).Instances); + } + [Fact] + public void MinimumSpacingAlsoHoldsAcrossCubeFaceAndChunkBoundaries() + { + var f=Field();var g=new VegetationTileGenerator(new(f.Planet,f.Catalog with {Plants=[f.Catalog.Plants[0] with {MinimumSpacingMeters=8}]})); + var key=VegetationTileKey.At(new(1,.2,1),g.Level(0),0);var adjacent=key.Offset(-1,0); + Assert.NotEqual(key.Face,adjacent.Face); + var instances=new[]{key,adjacent,key.Offset(0,1)}.SelectMany(k=>g.Generate(k).Instances).ToArray();Assert.NotEmpty(instances); + for(var i=0;i=7.999,"Minimum spacing crossed a cube/chunk seam."); + } + [Fact] + public async Task StreamingIsBoundedReloadableAndUsesWorkerResults() + { + var f=Field();using var stream=new VegetationStream(f);var camera=new DVector3(1,.3,.2).Normalized()*(f.Planet.RadiusMeters+1.8); + stream.Update(camera);Assert.InRange(stream.PendingTiles,1,8); + for(var i=0;i<100&&stream.InstanceCount==0;i++){await Task.Delay(10);stream.Update(camera);Assert.InRange(stream.PendingTiles,0,8);} + Assert.True(stream.InstanceCount>0);var tile=stream.Tiles.First(t=>t.Instances.Length>0); + Assert.Equal(tile.Instances,stream.Generator.Generate(tile.Key).Instances); + stream.Clear();Assert.Equal(0,stream.InstanceCount); + } + [Fact] + public async Task StandingStillRetainsTheWholeVegetationNeighbourhood() + { + var field=Field();using var stream=new VegetationStream(field); + var camera=new DVector3(1,.3,.2).Normalized()*(field.Planet.RadiusMeters+1.8); + var deadline=DateTime.UtcNow.AddSeconds(10); + do + { + stream.Update(camera); + Assert.InRange(stream.PendingTiles,0,8); + Assert.InRange(stream.Tiles.Count()+stream.PendingTiles,0,field.Planet.Vegetation!.MaximumCachedTiles); + if(stream.PendingTiles==0)break; + await Task.Delay(2); + }while(DateTime.UtcNowt.Key.Layer==0).Select(t=>t.Origin.Normalized()*camera.Length-camera).ToArray(); + Assert.Contains(offsets,d=>DVector3.Dot(d,forward)>200); + Assert.Contains(offsets,d=>DVector3.Dot(d,forward)<-200); + var revision=stream.Revision;var instances=stream.InstanceCount; + // More than the former 120-frame expiry: every direction remains resident. + for(var frame=0;frame<500;frame++)stream.Update(camera); + Assert.Equal(revision,stream.Revision);Assert.Equal(instances,stream.InstanceCount); + Assert.Equal(0,stream.PendingTiles); + } + [Fact] + public void PaddedVegetationViewCoversSmallHeadTurnsIncludingRoll() + { + var cached=new VegetationView(new(0,0,1),new(1,0,0),new(0,1,0),.6,1.6); + var random=new Random(317); + for(var trial=0;trial<500;trial++) + { + var axis=new DVector3(random.NextDouble()-.5,random.NextDouble()-.5,random.NextDouble()-.5).Normalized(); + var angle=random.NextDouble()*3.99*System.Math.PI/180; + DVector3 Rotate(DVector3 v)=>v*System.Math.Cos(angle)+DVector3.Cross(axis,v)*System.Math.Sin(angle) + +axis*(DVector3.Dot(axis,v)*(1-System.Math.Cos(angle))); + var current=cached with {Forward=Rotate(cached.Forward),Right=Rotate(cached.Right),Up=Rotate(cached.Up)}; + Assert.True(cached.CanReuse(current)); + foreach(var x in new[]{-1d,0,1})foreach(var y in new[]{-1d,0,1}) + { + var offset=current.Forward*100+current.Right*(100*x*current.TanHalfFov*current.Aspect)+current.Up*(100*y*current.TanHalfFov); + Assert.True(cached.Intersects(offset,0),"Reusing the buffer discarded a point inside the rotated view."); + } + } + var a=5*System.Math.PI/180; + Assert.False(cached.CanReuse(cached with {Right=new(System.Math.Cos(a),System.Math.Sin(a),0),Up=new(-System.Math.Sin(a),System.Math.Cos(a),0)})); + Assert.False(cached.CanReuse(cached with {Aspect=2})); + Assert.False(cached.CanReuse(cached with {TanHalfFov=.8})); + Assert.False(cached.Intersects(new(0,0,-100),1)); + } + [Fact] + public void ForestBenchmarkLandsAtEyeHeightAndReturnsToEarthView() + { + var system=KnownSolarSystemCatalog.Load(Path.Combine(AppContext.BaseDirectory,"KnownSolarSystem","solar-systems.json")).GetById("sol"); + var earth=system.Planets.Single(p=>p.Id=="earth");var cell=UniverseScale.DefaultCellSizeMeters; + var origin=UniversePosition.AtCellCenter(system.Coordinate,cell);var body=origin.Add(earth.PositionMeters,cell); + var camera=new FlyCamera(body.Add(new(0,0,earth.RadiusMeters*4),cell),cell);var home=camera.Position;var forward=camera.Forward;var up=camera.Up; + var catalog=VegetationCatalog.Load(Path.Combine(AppContext.BaseDirectory,"Content","Vegitations","vegetation.json")); + var benchmark=new ForestBenchmark(system,earth,catalog,camera,home,forward,up); + var nearest=double.PositiveInfinity; + for(var i=0;i<961;i++) + { + benchmark.Update(.1,camera);benchmark.RecordFrame(1d/60); + if(benchmark.ElapsedSeconds is >=40 and <80) + { + var relative=camera.Position.RelativeTo(body,cell);var height=relative.Length-TerrainHeight.Sample(earth,relative.Normalized()); + Assert.InRange(height,1.798,1.802); // AU-scale sector arithmetic retains millimetre precision here. + nearest=System.Math.Min(nearest,height); + } + } + Assert.True(benchmark.IsComplete);Assert.InRange(nearest,1.798,1.802); + Assert.InRange(camera.Position.RelativeTo(home,cell).Length,0,.00001);Assert.Equal(forward,camera.Forward); + Assert.InRange(benchmark.AverageFps,59.999,60.001); + Assert.True(benchmark.PerformancePassed); + } + [Fact] + public void TransparentFoliageMipsKeepEdgeColourInsteadOfBlackHalos() + { + var image=new RasterImage(2,2,[255,20,10,255,0,0,0,0,0,0,0,0,0,0,0,0]); + var mip=PlantMipmaps.Get(image,false)[1];Assert.Equal(1,mip.Width); + Assert.Equal(new byte[]{255,20,10,63},mip.Rgba); + } +} diff --git a/tools/vegetation/download.py b/tools/vegetation/download.py new file mode 100644 index 0000000..b3be290 --- /dev/null +++ b/tools/vegetation/download.py @@ -0,0 +1,90 @@ +"""Fetch reproducible Poly Haven glTF sources and PBR ground materials (CC0).""" +import concurrent.futures, hashlib, json, pathlib, urllib.request, time + +ROOT = pathlib.Path(__file__).resolve().parents[2] +DEST = ROOT / 'Content' / 'Vegitations' +ASSETS = ['pine_tree_01', 'fir_tree_01', 'tree_small_02', 'fir_sapling_medium', + 'shrub_01', 'shrub_04', 'fern_02', 'grass_medium_01', 'grass_medium_02', 'weed_plant_02'] +HEADERS = {'User-Agent': 'UniverseVegetationImporter/1.0 (Poly Haven CC0 asset preparation)'} + +def request(url): + return urllib.request.urlopen(urllib.request.Request(url, headers=HEADERS), timeout=120) + +def md5(path): + h = hashlib.md5() + with path.open('rb') as f: + for block in iter(lambda: f.read(4*1024*1024), b''): h.update(block) + return h.hexdigest() + +def fetch(item): + path = DEST / item['path'] + if not path.resolve().is_relative_to(DEST.resolve()): raise RuntimeError('Asset path escapes content folder') + path.parent.mkdir(parents=True, exist_ok=True) + if path.exists() and path.stat().st_size == item['size'] and md5(path) == item['md5']: + return + partial = path.with_suffix(path.suffix + '.partial') + for attempt in range(4): + try: + with request(item['url']) as response, partial.open('wb') as output: + received = 0 + while True: + block = response.read(1024*1024) + if not block: break + received += len(block) + if received > item['size']: raise RuntimeError('Download exceeds declared size: '+str(path)) + output.write(block) + if partial.stat().st_size != item['size'] or md5(partial) != item['md5']: + raise RuntimeError('Checksum/length mismatch: ' + str(path)) + partial.replace(path) + print('Downloaded', item['path'], round(item['size']/1048576, 1), 'MiB', flush=True) + return + except Exception: + if attempt == 3: raise + time.sleep(attempt + 1) + +def main(): + DEST.mkdir(parents=True, exist_ok=True) + manifest_path = DEST / 'sources.json' + if manifest_path.exists(): + manifest = json.loads(manifest_path.read_text()) + else: + files = [] + for asset in ASSETS: + with request('https://api.polyhaven.com/files/' + asset) as f: data = json.load(f) + model = data['gltf']['2k']['gltf'] + entries = {'model.gltf': model, **model.get('include', {})} + for relative, entry in entries.items(): + files.append({**{k: entry[k] for k in ('url','size','md5')}, 'path': asset + '/source/' + relative}) + # Ground materials share the same biome field as the plants. + for asset in ['forest_ground_04', 'withered_grass', 'snow_02']: + with request('https://api.polyhaven.com/files/' + asset) as f: data = json.load(f) + for kind in ['diff', 'nor_gl', 'rough']: + key = next(k for k in data if k.lower() in ({'diff': ['diff','diffuse'], 'rough':['rough','roughness'], 'nor_gl':['nor_gl']}[kind])) + entry = data[key]['2k']['jpg'] + files.append({**{k:entry[k] for k in ('url','size','md5')}, 'path':'ground/'+asset+'/'+kind+'.jpg'}) + manifest = {'provider':'Poly Haven', 'license':'CC0', 'source':'https://polyhaven.com', 'files':files} + size = sum(f['size'] for f in files) + if size > 2_000_000_000: raise RuntimeError('Original asset budget exceeded: '+str(size)) + manifest_path.write_text(json.dumps(manifest, indent=2)+'\n') + if not manifest.get('foliageMasksIncluded'): + for asset in ASSETS: + with request('https://api.polyhaven.com/files/' + asset) as f: data = json.load(f) + for kind, variants in data.items(): + if 'alpha' not in kind.lower() and 'dry_diff' not in kind.lower(): continue + formats=variants.get('2k',{}) + extension='png' if 'png' in formats else 'jpg' + if extension not in formats: continue + entry=formats[extension] + manifest['files'].append({**{k:entry[k] for k in ('url','size','md5')}, + 'path':asset+'/source/masks/'+kind+'.'+extension}) + manifest['foliageMasksIncluded']=True + if sum(f['size'] for f in manifest['files'])>2_000_000_000: raise RuntimeError('Asset budget exceeded') + manifest_path.write_text(json.dumps(manifest,indent=2)+'\n') + print('Poly Haven CC0 assets:', len(manifest['files']), 'files,', round(sum(f['size'] for f in manifest['files'])/1048576), 'MiB', flush=True) + if any(f['size']<=0 for f in manifest['files']) or sum(f['size'] for f in manifest['files'])>2_000_000_000: + raise RuntimeError('Source download budget exceeded') + with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: + list(executor.map(fetch, manifest['files'])) + print('All source checksums verified.', flush=True) + +if __name__ == '__main__': main() diff --git a/tools/vegetation/package-lock.json b/tools/vegetation/package-lock.json new file mode 100644 index 0000000..541f4d3 --- /dev/null +++ b/tools/vegetation/package-lock.json @@ -0,0 +1,738 @@ +{ + "name": "universe-vegetation-preparation", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "universe-vegetation-preparation", + "dependencies": { + "@gltf-transform/core": "^4.5.1", + "@gltf-transform/extensions": "^4.5.1", + "@gltf-transform/functions": "^4.5.1", + "meshoptimizer": "^1.3.0", + "sharp": "^0.35.5" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@gltf-transform/core": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@gltf-transform/core/-/core-4.5.1.tgz", + "integrity": "sha512-OWgBYssKHKoXMIrMb2N20jXqWRnMP9HdtcTFQBEAJtz4P+OOxx5fozl5eAeK7nktIs5uDE/lu4NbkE5VCZaR9g==", + "license": "MIT", + "dependencies": { + "property-graph": "^4.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/donmccurdy" + } + }, + "node_modules/@gltf-transform/extensions": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@gltf-transform/extensions/-/extensions-4.5.1.tgz", + "integrity": "sha512-3jTe+GWRy4fj1TU790LPsO+rpO/4byUWPTpNkjy6M6h8RvrUivRf12Mptc0770K4ZczibpJDNHXZFkFsFMRDLA==", + "license": "MIT", + "dependencies": { + "@gltf-transform/core": "^4.5.1", + "ktx-parse": "^1.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/donmccurdy" + } + }, + "node_modules/@gltf-transform/functions": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@gltf-transform/functions/-/functions-4.5.1.tgz", + "integrity": "sha512-rfm4nQY2DJ+ip1e15xp7YfQwhRno3kPLcGw4EZiRk1dVwMV3cdFMcF08glYcO/+oLyYkUEs3xXMJYXa0cPgcTg==", + "license": "MIT", + "dependencies": { + "@gltf-transform/core": "^4.5.1", + "@gltf-transform/extensions": "^4.5.1", + "ktx-parse": "^1.1.0", + "ndarray": "^1.0.19", + "ndarray-lanczos": "^0.3.0", + "ndarray-pixels": "^5.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/donmccurdy" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@types/ndarray": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/ndarray/-/ndarray-1.1.0.tgz", + "integrity": "sha512-go+C6vHBUuO9jH6Tj6qEZ63JEGoMSxFdmd+6QAtthGD6xumBfIz3RbNDDs2aoRVLf9naH8lk5jnWy2UHHKgYQQ==", + "license": "MIT" + }, + "node_modules/cwise-compiler": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cwise-compiler/-/cwise-compiler-1.1.3.tgz", + "integrity": "sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==", + "license": "MIT", + "dependencies": { + "uniq": "^1.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/iota-array": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz", + "integrity": "sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==", + "license": "MIT" + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" + }, + "node_modules/ktx-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ktx-parse/-/ktx-parse-1.1.0.tgz", + "integrity": "sha512-mKp3y+FaYgR7mXWAbyyzpa/r1zDWeaunH+INJO4fou3hb45XuNSwar+7llrRyvpMWafxSIi99RNFJ05MHedaJQ==", + "license": "MIT" + }, + "node_modules/meshoptimizer": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.3.0.tgz", + "integrity": "sha512-2oZqMAcEEiCtXWUjIGtZ71oPx1B0ZxYGTqIKsXB1wzWFhleNsCFxQuUJzgCV706bRcwPWtMGf1odmQvAcqKkIw==", + "license": "MIT" + }, + "node_modules/ndarray": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ndarray/-/ndarray-1.1.1.tgz", + "integrity": "sha512-AsE8w/M2MS+0pD3nP5qbRRtJFeJJJaJ+qXZaZr0XjVGXhy6LIj2TERTlkpdK2AI3WufoOP+pwPk3q2ilAJcZ9g==", + "license": "MIT", + "dependencies": { + "iota-array": "^1.0.0", + "is-buffer": "^1.0.2" + } + }, + "node_modules/ndarray-lanczos": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/ndarray-lanczos/-/ndarray-lanczos-0.3.0.tgz", + "integrity": "sha512-5kBmmG3Zvyj77qxIAC4QFLKuYdDIBJwCG+DukT6jQHNa1Ft74/hPH1z5mbQXeHBt8yvGPBGVrr3wEOdJPYYZYg==", + "license": "MIT", + "dependencies": { + "@types/ndarray": "^1.0.11", + "ndarray": "^1.0.19" + } + }, + "node_modules/ndarray-ops": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ndarray-ops/-/ndarray-ops-1.2.2.tgz", + "integrity": "sha512-BppWAFRjMYF7N/r6Ie51q6D4fs0iiGmeXIACKY66fLpnwIui3Wc3CXiD/30mgLbDjPpSLrsqcp3Z62+IcHZsDw==", + "license": "MIT", + "dependencies": { + "cwise-compiler": "^1.0.0" + } + }, + "node_modules/ndarray-pixels": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ndarray-pixels/-/ndarray-pixels-5.2.0.tgz", + "integrity": "sha512-lTh4tFKziAatVTa9crIsidUyn+lqujVOQpzfdBWvdFu2wo9Uo6z261lVX7SgMyP89xGmj3TMTPbbxl9YDnV4SA==", + "license": "MIT", + "dependencies": { + "@types/ndarray": "^1.0.14", + "ndarray": "^1.0.19", + "ndarray-ops": "^1.2.2", + "sharp": "^0.35.0" + } + }, + "node_modules/property-graph": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/property-graph/-/property-graph-4.1.0.tgz", + "integrity": "sha512-AvPcP7XECNWy4LGmFQ77k7un4lSKM4eS29PTvW4ck95uYeLxXPWJM7hLuBqK91FaHqCcgJvIUCuNJjjxKE7VKQ==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.9.1.tgz", + "integrity": "sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==", + "license": "0BSD", + "optional": true + }, + "node_modules/uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==", + "license": "MIT" + } + } +} diff --git a/tools/vegetation/package.json b/tools/vegetation/package.json new file mode 100644 index 0000000..5941d3b --- /dev/null +++ b/tools/vegetation/package.json @@ -0,0 +1,15 @@ +{ + "name": "universe-vegetation-preparation", + "private": true, + "type": "module", + "scripts": { + "prepare": "node --max-old-space-size=16384 prepare.mjs" + }, + "dependencies": { + "@gltf-transform/core": "^4.5.1", + "@gltf-transform/extensions": "^4.5.1", + "@gltf-transform/functions": "^4.5.1", + "meshoptimizer": "^1.3.0", + "sharp": "^0.35.5" + } +} diff --git a/tools/vegetation/prepare.mjs b/tools/vegetation/prepare.mjs new file mode 100644 index 0000000..70421a2 --- /dev/null +++ b/tools/vegetation/prepare.mjs @@ -0,0 +1,143 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {NodeIO, Document, getBounds} from '@gltf-transform/core'; +import {ALL_EXTENSIONS} from '@gltf-transform/extensions'; +import {prune, dedup, flatten, transformMesh, cloneDocument, mergeDocuments, simplify, compactPrimitive, unpartition} from '@gltf-transform/functions'; +import {MeshoptSimplifier} from 'meshoptimizer'; +import sharp from 'sharp'; + +const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'../../Content/Vegitations'); +const manifest=JSON.parse(await fs.readFile(path.join(root,'sources.json'),'utf8')); +const ids=[...new Set(manifest.files.filter(f=>f.path.endsWith('/source/model.gltf')).map(f=>f.path.split('/')[0]))]; +const io=new NodeIO().registerExtensions(ALL_EXTENSIONS); +await MeshoptSimplifier.ready; +for(const id of ids) { + const selected=process.argv.slice(2).filter(a=>!a.startsWith('--')); + if(selected.length&&!selected.includes(id))continue; + const out=path.join(root,id,'prepared');await fs.mkdir(out,{recursive:true}); + const cards=JSON.parse(await fs.readFile(path.join(out,'cards.json'),'utf8').catch(()=>'null')); + const sourceOnly=process.argv.includes('--source-only'); + const stamp=JSON.stringify({version:5,cards:!!cards,files:manifest.files.filter(f=>f.path.startsWith(id+'/')).map(f=>f.md5)}); + if(sourceOnly&&await fs.stat(path.join(root,id,'source/bake-source.glb')).catch(()=>null))continue; + if(!sourceOnly&&await fs.readFile(path.join(out,'stamp.json'),'utf8').catch(()=>null)===stamp){console.log('Cached',id);continue;} + console.log('Preparing',id); + const doc=await io.read(path.join(root,id,'source/model.gltf')); + await doc.transform(flatten()); + const scene=doc.getRoot().getDefaultScene()??doc.getRoot().listScenes()[0]; + const all=scene.listChildren(); + const chosen=all.length<=3?all:[all[0],all[Math.floor(all.length/3)],all[Math.floor(all.length*2/3)]]; + for(const node of all)if(!chosen.includes(node))node.dispose(); + // Apply source transforms and give each variant a gravity-aligned, bottom-centred origin. + for(let i=0;iv+(card.Right[k]*x+card.Up[k]*y)*card.Size)); + norm.push(...card.Normal); + uv.push(card.Uv[0]+(x+.5)*card.Uv[2],card.Uv[1]+(.5-y)*card.Uv[3]); + } + indices.push(index,index+1,index+2,index,index+2,index+3); + } + const buffer=doc.getRoot().listBuffers()[0]; + const accessor=(type,array)=>doc.createAccessor().setType(type).setArray(array).setBuffer(buffer); + mesh.addPrimitive(doc.createPrimitive().setAttribute('POSITION',accessor('VEC3',new Float32Array(pos))) + .setAttribute('NORMAL',accessor('VEC3',new Float32Array(norm))).setAttribute('TEXCOORD_0',accessor('VEC2',new Float32Array(uv))) + .setIndices(accessor('SCALAR',new Uint32Array(indices))).setMaterial(material)); + } + await doc.transform(prune()); + const masks=manifest.files.filter(f=>f.path.startsWith(id+'/source/masks/')&&f.path.toLowerCase().includes('alpha')); + for(const material of doc.getRoot().listMaterials()) { + const name=material.getName().toLowerCase(); + if(name==='baked_canopy')continue; + const alpha=masks.find(f=>path.basename(f.path).toLowerCase()==='alpha.png'||name.includes(path.basename(f.path).toLowerCase().replace('_alpha.png',''))); + const foliage=material.getAlphaMode()!=='OPAQUE'||/twig|leaves|grass|fern|shrub|weed/.test(name); + material.setMetallicFactor(0).setDoubleSided(foliage); + if(foliage&&alpha&&material.getBaseColorTexture()) { + const texture=material.getBaseColorTexture(); + const info=await sharp(texture.getImage()).metadata(); + const a=await sharp(await fs.readFile(path.join(root,alpha.path))).resize(info.width,info.height).extractChannel(0).toBuffer(); + const rgba=await sharp(texture.getImage()).removeAlpha().joinChannel(a).png().toBuffer(); + const replacement=doc.createTexture(name+'_alpha').setImage(rgba).setMimeType('image/png'); + material.setBaseColorTexture(replacement).setAlphaMode('MASK').setAlphaCutoff(.4); + } + } + const tree=/tree|sapling/.test(id),budgets=tree?[12000,2000,350]:/shrub/.test(id)?[12000,3000,600]:[2500,800,200]; + const merged=new Document();const stats=[]; + for(let lod=0;lod<3;lod++) { + const copy=cloneDocument(doc),sc=copy.getRoot().listScenes()[0];sc.setName('LOD'+lod); + for(const node of sc.listChildren()) { + const primitives=node.getMesh().listPrimitives(); + const total=primitives.reduce((n,p)=>n+(p.getIndices()?.getCount()??p.getAttribute('POSITION').getCount())/3,0); + const canopy=primitives.filter(p=>p.getMaterial().getName()==='baked_canopy').reduce((n,p)=>n+p.getIndices().getCount()/3,0); + for(const p of primitives) { + const count=p.getIndices()?.getCount()??p.getAttribute('POSITION').getCount(); + const target=p.getMaterial().getName()==='baked_canopy'?count:Math.max(6,Math.floor((budgets[lod]-canopy)*count/Math.max(1,total-canopy)/3)*3); + if(count<=target)continue; + // The library performs topology/attribute-preserving reduction first. + // simplify() acts on the whole document below; retain the target per primitive for a bounded fallback. + p.setExtras({...p.getExtras(),target}); + } + } + let original=0;for(const m of copy.getRoot().listMeshes())for(const p of m.listPrimitives())original+=p.getIndices().getCount()/3; + const retained=[]; + for(const mesh of copy.getRoot().listMeshes())for(const p of [...mesh.listPrimitives()]) + if(p.getMaterial().getName()==='baked_canopy'){mesh.removePrimitive(p);retained.push([mesh,p]);} + await copy.transform(simplify({simplifier:MeshoptSimplifier,ratio:Math.min(1,budgets[lod]*chosen.length/original),error:.01*(lod+1),lockBorder:false})); + for(const [mesh,p] of retained)mesh.addPrimitive(p); + for(const m of copy.getRoot().listMeshes())for(const p of m.listPrimitives()) { + const indices=new Uint32Array(p.getIndices().getArray()),positions=new Float32Array(p.getAttribute('POSITION').getArray()); + const target=p.getExtras().target; + if(target&&indices.length>target*1.1) { + let [reduced]=MeshoptSimplifier.simplify(indices,positions,3,target,1,['Permissive','Prune']); + if(reduced.length>target*1.1)[reduced]=MeshoptSimplifier.simplifySloppy(indices,positions,3,null,target,1); + p.setIndices(copy.createAccessor().setType('SCALAR').setArray(reduced).setBuffer(copy.getRoot().listBuffers()[0])); + compactPrimitive(p); + } + p.setExtras({}); + } + await copy.transform(prune()); + let triangles=0;for(const m of copy.getRoot().listMeshes())for(const p of m.listPrimitives())triangles+=p.getIndices().getCount()/3; + stats.push(triangles);mergeDocuments(merged,copy); + } + merged.getRoot().setDefaultScene(merged.getRoot().listScenes()[0]); + await merged.transform(dedup(),unpartition(),prune()); + for(const ext of merged.getRoot().listExtensionsUsed())ext.dispose(); + await io.write(path.join(out,'model.glb'),merged); + const dryFile=manifest.files.find(f=>f.path.startsWith(id+'/source/masks/dry_diff')); + if(dryFile) { + const dry=cloneDocument(merged); + const mask=masks[0]; + const image=await sharp(await fs.readFile(path.join(root,dryFile.path))).metadata(); + const a=await sharp(await fs.readFile(path.join(root,mask.path))).resize(image.width,image.height).extractChannel(0).toBuffer(); + const rgba=await sharp(await fs.readFile(path.join(root,dryFile.path))).removeAlpha().joinChannel(a).png().toBuffer(); + const texture=dry.createTexture('dry_grass').setImage(rgba).setMimeType('image/png'); + for(const m of dry.getRoot().listMaterials())m.setBaseColorTexture(texture); + await dry.transform(prune());await io.write(path.join(out,'dry.glb'),dry); + } + const variantBounds=chosen.map(n=>getBounds(n)); + await fs.writeFile(path.join(out,'preparation.json'),JSON.stringify({source:'https://polyhaven.com/a/'+id,license:'CC0',budgets,triangles:stats,variants:variantBounds},null,2)); + await fs.writeFile(path.join(out,'stamp.json'),stamp); + console.log('Prepared',id,stats); +}