ComputeInstanceCulling
new ComputeInstanceCulling(meshOrOptions : THREE.InstancedMesh|THREE.Mesh|Object, renderer : THREE.WebGPURenderer, options : Object)GPU-driven frustum and LOD culling for massive instanced rendering.
Architecture
- Runs entirely on GPU via compute shaders (TSL)
- Tests each instance against camera frustum and distance
- Compacts visible instances into packed survivor buffer
- Writes indirect draw args (instanceCount) atomically
- Optional depth sorting for transparent objects
Culling Modes
- Frustum Culling: Excludes instances outside camera view
- LOD Sampling: Continuous distance-based sampling (reduces far instances)
LOD System
- Purpose: Reduce draw density smoothly with distance by keeping each instance with probability
pKeep. - Near/Far:
lodNeardefines where LOD begins.lodFardefines where range mode reaches full falloff. - Modes (
lodMode):- Disabled (
LOD_MODE_DISABLED): LOD sampling is off. - Range (
LOD_MODE_RANGE): Smoothstep falloff betweenlodNearandlodFar.pKeep = 1 - smoothstep( lodNear, lodFar, d ) - Exp (
LOD_MODE_EXP): Exponential density falloff starting atlodNear.pKeep = exp( -density^2 * (d - lodNear)^2 )Note:lodNearshifts the start of the exp curve; increasing it delays the falloff.
- Disabled (
- Density Parameter:
lodDensityacts as exp density (smaller = slower decay). - Sampling: Each instance is kept if
hash(instanceId) < pKeep, giving stable stochastic thinning. - Compatibility: Internally computes
stepF = sqrt(1 / max(pKeep, eps))for legacy outputs.
Performance
- O(N) GPU parallel culling vs O(N) CPU serial testing
- Zero CPU overhead after setup
- Indirect draw eliminates CPU→GPU instance data sync
- Typical 10-100x performance gain for large instance counts
import { ComputeInstanceCulling, instanceCullingIndex as index } from '@three-blocks/core';
// Create instanced mesh
const instancedMesh = new THREE.InstancedMesh(
new THREE.BoxGeometry(),
new THREE.MeshBasicNodeMaterial(),
count
);
// Create culler
const culler = new ComputeInstanceCulling(instancedMesh, renderer);
// Use culling index instead of instanceIndex
material.positionNode = rotate(positionLocal, angle.add(hash(index(culler))))
Example: Custom positionNode with TSL
When using positionNode with GPU culling, the instanceCulling transform is applied
automatically after your positionNode. Use instanceCullingIndex to access per-instance
data like random seeds or animation offsets:
import { ComputeInstanceCulling, instanceCullingIndex } from '@three-blocks/core';
import { time, hash, rotate, positionLocal, normalLocal, transformNormalToView } from 'three/tsl';
const count = 10000;
const instancedMesh = new THREE.InstancedMesh(
new THREE.BoxGeometry(),
new THREE.MeshNormalNodeMaterial(),
count
);
// Setup GPU culling
const culler = new ComputeInstanceCulling(instancedMesh, renderer);
// Access the culled instance index for per-instance variation
const culledIndex = instanceCullingIndex(culler);
// Compute per-instance rotation angle based on time and instance hash
const angle = time.mul(0.6).add(hash(culledIndex).mul(Math.PI * 2));
// Apply rotation to position (runs BEFORE instance matrix transform)
instancedMesh.material.positionNode = rotate(positionLocal, angle);
// Transform normals to match the rotation
instancedMesh.material.normalNode = transformNormalToView(
rotate(normalLocal, angle)
).normalize();
Example: Advanced culling with custom visibility logic
For advanced use cases, you can access the culler's internal buffers directly:
import { ComputeInstanceCulling } from '@three-blocks/core';
import { storage, instanceIndex, If, uint } from 'three/tsl';
const culler = new ComputeInstanceCulling(instancedMesh, renderer, {
enabled: true,
sortObjects: true // Enable depth sorting for transparency
});
// Access culling parameters
culler.lodNear.value = 50; // LOD near radius
culler.lodFar.value = 400; // LOD far radius (range mode)
culler.lodMode.value = LOD_MODE_RANGE;
culler.lodDensity.value = 0.00025; // Exp density (exp mode)
// Read back survivor count for debugging
const args = await culler.readIndirectArgs();
console.log(`Visible instances: ${args[1]} / ${count}`);
Constructor Parameters
meshOrOptionsTHREE.InstancedMesh | THREE.Mesh | ObjectrendereroptionalTHREE.WebGPURendereroptionsoptionalObjectSee also
- {@link instanceCulling} - TSL node for applying culled instance transformations
- {@link IndirectBatchedMesh}
- {@link ComputeBatchCulling}
Example
import { ComputeInstanceCulling } from '@three-blocks/core';
import * as THREE from 'three/webgpu';
// Create instanced mesh
const count = 10000;
const instancedMesh = new THREE.InstancedMesh(
new THREE.BoxGeometry(),
new THREE.MeshBasicNodeMaterial(),
count
);
// Initialize instance matrices
const tempMat = new THREE.Matrix4();
for (let i = 0; i < count; i++) {
tempMat.setPosition(
Math.random() * 100 - 50,
Math.random() * 100 - 50,
Math.random() * 100 - 50
);
instancedMesh.setMatrixAt(i, tempMat);
}
// Enable GPU culling - that's it!
// Automatically patches material.setupPosition() to use culled instances
new ComputeInstanceCulling(instancedMesh, renderer);
// Render (culling happens automatically)
function animate() {
renderer.render(scene, camera);
}
Methods
setCameraUniforms#
setCameraUniforms(camera : THREE.Camera)Update camera uniforms for frustum culling.
Call before update() each frame.
Parameters
cameraTHREE.CameraattachGUI#
attachGUI(folder : Object)Attach culling controls to a GUI folder. Compatible with lil-gui, dat.gui, and Three.js Inspector.
Parameters
folderObjectdisposeGUI#
disposeGUI()Detach and destroy the GUI folder.
update#
update()Execute GPU culling and compaction. Runs compute shaders to test visibility, compact survivors, and optionally sort. Must be called every frame before rendering.
attachGeometry#
attachGeometry(geometry : THREE.BufferGeometry)Attach geometry to receive indirect draw args.
Parameters
geometryTHREE.BufferGeometryattachMesh#
attachMesh(mesh : THREE.Mesh)Attach mesh and auto-disable sorting for opaque materials.
Parameters
meshTHREE.MeshreadIndirectArgs#
readIndirectArgs() : Promise<(Uint32Array|null)>Read back indirect draw arguments from GPU (debug/stats).
Returns
Promise<(Uint32Array|null)> — Array of 5 values: [indexCount, instanceCount, firstIndex, baseVertex, firstInstance], or null if not ready.readSurvivorIndicesAsync#
readSurvivorIndicesAsync() : Promise<Uint32Array>Read back surviving instance IDs from GPU (debug/analysis).
Returns
Promise<Uint32Array> — Array of survivor instance indices.setBoundingSphereAt#
setBoundingSphereAt(instanceIndex : number, center : THREE.Vector3|Object, radius : number)Set bounding sphere for a specific instance.
Only effective when perInstanceBoundingBox is enabled.
Parameters
instanceIndexnumbercenterTHREE.Vector3 | ObjectradiusnumbergetBoundingSphereAt#
getBoundingSphereAt(instanceIndex : number, target : THREE.Vector4) : Object|nullGet bounding sphere for a specific instance.
Only effective when perInstanceBoundingBox is enabled.
Parameters
instanceIndexnumbertargetoptionalTHREE.Vector4Returns
Object | nullsetMaxBoundingSphere#
setMaxBoundingSphere(boundsData : Float32Array|Array<{center: {x: number, y: number, z: number}, radius: number}>)Set the shared bounding sphere used when perInstanceBoundingBox is disabled.
Computes the maximum bounding sphere that encompasses all provided per-instance bounds.
Parameters
boundsDataFloat32Array | Array<{center: {x: number, y: number, z: number}, radius: number}>initBoundingSpheresStorage#
initBoundingSpheresStorage(data : Float32Array)Initialize per-instance bounding sphere storage buffer. Call this to enable per-instance culling after construction.
Parameters
dataoptionalFloat32Arraydispose#
dispose()Dispose of GPU resources.