Ender Look
Ender Look

Reputation: 2401

How to make a smoother Perlin noise generator?

Using a Perlin noise generator to make the tiles of a map the noise is too spiky. It has many elevations and no flat places. They don't look like mountains, islands or lakes; they are random with a lot of peaks.

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)

You don't need Matplotlib or NumPy; I'm using them for the graph to visualize the result:

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 output isn't suitable for a map. Look at this output using:

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

enter image description here

Maybe it's difficult to notice in the 2D noise but in 1D it's 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 still too many. I am looking for something smoother, like this:

enter image description here

Or this:

enter image description here

I made this based on this pseudocode.

Pikalek:

enter image description here

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

Upvotes: 11

Views: 8378

Answers (3)

Davit Margaryan
Davit Margaryan

Reputation: 1

You can make noise smoother using simple algorithm -> f(n)=(f(n-1) + f(n+1))/2

Don't know why but it is working

smoother noise

Upvotes: 0

geza
geza

Reputation: 29952

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

Upvotes: 7

N.D.C.
N.D.C.

Reputation: 1601

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

Upvotes: 1

Related Questions