Low poly terrein generatie

Gemaakt door Luca
Inhoudsopgave
Introductie
In dit rapport moet duidelijk worden hoe je dit zou kunnen aanpakken en hoe ik dit kan gaan gebruiken in mijn game. Het doel van de generator is dat hij aansluitend terrein maakt wat er goed uit ziet met plaatsen van objecten. Denk daarbij aan bomen en rotsen. In de game waar ik deze generator zou willen gebruiken moeten ook dieren voorkomen en rivieren waar ze uit kunnen drinken maar naar de dieren ga ik niet kijken aangezien ik maar weinig tijd heb. Ook wil ik graag een water mesh in de wereld hebben om zo het water te simuleren. En als ik nog tijd over heb wil ik alles naar een compute shader maken maar ik weet niet hoe dat werkt dus daar ga ik waarschijnlijk ook niet aantoekomen.
1. Mesh
In eerste instantie, zoals ook in de opdracht heb ik gekeken naar een mesh op basis van een grid dus een vaste afstand tussen de punten. Dit maakt triangulation erg makkelijk door gewoon de vaste regel te volgen. Maar na de opdracht en nog verder expirimenteren na de opdracht heb ik gekozen dat ik wil gaan kijken naar het posseidon algortime om te punten te plaatsen om op die manier een meer low poly stijl te creëren. Daar ga in de volgende hoofdstukken verder op in.
1.1 Vertex

Eerst plaats ik volgens het poisson disk algortime vertecies, dit geeft het resultaat zoals je hierboven ziet. Zoals ook al gezegd door Stray (2017) gaf dit een beter resultaat dan op basis van een grid. Dit was nog steeds wat saai in mijn ogen en na het artikel verder te lezen bleek dat ook hij dat vond. Dus werden er vervolgens nog meer vertecies toegevoegd op random posities. Dit resultaat leverde al goede punten op.
De poisson disk sampling werkt door een 2d list te gebruiken en in elk vakje van de 2d list mag maar 1 punt zitten. En de grote tussen die cellen wordt bepaald door een gegeven waarde aan het begin van de functie. Het eerste punt word random gekozen en vervolgens in het raster geplaatst. Daarna worden er een aantal punten er om heen gepakt. Ik gebruik 6 punten er om heen. En voor al deze punten wordt gekeken of er een punt al in het grid is. Zo niet dan wordt dat punt het nieuwe punt in die cel. Zo wel dan mag het nieuwe punt er niet in. En vervolgens beginnen we weer bij het volgende punt. Dit is voor zo ver ik het snap hoe het in elkaar zit.

Vervolgens na werken aan Triangulation, zoals beschreven in het volgende hoofdstuk, merkte ik dat er gaten zaten tussen chunks. Om dit op te lossen had ik bedacht dat er langs de rand op vaste punten meer vertecies moesten worden toegevoegd. Dit was een kwestie van twee for loops die elke paar meter een nieuw punt toevoegen. Dit werkte al beter alleen zat ik nog steeds met van die gaten. Dus toen heb ik nog een restricitie toegevoegd dat er geen punten random of posseidon punten komen binnen een bepaalde afstand van de randen, om op die manier te voorkomen dat bij triangulation er een gat komt te zitten aan de rand.

Hier onder zie je het stukje waar ik de verschillende vertecies toevoeg. Dit doe ik door ze toe te voegen een een polygon, welke onderdeel is van de Triangle.NET bibliotheek.
// Add uniformly-spaced points foreach (Vector2 sample in sampler.Samples()) { polygon.Add(new Vertex((double)sample.x, (double)sample.y)); } // Add some randomly sampled points for (int i = 0; i < randomPoints; i++) { polygon.Add(new Vertex(Random.Range(minPointRadius/6, xsize - minPointRadius/6), Random.Range(minPointRadius/6, ysize - minPointRadius/6))); } // Creating the border to prevent gaps. for (int x = 0; x < xsize + 1; x += borderSpacing) { polygon.Add(new Vertex(x,0)); polygon.Add(new Vertex(x,ysize)); } for (int z = borderSpacing; z < ysize; z += borderSpacing) { polygon.Add(new Vertex(0 ,z)); polygon.Add(new Vertex(xsize,z)); }
1.2 Triangulation

Ik maak voor triangulation gebruik van een bibliotheek. Op advies van Stray (2017) heb ik gebruik gemaakt van de Triangle.NET bibliotheek. Die werkte in eerste instantie niet, zoals al gezegd door Stray dus ik heb zijn stappen gevolgd om het te fixen. Nadat ik de vertecies heb toegevoegd kan ik gebruik gaan maken van de Triangle.NET functie Triangulate.
TriangleNet.Meshing.ConstraintOptions options = new TriangleNet.Meshing.ConstraintOptions() { ConformingDelaunay = true }; mesh = (TriangleNet.Mesh)polygon.Triangulate(options);

1.3 UV mapping en de normaal
Normaal berekenen
Voor het berekenen van het normaal pak ik alle punten uit een driehoek. En vervolgens voeg ik die toe aan een cross product om op die manier de normaal van de driehoek uit te rekenen. Welke ik vervolgens aan een lijst met normals toevoeg en daarna voeg ik die toe aan de mesh. Dit is volgens de scripting API van Unity Technologies (2020)
Uv map

Aan het begin is dit het resultaat van als de chunk klaar is. Je ziet wel al duidelijk het terrein wat hiervoor is gegenereerd. Maar omdat dit er heel raar uit ziet wou ik ook het terrein kleuren. In eerste instantie dacht ik dat ik gebruik kon maken van materialen. Maar dat heb ik later tijdens het testen afgeschreven en ben ik overgestapt op het gebruik maken van één texture met verschillende kleuren en daarna een uv coordinate aangeven.
Ik bereken de juiste kleur op basis van de hoogte en soms ook op basis van de hoek die de normaal heeft tenopzichte van een vector die recht omhoog gaat. Hier onder zie je een stukje van de functie die dat doet. Het script begint met een base uv, namelijk sneeuw. Vervolgens werkt hij omlaag. Zo zie je bij de eerste dat hij kijkt of de vertex rots moet worden als de angle lager is dan 50. En vervolgens kijkt ie alles onder een bepaald niveau moet rots worden.
public Vector2 GetUvCoordinates(Vector3 vertex, Vector3 normal) { Vector2 uv = new Vector2(0.9f,0); if (vertex.y > maxRockLevel - 5 && vertex.y < maxRockLevel) { if (Vector3.Angle(normal, Vector3.up) < 50) uv = new Vector2(0.7f,0); } if (vertex.y < maxRockLevel - 5) { uv = new Vector2(0.7f,0); } return uv; }

2. Ruis

Ik ben in de eerste fase van dit project begonnen met een soort noise, met als enige doel bepalen van de hoogte van een punt. Dit gaf een prima resultaat maar ik wou meer. Boven deze alinea zie je hoe het er in eerste instantie uitzag, wel waren er al meerdere octaven maar je ziet wel dat het maar een soort ruis is. Dus vervolgens ben ik verder gaan kijken hoe ik dat kon gaan doen. Toen ben ik uitgekomen na wat googlen en brainstormen. Uitgekomen op het combineren van verschillende noise maps. Dus toen ging ik aan de slag.
2.1 De aanpak
Ik begon met een vaste formule die een combinatie maakte van verschillende noise types. Hier onder zie je een code snippit van de formule die ik heb gebruikt. Als je op deze link klikt dan kom je op een desmos grafiek uit die laat zien hoe de formule werkt.
double l = elevation; double y = 0; for (int i = 0; i < biomes.Length; i++) { if ((i - 1f) / biomes.Length <= l && l <= (i + 1f) / biomes.Length) y += (-math.abs(biomes.Length * l - i) + 1) * biomes[i].BiomeNoise(this.transform, vert, seed); }
Met hieronder een resultaat van een combinatie van 9 biomes. Dit omdat het eigenlijk niet goed genoeg werkte om een mooie overgang te creeren. Om dit op te lossen ben ik gaan kijken naar animatie grafiekken van unity. In de hoop het zo beter te kunnen laten overlopen. De rechter foto is nog maar met 3 biomes namelijk. Dus dat is gelukt.

2.2 Hoofd ruis
Ik maak dus gebruik van een simplex noise als basis, welke een getal tussen -1 en 1 terug geeft. Om die goed te kunnen gebruiken in de animatie grafiek gebruik in een unity functie Unlerp die er voor zorgt dat het getal weer tussen 0 en 1 komt. Voor de noise maak ik gebruik van de vertex positie, de positie van de chunk en een ruis schaal om daarmee de biome grote te bepalen.
float sample = noise.snoise(new float2( (float)(vert.x + this.transform.position.x) * noiseScale / (float)3000 * frequency, (float)(vert.y + this.transform.position.z) * noiseScale / (float)3000 * frequency) ); elevation = math.unlerp(-1, 1, sample);
Hierna maak ik gebruik van de animatie grafiek waar we het eerder over hadden. Om te bepalen hoeveel procent van het bepaalde terrein ruis ik ga gebruiken. Ik bepaal namelijk de hoogte van het biome en door die te vermenigvuldigen met de waarde van de animatie grafiek krijg je een nette blend.
double value = biomes[i].blendCurve.Evaluate((float) elevation); if (value != 0) { y += value * biomes[i].BiomeNoise(this.transform, vert, seed) * (biomes[i].elevationScale * value); }

2.3 Terrein ruis
Het terrein ruis is iets moeilijker, maar vooral omdat ik voor het terrein gebruik maak van octaves om daarmee een gedetailieerder terrein te creeren. Door de octaven elke keer een andere frequentie te geven, resulteert dit in verschillende niveaus van detail. Waardoor je dus steeds ruiger terrein krijgt. Zo zie je hieronder links een terrein met 7 octaven en rechts een terrein met 3 octaven. Naast dat het voor meer detail zorgt, zorgt het ook voor meer hoogte aangezien de waarde er bij wordt opgeteld.

Iets waar ik ook tegen aan liep, was een min getal tot de macht verheffen met een komma getal. Dit resulteerde tot een onecht getal. Dus dan deed Unity er niks mee en ging verder met het volgende getal. Om dat op te lossen heb ik gebruik moeten maken van de System.Numerics.Complex functies waarmee ik dit wel kon doen.
biomeElevation = math.pow(biomeElevation, redistribution); // vs biomeElevation = Complex.Pow(biomeElevation, redistribution).Real;
3. Objecten plaatsen
Om het terrein levendiger te laten lijken heb ik bedacht dat ik ook nog bomen en rotsen wil plaatsen. Dit zorgt er ook voor dat ik in de toekomst makkelijk verder kan gaan met het plaatsen van dieren in de juiste regios. Om zo in mijn game ook het overleven echt mogelijk te maken.
3.1 Vochtigheid ruis
Net als bij het terrein noise, maak ik gebruik van de Mathematics bibliotheek van unity. Hier onder zie je een code snippit die wordt aangeroepen vanuit het script dat de objecten plaats. Deze berekent een vochtigheid waarde en vervolgens checkt ie voor elk biome of het de juiste hoogte heeft, en het juiste vochtheids niveau.
private void CheckBiome(Vector3 hitPoint, Transform parent) { float moistureValue = noise.snoise(hitPoint * new float3(NoiseScale)); foreach (Biomes biome in biomesList) { if (hitPoint.y < biome.MaximumHeight && hitPoint.y > biome.MinimumHeight) { if (moistureValue < biome.MaximumMoistureValue && moistureValue > biome.MinimumMoistureValue) { GameObject newObject = Instantiate(biome.ObjectList[Random.Range(0, biome.ObjectList.Count - 1)], hitPoint, Quaternion.Euler(0, Random.Range(0f, 360f),0), parent); } } } }
Vervolgens wordt er een nieuwe instantie aangemaakt en op de juiste positie te geplaatst. Door gebruik te maken van de hoogte en vochtigheid kan ik vegetatie zones maken zoals in het plaatje hier onder.

3.2 Vegetatie Script
Op basis van de hoogte en vochtigheid van de grond worden een bepaald object geplaats. Zo heb je waar het minder vochtig en laag is bomen met blaadjes omdat die dieper de grond in gaan. Terwijl daar waar het minder vochtig is en hoog heb je dennen bomen omdat die wortels diep gaan en dus ook in rotsen vast kunnen zitten. Terwijl daar waar het vochtig is heb je gras landschap.

Hier boven zie je hoe een van de biomes in elkaar zit. Je ziet het bereik voor vochtigheid en hoogte en welke objecten geplaatst moeten worden. Dit heeft geresulteert in iets wat er in mijn ogen goed uit ziet en bruikbaar is. Daarnaast is het ook makkelijk om extra biomes toe te voegen. Wel zou ik later nog willen kijken of er een manier is om bijvoorbeeld een bos te maken wat dicht op elkaar zit of juist ver van elkaar af. Nu is de spacing overal het zelfde. Zowel van bijvoorbeeld een grasland als dat van bomen.
4. Oneindige wereld
Dit is een stukje waar ik veel aan heb gewerkt, in eerste instantie leek het me gewoon een leuk stukje om er bij te hebben. De eerste chunk loader die ik heb geschreven maakte gebruik van twee lijsten, eentje met ongeladen en eentje met geladen chunks. Vervolgen runde ik een loop waarin ik checkte welke chunks geladen zouden moeten worden op basis van de afstand. Dit koste heel veel tijd omdat naar mate je meer chunks laad, wordt je loop steeds langer.
Dus na wat googlen kwam ik uit op een tutorial van Sebastian Lague (2016), waarin hij een Dictionary gebruikte waarin hij de chunks opsloeg. Dus na het doorkijken van het tutorial ging ik aan de slag. Toen dat af was merkte ik op dat zodra ik objecten ging plaatsen het erg langzaam werd. Dit kwam omdat ik alles geladen chunks uitzetten aan het begin van het frame. Om vervolgens de meeste weer aan te zetten. Door elke frame alles uit te zetten, voorkwam ik dat als je verder liep de chunk geladen bleef. Dit verlaagde mijn frames naar 2 per seconden. Dus ik moest een manier vinden om het niet meer elk frame aan of uit te zetten. Om dit op te lossen besloot ik meer chunks te updaten. Dus als je kijkt naar de afbeelding hier onder, zou ik eerst alleen groen laden maar nu check ik ook elk frame of de chunks er om heen, dus blauw, voor de afstand tussen de speler en een rand van de chunk. Dit zorgde er voor dat alles een stuk sneller werd en dit maakte het spel ook weer speelbaar. Terwijl voorheen was het niet meer speelbaar.

Hier onder zie je de chunk lader aan het werk. Je ziet nu over het algemeen 21 chunks geladen. Hij zorgt er voor dat alle chunks die zichtbaar zouden zijn in een cirkel van 1024 meter zichtbaar is.

5. Conclusie

Ik ben over het algemeen best tevreden met hoe het is beeindigd. Nu ik er op terug kijk had ik wel minder tijd mogen spenderen aan het oneindig laden van de wereld in de game waar ik het wil gebruiken zat je namelijk orgineel op een eiland. Dit zou betekenen dat ik ook veel makkelijker rivieren had kunnen maken want dan zou je gewoon een vaste map hebben. Mijn optimalisatie doelen van boven de 30fps blijven met normaal speelgebruik is gelukt.
Wel vind ik dat ik veel voortgang heb gemaakt waarbij ik de dingen die miste heb gemaakt of de foute dingen heb verbeterd tot ik er tevreden mee was. Jammer genoeg ben ik niet helemaal toegekomen aan al mijn doelen, maar wel de hoofd doelen. Zo ben ik niet toegekomen aan een water mesh toevoegen. Dit had niet heel veel werk hoeven zijn maar ik heb gefocust op dit er goed uit laten zien. Het water zou namelijk alleen maar een shader hoeven zijn op een plane. Maar daar heb ik de tijd niet voor gehad. De compute shader om het sneller te maken was sowieso al geen mogelijkheid omdat het heel moeilijk is als compute shader.
6. Toekomstig werk
Zoals zichtbaar in de verschillende afbeeldingen en de gif, zijn er nu geen rivieren en in mijn ogen ook nog niet voldoende gebergte. Daarnaast is het erg herhalend. Het is altijd oceaan, vlak, berg. Daar heb ik ook wel naar gekeken. Als ik hier later verder aan wil gaan werken dan wil ik gebruik gaan maken van Voronoi. Maar toen ik daar naar aan het kijken was kwam ik er niet helemaal uit hoe ik dat kon gaan toepassen op de vertecies.
Daarnaast wil ik gaan kijken hoe ik de biomes meer kan laten varieren, zoals bijvoorbeeld de biomes gebruiken om de vertecies te kleuren, in combinatie met de hoogte. Zodat ik bijvoorbeeld ook een woestijn kan maken met cactussen inplaats van alleen maar bomen of gras.
Wat belangrijk is voor als ik dit wil gebruiken in de game is dat er ook dieren komen. Die zou ik dan het liefst in de juiste gebieden plaatsen en dus onderdeel laten zijn van de verschillende vegetatie zones. Om op die manier een levendig low poly wereld te maken waar je veel in kan spelen..
7. Bronnen
- Brackeys. (2017, mei 24). GENERATING TERRAIN in Unity – Procedural Generation Tutorial [Video]. YouTube. https://www.youtube.com/watch?v=vFvwyu_ZKfU
- itsKristinSrsly. (2020, april 20). Procedurally Generated Low-Poly Terrains in Unity [Video]. YouTube. https://www.youtube.com/watch?v=sRn8TL3EKDU
- Newman, C. (2018, november 24). Generating complex, multi-biome procedural terrain with Simplex noise in PSWG. Parzivail. http://parzivail.com/procedural-terrain-generaion/
- Nordeus, E. (2020). Use math to solve problems in Unity with C# – Delaunay Triangulation | Habrador. Habrador. https://www.habrador.com/tutorials/math/11-delaunay/
- Sebastian Lague. (2018, november 23). [Unity] Procedural Object Placement (E01: poisson disc sampling) [Video]. YouTube. https://www.youtube.com/watch?v=7WcmyxyFO7o
- Sebastian Lague. (2016, maart 14). Procedural Landmass Generation (E07: Endless terrain) [Video]. YouTube. https://www.youtube.com/watch?v=xlSkYjiE-Ck
- Stray. (2017, februari 20). Delaunay Triangulation for Terrain Generation in Unity. Stray Pixels. https://straypixels.net/delaunay-triangulation-terrain/
- Unity Technologies. (2020). Unity – Scripting API: Vector3.Cross. Unity Documentation. https://docs.unity3d.com/ScriptReference/Vector3.Cross.html