pvigier / perlin-numpy

A fast and simple perlin noise generator using numpy
https://pvigier.github.io/2018/06/13/perlin-noise-numpy.html
MIT License
304 stars 50 forks source link

fractal noise 2d with periodic boundaries / tileable #1

Closed steo85it closed 5 years ago

steo85it commented 5 years ago

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!

pvigier commented 5 years ago

Hi,

To make Perlin noise tileable, you must set the gradients of the right side equal to the ones of the left side and the same for top and bottom. So, here is the code to generate tileable 2D Perlin noise:

def generate_perlin_noise_2d(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*np.random.rand(res[0]+1, res[1]+1)
    gradients = np.dstack((np.cos(angles), np.sin(angles)))
    # Make the noise tileable
    gradients[-1,:] = gradients[0,:]
    gradients[:,-1] = gradients[:,0]
    # Same as before
    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(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 = 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)

Then, the fractal noise function will also generate tileable noise.

Seems, there is a typo in the condition, shape must be a multiple of 2^(octaves-1)*res. I will fix that in the README, thanks!

Hope it helps.

steo85it commented 5 years ago

Makes absolute sense, it's elegant and it also works perfectly! :) Thanks a lot for the other clarification too, now I should have what I need. perlin2d_fractal_periodic3

pvigier commented 5 years ago

You're welcome! :)

Nice picture by the way.

jjparkcv commented 5 years ago

@steo85it Hi, I'm very interested in how you generated the texture simulation. Would you share some more details or possibly the code to get your results?

Thank you.

steo85it commented 4 years ago

Hi, sorry, just saw your message. I only slightly modified the code provided by the author to adapt it to my needs (texture with a certain amplitude over a given scale, consistent with planetary terrain roughness). Plus, if you want shading, you can get hillshade here https://github.com/titusjan/hill_shading .

def generate_fractal_noise_2d(shape, res, octaves=1, persistence=0.5):

    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]))
        frequency *= 2
        amplitude *= persistence

    return noise

def generate_periodic_fractal_noise_2d(amplitude, shape, res, octaves=1, persistence=0.5):

    noise = generate_fractal_noise_2d(shape, res, octaves, persistence)

    noise *= amplitude

    return noise

if __name__ == '__main__':
    import matplotlib.pyplot as plt
    from hillshade import hill_shade

    np.random.seed(62)
    shape_text = 1024
    res_text = 2**3
    depth_text = 5
    size_stamp = 0.25
    noise = generate_periodic_fractal_noise_2d(30, (shape_text, shape_text), (res_text, res_text), depth_text, persistence=0.65)

    noise = hill_shade(noise,terrain=noise * 10)

    plt.figure()
    plt.imshow(noise, cmap='cubehelix', interpolation='None', extent=(0, 0.25, 0, 0.25))
    plt.colorbar()
    plt.savefig("tmp/test_fract_noise_res1.pdf")

This should be it. Please try and see what comes out (changing the random seed will change the output, obviously). Cheers.