Create 3D Terrain Map through Clicks
Map, build, or modify a realistic 3D geography map on Python
As a kid in the 90s, I remember that my dad had a big long Motorola phone that was almost like a brick with antenna and that he got a laptop from his workplace. He would allow us brothers to learn from the computer sometimes and because it could be quite boring, he found Sim City 3000 for us. It was so interesting for me back then that you could build a city and shape the terrain of the entire city from this tiny little box.
The ability to understand the geography and terrain intuitively is a valuable knowledge. This is why we will try to replicate the ability to map or create your own terrain like in Sim City on Python. We will use the matplotlib and mpl_toolkits libraries.
Let’s get started!

The way I envision this to work is that the map should allow us to click to either increase or decrease the height of the selected point. Repeatedly click on the same point will further add or decrease the height depending on the way you click. Here, I have set the left click to increase height and the right click to decrease height.
When we increase or decrease the height of a selected grid, the surrounding 8 grids will be affected as well. They will also rise or fall proportionate to the clicked grid (similar to the Gaussian curve).
Let’s start with the library and generating the terrain canvas:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# Constants
TERRAIN_SIZE = 20
TERRAIN_HEIGHT_SCALE = 1
# Generate grid coordinates
x = np.arange(0, TERRAIN_SIZE)
y = np.arange(0, TERRAIN_SIZE)
X, Y = np.meshgrid(x, y)
# Create initial terrain
terrain = np.zeros((TERRAIN_SIZE, TERRAIN_SIZE))
Then we formulate the function to increase or decrease the heights of the selected points.
# Function to generate the initial terrain
def generate_terrain():
for x in range(TERRAIN_SIZE):
terrain[x] = int(TERRAIN_HEIGHT_SCALE * abs(TERRAIN_SIZE // 2 - x) / (TERRAIN_SIZE // 2))
# Function to update the 3D plot
def update_3d_plot(ax):
ax.clear()
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Elevation')
ax.set_title('3D Terrain Generator')
ax.plot_surface(X, Y, terrain, cmap='viridis', edgecolor='k', alpha=0.6)
ax.set_zlim(-15, 15) # Set the z-axis range
ax.view_init(elev=30, azim=30) # Set the view angle
# Function to update the 2D plot
def update_2d_plot(ax):
ax.clear()
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_title('2D Terrain Generator')
ax.imshow(terrain, cmap='viridis', origin='upper', extent=[0, TERRAIN_SIZE, 0, TERRAIN_SIZE])
# Generate initial terrain
generate_terrain()
Next, we create the plot so that we can see and moify the terrain further.
# Create figure and subplots
fig = plt.figure(figsize=(10, 5))
# 3D subplot
ax_3d = fig.add_subplot(121, projection='3d')
fig.canvas.mpl_connect('button_press_event', lambda event: on_click(event, terrain, ax_3d))
update_3d_plot(ax_3d)
# 2D subplot
ax_2d = fig.add_subplot(122)
fig.canvas.mpl_connect('button_press_event', lambda event: on_click(event, terrain, ax_3d))
update_2d_plot(ax_2d)
# Function to handle clicks
def on_click(event, terrain, ax):
if event.inaxes is not None:
x, y = int(event.xdata + 0.5), int(event.ydata + 0.5)
change = 1 if event.button == 1 else -1 # Left-click increases, right-click decreases
update_terrain(x, y, change)
update_3d_plot(ax_3d)
update_2d_plot(ax_2d)
plt.draw()
# Function to update the terrain
def update_terrain(i, j, change):
terrain[i, j] += change
for di in [-1, 0, 1]:
for dj in [-1, 0, 1]:
ni, nj = i + di, j + dj
if 0 <= ni < TERRAIN_SIZE and 0 <= nj < TERRAIN_SIZE:
terrain[ni, nj] += change / 2 # Adjust the surrounding points
plt.show()
Interestingly, I wanted to allow users to be able to click on the 3D map irectly for adjusting the heights. But, the coordinates of the 3D map are not matched with the actual coordinate that we see at all. What this mean is that when I thought I click on the grid on the 3D map, it turns out to be a completely different points.
Hence, I needed to add the 2D plot for us to click and update there instead and then render the results on the 3D side by side. I would be interested to learn if there are better ways around this so that we can directly update the terrain on the 3D map.

What’s next?
This code is still not perfect since there are still coordination mapping issues. And the 3D terrain click issue still needs to be addressed. Nonetheless, I hope it helps you to be able to create a basic terrain mapping tool.
I still hope that this can be build upon further by:
- Adding the option to add the water body in any enclosed area. This includes being able to add the default seawater level for benchmarking
- Changing the colour of the terrain to better fit the climate (i.e. snow, dessert, Savannah, grassland, tropical forest, etc.)
- Adding the widget to allow users to change the height variable when clicked (Currently the height is fixed at +/- 1, which means that each click gives a fixed height change of 1, but there will be use cases where you need variable heights upon clicking)
- (Optional) Adding animations for the water bodies, trees, etc.
- Allowing users to map the terrain from walking or surveying the area directly
There are many possibilities to such applications, so go ahead and build a new metropolis, Mayor! (haha)