AI agent skill · by @kevinpbuckley
Terrain Data
Generate heightmaps, map reference images, and water feature splines (rivers, lakes, oceans) from real-world geographic data (terrain_data tool). Use when the user asks to build terrain from a real location/coordinates, download a real-world heightmap, or add rivers/lakes/oceans from map data. Downloads Mapbox tiles server-side and produces UE5-compatible heightmaps + landscape splines.
Archived 2026-10-23 · Source updated 2026-10-07 · License: MIT. Check the original repository for the latest version.
SKILL.md instructions
> 🧠 **Brains complement:** IF an `unreal-engine-skills-manager` tool (external MCP) exists in this session, call it with `{action: "load", skill: "landscape-and-foliage"}` for UE domain knowledge on this topic — correct APIs, architecture, best practices — and treat it as the rubric for any review / "best practices" question. If no such tool is available (e.g. running under Claude Code or Codex without that MCP), skip this line entirely and proceed with this skill alone — do NOT attempt the call.
# Real-World Terrain Skill
Generates heightmaps and water feature splines from real geographic coordinates via the `terrain_data`
MCP tool. The tool downloads Mapbox tiles server-side and writes UE5-compatible heightmaps + landscape
splines into the project's `Saved/Terrain/` folder — no API key or chat configuration is required.
## Workflow
### Heightmap (always run first)
1. `preview_elevation` — fetch elevation stats + suggested settings. Returns `suggestedZScale`, `suggestedXYScales` (resolution → XY scale), height range.
2. `generate_heightmap` — generate the 16-bit PNG using suggested settings
3. *(optional)* `get_map_image` — satellite/topo reference image for the same area
4. **`attach_image`** — attach the satellite image so you can **see** the terrain colors and features for material/painting decisions
5. Import into UE5 using the `landscape` skill
**⚠️ CRITICAL — Landscape Scale:** When creating the landscape, use `suggestedXYScales[resolution]` from step 1 as the X and Y scale. Do NOT use the default value of 100. Use `suggestedZScale` for the Z scale. Example: `scale=unreal.Vector(suggestedXYScales["1009"], suggestedXYScales["1009"], suggestedZScale)`
### Water features (after heightmap is imported)
6. `get_water_features` — fetch rivers, lakes, and oceans for the same lng/lat/map_size. **Saves full JSON to `Saved/Terrain/` and returns a compact summary with the file path.**
7. Read the saved JSON file: `json_str = open(file_path).read()`
8. Water bodies use `ue5_rings`, waterways use `ue5_points`. Both are origin-centered.
---
## Test Prompts
```
Create a landscape from the terrain around Mount Fuji
```
```
Generate a heightmap for San Francisco at coordinates -122.4194, 37.7749
```
```
Build a real-world landscape from the Swiss Alps near Zermatt (lng=7.7480, lat=46.0207)
```
```
Get the terrain around the Grand Canyon and make a landscape
```
```
Generate heightmap for Tokyo at 139.6917, 35.6895 with a satellite reference image
```
---
## Step-by-Step Example (Mount Fuji)
### Step 1 — Preview elevation
```
terrain_data(action="preview_elevation", lng=138.7274, lat=35.3606)
```
Response:
```json
{
"min_height": 340.0,
"max_height": 3776.0,
"height_range": 3436.0,
"suggested_base_level": 340,
"suggested_height_scale": 27,
"suggestedZScale": 741,
"suggestedXYScales": { "505": 3429, "1009": 1714, "2017": 857, "4033": 429, "8129": 213 },
"tile_zoom": 13,
"tile_count": 9
}
```
Use `suggestedXYScales["1009"]` (or whichever resolution you chose) as the X and Y scale when calling `create_landscape`. Use `suggestedZScale` for the Z scale.
### Step 2 — Generate heightmap using suggested values
```
terrain_data(
action="generate_heightmap",
lng=138.7274, lat=35.3606,
base_level=340,
height_scale=27,
format="png"
)
```
Response:
```json
{
"success": true,
"file": "C:/Project/Saved/Terrain/heightmap_35.3606_138.7274.png",
"min_height_m": 340.0,
"max_height_m": 3776.0,
"dimensions": "1081x1081"
}
```
### Step 3 — Get satellite reference image
```
terrain_data(
action="get_map_image",
lng=138.7274, lat=35.3606,
style="satellite-v9"
)
```
Response:
```json
{
"success": true,
"file": "E:/Project/Saved/Terrain/map_satellite_v9_35.3606_138.7274.png",
"style": "satellite-v9",
"size_bytes": 1981147
}
```
### Step 3b — Attach satellite image for vision analysis
**⚠️ CRITICAL: After downloading a satellite image, ALWAYS attach it so you can see the terrain colors and features.** This lets you make informed decisions about material layers and painting.
```
attach_image(file_path="E:/Project/Saved/Terrain/map_satellite_v9_35.3606_138.7274.png")
```
`attach_image` is a **tool call** (like `terrain_data`), NOT a Python function. Do NOT put it inside `execute_python_code`. Call it directly as a tool. If no `attach_image` tool is available in your session, skip this step and proceed using the saved file path returned by `get_map_image`.
After attaching, you will see the satellite image in your next response. Use it to:
- Identify terrain features (rock, grassland, water, sand, forest, urban)
- Choose appropriate material layer names and colors
- Design accurate procedural painting rules (height/slope thresholds)
- Match real-world color distribution to layer weights
### Step 4 — Import into UE5
Use the `landscape` skill to import the heightmap file:
- Component setup: `QuadsPerSection=63, SectionsPerComponent=2, ComponentCount=8x8` → 1009×1009 resolution
- **X and Y scale**: Use `suggestedXYScales["1009"]` from preview (e.g. 1714 for a 17.28km map). **Do NOT use the default 100** — that makes the landscape ~17x too small!
- **Z scale**: Use `suggestedZScale` from preview (e.g. 741 for Mount Fuji)
- File: the `.png` path returned in step 2
Example:
```python
result = unreal.LandscapeService.create_landscape(
location=unreal.Vector(0, 0, 0),
rotation=unreal.Rotator(0, 0, 0),
scale=unreal.Vector(1714, 1714, 741), # XY from suggestedXYScales["1009"], Z from suggestedZScale
sections_per_component=2,
quads_per_section=63,
component_count_x=8,
component_count_y=8,
landscape_label="MyLandscape"
)
```
---
## Parameters Reference
### generate_heightmap
| Parameter | Default | Notes |
|-----------|---------|-------|
| `lng` / `lat` | required | Decimal degrees. Positive = East/North |
| `format` | `png` | `png` = 16-bit grayscale, `raw` = binary, `zip` = PNG + info |
| `map_size` | `17.28` | km — Cities: Skylines standard |
| `base_level` | `0` | Use `suggested_base_level` from preview |
| `height_scale` | `100` | Use `suggested_height_scale` from preview |
| `water_depth` | `40` | Cities: Skylines water units |
| `gravity_center` | `0` | 0=off, 2=N, 4=E, 6=S, 8=W (tilts terrain for water flow) |
| `level_correction` | `0` | 0=none, 2=flatten coastlines, 3=aggressive |
| `blur_passes` | `10` | **Adjust per terrain character!** 5–10=rugged, 15–25=hills, 25–40=smooth/flat. See Terrain Character Guide |
| `plains_height` | `140` | Meters — threshold between plains and mountains |
| `save_path` | auto | Saves to `<ProjectDir>/Saved/Terrain/` by default |
### get_map_image styles
| Style | Description |
|-------|-------------|
| `satellite-v9` | Aerial imagery (best for landscape texturing) |
| `outdoors-v11` | Topo with trails and contours |
| `streets-v11` | Street map |
| `light-v10` / `dark-v10` | Minimal |
---
## UE5 Landscape Import Settings
### Valid Resolutions — Always Pass `resolution=N`
**⚠️ Never use the default 1081×1081 for UE landscapes** — it requires 36×36 components and will timeout. Always pass `resolution=` matching your landscape config.
| resolution= | Components | Quads | Sections | km at UE scale=100 |
|-------------|-----------|-------|----------|--------------------|
| 505 | 8×8 | 63 | 1 | ~0.5 km |
| 1009 | 8×8 | 63 | 2 | ~1.0 km |
| 1009 | 16×16 | 63 | 1 | ~1.0 km |
| 2017 | 16×16 | 63 | 2 | ~2.0 km |
| 4033 | 32×32 | 63 | 2 | ~4.0 km |
| 8129 | 32×32 | 127 | 2 | ~8.1 km |
> "km at scale=100" is the UE world size when landscape XY scale = 100 cm/quad (default). Adjust scale to match real geography (see below).
### Matching Real-World Scale
`terrain_data`'s `map_size` (default: 17.28 km) controls the geographic area captured. To make the UE landscape match actual geography:
```
UE XY Scale (cm/quad) = (map_size_km × 100,000) / (resolution − 1)
```
Example: `map_size=20`, `resolution=2017` → `(20 × 100,000) / 2016 ≈ 992 cm/quad (~9.9 m/quad)`
Set this as the landscape's X and Y scale when creating it.
### Format and Z Scale
- **Format**: 16-bit grayscale PNG
- **Z Scale**: `20000 / height_scale` cm — e.g., `height_scale=27` → Z scale ≈ 741
**⚠️ ALWAYS calculate Z Scale from height_scale. Do NOT guess.** Use this formula when creating the landscape:
```
z_scale = 20000 / height_scale
```
Derivation: pixel encoding is `pixel = elevation_m × heightScale × 64 / 100` and UE interprets `height_cm = pixel × z_scale / 128`. Setting `height_cm = elevation_m × 100` (meters→cm) gives `z_scale = 20000 / heightScale`.
| height_scale | Z Scale | Terrain Type Example |
|-------------|---------|---------------------|
| 27 | 741 | Tall mountains (Mt. Fuji, Alps) |
| 50 | 400 | Moderate mountains (Appalachians) |
| 100 | 200 | Hills, canyons (Grand Canyon) |
| 150 | 133 | Low hills, mesas |
| 200 | 100 | Gentle rolling terrain |
| 250 | 80 | Very flat terrain, gentle domes |
For Cities: Skylines, export as PNG and import via the standard heightmap importer (1081 default is fine there).
---
## Terrain Character Guide
**⚠️ CRITICAL: The `suggested_height_scale` from `preview_elevation` maximizes detail, but also amplifies noise. You MUST adjust `blur_passes` based on the terrain character.**
### Identifying Terrain Character
After `preview_elevation`, look at the `height_range` and think about what the terrain actually looks like:
| height_range | suggested_height_scale | Terrain Character | blur_passes |
|-------------|----------------------|-------------------|-------------|
| > 2000m | Low (< 50) | **Rugged mountains** — keep default blur | 5–10 |
| 500–2000m | Moderate (50–100) | **Mixed terrain** — moderate smoothing | 10–15 |
| 100–500m | High (100–200) | **Hills/mesas** — needs more smoothing | 15–25 |
| < 100m | Very high (200–250) | **Flat/gentle** — needs heavy smoothing | 25–40 |
### Smooth vs Rugged Terrain
**Smooth terrain** (granite domes, rolling hills, plains, gentle slopes):
- Use higher `blur_passes` (20–40) to remove data noise
- The high `height_scale` amplifies every pixel of noise — smoothing counteracts this
- Examples: Enchanted Rock, Uluru, sand dunes, prairies
**Rugged terrain** (jagged peaks, canyons, volcanic craters):
- Use lower `blur_passes` (5–10) to preserve detail
- Lower `height_scale` means less noise amplification, so less smoothing needed
- Examples: Grand Canyon, Matterhorn, Iceland lava fields
### Common Mistake: Jagged Smooth Terrain
If you get a terrain that looks jagged/spiky when the real place is smooth:
1. The `height_scale` was too high without enough `blur_passes`
2. Fix: Re-generate with `blur_passes=30` or higher
3. Also check: Z scale should be `20000 / height_scale`, NOT an arbitrary value like 200
---
## Water Features Reference
### get_water_features
Fetches waterways and water bodies for a map area using the same Mapbox Vector Tile source as the heightmap. Use the **exact same `lng`, `lat`, and `map_size`** as your heightmap call.
```
terrain_data(action="get_water_features", lng=-105.0, lat=39.7, map_size=17.28)
```
Response includes:
- `file` — path to the saved JSON file (e.g. `Saved/Terrain/water_features_42.9720_-71.3480_10km.json`)
- `num_waterways` / `num_water_bodies` — counts
- `waterway_class_breakdown` — object with class counts, e.g. `{"stream": 191, "river": 26, "ditch": 12}`
- `waterways[]` — summary of each waterway: `name`, `class`, `estimated_width_m`, `num_points`
- `water_bodies[]` — summary of each water body: `name`, `class`, `num_ring_points`
- `message` — instructions on how to use the saved file, coordinate system info
- `ue5_coordinate_note` — critical info about coordinate offset for water planes
The **saved JSON file** contains the full data with:
- `waterways[]` — rivers, streams, canals. Each has `name`, `class`, `estimated_width_m`, `points` (lng/lat array), **`ue5_points`** (array of `{x, y, z}` objects in UE5 coords)
- `water_bodies[]` — lakes, ponds, oceans. Each has `name`, `class`, **`rings`** (lng/lat array of arrays), **`ue5_rings`** (array of arrays of `{x, y, z}` objects in UE5 coords)
**⚠️ CRITICAL FIELD NAMES:**
- Waterway positions: `ue5_points` (NOT `points` — those are lng/lat)
- Water body polygons: `ue5_rings` (NOT `polygon` or `ring`)
- Waterway type: `class` (NOT `waterway_class` or `type`)
### Coordinate system
`ue5_points` and `ue5_rings` are **landscape-center-relative** (origin-centered):
- Map geographic center = **(0, 0, Z)** in UE5 space
- **+X = East, +Y = North**, 1 meter = 100 UU
---
## Troubleshooting
| Issue | Fix |
|-------|-----|
| `429` rate limit | The Mapbox tile source is rate-limited — wait and retry, or reduce request frequency |
| Flat heightmap | `height_range` < 50m — use `height_scale: 250` for detail |
| Clipped mountains | Lower `height_scale` or increase `base_level` |
| Jagged/spiky terrain | Increase `blur_passes` (20–40 for smooth terrain) and check Z scale formula |
| Terrain doesn't match real place | Check terrain character guide — smooth places need high blur_passes |
| Timeout | Large map sizes at high zoom may hit Vercel's 10s limit — try smaller `map_size` |
| No waterways returned | Area may have no mapped water features (desert, urban grid) — check satellite image |
| Landscape too small | You used XY scale=100 (default) instead of the correct value from `suggestedXYScales`. Recreate with the correct scale |
| Black screenshots after camera move | Camera inside terrain. Use `ActorService.get_actor_view_camera()` to frame from TOP view instead of guessing Z height |
| Water features timeout | PBF tile fetch uses 45s timeout — large map_size at high zoom may still be slow |
| Field name errors | Use `ue5_points` (not `points`), `ue5_rings` (not `polygon`), `class` (not `waterway_class`) |