Foliage
A field of grass is tens of thousands of blades and a forest is thousands of trees, which no engine draws one entity at a time. The Foliage
component grows them as cells: the area is cut into squares and each square is one mesh with all its plants in it, so a field is a few
hundred draws that culling can drop whole, and each cell is built at the detail its distance deserves.
The component
Section titled “The component”Add a Foliage component to an entity. The area is centred on it, and its children are the cells. They are
procedural chunks: grown round the streaming sources (or the eye, in a game without any, and in
the editor’s scene view) out to drawDistance, nearest first, and let go with their meshes past it, so a large area holds only the cells
near what looks at it. When a field, the ground or the water changes they are grown again, each old cell staying until its new one
replaces it. Nothing is baked.
| Field | What it does |
|---|---|
mesh |
The plant to scatter. Empty grows grass, with blades made here. |
material |
A material on the Foliage shader, which makes the wind (below). Empty draws the plants still, in their own colours. |
sizeX, sizeZ, cellSize |
The area, and the side of the cells it is cut into. |
count, seed |
Plants per cell, and the seed: another seed grows the same plants in other places. |
minScale, maxScale |
Each plant is given a size between these. |
minUp, heightMin, heightMax |
Where plants grow: the least a place may face up (lower lets them onto slopes), and a band of world heights. |
waterDepth |
How deep under water a plant may stand: 0 (the default) keeps them on dry ground, reeds stand in the shallows. |
bladeHeight, bladeWidth |
Grass only: the size of a blade at scale 1. |
lodDistance, drawDistance |
Past the first a cell is drawn as its far version; past the second it is not drawn. |
cardU0, cardV0, cardU1, cardV1 |
A mesh’s far version is two crossed cards showing this rectangle of its texture (cardTent 1 cuts them as tents, a conifer’s outline, instead of rectangles). |
If the scene has a Terrain, plants stand on it, keep off what is too steep, and wait for it to be built; with none, the area is flat at the entity’s height.
Budget and reservations
Section titled “Budget and reservations”count is per cell, not for the whole area. Before distance and terrain rejection, a 700 × 700 field with 35-unit cells and
750 plants per cell has 400 cells and up to 300,000 plants. Review density (count / cellSize²), cell count, near/far distance and
shadow passes together. Larger cells reduce submissions but cull more coarsely. Short grass can set castShadows: false; it still
receives light and shadows. Existing scenes keep casting unless this option is changed.
When a WorldGen exists, Foliage respects its exclusion zones. zoneWorld selects the world by name (empty uses the first),
zoneTags adds comma-separated tags to foliage and grass (or tree for a mesh), and zonePadding adds clearance in world units.
Reserve building floors, steps and entrances with WorldZone boxes and roads with zones from roadZones / setRoad in
esengine/worldgen. Moving or deleting a zone regenerates the field. Without WorldGen the field grows as before.
The MCP audit_vegetation tool reports authored upper bounds and live counters. Warm generation with step before measuring;
gpuDraws includes shadow and other passes, while drawCalls counts scene lists. Background-window idle time is not a foreground
frame-rate benchmark. For large forests, WorldRules.plants and PlacedFoliage share geometry and support per-instance LOD.
The Foliage shader (builtin:foliage) bends each vertex by a weight painted in its colour (red is how far up the plant it is, so the root
stays and the tip moves; green is a phase of the plant’s own, so a meadow does not sway as one), with a gust that travels across the world and
a flutter on top. Its parameters are in the material: u_windStrength, u_windSpeed, u_windDirection (degrees), u_gustScale (the lower, the
longer a gust) and u_flutter. Plants are two-sided and let light through them (u_translucency); a leaf texture with an alpha channel
(u_albedo, u_useTexture, u_alphaCutoff) cuts the outline of a leaf out of a card.
A shader of your own can read the same weights: a vertex hook (#pragma vertexoffset) sees the vertex colour as es_vertexColor.
Levels of detail
Section titled “Levels of detail”Each cell is built twice. Near, grass is a bent, tapered blade for every plant; far, a third of the blades, broader and flat. A mesh is
copied in full near, and far is two crossed cards a plant. The engine’s own level of detail picks between
them by how much of the screen the cell covers, and drops the cell past drawDistance.
Placed plants
Section titled “Placed plants”Foliage scatters plants by rule. A scene an artist composed has every plant where the artist put it, tens of thousands of them, and as entities that is a
transform, a renderer and a draw each. PlacedFoliage keeps them as data in one entity: a table of kinds and a list of records.
| Field | What it does |
|---|---|
sizeX, sizeZ, cellSize |
The area, centred on the entity, and the side of the cells it is cut into. |
kinds |
What each kind is made of: its parts (a mesh, a cheaper lod1Mesh, a material, a texture, a tint and whether it is opaque), the lodDistance past which the cheaper mesh stands in and the drawDistance past which it is not drawn, and a far version, far parts drawn from drawDistance out to farDistance. |
records |
Where each plant stands: six numbers a plant, the kind’s index, x, y, z in the entity’s space, the turn about the vertical in radians, and the scale. |
The cells round the streaming sources (or the eye) are grown and let go past the distance. A cell appears at once and fills in over the frames after it, each kind
and part of it a few microseconds of the frame’s budget, nearest first. Each part is one entity that draws the part’s mesh, shared by every cell, at every place a plant
stands: a MeshInstances component holds the places (sixty-four bytes a plant) and the engine culls each plant against the view, takes the cheaper mesh for the plants
past lodDistance and none past drawDistance. The geometry exists once however many plants there are, and draws that share a mesh and a material are one draw. A mesh
keeps every channel the artist made (a normal map, a second uv, a vertex colour); nothing is painted over it, so a plant is drawn with an ordinary material, not the wind’s.
The far version is the hierarchical level of detail: a tree’s crossed cards, baked to an atlas of their own, a set of placements like the plants themselves that is drawn
from drawDistance out to farDistance from the eye. It is cheap enough that the whole area stands at once. A kind with no far version is not drawn past drawDistance.
A kind with no cards of its own can use its cheaper mesh as its far version and set farKeep, the share of its plants the far version draws (above 0 to 1):
every n-th is kept, n the nearest whole number to 1 / farKeep, each grown by the square root of n (at most twofold) so the ground they cover is what it was.
Ground cover far off then costs a share of what it does near, and the ground’s own texture carries the rest.
Plants stand upright and are scaled uniformly; the records carry no tilt. The paths in kinds are the project’s, listed so a package carries them.
Touching plants
Section titled “Touching plants”A forest cannot be entities and does not need to be: only what stands near something can be touched. Put a FoliageContact on the character (or anything else plants
should feel). It does two things, one for each way plants are drawn.
| Field | What it does |
|---|---|
radius |
How far from the entity plants lean away, in world units. |
strength |
How hard: 1 is a full lean. |
recovery |
Seconds grass takes to stand up again behind it. |
Grass parts. A Foliage cell is one mesh, so its bend is worked out in the vertices: the Foliage shader takes four places (u_contact0 to u_contact3) and moves each blade
away from them by how far up the blade it is, less the further it stands above or below. The four are the contacts nearest the streaming source, then the places they recently left, which close over recovery,
so a character leaves a parting that closes behind it. u_contactBend is how far a blade parts as a share of the contact’s reach. A contact past the four does not part anything, which a player sees
as grass unmoved round a creature far off. Nothing needs to be turned on but the component.
Placed plants come near. A kind in PlacedFoliage or WorldRules.plants opts in with interaction. It stays a placement in its MeshInstances set, and within activationDistance of a contact each plant is also given an entity,
a PlantProxy, which goes when the contact does. The picture never changes hands, so nothing pops. The entity is what a ray or a collision finds, and a game’s own components can go on it.
interaction field |
What it does |
|---|---|
activationDistance |
How near a contact a plant becomes an entity, in world units. 0 keeps it data. |
radius, height |
The capsule that stands for it to physics, standing on its foot, in the mesh’s units (multiplied by its scale). Either 0 has none. |
bend |
How far it leans from a contact, in radians. It is a shear of the placement, with a spring, so any material bends with it and it rustles when let go. |
health |
What damage must take to fell it. 0 cannot be felled that way. |
fallSeconds, fallAngle, lieSeconds |
How long it takes to come down (default 1.8), how far (default 85 degrees), and how long it lies before it sinks away (default 30). |
felledKind, harvestedKind |
The kind it leaves when felled, a stump, or becomes with nothing left to take, a bare bush: an index into kinds, or a plant’s name in WorldRules.plants. Absent leaves nothing. |
yields |
How many times harvest can take from it. |
regrowSeconds |
Seconds after felling or the last harvest before it stands again, growing back from small. 0 is never. |
The plants are reached through a resource, from esengine/foliage:
import { FoliageContact, PlantInteractionRes } from 'esengine/foliage';
const plants = app.getResource(PlantInteractionRes);
// Whatever an axe swung here reaches, nearest first.const [tree] = plants.near(player.x, player.z, 2);if (tree) plants.damage(tree, 25, { x: player.x, z: player.z }); // comes down away from the blow at no health left
// What a ray found.const hit = physics.raycast(eye, forward);const plant = hit && plants.of(hit.entity);if (plant) inventory.berries += plants.harvest(plant, 1);
plants.on((event) => { if (event.type === 'landed') spawnDust(event.plant); });A felled plant comes down as an entity with its own meshes, a kinematic body turning about its foot, so it stops what it lies across while it lies there, and its placement is gone at once, from the far version too.
What became of a plant lasts. It is kept against the plant’s key, not its cell, so a felled tree is felled when its chunk is let go and grown again. plants.saveState() is all of it as plain data, the clock of what is regrowing included, and plants.loadState(state) puts it back, before or after the cells are grown.
A world’s plants are known by their places in its plan. A PlacedFoliage’s are known by their index in records under its stateKey (or its entity’s name), so a save outlives everything but reordering the records; give two fields different names.
In a package
Section titled “In a package”Foliage is an optional module, like terrain: a package carries it when a scene has a Foliage or PlacedFoliage component or a script imports esengine/foliage. It stands on a Terrain when the scene has one and needs no terrain module to stand on a flat floor.
Limits
Section titled “Limits”- Position only. The area is taken to be unrotated and unscaled; the terrain’s position and uniform scale are followed, a rotation is not.
- Only placed plants can be felled. Scattered
Foliageparts for a contact, but is one mesh a cell, so it has no plants to collide with, fell or harvest: a forest that must be cut is aPlacedFoliageorWorldRules.plants. - A plant falls by a rule, not by the solver. It turns about its foot to
fallAngleand does not settle on a slope or roll, so on steep ground a trunk can stand off the ground or into it. Its body is kinematic: it pushes what it lands on and nothing pushes it. - Collision takes the 3D physics module. A proxy’s capsule is added when the module is loaded, which a scene with a
CharacterController3Dor a body does by itself; without one, plants still lean and can be felled, and nothing collides. A capsule is the one shape: a wide crown or a fork is not followed. - A fallen plant is not in a save. A tree still coming down is saved as felled, and is gone on loading.
- The far version of a mesh is a card, not an impostor. It shows the rectangle of the plant’s texture you name, from every side the same; a tree whose texture has no such picture shows its texture there.
- Cells are grown within the frame’s budget, nearest first, so the distance fills in over a moment.