This article is also available on Medium.
For lots of my projects, especially the video games I often work on, I need to create somewhat random but still credible scenery. And because I’m lazy and I’m a fan of exploration games like Minecraft or Valheim, I always eventually revert to creating procedurally generated level.
What is procedural generation?
Basically, instead of preparing all your levels beforehand manually (by setting the exact position, color, rotation, etc. of the ground tiles and the objects) during a game level design phase, you instead define a set of rules and a machine able to use those rules to automatically create valid levels. The big advantage is that, once you’re done making the engine, you can create as many levels (or pieces of a level) as you want! Moreover, this generation can be pretty quick and this is how Minecraft or No Man’s Sky immerse you in continuous and infinite worlds. But, of course, it’s usually a bit harder than hand-design because you need to teach your software the right and wrong patterns (those are what I called the “rules” in the previous sentence).
Procedural generation is a very vast and complex topic. There are plenty of tools in your generation engine – and to be honest, a large amount relies on randomness. The idea is to start off with random values and then somehow “control the craziness” so it is valid.
All in all, when you do procedural generation, you often like to make noise.
No, not sound noise, but image noise! The word “noise” is mostly known in acoustics, but it’s also used in computer graphics where it refers to pseudo-random functions that generate procedural textures. Let’s break down this notion of “procedural texture”.
- Firstly, textures are 2D images that we put on objects in 3D scenes. Thanks to texture mapping, we can define highly detailed information and wrap it on the surface of a 3D object; this info can be about colors, light reflection, bump deformations…
- Secondly, textures can be of two types:
- “data stored” images someone drew before by hand
- or “procedural” computer-generated: those are computed using math formulas and usually create textures pixel by pixel
So, among those math formulas, there are well-known algorithms called “noises” that result in useful textures that can be applied to many use cases. 3D software like Blender or Cinema 4D (and lots of others!) show in their docs some noise examples, but here are a few common ones:
Recently, while I was working on a video game project, I focused on Perlin noise for my level generation.
This noise is quite interesting because it is very customizable, it makes smooth transitions between areas and it can be seeded, which means that your noise generator will consistently give out the same result if you give the exact same inputs. On the other hand, a slight change in those inputs translates into widely different noises. All of this makes the Perlin noise quite an intuitive-to-use and powerful procedural texture primitive.
As you can see, noise maps are black and white 2D images with more or less repetitive patterns. In practise, “black and white” is also called “grayscale” because you only have varying levels of gray. I mentioned this in a recent post about ASCII video conversion: your image is composed of width * height pixels and each of those pixels is a value ranging from 0 (pure black) to 255 (pure white). All the values in-between correspond to different grays.
The great thing with noises is that they can be used for lots of things:
- height maps and elevation generation: the black pixels can be matched to the lowest areas and the white pixels to the highest peaks
- density maps: when you add objects in your 3D scene, you often want them to be somewhat spread out or grouped together in a “logical” way – by using smooth noise maps (like the Perlin or the value noises), you can get darker areas for empty spaces and whiter areas for densely populated zones
- direct color texturing: although it’s not the most frequent usage, you can set these noises on your objects to simply display them on your 3D surface (for example, a few white noises can be put one after the other on a flat screen to simulate an old broken TV set ;))
For example, video game developers can use Perlin noise to setup the altitude of the terrain, the vegetation and more!
The issue with 3D
As long as you’re working on 2D side or top-down levels, noise-generated procedural textures can be used directly without too much of a hassle. But when you get to 3D, it can become a bit more complex. Suppose that you are creating a 3D game where the world is a cube, and you want to put mountains on it based on a Perlin noise (that creates an altitude map). How do you apply a 2D texture in this 3D space?
For a cube, creating 3D Perlin noise can be done by creating 2D Perlin noises and stitching them together along one edge. The problem is that, as you can see, there is clearly something wrong where we made the stitch. The first texture brutally stops and the second one starts without any consideration for the current values. This is a texture that is not seamless, or tileable: you see the cut.
And now, what if I told you that I wanted to put this texture on a sphere? Can you image how disturbing the stitches will be then? Well…
That’s pretty ugly, right? At that point, I knew I had to rework my Perlin noise-generated texture to be seamless, so it would accommodate for this rough sewing.
Making the noise seamless
Looking at the video above, you see that there are two different problems:
- there is a horizontal discontinuity: when the sphere rolls around the vertical axis at the beginning of the movie, we see that the texture is not seamless longitude-wise
- and there is a vertical discontinuity: at the pole of the sphere, everything is harshly shrunk into one point – but since the lower latitudes show quite different patterns, we see the motifs!
We can separate and treat those discontinuities sequentially. We’ll suppose that we already have a C# implementation of a 2D Perlin noise generator, for example by following tutorial like this one by ven0mous. We therefore have a static function called
GenerateNoiseMap() that we can call to create a 2D float array – it takes in the various parameters detailed in the tutorial:
float[,] noiseMap = Noise.GenerateNoiseMap( width, height, seed, scale, octaves, persistance, lacunarity, offset )
Fixing the horizontal discontinuity
At the moment, if I repeat my noise texture and put the two copies side by side, it’s pretty obvious they don’t match (the vertical mark we see in the middle corresponds to the ugly vertical stitch on the sphere in the movie above):
What we would like is for the right of the texture to sync up with the left – in order to do that, we are going to:
- decide on a given stitch size
- copy a band of stitch pixels in width on the left of the noise and paste it on the right
- mirror this band horizontally so the pixels on the far left of the band end up on the right, and the pixels on the far right of the band end up on the left
- apply a linear gradient to the pasted band transparency so it is completely transparent on the left and completely opaque on the right
I think that here, a picture is worth a million words:
This can be done with the following C# function:
Once we’ve applied it to our noise map, we see that it is much better! Sure, we still see the stitch a bit but it is far less awful than before and at a larger scale most untrained eyes won’t be able to notice it:
If we put this new texture on our sphere, we see how it improves the texturing:
Now, we need to take care of the vertical discontinuity!
Fixing the vertical discontinuity
This second issue is a bit more tricky. This time, we won’t be able to simply copy and paste some pixels. Rather, we need to modify the texture at the very top and the very bottom, row by row. More precisely, we have to:
- find a way to “mix” all the pixels together at the pole
- then, as we go towards the equator, we mix the pixels less and less
- until we are out of stitch band, at which point we are not mixing anymore (i.e.: we leave the noise as is)
In order to mix pixels, we are going to compute the average value, in other words, we will compute the mean grayscale value and assign it to all the pixels on the horizontal band. The number of pixels that we will take into account for this computation depends on the row we are currently computing the average for: on the first and last rows, every pixel value will be replaced by the mean of the entire row; on the second and last but one rows, every pixel value will be replaced by the mean of the entire row minus the left and right pixels; and so on…
Note: for math lovers – remember that computing a mean can be written as a convolution; so here, we simply take a dynamic kernel size that depends on the current row.
Here is the corresponding C# code – you’ll notice that the top and bottom sides can be treated simultaneously since we have mirrored values:
By combining this function with
MakeSeamlessHorizontally(), we eventually get a pretty different noise texture, compared to the previous ones:
But when we put it on the sphere, we finally get an interesting result! At this point, we can see the stitches but they are way less noticeable than they used to…
And if we use this texture as height map on our sphere, it globally works, even on our sphere – it’s not yet perfect but at least there is no horrendous discontinuity! 🙂
Procedural generation is an amazing way of creating (theoretically) endless games but it’s not an easy tool to master. Noises are a common way of generating procedural textures and they can be used as height maps, density maps, plain old color textures… They are powerful and somewhat controllable pseudo-random math functions that allow us to populate a scene in a “logical” way when provided to our level creation engine along with some rules.
Going from 2D to 3D can be complex and, more generally it’s always valuable to make our textures seamless. However by using some basic copy/paste, linear transparency masking and averaging, we can modify noise textures to make them tileable.
Thanks to these techniques, we can create various procedural textures that are even valid for more demanding geometries like spheres!