Creating a procedurally generated background
Project created by: Tristan Mul
Table of contents
- Terrain building
- Creating a shader
- Features & customisation
- performance testing
- Future improvements
Last semester, a group of students were building a type of bullet hell game similar to Sky force: Reloaded. They managed to make a lot of progress but couldn’t really finish the project. One of the things that had to be made was to make a background that would be generated based on the amount of units spawning inside the map. They wanted to make build this background procedurally so that level design didn’t need to take a long time and could be generated in realtime.
My task in this project was to procedurally generate a background that would generate new pieces of the map for as long as the game would run. The two most important specifications were that the game had to be light, as it had to be playable on a mobile phone, and there had to be many places for tanks and turrets to spawn. In addition to that, I also wanted the background to be easy to customize so many more backgrounds could be generated with this project.
I started the project by testing the game and trying to figure out what I wanted to change about the background. I also tried to figure out what kind of research had been done on procedural background generation. Once I felt confident in what I wanted, I started the desk research and started building the background.
While Looking up other game backgrounds I noticed that most bullet hell like shooters felt a little flat on pictures. This was strange because while I played the game, it was clear that it was built in 3D. It is unclear what causes this phenomenon but I assume it might be because the background is made by hand instead of being generated procedurally.
Once the research on backgrounds was finished, I added multiple requirements for myself in order to get an idea of how I wanted the background to look. It had to feel like 3D in pictures and the game itself, the tiles that were being created had to be seamless and the tiles that were generated had to be created randomly in such a way that the background didn´t feel like it was repeating. There weren’t many constraints in terms of design from the Client. The only requirement they added was that there had to be space for turrets and tanks to spawn and provided a size chart for all of the necessary sizes. The sandy planes were ideal for this.
The type of background I decided to make was a bare planet that would be portrayed by sandy plains and tall rocklike mountains. I decided to create a moodboard in order to see what elements I would need to create the feeling of an spaceship soaring over a barren landscape. This showed that I could create the feeling I wanted by making large square rocks and large sandy dunes in combination with a reddish sun or glow.
3. Terrain building
The first step of creating a background was creating a plane that would allow me to change it’s height and thus creating mountains. Luckily I had done this before during one of the bootcamps and finished this rather quickly.
3.1 Creating one mesh
The next thing I had to do was creating a noise map based on perlin noise. Luckily I didn’t have to write the perlin algorithm myself as Unity already has a function that allows me to use perlin noise, meaning I could focus on creating the noisemap rather that creating the algorithm. This noisemap is created by looping over a specific point a number of times equal to the number of octaves that have been set, adding the value that was created to the the height of that point. Then the height is added to the noisemap in that point and can be used to create heights on the map.
Adding these heights is as simple as changing the y value of a vertex that has been created to the height of the noisemap at that point. Once you did that you can add colours based on the height in a similar way to how we changed the height of the vertexes itself. This gives a bit of a pixelated result but already makes the environment a bit nicer to look at while progressing further.
3.2 infinite meshes
Now it was time to work on creating code that could generate these meshes endlessly so I started working on a system that could simply add planes next to each other when a certain object moved around on the x or z axis. These planes had to be deactivated when they were out of the line of sight and had to be reactivated instead of recreated when they would come back in sight. Next these planes had to be exchanged with the actual meshes that had to be created. In order to do that without causing the screen to freeze as the computer was calculating the mesh data, I had to implement threading. This would allow me to calculate the data over multiple frames.
The last part of the mesh generation had to do with a couple small bugs, caused by the meshes not lining up properly. This meant we had to work on fixing the seams and thus changing how the minimal height and maximum height of the mesh was calculated. Instead of checking the minimal and maximal height of one mesh, I had to estimate the minimal and maximal height. It would mean that I couldn’t have the full range of heights within a heightmap anymore, but it would mean the meshes would connect seamlessly. Fixing these seams was the last thing I had to do in order to make the terrain generate infinitely.
4. Creating a shader
In order to make a good looking environment I would need a shader that could generate textures based on the worldposition of the mesh. I did not have a lot of experience with creating shaders but knew normal shaders could use up a lot of memory while mobile shaders would preform a lot better on mobile phones. I did not know the reason but after looking at a compiled normal shader and a compiled mobile shader I assumed the cause of this were the many lines of code that were written in order to make it adaptable for many different types of shaders.
Since I only needed one shader for one specific purpose I started to write a shader starting with a surface shader. While I was told it was heavier then an unlit mobile shader, it was still at least as light as a mobile shader.
4.1 Recreating the colours
There were a lot of variables I used when working with just changing the colours of the triangles. In order to make it look at least as good I had to recreate these values in some way within the shader. I did this by creating a texture Data asset that would send the values stored within the asset to the material that had the shader on it.
4.2 Blending the colours
Once I was back at the same level I started improving the shader, adding a blending feature to the shader. Using an inverse lerp method to calculate how strong a certain colour had to be drawn on a certain location allowed me to blend two colours nicely together. What I noticed when doing this was that I had to create this method before I actually started using it because the mesh doesn’t work otherwise. Eventually I managed to blend the colours together, creating a rather nice effect
4.3 Adding textures
Finally it was time to add textures to the shader. I started this just like I did with the other variables but by doing so I ran into 3 different problems: tiling, stretching and the fact that there was no SetTextureArray in C#.
Tiling was the easiest issue to fix. Simply scaling the texture would make the pattern less repeatable and made the tiling go away. The SetTextureArray issue wasn’t that hard to fix either, as I could simply create a method that would do something similar to the setTextureArray. The stretching issue however, was something very interesting. It was caused by projecting the 2D textures on a 3D plane. This texture can only be projected in one of the three planes but if I wanted to project the texture without stretching I had to project it on all three planes simultaniously. The solution for this was triplanar mapping. A technique that uses the world normal of a triangle and blends the texture according to the normal and return these three values. This caused the texture to be projected on the mesh without any stretching.
5. Features & customisation
One of the requirements I added for myself was to make the background highly customisable. I feel I did quite a good job with it because it is easy to change textures and change the mesh within the project. It is even possible to save the configurations you want to use, meaning that you don’t need to write the different variables down in order to remember how a certain landscape worked.
5.1 Noise customisation
One thing that hasn’t been added yet is the possible to generate different types of noise, creating different types of height maps. This would allow for even more customisation and thus allows it to be even more adaptable. The current perlin noise algorithm is still customisable however.
- Normalize mode – This is used to create a mesh that is either seamless in an endless terrain or a mesh that makes full use of the heights within a noisemap.
- Noise scale – This allows the user to ‘zoom in’ on the noise map, allowing for more
- Octaves – How many levels of detail you want the mountains to have.
- Persistance – Changes the general shape of the mountains.
- Lacunarity – Makes the map more detailed by differentiating between larger height value averages.
- Seed – A random offset on where the perlin noise needs to start. Calculated by creating a random value from a start value.
- Offset – A self chosen offset on where the perlin noise needs to start.
5.2 Mesh customisation
The mesh can be altered with much ease. It makes use of a terrain asset and a noise asset that store the data being used to create a height map. The varables within these assets are:
- Uniform scale – this allows the user to scale the size of the mesh. This can be used when the scale between the background and another asset.
- Mesh Height multiplier – This can be used to create steeper mountains.
- Mesh height curve – This is a curve that allows to user to select the specific heights at which the mountains have to become steeper. It can be used to create flat areas on the mountains.
5.3 Texture customisation
The shader uses makes use of a TextureData asset that have multiple variables in placed in layers. That way the user can change how the background looks with ease. They can change:
- Tint – Changes the colour of a certain texture to become more of the chosen tint.
- Starting height – This would be the height percentage at which the lower texture would be replaced with the new texture. If the first texture’s height is higher then 0 all of the space underneath that texture would simply be black.
- Texture blend – This changes how much the texture will bleed into the texture below the current texture.
- Texture scale – Allows the user to change the size of the texture that is being used. This can be used to make tiling of the texture less noticable.
One of the requirements for the game was that it had to preform well on a mobile phone. The goal was to consistently reach a performance of at least 60 FPS during testing. Another requirement was that the project shouldn’t drain the phone battery too quickly.
Initial testing without shaders showed that running just the code didn’t give many issues on maximum preformance, running at about 120 to 180 frames per second. This gave good hopes for testing the game on minimal preformance. Those hopes were quickly shattered as the test on minimal preformance showed that the code ran between 30 and 60 fps, which was far below the requirement we set.
Luckily there were a couple small things we could do to gain large amount of preformance. It became clear that the game only needed 2 planes to be active at times, while there were 12 active during initial testing. Scaling the active planes during runtime down to just 2 already caused us to gain a boost of around 20 fps, making our game run at 50 to 80 fps. Another thing I had done was requesting new mesh data every time the frame updated, causing huge fluctuations and spikes in preformance during runtime. Changing this to a fixed update improved the situation and made the preformance drop to a reasonable level when generating a new mesh. It could be further improved by creating a timer that runs the code fewer times every second.
Another noticable change in preformance was by changing the chader from a normal shader to a mobile shader. Standard unity shaders use about five times as many lines of code then mobile shaders, meaning that standard shaders give a huge hit in terms of preformance. Just changing the shader -improved the preformance from 50-80 fps to 60-100 fps, meaning a small improvement when more code has to run but a much larger improvement when fewer code has to run.
The second test, after creating a shader with as few lines of code as I could, showed that the preformance hadn’t really taken a hit which was a good thing. However, one thing that stood out during the builds was the huge drop in preformance when a new plane is being created. I had already tried to combat this with threading in order to split the large code that had to be generated at one specific moment and smear it out over multiple frames. It appeared that the threading wasn’t splitting the code up and simply allowed every queued request to pass trough, causing the large drop in preformance.
After looking at what I have learned during this project and what kind of a product I could create, I think I can be content with the result. There is a lot I have learned during the project, like building complex shaders, using procedural generation to create a procedurally generated terrain that could run endlessly, the basics of threading and even learning a new language in the process!
The landscape itself also looks better than I expected due to the blending options I added to the shader. I managed to make it very customisable, which is something I strived for but didn’t necessarily focus on. While I couldn’t get a one on one recreation of the design I wanted to make, I think I got close enough to the feeling I wanted the background to have.
8. Future improvements
There are still quite a few things that could be improved for the project. Most of these improvements are adding extra features but there are also smaller fixes or general improvements, like preformance, that need some closer examination.
8.1 More noise algorithms
One of the features that could be added would be Adding more different types of noise. Currently all that can be used as noise is perlin noise and while it preforms incredibly well, it would be fun to be able to create backgrounds with different algorithms and heightmaps.
8.2 Spawning system
The second feature is that has already been mentioned during the design. The client wanted locations for tanks and turrets to spawn. That would mean a spawning system will have to be created, possibly being handled by the map generator. While there are already a couple ideas on how to implement a system like this, it wasn’t the main focus within this project.
8.3 Better shading
One of the current issues within the project is that there appear to be seams on the locations where two meshes touch each other. This phenomenon is being caused by the shading that is being calculated differently on the two meshes and not by the two meshes having a different height as one might assume at first glance. This issue might be caused by the normals of the triangles being calculated incorrectly but further examination is needed.
8.4 Further preformance improvement
A consistent thing that can be improved upon is the preformance. It is currently within acceptable levels but is by no means very good. Threading is still something that needs to be adressed and could help quite a bit once it is implemented properly. There are probably also different improvents that can still be made, like creating a mesh that is just as detailed but with fewer triangles or a better big O notation.
Rose, T., & Bakaoukas, A. G. (2016, september). ResearchGate – Temporarily Unavailable. Algorithms and Approaches for Procedural Terrain Generation – A Brief Review of Current Techniques. Geraadpleegd op 11 april 2022, van https://www.researchgate.net/publication/309222205_Algorithms_and_Approaches_for_Procedural_Terrain_Generation_-_A_Brief_Review_of_Current_Techniques
Archer, T. (N/A). Procedurally Generating Terrain. Morningside College, 1–15. http://micsymposium.org/mics_2011_proceedings/mics2011_submission_30.pdf