ComputeInstanceCulling

@three-blocks/coreWebGPU
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: lodNear defines where LOD begins. lodFar defines where range mode reaches full falloff.
  • Modes (lodMode):
    • Disabled (LOD_MODE_DISABLED): LOD sampling is off.
    • Range (LOD_MODE_RANGE): Smoothstep falloff between lodNear and lodFar. pKeep = 1 - smoothstep( lodNear, lodFar, d )
    • Exp (LOD_MODE_EXP): Exponential density falloff starting at lodNear. pKeep = exp( -density^2 * (d - lodNear)^2 ) Note: lodNear shifts the start of the exp curve; increasing it delays the falloff.
  • Density Parameter: lodDensity acts 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 | Object
Instanced mesh to cull or options bag.
rendereroptionalTHREE.WebGPURenderer
WebGPU renderer (required when first param is a mesh).
optionsoptionalObject
Options object when using the mesh signature.
See 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.Camera
Active camera for culling tests.

attachGUI#

attachGUI(folder : Object)

Attach culling controls to a GUI folder. Compatible with lil-gui, dat.gui, and Three.js Inspector.

Parameters
folderObject
GUI folder instance (e.g., from lil-gui or renderer.inspector.createParameters()).

disposeGUI#

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.BufferGeometry
Target geometry for indirect rendering.

attachMesh#

attachMesh(mesh : THREE.Mesh)

Attach mesh and auto-disable sorting for opaque materials.

Parameters
meshTHREE.Mesh
Target mesh instance.

readIndirectArgs#

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
instanceIndexnumber
Index of the instance.
centerTHREE.Vector3 | Object
Center of the bounding sphere in local space.
radiusnumber
Radius of the bounding sphere.

getBoundingSphereAt#

getBoundingSphereAt(instanceIndex : number, target : THREE.Vector4) : Object|null

Get bounding sphere for a specific instance. Only effective when perInstanceBoundingBox is enabled.

Parameters
instanceIndexnumber
Index of the instance.
targetoptionalTHREE.Vector4
Optional target to store the result (x,y,z = center, w = radius).
Returns
Object | null

setMaxBoundingSphere#

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}>
Either a Float32Array of vec4 (centerX, centerY, centerZ, radius) per instance, or an array of objects with center and radius properties.

initBoundingSpheresStorage#

initBoundingSpheresStorage(data : Float32Array)

Initialize per-instance bounding sphere storage buffer. Call this to enable per-instance culling after construction.

Parameters
dataoptionalFloat32Array
Optional initial data (vec4 per instance: centerX, centerY, centerZ, radius).

dispose#

dispose()

Dispose of GPU resources.