In [1]:
using FFTW
using Plots
In [2]:
N = 256 # size in grid cells
M = 4 # size of a grid cell in pixels

# make a big 2D array of gradients
values = rand(N, N, 2)

# distribute the values in a [-1, 1] box
grad_box = 2.0.*values .- 1.0;

# distribute the values in a unit circle
grad_circle = Array(values)
grad_circle[:,:,1] = sqrt.(values[:,:,1]).*cos.(2Ï€*values[:,:,2])
grad_circle[:,:,2] = sqrt.(values[:,:,1]).*sin.(2Ï€*values[:,:,2])

norm = hypot.(grad_circle[:,:,1], grad_circle[:,:,2])
grad_norm = grad_circle./norm
;
In [3]:
# make a scatter plot of the gradients to double check the distributions
plot(
    scatter(grad_box[:,1,1], grad_box[:,1,2]),
    scatter(grad_circle[:,1,1], grad_circle[:,1,2]),
    scatter(grad_norm[:,1,1], grad_norm[:,1,2]),
    aspectratio=1
)
Out[3]:
In [4]:
# hermite(x) = (3 - 2*x)*x*x
hermite(x) = ((6*x - 15)*x + 10)*x*x*x
lerp(a, b, t) = (1 - t)*a + t*b
perlin_dotgrad(ix, iy, x, y, grads) = (x - ix)*grads[ix%N + 1, iy%N + 1, 1] + (y - iy)*grads[ix%N + 1, iy%N + 1, 2]

function perlin(grads)
    noise = zeros(N*M, N*M)
    for j in 1 : M*N
        for i in 1 : M*N
            x = (i - 1)/M; ix = Integer(floor(x)); fx = x - ix;
            y = (j - 1)/M; iy = Integer(floor(y)); fy = y - iy;
        	noise[i, j] = lerp(
        		lerp(perlin_dotgrad(ix + 0, iy + 0, x, y, grads), perlin_dotgrad(ix + 1, iy + 0, x, y, grads), hermite(fx)),
        		lerp(perlin_dotgrad(ix + 0, iy + 1, x, y, grads), perlin_dotgrad(ix + 1, iy + 1, x, y, grads), hermite(fx)),
        		hermite(fy)
        	);
        end
    end
    return noise
end
Out[4]:
perlin (generic function with 1 method)
In [5]:
# render the noise functions with the different gradients
noise_box = perlin(grad_box)
noise_circle = perlin(grad_circle)
noise_norm = perlin(grad_norm)

# plot a slice of the noise functions 
r = 1:128
plot(
    heatmap(noise_box[r, r], c = :grays, title="box"),
    heatmap(noise_circle[r, r], c = :grays, title="circle"),
    heatmap(noise_norm[r, r], c = :grays, title="norm"),
    size=(1024,1024), aspectratio=1
)
Out[5]:
In [6]:
# get the spectra of the noise functions
fft_norm = abs.(fft(noise_norm))
fft_box = abs.(fft(noise_box))
fft_circle = abs.(fft(noise_circle))

# plot the FFT of the noise functions
r = 1: 300
plot(
    heatmap(fft_box[r, r], title="box"),
    heatmap(fft_circle[r, r], title="circle"),
    heatmap(fft_norm[r, r], title="norm"),
    size=(1024, 1024), aspectratio=1,
)
Out[6]:

Ok! I'm actually quite surprised here. I could swear I remember reading that you end up with diagonal artifacts if the gradients weren't isotropic, but... nope!

You get a slight difference in the levels which makes sense because the average gradient length changes, but otherwise the shape looks identical.

In [7]:
# plot a histogram of the values to see the contrast
len = N*N*M*M
histogram([
    reshape(noise_box, len),
    reshape(noise_circle, len),
    reshape(noise_norm, len),
], labels=["box" "circle" "norm"])
Out[7]:

!! Legitimately surprised by this one. I figured the box version would be more contrasty due to the larger average gradient, but what is going on with the normalized version!?

Are those spikes some sort of interaction with the lattice or something? Why wouldn't the gradients inside the unit circle have the same artifacts?

In [8]:
# calculate the autocorrelation of the noise functions
ac_box = ifft(abs.(fft(noise_box)))
ac_circle = ifft(abs.(fft(noise_circle)))
ac_norm = ifft(abs.(fft(noise_norm)))

# zero out the constant no-offset bucket so it doesn't overpower the graph
ac_box[1,1] = 0
ac_circle[1,1] = 0
ac_norm[1,1] = 0

r = 1:8
plot(
    heatmap(abs.(ac_box[r,r])),
    heatmap(abs.(ac_circle[r,r])),
    heatmap(abs.(ac_norm[r,r])),
    size=(1024,512), aspectratio=1,
)
Out[8]:

Eh, no real difference here either. I was mostly just curious.