Ci sono due parti per creare un rumore fBm perfettamente piastrellabile come questo. Innanzitutto, è necessario rendere piastrellabile la funzione di rumore Perlin stessa. Ecco un po 'di codice Python per una semplice funzione di rumore Perlin che funziona con qualsiasi periodo fino a 256 (puoi estenderlo banalmente quanto vuoi modificando la prima sezione):
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))
Il rumore di Perlin è generato da una somma di piccole "superfici" che sono il prodotto di un gradiente orientato in modo casuale e di una funzione di decadimento polinomiale separabile. Questo dà una regione positiva (gialla) e una regione negativa (blu)

Le surflet hanno un'estensione di 2x2 e sono centrate sui punti reticolari interi, quindi il valore del rumore di Perlin in ciascun punto dello spazio viene prodotto sommando le surflet agli angoli della cella che occupa.

Se si fanno avvolgere le direzioni del gradiente con un certo periodo, il rumore stesso si avvolgerà perfettamente con lo stesso periodo. Questo è il motivo per cui il codice sopra prende la coordinata reticolare modulo il periodo prima di eseguire l'hashing attraverso la tabella di permutazione.
L'altro passo è che quando si sommano le ottave si desidera ridimensionare il periodo con la frequenza dell'ottava. In sostanza, ogni ottava dovrà affiancare l'intera immagine giusta una volta, anziché più volte:
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
Mettilo insieme e otterrai qualcosa del genere:
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")

Come puoi vedere, questo effettivamente affianca perfettamente:

Con alcune piccole modifiche e mappatura dei colori, ecco un'immagine nuvola piastrellata 2x2:

Spero che sia di aiuto!