value noise derivatives
2024-09-30 00:02:18

Intro

Here, I will write about some interesting facts regarding Value Noise, which is similar but not identical to Gradient Noise, of which Perlin Noise is one possible implementation. Surprisingly, you will find information here that isn’t available elsewhere, especially about the derivatives of Value Noise, such as how to calculate them analytically and their applications. They are indeed very useful, as I have used them to create landscapes in the Elevated 4k demo. For Gradient Noise derivatives, you can refer to another article. Let’s explore them.

Noise derivatives are utilized to slightly modify the traditional fbm construction in a very simple manner. Note the much more pleasing variety achieved compared to regular fbm.

Analytical computation of derivatives is significantly faster and more precise than the central differences method. Depending on the fractal sum function (e.g., ridged noise, turbulence), analytical normals can be computed for the entire heightmap.

The image is rendered by directly raymarching the procedural function, employing no normal maps or materials, only diffuse lighting and fog.

The Derivatives

Let’s denote our 3D Value Noise as n(x,y,z). Of course, everything applies to any number of dimensions. We use the standard notation for derivatives.

The (3D) noise function is based on a (tri)linear interpolation of random values at specific lattice points, as follows:

n = lerp(w, lerp(v, lerp(u, a, b), lerp(u, c, d)), lerp(v, lerp(u, e, f), lerp(u, g, h)));

where u(x), v(y), w(z) are typically cubic or quintic polynomials of the form

1
u(x) = 3x^2 - 2x^3

or

1
u(x) = 6x^5 - 15x^4 + 10x^3

Now, n(x,y,z) can be expanded as:

1
n(u,v,w) = k0 + k1·u + k2·v + k3·w + k4·uv + k5·vw + k6·wu + k7·uvw

with

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
k0 = a

k1 = b - a

k2 = c - a

k3 = e - a

k4 = a - b - c + d

k5 = a - c - e + g

k6 = a - b - e + f

k7 = -a + b + c - d + e - f - g + h

The derivatives can now be easily computed, for example, for the x dimension:

1
∂n/∂x = (k1 + k4·v + k6·w + k7·vw)·u'(x)

with

1
u'(x) = 6·x·(1-x)

or

1
u'(x) = 30·x^2(x^2 - 2x + 1)

depending on whether we chose the cubic or quintic u(x) function above.

It’s very straightforward to create a function that returns the noise value and the three derivatives in one go, making it extremely efficient compared to the central difference method, which is 5 times slower.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// returns 3D value noise and its 3 derivatives
vec4 noised( in vec3 x )
{
vec3 p = floor(x);
vec3 w = fract(x);

vec3 u = w*w*w*(w*(w*6.0-15.0)+10.0);
vec3 du = 30.0*w*w*(w*(w-2.0)+1.0);

float a = myRandomMagic( p+vec3(0,0,0) );
float b = myRandomMagic( p+vec3(1,0,0) );
float c = myRandomMagic( p+vec3(0,1,0) );
float d = myRandomMagic( p+vec3(1,1,0) );
float e = myRandomMagic( p+vec3(0,0,1) );
float f = myRandomMagic( p+vec3(1,0,1) );
float g = myRandomMagic( p+vec3(0,1,1) );
float h = myRandomMagic( p+vec3(1,1,1) );

float k0 = a;
float k1 = b - a;
float k2 = c - a;
float k3 = e - a;
float k4 = a - b - c + d;
float k5 = a - c - e + g;
float k6 = a - b - e + f;
float k7 = - a + b + c - d + e - f - g + h;

return vec4( -1.0+2.0*(k0 + k1*u.x + k2*u.y + k3*u.z + k4*u.x*u.y + k5*u.y*u.z + k6*u.z*u.x + k7*u.x*u.y*u.z),
2.0* du * vec3( k1 + k4*u.y + k6*u.z + k7*u.y*u.z,
k2 + k5*u.z + k4*u.x + k7*u.z*u.x,
k3 + k6*u.x + k5*u.y + k7*u.x*u.y ) );
}

You can see an example of the code above here: Shadertoy

Fbm Derivatives

The fBM (Fractional Brownian Motion) is typically implemented as a fractal sum of Value Noise functions.

img

with w=1/2 and s=2, or something similar. When s=2, each iteration is called an “octave” due to the doubling of frequency, akin to musical octaves. The total derivative is, of course, the weighted sum of the derivatives for each octave, following regular derivative rules. If you implement a ridged Value Noise or other variations, you can also easily guide the correct way to combine the derivatives, unless you have a discontinuous shaping function like a fabsf().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// returns 3D fbm and its 3 derivatives
vec4 fbm( in vec3 x, int octaves )
{
float f = 1.98; // could be 2.0
float s = 0.49; // could be 0.5
float a = 0.0;
float b = 0.5;
vec3 d = vec3(0.0);
mat3 m = mat3(1.0,0.0,0.0,
0.0,1.0,0.0,
0.0,0.0,1.0);
for( int i=0; i < octaves; i++ )
{
vec4 n = noised(x);
a += b*n.x; // accumulate values
d += b*m*n.yzw; // accumulate derivatives
b *= s;
x = f*m3*x;
m = f*m3i*m;
}
return vec4( a, d );
}

Having derivatives available is useful for other purposes. For instance, derivatives of noise allow for the computation of analytic normals without central differences. In the case of raymarching terrain, this can be incredibly useful if normals are needed at each raymarching step (for example, to determine if trees grow or not at a given point based on slope). Similarly, when performing volumetric raymarching of clouds, having analytic normals (extracted from noise derivatives) without resorting to central differences can make the entire algorithm up to 6 times faster (depending on how the central differences are implemented).

This is a real-time raymarched landscape computed with analytical normals through value noise derivatives as explained in this article:

img

Source code: Shadertoy

This rock has surface normals computed analytically without central differences, by using the code explained above in this article:

img

Source code: Shadertoy

Other Uses

Another use of noise derivatives is to modify the fbm() construction to achieve different visual effects. For example, it is sufficient to inject the derivatives into the core of the fbm to simulate different erosion-like effects and create a rich variety of shapes for the terrain, with both flat and rough areas.

img

Rendered with the cubic u(x). Note the continuity artifact.

img

Rendered with the quintic u(x)

I rendered these images in 2008 with the code below. The image on the right uses the cubic version of u(x), while the one on the left uses the quintic. Note how the left image has some discontinuity artifacts due to the non-continuity of the second derivatives of u(x) and therefore of the fbm function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const mat2 m = mat2(0.8,-0.6,0.6,0.8);

float terrain( in vec2 p )
{
float a = 0.0;
float b = 1.0;
vec2 d = vec2(0.0);
for( int i=0; i<15; i++ )
{
vec3 n=noised(p);
d +=n.yz;
a +=b*n.x/(1.0+dot(d,d));
b *=0.5;
p=m*p*2.0;
}
return a;
}

You can see the code live and in action on Shadertoy: Shadertoy