Thin instances visibility and optimization

I am rendering hundreds of thousands of the thin instances. The documentation tells that all thin instances are rendered or None.

What kind of test is being made to determine that?
From looking at my trees it seems that bounding box for ALL of the instances is taken.

Conclusion from this observation:

Would it be a speed-up if I would split my original mesh into copies and use them as “root/template” for thin instances all around a map in different tiles?
in other words does it make sense to split my scene into 10x 10 tiles and make a clone of mesh to be instantiated and assign some thin instances to each clone, instead of sourcing from just one mesh?

Does it make sense?

After this is made, should I do something extra or rendering non-visible groups of thin instances would be prevented?

Thin instances are such by their nature. The documentation is correct. A mesh with N thin instances is treated as a single, gigantic object by the GPU. When the renderer checks if this mesh is on screen (frustum culling), it looks at the collective bounding box that encompasses every single thin instance.

So, if you have instances spread across a massive map, this collective bounding box is also massive. It will almost always be at least partially in the camera’s view. Therefore, the entire set of hundreds of thousands of instances is sent to the GPU for rendering every single frame, even if you can only see a few hundred of them. This is the “all” part of “all or nothing,” and it may be a huge performance killer.

When you split your original mesh, for example, into 10 clones (one for each tile), you now have 10 separate meshes, each with their own set of thin instances and, crucially, their own bounding box.

Now, the rendering engine performs frustum culling on each tile mesh individually. If your camera is only looking at, say, 2 tiles, then only the thin instances belonging to those 2 tile meshes are sent to the GPU. The instances belonging to the other 8 tiles are culled and never processed.

Babylon.js will handle this for you automatically. You do not need to do anything extra. The engine’s internal frustum culling system will correctly check each individual tile mesh’s bounding box and skip rendering it (and all its thin instances) if it’s not in the view frustum.

You can verify this is working by using the Inspector and turning on bounding boxes.

But when tiling you need to set the mesh’s bounding box to tightly fit ONLY its instances.
Here is a conceptual code example with some comments:

// Original mesh (your template)
let originalMesh = /* ... your mesh creation code ... */;
originalMesh.setEnabled(false); // Hide the original

// Create your tile meshes using CLONE, not createInstance
let tileMeshes = [];
let instancesPerTile = 10000; // Example
let tileSize = 100; // Example tile size

for (let tileX = 0; tileX < 10; tileX++) {
    for (let tileZ = 0; tileZ < 10; tileZ++) {
        // Use clone() instead of createInstance()
        let tileMesh = originalMesh.clone("tile_" + tileX + "_" + tileZ);
        
        // Reset position since clone copies transform - we'll use thin instance matrices instead
        tileMesh.position.set(0, 0, 0);
        
        // Create thin instances for this specific tile
        let matricesData = new Float32Array(instancesPerTile * 16);
        for (let i = 0; i < instancesPerTile; i++) {
            let matrix = BABYLON.Matrix.Translation(
                tileX * tileSize + Math.random() * tileSize, // X within tile
                0,
                tileZ * tileSize + Math.random() * tileSize  // Z within tile
            );
            matrix.copyToArray(matricesData, i * 16);
        }
        
        tileMesh.thinInstanceSetBuffer("matrix", matricesData, 16);
        
        // Set the mesh's bounding box to tightly fit ONLY its instances
        let tileMin = new BABYLON.Vector3(tileX * tileSize, 0, tileZ * tileSize);
        let tileMax = new BABYLON.Vector3((tileX + 1) * tileSize, 10, (tileZ + 1) * tileSize);
        tileMesh.setBoundingBox(new BABYLON.BoundingInfo(tileMin, tileMax));

        tileMeshes.push(tileMesh);
    }
}

Hope it may help :slight_smile:

2 Likes

one thing that your solution may be lacking is moving the tileMesh to the right tile, right now they are all on 0,0,0 (so itle tile_0_0)
Idk if its necessary.

Thanks for an elaborate answer, extra clear!

1 Like

Bragging time, each tree cluster up to 19 trees, 8000+ clusters still extra smooth:

4 Likes

The terrain looks great! Was it modeled by hand or generated procedurally?

Kinda of a mixture, handmade topology map (heightmap).

That depends on your scene and your target hardware.
Subdividing in tiles will lower the number of rendered triangles (GPU), but raise the number of draw calls (GPU & CPU) and raise the time spent in mesh selection (CPU).

That’s usually a good thing but a bit of testing is needed to find the optimal tile size. It’s even better if you can easily hide/show (or remove/add) tiles, for instance when moving the camera.

1 Like