Procedurally Generated Rivers Using OpenSimplex Noise

Procedurally generating rivers, especially purely with noise functions, is notoriously difficult. Consider this commonly seen naive approach:

if(|simplex(x, z)| > 0.2, 0, 1)
Naive solution to noise rivers.
In this case, simplex is a 1-octave OpenSimplex2 noise function, domain-warped with another1-octave OpenSimplex2 noise function with twice the frequency.

At first glance, this looks pretty good. However, there are several problems to this approach. First, look at places where "forks" in the river form. The river becomes incredibly wide in these areas, which is undesirable. The river also has inconsistent width in general. In some places it is very wide, in others it pinches into very small areas.

Ideally, we could procedurally generate lines with a constant width, to allow finer control over how wide the rivers are.

Procedurally generating constant-width lines

A simple way to generate constant-width lines from an arbitrary input function is to posterize it and apply an edge-detection kernel. A simple edge-detection kernel I have found to work well is:

[
[-1, -1, -1],
[-1,  8, -1],
[-1, -1, -1]
]

To get a useful result from our Simplex function, first we posterize it to 2 values:

if(simplex(x, z) > 0, -1, 1)
Posterized noise function

Then, we apply the kernel to it:

kernel(if(simplex(x, z) > 0, -1, 1), 1)
Posterized noise function with kernel applied
kernel is simply a function that accepts a function to have the edge-detection kernel applied, and the step at which to sample values for the kernel. E.G. 1 = sample every pixel, 4 = sample every 4 pixels.

This gives us lines that are exactly 1 pixel wide in all locations. To get smooth lines of any width, we simply sample the kernel at larger intervals.

kernel(if(simplex(x, z) > 0, -1, 1), 4)
Posterized, scaled noise function with kernel applied

Finally, we posterize again, and we have our result:

if(kernel(if(simplex(x, z) > 0, -1, 1), 4) > 0, 1, -1)
Posterized, scaled noise function with kernel applied, then posterized again

This completed function can easily be inserted into a biome generation system to create good-looking fully procedural noise-based rivers. If slight width variations are desired, a simple domain warp can be applied to the function. This approach to generating rivers is very fast, and produces visually appealing results.