Quick answer: Godot treats a MultiMeshInstance3D as one renderer with a single AABB. Whether the camera sees one instance or none, the whole buffer is submitted. To get real culling, partition your instances into multiple MultiMesh nodes with tight AABBs and use visibility range to hide distant ones.
MultiMesh is Godot’s fast path for rendering thousands of identical meshes in one draw call. The tradeoff is that it is one draw call — one renderer, one AABB, one visibility test. If you put ten thousand trees into a single MultiMesh and the camera can see even one of them, the GPU renders the full ten thousand. Performance does not scale with screen coverage, and that surprises most people the first time they profile a forest.
Confirm what Godot sees as the bounding box
Open the scene, select the MultiMeshInstance3D, and look at the Custom AABB property in the Inspector. If it is the default zero-size box, Godot is deriving the AABB from the instance transforms at load time. For a buffer you populate from code, that derivation may run before the transforms are written, leaving you with a collapsed AABB centered on the origin. Symptom: the node is culled whenever the camera looks away from the origin, even though instances are spread across the level.
Write a correct AABB explicitly after you fill the transforms:
func populate() -> void:
var mm := multimesh
mm.instance_count = instance_count
var aabb := AABB(Vector3.ZERO, Vector3.ZERO)
for i in instance_count:
var t := Transform3D(Basis.IDENTITY, positions[i])
mm.set_instance_transform(i, t)
aabb = aabb.expand(positions[i])
custom_aabb = aabb.grow(mesh_radius)
The grow accounts for the radius of each mesh so the edge of the frustum does not clip instances whose center is just outside the box.
Partition into cells
Once the AABB is correct the node is cullable, but it is still all-or-nothing. A forest of 10,000 trees in one MultiMeshInstance3D renders all 10,000 if even one is visible. For real savings, split the world into a grid:
const CELL_SIZE := 32.0
func build_cells(trees: Array) -> void:
var buckets := {}
for t in trees:
var key := Vector2i(floor(t.x / CELL_SIZE), floor(t.z / CELL_SIZE))
if not buckets.has(key): buckets[key] = []
buckets[key].append(t)
for key in buckets:
_spawn_multimesh(buckets[key])
A 32m cell size is a reasonable starting point for dense vegetation. Each cell becomes a separate MultiMeshInstance3D with its own AABB, and Godot culls each one independently against the frustum. Frame time scales with the number of visible cells instead of the total instance count.
Add visibility range for distant chunks
Frustum culling only hides what is off-screen. You still pay for cells that are on-screen but beyond a reasonable draw distance. Use the GeometryInstance3D visibility range fields on each MultiMeshInstance3D:
visibility_range_end = 120.0
visibility_range_end_margin = 20.0
visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF
Godot will smoothly fade cells out as the camera backs away. Combined with a lower-LOD billboard MultiMesh for long range, you keep visual density without paying for full-resolution meshes across the entire world.
Per-instance culling is not built in
A frequent ask is to have Godot skip individual instances inside a MultiMesh based on the frustum. The engine does not do this in the standard pipeline because the whole point of MultiMesh is to avoid per-instance work on the CPU. If you need per-instance culling — for example, to disable grass instances behind hills — you have to implement it manually with a compute shader that writes indirect draw arguments, which is an expert-level path.
Verify in the profiler
Open the Monitor tab and watch Draw Calls (3D) and Vertices Drawn. Before partitioning you will see vertices equal to instances times mesh verts, regardless of camera angle. After partitioning, turning the camera should drop the vertex count immediately as cells leave the frustum. If it does not, your AABBs are still wrong.
“A MultiMesh is one cullable object to Godot. Want finer culling? Give Godot more objects by splitting the buffer.”
Related Issues
For related rendering performance work, see Fix Godot 3D spatial audio not attenuating, and for viewport-related rendering gotchas, Fix Godot viewport stretch mode black bars unexpected.
Tip: visualize the AABB in the editor — if it looks like a thin slice instead of the full distribution, it is stale.