GroundCover

GroundCover procedurally covers terrain with many small foliage/detail elements such as grass, flowers, weeds, reeds, rocks, shrubs, and other low-cost environmental clutter.

A GroundCover object does not place every element as an individual scene object. Instead, it generates and renders cells around the camera at runtime.

Ground cover elements can be rendered as:

  • Billboards using a shared foliage material/texture atlas.
  • Instanced TS shapes using shapeFilename entries.

GroundCover is terrain-based. It places elements on TerrainBlock surfaces and can filter placement by terrain material layer, elevation, and slope.


Basic example

A simple grass/flower ground cover object:

{
  "class": "GroundCover",
  "name": "grass_groundcover",
  "material": "grass_atlas",
  "radius": 80,
  "dissolveRadius": 65,
  "gridSize": 7,
  "maxElements": 12000,
  "zOffset": 0.02,
  "seed": 12345,

  "billboardUVs": [
    [0.0, 0.0, 0.25, 0.5],
    [0.25, 0.0, 0.25, 0.5]
  ],

  "probability": [0.85, 0.15],
  "sizeMin": [0.4, 0.25],
  "sizeMax": [1.1, 0.7],
  "sizeExponent": [1.5, 1.2],
  "windScale": [1.0, 0.6],
  "maxSlope": [35, 30],
  "minElevation": [-99999, -99999],
  "maxElevation": [99999, 99999],

  "minClumpCount": [3, 1],
  "maxClumpCount": [10, 4],
  "clumpExponent": [1.5, 1.0],
  "clumpRadius": [2.0, 1.0],

  "windDirection": [1, 0],
  "windGustLength": 20,
  "windGustFrequency": 0.15,
  "windGustStrength": 0.25,
  "windTurbulenceFrequency": 1.5,
  "windTurbulenceStrength": 0.05
}

How GroundCover works

GroundCover maintains a square grid of cells around the camera.

For each active cell, it:

  1. Chooses random placement points.
  2. Samples the terrain height and normal.
  3. Checks terrain material layer filters.
  4. Checks elevation and slope limits.
  5. Chooses element type using per-type probabilities.
  6. Applies random size, rotation, clumping, wind scale, and lightmap color.
  7. Builds billboard buffers and/or shape instance data.
  8. Renders only visible cells.

As the camera moves, cells are shifted and recycled. New cells are generated at the edge of the grid.


Important fields

Field Type Description
class string Must be "GroundCover".
name string Scene object name.
material string Material used for billboard ground cover.
radius number Outer generation/render radius from the camera.
dissolveRadius number Distance where billboard fading begins. Should be less than or equal to radius.
reflectScale number Scales culling radius in reflection passes. Use 0 to skip reflections.
gridSize integer Number of ground cover cells per axis.
zOffset number Vertical offset added to placed elements.
seed integer Random seed used for deterministic placement.
maxElements integer Maximum approximate number of generated elements across the active grid.
maxBillboardTiltAngle number Maximum angle billboards may tilt down toward the camera.
shapeCullRadius number Distance at which shape-based elements are culled.
shapesCastShadows bool Whether TS shape elements render in shadow passes.

Per-type fields

Ground cover supports multiple cover types. Each type has its own billboard UVs, shape, probability, size range, layer filters, and placement settings.

In JSON, these fields are commonly represented as arrays. Index 0 describes type 0, index 1 describes type 1, and so on.

Field Type Description
billboardUVs array UV rectangle in the ground cover material atlas for each billboard type.
useWorldRandomYaw array[bool] Uses random world-space yaw for billboard orientation where supported.
shapeFilename array[string] Optional shape file for this type. If set, the type renders as instanced shapes instead of billboards.
layer array[string] Terrain material internal name to restrict placement to. Empty means any terrain layer.
invertLayer array[bool] Treats layer as an exclusion mask instead of an inclusion mask.
probability array[number] Relative probability of each type. Values are normalized internally.
sizeMin array[number] Minimum random size/scale.
sizeMax array[number] Maximum random size/scale.
sizeExponent array[number] Bias exponent for random size selection.
windScale array[number] Per-type wind displacement multiplier.
maxSlope array[number] Maximum terrain slope in degrees. 0 disables slope limiting.
minElevation array[number] Minimum world-space elevation for placement.
maxElevation array[number] Maximum world-space elevation for placement.
minClumpCount array[integer] Minimum number of elements in a clump.
maxClumpCount array[integer] Maximum number of elements in a clump.
clumpExponent array[number] Bias exponent for clump size/count.
clumpRadius array[number] Maximum radius of a clump.

Billboard types

Billboard ground cover uses the material field and billboardUVs.

Example with two billboard types in one texture atlas:

{
  "class": "GroundCover",
  "name": "meadow_grass",
  "material": "meadow_grass_atlas",
  "radius": 90,
  "dissolveRadius": 75,
  "gridSize": 7,
  "maxElements": 16000,

  "billboardUVs": [
    [0.0, 0.0, 0.25, 0.5],
    [0.25, 0.0, 0.25, 0.5]
  ],

  "probability": [0.8, 0.2],
  "sizeMin": [0.4, 0.3],
  "sizeMax": [1.2, 0.8],
  "windScale": [1.0, 0.8]
}

Each billboardUVs entry is a rectangle in the material texture atlas:

[u, v, width, height]

The engine uses the material’s diffuse texture dimensions to calculate billboard aspect ratio from the UV rectangle.


Shape types

If shapeFilename is set for a type, that type renders as instanced TS shape geometry instead of a billboard.

Example:

{
  "class": "GroundCover",
  "name": "rocks_and_grass",
  "material": "grass_atlas",
  "radius": 70,
  "dissolveRadius": 55,
  "gridSize": 6,
  "maxElements": 8000,
  "shapeCullRadius": 60,
  "shapesCastShadows": true,

  "billboardUVs": [
    [0.0, 0.0, 0.25, 0.5],
    [0.0, 0.0, 1.0, 1.0]
  ],

  "shapeFilename": [
    "",
    "/levels/example/art/shapes/rocks/small_rock.dae"
  ],

  "probability": [0.9, 0.1],
  "sizeMin": [0.4, 0.4],
  "sizeMax": [1.1, 1.5],
  "windScale": [1.0, 0.0]
}

Here:

  • Type 0 is billboard grass.
  • Type 1 is an instanced rock shape.
Shape-based ground cover is more expensive than billboard ground cover. Use it sparingly for rocks, shrubs, small plants, or detail props.

Material requirements

GroundCover billboard rendering uses a foliage material feature internally.

The billboard material should generally be authored as a foliage/alpha material with:

  • Diffuse/albedo texture atlas
  • Alpha channel or opacity for cutout foliage
  • Good mipmaps
  • Correct texture atlas layout for billboardUVs
  • Optional normal/roughness maps where supported by the material setup

If the material cannot be found, GroundCover falls back to a warning material.

Example:

"material": "grass_atlas"

Terrain layer filtering

Use layer to restrict a type to a terrain material.

"layer": ["grass"],
"probability": [1.0]

This places the type only on terrain areas using the grass terrain material.

Use invertLayer to exclude a terrain material:

"layer": ["rock"],
"invertLayer": [true]

This places the type everywhere except on the rock terrain material.

The layer value should match the terrain material internal name, not necessarily the displayed material label.

Elevation filtering

Use minElevation and maxElevation to restrict placement by world-space height.

Example: only place alpine grass above 500 meters:

"minElevation": [500],
"maxElevation": [99999]

Example: only place beach grass below 8 meters:

"minElevation": [-99999],
"maxElevation": [8]

Slope filtering

Use maxSlope to restrict placement to flatter terrain.

"maxSlope": [35]

This allows placement only where terrain slope is up to about 35 degrees.

A value of 0 disables slope filtering:

"maxSlope": [0]

Size settings

Each type can have randomized size:

"sizeMin": [0.4],
"sizeMax": [1.2],
"sizeExponent": [1.5]
Field Meaning
sizeMin Smallest possible size.
sizeMax Largest possible size.
sizeExponent Bias between min and max.

Higher sizeExponent values bias more placements toward the smaller end of the range.


Probability

probability controls the relative distribution of types.

"probability": [0.7, 0.2, 0.1]

The values are normalized internally, so these are equivalent in ratio to:

"probability": [7, 2, 1]

Types with no valid billboard UV/aspect and no valid shapeFilename are ignored.


Clumping

Clumping controls whether elements appear uniformly scattered or grouped in patches.

Example:

"minClumpCount": [4],
"maxClumpCount": [12],
"clumpExponent": [1.5],
"clumpRadius": [2.5]
Field Meaning
minClumpCount Minimum number of elements in a clump.
maxClumpCount Maximum number of elements in a clump.
clumpExponent Bias for clump count distribution.
clumpRadius Radius around the clump center where elements may appear.

Use clumping for:

  • Grass tufts
  • Flowers in patches
  • Reeds near water
  • Rock scatter groups
  • Bush clusters

Use lower clump counts and larger radius for more even distribution.


Wind

Ground cover billboards support wind animation.

Wind fields

Field Type Description
windDirection array[2] 2D wind direction.
windGustLength number Distance in meters between gust peaks.
windGustFrequency number How often gust peaks occur per second.
windGustStrength number Maximum gust displacement in meters.
windTurbulenceFrequency number Frequency/rapidity of turbulence.
windTurbulenceStrength number Maximum turbulence displacement in meters.

Example:

"windDirection": [1, 0],
"windGustLength": 25,
"windGustFrequency": 0.12,
"windGustStrength": 0.25,
"windTurbulenceFrequency": 1.2,
"windTurbulenceStrength": 0.05

Per-type windScale multiplies the wind effect:

"windScale": [1.0, 0.4, 0.0]

Use 0 for objects that should not sway, such as rocks.


Grid, radius, and density

radius

"radius": 80

radius is the outer area around the camera where ground cover is generated and rendered.

Larger values show foliage farther away but require more cells/elements.


dissolveRadius

"dissolveRadius": 65

dissolveRadius controls where billboards begin fading out.

The fade range is roughly:

dissolveRadius -> radius

For example:

"radius": 80,
"dissolveRadius": 65

means elements are fully visible up to about 65 meters and fade out by 80 meters.


gridSize

"gridSize": 7

gridSize is the number of cells per axis.

Total active cells:

gridSize * gridSize

Examples:

gridSize Cells
5 25
7 49
9 81

Increasing gridSize creates smaller cells and can reduce per-cell billboard limits, but more cells must be managed.


maxElements

"maxElements": 12000

maxElements is the approximate maximum number of elements distributed across the active grid.

Per-cell placement count is roughly:

maxElements / (gridSize * gridSize)

modified by global density scaling.

If a cell has too many billboards, the engine may warn that the object has too many elements. Decrease maxElements or increase gridSize.

Global density scale

Ground cover density is also affected by the global preference variable:

$pref::GroundCover::densityScale

This scales placement density globally.

The engine also exposes runtime stats:

$GroundCover::renderedCells
$GroundCover::renderedBillboards
$GroundCover::renderedBatches
$GroundCover::renderedShapes

These are useful for debugging and performance tuning.


Reflections and shadows

reflectScale

"reflectScale": 0.5

reflectScale scales the render/culling radius when rendering reflection passes, typically for water reflections.

Use lower values to reduce reflection cost.

Set to 0 to skip ground cover in reflections:

"reflectScale": 0

shapesCastShadows

"shapesCastShadows": true

Controls whether shape-based ground cover renders during shadow passes.

Billboards are not submitted to shadow passes by this code path.

For performance, leave this disabled unless the shapes are important enough to cast visible shadows.


Debug fields

Field Type Description
lockFrustum bool Freezes culling/generation frustum for debugging.
renderCells bool Draws debug boxes for active render cells.
noBillboards bool Disables billboard rendering.
noShapes bool Disables shape rendering.

Example:

"renderCells": true,
"noShapes": true

Use these fields to inspect placement cells and isolate billboard vs shape cost.


Terrain dependency

GroundCover requires terrain.

It queries active TerrainBlock objects and samples:

  • Terrain height
  • Terrain normal
  • Terrain material layer
  • Terrain lightmap color
  • Terrain layer texture/height data for newer placement paths

If there is no terrain, ground cover cells cannot generate.

GroundCover is intended for terrain surfaces. It does not project onto arbitrary static meshes like DecalRoad with overObjects.

Terrain updates

GroundCover listens for terrain updates.

When terrain changes:

Terrain update Behavior
Lightmap update Frees/regenerates all cells.
Heightmap update Frees cells overlapping the changed area.
Layer update Frees cells overlapping the changed area.
Empty/update marker Frees cells overlapping the changed area where supported.

This allows ground cover to regenerate after terrain sculpting, painting, or lighting changes.


Full examples

Grass on one terrain layer

{
  "class": "GroundCover",
  "name": "grass_only_groundcover",
  "material": "grass_atlas",
  "radius": 75,
  "dissolveRadius": 60,
  "gridSize": 7,
  "maxElements": 14000,
  "zOffset": 0.02,
  "seed": 1001,

  "billboardUVs": [
    [0.0, 0.0, 0.25, 0.5]
  ],

  "layer": ["grass"],
  "probability": [1.0],
  "sizeMin": [0.45],
  "sizeMax": [1.15],
  "sizeExponent": [1.6],
  "windScale": [1.0],
  "maxSlope": [35],

  "minClumpCount": [3],
  "maxClumpCount": [9],
  "clumpExponent": [1.5],
  "clumpRadius": [2.0],

  "windDirection": [1, 0.2],
  "windGustLength": 25,
  "windGustFrequency": 0.12,
  "windGustStrength": 0.2,
  "windTurbulenceFrequency": 1.0,
  "windTurbulenceStrength": 0.04
}

Mixed grass and flowers

{
  "class": "GroundCover",
  "name": "meadow_groundcover",
  "material": "meadow_foliage_atlas",
  "radius": 85,
  "dissolveRadius": 70,
  "gridSize": 8,
  "maxElements": 18000,
  "zOffset": 0.02,
  "seed": 4321,

  "billboardUVs": [
    [0.0, 0.0, 0.25, 0.5],
    [0.25, 0.0, 0.25, 0.5],
    [0.50, 0.0, 0.25, 0.5]
  ],

  "layer": ["grass", "grass", "grass"],
  "probability": [0.75, 0.15, 0.10],
  "sizeMin": [0.4, 0.25, 0.25],
  "sizeMax": [1.1, 0.6, 0.7],
  "sizeExponent": [1.5, 1.2, 1.2],
  "windScale": [1.0, 0.7, 0.7],
  "maxSlope": [35, 30, 30],

  "minClumpCount": [4, 2, 2],
  "maxClumpCount": [12, 6, 5],
  "clumpExponent": [1.5, 1.2, 1.2],
  "clumpRadius": [2.5, 1.2, 1.0]
}

Reeds near low elevation

{
  "class": "GroundCover",
  "name": "shore_reeds",
  "material": "reed_atlas",
  "radius": 60,
  "dissolveRadius": 48,
  "gridSize": 6,
  "maxElements": 6000,
  "zOffset": 0.01,
  "seed": 777,

  "billboardUVs": [
    [0.0, 0.0, 0.5, 1.0]
  ],

  "layer": ["mud"],
  "probability": [1.0],
  "sizeMin": [0.8],
  "sizeMax": [1.8],
  "sizeExponent": [1.3],
  "windScale": [1.2],
  "maxSlope": [15],
  "minElevation": [-2],
  "maxElevation": [4],

  "minClumpCount": [5],
  "maxClumpCount": [20],
  "clumpExponent": [1.5],
  "clumpRadius": [3.0]
}

Rocks as shape ground cover

{
  "class": "GroundCover",
  "name": "small_rocks_groundcover",
  "material": "groundcover_dummy_atlas",
  "radius": 70,
  "dissolveRadius": 60,
  "gridSize": 7,
  "maxElements": 3000,
  "shapeCullRadius": 55,
  "shapesCastShadows": false,
  "seed": 2468,

  "shapeFilename": [
    "/levels/example/art/shapes/rocks/small_rock_a.dae",
    "/levels/example/art/shapes/rocks/small_rock_b.dae"
  ],

  "layer": ["rock", "rock"],
  "probability": [0.6, 0.4],
  "sizeMin": [0.4, 0.3],
  "sizeMax": [1.4, 1.2],
  "sizeExponent": [1.8, 1.8],
  "windScale": [0.0, 0.0],
  "maxSlope": [45, 45],

  "minClumpCount": [1, 1],
  "maxClumpCount": [4, 3],
  "clumpExponent": [2.0, 2.0],
  "clumpRadius": [4.0, 3.5]
}

Performance guidelines

Ground cover can render many elements, so tuning matters.

Main cost drivers

Setting Performance impact
maxElements Higher values generate/render more instances.
radius Larger radius increases visible cells/elements.
gridSize More cells to manage; smaller per-cell batches.
Shape types More expensive than billboards.
shapeCullRadius Larger value renders shapes farther away.
shapesCastShadows Shape shadows add shadow-pass cost.
Reflection rendering Controlled by reflectScale.

Recommended tuning

  • Prefer billboards for grass and small plants.
  • Use shapes only for sparse objects such as rocks or shrubs.
  • Keep radius modest; use dissolveRadius to hide pop-in.
  • Increase gridSize if per-cell element warnings appear.
  • Reduce maxElements on lower-end maps or dense scenes.
  • Set reflectScale to 0 or a small value unless reflected foliage is important.
  • Disable shapesCastShadows unless shape shadows are visually necessary.
  • Use terrain layer filters to avoid wasting placement on unsuitable areas.
  • Use maxSlope and elevation limits to reduce unwanted placements.

Common issues

GroundCover does not appear

Possible causes:

  • No TerrainBlock exists.
  • maxElements is 0.
  • probability values are all 0.
  • No valid billboardUVs and no valid shapeFilename.
  • Material is missing or invalid.
  • layer does not match the terrain material internal name.
  • minElevation / maxElevation excludes the terrain.
  • maxSlope is too restrictive.
  • Global density scale is 0.

Check:

$pref::GroundCover::densityScale

Foliage appears on the wrong terrain material

Check that layer uses the terrain material internal name.

Also check invertLayer:

"invertLayer": [false]

If invertLayer is true, the selected layer is excluded instead of included.


Foliage floats or clips into terrain

Adjust:

"zOffset": 0.02

Use small values. Too much offset makes grass float.


Too many elements warning

The per-cell billboard count is too high.

Fix by reducing:

"maxElements": 8000

or increasing:

"gridSize": 9

Increasing gridSize spreads the same overall element budget across more cells.


Ground cover pops in or fades too close

Increase the render radius and fade range:

"radius": 100,
"dissolveRadius": 80

Keep in mind that larger radii cost more.


Shape ground cover is expensive

Try:

"shapeCullRadius": 35,
"shapesCastShadows": false

Also lower the shape type’s probability or split large/detail-heavy shapes into simpler assets.


Grass appears on steep cliffs

Set maxSlope:

"maxSlope": [30]

A value of 0 disables slope filtering, so use a positive angle to restrict placement.


Wind is too strong

Lower global wind fields:

"windGustStrength": 0.1,
"windTurbulenceStrength": 0.02

or lower per-type wind:

"windScale": [0.5]

Summary

GroundCover is the main terrain foliage/detail scatter object.

It procedurally generates camera-local cells of billboard and/or instanced shape elements. Placement can be controlled by terrain layer, probability, size range, clumping, elevation, slope, and random seed. It supports wind animation for billboards, runtime density scaling, reflection scaling, and debug visualization.

Use it for dense terrain details like grass and flowers, and use shape types sparingly for small rocks, shrubs, and other sparse clutter.

Last modified: June 2, 2026

Any further questions?

Join our discord
Our documentation is currently incomplete and undergoing active development. If you have any questions or feedback, please visit this forum thread.