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 manuallyiMesh.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);