Select a square region on a map and export it as a black/white heightmap — black is low elevation, white is high. Built for feeding real-world terrain into game engines and terrain tools (Unreal, Unity, World Machine, etc.).
Live: https://contour-swart.vercel.app
Download the desktop app (no browser needed): see the latest release for Windows (.exe), macOS Apple Silicon (.dmg) and Linux x64 (.flatpak, .AppImage) installers — see Desktop app below for setup notes.
Built it for importing heightmaps for Transport Fever 3. Read furthcer below.
- Pick a basemap. Use the switcher top-left — Streets, Topo, Satellite, or Satellite + Relief — to help you find the region you want. These are just for navigation; they aren't part of the exported data. Or use the search box below it to jump straight to a place by name.
- Select a region. Hold Shift and drag on the map to draw a square. The live size (km × km), elevation range, and a small live preview thumbnail show in the export panel. Selections are capped at 100 km × 100 km. Bigger regions cover more ground per pixel at the same resolution, so detail drops off as the selection grows — pick a higher resolution or a smaller area for sharper output.
- Rotate it, if needed. Drag the green dot at the top of the square to rotate the selection to any angle. The exported image is always a plain upright square — rotation just changes which ground it covers.
- Choose export options in the panel on the right:
- Format — 16-bit PNG (recommended, widest compatibility), 8-bit PNG
(lower precision), RAW
.r16(headerless 16-bit, for direct import into engines like Unreal/CryEngine/World Machine), or GeoTIFF (32-bit float, real elevation values in meters, georeferenced WGS84 — for GIS tools like QGIS/ArcGIS/gdal, not a regular image viewer). GeoTIFF pixels hold raw elevation (e.g.800.0), not a normalized 0–1 range, so opening it in Windows Photos, a browser, or any plain image viewer will show solid white — those tools assume float pixels are already in 0–1 and everything clips. Open it in a GIS tool instead, where you set your own display stretch. Rotated selections are georeferenced correctly via a full affine transform. - Resolution — output image size, 512 up to 8192 (8K) on the web
app, or up to 16384 (16K) in the desktop app. 16K needs enough
compute time that it only works reliably outside a serverless
function, so it's desktop-only. Above 4K, GeoTIFF is disabled
(uncompressed 32-bit float gets too large — ~256MB at 8K, ~1GB at
16K); use PNG or RAW
.r16instead at those sizes. - Normalization — Best contrast stretches the selection's own min/max elevation across the full black-white range (sharpest detail, but flat/coastal regions can look noisy since there's little real elevation range to stretch). Fixed scale (0–9000m) maps a constant real-world range instead, so results are comparable across exports and flat areas stay dark.
- Georeference sidecar — optional
.jsonwith the exact bounding box, resolution, elevation min/max, and normalization used, for reimporting with real-world scale. - Tiling mode — after an export, keeps a dashed "ghost" outline of that square on the map, plus ←/↑/↓/→ buttons to place a new square of the same size and rotation exactly touching one of its edges. Useful for exporting a region larger than one tile allows and stitching the pieces together in a photo editor — since placement is computed mathematically rather than dragged by eye, tiles line up perfectly.
- Batch grid export — a separate opt-in checkbox that exports a
whole N×M grid of tiles in one go (current square as the top-left,
extending right/down), bundled into a single
.zip. Independent of tiling mode — use either one on its own, or both together.
- Format — 16-bit PNG (recommended, widest compatibility), 8-bit PNG
(lower precision), RAW
- Click Export to download the file. Progress (tile fetching, resampling, encoding) is shown live, driven by the server's actual progress — not an estimate.
Elevation data comes from the Tilezen/AWS Terrarium tile dataset (free, global, no API key). Ocean and other below-sea-level areas are flattened to 0 (black).
This is what google got me:
Step-by-step guide to importing custom heightmaps using the updated creation workflow.
Before opening the game, you need a high-quality topographical image.
- Format: Save the file as a grayscale
.pngor.tif. - Color depth: Use 16-bit grayscale if possible. This prevents "staircasing" (sharp, pixelated ridges) on mountains.
- Dimensions: The image must be perfectly square. Standard sizes are
1025x1025,2049x2049, or4097x4097pixels, depending on the desired map scale.
The fastest way to make your file visible in the game's menu is to place it in the user directory.
-
Navigate to your Transport Fever 3 user directory. For Steam users this is typically:
...\Steam\userdata\[YourSteamID]\1845190\local\maps\Note: The exact Steam AppID folder number may vary based on final game build paths.
-
Create a new folder inside
mapsand name it after your project (e.g.MyCustomMap). -
Rename your image file to exactly
heightmap.png(orheightmap.tif). -
Paste the file into the new folder.
Transport Fever 3 processes maps through an interactive visual system.
- Launch Transport Fever 3 and select Map Editor from the main menu.
- Click Create New Map and choose your climate, vehicle set, and starting year.
- In the generator window, find the Generation Nodes / Pipeline panel.
- Click Add Node and select the Heightmap / Image Input Node.
- In the node's properties, click the file browser icon and select your custom folder/file from Phase 2.
- (Optional but recommended) Chain additional nodes:
- Connect an Erosion Node to smooth rough real-world edges.
- Connect a Water/River Node to carve realistic riverbeds into the imported topography.
- Adjust the Height Scale slider in the import node to set the distance between the lowest valleys and highest peaks.
- Click Generate Preview to view the 3D render. Click Save once you are satisfied with the layout.
This has yet to be tested once the game has been released!!
Prefer not to use a browser? Download a standalone desktop version — same app, no npm or terminal needed. Still needs an internet connection to load map tiles and elevation data.
-
Windows: run the
.exeinstaller, or build it yourself withnpm run electron:build:win(producesdist/contour Setup *.exe). -
macOS:
.dmgis unsigned (no Apple Developer certificate), so Gatekeeper blocks the first launch — right-click the app → Open to bypass it, or allow it under System Settings → Privacy & Security. Apple Silicon (arm64) only for now. -
Linux (x64): two options, same app.
- Flatpak:
flatpak install --user contour-*.flatpak, then start it from your app menu or withflatpak run com.contour.heightmap. The first install pulls the Freedesktop runtime from Flathub, so Flathub needs to be set up as a remote (setup guide). - AppImage: make it executable (
chmod +x contour-*.AppImage) and run it. Needs FUSE 2, which some newer distros no longer ship by default (libfuse2t64on Ubuntu 24.04,fuse-libson Fedora).
Build it yourself with
npm run electron:build:linux(produces both files indist/; the Flatpak needsflatpak-builderand the Flathub remote added with--user). - Flatpak:
All three are built automatically by .github/workflows/build-desktop.yml on
GitHub Actions (manually via "Run workflow", or by pushing a v* tag) —
download the artifacts from the workflow run.
npm install
npm run devOpen http://localhost:3000.
Stack: Next.js (App Router) + MapLibre GL JS for the map, a small
tile-fetch/decode/resample pipeline in src/lib/, and a hand-rolled PNG
encoder (src/lib/png.ts) for correct 16-bit grayscale output. The desktop
app (electron/main.cjs) runs this same Next.js app locally via Electron —
no separate implementation.
Pushes to main auto-deploy to Vercel via the connected GitHub integration.
- v0.1.7 — Added a location search box (jump to a place by name), a live preview thumbnail of the selection, and batch grid export (export an N×M grid of tiles bundled into one .zip, independent of tiling mode).
- v0.1.6 — Export now shows a real live progress bar (tiles fetched, resampling, encoding) instead of just a spinner — driven by actual server-side progress, not an estimate.
- v0.1.5 — Added Linux desktop support (AppImage + Flatpak), thanks to @ThomasMeisters.
- v0.1.4 — Added tiling mode: keep a ghost outline of the last export and place new squares exactly adjacent to it, for stitching multiple exports into one larger heightmap.
- v0.1.3 — Added 8K resolution (web + desktop) and 16K (desktop only — times out as a serverless function on the web app). GeoTIFF disabled above 4K since it's uncompressed and gets huge (~1GB at 16K).
- v0.1.2 — Fixed occasional "fetch failed" export errors: tile fetches now retry automatically on transient network blips, and any error that still surfaces shows a clearer "try again" message instead of a raw error.
- v0.1.1 — Fixed the desktop app returning "Export failed 500" on
every export. Cause: Next.js's default bundler (Turbopack) references
sharpin a way that breaks once packaged inside Electron's app archive; the desktop build now uses webpack instead, which doesn't have that problem. - v0.1.0 — First desktop app release (Windows
.exe, macOS.dmg). Known broken — see v0.1.1.