What does it mean to be procedural?

Procedurally generated content is any sort of content that was created using an algorithm by a computer, as opposed to a person by hand. There are many things that can be procedurally generated but here I will only be talking about textures.

Starting simple

If you just play around with some made up algorithms in a fragment shader you can get some pretty interesting results really easily. Here are a few examples with the output color vector listed below (texCoord is a standard OpenGL texture coordinate with a value between (0,0) and (1,1)):



vec4(floor(mod((texCoord.x – texCoord.y) * 10.0, 2.0)), floor(mod((texCoord.x + texCoord.y) * 10.0, 2.0)), 0.0, 1.0)

vec4(0.0, 0.0, floor(mod((pow((1.0-texCoord.x), texCoord.y)) * 10.0, 2.0)) – floor(mod((texCoord.x * texCoord.y) * 10.0, 2.0)), 1.0)

These are both neat examples but they aren’t very useful. To generate more interesting content we will need to look into noise.

Noise textures

Noise textures are textures generated using random numbers, these are one of the most commonly used things in procedural generation. There are many types of noise textures, but the most basic one is grain textures. A grain texture is a texture with uninterpolated random values at each pixel. To generate any type of noise you need a random number function. To generate grain textures on the CPU you can use any type of random number function but for the GPU and more advanced types of noise, you will need a pseudo-random number generator. A pseudo-random number generator will always generate the same value from the input but has to still seem unrelated. This is the number generator I use with noise in GLSL:

float r(float n) {

return fract(cos(n * 89.421) * 343.436);

}

If you are generating noise on the CPU then you can use the built-in random generator of whatever language you are using. A lot of the code I show will be written in GLSL so you may have to adjust it to work with pixel coordinates if you want to use it. Here is a basic grain texture example:

vec4(vec3(1.0,1.0,1.0) * r(texCoord.x * 23.323 + texCoord.y * 249.2412), 1.0)

This is already starting to become more useful, for example, some shadow mapping methods require noise to be used in the shaders. You could also overlay it on a texture to make it look grainy, or adjust the colors and use it as a sand texture.

Grain textures are great for all of that, however, if you want to procedurally generate stuff like height maps, or just don’t want something so coarse this won’t work. The next step is to do interpolated noise, for this, there are two main options: value noise and perlin noise. They both look really similar but I generally start with value noise because it can be easier to implement and I will use it here because it is easier to understand.

Value noise

First I will define a few helper methods, a noise method that takes in a vec2 and a function that does cosine interpolation. Cosine interpolation is a lot slower than regular interpolation but is commonly used for value noise because it creates rounder edges which is good for realistic heightmaps.

float r(vec2 p) {

return r(p.x * 23.323 + p.y * 249.2412);

} float cerp(float a, float b, float blend)

{

float theta = blend * 3.14159;

float f = (1.0 – cos(theta)) * 0.5;

return a * (1.0 – f) + b * f;

}

After that, the basic value noise function is pretty easy to define. It takes in a point and the square root of the total amount of noise cells that are visible. For example, if you want the texture to have 16×16 random values then the function will take in 16. If you set the value as the square size in pixels then it will look identical to the grain texture at this stage. To calculate the interpolated value the position is clipped to the noise grid and the fractional part is stored, then the 4 surrounding noise values are calculated and interpolated using the fractional part of the coordinate and finally, we do * 2.0 – 1.0 to allow the value to go below 0.

float value_noise(vec2 p, int c) {

vec2 scp = p * c;

vec2 flr = floor(scp);

vec2 fra = scp – flr;

float v0 = r(flr);

float v1 = r(flr + vec2(1.0, 0.0));

float v2 = r(flr + vec2(0.0, 1.0));

float v3 = r(flr + vec2(1.0, 1.0));

return cerp(cerp(v0, v1, fra.x), cerp(v2, v3, fra.x), fra.y) * 2.0 – 1.0;

}

A grid of 16×16 interpolated noise cells looks like this (adjusted to show values as 0.0-1.0):

vec4(vec3(1.0,1.0,1.0) * (value_noise(texCoord, 16) + 1.0) + 0.5, 1.0)

Most of the time this would be considered too noisy for these types of textures, so we will use a function to smooth it out.

Smooth noise

We can smooth the noise out by taking the average of the center cell and the 8 surrounding cells. You get slightly different results if you change the weights of the surrounding values so you may want to change them.

float smooth_noise(vec2 p) {

float corners = (r(p + vec2(-1.0, -1.0)) + r(p + vec2(1.0, -1.0)) + r(p + vec2(-1.0, 1.0)) + r(p + vec2(1.0, 1.0))) / 16.0;

float sides = (r(p + vec2(-1.0, 0.0)) + r(p + vec2(1.0, 0.0)) + r(p + vec2(0.0 -1.0)) + r(p + vec2(0.0, 1.0))) / 8.0;

float center = r(p) / 4.0;

return corners + sides + center;

}

Now that we have this we can replace the calls in value_noise to r() with smooth_noise().

float value_noise(vec2 p, int c) {

vec2 scp = p * c;

vec2 flr = floor(scp);

vec2 fra = scp – flr;

float v0 = smooth_noise(flr);

float v1 = smooth_noise(flr + vec2(1.0, 0.0));

float v2 = smooth_noise(flr + vec2(0.0, 1.0));

float v3 = smooth_noise(flr + vec2(1.0, 1.0));

return cerp(cerp(v0, v1, fra.x), cerp(v2, v3, fra.x), fra.y) * 2.0 – 1.0;

}

After this, the noise looks a bit smoother, here is an image with the new method using a 16×16 noise cell grid.

vec4(vec3(1.0,1.0,1.0) * (value_noise(texCoord, 16) + 1.0) * 0.5, 1.0)

This is a lot better than the first grain texture, however, it wouldn’t look very interesting when applied to terrain. A common way of fixing this is by applying multiple layers of noise over each other using a method called fractal Brownian motion aka fBm.

Fractal Brownian motion

The fBm function takes in the same parameters as value_noise() but the number of layers calculated is prefined as OCTAVES = 6, but you could also take in the octaves as a parameter.

#define OCTAVES 6

float fract_noise(vec2 p, int c) {

float value = 0.0;

float amplitude = 0.5;

float frequency = 0.0;

for (int i = 0; i < OCTAVES; i++)

{

value += amplitude * value_noise(p, c);

p *= 2.0;

amplitude *= 0.5;

}

return value;

}

Here is a fBm texture generated using a base grid size of 16.



vec4(vec3(1.0,1.0,1.0) * (fract_noise(texCoord, 16) + 1.0) * 0.5 / 2.0, 1.0)

If you plug this into a terrain generator you can see that we are starting to get pretty interesting results.

Some more noise

At this point, there is a number of small things we can do to get very interesting results. For example, if we offset the input position by noise then we get domain warped noise.

vec4(vec3(1.0,1.0,1.0) * (fract_noise(texCoord + fract_noise(texCoord, 16), 16) + 1.0) * 0.5 / 2.0, 1.0)

We could also do 1.0-abs(value_noise()) in fract_noise() to get ridged noise.

We can see where it got its name if we look at just one layer of ridged value noise.

You can really apply any operation you can think of to the noise and it will produce interesting results. For example, if you divided the elements in fBm instead of adding them you would get this.

There are many more ways of generating procedural textures and more types of noise and I will discuss some of those in future posts.