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 package contains multiple algorithms inside it for generating different types of noise.
For visualising our terrain in the first instance, we shall use
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 on built-in function noise2 in module noise._perlin: noise2(...) noise2(x, y, octaves=1, persistence=0.5, lacunarity=2.0, repeatx=1024, repeaty=1024, base=0.0) 2 dimensional perlin improved noise function (see noise3 for more info) Help on built-in function noise3 in module noise._perlin: noise3(...) noise3(x, y, z, octaves=1, persistence=0.5, lacunarity=2.0repeatx=1024, repeaty=1024, repeatz=1024, base=0.0) return perlin "improved" noise value for specified coordinate octaves -- specifies the number of passes for generating fBm noise, defaults to 1 (simple noise). persistence -- specifies the amplitude of each successive octave relative to the one below it. Defaults to 0.5 (each higher octave's amplitude is halved). Note the amplitude of the first pass is always 1.0. lacunarity -- specifies the frequency of each successive octave relative to the one below it, similar to persistence. Defaults to 2.0. repeatx, repeaty, repeatz -- specifies the interval along each axis when the noise values repeat. This can be used as the tile size for creating tileable textures base -- specifies a fixed offset for the input coordinates. Useful for generating different noise textures with the same repeat interval
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): for j in range(shape): 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.image.AxesImage at 0x24bd59119c8>
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,endpoint=False) lin_y = np.linspace(0,1,shape,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')
<mpl_toolkits.mplot3d.art3d.Poly3DCollection at 0x24be0b2af08>
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, C, C))]) return pl_colorscale terrain = matplotlib_to_plotly(terrain_cmap, 255)
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)