Skip to content

Frustum Culling

With frustum culling, instances outside the camera’s frustum are not rendered.
This saves GPU resources by using the CPU to calculate the indices to render.

iMesh.perObjectFrustumCulled = true; // default is true

How It Works

Frustum culling is performed every frame in two ways:

  • Linear (default): Iterates through all instances, checking if their boundingSphere is inside the camera’s frustum.
    Best for dynamic scenarios.
  • BVH: If a BVH is built, its nodes are recursely iterated. If a node is outside the camera’s frustum, the node and all its children are discarded.
    Best for mostly static scenarios.

When Not to Use It

Sometimes, frustum culling can be more costly than beneficial, so it’s better to skip it if:

  • Most instances are always within the camera’s frustum.
  • The geometry is too simple (e.g., cubes, blades of grass, etc.).

Disable Autoupdate

It’s possible to disable the automatic computing of frustum culling and sorting before each rendering in this way:

iMesh.autoUpdate = false;
// compute frustum culling and sorting manually
iMesh.performFrustumCulling(camera);

OnFrustumEnter Callback

When frustum culling is performed, the onFrustumEnter callback is called for each instance in the camera frustum. This callback is very useful for animating only the bones of visible instances.

If the callback returns true, the instance will be rendered.

iMesh.onFrustumEnter = (index, camera) => {
// render only if not too far away
return iMesh.getPositionAt(index).distanceTo(camera.position) <= maxDistance;
};

Example

import { InstancedMesh2 } from '@three.ez/instanced-mesh';
import { MeshStandardMaterial, TorusKnotGeometry } from 'three';
const geo = new TorusKnotGeometry();
const mat = new MeshStandardMaterial()
export const torusKnots = new InstancedMesh2(geo, mat);
torusKnots.perObjectFrustumCulled = true; // default is true
torusKnots.onFrustumEnter = (index, camera) => {
// render only if not too far away
return torusKnots.getPositionAt(index).distanceTo(camera.position) <= 25;
};
torusKnots.addInstances(25, (obj, index) => {
obj.position.x = (index % 5 - 2) * 5;
obj.position.y = (Math.trunc(index / 5) - 2) * 5;
obj.quaternion.random();
obj.color = Math.random() * 0xffffff;
});
import { Scene, DirectionalLight, AmbientLight } from 'three';
import { Main, PerspectiveCameraAuto } from '@three.ez/main';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { torusKnots } from './app.js';
const main = new Main();
const camera = new PerspectiveCameraAuto().translateZ(20);
const scene = new Scene().add(torusKnots);
main.createView({ scene, camera });
const controls = new OrbitControls(camera, main.renderer.domElement);
controls.update();
const ambientLight = new AmbientLight('white', 0.8);
scene.add(ambientLight);
const dirLight = new DirectionalLight('white', 2);
dirLight.position.set(0.5, 0.866, 0);
camera.add(dirLight);