For instanced geometries three.js can not do any frustum culling as there is no information about where the objects will end up. This will be computed in the vertex-shader and three.js doesn't have any knowledge about what happens there.
The earliest possible time for any culling to happen is after running the vertex-shader for every vertex of every instance. This already explains the drop in framerate you're seeing. Although the same number of objects/triangles are visible, the number of vertex-shader invocations goes up to 166%.
If you want to implement culling yourself, you could try to rearrange the instances in the attribute-buffers for every frame (skip invisible instances) and adjust the max-instance count to the number of visible instances. That might be a bit counter-intuitive, but recomputing all instance-attribute buffers on every frame can actually give a better performance here.
To do the visibility-test, the simplest way would probably be to use the THREE.Frustum()
class and frustum.containsPoint()
. That would look something like this
const frustum = new Frustum();
const projScreenMatrix = new Matrix();
// assume instances is an array of objects containing all the
// relevant information for all instances
const instances = [
// {position: ...}, {position: ...}, ...
];
// for every frame
projScreenMatrix.multiplyMatrices(
camera.projectionMatrix,
camera.matrixWorldInverse
);
frustum.setFromMatrix(projScreenMatrix);
let visibleInstanceCount = 0;
for (let i = 0; i < instanceCount; i++) {
const pos = instances[i].position;
if (!frustum.containsPoint(pos)) {
continue;
}
// add instance to instance-attribute buffers
pos.toArray(instancePositionBuffer, visibleInstanceCount * 3);
visibleInstanceCount++;
}
geometry.maxInstanceCount = visibleInstanceCount;