Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make a smoother Perlin noise generator?

I'm trying to use a Perlin noise generator to make the tiles of a map, but I notice that my noise is too spiky, I mean, it has too many elevations and no flat places, and they don't seem like mountains, islands, lakes or anything; they seem much too random and with a lot of peaks.

At the end of the question there are the changes needed in order to fix it.

The important code for the question is:

1D:

def Noise(self, x):     # I wrote this noise function but it seems too random
    random.seed(x)
    number = random.random()
    if number < 0.5:
        final = 0 - number * 2
    elif number > 0.5:
        final = number * 2
    return final

 def Noise(self, x):     # I found this noise function on the internet
    x = (x<<13) ^ x
    return ( 1.0 - ( (x * (x * x * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0)

2D:

def Noise(self, x, y):     # I wrote this noise function but it seems too random
    n = x + y
    random.seed(n)
    number = random.random()
    if number < 0.5:
        final = 0 - number * 2
    elif number > 0.5:
        final = number * 2
    return final

def Noise(self, x, y):     # I found this noise function on the internet
    n = x + y * 57
    n = (n<<13) ^ n
    return ( 1.0 - ( (x * (x * x * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0)

I left in my code for 1D and 2D Perlin noise because maybe someone is interested in it: (It took me a long time to find some code, so I think someone would be glad to find an example here).
You don't need Matplotlib or NumPy to make the noise; I'm only using them to make the graph and see the result better.

import random
import matplotlib.pyplot as plt              # To make graphs
from mpl_toolkits.mplot3d import Axes3D      # To make 3D graphs
import numpy as np                           # To make graphs

class D():     # Base of classes D1 and D2
    def Cubic_Interpolate(self, v0, v1, v2, v3, x):
        P = (v3 - v2) - (v0 - v1)
        Q = (v0 - v1) - P
        R = v2 - v0
        S = v1
        return P * x**3 + Q * x**2 + R * x + S

class D1(D):
    def __init__(self, lenght, octaves):
        self.result = self.Perlin(lenght, octaves)
    
    def Noise(self, x):     # I wrote this noise function but it seems too random
        random.seed(x)
        number = random.random()
        if number < 0.5:
            final = 0 - number * 2
        elif number > 0.5:
            final = number * 2
        return final

    def Noise(self, x):     # I found this noise function on the internet
        x = (x<<13) ^ x
        return ( 1.0 - ( (x * (x * x * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0)
        
    def Perlin(self, lenght, octaves):
        result = []
        for x in range(lenght):
            value = 0
            for y in range(octaves):
                frequency = 2 ** y
                amplitude = 0.25 ** y            
                value += self.Interpolate_Noise(x * frequency) * amplitude
            result.append(value)
            print(f"{x} / {lenght} ({x/lenght*100:.2f}%): {round(x/lenght*10) * '#'} {(10-round(x/lenght*10)) * ' '}. Remaining {lenght-x}.")     # I don't use `os.system('cls')` because it slow down the code.
        return result

    def Smooth_Noise(self, x):
        return self.Noise(x) / 2 + self.Noise(x-1) / 4 + self.Noise(x+1) / 4

    def Interpolate_Noise(self, x):
        round_x = round(x)
        frac_x  = x - round_x
        v0 = self.Smooth_Noise(round_x - 1)
        v1 = self.Smooth_Noise(round_x)
        v2 = self.Smooth_Noise(round_x + 1)
        v3 = self.Smooth_Noise(round_x + 2)
        return self.Cubic_Interpolate(v0, v1, v2, v3, frac_x)
            
    def graph(self, *args):
        plt.plot(np.array(self.result), '-', label = "Line")
        for x in args:
            plt.axhline(y=x, color='r', linestyle='-')    
        plt.xlabel('X')
        plt.ylabel('Y')
        plt.title("Simple Plot")
        plt.legend()
        plt.show()

class D2(D):
    def __init__(self, lenght, octaves = 1):
        
        self.lenght_axes = round(lenght ** 0.5)
        self.lenght = self.lenght_axes ** 2
        
        self.result = self.Perlin(self.lenght, octaves)

    def Noise(self, x, y):     # I wrote this noise function but it seems too random
        n = x + y
        random.seed(n)
        number = random.random()
        if number < 0.5:
            final = 0 - number * 2
        elif number > 0.5:
            final = number * 2
        return final

    def Noise(self, x, y):     # I found this noise function on the internet
        n = x + y * 57
        n = (n<<13) ^ n
        return ( 1.0 - ( (x * (x * x * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0)
    
    def Smooth_Noise(self, x, y):
        corners = (self.Noise(x - 1, y - 1) + self.Noise(x + 1, y - 1) + self.Noise(x - 1, y + 1) + self.Noise(x + 1, y + 1) ) / 16
        sides   = (self.Noise(x - 1, y) + self.Noise(x + 1, y) + self.Noise(x, y - 1)  + self.Noise(x, y + 1) ) /  8
        center  =  self.Noise(x, y) / 4
        return corners + sides + center
    
    def Interpolate_Noise(self, x, y):

        round_x = round(x)
        frac_x  = x - round_x

        round_y = round(y)
        frac_y  = y - round_y

        v11 = self.Smooth_Noise(round_x - 1, round_y - 1)
        v12 = self.Smooth_Noise(round_x    , round_y - 1)
        v13 = self.Smooth_Noise(round_x + 1, round_y - 1)
        v14 = self.Smooth_Noise(round_x + 2, round_y - 1)
        i1 = self.Cubic_Interpolate(v11, v12, v13, v14, frac_x)

        v21 = self.Smooth_Noise(round_x - 1, round_y)
        v22 = self.Smooth_Noise(round_x    , round_y)
        v23 = self.Smooth_Noise(round_x + 1, round_y)
        v24 = self.Smooth_Noise(round_x + 2, round_y)
        i2 = self.Cubic_Interpolate(v21, v22, v23, v24, frac_x)
        
        v31 = self.Smooth_Noise(round_x - 1, round_y + 1)
        v32 = self.Smooth_Noise(round_x    , round_y + 1)
        v33 = self.Smooth_Noise(round_x + 1, round_y + 1)
        v34 = self.Smooth_Noise(round_x + 2, round_y + 1)
        i3 = self.Cubic_Interpolate(v31, v32, v33, v34, frac_x)

        v41 = self.Smooth_Noise(round_x - 1, round_y + 2)
        v42 = self.Smooth_Noise(round_x    , round_y + 2)
        v43 = self.Smooth_Noise(round_x + 1, round_y + 2)
        v44 = self.Smooth_Noise(round_x + 2, round_y + 2)
        i4 = self.Cubic_Interpolate(v41, v42, v43, v44, frac_x)
        
        return self.Cubic_Interpolate(i1, i2, i3, i4, frac_y)
    
    def Perlin(self, lenght, octaves):
        result = []
        for x in range(lenght):
            value = 0
            for y in range(octaves):
                frequency = 2 ** y
                amplitude = 0.25 ** y            
                value += self.Interpolate_Noise(x * frequency, x * frequency) * amplitude
            result.append(value)
            print(f"{x} / {lenght} ({x/lenght*100:.2f}%): {round(x/lenght*10) * '#'} {(10-round(x/lenght*10)) * ' '}. Remaining {lenght-x}.")     # I don't use `os.system('cls')` because it slow down the code.
        return result

    def graph(self, color = 'viridis'):
        # Other colors: https://matplotlib.org/examples/color/colormaps_reference.html
        fig = plt.figure()
        Z = np.array(self.result).reshape(self.lenght_axes, self.lenght_axes)

        ax = fig.add_subplot(1, 2, 1, projection='3d')
        X = np.arange(self.lenght_axes)
        Y = np.arange(self.lenght_axes)
        X, Y = np.meshgrid(X, Y)        
        d3 = ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap=color, linewidth=0, antialiased=False)
        fig.colorbar(d3)

        ax = fig.add_subplot(1, 2, 2)
        d2 = ax.imshow(Z, cmap=color, interpolation='none')
        fig.colorbar(d2)
               
        plt.show()

The problem is that the output doesn't seem suitable for a map.

Look at this output using:

test = D2(1000, 3)
test.graph()

enter image description here

I am looking for something smoother.

Maybe it's difficult to notice in the 2D noise what I'm talking about but in 1D it's much easier:

test = D1(1000, 3)
test.graph()

enter image description here

The noise function from the internet has slightly smaller and less frequent peaks, but it still has too many. I am looking for something smoother.

Something like this maybe:
enter image description here

Or this:
enter image description here

P.S: I made this based on this pseudocode.

EDIT:

Pikalek:

enter image description here

Even with low values it has peaks and no curves or smooth/flat lines.

geza: SOLUTION

Thanks to geza's suggestions I found the solution to my problem:

def Perlin(self, lenght_axes, octaves, zoom = 0.01, amplitude_base = 0.5):
    result = []
    
    for y in range(lenght_axes):
        line = []
        for x in range(lenght_axes):
            value = 0
            for o in range(octaves):
                frequency = 2 ** o
                amplitude = amplitude_base ** o
                value += self.Interpolate_Noise(x * frequency * zoom, y * frequency * zoom) * amplitude
            line.append(value)
        result.append(line)
        print(f"{y} / {lenght_axes} ({y/lenght_axes*100:.2f}%): {round(y/lenght_axes*20) * '#'} {(20-round(y/lenght_axes*20)) * ' '}. Remaining {lenght_axes-y}.")
    return result

Other modifications were:

  • Z = np.array(self.result) instead of this Z = np.array(self.result).reshape(self.lenght_axes, self.lenght_axes)in the graph function.
  • Use of math.floor() (remember import math) instead of round() in Interpolate_Noise function in round_x and round_y variables.
  • Modification of the return line in Noise (the second one) to return ( 1.0 - ( (n * (n * n * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0) . D2(10000, 10) enter image description here The only thing strange right now is that the mountains (yellow) are always near the same place, but I think that is a matter of changing the numbers in the Noise function.
like image 879
Ender Look Avatar asked Dec 15 '17 18:12

Ender Look


2 Answers

I've spotted these mistakes in your code:

  • You need to multiply Interpolate_Noise parameter, to "zoom" into the map (for example, multiply x with 0.01). If you do this in the 1D case, you'll see that the generated function is already much better
  • Increase the octave count from 3 to something larger (3 octaves don't generate too much detail)
  • Use amplitude 0.5^octave, not 0.25^octave (but you can play with this parameter, so 0.25 is not inherently bad, but it doesn't give too much detail)
  • For the 2D case, you need to have 2 outer loops (one for horizontal, and one for vertical. And of course, you still need to have the octave loop). So you need to "index" the noise properly with horizontal and vertical position, not just x and x.
  • Remove smoothing altogether. Perlin noise doesn't need it.
  • 2D noise function has a bug: it uses x instead of n in the return expression
  • at cubic interpolation, you use round instead of math.floor.

Here's an answer of mine, with a simple (C++) implementation of Perlin-like (it is not proper perlin) noise: https://stackoverflow.com/a/45121786/8157187

like image 155
geza Avatar answered Sep 21 '22 06:09

geza


You need to implement a more aggressive smoothing algorithm. The best way to do this is to use Matrix Convolution. The way this works is, you have a matrix which we refer to as the "Kernel" that is applied to every cell in the grid, creating a new, transformed dataset. An example Kernel might be:

0.1 0.1 0.1
0.1 0.2 0.1
0.1 0.1 0.1

Say you had a grid like this:

2 4 1 3 5
3 5 1 2 3
4 9 2 1 2
3 4 9 5 2
1 1 3 6 7

And say we wanted to apply the Kernel to the centermost 2, we would cut out the grid in the shape of the Kernel and multiply each cell with its corresponding Kernel cell:

. . . . .
. 5 1 2 .       0.1 0.1 0.1       0.5 0.1 0.2
. 9 2 1 .   x   0.1 0.2 0.1   =   0.9 0.4 0.1
. 4 9 5 .       0.1 0.1 0.1       0.4 0.9 0.5
. . . . .

Then we can sum all of these values to get the new value of the cell, 0.5+0.1+0.2+0.9+0.4+0.1+0.4+0.9+0.5 = 4, and we fill in that space on our new dataset:

? ? ? ? ?
? ? ? ? ?
? ? 4 ? ?
? ? ? ? ?
? ? ? ? ?

... as you can imagine, we have to repeat this operation for each other space in the grid in order to fill out our new dataset. Once that is done, we throw away the old data and use this new grid as our dataset.

The advantage of this is that you can use massive kernels in order to perform very large smoothing operations. You could, for instance, use a 5x5 or 9x9 sized kernel, which will make your noise much smoother.

One more note, the kernel needs to be built so that the sum of all its cells is 1, or else you won't have conservation of mass (so to speak; e.g. if the sum was >1 your peaks would tend to get higher and the mean of your data would be higher). An example of a 5x5 matrix would be:

0.010 0.024 0.050 0.024 0.010
0.024 0.050 0.062 0.050 0.024
0.050 0.062 0.120 0.062 0.050
0.024 0.050 0.062 0.050 0.024
0.010 0.024 0.050 0.024 0.010

One way to ensure this quality is simply to normalize the matrix; divide each cell by the sum of all cells. E.g.:

1  4  16 4  1                    0.002808989    0.011235955 0.04494382  0.011235955 0.002808989
4  16 32 16 4                    0.011235955    0.04494382  0.08988764  0.04494382  0.011235955
16 32 64 32 16  (sum = 356) -->  0.04494382     0.08988764  0.179775281 0.08988764  0.04494382
4  16 32 16 4                    0.011235955    0.04494382  0.08988764  0.04494382  0.011235955
1  4  16 4  1                    0.002808989    0.011235955 0.04494382  0.011235955 0.002808989
like image 31
N.D.C. Avatar answered Sep 21 '22 06:09

N.D.C.