Composing noise functions for more complex / looping noise

The orx-noise OPENRNDR extension received new capabilities in this and this commits. This post describes the new functionality and was posted by Edwin on Slack in July 2021.

It starts with functions

We know that simplex3D is a

(Int, Double, Double, Double) -> Double

Here (Int, Double, Double, Double) -> Double is our commonly used function signature for noise functions that given an Int seed and a 3d coordinate returns a scalar noise value. perlin3D, value3D etc. use that same signature.

orx-noise already had a fbm function that is much like

fun fbm(lacunarity:Double, gain:Double, 
  seed:Int, x:Double, y:Double, z:Double, 
  noise: (Int, Double, Double, Double) -> Double): Double

So that too is a scalar noise function, however with a different signature.

orx-noise also has an fbmFunc3D function that instead of returning a scalar returns a function with our noise function signature.

inline fun fbmFunc3D(
    crossinline noise: (Int, Double, Double, Double) -> Double,
    octaves: Int = 8,
    lacunarity: Double = 0.5,
    gain: Double = 0.5
): (Int, Double, Double, Double) -> Double

You’d use fbmFunc3D to compose a new function like this:

val fbmSimplex = fbmFunc3D(simplex3D)

Which can be used like any other function.

val noise = fbmSimplex(432, 0.0, 1.0, 2.0)

Since our fbmSimplex has the (Int, Double, Double, Double) -> Double signature we can apply fbmFunc3D on it again if we want to:

val fbmFbmSimplex = fbmFunc3D(fbmSimplex, lacunarity = 1.3222)

Or alternatively, to demonstrate how quickly this becomes hard to read:

val fbmFbmSimplex = fbmFunc3D(fbmFunc3D(simplex3D), lacunarity = 1.3222)

To improve readability I introduce the use of extension functions, I assume the reader knows what those are. The fbm function is an extension function on functions with the (Int, Double, Double, Double) -> Double) signature:

    fun ((Int, Double, Double, Double) -> Double).fbm : 
         (Int, Double, Double, Double) -> Double

It does the same thing as fbmFunc3D, which is to return a function.So now we can compose our previous noise functions without clutter:

val fbmSimplex = simplex3D.fbm()
val fbmFbmSimplex = simplex3D.fbm().fbm(lacunarity = 1.3222)

Now let’s look into some of the additional tooling I made.

.crossFade()

crossFade is used for seamless noise looping animations and assumes the z axis of 3D noise functions is used for time. (Perhaps it makes sense to have generalized version of this in which the axis/axes can be specified). The crossFade function uses the receiver noise function (this) to calculate a blend. The start, end and width arguments are used to define the loop of the z value.

fun ((Int, Double, Double, Double) -> Double).crossFade(
    start: Double, end: Double, width: Double = 0.5): 
     (Int, Double, Double, Double) -> Double {
    return { seed, x, y, z ->
        val a = z.map(start, end, 0.0, 1.0).mod_(1.0)
        val f = (a / width).coerceAtMost(1.0)
        val o = this(seed, x, y, a.map(0.0, 1.0, start, end)) * f
        val s = this(seed, x, y, (a + 1.0).map(0.0, 1.0, start, end) * (1.0 - f))
        o + s
    }
}

.withVector2Output()

The withVector2Output function changes the function signature to return Vector2 instead of Double. So the noise function outputs 2d noise, classically you’d pass in different seeds and juggle a bit with your input coordinates to get uncorrelated but similarly distributed noise for x and y. withVector2Output does that for you by xor-ing the seed and rotating x,y by 90 degrees. Also here we assume the z axis is used for time.

fun ((Int, Double, Double, Double) -> Double).withVector2Output(): 
    (seed: Int, x: Double, y: Double, z: Double) -> Vector2 =
    { seed, x, y, z -> Vector2(this(seed, x, y, z), 
                               this(seed xor 0x7f7f7f7f, y, -x, z)) }

.gradient()

And finally, we have gradient function. Which returns a function that approximates the gradient of the receiver function, also known as (curl noise):

fun ((Int, Double, Double, Double) -> Vector2).gradient(epsilon: Double = 1e-2 / 2.0): 
     (Int, Double, Double, Double) -> Vector2 =
    { seed, x, y, z ->
        val dfdx = (this(seed, x + epsilon, y, z) - this(seed, x - epsilon, y, z)) / (2 * epsilon)
        val dfdy = (this(seed, x, y + epsilon, z) - this(seed, x, y - epsilon, z)) / (2 * epsilon)
        dfdx + dfdy
    }

Notes and references

Original text by Edwin, formatted and edited by Abe, reference links by Yann.

1 Like

Now we need some images to show how those concepts actually look like! Any volunteers? :wink: