Rendering 10,000 Trees Without Melting the CPU: MultiMeshInstance3D in Godot 4
Scattering thousands of identical props as individual nodes kills your frame rate. Godot 4's MultiMeshInstance3D batches them into one draw call — here's how it works, with a working grass-field example and the culling trade-offs to plan around.
22 Mar 2026, 05:02 UTC

You've scattered a few hundred trees across your terrain as individual MeshInstance3D nodes, and the editor is already unhappy. The scene tree groans, the frame rate dips, and every node carries its own overhead: transform updates, culling checks, and a separate draw call. This is one of the most common 3D performance walls in Godot, and the engine ships a purpose-built answer: MultiMeshInstance3D.
The thesis is simple: when many objects share one mesh and one material, stop paying per-node costs and let the GPU draw them all in a single instanced call.
What MultiMesh actually does
A MultiMeshInstance3D node holds a MultiMesh resource. That resource stores one mesh reference plus a buffer of transforms — one Transform3D per instance. At render time, Godot hands the whole buffer to the GPU, which draws the mesh once per instance in a single draw call (a technique called hardware instancing). Instead of 10,000 nodes each submitting geometry, you have one node and one submission.
The CPU savings are real but come from two places: far fewer nodes to process each frame, and far fewer draw calls hitting the graphics API. The GPU still rasterizes every triangle of every instance, so instancing does not make dense geometry free — it removes the per-object overhead, not the per-pixel cost.
A worked example: a procedural grass patch
This runs in Godot 4.x (the API differs from Godot 3, where MultiMeshInstance had different property names). Attach this script to a MultiMeshInstance3D node in a 3D scene. No special permissions or project settings are needed.
extends MultiMeshInstance3D
const INSTANCE_COUNT := 10000
const AREA_SIZE := 100.0
func _ready() -> void:
var mm := MultiMesh.new()
mm.transform_format = MultiMesh.TRANSFORM_3D
mm.mesh = preload("res://grass_blade.tres") # your mesh resource
mm.instance_count = INSTANCE_COUNT
var rng := RandomNumberGenerator.new()
rng.randomize()
for i in INSTANCE_COUNT:
var pos := Vector3(
rng.randf_range(-AREA_SIZE, AREA_SIZE),
0.0,
rng.randf_range(-AREA_SIZE, AREA_SIZE)
)
var basis := Basis(Vector3.UP, rng.randf_range(0.0, TAU))
var scale := rng.randf_range(0.8, 1.4)
basis = basis.scaled(Vector3.ONE * scale)
mm.set_instance_transform(i, Transform3D(basis, pos))
multimesh = mm
Run the scene and you should see the field appear. To confirm the batching worked, open the editor's Debugger > Monitors and check Render > Draw Calls — the grass contributes one draw call instead of thousands. Also compare the scene tree: one node, not 10,000.
Adding variety without extra materials
Every instance shares the same mesh and material, which is the core constraint. But you can still get visual variety. Set mm.use_colors = true before setting the instance count, then call mm.set_instance_color(i, some_color) per instance. In your material, enable Vertex Color > Use as Albedo (or read COLOR in a custom shader) to tint each blade differently — a cheap way to break up repetition. For anything fancier (per-instance wind phase, damage state), you write a custom shader that reads instance data, because arbitrary per-instance shader parameters are not automatic.
The trade-offs you sign up for
- Culling is per MultiMesh, not per instance. The engine frustum-culls the whole batch using its combined bounds. If one blade is visible, all 10,000 are submitted. For huge worlds, split instances into several MultiMeshes by region so off-screen chunks cull away.
- No per-instance logic. Instances are not nodes. No scripts, no signals, no collision unless you add separate physics bodies. If a tree needs to be chopped down, you either rewrite its transform (e.g., scale to zero) or manage gameplay data separately and keep the MultiMesh purely visual.
- One mesh, one material. Mixed meshes need multiple MultiMeshInstance3D nodes — still cheap, just not one.
How to verify it's worth it
Don't trust blanket performance claims — gains depend on your hardware, mesh complexity, and driver. Measure your own scene: build the same visual set twice, once as individual MeshInstance3D nodes and once as a single MultiMeshInstance3D, then compare Render > Draw Calls, Render > Objects in Frame, and frame time in the debugger monitors. Also try updating a few transforms per frame via set_instance_transform() and confirm the batch updates without touching the scene tree.
The actionable rule: if a scene element is decorative, repeated, and shares a mesh, default to MultiMesh and reach for individual nodes only when an object genuinely needs its own behavior.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.