From random number to texture - GLSL noise functions

A noise function for 3d rendering is a function which inputs at least a coordinate vector (either 2d or 3d) and possibly more control parameters and outputs a value (for the sake of simplicity between 0 and 1) such that the output value is not a simple function of the coordinate vector but contains a good mixture of randomness and smoothness. Dependent on the type of noise, that may mean different things in practice. A noise function is usually chosen based on its ability to represent a natural shape, so it should emulate structures in nature.

Random numbers

At the core of any noise functions is a (pseudo-)random-number generator, i.e. a function which inputs a coordinate vector and outputs a value between 0 and 1. Unlike the full noise function which is supposed to have smoothness, the raw output of the random number generator may jump wildly even on a short distance and is not supposed to resemble anything.

What works well to give an essentially unpredictable output is to use a truncation on a rapidly oscillating function. The following implementations produce viable raw noise for 2d or 3d coordinates:

float rand2D(in vec2 co){
    return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
}

float rand3D(in vec3 co){
    return fract(sin(dot(co.xyz ,vec3(12.9898,78.233,144.7272))) * 43758.5453);
}

The result is just a very fine grained surface with pixel to pixel uncorrelated color values between 0 and 1:

The raw random number function applied to a surface.

Noise function characteristics

The function above shows what is essentially a pixel to pixel uncorrelated noise - knowing the value of the function at one pixel, one can't say what the value of the adjacent pixel will be. That's not how random patterns in nature typically look. For instance, while the shape of a cloud is not predictable, if one bit of it is opaque white, it's very likely that everything nearby will also be. That is to say, in many natural random patterns there is a correlation length over which the characteristics of the pattern does not change, and only beyond this correlation length the pattern appears random and unpredictable.

If one takes a picture of a natural random pattern and runs a Fourier transformation over it, these characteristic length scales would emerge was frequencies (or wavelengths) of the Fourier analysis.

In the following, we'll frequently refer to noise 'at a certain wavelength'. The meaning is roughly that the distribution is predictable for any length scale much below that number, but random for any distance above that. This is a statement quite independent of the particular type of noise. Put a different way, noise a 1 m wavelength will create a pattern that looks completely random and point to point uncorrelated when a 10 km x 10 km chunk area is considered, but very homogeneous over a 1 cm x 1 cm patch.

Note however that a noise function can have several characteristic wavelengths,

Perlin noise

Perlin noise, named after its inventor Ken Perlin, is perhaps the best known example of noise in 3d rendering. It gives a smooth, organic distribution of shapes at a certain wavelength, and by adding noise at different wavelengths, a large variety of effects can be achieved.

Basic idea

The basic algorithm is surprisingly simple - to generate Perlin noise at a wavelength L:
  • create a regular grid with distance L between grid points
  • assign a random number to each of the grid points
  • for any coordinate vector which points to a grid point, return the number
  • for any other coordinate vector, use a smooth interpolating function between the grid points
The precise shape of the interpolation function influences the visuals a lot - a linear interpolation will give harder results, a soft function like cosine or smoothstep will give a more organic look.

GLSL realization

The following snippet of code is a 3d variant of Perlin noise, using the smoothstep function to interpolate between grid points.

float simple_interpolate(in float a, in float b, in float x)
{
   return a + smoothstep(0.0,1.0,x) * (b-a);
}

float interpolatedNoise3D(in float x, in float y, in float z)
{
    float integer_x = x - fract(x);
    float fractional_x = x - integer_x;

    float integer_y = y - fract(y);
    float fractional_y = y - integer_y;

    float integer_z = z - fract(z);
    float fractional_z = z - integer_z;

    float v1 = rand3D(vec3(integer_x, integer_y, integer_z));
    float v2 = rand3D(vec3(integer_x+1.0, integer_y, integer_z));
    float v3 = rand3D(vec3(integer_x, integer_y+1.0, integer_z));
    float v4 = rand3D(vec3(integer_x+1.0, integer_y +1.0, integer_z));

    float v5 = rand3D(vec3(integer_x, integer_y, integer_z+1.0));
    float v6 = rand3D(vec3(integer_x+1.0, integer_y, integer_z+1.0));
    float v7 = rand3D(vec3(integer_x, integer_y+1.0, integer_z+1.0));
    float v8 = rand3D(vec3(integer_x+1.0, integer_y +1.0, integer_z+1.0));

    float i1 = simple_interpolate(v1,v5, fractional_z);
    float i2 = simple_interpolate(v2,v6, fractional_z);
    float i3 = simple_interpolate(v3,v7, fractional_z);
    float i4 = simple_interpolate(v4,v8, fractional_z);

    float ii1 = simple_interpolate(i1,i2,fractional_x);
    float ii2 = simple_interpolate(i3,i4,fractional_x);

    return simple_interpolate(ii1 , ii2 , fractional_y);
}

float Noise3D(in vec3 coord, in float wavelength)
{
   return interpolatedNoise3D(coord.x/wavelength, coord.y/wavelength, coord.z/wavelength);
}

The raw output of this function, with black representing 0 and white 1, looks like this:

Perlin noise for a single wavelength.

Examples

A single wavelength of Perlin noise does not look particularly compelling and the beauty of it only emerges after mixing several wavelengths. Often 'octaves' are used in rendering, i.e. Perlin noise is generated for L, L/2, L/4,...L/n and then all are summed with a weight of a constant c to the power of i.

Two octaves already look less artificial:

Two octaves of Perlin noise added with equal weight.

With four octaves added, the result already starts to resemble a rock surface:

Four octaves of Perlin noise added with equal weight.

Note the regression to the mean - the more octaves are added, the more likely it is to find a value close to 0.5, i.e. to produce a uniform grey surface and any domains where 0 or 1 are reached become rarer and rarer. If that is not the goal, the weight with which octaves are added has to be changed or non-linear post-processing has to be used.

If c < 1, then the shorter wavelengths are progressively suppressed and the base pattern is given by long wavelength noise with less visible details determined by finer noise. The following example shows a rock surface generated from Perlin noise with c=0.8 where the base grey color is multiplied with the output of the noise function:

Procedural rock: Perlin noise dominated by long wavelengths.

Changing to c=1.3 instead emphasizes the short wavelength noise components and gives visuals which are fairly homogeneous on a large scale but very gritty from close-up:

Procedural rock: Perlin noise dominated by short wavelengths.

However, summing Perlin noise in octaves is neither necessary nor always useful.

Raw Perlin noise outputs a smooth distribution varying between 0 and 1 with fairly equal probability. Often, a more desirable goal (for instance in blending textures) is to create domains, i.e. regions where the noise function is 0 and other regions where it is 1, separated by a fairly small boundary region in which the values blend into each other. This can nicely be achieved by a non-linear post-processing step, for instance via the smoothstep function as follows:

noise = smoothstep(fraction - transition, fraction + transition, noise);

Here, fraction controls what amount of the area will be zero and what amount unity, and transition how rapid the two domains will blend into each other. Using such a non-linear post-processing, for instance a non-tiling terrain texture can be blended from multiple texture channels:

Non-linearly filtered Perlin noise for domains.

Sparse dot noise

The idea underlying sparse dot noise is partially similar to Worley noise, except that it's computationally cheaper because it makes the sparseness assumption. Suppose you have a surface which is covered by comparatively rare, dot-like structures. They're irregularly spaced, but at a mean distance of the noise wavelength. Since they are rare, one can safely assume any two won't overlap.

Basic idea

The algorithm to generate such a situation is as follows:

  • create a regular grid
  • assign random numbers to the grid points
  • use one of these to determine whether there is a point in the cell
  • if yes, use the next one to determine its radius
  • and the last two to position it in the cell such that it can't overlap with any other cell
  • draw the dot itself given position and radius
This contains a lot of fudging, especially placing it such that it can't overlap, and if we'd choose a large probability to have a dot in the cell and a large radius, this would never look irregular. This is where the Worley noise algorithm invests lots of effort. However, distributing the dots sparsely, i.e. probability to have a dot small per cell and radius being small as compared to cell area, this actually works nicely.

GLSL realization

A GLSL realization of the above algorithm is given by the following code which inputs the maximal dot radius and the dot density per cell as additional control parameters:

float dotNoise2D(in float x, in float y, in float fractionalMaxDotSize, in float dDensity)
{
    float integer_x = x - fract(x);
    float fractional_x = x - integer_x;

    float integer_y = y - fract(y);
    float fractional_y = y - integer_y;

    if (rand2D(vec2(integer_x+1.0, integer_y +1.0)) > dDensity)
       {return 0.0;}

    float xoffset = (rand2D(vec2(integer_x, integer_y)) -0.5);
    float yoffset = (rand2D(vec2(integer_x+1.0, integer_y)) - 0.5);
    float dotSize = 0.5 * fractionalMaxDotSize * max(0.25,rand2D(vec2(integer_x, integer_y+1.0)));

    vec2 truePos = vec2 (0.5 + xoffset * (1.0 - 2.0 * dotSize) , 0.5 + yoffset * (1.0 -2.0 * dotSize));

    float distance = length(truePos - vec2(fractional_x, fractional_y));

    return 1.0 - smoothstep (0.3 * dotSize, 1.0* dotSize, distance);

}

float DotNoise2D(in vec2 coord, in float wavelength, in float fractionalMaxDotSize, in float dDensity)
{
   return dotNoise2D(coord.x/wavelength, coord.y/wavelength, fractionalMaxDotSize, dDensity);
}

The raw output of this function, with black representing 0 and white 1, looks like this:

Raw sparse dot noise.

Examples

The sparse dot noise algorithm can easily be extended to sometimes place an overlapping dot to a dot, creating a slightly irregular appearance. Also, applying a non-linear filter gives a segmentation into domains. Both techniques can be combined to create for instance the appearance of rain drops on glass procedurally:

Raindrops on glass created by sparse dot noise.

Sparse dot actually does not have to be sparse at all. Simply create multiple overlapping grids at slightly different L and draw all the noise patterns on top of each other and you'll get a fairly dense yet still irregular dot distribution. Multiply some of these with a time-dependent periodic function between 0 and 1, and you'll see groups appear and disappear - using enough groups, this is a credible mockup of individual rain drops impacting.

The following algorithm is a GLSL realization of this idea (using osg_SimulationTime provided by OpenSceneGraph as the time variable, splash_speed as a measure for how fast the droplets impact and rnorm as the normalized amount of rain). It creates four groups of impact dots from sparse dot noise and fades them in and out in a staggered way controlled by a filtered sine function:

float rain = 0;
float base_rate = 6.0 + 3.0 * rnorm + 4.0 * (splash_speed - 1.0);
float base_density = 0.6 * rnorm + 0.4 * (splash_speed -1.0);
float time_fact1 = (sin(base_rate*osg_SimulationTime));
float time_fact2 = (sin(base_rate*osg_SimulationTime + 1.570));
float time_fact3 = (sin(base_rate*osg_SimulationTime + 3.1415));
float time_fact4 = (sin(base_rate*osg_SimulationTime + 4.712));

time_fact1 = smoothstep(0.0,1.0, time_fact1);
time_fact2 = smoothstep(0.0,1.0, time_fact2);
time_fact3 = smoothstep(0.0,1.0, time_fact3);
time_fact4 = smoothstep(0.0,1.0, time_fact4);

rain += DotNoise2D(Pos.xy, 0.02 * droplet_size ,0.5, base_density ) * time_fact1;
rain += DotNoise2D(Pos.xy, 0.03 * droplet_size,0.4, base_density) * time_fact2;
rain += DotNoise2D(Pos.xy, 0.04 * droplet_size ,0.3, base_density)* time_fact3;
rain += DotNoise2D(Pos.xy, 0.05 * droplet_size ,0.25, base_density)* time_fact4;

Domain (Voronoi) noise

We have already mentioned above that it's sometimes useful to have a function which segments a surface into domains which can then be textured separately. If the segmentation is to have an organic look, Perlin noise works fine. However, for man-made structures, often a less organic segmentation pattern is useful. Consider for example patches of managed forest or fields (especially in Europe) - they are usually bounded by straight lines, but they may be rather complex polygons rather than simple squares or rectangles. Voronoi noise is a tool to achieve such a segmentation.

Basic idea

The algorithm to generate such a situation is as follows:

  • create a regular grid at the desired wavelength
  • create a random value associated with each grid point
  • by calling the random number function displaced from the grid point, generate an x and y shift
  • move the grid points by their x and y shifts
  • for any coordinate inside a cell, find the shortest distance to a grid point considering only the four corners of the cell
  • return the value associated with that point

(the last two steps are known as Voronoi partitioning). There is (again) a good degree of sloppiness to this algorithm, in particular a distorted regular grid doesn't give the same amount of randomness than covering a surface with randomly placed points, and limiting the Voronoi partitioning to the four grid points framing the cell also sometimes implies that the wrong domain is assigned. However, in doing so, the algorithm avoids the expensive search over multiple nearest neighbour candidates and performs reasonably fast, while yielding results which look good enough.

GLSL realization

A GLSL realization of the above algorithm is given by the following code which inputs the domain distortion from a regular grid in x and y directions as additional control parameters:

float voronoiNoise2D(in float x, in float y, in float xrand, in float yrand)
{
      float integer_x = x - fract(x);
      float fractional_x = x - integer_x;

      float integer_y = y - fract(y);
      float fractional_y = y - integer_y;

      float val[4];

      val[0] = rand2D(vec2(integer_x, integer_y));
      val[1] = rand2D(vec2(integer_x+1.0, integer_y));
      val[2] = rand2D(vec2(integer_x, integer_y+1.0));
      val[3] = rand2D(vec2(integer_x+1.0, integer_y+1.0));

      float xshift[4];

      xshift[0] = xrand * (rand2D(vec2(integer_x+0.5, integer_y)) - 0.5);
      xshift[1] = xrand * (rand2D(vec2(integer_x+1.5, integer_y)) -0.5);
      xshift[2] = xrand * (rand2D(vec2(integer_x+0.5, integer_y+1.0))-0.5);
      xshift[3] = xrand * (rand2D(vec2(integer_x+1.5, integer_y+1.0))-0.5);

      float yshift[4];

      yshift[0] = yrand * (rand2D(vec2(integer_x, integer_y +0.5)) - 0.5);
      yshift[1] = yrand * (rand2D(vec2(integer_x+1.0, integer_y+0.5)) -0.5);
      yshift[2] = yrand * (rand2D(vec2(integer_x, integer_y+1.5))-0.5);
      yshift[3] = yrand * (rand2D(vec2(integer_x+1.5, integer_y+1.5))-0.5);

      float dist[4];

      dist[0] = sqrt((fractional_x + xshift[0]) * (fractional_x + xshift[0]) + (fractional_y + yshift[0]) * (fractional_y + yshift[0]));
      dist[1] = sqrt((1.0 -fractional_x + xshift[1]) * (1.0-fractional_x+xshift[1]) + (fractional_y +yshift[1]) * (fractional_y+yshift[1]));
      dist[2] = sqrt((fractional_x + xshift[2]) * (fractional_x + xshift[2]) + (1.0-fractional_y +yshift[2]) * (1.0-fractional_y + yshift[2]));
      dist[3] = sqrt((1.0-fractional_x + xshift[3]) * (1.0-fractional_x + xshift[3]) + (1.0-fractional_y +yshift[3]) * (1.0-fractional_y + yshift[3]));

      int i, i_min;
      float dist_min = 100.0;
      for (i=0; i<4;i++)
           {
           if (dist[i] < dist_min)
                {
                dist_min = dist[i];
                i_min = i;
                }
           }

      return val[i_min];

}

float VoronoiNoise2D(in vec2 coord, in float wavelength, in float xrand, in float yrand)
{
     return voronoiNoise2D(coord.x/wavelength, coord.y/wavelength, xrand, yrand);
}

The raw output of this function, with black representing 0 and white 1, looks like this for a low grid distortion:

Voronoi noise with low distortion.

and changes to a more irregular pattern for a high grid distortion:

Voronoi noise with high distortion.

Examples

Usually one thinks of procedurally generated textures as something to be applied in the vertex shader, but in fact the patterns are often equally useful in the vertex shader to provide regular distortions of geometry. Voronoi noise can for instance be used to quickly get the size distribution of patches in managed forest. Especially in Europe, they tend to be irregular, and often one patch is cut down at a time and then allowed to re-grow for the next fourty years, so a forest always is a collection of domains of trees of different size. Scaling the tree quad size in the vertex shader using Voronoi noise gives a very credible partitioning of that type:

Tree patches of different age in managed forest using Voronoi noise.

Topology-driven noise - vertical strata

float strata3D(in float x, in float y, in float z, in float variation)
{
      float integer_x = x - fract(x);
      float fractional_x = x - integer_x;

      float integer_y = y - fract(y);
      float fractional_y = y - integer_y;

      float integer_z = z - fract(z);
      float fractional_z = z - integer_z;

      float rand_value_low = rand3D(vec3(0.0, 0.0, integer_z));
      float rand_value_high = rand3D(vec3(0.0, 0.0, integer_z+1));

      float rand_var = 0.5 - variation + 2.0 * variation * rand3D(vec3(integer_x, integer_y, integer_z));

    return (1.0 - smoothstep(rand_var -0.15, rand_var + 0.15, fract(z))) * rand_value_low + smoothstep(rand_var-0.15, rand_var + 0.15, fract(z)) * rand_value_high;

}

float Strata3D(in vec3 coord, in float wavelength, in float variation)
{
     return strata3D(coord.x/wavelength, coord.y/wavelength, coord.z/wavelength, variation);
}

Topology-driven noise - slope lines

float slopeLines2D(in float x, in float y, in float sx, in float sy, in float steepness)
{
    float integer_x = x - fract(x);
    float fractional_x = x - integer_x;

    float integer_y = y - fract(y);
    float fractional_y = y - integer_y;

    vec2 O = vec2 (0.2 + 0.6* rand2D(vec2 (integer_x, integer_y+1)), 0.3 + 0.4* rand2D(vec2 (integer_x+1, integer_y)));
    vec2 S = vec2 (sx, sy);
    vec2 P = vec2 (-sy, sx);
    vec2 X = vec2 (fractional_x, fractional_y);

    float radius = 0.0 + 0.3 * rand2D(vec2 (integer_x, integer_y));

    float b = (X.y - O.y + O.x * S.y/S.x - X.x * S.y/S.x) / (P.y - P.x * S.y/S.x);
    float a = (X.x - O.x - b*P.x)/S.x;

    return (1.0 - smoothstep(0.7 * (1.0-steepness), 1.2* (1.0 - steepness), 0.6* abs(a))) * (1.0 - smoothstep(0.0, 1.0 * radius,abs(b)));
}

float SlopeLines2D(in vec2 coord, in vec2 gradDir, in float wavelength, in float steepness)
{
   return slopeLines2D(coord.x/wavelength, coord.y/wavelength, gradDir.x, gradDir.y, steepness);
}

More on non-linear filtering

Non-linear filtering as such isn't a technique specific to one noise type, though it has perhaps most applications with Perlin noise. As indicated above, the idea is to map the output of the noise function (which is anywhere between 0 and 1) into the same domain, but non-linearly. For instance, one could map all values below 0.9 to one number and the rest to another number.

Such filtering can bring out a surprising number of unexpected shapes out of noise functions. In the following, this is illustrated with Perlin noise of a single wavelength. As a reminder, the noise function itself looks like this:

Perlin noise for a single wavelength.

Filtering this e.g. with

noise = smoothstep(0.4 , 0.6, noise);

results in a surface partitioned between black and white domains with only very few transition regions:

Perlin noise partitioned into equal fraction domains.

Filtering instead with

noise = smoothstep(0.7 , 0.9, noise);

results in a structure increasingly similar to dot noise in which just a few isolated islands form the white domain:

Perlin noise partitioned into unequal fraction domains.

A completely different structure can be obtained by tagging the equipotential lines, i.e. the region where the noise function is close to a given value (here 0.5). Using

noise = smoothstep(0.45 , 0.5, noise) * (1.0 - smoothstep(0.5, 0.55, noise));

results in this interesting pattern of lines:

Perlin noise, tagging equipotential lines.


Back to main index     Back to rendering

Created by Thorsten Renk 2015 - see the disclaimer and contact information.