Landscape logo

Landscape

Community
kevinpbuckley
landscape

Create and edit landscape terrain — heightmaps, sculpting, paint layers, terrain analysis, mesh projection, and procedural features like mountains/valleys/craters (LandscapeService). Use when the user asks to create a landscape, sculpt/raise/lower terrain, import or export a heightmap, paint terrain layers, or add procedural terrain features. For terrain materials load landscape-materials; for real-world heightmaps load terrain-data.

Overview

Publisherkevinpbuckley
RepositoryVibeUE
Skill namelandscape
Stars
735
Forks
161
Bundled files
5
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 5 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by kevinpbuckley on GitHub. Read the source before you install it.

Installation

Install the Landscape AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/kevinpbuckley/VibeUE/tree/master/Content/Skills/landscape
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/kevinpbuckley/VibeUE.git /tmp/VibeUE
mkdir -p .claude/skills
cp -r /tmp/VibeUE/Content/Skills/landscape .claude/skills/landscape
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Landscape in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Landscape on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Landscape is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

🧠 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.

Landscape Terrain Skill

Sub-docs available

This skill is split into the lean index (you are reading it) plus sibling sub-docs. Skills load via the engine's AgentSkillToolset (ListSkills / GetSkills) — request landscape/<section> through GetSkills, or read the sibling *.md file (it sits next to this SKILL.md on disk) directly:

Sub-docWhen to load
return-types.mdExact property names of LandscapeCreateResult, LandscapeInfo_Custom, LandscapeLayerInfo_Custom, LandscapeSplinePointInfo, LandscapeSplineSegmentInfo, LandscapeSplineMeshEntryInfo, plus the create_landscape signature.
workflows-creation.mdCreating landscapes, getting info, importing/exporting heightmaps, recreating splines, copying terrain/paint layers between landscapes, calculating side-by-side offsets, reading height data in a region.
workflows-editing.mdSculpting, direct region editing, procedural noise, painting layers, setting material, querying terrain, LandscapeGrassType creation/cloning, full landscape cloning, existence checks, heightmap sizing utilities.
intelligent-sculpting.mdv3 semantic features (mountain/valley/ridge/plateau/crater/terraces/erosion/blend), terrain analysis (slope/normal/flat areas), mesh projection, batch line traces, complete v3 workflow example.

Critical Rules

Valid Quad Sizes

QuadsPerSection must be one of: 7, 15, 31, 63, 127, 255 SectionsPerComponent must be 1 or 2

Common setup: QuadsPerSection=63, SectionsPerComponent=1, ComponentCount=8x8

⚠️ Performance: Landscape Creation Timeout

Creating landscapes with many components is SLOW. Python execution has a 30-second timeout.

Safe configurations (under 30s):

  • ComponentCount=4x4 (253x253 resolution) — safe for throwaway/test terrains
  • ComponentCount=8x8 (505x505 resolution) — can exceed 30s in a populated level (measured 67s in a level that already had another landscape); safe only in near-empty levels

Will TIMEOUT (avoid):

  • ComponentCount=16x16 or larger in a populated level
  • ComponentCount=36x36 or larger — too many components, takes minutes
  • ComponentCount=72x72 — definitely exceeds timeout

⚠️ A timeout does NOT cancel the operation. PYTHON_EXECUTION_TIMEOUT means the tool stopped waiting — the create keeps running on the game thread and usually completes. Never blindly retry: you'd stack a second landscape with the same label (then delete_landscape removes only the first match and landscape_exists stays true, which looks like a delete failure). After a timeout, check landscape_exists(label) first. create_landscape also refuses to create a landscape whose label already exists.

⚠️ Heightmap Import: Resolution MUST Exactly Match

import_heightmap() requires the heightmap file resolution to exactly match the landscape resolution. There is NO automatic scaling. A size mismatch will fail or produce flat/corrupt terrain.

If the target landscape label does not exist, import_heightmap() now auto-creates a matching landscape at world origin with default scale (100,100,100) before importing. For real-world terrain workflows, explicitly creating the landscape first is still recommended so you can control XY/Z scale.

Landscape resolution formula: Resolution = (ComponentCount × QuadsPerSection × SectionsPerComponent) + 1

Safe performant configs (common resolutions):

ComponentsQuadsSectionsResolutionkm at scale=100Notes
8×8631505×505~0.5 kmFast, good for prototypes
8×86321009×1009~1.0 kmGood balance of detail/speed
16×166311009×1009~1.0 kmSame resolution, more LOD components
8×812711017×1017~1.0 kmLarger quads, fewer components
16×166322017×2017~2.0 kmEpic recommended for medium landscapes
32×326312017×2017~2.0 kmSame resolution, more LOD components
8×812722033×2033~2.0 kmHigh detail, fewer components
16×1612712033×2033~2.0 kmHigh detail, more components
32×326324033×4033~4.0 kmLarge open world
32×3212728129×8129~8.1 kmMax single tile

⚠️ 1081×1081 is NOT a valid performant config! The only config producing 1081 is 36×36 components, 15 quads, 2 sections — which has 1296 components and will timeout. Never request or assume 1081.

Heightmap ↔ Landscape Sizing Utilities

Four Python utility functions help match heightmaps to landscapes:

FunctionPurpose
get_heightmap_dimensions(file_path)Read width/height/bitdepth of a PNG or RAW heightmap file
resize_heightmap(source, width, height, output, interpolation)Resample a 16-bit heightmap to new dimensions. Default interpolation="bicubic" (Catmull-Rom, smooth normals); pass "bilinear" only if you need the legacy behavior — bilinear upsampling of a low-res DEM produces blocky planar facets on flat plains
calculate_landscape_resolution(cx, cy, quads, sections)Compute resolution from landscape config parameters
find_landscape_config_for_resolution(width, height)Find the best landscape config that matches a given resolution

✅ Recommended Workflow: Request Matching Resolution

BEST approach — request the heightmap at the exact resolution you need:

  1. Decide landscape config (e.g., 8×8 components, 63 quads, 2 sections)
  2. Calculate resolution: (8 × 63 × 2) + 1 = 1009
  3. Use terrain_data generate_heightmap with resolution=1009 and map_size=N
    • map_size controls how many real-world km are captured (default 17.28)
    • Smaller map_size = more detail for a specific feature (e.g., 2-5 km for a single mountain)
    • Larger map_size = wider area with less per-pixel detail (e.g., 10-20 km for a region)
  4. Create landscape with matching config
  5. Import heightmap — sizes match perfectly

✅ Fallback Workflow: Resize After Download

If you already have a heightmap at a non-matching resolution:

  1. Check dimensions: get_heightmap_dimensions(file_path)
  2. Either:
    • Find a landscape config that matches: find_landscape_config_for_resolution(width, height)
    • Or resize the heightmap to match your landscape: resize_heightmap(source, target_w, target_h)
  3. Create landscape / import resized heightmap

World Partition landscapes (streaming proxies)

In a World Partition level, pass world_partition_grid_size (components per proxy grid cell, e.g. 2) to create_landscape to split the landscape into ALandscapeStreamingProxy actors — the same thing the editor's New Landscape tool does in a WP world:

python
result = unreal.LandscapeService.create_landscape(
    unreal.Vector(0, 0, 0), unreal.Rotator(0, 0, 0), unreal.Vector(100, 100, 100),
    sections_per_component=1, quads_per_section=63,
    component_count_x=8, component_count_y=8,
    landscape_label="WPLandscape", world_partition_grid_size=2)
  • The default world_partition_grid_size=0 keeps the single monolithic ALandscape (old behavior).
  • The call fails with an error if the level is not World Partition — the engine's grid split is a silent no-op there, so VibeUE refuses instead of pretending.
  • import_heightmap, sculpting, painting, and region edits are proxy-transparent: they resolve the parent ALandscape and fan updates out to every proxy sharing its landscape GUID. Nothing else in your workflow changes.

🚨 Save semantics: the dirty list lies — audit with get_last_landscape_write() + git status

On a World Partition landscape the engine's dirtying mode (LandscapeSettings.LandscapeDirtyingMode, default In Landscape Mode And User Triggered Changes) does not mark an edit dirty when it is made outside Landscape mode — and every VibeUE/Python edit is outside Landscape mode. The engine tracks the touched packages in an internal modified-package list instead of the normal dirty flag. The practical consequences, all measured:

  • import_heightmap dirties the proxies but does not save them. You must save explicitly.
  • A brush / region / semantic op (flatten_at_location, set_height_in_region, sculpt/smooth/…) can write the proxy .uasset files to disk during the edit-layer resolve while the editor's dirty list shows nothing. One flatten_at_location left 49 proxy files modified on disk with an empty dirty list. So the dirty list is not a reliable audit of a WP landscape after a brush op.
  • git status is the only fully honest audit (the modified files live under Content/__ExternalActors__/...; restore an unwanted change with the editor closed via git checkout -- Content/__ExternalActors__/...).

Two tools make this auditable from Python:

python
import unreal
svc = unreal.LandscapeService

svc.flatten_at_location("Landscape", 0.0, 0.0, 0.0, 250.0)   # any height writer

# What did that write actually touch?
rep = svc.get_last_landscape_write()
print(rep.summary)                       # e.g. "49 proxies dirtied; 49 written to disk by the edit-layer flush (mtime changed)"
print(rep.num_proxies_dirtied, rep.num_written_to_disk, rep.b_any_written_to_disk)
for p in rep.proxies:
    print(p.package_name, "dirty:", p.b_dirty_after, "written:", p.b_written_to_disk, p.file_size_bytes)
  • get_last_landscape_write() reports, per proxy package the last write touched: b_dirty_before / b_dirty_after (the unreliable flag) and b_written_to_disk — an on-disk mtime comparison bracketing the edit-layer resolve, which is the signal you can trust. num_written_to_disk / b_any_written_to_disk summarise it.

    • Caveat: the mtime probe only sees writes that complete during the MCP call (around the synchronous ForceUpdateLayersContent resolve). If the engine ever defers the on-disk save to a later editor tick, b_written_to_disk reads false even though the file changes shortly after — so git status remains the final word.
  • save_landscape(label) is the deterministic, explicit save: it collects the parent ALandscape package plus every proxy sharing the landscape GUID and saves them all through UEditorLoadingAndSavingUtils::SavePackages(..., only_dirty=False) — i.e. regardless of the dirty flag. Use it after import_heightmap (which never saves), or any time you want a clean, audited flush.

python
res = svc.save_landscape("Landscape")
if res.b_success:
    print("saved", res.num_requested, "packages:", list(res.saved_packages))
else:
    print("save failed:", res.error_message)   # e.g. a read-only / source-controlled file

Blocky / faceted flat areas (plains)

Low-res source DEMs upsampled to landscape resolution show planar facets on flat terrain. Fixes, in order of preference:

  1. resize_heightmap(..., interpolation="bicubic") — now the default; re-run the resize+import if the landscape was built with an older (bilinear) resize.
  2. Increase blur_passes (20–40) on terrain_data generate_heightmap for plains-heavy regions.
  3. Material-level smoothing without touching heights: derive a world-space smoothed normal in the landscape material (see landscape-materials skill) when height data must stay pixel-exact.

Landscape Scale

  • Default scale (100, 100, 100) = 1 meter per unit
  • Larger Z scale = taller terrain range (e.g., Z=200 doubles height range)

⚠️ When importing real-world terrain from terrain_data, ALWAYS calculate Z scale:

z_scale = 20000 / height_scale

Do NOT hardcode Z scale. The height_scale from preview_elevation determines the correct Z scale.

Height Limits (uint16 Saturation)

The heightmap is uint16 (0–65535, midpoint 32768). Maximum world height depends on Z scale:

Z ScaleMax Height (world units)Min Height (world units)Total Range
100~25,599~-25,600~51,200
200~51,198~-51,200~102,400
50~12,800~-12,800~25,600

Formula: MaxHeight = (65535 - 32768) × (1/128) × ZScale

When sculpting pushes vertices to 0 or 65535, a saturation warning is logged. To increase range:

  • Increase landscape Z scale (halves vertical resolution)
  • Offset landscape Z location downward to use the full ±range

World Coordinates vs Landscape Coordinates

  • Most methods accept world coordinates (WorldX, WorldY)
  • get_height_in_region and set_height_in_region use landscape-local vertex indices
  • Heights in set_height_in_region are world-space Z values
  • Heights returned by get_height_in_region are world-space Z values

⚠️ GrassVariety Struct Properties Are Read-Only via Direct Assignment

GrassVariety (and its nested structs like PerPlatformFloat, FloatInterval, etc.) cannot be set via direct attribute assignment. You MUST use set_editor_property() for all properties.

python
# WRONG - raises "Property is read-only and cannot be set"
nv = unreal.GrassVariety()
nv.affect_distance_field_lighting = True
nv.grass_density.default = 50.0

# CORRECT - use set_editor_property and construct new struct instances
nv = unreal.GrassVariety()
nv.set_editor_property('affect_distance_field_lighting', True)
nv.set_editor_property('grass_density', unreal.PerPlatformFloat(default=50.0))
nv.set_editor_property('scale_x', unreal.FloatInterval(min=1.0, max=3.0))

This applies to ALL nested structs: PerPlatformFloat, PerPlatformInt, PerQualityLevelFloat, PerQualityLevelInt, FloatInterval, LightingChannels. Always construct a new struct instance with keyword args.

⚠️ Creating LandscapeGrassType Assets Requires AssetTools + Factory

LandscapeGrassType assets are NOT created via LandscapeService. Use AssetToolsHelpers with LandscapeGrassTypeFactory:

python
# WRONG - no such method exists
unreal.LandscapeService.create_grass_type("LGT_MyGrass", "/Game/Grass")

# CORRECT - use AssetTools + Factory
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
factory = unreal.LandscapeGrassTypeFactory()
new_asset = asset_tools.create_asset("LGT_MyGrass", "/Game/Grass", unreal.LandscapeGrassType, factory)

⚠️ Use LandscapeMaterialService for Landscape Materials - NOT MaterialService

  • CORRECT: unreal.LandscapeMaterialService.create_landscape_material("M_Terrain", "/Game/Mats")
  • WRONG: unreal.MaterialService.create_material("M_Terrain", "/Game/Mats") → causes TypeError: Nativize: Cannot nativize 'MaterialCreateResult' as 'String'

MaterialService is for generic materials. Landscape materials MUST use LandscapeMaterialService.

Layer Info Objects Required

Every paint layer needs a ULandscapeLayerInfoObject asset:

  1. Create with LandscapeMaterialService.create_layer_info_object()
  2. ALWAYS store and use .asset_path from the result — never guess paths
  3. Naming convention is LI_<LayerName> (e.g., LI_Grass, LI_Rock) — NOT Grass_LayerInfo
  4. Then add to landscape with LandscapeService.add_layer(landscape_label, info.asset_path)

Material Must Be Compiled First

After creating/changing a landscape material, compile it before assigning to the landscape.

⚠️ Split Material Operations Into Separate Code Blocks

compile_material() can take minutes for complex landscape materials. NEVER combine material creation + compile + layer info creation in one code block — it WILL timeout.

Do this in separate steps:

  1. Block 1: Create material, add blend nodes, add layers, connect, save
  2. Block 2: compile_material() (this is slow)
  3. Block 3: Create layer info objects, assign to landscape

⚠️ Common Mistakes to Avoid

WRONGCORRECT
create_landscape(..., actor_label="X")create_landscape(..., landscape_label="X")
result.actor_pathresult.actor_label (no actor_path exists)
info.successCheck info.actor_label (no success field on info)
info.num_sectionsinfo.num_subsections
info.quads_per_sectioninfo.subsection_size_quads
MaterialService.create_material() for landscapesLandscapeMaterialService.create_landscape_material()
Guessing layer info path Grass_LayerInfoUse create_layer_info_object().asset_path → LI_Grass
segment.start_control_point_indexsegment.start_point_index (spline segment)
segment.end_control_point_indexsegment.end_point_index (spline segment)
layer.name on list_layers resultlayer.layer_name (LandscapeLayerInfo_Custom property)
LandscapeService.create_spline_control_point(...)LandscapeService.create_spline_point(...) (no "control" in name)
LandscapeMaterialService.add_material_layer(...)LandscapeMaterialService.add_layer_to_blend_node(...)
mat_path = create_landscape_material(...) as stringReturns LandscapeMaterialCreateResult — use result.asset_path
connect_spline_points(..., tangent_length=25.0) ignoring negative sourcePass tangent_length=seg.start_tangent_length, end_tangent_length=seg.end_tangent_length — both tangents are needed for exact shape
Only passing tangent_length to connect_spline_pointsMust also pass end_tangent_length=seg.end_tangent_length — end tangent is typically negative (UE convention). Omitting it defaults to -start_tangent which is usually correct for NEW splines, but when copying, always pass both explicitly
create_spline_point("L", location=vec)Parameter is world_location, NOT location: create_spline_point("L", world_location=vec)
modify_spline_point("L", 0, location=vec)Parameter is world_location, NOT location: modify_spline_point("L", 0, world_location=vec)
modify_spline_point(...) with wrong rotationUse auto_calc_rotation=False, rotation=pt.rotation to set exact rotation from get_spline_info
Setting rotation BEFORE connect_spline_pointsconnect_spline_points triggers auto-calc rotation — set rotations AFTER all segments are connected
pt.paint_layer_name on LandscapeSplinePointInfoProperty is pt.layer_name (NOT paint_layer_name) — paint_layer_name is the parameter name in create_spline_point
Reading weights right after painting and getting 0.0Weights are written to edit layer — reads use same path
spline_info.points on LandscapeSplineInfoProperty is spline_info.control_points (NOT points)
Not setting spline segment meshes after connectingUse set_spline_segment_meshes() to assign mesh entries (e.g. SM_River) — splines are green without meshes
Forgetting to set control point meshesUse set_spline_point_mesh() — control points can have their own static mesh
layer.asset_path on LandscapeLayerInfo_CustomProperty is layer.layer_info_path (NOT asset_path)
Guessing random offset like 110000 for side-by-sideCalculate: offset = (resolution - 1) * scale.x e.g. (1009-1)*100 = 100800
Using FoliageService.clear_all_foliage() thinking it only clears one landscapeclear_all_foliage() removes ALL foliage from the ENTIRE level — use remove_foliage_in_radius() or remove_all_foliage_of_type() to target specific areas
Manually scattering foliage to replicate a landscape that uses LandscapeGrassTypeIf foliage is procedural (from LandscapeGrassType on the material), just copy paint layers — foliage auto-generates from weights. Check if list_foliage_types() returns 0; if so, foliage is procedural.
Using terrain_data without resolution and getting 1081×1081Always pass resolution=N matching your landscape. Common values: 505, 1009, 1017, 2033
Assuming 1081×1081 heightmap will fit any landscape1081 requires 36×36 components which WILL timeout. Use resolution=1009 (8×8, 63q, 2s) instead
Importing a heightmap without checking dimensionsAlways call get_heightmap_dimensions() first, then resize_heightmap() if sizes don't match
Creating landscape then downloading heightmap (wrong order)Decide config → calculate resolution → download at that resolution → create landscape → import
info.scale_x on LandscapeInfo_Custominfo.scale.x — scale, location, rotation are Vector/Rotator structs, not flattened floats
Assuming a landscape is centered on its locationIt spans +X/+Y from its location (corner-anchored): bounds = location to location + (resolution-1)*scale. A landscape created at (0,0) with 505 res @ scale 100 covers (0,0)–(50400,50400); its center is (25200,25200)
create_layer_info_object("Grass", "/Game/X", "LI_Grass")Third param is a bool: create_layer_info_object(layer_name, destination_path, is_weight_blended=True) — passing a string asset name causes TypeError: Cannot nativize 'str' as 'bool'
AssetTools.create_asset(..., unreal.LandscapeLayerInfoObject, None) to make a layer infoYields a broken asset with LayerName=None, and layer_name is read-only from Python so it can't be fixed afterwards. Use LandscapeMaterialService.create_layer_info_object(layer_name, path) — or EditorAssetLibrary.duplicate_asset an existing LayerInfo, which keeps its baked LayerName
Retrying create_landscape after PYTHON_EXECUTION_TIMEOUTThe first call usually still completes in the background — check landscape_exists(label) before retrying (duplicate labels now return an error)
result.location on LandscapeCreateResultResult has ONLY success, actor_label, error_message — get location via get_landscape_info(label).location
result.resolution_x / .resolution_y on HeightmapImportResultThe field is result.resolution — a string like "1009x1009", not separate ints. Split it if you need numbers.
Treating get_height_at_location() as a floatIt returns a LandscapeHeightSample struct — use .height (float) and .valid (bool); float(sample) / f"{sample}" raise TypeError
Clearing a World-Partition landscape (parent + streaming proxies)delete_landscape(label) now removes the streaming proxies too by default (include_proxies=True): it unregisters each LandscapeStreamingProxy sharing the landscape's GUID from the LandscapeInfo, destroys the proxies, then the parent. Pass include_proxies=False for the parent actor only. Do NOT destroy_actor the proxies yourself — destroying a proxy while the LandscapeInfo still references it crashes the editor (access violation in the Landscape module). With the editor closed, deleting the proxy package files under __ExternalActors__ is the other safe route.
info.component_count_x / info.component_count_yinfo.num_components is the TOTAL count (e.g. 64 for 8×8) — there are no per-axis fields
info.rotation.x (Rotator)Rotator fields are .roll / .pitch / .yaw — there is no .x/.y/.z on Rotator (only Vector has those)

Related Skills

TaskSkills to Load
Sculpt terrain onlylandscape
Simple painted material (LayerBlend)landscape + landscape-materials
Production auto-material (material functions, RVT, instances)landscape + landscape-auto-material
Material instance / biome configurationlandscape-auto-material
  • landscape-materials: Simple landscape materials with LandscapeLayerBlend nodes. Good for prototyping with 2-5 layers.
  • landscape-auto-material: Production-quality auto-materials using material functions, Runtime Virtual Textures, and material instances (Real_Landscape paradigm). Good for shipping quality.

When you need to create or modify the material applied to a landscape, load the appropriate material skill through GetSkills (AgentSkillToolset):

  • For simple materials: load landscape-materials
  • For production auto-materials: load landscape-auto-material

NOTE: For landscape material, texture, and layer blending operations, use LandscapeMaterialService (load the landscape-materials or landscape-auto-material skill).

Sample scripts (run via execute_python_code)

  • scripts/sculpt_terrain.txt — create a landscape and sculpt procedural mountain/valley features.

Additional gotchas

  • On a World Partition landscape, verify every sculpt and every reader with a line_trace_single grid; the only safe write path is export_heightmap -> stamp in Python -> import_heightmap. Never synthesise a flat baseline — a flat-plus-pit import erases carved features.
  • Pixel mapping: origin is the proxy min corner, world = min + px * scale, worldZ = (H - 32768) * 100/128, no row flip; check against a known trace height before writing.
  • import_heightmap does not write proxies to disk — call save_landscape (or save_packages the Landscape and every proxy). Use get_last_landscape_write to see what changed, since the dirty list is unreliable right after landscape calls.
  • flatten_at_location and set_height_in_region read and write the same edit layer now, so a region write stays inside its brush on World Partition; apply_erosion was always safe.
  • load_level from Python leaves all WP cells unloaded for the rest of the session, and create_landscape run headless produces a broken landscape (zero heights, no collision).
  • Delete a landscape together with its streaming proxies via delete_landscape(include_proxies=True); destroying proxy actors by hand crashes the editor.
  • The edit-layer merge can re-surface old garbage after a later merge (a brush spawn, apply_splines_to_landscape); re-verify landmarks by trace after any landscape-touching op.
  • Spawning any WaterBody auto-spawns a WaterBrushManager (affects_landscape defaults True) that adds a "Water" edit layer and poisons the terrain; flip that CDO default first and delete any spawned manager. WaterZone.zone_extent is on the actor; the river/lake seam material is lake_transition_material; target_wave_mask_depth is the still-water knob.

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Landscape AI skill do?

Create and edit landscape terrain — heightmaps, sculpting, paint layers, terrain analysis, mesh projection, and procedural features like mountains/valleys/craters (LandscapeService). Use when the user asks to create a landscape, sculpt/raise/lower terrain, import or export a heightmap, paint terrain layers, or add procedural terrain features. For terrain materials load landscape-materials; for real-world heightmaps load terrain-data.

Why use Landscape on TypingMind?

Because you install it once and use it with any model. Landscape is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Landscape in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/kevinpbuckley/VibeUE/tree/master/Content/Skills/landscape. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Landscape?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Landscape?

As many as you like. As long as a model supports skills, you can use Landscape with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Landscape AI skill free?

Yes. It is published on GitHub by kevinpbuckley under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇