← Back to Blog optimization

Foliage optimization in Godot 4: MultiMesh, LODs, and billboards

By BitSoul Team5/29/2026Updated 8/2/20265 min read100 views
Foliage optimization in Godot 4: MultiMesh, LODs, and billboards

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.

Why foliage tanks performance in Godot 4 — illustrated

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)
```

LOD distance thresholds for foliage — illustrated

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

  1. Place your tree mesh on a transparent background
  2. Render a 512×512 or 1024×1024 RGBA image from the front (or use an octahedral impostor bake for multi-angle accuracy)
  3. Export as PNG, import into Godot with Detect 3D2D 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

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.*

Tags: optimization godot 3d-models low-poly game-assets workflow tutorials

Skip the modelling — download it instead

A free BitSoul account gets you 2 game-ready models every month plus 25 AI Engine credits to generate one of your own, no card required. Clean topology, PBR textures, and GLB downloads that drop straight into Unreal, Unity, Godot or Blender — plus OBJ and 3D-printable STL export.

Create a free account → Browse 846 models