Level Foliage System
Optimizing large-scale vegetation rendering, grass displacement physics, and wind-animated plant life in Unturned levels begins with understanding how the GPU-instanced foliage system stores per-tile instance data, bakes procedurally, and integrates with landscape tile coordinates. The foliage system (SDG.Framework.Foliage) operates at the landscape level, independent of the Level subclasses. It stores per-tile foliage instance data, supports GPU-instanced rendering, wind interaction, LOD groups, and collision detection. LevelGround.handlePreBakeTile provides the bridge between LevelGround's resource trees and the foliage bake pipeline — ensuring generated trees do not duplicate with baked foliage.
Source location: SDG.Framework.Foliage namespace, Unturned/Level/LevelGround.cs (handlePreBakeTile method)
Architecture Overview
The foliage system is part of the SDG.Framework.Foliage namespace, separate from the Unturned.Level subclasses. It operates on landscape tiles, each with an associated FoliageTile that stores per-instance foliage data. The system bakes foliage at edit time and renders it at runtime using GPU instancing for efficient rendering of thousands of individual grass/plant instances.
Key architectural components:
FoliageTile: Per-tile data container for foliage instances.FoliageBakeSettings: Configuration struct controlling the bake process.FoliageSystem: Central manager that stores tile data inEnvironment/Foliage/directory.- GPU instancing: Efficient batch rendering for grass and plant instances.
- Wind integration:
WindZonecomponent plusLevelLighting.windfor animated sway.
FoliageTile
Each landscape tile has an associated FoliageTile that stores per-instance foliage data:
- Instance position, rotation, and scale: Per-instance transform data.
- Foliage asset reference: Material, mesh, density parameters.
- Generated vs. hand-placed flag: Whether the instance was baked procedurally or placed manually.
Tiles are organized in the same grid as landscape tiles, ensuring 1:1 correspondence between terrain patches and their foliage.
FoliageBakeSettings
csharp
public struct FoliageBakeSettings
{
public bool bakeResources;
public bool bakeObjects;
// ...
}| Field | Purpose |
|---|---|
bakeResources | Whether to remove resource trees within the tile's bounds before baking |
bakeObjects | Whether to bake object-aligned foliage |
When a foliage bake is triggered, the bakeResources flag controls whether resource trees within the tile's bounds are removed before baking. This prevents doubling up on trees — if a resource tree was procedurally placed by LevelGround, the bake will remove it before generating foliage.
LevelGround.handlePreBakeTile
csharp
protected static void handlePreBakeTile(FoliageBakeSettings bakeSettings, FoliageTile foliageTile)Called before foliage tile baking. This bridge method removes procedurally generated trees that overlap with the foliage tile's bounds:
- Receives the
FoliageBakeSettingsand the targetFoliageTile. - If
bakeResourcesis true, finds allResourceSpawnpointinstances within the tile's world bounds. - Calls
destroy()on each tree's model, stump, and skybox transforms. - Removes each tree from the
_regionTreesdictionary. - Releases empty region lists.
This ensures procedurally placed trees (from LevelGround.addSpawn) are replaced by the foliage bake rather than creating visual duplicates.
Foliage Rendering
GPU Instancing
Foliage uses GPU instancing for efficient rendering of thousands of individual grass/plant instances. The system supports:
- Wind interaction: Via
WindZoneandLevelLighting.wind— foliage sways in response to wind strength and direction. - LOD groups: Three levels (near, medium, far) control rendering detail at different distances.
- Collision detection: Gameplay interaction — player pushes through grass, grass reacts to player movement.
Wind Animation
The wind system drives foliage animation:
- Tree and foliage animation (swaying) at varying amplitudes.
- Particle system velocity modifications.
- Cloud movement speed integration.
- Audio ambiance (wind sound volume).
The WindZone component applies wind to Unity's built-in wind-affected shaders and the landscape foliage system.
Collision Detection
Foliage supports collision detection for gameplay interaction — the player character can push through grass, with the grass visually responding to the collision. This requires foliage instances to have collision detection enabled.
Foliage Data Storage
Level foliage data is saved and loaded through the FoliageSystem which stores FoliageTile data in the Environment/Foliage/ directory. Each tile's data includes:
- Foliage instance transforms.
- Asset references (material, mesh, density).
- Instance flags (generated vs. hand-placed).
The data is serialized in a binary format compatible with the landscape tile coordinate system.
Grass Displacement
csharp
// GrassDisplacement.csA component in Unturned/Level/GrassDisplacement.cs handles grass and detail mesh displacement when the player walks over foliage. This provides the visual feedback of grass flattening under the player's feet.
Foliage and Terrain Details
The Terrain/Details.unity3d and Terrain/Details.dat files store detail object configuration:
- Grass and detail mesh prefabs.
- Detail density per region.
- Detail render distance.
These are separate from the FoliageTile data — they represent the old Unity Terrain detail system, which the foliage system supersedes.
Foliage Baking Workflow
- Editor triggers bake: Map author initiates foliage generation.
- Tile selection: The foliage system determines which tiles to bake based on changed regions.
- Pre-bake callback:
LevelGround.handlePreBakeTileremoves conflicting resource trees. - Procedural placement: Foliage instances are generated based on terrain material, slope, and biome rules.
- Data serialization: Each tile's foliage data is written to
Environment/Foliage/. - Runtime loading: During level load, foliage tiles are loaded and instantiated with GPU instancing.
Runtime Performance
The foliage system's GPU instancing approach means:
- Thousands of grass instances render in a single draw call.
- Wind animation is computed on the GPU (via vertex shader offset).
- LOD groups reduce triangle count at distance.
- Dedicated servers skip foliage entirely (no visual rendering).
Worked Code Example: Foliage Manipulation
Region-Based Foliage Clearance
csharp
using SDG.Framework.Foliage;
using UnityEngine;
public class FoliageClearer
{
/// <summary>
/// Clears all foliage within a specified radius from a world position,
/// updating both the FoliageTile data and removing GameObjects.
/// Useful for construction sites or deforestation mechanics.
/// </summary>
public static int ClearFoliageInRadius(Vector3 center, float radius)
{
int clearedCount = 0;
float sqrRadius = radius * radius;
foreach (FoliageTile tile in FoliageSystem.tiles)
{
if (tile == null || tile.instances == null)
continue;
FoliageInstance[] instances = tile.instances;
for (int i = instances.Length - 1; i >= 0; i--)
{
FoliageInstance instance = instances[i];
Vector3 worldPos = tile.localToWorldMatrix.MultiplyPoint3x4(
instance.position
);
if ((worldPos - center).sqrMagnitude <= sqrRadius)
{
// Remove instance at this index
tile.RemoveInstanceAt(i);
clearedCount++;
}
}
}
return clearedCount;
}
}Custom Wind Strength Controller
csharp
using SDG.Unturned;
using UnityEngine;
public class WindController : MonoBehaviour
{
/// <summary>
/// Overrides the global wind strength for foliage animation,
/// allowing per-region wind zones (storm cells, indoor areas).
/// </summary>
public static void SetWindStrength(float strength)
{
// LevelLighting.wind drives the WindZone component
LevelLighting.wind = Mathf.Clamp01(strength);
// This propagates to all active WindZone components in the scene
WindZone[] windZones = GameObject.FindObjectsOfType<WindZone>();
foreach (WindZone zone in windZones)
{
zone.windMain = strength * 2f; // Amplitude multiplier
zone.windTurbulence = strength * 0.5f;
}
}
/// <summary>
/// Returns the current wind strength, factoring in time-of-day
/// modulation from the DayNight cycle.
/// </summary>
public static float GetEffectiveWind()
{
float baseWind = LevelLighting.wind;
float timeOfDayFactor = Mathf.Sin(
LevelLighting.time * Mathf.PI * 2f / LevelLighting.CYCLE
);
return baseWind * (0.8f + 0.2f * timeOfDayFactor);
}
}Mermaid Diagram: Foliage Baking Pipeline
Comparison: Foliage System vs. Related Rendering Systems
| Feature | FoliageTiles (GPU Instanced) | Terrain Detail Objects | Resource Trees | Object Props |
|---|---|---|---|---|
| Rendering | GPU instancing, single draw call per tile | Unity terrain detail renderer | Individual MeshRenderers | Individual MeshRenderers |
| Wind animation | Vertex shader wind offset | Limited detail wind | Game object-based | None |
| LOD support | 3-level LOD groups | Configurable density at distance | Per-asset LOD | Per-asset LOD |
| Collision detection | Enabled (grass push) | No | Full collider | Full collider |
| Baking workflow | Editor procedural bake | Terrain painter | Spawn system | Hand-placement |
| Storage | Binary files per tile in Environment/Foliage/ | Terrain data embedded in level | Part of LevelGround tree dict | Object scene files |
| Server rendering | Disabled on dedicated | Disabled | N/A for servers | Partially (colliders) |
| Interaction | GrassDisplacement component | None | Harvestable | Pickup/use |
| Draw call cost | 1 per tile (~1000 instances) | 1 per detail layer | 1 per tree | 1 per object |
Failure Modes and Common Mistakes
Foliage tile corruption on partial bake — If the editor crashes or is force-closed during tile baking, the serialized tile file in
Environment/Foliage/may be truncated. On next level load,FoliageSystemattempts to deserialize a partial file, resulting in visible gaps where foliage instances were lost.Grass displacement not resetting —
GrassDisplacementpushes grass vertices away from the player during movement. If a player logs out or teleports while standing in grass, the displaced vertices do not reset. The grass remains permanently flattened at that position until the region unloads and reloads.Wind strength desync across clients —
LevelLighting.windis synchronized by the server, but theWindZonecomponent values are client-side. A custom client with modified wind settings will see different grass animation amplitude than other players. This is cosmetic but can reveal hidden players through grass (lower wind = less sway = easier to spot a stationary player).Memory leak from tile caching — If
FoliageSystemcaches tiles without evicting distant ones, a player who flies across the map at high speed can accumulate hundreds of loaded tiles in memory. Each tile stores instance data (positions, rotations, scales, asset references) — roughly 64 bytes per instance × thousands of instances per tile × hundreds of tiles = several hundred MB of RAM.Resource tree duplication after re-bake — If
bakeResourcesis set to false during a re-bake,handlePreBakeTiledoes not remove existing resource trees. The bake generates new foliage instances in the same positions as the resource trees, creating visual doubling (two trees at the same spot). This is a common editor workflow error.
How This Field Behaves Differently from the SDG Docs
SDG docs describe foliage as "Unity terrain detail." The community resources sometimes conflate the foliage system with Unity's built-in terrain detail system. In the SDK,
FoliageTileinstances are custom GPU-instanced meshes, not UnityDetailPrototypeobjects. They bypass the Unity terrain detail renderer entirely.SDG docs claim foliage is "static after bake." The documentation suggests foliage is immutable after the bake step. In the SDK,
FoliageSystem.tilesand theFoliageInstancearrays are writable at runtime. Plugins can add, remove, and reposition foliage instances programmatically — though this is uncommon and requires a full GPU buffer rebuild.SDG docs mention "grass settings in terrain config." Some documentation points to
Terrain/Details.datfor grass configuration. In the SDK, this file is a legacy DetailObject configuration separate from theFoliageTilesystem. Modern foliage usesFoliageBakeSettingsand asset-driven parameters, not terrain detail configuration.
Performance Considerations
GPU Instancing Benchmarks
- 10,000 grass instances: 10 draw calls (1 per tile per material) — ~0.1ms GPU time.
- 100,000 instances: ~100 draw calls — ~1ms GPU time.
- 1,000,000 instances: ~1000 draw calls — ~10ms GPU time (may exceed 60 FPS budget).
Optimization Strategies
- Aggressive LOD culling: Reduce LOD group distances in the foliage asset. Foliage beyond 150 meters rarely needs highest-LOD rendering.
- Wind animation culling: Disable wind calculations for foliage beyond a certain distance (wind is imperceptible at 100+ meters).
- Tile streaming: Do not load all tiles at once. Stream foliage tiles based on the player's position (load within 3 regions, unload beyond 5).
- Collision culling: Disable
GrassDisplacementon dedicated servers and for players with graphics settings below Medium.
Memory Budget
| Instance Count | Memory (64 bytes/instance) | Per-Tile Overhead | Total (100 tiles) |
|---|---|---|---|
| 500K | 32 MB | 250 KB | ~57 MB |
| 1M | 64 MB | 250 KB | ~89 MB |
| 5M | 320 MB | 1 MB | ~420 MB |
Deeper FAQ
Q: Can I selectively bake foliage for only one biome?
Yes. The FoliageBakeSettings structure controls what gets baked. You can specify a material or biome filter during the bake process. The foliage system checks terrain materials (grass, gravel, dirt, sand) and applies density rules per material type. Baking "grass only" on sand tiles produces zero instances.
Q: How does the system handle foliage at the border of two tiles?
Foliage instances belong to exactly one FoliageTile. An instance whose world position falls at the boundary of two tiles is assigned to the tile containing its centroid. There is no explicit boundary blending — the tile grid forms a strict partition of the terrain.
Q: Does the foliage system work on custom maps?
Yes. The FoliageSystem is part of SDG.Framework and is level-agnostic. Custom maps with proper landscape tile setup (matching the coordinate grid expected by the foliage system) can use the bake pipeline. The tiles must align with the landscape coordinate system used by LevelGround.
Q: Can I add collision to foliage for projectile blocking?
Foliage collision is gameplay-only (grass push) and does not participate in bullet/projectile collision. To block projectiles, use ResourceSpawnpoint trees (which have MeshColliders) or object props with proper collision layers.
Q: Why does foliage disappear when I open the inventory?
On some graphics settings, foliage rendering is culled when the player's view is in a UI overlay (inventory, notes, signs). This is a deliberate performance optimization to avoid rendering foliage behind the inventory screen. The behavior depends on the RenderTexture path used for the inventory blur effect.
Cross-References
- Level Objects Loading — How object assets interact with foliage-occupied tiles.
- Level Terrain Generation — Terrain height and material data that drive foliage density rules.
- ItemFarmAsset — Farmable Crop Definitions — Crops are a separate growth system from foliage; they do not use FoliageTile.
- Architecture LOD System / Culling — LOD group design used by both foliage and structural models.
