Git Product home page Git Product logo

perlin-numpy's Introduction

perlin-numpy

I wrote two articles on my blog about this project, the first one is about the generation of 2D noise while the second one is about the generation of 3D noise, feel free to read them!

You can find implementations using numba here.

Description

A fast and simple perlin noise generator using numpy.

Installation

You can install this package via:

pip3 install git+https://github.com/pvigier/perlin-numpy

Usage

from perlin_numpy import (
    generate_fractal_noise_2d, generate_fractal_noise_3d,
    generate_perlin_noise_2d, generate_perlin_noise_3d
)

2D noise

The function generate_perlin_noise_2d generates a 2D texture of perlin noise. Its parameters are:

  • shape: shape of the generated array (tuple of 2 ints)
  • res: number of periods of noise to generate along each axis (tuple of 2 ints)
  • tileable: if the noise should be tileable along each axis (tuple of 2 bools)

Note: shape must be a multiple of res

The function generate_fractal_noise_2d combines several octaves of 2D perlin noise to make 2D fractal noise. Its parameters are:

  • shape: shape of the generated array (tuple of 2 ints)
  • res: number of periods of noise to generate along each axis (tuple of 2 ints)
  • octaves: number of octaves in the noise (int)
  • persistence: scaling factor between two octaves (float)
  • lacunarity: frequency factor between two octaves (float)
  • tileable: if the noise should be tileable along each axis (tuple of 2 bools)

Note: shape must be a multiple of lacunarity^(octaves-1)*res

3D noise

The function generate_perlin_noise_3d generates a 3D texture of perlin noise. Its parameters are:

  • shape: shape of the generated array (tuple of 3 ints)
  • res: number of periods of noise to generate along each axis (tuple of 3 ints)
  • tileable: if the noise should be tileable along each axis (tuple of 3 bools)

Note: shape must be a multiple of res

The function generate_fractal_noise_2d combines several octaves of 3D perlin noise to make 3D fractal noise. Its parameters are:

  • shape: shape of the generated array (tuple of 3 ints)
  • res: number of periods of noise to generate along each axis (tuple of 3 ints)
  • octaves: number of octaves in the noise (int)
  • persistence: scaling factor between two octaves (float)
  • lacunarity: frequency factor between two octaves (float)
  • tileable: if the noise should be tileable along each axis (tuple of 3 bools)

Note: shape must be a multiple of lacunarity^(octaves-1)*res

Recipes

Note these snippets require matplotlib.

2D Perlin and Fractal Noise

import matplotlib.pyplot as plt
import numpy as np
from perlin_numpy import (
    generate_perlin_noise_2d, generate_fractal_noise_2d
)

np.random.seed(0)
noise = generate_perlin_noise_2d((256, 256), (8, 8))
plt.imshow(noise, cmap='gray', interpolation='lanczos')
plt.colorbar()

np.random.seed(0)
noise = generate_fractal_noise_2d((256, 256), (8, 8), 5)
plt.figure()
plt.imshow(noise, cmap='gray', interpolation='lanczos')
plt.colorbar()
plt.show()

2D Perlin noise 2D fractal noise

3D Fractal Noise

import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np
from perlin_numpy import generate_fractal_noise_3d

np.random.seed(0)
noise = generate_fractal_noise_3d(
    (32, 256, 256), (1, 4, 4), 4, tileable=(True, False, False)
)

fig = plt.figure()
images = [
    [plt.imshow(
        layer, cmap='gray', interpolation='lanczos', animated=True
    )]
    for layer in noise
]
animation_3d = animation.ArtistAnimation(fig, images, interval=50, blit=True)
plt.show()

3D fractal noise

3D Perlin Noise

import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np
from perlin_numpy import generate_perlin_noise_3d

np.random.seed(0)
noise = generate_perlin_noise_3d(
    (32, 256, 256), (1, 4, 4), tileable=(True, False, False)
)

fig = plt.figure()
images = [
    [plt.imshow(
        layer, cmap='gray', interpolation='lanczos', animated=True
    )]
    for layer in noise
]
animation_3d = animation.ArtistAnimation(fig, images, interval=50, blit=True)
plt.show()

3D Perlin noise

perlin-numpy's People

Contributors

laurencewarne avatar pvigier avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

perlin-numpy's Issues

fractal noise 2d with periodic boundaries / tileable

Hi! Thanks for making the code available, I'm trying to use it to generate a high-resolution texture/topography for a planetary science simulation. The idea would be to obtain something similar to this map (which was obtained by a double integration and filtering of a white noise grid)
2fft_whitenoise_gmt
I manage to obtain something quite similar using your code with res=1, octaves=8 and persistence=0.8
perlin2d_fractal_periodic2
It's quite good, but it clearly shows some effects of the mirroring I used to make it periodic as the former one. I actually found an interesting answer about how to make a tileable (i.e., with periodic boundaries) fractal noise here (see the most voted answer by Boojum from 2012) but I'm struggling to see how to adapt it to your code. Would you say it's easily possible?
Also, I'm having some trouble with your shape vs res vs octaves condition: I get the impressione that the program does not allow more than 10 octaves for a 1024 x 1024 shape. I also tried to use a res=1, octave=16 on a 256x256 tile, but I get a

ValueError: operands could not be broadcast together with shapes (256,256,2) (0,0,2)

message. What I'm I misunderstanding in your condition that shape = Nresoctaves?
Thanks!

Question about the video

Hi, thanks for the good code!

I was wondering how you generated the video in the main page -- what parameter are you smoothly changing to obtain that effect?

Thanks again.

Speeding up with Numexpr

Hi Pierre,

I love this generator, its working great for me! However, I would like to be able to use this to generate arrays with minimum of 300 elements in each dimension and it's quite slow at the moment. Have you ever heard of the Numexpr package? It's able to perform basic operations on large arrays much faster than NumPy. Maybe this would be able to reduce the time taken to run your generator?

For example, your 'f(t)' function (line5) could be written using Numexpr as follows:

import numexpr as ne

def f(t):
return ne.evaluate('6t**5 - 15t4 + 10*t3')

This will massively reduce the time. Just a suggestion for you!

Base offset

It would be nice to have the ability to specify the base offset for the coordinates of the generated array.

I need to generate the terrain height map for the scene, but since the player is moving I also need to generate the map with respect to the player position. This could be easily achieved with base offset similar to the one the vnoise library uses: https://github.com/plottertools/vnoise

Choppy value distribution

I noticed that the noise generated by this library shows some sort of choppiness. I'm not entirely sure if this is due to how the image is being saved vs. generated, but it's present in the example image in your readme. Here's the second readme noise map's histogram:

image

It doesn't seem to be caused by octaves. Here's a smoother one:

image

Perhaps there's rounding going on somewhere it shouldn't be?

Also, it isn't just GIMP. I tried a few different tools and they all show similar histogram anomalies.

Add numba support to perlin2d noise generator

Numba support decrease the computation time up to nearly 5 times. No much changes are required

import numpy as np
from numba import njit

@njit
def interpolant(t: np.ndarray):
    return t*t*t*(t*(t*6 - 15) + 10)

@njit
def generate_perlin_noise_2d(
        shape, res, tileable=(False, False), interpolant=interpolant
):
    """Generate a 2D numpy array of perlin noise.
    Args:
        shape: The shape of the generated array (tuple of two ints).
            This must be a multple of res.
        res: The number of periods of noise to generate along each
            axis (tuple of two ints). Note shape must be a multiple of
            res.
        tileable: If the noise should be tileable along each axis
            (tuple of two bools). Defaults to (False, False).
        interpolant: The interpolation function, defaults to
            t*t*t*(t*(t*6 - 15) + 10).
    Returns:
        A numpy array of shape shape with the generated noise.
    Raises:
        ValueError: If shape is not a multiple of res.
    """
    delta = (res[0] / shape[0], res[1] / shape[1])
    d = (shape[0] // res[0], shape[1] // res[1])
    xvals = np.arange(0,res[0], delta[0])
    yvals = np.arange(0,res[1], delta[1])
    grid = np.empty((2, len(xvals), len(yvals)))
    for j, y in enumerate(yvals):
        for i, x in enumerate(xvals):
            grid[0][i, j] = x
    yy = np.empty((len(xvals), len(yvals)))
    for i, x in enumerate(xvals):
        grid[1][i, :] = yvals
    grid = grid.transpose(1, 2, 0) % 1
    # Gradients
    angles = 2*np.pi*np.random.rand(res[0]+1, res[1]+1)
    gradients = np.dstack((np.cos(angles), np.sin(angles)))
    if tileable[0]:
        gradients[-1,:] = gradients[0,:]
    if tileable[1]:
        gradients[:,-1] = gradients[:,0]
    grad_matrix = np.empty((d[0] * gradients.shape[0], d[1] * gradients.shape[1], 2))
    for i in range(gradients.shape[0]):
        for j in range(gradients.shape[1]):
            grad_matrix[i * d[0] : (i+1) * d[0], j * d[1] : (j+1) * d[1]] = gradients[i, j]
    gradients = grad_matrix
        
    g00 = gradients[    :-d[0],    :-d[1]]
    g10 = gradients[d[0]:     ,    :-d[1]]
    g01 = gradients[    :-d[0],d[1]:     ]
    g11 = gradients[d[0]:     ,d[1]:     ]
    # Ramps
    n00 = np.sum(np.dstack((grid[:,:,0]  , grid[:,:,1]  )) * g00, 2)
    n10 = np.sum(np.dstack((grid[:,:,0]-1, grid[:,:,1]  )) * g10, 2)
    n01 = np.sum(np.dstack((grid[:,:,0]  , grid[:,:,1]-1)) * g01, 2)
    n11 = np.sum(np.dstack((grid[:,:,0]-1, grid[:,:,1]-1)) * g11, 2)
    # Interpolation
    t = interpolant(grid)
    n0 = n00*(1-t[:,:,0]) + t[:,:,0]*n10
    n1 = n01*(1-t[:,:,0]) + t[:,:,0]*n11
    t1 = (1-t[:,:,1])*n0
    t2 = t[:,:,1]*n1
    sum_t = t1 + t2
    mult_s2 = np.sqrt(2)*sum_t
    return mult_s2

@njit
def generate_fractal_noise_2d(
        shape, res, octaves=1, persistence=0.5,
        lacunarity=2, tileable=(False, False),
        interpolant=interpolant
):
    """Generate a 2D numpy array of fractal noise.
    Args:
        shape: The shape of the generated array (tuple of two ints).
            This must be a multiple of lacunarity**(octaves-1)*res.
        res: The number of periods of noise to generate along each
            axis (tuple of two ints). Note shape must be a multiple of
            (lacunarity**(octaves-1)*res).
        octaves: The number of octaves in the noise. Defaults to 1.
        persistence: The scaling factor between two octaves.
        lacunarity: The frequency factor between two octaves.
        tileable: If the noise should be tileable along each axis
            (tuple of two bools). Defaults to (False, False).
        interpolant: The, interpolation function, defaults to
            t*t*t*(t*(t*6 - 15) + 10).
    Returns:
        A numpy array of fractal noise and of shape shape generated by
        combining several octaves of perlin noise.
    Raises:
        ValueError: If shape is not a multiple of
            (lacunarity**(octaves-1)*res).
    """
    noise = np.zeros(shape)
    frequency = 1
    amplitude = 1
    for _ in range(octaves):
        noise += amplitude * generate_perlin_noise_2d(
            shape, (frequency*res[0], frequency*res[1]), tileable, interpolant
        )
        frequency *= lacunarity
        amplitude *= persistence
    return noise

Works for numba==0.53.1 and numpy==1.21.1

Numerical issues

When I run generate_perlin_noise_2d((721, 1281), (7, 7), (True, True))
I get the following issue:
ValueError: operands could not be broadcast together with shapes (722,1281,2) (721,1281,2)

This can be fixed by enumerating the grid manually instead of using np.mgrid:

    grid = np.stack(np.meshgrid(
        np.arange(0, shape[1]) * res[1] / shape[1],
        np.arange(0, shape[0]) * res[0] / shape[0],
    )[::-1], axis=-1)

ValueError at high Octaves & Lacunarity

When using larger octaves and lacunarity (e.g. 2 & 3 respectively) the following error occurs in perlin2d

ValueError: operands could not be broadcast together with shapes (1024,1024,2) (1023,1023,2)

At the line

n00 = np.sum(np.dstack((grid[:,:,0] , grid[:,:,1] )) * g00, 2)

I believe this is because the delta tuple doesnt consider what happens when there's a remainder from shape/resolution

Question regarding the number of octaves in fractal noise generator

Hi,

As per the documentation of generate_fractal_noise_2d function, the third argument is the number of octaves in the noise. If the number of octaves is 1, will this function generate the perlin noise since the fractal noise is nothing but multiple octaves of perlin noise combined. So, when the octave is 1 is the perlin noise generated?

Thanks in advance

Does not take into account non integer divisors

I got this error when generating noise

Traceback (most recent call last):
  File "c:/Software/Test Software/PerlinNoise.py", line 93, in <module>
    noise = terrain.generate_fractal_noise_2d((100,100),(10,10),3)
  File "c:/Software/Test Software/PerlinNoise.py", line 85, in generate_fractal_noise_2d
    noise += amplitude * self.generate_perlin_noise_2d(shape, (int(frequency*res[0]), int(frequency*res[1])))
  File "c:/Software/Test Software/PerlinNoise.py", line 69, in generate_perlin_noise_2d
    n00 = np.sum(grid * g00, 2)
ValueError: operands could not be broadcast together with shapes (100,100,2) (80,80,2)

I realized it was due to this line

        delta = (res[0] / shape[0], res[1] / shape[1])
        d = (shape[0] // res[0], shape[1] // res[1])
        grid = np.mgrid[0:res[0]:delta[0],0:res[1]:delta[1]].transpose(1, 2, 0) % 1
        # Gradients
        angles = 2*np.pi*self.random.rand(res[0]+1, res[1]+1)
        gradients = np.dstack((np.cos(angles), np.sin(angles)))
        g00 = gradients[0:-1,0:-1].repeat(d[0], 0).repeat(d[1], 1)
        g10 = gradients[1:,0:-1].repeat(d[0], 0).repeat(d[1], 1)
        g01 = gradients[0:-1,1:].repeat(d[0], 0).repeat(d[1], 1)
        g11 = gradients[1:,1:].repeat(d[0], 0).repeat(d[1], 1)

Grid must be a integer multiple of the gradiant size when repeating the gradient or else the sizes will not line up.

for the above example the shape of the grid was (100, 100, 2)
and the shape of g00 was (80, 80, 2). so they were not able to be broadcast by the np.sum function.

This was my fix

    def generate_perlin_noise_2d(self,shape, res):
        def f(t):
            return 6*t**5 - 15*t**4 + 10*t**3

        delta = (res[0] / shape[0], res[1] / shape[1])
        d = (shape[0] // res[0], shape[1] // res[1])
        grid = np.mgrid[0:res[0]:delta[0],0:res[1]:delta[1]].transpose(1, 2, 0) % 1
        # Gradients
        angles = 2*np.pi*self.random.rand(res[0]+1, res[1]+1)
        gradients = np.dstack((np.cos(angles), np.sin(angles)))
        if d[0]*delta[0] != 1:
            d = (d[0] + 1,d[1])
        if d[1]*delta[1] != 1:
            d = (d[0],d[1] + 1)
        g00 = gradients[0:-1,0:-1].repeat(d[0], 0).repeat(d[1], 1)
        g10 = gradients[1:,0:-1].repeat(d[0], 0).repeat(d[1], 1)
        g01 = gradients[0:-1,1:].repeat(d[0], 0).repeat(d[1], 1)
        g11 = gradients[1:,1:].repeat(d[0], 0).repeat(d[1], 1)
        # Ramps
        n00 = np.sum(grid * g00[:len(grid),:len(grid[0])], 2)
        n10 = np.sum(np.dstack((grid[:,:,0]-1, grid[:,:,1])) * g10[:len(grid),:len(grid[0])], 2)
        n01 = np.sum(np.dstack((grid[:,:,0], grid[:,:,1]-1)) * g01[:len(grid),:len(grid[0])], 2)
        n11 = np.sum(np.dstack((grid[:,:,0]-1, grid[:,:,1]-1)) * g11[:len(grid),:len(grid[0])], 2)
        # Interpolation
        t = f(grid)
        n0 = n00*(1-t[:,:,0]) + t[:,:,0]*n10
        n1 = n01*(1-t[:,:,0]) + t[:,:,0]*n11

        return np.sqrt(2)*((1-t[:,:,1])*n0 + t[:,:,1]*n1)

The idea was just to repeat one more time then slice the matrix for the sum.
This way any number should be able to be entered and there are still no loops.

I don't know if this is even an issue for most people, I just wanted to put it here.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.