Notes from the book “Noise is Beautiful”.

Generating Randomness

True randomness is difficult to generate. Instead, we often will generate pseudo-random numbers, deterministic algorithms that generate numbers that look random.

In this section, we describe various ways we can generate these pseudo-random numbers.

Integer Hash

Classical random number generators typically use a single integer as a seed, which can be set when calling the function. They then use this seed to apply a fixed number of operations on the input to generate a “random” number.

One example of this is the Permuted Congruential Generator (PCG) Hash, originally presented by Melissa O’Neill in 2014. This is an integer hash that is widely used in computer graphics.

uint pcg_hash(uint input) {
    uint state = input * 747796405u + 2891336453u;
    uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
    return (word >> 22u) ^ word;
}

This hash works on a 1-dimensional input, but can be generalized to accept higher-dimensional input by chaining hash function calls on the input parameters

hash(x,y) = hash(hash(x) + y)

One example is as follows:

flag cellnoise_pcg2(vec2 p) {
    // Convert to integer input, and apply a bit mask to remove the 
    // sign bit
    uvec2 pu = uvec2(ivec2(floor(p)) & ivec2(0x7FFFFFFF));
    // Chain hash calls and then squish the noise output to [0,1]
    return float(pcg_hash(pu.x + pcg_hash(pu.y)) & 0xFFFFu) / 65536.0;
}

Floating Point Hashes

While the above hash works well, integers are often not very well supported on GPUs. In many cases, integer math can have lower performance than floating point math on GPUs. Because of this, we also need to discuss hash functions that also work on floating point inputs.

Permutation Polynomials

One algorithm that works well is a permutation polynomial. These are polynomials that permute a sequence of consequtive integers .

Interestingly, quadratic polynomials of the form

For prime are guaranteed to be permutation polynomials. This gives us well-distributed output values if the input values are also evenly distributed.

One example of this function is ().

float permute(float x) {
    float h = mod(x, 289.0f);
    return mod((34.0 * h + 10.0) * h, 289.0);
}
 
// As always, we can generalize this to higher dimensions by chaining
float cellnoise_perm(vec2 p) {
    // The division by 288.0 normalizes to [0,1].
    return permute(permute(p.x) + p.y) / 288.0;
}

Trash Bits

Another method, albeit a bit of a hack, is by using imprecision in the sin function.

float cellnoise_sin(vec2 p) {
    return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}

When scaled to to large values, bit imprecision makes sin generate values that appear random. So, we turn out 2D vector into a 1D input using values that probably don’t cause artifacts, and only take the fractional part of the scaled output.

This is a hack that is not recommended to be used, as it abuses imprecision in the sin function which is undocumented for this scale.

Ad-Hoc Float Hash

Finally, we have a methjod suggested by Dave Hoskins on Shadertoy, which uses the fract function. This method is similar to the use of sin above, but doesn’t use the imprecision of sin.

float hash12(vec2 p) {
    vec3 p3 = fract(vec3(p.xyx) * 0.1031);
    p3 += dot(p3, p3.yzx + 33.33);
    return fract((p3.x + p3.y) * p3.z);
}

This method generates pretty good results!

Perlin Noise

In many cases, we want randomness, but randomness that has an inherent pattern to it. This “random pattern” can then be used to add smaller details to our objects, at (relatively) cheap costs.

The defacto standard for doing generating this noise is Perlin Noise, first proposed by Ken Perlin in 1985.

Effects like this can also be generated by summing sine waves, but Perlin Noise achieves the same effect in significantly less computations.

Generating Noise (2D)

Perlin noise works as follows:

  • On a regular grid, generate random gradient vectors at each point.
  • For any point, find the gradient vectors of the grid cell it’s in, and interpolate between the gradient vectors to find the noise value.

Let’s consider the 2D case. The algorithm is as follows:

  1. Assign pseudo-random gradients to each point in the grid, and a function value of 0.
  2. For a point on the grid, find , where in the cell the point is, by taking ’s fractional component.
    • .
  3. Find , the directional vectors from the cell’s corner to .
    • The binary in the subscript denotes what corner we’re talking about. is the bottom-left corner, and is the top-right corner.
  4. Find , the “influence” the gradient has at that point.
  5. Interpolate between each of our to find the value at our point, first along one dimension (say x), then the next (say y).
    • Before interpolating, we first apply a blending function to smooth the output.

There are many different ways to select the gradient vectors.

Sample code is given below.

Generating Noise (3D)

This algorithm can be extended to 3D. 3D noise has a lot of uses, including:

  • Describing local density of things like smoke, clouds, fog
  • Eliminate the need for a 2D surface mapping onto objects with complicated shapes (just use the surface coordinates!)

Extending to 3D is relatively simple! Now, we have 8 gradients and vectors, and need to perform 6 interpolations.

Properties and Defects of Perlin Noise

Problem 1: Creases

With the blending polynomial

Interpolation between the gradients maintains continuity but does not maintain continuity. This can create visible creases in the noise function along the boundaries of grid cells.

The fix to this is to replace the blending polynomial with one that also forces the 2nd derivative to be 0 at the endpoints too! Solving for this, we get new blending polynomial

This is the polynomial used in the code example above.

Problem 2: Scaling Artifacts

One useful property of the noise function is it’s ability to be combined with scaled versions of itself to create complexity.

Simply scaling it about the origin can produce artifacts however, as the origin point will become visible. Instead, consider:

  • Scaling by factors that are not 2
  • Translating the scaled noise

Simplex Noise

Simplex Noise is another form of noise that uses simplexes (triangles in 2D / tetrahedrons in 3D) for tiling instead of a square grid. This method was also suggested by Ken Perlin, and offers a few benefits over Perlin Noise:

  • The direction of the noise is less clear in simplex noise, as it is no longer aligned with the x,y principal axes
  • It’s possible to analytically compute the derivative of simplex noise, as it does not use interpolations between the cell endpoints

Generating Noise (2D)

2D simplex noise uses a tiling of triangles. Given this tiling, we can generate noise as follows:

  • Let be the point we’re sampling noise at.
  • For each corner of the simplex ,
    1. Let be the vector from the simplex corner to the point
    2. Compute , the squared distance to the point
    3. Compute , a radial decay
      • The radius here was chosen to cover as much of the neighboring triangles (from a simplex vertex), without spilling over into distant cells.
    4. Compute , where is the gradient vector at that corner
      • Like before, the gradient vector can be pseudo-randomly generated via a hashing function.
  • Finally, compute the noise as .

Notice how here, we’re summing the contributions to each corner! This makes it a lot easier to analytically derive the noise.

But for a point , how do we actually find the simplex corners that contian the point?

  1. Given a point , transform it to a skewed coordinate system .
    • The vectors are parallel, but is rotated 30 degrees counter-clockwise from .
    • We also scale the grid in the direction so that the points fall on integer positions in .
  2. For , we can find the following:
    • The lower left vertex of the cell is at .
    • Local coordinates of the point are
    • Check if , to see if the point is in the right or left triangle of the skewed grid box.
      • If , we’re in the right side, so the three corners are
      • If , we’re in the left side, so the three corners are

We can transform the points in the coordinate system to the coordinate system once the corner points are found.

The transformation is as follows:

This slightly stretches the grid a bit, but doesn’t produce any visual artifacts.

Under this stretched grid, we can use 0.8 instead of 0.75 for the radial decay.

Sample code can be found below.

Generating Noise (3D)

The 3D case is similar to the 2D case, but instead of triangles, we’re now considering tetrahedrons.

  1. Transform the point to skewed space
  2. Find the cell we are in, which has 8 possible corners
  3. Find the 4 closest corners in the cell, giving us our simplex
  4. Generate a gradient for each corner
  5. Transform all 4 corners back to unskewed space
  6. Compute the radial falloff and gradient contribution
  7. Compute and sum the noise values from all 4 corners
  8. Scale the output to the desired range of values

Now, in our 3D case, we have the following transformation:

Then, for , to find the 4 corners of our simplex, we compute

And can find the other two corners by computing

The other two corners are found by adding 1 to the coordinate with the largest coordinate value, and adding 1 to the coordinates with the 2 largest coordinate values. This can be done with step functions!

Then, we can compute for each corner ,

  • , where is the vector from the corner to our point

To generate gradients in 3D, we can use a Fibonacci Sphere, which defines a mapping from an integer to one of uniformly spaced vectors on a sphere.

Derivatives of Simplex Noise

One of the major advantages of simplex noise is that it’s structure lets us analytically compute its derivative. This can be useful for a variety of effects, and is made possible as we are summing the noise contributions, instead of intepolerating them (like in Perlin Noise).

For 2D noise, we can find the derivatives by summing

And for 3D noise, we can find the derivative by summing

Noise Techniques

Multifractal Combinations

To get noise that is more detailed, we can consider overlaying noise on top of itself at different scales. Noise scaled up can define larger features, and noise scaled down can define smaller details.

The most generic way this can be done is as follows:

Make sure to divide by 2 after if you want to normalize back to [0,1].

To get more interesting results, it may help to weigh later terms on a value dependent on previous terms, instead of a fixed value only dependent on .

One example of this is setting , which gives good results for terrain.

Warping

Another technique we can perform with noise is warping. Here, we push the noise input coordinate around, to create a “warping” effect on the resultant image.

One way we could do this is by displacing each term of the noise sum with the summed gradients of the previous terms.

float k = 3.;
float c = 0.;
vec2 disp = vec2(0);
 
for (float i = 0.; i < k; i++) {
    // We displace what we're sampling from the noise function here
    vec2 trans_xy = pow(2., i) * (xy + disp);
    c += pow(2., -i) * noise(trans_xy);
    // Update our displacement based on the sampled gradient
    disp += dnoise(trans_xy);
}
    
c /= 2.;

A sample using what we’ve made from this section! https://www.shadertoy.com/view/tfdXRB

Scatterings

A scattering is a random distribution of positions. They occur naturally and frequently in the real world:

  • The distribution of trees on a landspace
  • The appearance of gravel
  • The distribution of rain drops on a window

Here, we explore how we can generate scatterings in a procedural shader.

Groundwork

Given a 2D plane, what we want is a seemingly random distribution of points where no two points are closer than distance of each other. This distance is variable, and lets us configure the appearance of our distribution.

To do this, we can split our area into cells, and for each cell, compute a jitter that determines the location of a point. This jitter can be computed pseudorandomly using one of the previously mentioned hashing methods.

By smartly toggling the size of the jitter, we can get a distribution that looks similar to what we want!

But where do we compute these points? On the GPU, we can’t simply generate every point on every invocation of the pixel shader! This is simply too computationally expensive.

Of course, we could precompute the point set on the CPU, but we want to find an alternative using procedural shaders (as that’s the point of this book).

What we can do instead, is only generate points in regions close to any given pixel. This limits the number of points we need to generate, and only gives us points that are of interest (reasonably close to the pixel).

  1. For a pixel, determine what grid cell we’re in
  2. Iterate through nearby grid cells, and compute their jittered points
  3. Perform a brute force search on these points to find which are the nearest.
    • Our jitter and search area must be compatible, such that there can’t possibly be closer points from cells outside of our search area.

Some research has found that using staggered grid cells, or forms of cells other than square may produce better results.

One example program using this technique is given below. It finds the closest point and computes the color as , where is the distance to that point.