Foliage is one of the fastest ways to destroy a game's frame rate — and one of the most fixable. Dense forests, grass fields, and scattered shrubs share the same root problem: thousands of individual draw calls that the GPU has to process one by one. Godot 4 gives you three complementary tools to collapse that cost: MultiMeshInstance3D for instanced rendering, LOD (Level of Detail) mesh swapping, and billboard impostors for the far distance. Used together, they can take a scene from 8 FPS to 60+ with no visible quality loss.
Why foliage tanks performance in Godot 4
Each `MeshInstance3D` node you place in the scene tree generates at least one draw call. A forest with 5,000 trees = 5,000 draw calls per frame — before shadows, before transparency, before anything else. The GPU can handle enormous polygon counts but chokes on *batching overhead*: the CPU overhead of submitting thousands of separate draw calls eats frame budget before the GPU even starts rendering.
The Godot 4 rendering server processes draw calls in a multi-threaded pipeline, but the bottleneck moves to the CPU-GPU command buffer. The fix is always the same: reduce the number of draw calls, not just the polygon count.
![]()
MultiMesh instancing: one draw call for thousands of trees
`MultiMeshInstance3D` renders N copies of a single mesh in a single draw call. Each instance can have an independent transform (position, rotation, scale) and custom per-instance data (color, wind phase offset).
Setting up MultiMeshInstance3D in GDScript
```gdscript
var mm = MultiMesh.new()
mm.transform_format = MultiMesh.TRANSFORM_3D
mm.instance_count = 2000
for i in 2000:
var t = Transform3D()
t.origin = Vector3(
randf_range(-100, 100),
0.0,
randf_range(-100, 100)
)
# Random Y rotation
t = t.rotated(Vector3.UP, randf() * TAU)
# Random non-uniform scale for natural variation
var s = randf_range(0.8, 1.4)
t = t.scaled(Vector3(s, s * randf_range(0.9, 1.1), s))
mm.set_instance_transform(i, t)
var mmi = MultiMeshInstance3D.new()
mmi.multimesh = mm
mmi.multimesh.mesh = preload("res://assets/tree_low.glb").meshes[0]
add_child(mmi)
```
This scatters 2,000 trees in one draw call. For comparison, 2,000 individual `MeshInstance3D` nodes = 2,000 draw calls.
Per-instance color for variation
Enable `use_colors = true` on the MultiMesh and set per-instance color to vary tint slightly — breaks the clone-army look without extra geometry:
```gdscript
mm.use_colors = true
# In the loop:
var hue_shift = randf_range(-0.05, 0.05)
mm.set_instance_color(i, Color(0.3 + hue_shift, 0.55 + hue_shift * 0.5, 0.2, 1.0))
```
Your tree shader must sample `INSTANCE_CUSTOM` to use this — enable Use As Albedo in the MultiMesh resource.
LOD mesh swapping with GeometryInstance3D
Godot 4's `GeometryInstance3D` (parent of `MeshInstance3D` and `MultiMeshInstance3D`) exposes `lod_bias` and per-mesh LOD distances. For MultiMesh, you swap the mesh reference itself at runtime based on camera distance.
LOD distance thresholds for foliage
| Distance from camera | Mesh | Typical tri count |
|---|---|---|
| 0–20 m | High detail (LOD0) | 800–1200 tris |
| 20–60 m | Mid detail (LOD1) | 200–400 tris |
| 60–120 m | Low detail (LOD2) | 40–80 tris |
| 120+ m | Billboard impostor | 2 tris (quad) |
For a MultiMesh forest, maintain 3–4 separate `MultiMeshInstance3D` nodes, one per LOD band, each populated with the instances falling within that distance range. Update the bands on a timer (every 0.5–1 s) rather than every frame — camera movement is slow relative to LOD transitions.
```gdscript
func _update_lod_bands(cam_pos: Vector3) -> void:
for i in instance_positions.size():
var dist = cam_pos.distance_to(instance_positions[i])
var band = 0
if dist > 120: band = 3
elif dist > 60: band = 2
elif dist > 20: band = 1
_assign_to_band(i, band)
```
![]()
Billboard impostors for the far distance
Beyond 100–120 m, individual tree silhouettes are indistinguishable. A billboard — a camera-facing quad with a pre-rendered tree texture — is visually identical at that range and costs 2 triangles instead of 800.
Creating the impostor texture in Blender
- Place your tree mesh on a transparent background
- Render a 512×512 or 1024×1024 RGBA image from the front (or use an octahedral impostor bake for multi-angle accuracy)
- Export as PNG, import into Godot with Detect 3D → 2D Mesh and compression set to VRAM Lossless to preserve alpha
Billboard shader in Godot 4
In your `StandardMaterial3D` on the billboard quad:
- Billboard Mode: `Enabled`
- Shading Mode: `Unshaded` (avoids fake lighting)
- Transparency: `Alpha Scissor` with threshold `0.5` (no alpha blending = no transparency sort)
- Cull Mode: `Disabled` (billboards seen from all angles)
For wind simulation, add a simple vertex shader offset — sample a noise texture using world XZ position + `TIME` to animate foliage sway without bones:
```glsl
void vertex() {
float wind = texture(wind_noise, VERTEX.xz * 0.05 + TIME * 0.3).r;
VERTEX.x += wind * 0.08 * VERTEX.y;
}
```
Apply this to both your MultiMesh LOD0/1 meshes and the billboard. The amplitude (`0.08`) should be tiny — more than 0.1 looks like a fan, not wind.
Performance checklist
- [ ] All foliage on `MultiMeshInstance3D`, zero individual `MeshInstance3D` for repeated assets
- [ ] Minimum 3 LOD bands per tree species
- [ ] Billboard impostors active beyond 100 m
- [ ] LOD band updates on timer, not `_process`
- [ ] Shadow casting disabled on LOD2 and billboard bands (`cast_shadow = SHADOW_CASTING_SETTING_OFF`)
- [ ] Foliage meshes use Alpha Scissor, not Alpha Blend (eliminates transparency sort)
- [ ] Per-instance color variation enabled to break visual repetition
- [ ] Wind vertex offset uses world-space noise, not object-space (prevents pop on LOD swap)
Disabling shadow casting on distant LODs alone can cut shadow map render time by 40–60% in forested scenes — it's the single highest-impact toggle on this list.
Grab optimized foliage assets from BitSoul
Every tree, shrub, and grass clump in the BitSoul marketplace ships as a GLB with pre-built LOD meshes and correct pivot placement for MultiMesh scattering. Drop them in, set the distances in the table above, and your forest runs.
Browse the full foliage collection at bitsoulhosting.com/marketplace.
---
*This post is part of the Ultimate Guide to Free 3D Game Assets — BitSoul's complete reference for formats, texturing, rigging, optimization, and engine integration.*