\$\begingroup\$

There's two parts to making seamlessly tileable fBm noise like this. First, you need to make the Perlin noise function itself tileable. Here's some Python code for a simple Perlin noise function that works with any period up to 256 (you can trivially extend it as much as you like by modifying the first section):

import random import math from PIL import Image perm = range(256) random.shuffle(perm) perm += perm dirs = [(math.cos(a * 2.0 * math.pi / 256), math.sin(a * 2.0 * math.pi / 256)) for a in range(256)] def noise(x, y, per): def surflet(gridX, gridY): distX, distY = abs(x-gridX), abs(y-gridY) polyX = 1 - 6*distX**5 + 15*distX**4 - 10*distX**3 polyY = 1 - 6*distY**5 + 15*distY**4 - 10*distY**3 hashed = perm[perm[int(gridX)%per] + int(gridY)%per] grad = (x-gridX)*dirs[hashed][0] + (y-gridY)*dirs[hashed][1] return polyX * polyY * grad intX, intY = int(x), int(y) return (surflet(intX+0, intY+0) + surflet(intX+1, intY+0) + surflet(intX+0, intY+1) + surflet(intX+1, intY+1))

Perlin noise is generated from a summation of little "surflets" which are the product of a randomly oriented gradient and a separable polynomial falloff function. This gives a positive region (yellow) and negative region (blue)

The surflets have a 2x2 extent and are centered on the integer lattice points, so the value of Perlin noise at each point in space is produced by summing the surflets at the corners of the cell that it occupies.

If you make the gradient directions wrap with some period, the noise itself will then wrap seamlessly with the same period. This is why the code above takes the lattice coordinate modulo the period before hashing it through the permutation table.

The other step, is that when summing the octaves you will want to scale the period with the frequency of the octave. Essentially, you will want each octave to tile the entire just image once, rather than multiple times:

def fBm(x, y, per, octs): val = 0 for o in range(octs): val += 0.5**o * noise(x*2**o, y*2**o, per*2**o) return val

Put that together and you get something like this:

size, freq, octs, data = 128, 1/32.0, 5, [] for y in range(size): for x in range(size): data.append(fBm(x*freq, y*freq, int(size*freq), octs)) im = Image.new("L", (size, size)) im.putdata(data, 128, 128) im.save("noise.png")

As you can see, this does indeed tile seamlessly:

With some small tweaking and color mapping, here's a cloud image tiled 2x2:

Hope this helps!