Planet Gen

In [ ]:
!pip3 install --user numpy matplotlib pillow
In [1]:
from matplotlib import pylab, pyplot, cm
import numpy

%matplotlib inline
pylab.rcParams['figure.figsize'] = (7, 7)
pylab.rcParams['animation.writer'] = 'ffmpeg'
pylab.rcParams['animation.codec'] = 'libvp8'
pylab.rcParams['animation.html'] = 'html5'

Diamond Square

In [2]:
def ds(rp=2, base=.5, wrap=(False, False)):
    power = 1
    data = None
    
    while True:
        # length of side
        side = 2**power + 1
        sz = [side, side]
        
        # create new numpy image
        image = numpy.zeros(sz)
                
        # if wrapped on either (x,y)
        if wrap[0]:
            sz[1] -= 1
        if wrap[1]:
            sz[0] -= 1
            
        # copy & expand last image
        if type(data) != type(None):
            for x in range(data.shape[0]):
                for y in range(data.shape[1]):
                    image[(2*x) % sz[0], (2*y) % sz[0]] = data[x, y]

        # set corners to random data
        else:
            for x in ((0, 0), (sz[0]-1, 0), (0, sz[1]-1), (sz[0]-1, sz[1]-1)):
                image[x] = max(min(image[x] + (numpy.random.random() - .5) + base, 1.0), 0.0)

        # generate new pixels
        for x in range(1, side, 2):
            for y in range(1, side, 2):
                xp1 = (x + 1) % sz[0]
                yp1 = (y + 1) % sz[1]
                mod = (numpy.random.random() - 0.5)/rp**power
                image[x-1, y] = mod + (image[x-1, yp1] + image[x-1, y-1]) / 2
                image[xp1, y] = mod + (image[xp1, yp1] + image[xp1, y-1]) / 2
                image[x, y-1] = mod + (image[x-1, y-1] + image[xp1, y-1]) / 2
                image[x, yp1] = mod + (image[x-1, yp1] + image[xp1, yp1]) / 2
                image[x, y] = mod + (image[x-1, y-1] + image[x-1, yp1] + image[xp1, y-1] + image[xp1, yp1]) / 4

        # normalize all pixels
        for x in numpy.nditer(image, op_flags=['readwrite']):
            x[...] = x if x >= 0. else 0.
            x[...] = x if x <= 1. else 1.
        
        # save current image
        data = image[0:sz[0], 0:sz[1]]
        power += 1
                
        # expand to size (side, side)
        if wrap[1]:
            image[sz[0]] = image[0]
        if wrap[0]:
            image[..., sz[1]] = image[..., 0]
        
        # return image
        yield image
In [3]:
from matplotlib import animation

# save the first 9 iterations
ims = []
fig, ax = pyplot.subplots()
ax.set_axis_off()
for x,y in zip(ds(1.8, .5), range(9)):
    ims.append([pyplot.imshow(x, extent=[0,1,0,1])])
pyplot.close(fig)

# animate!
animation.ArtistAnimation(fig, ims, interval=750, blit=True)
Out[3]:
In [4]:
# diamond square
def dsn(n, *args):
    p_gen = ds(*args)
    for it, s in zip(p_gen, range(n)):
        square = it
    return p_gen, square

p_gen, planet = dsn(7, 1.8, .5, (True, True))

# https://matplotlib.org/examples/color/colormaps_reference.html
pyplot.imshow(cm.Vega20c(planet))
Out[4]:
<matplotlib.image.AxesImage at 0x7f5109282550>

Potential Lanscapes

In [5]:
# show potential ds output
fig = pylab.figure()
fig.set_figwidth(15)

for n in range(0,6):
    fig.add_subplot(2, 3, n+1)
    tmp_gen = ds(1.8, .5, (False, False))
    for it, s in zip(tmp_gen, range(6)):
        copy = it
    pyplot.imshow(cm.Vega20c(copy))
pylab.show()

Worley Noise

In [6]:
def worley(image, n, nth=2, coef=.5):
    import heapq
    
    # image shape
    sz = image.shape
    
    # maximum possible distance between two points
    max_dist = (sz[0]**2 + sz[1]**2)**.5
    
    # random points on map
    points = numpy.random.randint(0, min(sz), (n, 2))
    
    # wrapping distance function
    sd = lambda a,b,side: min((a - b) % side, (b - a) % side)
    
    for x in range(sz[0]):
        for y in range(sz[1]):
            # compute distances (wrapping)
            dist = [(sd(rx,x,sz[0])**2 + sd(ry,y,sz[1])**2)**.5 for (rx, ry) in points]
            
            # compute noise, using nth shortest distance
            noise = coef * heapq.nsmallest(nth, dist)[-1] / max_dist
            
            # add noise and normalize
            image[x, y] = image[x, y] - noise
            image[x, y] = max(min(image[x, y], 1.), 0.)
In [7]:
copy = planet.copy()
fig = pylab.figure()
fig.set_figwidth(15)

fig.add_subplot(2, 3, 1)
pyplot.imshow(cm.Vega20c(copy))
pyplot.title('original')

# show potential worley output
for n in range(1,6):
    fig.add_subplot(2, 3, n+1)
    copy = planet.copy()
    worley(copy, n, coef=-0.5)
    pyplot.imshow(cm.Vega20c(copy))
    pyplot.title('worley({})'.format(n))
pylab.show()

Shadows

In [8]:
def shadow(image, a=.5, b=0, frac=.05, tol=.25, below=True):
    b *= image.shape[0]
    tol *= image.shape[0]
    for x in range(image.shape[0]):
        for y in range(image.shape[1]):
            fx = a*x + b
            if y - fx < tol and y > fx:
                image[x, y, 0:3] *= (y - fx)/tol * (1 - frac) + frac
            elif (below and y <= fx) or (not below and y >= fx):
                image[x, y, 0:3] *= frac
In [9]:
copy = cm.Vega20c(planet.copy())
shadow(copy)
pyplot.imshow(copy)
Out[9]:
<matplotlib.image.AxesImage at 0x7f5106b8d908>

Sphere Distortion

In [10]:
def lens_distort(image, alpha = []):
    from math import pi as π
    from math import atan2, cos, sin, asin, hypot
    
    # copy image
    copy = image.copy()
    # side
    side = image.shape[0]

    for cx in range(side):
        for cy in range(side):
            # normalized coordinates
            nx, ny = cx/side - 0.5, cy/side - 0.5
            # normalized visual distance
            nr = hypot(nx, ny)
            if nr > 0.5:
                image[cx, cy] = [0., 0., 0., 0.]
                continue
            # angle of x,y
            Ï• = atan2(ny, nx)
            # distorted distance
            d = asin(2*nr) / π
            # map pixel on new image -> pixel on old image
            image[cx, cy] = copy[round(side*(0.5 + d * cos(Ï•))), round(side*(0.5 + d * sin(Ï•)))]
In [11]:
copy = cm.Vega20c(planet.copy())

fig = pylab.figure()
fig.set_figwidth(15)

fig.add_subplot(1, 2, 1)
pyplot.imshow(copy)

fig.add_subplot(1, 2, 2)
copy = cm.Vega20c(planet.copy())
lens_distort(copy)
pyplot.imshow(copy)

pylab.show()

This effect will be more apparent if we superimpose a grid.

In [12]:
def grid(image, count=10, width=0.01, color=[0., 1., 0., 1.]):
    side = image.shape[0]
    width = round(width*side)
    div = side / count
    
    # latitude lines
    cols = [round(div*x) for x in range(count)]
    for w in range(1, width):
        cols += [x + w for x in cols]
    # longitude lines
    rows = [round(div*x) for x in range(count)]
    for w in range(1, width):
        rows += [x + w for x in rows]
        
    image[:,cols] = color
    image[rows,:] = color
In [13]:
copy = cm.Vega20c(planet.copy())
grid(copy)

fig = pylab.figure()
fig.set_figwidth(15)

fig.add_subplot(1, 2, 1)
pyplot.imshow(copy)

fig.add_subplot(1, 2, 2)
lens_distort(copy)
pyplot.imshow(copy)

pylab.show()

Atmosphere

In [14]:
def edge(image, thick=0.01, color=[1., 0., 0., 0.1]):
    from math import hypot
    side = image.shape[0]
    for x in range(side):
        for y in range(side):
            # there should be pixel composition here...
            nr = hypot(x/side - 0.5, y/side - 0.5)
            if 0.5 < nr and nr < 0.5 + thick:
                image[x,y] = color
In [15]:
copy = cm.Vega20c(planet.copy())
lens_distort(copy)
edge(copy)
pyplot.imshow(copy)
Out[15]:
<matplotlib.image.AxesImage at 0x7f5106898da0>

Background

In [16]:
from math import hypot

def star(image, cx, cy, rad, color):
    xrange = range(max(0, cx-rad), min(len(image), cx+rad+1))
    yrange = range(max(0, cy-rad), min(len(image), cy+rad+1))

    for x in xrange:
        for y in yrange:
            if hypot(x-cx, y-cy) < rad:
                image[x,y] = color

def background(shape, color=[1., 1., 1., 1.], maxrad=3, count=500):    
    bg = numpy.full(shape, color)
    for x,y in numpy.random.randint(0, shape[0], (count, 2)):
        rad = numpy.random.randint(0, maxrad)
        col = numpy.append(numpy.full((3), numpy.random.rand(1)), 1.)
        star(bg, x, y, rad, col)
    return bg
    
def set_bg(image, bg):
    mask = numpy.all(image == [0., 0., 0., 0.], axis=2)
    image[mask] = bg[mask]
In [17]:
copy = cm.Vega20c(planet.copy())
lens_distort(copy)
set_bg(copy, background(copy.shape))
pyplot.imshow(copy)
Out[17]:
<matplotlib.image.AxesImage at 0x7f5106ad2b70>

Animate

In [18]:
def gifify(gen, time, *funcs):
    global im
    from PIL import Image
    from functools import reduce
    
    fig, ax = pyplot.subplots()
    ax.set_axis_off()
    
    data = zip(zip(*gen), range(time))
    
    im = None
        
    def draw(n):
        global im
        
        # get data
        try:
            g, t = data.__next__()
        except StopIteration:
            return []
        
        # compose layers
        print('\r{}/{}'.format(t,time-1), end='')
        images = (Image.fromarray(numpy.uint8(x * 255), 'RGBA') for x in g)
        frame = reduce(lambda x,y: Image.alpha_composite(x, y), images)
        frame = numpy.array(frame, dtype=float) / 255.0
        
        # process image
        for func in funcs:
            func(frame)
            
        # draw (using thread-unsafe memory optimization)
        if not im:
            im = pyplot.imshow(frame, animated=True)
        else:
            im.set_data(frame)

        return [im]

    anim = animation.FuncAnimation(fig, draw, frames=time, interval=75, repeat_delay=75, blit=True)
    pyplot.close(fig)
    return anim
In [19]:
def roll(image, inc=1, axis=1):
    x = 0
    while True:
        x = (x + inc) % image.shape[0]
        yield numpy.roll(image, int(x), axis=axis)
In [20]:
copy = cm.Vega20c(planet.copy())
gifify((roll(copy),), len(copy))
128/128
Out[20]:

Clouds

In [28]:
c_gen = ds(1.5, .35, (True, True))
for it, s in zip(c_gen, range(7)):
    clouds = it
 
# worley noise
worley(clouds, 5, 2, coef=0.75)
clouds = cm.gray(clouds)
pyplot.imshow(clouds)

for x in range(clouds.shape[0]):
    for y in range(clouds.shape[1]):
        clouds[x, y] = [1., 1., 1., clouds[x, y, 0]]

Composition

In [29]:
copy = cm.Vega20c(planet.copy())
bg = background(copy.shape)
gen = (roll(copy, axis=(0,1)), roll(clouds, -2, 0), roll(clouds))
gifify(gen, 2*len(clouds), shadow, lens_distort, lambda x: set_bg(x, bg), edge)
257/257
Out[29]:

Hi-Res

In [30]:
# since we took advantage of generators
# we can iterate to a higher resolution
hi_p = p_gen.__next__()
hi_p = cm.Vega20c(hi_p)
pyplot.imshow(hi_p)
In [41]:
_, copy = dsn(7, 1.8, .5, (True, False))
copy = cm.Vega20c(copy)
bg = background(copy.shape)
gen = (roll(copy, axis=(1)),)
gifify(gen, len(copy), shadow, lens_distort, lambda x: set_bg(x, bg), edge)
128/128
Out[41]:
In [ ]:
# since we took advantage of generators
# we can iterate to a higher resolution
hi_p = p_gen.__next__()
hi_p = cm.Vega20c(hi_p)
In [50]:
fig = pylab.figure()
fig.set_figwidth(15)
fig.set_figheight(15)
im = pyplot.imshow(hi_p)