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