top of page
Search

Better Procedural Noise for Terrain

  • chandra618
  • Oct 20, 2023
  • 5 min read

Often when building procedural terrain, we reach for a noise function to add detail to the landscape. More often than not, that noise function is Perlin or Simplex noise. It’s ubiquitous, has API calls in most engines, and presets in DCC apps, so it’s an obvious first choice. But I’m here to tell you “Wait! there is a better option for terrain!”. So put that Perlin noise function back on the shelf and let’s explore another option: Gradient Aligned Simplex Gabor Noise


Shortcomings of Perlin Noise

Ok, so why don’t I like Perlin Noise for terrain? A few reasons. First, it is an isotropic noise, meaning that it has no directionality or grain to it. We can clearly see this if we look at an image of any Perlin or Simplex noise. There is a uniformity or “sameness” to it in all directions.

Fractal Perlin Noise

Contrast that uniformity to what we tend to see in natural terrain - erosion features, which typically form elongated shapes aligned to the slope of the terrain:


Prominent Hydraulic Erosion Features

Natural terrain is in fact not isotropic, but has a clear and obvious grain to its features, and that’s what makes it feel natural. In the above image, note the deep valleys carved from the effects of hydraulic erosion - the result of rain falling on soft soil. If you look closer, you'll note that there is even a fractal nature to these features - large valleys give way to smaller valleys in a branching structure. Now, compare the above photo to a typical result we might achieve from using Perlin noise as a height displacement for terrain:


Perlin Noise Displaced Heightmap

You’ll notice that while we do have detail (due to the fractal nature of octave sampling), we don’t have any features that imply erosion processes. The terrain feels artificial and “blobby”. While this may be ok for certain material types (granite), it doesn’t work well for anything that would be susceptible to erosion processes.


Another very compelling reason to avoid Perlin/Simplex noise by default is that it is what everyone uses. By choosing a somewhat exotic noise basis, we can create a more interesting result, and add a new, interesting visual component to our noise toolbox.


Building an Anisotropic Noise Function For Terrain

Ok, great, so what should we do instead? Well, let’s start by finding a better noise basis. A great option is Simplex Gabor Noise, and is notable for a few reasons. First, it is anisotropic, meaning that it has a directional grain to it. Secondly, since it is based on Simplex noise, it is optimized over standard Gabor noise.


Here is the code for sampling a single octave of Simplex Gabor Noise (yoinked from shadertoy - thanks KDotJPG!):


////////////////// K.jpg's Smooth Re-oriented 8-Point BCC Noise //////////////////
////////////////////// a.k.a. OpenSimplex2, Smooth Version ///////////////////////
///////////// Modified to produce a Gabor noise like output instead. /////////////
//////////////////// Output: vec4(dF/dx, dF/dy, dF/dz, value) ////////////////////

// Borrowed from Stefan Gustavson's noise code
vec4 permute(vec4 t) {
    return t * (t * 34.0 + 133.0);
}

// BCC lattice split up into 2 cube lattices
vec2 simplexGaborNoisePart(vec3 X, vec3 dir) {
    vec3 b = floor(X);
    vec4 i4 = vec4(X - b, 2.5);
    
    // Pick between each pair of oppposite corners in the cube.
    vec3 v1 = b + floor(dot(i4, vec4(.25)));
    vec3 v2 = b + vec3(1, 0, 0) + vec3(-1, 1, 1) * floor(dot(i4, vec4(-.25, .25, .25, .35)));
    vec3 v3 = b + vec3(0, 1, 0) + vec3(1, -1, 1) * floor(dot(i4, vec4(.25, -.25, .25, .35)));
    vec3 v4 = b + vec3(0, 0, 1) + vec3(1, 1, -1) * floor(dot(i4, vec4(.25, .25, -.25, .35)));
    
    // Gradient hashes for the four vertices in this half-lattice.
    vec4 hashes = permute(mod(vec4(v1.x, v2.x, v3.x, v4.x), 289.0));
    hashes = permute(mod(hashes + vec4(v1.y, v2.y, v3.y, v4.y), 289.0));
    hashes = mod(permute(mod(hashes + vec4(v1.z, v2.z, v3.z, v4.z), 289.0)), 48.0);
    vec4 sineOffsets = hashes / 48.0 * 3.14159265 * 4.0;
    
    // Gradient extrapolations are replaced with sin(dot(dX, inputVector) + pseudorandomOffset)
    vec3 d1 = X - v1; vec3 d2 = X - v2; vec3 d3 = X - v3; vec3 d4 = X - v4;
    vec4 a = max(0.75 - vec4(dot(d1, d1), dot(d2, d2), dot(d3, d3), dot(d4, d4)), 0.0);
    vec4 aa = a * a; vec4 aaa = aa * a;
    vec4 extrapolations = vec4(dot(d1, dir), dot(d2, dir), dot(d3, dir), dot(d4, dir)) + sineOffsets;
    extrapolations = sin(extrapolations);
    
    // Return (kernels^3) * sinusoids, and just (kernels^3), so we can average them later
    return vec2(dot(aaa, extrapolations), dot(aaa, vec4(1.0)));
}

// Rotates domain, but preserve shape. Hides grid better in cardinal slices.
// Good for texturing 3D objects with lots of flat parts along cardinal planes.
float simplexGaborNoise_Classic(vec3 X, vec3 dir) {
    X = dot(X, vec3(2.0/3.0)) - X;
    dir = dot(dir, vec3(2.0/3.0)) - dir;
    
    vec2 both = simplexGaborNoisePart(X, dir) + simplexGaborNoisePart(X + 144.5, dir);
    return both.x / both.y;
}

// Gives X and Y a triangular alignment, and lets Z move up the main diagonal.
// Might be good for terrain, or a time varying X/Y plane. Z repeats.
float simplexGaborNoise_XYBeforeZ(vec3 X, vec3 dir) {
    
    // Not a skew transform.
    mat3 orthonormalMap = mat3(
        0.788675134594813, -0.211324865405187, -0.577350269189626,
        -0.211324865405187, 0.788675134594813, -0.577350269189626,
        0.577350269189626, 0.577350269189626, 0.577350269189626);
    
    X = orthonormalMap * X;
    dir = orthonormalMap * dir;
    vec2 both = simplexGaborNoisePart(X, dir) + simplexGaborNoisePart(X + 144.5, dir);
    return both.x / both.y;
}

//////////////////////////////// End noise code ////////////////////////////////

One thing to note is that the noise function takes not only a 3D point as input, but also a vector direction, which specifies the grain direction. The magnitude of that vector even specifies how elongated the grain appears. This is great! We can align this noise in any direction at a per-pixel / per-sample basis, as in this image, which aligns the grain concentrically around (0,0)

Concentrically Aligned Simplex Gabor Noise

If we take this noise function, and use it to displace the height of a smooth mound of terrain, but use the gradient vector as the “direction” input to the above noise function, we get something like this:



While that is nifty and all, it really doesn’t look like natural terrain, so we need to sample a few octaves, and tune the noise parameters a bit. With tuned parameters, we can layer several octaves at successively smaller scale to produce something more natural looking:






Neat! We have some very strong erosion features now, and we didn’t even have to run a physical simulation. This is great too because we can easily tune the noise parameters to suit our needs, or even drive some of them from a secondary noise function (perlin, now’s your time to shine!) For example, with a lower lacunarity setting, we can calm the small-scale erosion features down a bit to get something a little softer:


Conclusion

I hope this made you excited about exploring different noise functions beyond the classic Perlin noise we see so often, and it’s worth noting that this is just the tip of the iceberg! (check out Voronoise, for example). Richness and realism in procedural content comes from variety, and layering different noise functions together is a great way to add that variety. In practice, we would want to mix this erosion noise with other noises, perhaps on a per-material basis, to achieve a more realistic and interesting result.

As a bonus, here is a blender file where I’ve implemented this terrain function. I hope this can be used as a reference and inspiration when building our own terrain tools.


Cheers!

 
 
 

Comments


© 2023 by Chad Foxglove. Powered and secured by Wix

bottom of page