Namespace culling

xeokit View Culling


Off-thread frustum and solid-angle culling for a View. A bounding sphere per object is mirrored into a Web Worker, which flips viewing!viewer.ViewObject.culled as the camera moves so the renderer skips what's off-screen or too small to see.


The culling module trims a View's draw load without blocking the main thread. One bounding sphere per SceneObject is mirrored into a worker; each camera change posts the view's frustum, and the worker returns only the objects whose state flipped since the last pass. Those deltas are written to ViewObject.culled, and the renderer drops their meshes from the view's draw index.

One worker and one bounds mirror serve a whole Scene, retrieved via getSceneCuller. The mirror — one sphere per object — is held once and shared by every View, while cull state is tracked per view, so each camera culls independently.

The worker scans a flat list of spheresO(n) per camera, with no spatial hierarchy. Bounds come from the collision index and stay current through the same scene-mutation events, but that index's BVH is never traversed: it lives on the main thread, beyond the worker's reach. The trade is deliberate — hierarchical O(log n) acceleration is given up so the scan never blocks the render loop. For BVH-accelerated frustum culling on the main thread instead, use SceneCollisionIndex.intersectFrustum().


%%{init:{"theme":"dark"}}%% classDiagram direction TB class View { <<viewer>> } class ViewCuller { +view : View +destroy() } class getSceneCuller { +(scene) SceneCuller } class SceneCuller { +scene : Scene +registerView(viewId, listener) +unregisterView(viewId) +postViewChanged(message) +objectIdAt(index) string +destroy() } class CullParams { +solidAngleLimit? : number +cullEveryNUpdates? : number } class Scene { <<scene>> } ViewCuller o-- View : drives ViewCuller ..> getSceneCuller : uses getSceneCuller ..> SceneCuller : caches per Scene ViewCuller o-- SceneCuller : shares ViewCuller ..> CullParams : reads SceneCuller o-- Scene : observes
%%{init:{"theme":"default"}}%% classDiagram direction TB class View { <<viewer>> } class ViewCuller { +view : View +destroy() } class getSceneCuller { +(scene) SceneCuller } class SceneCuller { +scene : Scene +registerView(viewId, listener) +unregisterView(viewId) +postViewChanged(message) +objectIdAt(index) string +destroy() } class CullParams { +solidAngleLimit? : number +cullEveryNUpdates? : number } class Scene { <<scene>> } ViewCuller o-- View : drives ViewCuller ..> getSceneCuller : uses getSceneCuller ..> SceneCuller : caches per Scene ViewCuller o-- SceneCuller : shares ViewCuller ..> CullParams : reads SceneCuller o-- Scene : observes
classDiagram
    direction TB
    class View {
      <<viewer>>
    }
    class ViewCuller {
      +view : View
      +destroy()
    }
    class getSceneCuller {
      +(scene) SceneCuller
    }
    class SceneCuller {
      +scene : Scene
      +registerView(viewId, listener)
      +unregisterView(viewId)
      +postViewChanged(message)
      +objectIdAt(index) string
      +destroy()
    }
    class CullParams {
      +solidAngleLimit?   : number
      +cullEveryNUpdates? : number
    }
    class Scene {
      <<scene>>
    }
    ViewCuller o-- View : drives
    ViewCuller ..> getSceneCuller : uses
    getSceneCuller ..> SceneCuller : caches per Scene
    ViewCuller o-- SceneCuller : shares
    ViewCuller ..> CullParams : reads
    SceneCuller o-- Scene : observes

  • Off the main thread — the frustum and solid-angle tests run in a Web Worker. The main thread only posts frustums and applies the returned cull deltas.
  • Frustum + solid-angle — an object is culled when its bounding sphere falls outside the view frustum, or subtends less than solidAngleLimit on screen. The size test is resolution-independent.
  • One worker per ScenegetSceneCuller is a cached singleton; the bounds mirror (one sphere per object) is held once and shared by every View. The culler auto-destroys on onSceneDestroyed.
  • Per-view cull state — each View culls its own camera against the shared mirror, so multi-view setups don't interfere.
  • Self-maintaining bounds — the mirror follows scene mutations (objects added, removed, or moved) through the same events the collision!SceneCollisionIndex | SceneCollisionIndex observes, and reads bounds back from that index on a coalesced flush.
  • Throttled — only every Nth camera change posts a pass, with a trailing "settled" pass once motion stops and at most one pass in flight at a time.
  • Independent of visibility — culling drops a mesh from the draw index without disturbing viewing!viewer.ViewObject.visible, so toggling it never reveals an object the app deliberately hid.

npm install @xeokit/sdk

Construct a ViewCuller against the View. It starts culling at once and self-destroys when the View is destroyed.

import { ViewCuller } from "@xeokit/sdk/spatial/culling";

const culler = new ViewCuller(view);

const culler = new ViewCuller(view, {
solidAngleLimit: 0.01, // cull small objects sooner
cullEveryNUpdates: 5 // post a pass every 5th camera change
});

Un-culls every object, restoring full visibility.

culler.destroy();

One ViewCuller per View. They share a single per-Scene worker and bounds mirror, reached internally through getSceneCuller, but cull their own cameras independently.

const cullerA = new ViewCuller(viewA);
const cullerB = new ViewCuller(viewB); // same Scene, independent camera

Classes

SceneCuller
ViewCuller

Interfaces

CullParams

Type Aliases

CullingResultListener

Variables

DEFAULT_CULL_PARAMS

Functions

getSceneCuller