-
-
Save eevee/26f547457522755cb1fb8739d0ea89a1 to your computer and use it in GitHub Desktop.
"""Perlin noise implementation.""" | |
# Licensed under ISC | |
from itertools import product | |
import math | |
import random | |
def smoothstep(t): | |
"""Smooth curve with a zero derivative at 0 and 1, making it useful for | |
interpolating. | |
""" | |
return t * t * (3. - 2. * t) | |
def lerp(t, a, b): | |
"""Linear interpolation between a and b, given a fraction t.""" | |
return a + t * (b - a) | |
class PerlinNoiseFactory(object): | |
"""Callable that produces Perlin noise for an arbitrary point in an | |
arbitrary number of dimensions. The underlying grid is aligned with the | |
integers. | |
There is no limit to the coordinates used; new gradients are generated on | |
the fly as necessary. | |
""" | |
def __init__(self, dimension, octaves=1, tile=(), unbias=False): | |
"""Create a new Perlin noise factory in the given number of dimensions, | |
which should be an integer and at least 1. | |
More octaves create a foggier and more-detailed noise pattern. More | |
than 4 octaves is rather excessive. | |
``tile`` can be used to make a seamlessly tiling pattern. For example: | |
pnf = PerlinNoiseFactory(2, tile=(0, 3)) | |
This will produce noise that tiles every 3 units vertically, but never | |
tiles horizontally. | |
If ``unbias`` is true, the smoothstep function will be applied to the | |
output before returning it, to counteract some of Perlin noise's | |
significant bias towards the center of its output range. | |
""" | |
self.dimension = dimension | |
self.octaves = octaves | |
self.tile = tile + (0,) * dimension | |
self.unbias = unbias | |
# For n dimensions, the range of Perlin noise is ±sqrt(n)/2; multiply | |
# by this to scale to ±1 | |
self.scale_factor = 2 * dimension ** -0.5 | |
self.gradient = {} | |
def _generate_gradient(self): | |
# Generate a random unit vector at each grid point -- this is the | |
# "gradient" vector, in that the grid tile slopes towards it | |
# 1 dimension is special, since the only unit vector is trivial; | |
# instead, use a slope between -1 and 1 | |
if self.dimension == 1: | |
return (random.uniform(-1, 1),) | |
# Generate a random point on the surface of the unit n-hypersphere; | |
# this is the same as a random unit vector in n dimensions. Thanks | |
# to: http://mathworld.wolfram.com/SpherePointPicking.html | |
# Pick n normal random variables with stddev 1 | |
random_point = [random.gauss(0, 1) for _ in range(self.dimension)] | |
# Then scale the result to a unit vector | |
scale = sum(n * n for n in random_point) ** -0.5 | |
return tuple(coord * scale for coord in random_point) | |
def get_plain_noise(self, *point): | |
"""Get plain noise for a single point, without taking into account | |
either octaves or tiling. | |
""" | |
if len(point) != self.dimension: | |
raise ValueError("Expected {} values, got {}".format( | |
self.dimension, len(point))) | |
# Build a list of the (min, max) bounds in each dimension | |
grid_coords = [] | |
for coord in point: | |
min_coord = math.floor(coord) | |
max_coord = min_coord + 1 | |
grid_coords.append((min_coord, max_coord)) | |
# Compute the dot product of each gradient vector and the point's | |
# distance from the corresponding grid point. This gives you each | |
# gradient's "influence" on the chosen point. | |
dots = [] | |
for grid_point in product(*grid_coords): | |
if grid_point not in self.gradient: | |
self.gradient[grid_point] = self._generate_gradient() | |
gradient = self.gradient[grid_point] | |
dot = 0 | |
for i in range(self.dimension): | |
dot += gradient[i] * (point[i] - grid_point[i]) | |
dots.append(dot) | |
# Interpolate all those dot products together. The interpolation is | |
# done with smoothstep to smooth out the slope as you pass from one | |
# grid cell into the next. | |
# Due to the way product() works, dot products are ordered such that | |
# the last dimension alternates: (..., min), (..., max), etc. So we | |
# can interpolate adjacent pairs to "collapse" that last dimension. Then | |
# the results will alternate in their second-to-last dimension, and so | |
# forth, until we only have a single value left. | |
dim = self.dimension | |
while len(dots) > 1: | |
dim -= 1 | |
s = smoothstep(point[dim] - grid_coords[dim][0]) | |
next_dots = [] | |
while dots: | |
next_dots.append(lerp(s, dots.pop(0), dots.pop(0))) | |
dots = next_dots | |
return dots[0] * self.scale_factor | |
def __call__(self, *point): | |
"""Get the value of this Perlin noise function at the given point. The | |
number of values given should match the number of dimensions. | |
""" | |
ret = 0 | |
for o in range(self.octaves): | |
o2 = 1 << o | |
new_point = [] | |
for i, coord in enumerate(point): | |
coord *= o2 | |
if self.tile[i]: | |
coord %= self.tile[i] * o2 | |
new_point.append(coord) | |
ret += self.get_plain_noise(*new_point) / o2 | |
# Need to scale n back down since adding all those extra octaves has | |
# probably expanded it beyond ±1 | |
# 1 octave: ±1 | |
# 2 octaves: ±1½ | |
# 3 octaves: ±1¾ | |
ret /= 2 - 2 ** (1 - self.octaves) | |
if self.unbias: | |
# The output of the plain Perlin noise algorithm has a fairly | |
# strong bias towards the center due to the central limit theorem | |
# -- in fact the top and bottom 1/8 virtually never happen. That's | |
# a quarter of our entire output range! If only we had a function | |
# in [0..1] that could introduce a bias towards the endpoints... | |
r = (ret + 1) / 2 | |
# Doing it this many times is a completely made-up heuristic. | |
for _ in range(int(self.octaves / 2 + 0.5)): | |
r = smoothstep(r) | |
ret = r * 2 - 1 | |
return ret |
If anybody is interested, here is an implementation as a C++ python module (with a lot of speed hacks) that exposes (at least) the same interface (and also shares most of the algorithm).
On my computer, it seems to be more or less 10-20 times faster.
Hello Veluca93 , what sequence should i execute the files in C++ ?
Usage example? Gives me constantly values of 0.0.
Try calling the factory with values inside the interval [0-1].
For example
for i in range(frameSize):
for j in range(frameSize):
noise[i,j] = PNFactory(i/frameSize,j/frameSize)
instead of
for i in range(frameSize):
for j in range(frameSize):
noise[i,j] = PNFactory(i,j)
The improved version of Ken Perlin uses 6t**5 - 15t**4 + 10t**3
instead of 3t**2 - 2t**3
as smoothstep function.
Original blog post with instructions and explanation is here https://eev.ee/blog/2016/05/29/perlin-noise/
Is this O(1) to calculate any random location on the continuous curve, or is this O(n)?
Correct me if I'm wrong but there is an inconsistency in this snipet:
for grid_point in product(*grid_coords):
if grid_point not in self.gradient:
self.gradient[grid_point] = self._generate_gradient()
gradient = self.gradient[grid_point]
dot = 0
for i in range(self.dimension):
dot += gradient[i] * (point[i] - grid_point[i])
dots.append(dot)
When you call self.gradient[grid_point] (say grid_point = [2,3]) you are calling the gradient at row 2 and column 3; But in "position" space it is actually the reverse: the second row is on y = 2 and third col at x = 3 so when you make (point[i] - grid_point[i]) you're calculating the wrong difference because it is reversed. It should be grid_point[::-1][i] at least for 2D;
This code is very useful for terrain generation. I'll try using it in my game!
If anybody is interested, here is an implementation as a C++ python module (with a lot of speed hacks) that exposes (at least) the same interface (and also shares most of the algorithm).
On my computer, it seems to be more or less 10-20 times faster.