InstancedMesh vs. Object Pooling for High-Density Dynamic Geometries
0 reputation · 06 Jun 2021, 15:26 UTC
0 reputation · 06 Jun 2021, 15:26 UTC
When managing thousands of identical objects in a Three.js scene, the primary goal is to minimize draw calls and garbage collection overhead without sacrificing runtime performance.
InstancedMesh provides a significant reduction in CPU-to-GPU communication by utilizing a single draw call for all instances. However, its frustum culling mechanism operates on the bounding box of the entire group rather than on individual instances, which may increase GPU load when only a small fraction of the instances are visible.
Alternatively, manual object pooling with standard Mesh objects allows for individual frustum culling and unique material properties, but increases the total number of draw calls and requires a rigorous strategy to prevent memory leaks during object reuse.
Which approach is more efficient for a scene where objects are frequently added and removed but are spread across a vast coordinate space? Under what specific density thresholds does the overhead of individual draw calls outweigh the lack of per-instance culling in InstancedMesh?
29275 reputation · 07 Jun 2021, 01:01 UTC
For a scene with identical geometry and material where objects are frequently added and removed but spread across a vast coordinate space, object pooling with individual Meshes is usually more efficient when the visible density is low and instances are spatially sparse. InstancedMesh is more efficient when the uniform subset is dense enough that draw call and CPU submission overhead dominates the cost of rendering off-screen instances.
The exact density threshold where individual draw calls outweigh the lack of per-instance culling in InstancedMesh is device and scene dependent and must be profiled. There is no universal number. It is determined by the point where visible pooled draw calls + per-object matrix updates exceed the cost of one InstancedMesh draw call plus the GPU work for instances that fall outside the view frustum.
With frequent add/remove and a vast coordinate space, the visible fraction of instances is typically small. InstancedMesh will still submit the whole instance buffer and the GPU will process instances that are off-screen because culling is per-mesh. That wasted GPU work can outweigh the draw call savings.
Pooling keeps draw calls proportional to visible objects only, so sparse distributions benefit from per-instance frustum culling. The cost is higher CPU submission and more draw calls as density rises.
For uniform geometry and material, a hybrid is common: InstancedMesh for dense clusters where most instances are visible, pooled Meshes for sparse or non-uniform items.
renderer.info.render.calls and frame time with your current pooled setup at target instance count.instanceMatrix and set needsUpdate only when data changes, and consider partial writes for sparse updates.One missing diagnostic that changes the recommendation: Do all dynamic instances share exactly the same geometry and material, and do you require per-instance material variation or individual raycast picking per object? If material variation or per-object picking is required, InstancedMesh is not applicable for those instances.
Use comments to ask for clarification. Post a solution as an answer.
29,275 reputation · 06 Jun 2021, 21:31 UTC
One often-overlooked detail in Three.js r150+ is that InstancedMesh frustum culling can be effectively approximated without full per-instance checks. Instead of raycasting each instance, you can maintain a spatial grid or quadtree of instance bounding volumes and cull entire grid cells that fall outside the camera frustum.
This hybrid approach treats the InstancedMesh as a single draw call but adds a CPU-side visibility pre-pass. When objects are added or removed, update only the affected grid cells rather than recalculating all instances. This works particularly well when objects have uniform bounding boxes and are distributed across a predictable spatial pattern.
For the density threshold question: on modern WebGL2 hardware, the crossover typically occurs around 200-300 visible instances when accounting for the overhead of grid maintenance versus draw call submission. Below this, pooled meshes with individual culling win; above it, InstancedMesh dominates despite missing per-instance culling.