OpenParable
Skip to content
← Back to skills

foliage

Development

Create foliage types and place/scatter foliage instances on landscapes and meshes (FoliageService). Use when the user asks to add trees/grass/bushes/rocks as foliage, create a foliage type from a mesh, scatter or paint foliage instances, or query/remove foliage on a surface.

View on GitHub ↗
QUICK START

How to use this skill

Bring this guide into your coding agent with a prompt tailored to the tool you use.

  1. Open your project in Codex.
  2. Copy and paste the prompt below.
  3. Review files and risks before approving any action.
Prompt to paste
Inspect the Agent Skill at https://github.com/kevinpbuckley/VibeUE/blob/HEAD/Content/Skills/foliage/SKILL.md and all supporting files. Report provenance, permissions, scripts, dependencies, and security risks. Propose an exact installation into .agents/skills/foliage, but wait for my approval before writing files or running scripts.

Copying this prompt does not install or run the skill. Review third-party files before use.

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

Foliage & Vegetation Skill

Critical Rules

Mesh Path Required

All placement methods require a valid UStaticMesh or UFoliageType asset path. Use discover_python_class('unreal.FoliageService') to see available methods.

Surface Trace

All scatter/add methods trace downward to find the ground surface. If no surface is hit (e.g., position is outside the landscape bounds), that instance is rejected. Check instances_rejected in the result.

Foliage Type Auto-Creation

When you pass a UStaticMesh path, the service auto-creates a transient UFoliageType internally. For persistent, reusable foliage types with custom properties, use create_foliage_type() first.

Seed for Reproducibility

Use the seed parameter to get reproducible scatter patterns. Same seed + same parameters = same layout. Use seed=0 for random.

Layer-Aware Placement Requires Landscape

scatter_foliage_on_layer() requires:

  1. A valid landscape name/label
  2. A painted layer on that landscape
  3. Layer weight threshold (0.0-1.0) — only places where weight exceeds threshold

If you get instances_added=0 with instances_rejected ≈ 4× requested, every candidate failed the surface trace or the weight check. Before blaming the threshold, check geometry:

  • The scatter circle must overlap the painted part of the named landscape. A landscape is corner-anchored — created at (0,0) it spans (0,0)→(+50400,+50400) for 505 res @ scale 100, so a scatter "around (0,0)" is 3/4 off-landscape.
  • Verify weights at a few candidate points first: unreal.LandscapeService.get_layer_weights_at_location(label, x, y).
  • If another landscape/mesh overlaps the area at a higher Z, the surface trace hits that surface instead — instances land on the wrong surface or get rejected. Paint a larger area or center the scatter where only the target landscape exists.

⚠️ Common Mistakes to Avoid

WRONGCORRECT
result.successresult.success (Python auto-converts bSuccess)
result.countresult.instances_added
result.instances_placedresult.instances_added (NOT instances_placed)
ft.asset_path on VibeUEFoliageTypeInfoft.foliage_type_path (NOT asset_path)
scatter_foliage(..., landscape="X")scatter_foliage(..., landscape_name_or_label="X")
clear_all_foliage() to clear one landscapeclear_all_foliage() removes ALL foliage from the ENTIRE level — use remove_foliage_in_radius() for targeted removal
Manually scattering foliage on a cloned landscapeIf source uses LandscapeGrassType (procedural), just copy paint layers — foliage auto-generates. Check list_foliage_types(): if 0, foliage is procedural.
Forgetting to check instances_rejectedAlways check — tells you how many failed surface trace or layer filter
Expecting get_instance_count() == -1 after removing all instances-1 means the type was never in the level. Once registered, the count is 0 after removal — the foliage type entry persists
Expecting list_foliage_types() to shrink after clear_all_foliage()Clearing removes instances, not type registrations — 0-instance entries remain listed. Filter on ft.instance_count > 0
set_foliage_type_property(path, "CullDistance", "30000")Whole structs need UE import text: "(Min=0,Max=30000)". Simpler: set the member via a dotted path — set_foliage_type_property(path, "CullDistance.Max", "30000")
Scattering "around the landscape at (0,0)" centered on (0,0)Landscapes are corner-anchored: created at (0,0) the terrain spans +X/+Y only. Center the scatter on the landscape's actual center (location + (resolution-1)*scale/2) or most candidates miss the surface
AssetDiscoveryService.list_assets(path) for cleanupAssetDiscoveryService.list_assets_in_path(path, asset_type) — or the manage_asset tool with action='list'

Return Types - EXACT Properties

FFoliageScatterResult (from scatter_foliage, scatter_foliage_rect, add_foliage_instances, scatter_foliage_on_layer)

PropertyTypeDescription
successboolWhether operation succeeded
instances_addedintNumber of instances actually placed
instances_requestedintNumber of instances requested
instances_rejectedintNumber rejected (no surface, slope, layer weight)
error_messagestrError details if failed

FFoliageRemoveResult (from remove_foliage_in_radius, remove_all_foliage_of_type, clear_all_foliage)

PropertyTypeDescription
successboolWhether operation succeeded
instances_removedintNumber of instances removed
error_messagestrError details if failed

VibeUEFoliageTypeInfo (from list_foliage_types)

PropertyTypeDescription
foliage_type_namestrName of the foliage type
mesh_pathstrPath to the static mesh
instance_countintNumber of instances in level
foliage_type_pathstrAsset path of the foliage type

FFoliageTypeCreateResult (from create_foliage_type)

PropertyTypeDescription
successboolWhether creation succeeded
asset_pathstrPath to the created foliage type asset
error_messagestrError details if failed

FFoliageQueryResult (from get_foliage_in_radius)

PropertyTypeDescription
successboolWhether query succeeded
total_instancesintTotal matching instances (may exceed returned list)
instancesArrayList of FFoliageInstanceInfo
error_messagestrError details if failed

FFoliageInstanceInfo (individual instance data)

PropertyTypeDescription
locationVectorWorld location
rotationRotatorWorld rotation
scaleVectorScale (DrawScale3D)
instance_indexintIndex in the foliage info array

Workflows

Quick Scatter Trees

import unreal

result = unreal.FoliageService.scatter_foliage(
    "/Game/Meshes/SM_Tree_01",
    0.0, 0.0,     # center X, Y
    5000.0,        # radius
    200,           # count
    min_scale=0.8,
    max_scale=1.2,
    seed=42
)
if result.success:
    print(f"Placed {result.instances_added}/{result.instances_requested}")
    print(f"Rejected: {result.instances_rejected}")

Create Foliage Type + Scatter

import unreal

# Step 1: Create a reusable foliage type with custom properties
ft = unreal.FoliageService.create_foliage_type(
    mesh_path="/Game/Meshes/SM_Pine_Tree",
    save_path="/Game/Foliage",
    asset_name="FT_PineTree",
    min_scale=0.7,
    max_scale=1.3,
    align_to_normal=True,
    align_to_normal_max_angle=30.0,
    ground_slope_max_angle=40.0,
    cull_distance_max=25000.0
)
if ft.success:
    print(f"Created: {ft.asset_path}")

    # Step 2: Scatter using the foliage type
    result = unreal.FoliageService.scatter_foliage(
        ft.asset_path, 0.0, 0.0, 8000.0, 500, seed=42)

Scatter on Paint Layer

import unreal

# Place grass clumps only where the "Grass" paint layer is dominant
result = unreal.FoliageService.scatter_foliage_on_layer(
    "/Game/Meshes/SM_Grass_Clump",
    "MyTerrain",            # landscape name
    "Grass",                 # layer name
    1000,                    # count
    min_scale=0.5,
    max_scale=1.5,
    layer_weight_threshold=0.6,
    seed=99
)
print(f"Placed {result.instances_added}, rejected {result.instances_rejected}")

Scatter in Rectangle

import unreal

result = unreal.FoliageService.scatter_foliage_rect(
    "/Game/Meshes/SM_Bush",
    -5000.0, -5000.0,  # min X, Y
    5000.0, 5000.0,     # max X, Y
    300,                 # count
    min_scale=0.5,
    max_scale=1.0,
    seed=7
)

Place at Specific Locations

import unreal

locations = [
    unreal.Vector(0, 0, 0),
    unreal.Vector(500, 500, 0),
    unreal.Vector(-500, 500, 0),
    unreal.Vector(500, -500, 0),
    unreal.Vector(-500, -500, 0),
]

result = unreal.FoliageService.add_foliage_instances(
    "/Game/Meshes/SM_Tree_01",
    locations,
    min_scale=1.0,
    max_scale=1.0,
    align_to_normal=True,
    random_yaw=True,
    trace_to_surface=True
)

Multi-Type Forest

import unreal

configs = [
    ("/Game/Meshes/SM_Oak",   300, 0.8, 1.2),
    ("/Game/Meshes/SM_Pine",  200, 0.7, 1.3),
    ("/Game/Meshes/SM_Birch", 100, 0.6, 1.0),
    ("/Game/Meshes/SM_Bush",  400, 0.4, 0.8),
]

for mesh, count, min_s, max_s in configs:
    result = unreal.FoliageService.scatter_foliage(
        mesh, 0.0, 0.0, 10000.0, count,
        min_scale=min_s, max_scale=max_s, seed=42)
    print(f"{mesh}: {result.instances_added} placed")

List and Query

import unreal

# List all foliage types in the level
types = unreal.FoliageService.list_foliage_types()
for ft in types:
    print(f"{ft.foliage_type_name}: {ft.instance_count} instances ({ft.mesh_path})")

# Get count for a specific type
count = unreal.FoliageService.get_instance_count("/Game/Meshes/SM_Tree_01")
print(f"Tree count: {count}")

# Query instances in a region
query = unreal.FoliageService.get_foliage_in_radius(
    "/Game/Meshes/SM_Tree_01", 0.0, 0.0, 2000.0, max_results=50)
for inst in query.instances:
    print(f"  [{inst.instance_index}] at {inst.location}")

Remove Foliage

import unreal

# Remove trees from a building site
result = unreal.FoliageService.remove_foliage_in_radius(
    "/Game/Meshes/SM_Tree_01", 2000.0, 3000.0, 500.0)
print(f"Removed {result.instances_removed}")

# Remove ALL instances of one type
result = unreal.FoliageService.remove_all_foliage_of_type("/Game/Meshes/SM_Bush")

# Nuclear option: remove everything
result = unreal.FoliageService.clear_all_foliage()
print(f"Cleared {result.instances_removed} total instances")

Check Existence

import unreal

# Does the mesh/foliage type exist as an asset?
if unreal.FoliageService.foliage_type_exists("/Game/Meshes/SM_Tree_01"):
    print("Asset exists")

# Are there any instances of this type in the level?
if unreal.FoliageService.has_foliage_instances("/Game/Meshes/SM_Tree_01"):
    print("Instances exist in level")

Sample scripts (run via execute_python_code)

  • scripts/scatter_foliage.txt — create a foliage type from a mesh and scatter instances onto the surface.