Landscape Generator

Table of content

  1. Introduction
  2. Development
    2.1. A mountain of work
    2.2. Biomes and trees
    2.3. Patches and batches
    2.4. Down by the river
  3. The end
    3.1. Final product
    3.2. Final word

Introduction

Welcome to my Game Lab page! For my project, I have decided to make a procedurally generated landscape. With this landscape, I wish to generate mountains, plains, rivers, and forests.

The reason I wanted to create a PCG landscape for my project is that I feel I can always reuse it in later stages of my career. Worldbuilding can be one of the most important factors that make or break a game; therefore, I want to expand my knowledge in this area.

In the chapter Development I will be taking you through my entire process of making my procedurally generated landscape.

In chapter 2.1. A mountain of work I will be talking about the start of the project, about how I took a wrong path and my creating of the mountains.

In chapter 2.2. Biomes and trees I will be showing you my development of biomes using perlin noise and the creation of forests using poisson disc sampling.

Chapter 2.3. Will be a lot about fixing the trees I made in chapter 2.2. And about performance fixes that I did. And fixing an artifact that was bugging me since day 1.

Chapter 2.4. Tells the tale of how I made rivers flow through my terrain.

Development

2.1. A mountain of work

My goal for mountains was a realistic shaped mountain using Perlin noise and specifically to avoid simplistic cone-like structures and instead create diverse, rugged mountain ranges.

In the first week of starting the project, I decided to reuse my old code from mesh generation, as I had made a mountain in that, and I needed a mountain now too. However, the mountain had a few issues. To be blunt, it was just a cone with some random noise. I decided that in order to create actual good-looking mountains, I would have to delve deeper into my code and research Perlin noise. I first started looking again at old video’s and tutorials I had used for the mesh generation assignment and looked into how they used perlin noise.1,2,3

Here I encountered my first problem of taking the wrong approach to a problem. I decided to focus solely on the mountains first and whilst that did make the mountains look alright, it gave me huge issues when trying to implement the plains next to the mountains. In order to solve this I decided I would have to split my terrain into biomes.

Then based on what biome the area was I would throw different kinds of noise at it. For plains I used perlin noise.

And for mountains I used something called multifractal noise. Which is basically perlin noise stack on top of each other. The code below is still a very basic form of multifractal noise, but further on into the project I will make the noise more advanced after looking more into it.

private float GenerateMountainousTerrain(int x, int z)
    {
        // Calculate the position in the noise field
        float sampleX = (float)x / xSize * noiseScale;
        float sampleZ = (float)z / zSize * noiseScale;

        float height = 0;
        float amplitude = 1;
        float frequency = 1;

        for (int i = 0; i < octaves; i++)
        {
            float perlinValue = Mathf.PerlinNoise(sampleX * frequency, sampleZ * frequency);
            // Apply a power function to accentuate the peaks
            height += Mathf.Pow(perlinValue, 3) * amplitude;
            amplitude *= persistence;
            frequency *= lacunarity;
        }

        return height * mountainScale;
    }

One issue that I faced is that the height difference between the different biomes was kind of steep like a sudden two meter height difference. (Left before smoothing, right after smoothing)

In order to fix this problem I decided to loop through all vertices, look at their neighbours and get smoothed out on the heights of their neighbours and then change their height on basis of those. This allows for smooth transitions between the terrains.

And there I ended on week one with some botched mountains.

When I was making the code for the rivers, I also updated the mountains. My second iteration of the mountain, which I used for a while before I made the rivers, was a more flat table mountain.

Looking very much like this.

Whilst these mountains were decent for early development of things like biomes and trees they weren’t great visually or good enough to let rivers run through. So I had to update my mountains. To do this I figured that the reason that the mountains were so flat was because the perlin noise map I used for the mountains was a very upscaled one. This meant that the mountain was very likely to be the same height as the splotches on the map were extended over the entire mountain, by lowering the scale of the map to make it very small the scaling of the splotches was much more divided and as such the mountains became much peakier.

2.2. Biomes and trees.

My goal for biomes was to make a biome system that is random in it shape and size, yet ensures that the landscape feels dynamic and engaging.

Whilst having mountains is cool and all, they are just circles on a perlin noise plane instead of actual biomes in different shapes and sizes. To combat this I made a small list of what I wanted to achieve with biomes at that point.

1: Make biomes instead of randomly generated circles of mountains.

2: Put trees in the forest.

First, I started working on biomes. I begun by adding a new biome called ocean, I figured I would need them later for my rivers so thought I might aswell add them in now. Oceans are kinda boring they got nothing except a negative height to be deep.

The way I decided to make biome is by making a perlin noise map for each biome and where the noise is above a certain treshold would be a biome. In the end it would look something like this

Blue = Ocean, Green = Plains, Red = Forest, Gray = Mountain.

Not the same map but it shows the final result of the biomes.

With this step done I decided to add trees using Poisson Disc Sampling(PDS). Since I had no idea how PDS works I look up a tutorial and found a tutorial by sebastian lague4.

A tutorial by Sebastian Lague which helped a lot and help me create a basic variant of PDS

Sadly the tutorial gives you the basic variant of PDS before ending on a “In the next video we will do this” and then there is no next video.

Not to be defeated by a lack of given knowledge I decided to fiddle around with the system myself and with great help of ChatGPT.

Well I got trees but they are still really bad.

2.3. Patches and batches

My goal for the trees was to populate the forested areas with trees in a natural and realistic manner. Achieving a compromise between tree density, variety, and height distribution to create realistic forest ecosystems.

Okay so I had trees, but they were really bad, like actually really bad. So in order to fix this I look at all that was still wrong with them:

The poisson disc sampling was off and didn’t seem to work well.
Trees spawned at the wrong heights and sometimes in the ground.
I only had one type of tree.
Trees weren’t batched and caused a lot of frame drops.

I started by fixing the poisson disc sampling, I figured out that the major reason it didn’t work properly was because instead of making one poisson disc sampling region I made one for every forest node…
Of course this would not work right as the disc sampling of one region would not check the sampling of the other and so trees spawned in each other more often then not.

To adress this, I had to break apart my tree spawning into two parts.
I would first make the poisson disc sampling and then add the the trees in after.

The Image contains an artifact that I fixed later.

This made trees a lot better, like a lot a lot. After the disc sampling was done I decided to tackle the tree heights.

Before, the trees would often spawn in the ground, as they took the height of the vertice they were on and never compensated for the difference in terrain heights after that. Since my terrain didn’t really have a good system to figure out the height on a certain point, I had a lot of trouble trying to figure out how to fix this issue, I decided to ask chatGPT for help, which came with the solution of trying to do it through raycasts. Make a raycast from high above and measure how high the terrain is and then base the tree height off of that. This worked relatively well.

Also visible in this picture is the two types of trees, I got lush trees and pine trees for different heights.

Basically, if above a certain line trees will always be pine, then a small area where the trees are either pine or lush and below that always lush. One issue I had was a lot of lag due to the trees not being made very well. I had already made the trees have one material, but trees still had two components. So I opened up blender and combined the components. This fixed a bit of lag but not really enough. And it was still not batching. At this point I figured out I was being stupid and never made the prefab static. I had previously tried this during run time which didn’t seem to do anything, but making the prefabs static did work.

In the meantime I also sought out the error of the artifact that I had in my landscape, which was a problem of never preparing the code for the fact that every vertice line adds an extra vertice.

The artifact in question


In most grid-based systems, vertices are laid out in a grid, and each row typically has one more vertex than the previous row. The extra vertex is required to connect vertices from adjacent rows. This is why you have (xSize + 1) vertices per row. It’s essential for proper indexing of vertices because the last vertex in a row connects to the first vertex in the next row.

ChatGPT

After this small fix the artifact no longer shows up.

Another small thing I did is from a feedback point I got. When you reach the forest edge the density of trees should be less in other words fewer trees. To simulate this, I basically made every forest look to its neighbours and based on the amount of forest neighbours it had, the higher its density would be. Going from 25% density to 100% depending on its neighbours.

To make sure I got a visual aspect of this, I decided to add bushes which are basically small spheres. These would go in place of the trees where the tree density would be less. It is mostly a visual aspect, but I could at a later date change them to some more visually pleasing bushes.

2.4. Down by the river

To understand how to make rivers I first had to look at the rules of rivers. These can be mainly defined as5:
1. The end point: A river always ends in a body of water.
2. Gravitational flow: Rivers always flow from high to low as such is the way gravity will go.
3. Combined Rivers: Starting small a river can grow combing seperate smaller rivers into one large one.
4. Curving flatlands: When the land gets flatter the rivers get curvier.

Implementing these four rules into my very mediocre landscape generator might be a larger task then I first thought. For example, curvy rivers whilst looking cool aren’t that easy to conjure up. And as such have decided to do curvy rivers last to make sure I can first make normal rivers.

First I decided that rule 1 and 2 must be done first, as they are the requirement for all the other rules too a river must flow down and towards a body of water. Earlier in my program, I added the ocean saying I needed them in the future so I might aswell have made them then. And now is finally the time to use them!

To make it simple for myself I decided that the starting point of a river shall always be in the mountains. And the end point will always be in the ocean. This would ensure that the rivers would always start high and end low.
I also decided that rivers would go for the nearest body of water and would keep searching for this body on their way down. Meaning that if a river came across another body of water that was closer whilst on it’s travels it would go there.

This allowed me to easily tackle rule 1 and find the end point of the rivers.

For rule 2, the rule that states a river must always flow downhill, I had to do a bit more work. To ensure the river flowed correctly downhill, we had to find the path of least resistance, simulating the flow of water down the landscape. I initially attempted to use A* for this, hoping that with the heuristic and node cost of A*, I could make the cost determined by the height of the vertices. This way, it would seek out paths avoiding the high peaks while searching for the low points. Despite multiple code checks, trying different algorithms, and attempting an old one from another project, I couldn’t get it to work. Each time I ran it, my Unity froze and didn’t react. As the deadline approached, and with time running out, after two and a half days and probably 24 work hours, I decided to abandon the use of A* and implemented a different method to create the river path. I created a method where, at every step, the river considered the elevation of itself and its neighbors, directing the river to follow the steepest path. This approach effectively replicated gravity and allowed me to fulfill rule 2, ensuring that the river flows downhill.

After this I was left with rule 3 the combining of rivers and rule 4 the curving of rivers. For combining the rivers I decided to check for each river if it was closer to the ocean or to another river and if the river was closer to another river it would go towards the river.

The rivers (now in cyan) meeting up before going bottom right to the ocean.

The implementation of this, made it so that if a bunch of rivers got spawned next to each other. They would meet up before going to the ocean, unlike before where they would all carve their own path to the ocean.

Rule 4 curving river was very complicated and I asked another student for help as it involved math and that simply isn’t my strong suit. Before starting on this rule rivers would sometimes be really straight as their code simply told them to take a straight path to the nearest ocean.

It sometimes ended up looking more like a channel then a river as it would be really straight, whilst explaining the problem to a fellow student he came up with the solution of using a sinus curve to change the direction of the river.

Now in order to change the direction using the sinus curve I had to turn the direction Vector3 into an angle which I could modify and then change it into a Vector3 again. The code shared below was used to do this.

Vector3 AngleConvert(Vector3 direction, float angle)
    {
        Vector3 baseAngleDirection = new Vector3(1, 0, 0);

        float a = Mathf.Sqrt(baseAngleDirection .x * baseAngleDirection .x + baseAngleDirection .z + baseAngleDirection .z);
        float b = Mathf.Sqrt(direction.x * direction.x + direction.z * direction.z);
        float c = baseAngleDirection .x * direction.x + baseAngleDirection .z * direction.z;
        float directionAngle = Mathf.Acos(c / (a * b));
        directionAngle += angle;
        direction = new Vector3(Mathf.Cos(directionAngle), 0, Mathf.Sin(directionAngle));

        return direction;
    }

We begin by introducing a placeholder vector named ‘baseAngleDirection’ to help with mathematical calculations. Then, we compute the magnitudes ‘a’ and ‘b’ by utilizing ‘baseAngleDirection’ and the input direction vector. Additionally, we determine ‘c’ as the dot product between ‘test’ and the direction vector. Using these values, we calculate the direction angle between ‘test’ and the direction vector employing the arccosine function. Next, we adjust the angle by incorporating an additional angle derived from the assigned maximum and minimum curve amounts. Finally, we construct a new Vector3 direction using the modified direction angle, and this adjusted vector is returned as the result of the function.

The end result looking like this:

After the curving of the rivers was done, I decided to check if I had achieved all the goals that I set for the rivers:

  1. The End Point: A river always ends in a body of water. The endpoint for rivers would be set in the nearest ocean biome, but when a river was making its way toward that ocean, it would also sometimes check if there was another ocean closer on its journey. This made it so that it wouldn’t skip past closer oceans but would instead always seek them out.
  2. Gravitational Flow: Rivers always flow from high to low, as such is the way gravity will go. Rivers always spawned near/in the mountains, which were always the highest points on the map, and they always flowed towards the ocean, which was always the lowest point on the map.
  3. Combined Rivers: Starting small, a river can grow by combining separate smaller rivers into one large one. Every river did a check to see if it was closer to the ocean or another river. If it was closer to a river, it would meet up with said river before going to the ocean.
  4. Curving Flatlands: When the land gets flatter, the rivers get curvier. Using a sinus curve when a river was in its last part of its journey to the ocean, it would start to curve to make it more in sync with real rivers.

3. The end

3.1. Final Product

This chapter will contain some images of the final product.

3.2. Final Words

When I set out to make a procedural landscape generator, I thought that it was less of a task than it actually was. There were quite some parts where I struggled a lot, like with the rivers, for example. I had tried for a while to make my rivers using A*, but my Unity kept freezing, so in the end, I decided to give up on it. And so, in the end, I decided to stop trying to use A* and do it the way I had done before.

In retrospect, I think the creation of realistic rivers could have been its own project. In my research, I came across something called GeNa Pro, which is this terrain-building tool for Unity that can create some really advanced rivers with floodplains and everything.

https://www.youtube.com/watch?v=8kk9pLTAqcw
https://assetstore.unity.com/packages/tools/terrain/gena-pro-terrains-villages-roads-rivers-183186

It looks really advanced but also very expensive, but after seeing the work it took me to make my own rivers, I think it is probably worth it.

I could have definitely used some smarter planning while doing the project. I started with the mountains before I made the biome map, and I think if I had been smarter, I would have put more effort into biome generation. And I would 100% start with getting the biomes right if I were to remake the project.

If I were to continue with the program, I would probably start with roads, as they seem fun but also hard. Roads are like rivers; they try to follow the path of least resistance, but unlike rivers, they try to do that for both directions, as people travel over roads and going up takes more effort. So roads try to stay as flat as possible while also trying to take the path of least resistance. Roads would also need bridges over my newly made rivers, which need to be based on how big the river is. I think it could be a real challenge.

At the end of it all, looking back, while the project was hard and I am glad to be done with it, I also really enjoyed it. I am not the most proud of what I made, but I am proud nonetheless.

Sources

  1. https://catlikecoding.com/unity/tutorials/procedural-grid/
  2. https://www.youtube.com/watch?v=64NblGkAabk
  3. https://www.youtube.com/watch?v=bG0uEXV6aHQ
  4. https://www.youtube.com/watch?v=7WcmyxyFO7o
  5. https://education.nationalgeographic.org/resource/understanding-rivers/

Leave a Reply

Your email address will not be published. Required fields are marked *