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:
shapeFilename entries.GroundCover is terrain-based. It places elements on TerrainBlock surfaces and can filter placement by terrain material layer, elevation, and slope.
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
}
GroundCover maintains a square grid of cells around the camera.
For each active cell, it:
As the camera moves, cells are shifted and recycled. New cells are generated at the edge of the grid.
| 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. |
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 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.
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:
0 is billboard grass.1 is an instanced rock shape.GroundCover billboard rendering uses a foliage material feature internally.
The billboard material should generally be authored as a foliage/alpha material with:
billboardUVsIf the material cannot be found, GroundCover falls back to a warning material.
Example:
"material": "grass_atlas"
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.
layer value should match the terrain material internal name, not necessarily the displayed material label.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]
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]
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 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 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:
Use lower clump counts and larger radius for more even distribution.
Ground cover billboards support wind animation.
| 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.
"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": 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": 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": 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.
maxElements or increase gridSize.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.
"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": 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.
| 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.
GroundCover requires terrain.
It queries active TerrainBlock objects and samples:
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.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.
{
"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
}
{
"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]
}
{
"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]
}
{
"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]
}
Ground cover can render many elements, so tuning matters.
| 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. |
radius modest; use dissolveRadius to hide pop-in.gridSize if per-cell element warnings appear.maxElements on lower-end maps or dense scenes.reflectScale to 0 or a small value unless reflected foliage is important.shapesCastShadows unless shape shadows are visually necessary.layer filters to avoid wasting placement on unsuitable areas.maxSlope and elevation limits to reduce unwanted placements.Possible causes:
TerrainBlock exists.maxElements is 0.probability values are all 0.billboardUVs and no valid shapeFilename.layer does not match the terrain material internal name.minElevation / maxElevation excludes the terrain.maxSlope is too restrictive.0.Check:
$pref::GroundCover::densityScale
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.
Adjust:
"zOffset": 0.02
Use small values. Too much offset makes grass float.
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.
Increase the render radius and fade range:
"radius": 100,
"dissolveRadius": 80
Keep in mind that larger radii cost more.
Try:
"shapeCullRadius": 35,
"shapesCastShadows": false
Also lower the shape type’s probability or split large/detail-heavy shapes into simpler assets.
Set maxSlope:
"maxSlope": [30]
A value of 0 disables slope filtering, so use a positive angle to restrict placement.
Lower global wind fields:
"windGustStrength": 0.1,
"windTurbulenceStrength": 0.02
or lower per-type wind:
"windScale": [0.5]
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.
Was this article helpful?