Two Python scripts that process GeoTIFF elevation data into 3D mesh geometry and geographic coordinate lookups, used to bring real-world terrain into a Unity scene.
The script accepts a TIFF filename and an optional integer scale factor from the command line.
tifffile.imread loads the raw elevation array, then every
n-th row and column is sampled to produce a smaller grid before export.
import sys
from tifffile import imread
filename = sys.argv[1]
scalefactor = 1
if len(sys.argv) > 2:
scalefactor = int(sys.argv[2])
elevData = imread(filename)
elev = []
if scalefactor != 1:
for line in elevData[::scalefactor]:
elev.append(line[::scalefactor])
else:
elev = elevData
After scaling, the grid is written out as a CSV and immediately visualised as a 3D surface with
matplotlib so the data can be sanity-checked before the heavier
mesh-generation step.
import csv
import matplotlib.pyplot as plt
import numpy as np
# write grid to CSV
f = open("gridexport.csv", 'w')
w = csv.writer(f)
w.writerows(elev)
f.close()
# 3-D preview
x = range(int(len(elevData[0]) / scalefactor))
y = range(int(len(elevData) / scalefactor))
X, Y = np.meshgrid(x, y)
hf = plt.figure()
ha = hf.add_subplot(111, projection='3d')
ha.plot_surface(X, Y, elev)
plt.show()
Each 2×2 block of grid cells is split into two triangles using the standard quad-to-tri pattern, producing a flat index list that Unity's Mesh API can consume directly.
matLen = len(elev[0])
tris = []
for i, row in enumerate(elev[:-1]):
for j, item in enumerate(row[:-1]):
index = i * matLen + j
# first triangle (top-left, top-right, bottom-left)
tris.append(index)
tris.append(index + 1)
tris.append(index + matLen)
# second triangle (top-right, bottom-right, bottom-left)
tris.append(index + 1)
tris.append(index + matLen + 1)
tris.append(index + matLen)
makeJson() flattens the vertex grid into three parallel arrays
(X, Y, Z) and bundles them with the triangle index list. Three separate files are written:
the raw smooth mesh, a slope-tile variant where each 2×2 block is flattened to its
top-left value, and a tile variant that steps the value forward diagonally — both
useful for stylised low-poly terrain.
def makeJson(matrix, tris):
jsonObj = {"verticesX": [], "verticesY": [], "verticesZ": [], "tris": []}
for point in matrix:
jsonObj["verticesX"].append(float(point[0]))
jsonObj["verticesY"].append(float(point[1]))
jsonObj["verticesZ"].append(float(-point[2])) # negate for Unity's y-up
jsonObj["tris"] = tris
return jsonObj
# smooth mesh
f = open(filename.split('.')[0] + '.json', 'w')
f.write(json.dumps(makeJson(matrix, tris)))
f.close()
# slope-tile and flat-tile variants follow the same pattern …
A single utility function maps a latitude/longitude pair to a floating-point row and column
within the elevation grid, given the grid's bounding box. The row is inverted
(1 - …) because TIFF rows run top-to-bottom while latitude
increases upward.
def getGridPosition(grid, lat, lng, latMin, latMax, longMin, longMax):
gridRows = len(grid)
gridCols = len(grid[0])
latStep = (latMax - latMin) / gridRows
lngStep = (longMax - longMin) / gridCols
row = ((1 - (lat - latMin)) / latStep)
col = ((lng - longMin) / lngStep)
return row, col