3D Terrain in Python
Posted on Fri 24 January 2020 in Python • 21 min read
Generating & Visualising 3D Terrains in Python
Today, let's put together a 3D visualisation of randomly generated 'terrain' with Python. Data visualisation is an absolutely key skill in any developers pocket, as communicating both data, analysis and more is thoroughly simplified through the use of graphs. While a picture tells a thousand words, this also means that it'll bring a thousand intepretations.
This is what we are going to try and reproduce:
First off, before generating 3D terrain, it would be nice to generate 2D terrain. To achieve this, we are going to make use of Perlin Noise. Perlin noise, created by Ken Perlin in 1983, for the movie Tron, was originally developed to make more natural looking textures on surfaces. The wikipedia site has a great graphical breakdown of how the algorithm works behind the scenes.
For Perlin noise in Python, rather than implementing it ourselves, there is a package (with a quite apt name) named noise
. The noise
package contains multiple algorithms inside it for generating different types of noise.
For visualising our terrain in the first instance, we shall use matplotlib
.
As always, we import the necessary libraries for our project at the beginning. Make note of mpl_toolkits.mplot3d
, this comes along with matplotlib
and is required for plotting in 3 dimensions. If you are working in a jupyter
notebook, when you plot in 3D with matplotlib, the resulting graph will not be interactive unless the magic command %matplotlib qt
is used and pyqt5
is installed in your environment. This will create a new window for your plot where you can interact with it.
%matplotlib inline
import noise
import numpy as np
import matplotlib
from mpl_toolkits.mplot3d import axes3d
Now that we have imported all of our necessary libraries to begin with. Let's find out how to interact with the noise
package through use of the help()
function. As we can see below, there are a number of settings used when passed into any of the pnoisex
functions (replace x with amount of dimensions, eg, 2 for 2 dimensions).
help(noise.pnoise2)
help(noise.pnoise3)
shape = (50,50)
scale = 100.0
octaves = 6
persistence = 0.5
lacunarity = 2.0
Now to generate our 2D terrain! We initialise a numpy array, that will contain the values of our world. As we initalise the array with all zero values, now it is time to iterate through the empty array and fill it with Perlin Noise!
world = np.zeros(shape)
for i in range(shape[0]):
for j in range(shape[1]):
world[i][j] = noise.pnoise2(i/scale,
j/scale,
octaves=octaves,
persistence=persistence,
lacunarity=lacunarity,
repeatx=1024,
repeaty=1024,
base=42)
We have now initialised our 2 dimensional array with all the values inside for our terrain, let's plot it now! Since we are mimicking topography, let's use the 'terrain' colormap. All the available colormaps in matplotlib
are listed here: https://matplotlib.org/examples/color/colormaps_reference.html
matplotlib.pyplot.imshow(world,cmap='terrain')
Beautiful! We can now see our 'lake' off to the side and our 'mountains' over on the right.
For plotting this in 3 dimensions, we must initialise 2 more arrays which will contain the x-y co-ordinates of our world.
lin_x = np.linspace(0,1,shape[0],endpoint=False)
lin_y = np.linspace(0,1,shape[1],endpoint=False)
x,y = np.meshgrid(lin_x,lin_y)
Now it's time to plot in 3D with matplotlib, there is a note above if you are using jupyter regarding interactivity.
fig = matplotlib.pyplot.figure()
ax = fig.add_subplot(111, projection="3d")
ax.plot_surface(x,y,world,cmap='terrain')
terrain_cmap = matplotlib.cm.get_cmap('terrain')
def matplotlib_to_plotly(cmap, pl_entries):
h = 1.0/(pl_entries-1)
pl_colorscale = []
for k in range(pl_entries):
C = list(map(np.uint8, np.array(cmap(k*h)[:3])*255))
pl_colorscale.append([k*h, 'rgb'+str((C[0], C[1], C[2]))])
return pl_colorscale
terrain = matplotlib_to_plotly(terrain_cmap, 255)
Finally time to produce the interactive graph! Luckily for us, Plotly has created an API to the JavaScript library so this can be produce solely in Python.
import plotly
import plotly.graph_objects as go
plotly.offline.init_notebook_mode(connected=True)
fig = go.Figure(data=[go.Surface(colorscale=terrain,z=world)])
fig.update_layout(title='Random 3D Terrain')
# Note that include_plotlyjs is used as cdn so that the static site generator can read it and present it on the browser. This is not typically required.
html = plotly.offline.plot(fig, filename='3d-terrain-plotly.html',include_plotlyjs='cdn')
# This is for showing within a jupyter notebook on the browser.
from IPython.core.display import HTML
HTML(html)