three-virtual-geometry
Version:
Virtual geometry for three.js WebGPU: automatic cluster LOD and GPU-driven culling for billions of triangles. Independent implementation, not affiliated with Epic Games.
101 lines (100 loc) • 5.28 kB
TypeScript
/**
* Occlusion culling against a hierarchical depth buffer (HZB), on by default.
*
* Per frame, before the cut is computed:
* 1. occluder pass - the occluder meshes' previous-frame camera cuts (their index buffers still hold them)
* are drawn depth-only with the CURRENT camera into a small depth target. That geometry was visible last
* frame, so it is a subset of the real occluders.
* 2. HZB build - mip 0 is that depth (copied into a storage buffer), each further mip is the MAX of its
* 2x2 children: a texel only hides what lies behind all of its occluders.
* 3. cull - an instance or meshlet sphere is dropped from the camera cut when its projected box, read at
* the mip where it covers at most 2x2 texels, is entirely behind the max depth there.
*
* Why it is safe: removing occluders can only push the depth buffer back, so anything hidden by the subset
* is hidden by the full scene. Stale or missing occluders cost efficiency, never correctness. The occluders
* are LOD-simplified (they may bulge up to the error threshold in front of the true surface), so the tested
* sphere is grown by that error first.
*
* Only meshes flagged `occluder` are drawn in step 1 (by default: opaque, single-sided, no alpha test, i.e.
* solid geometry such as terrain, rocks, buildings; foliage is mostly holes). Every mesh is tested, unless
* its `occlusionCulling` is off. Shadow cuts are never occlusion culled: a caster hidden from the camera can
* still shadow something visible.
*
* Storage: the whole HZB (a header with each mip's offset and size, then all mips) is one storage buffer,
* sized for `maxSize`, so nothing is reallocated on resize.
*/
import * as THREE from 'three/webgpu';
import type { VirtualMesh } from './VirtualMesh.js';
type Node = any;
export declare class OcclusionCulling {
/** Lever: set false to turn occlusion culling off (no occluder pass, no HZB, nothing culled by it). */
enabled: boolean;
/**
* Lever: decide by measurement. Whether occlusion pays off depends on the scene and the GPU: the occluder
* pass has a fixed cost, which wins in cities and interiors and loses in open landscapes. With `auto`,
* frame times are compared with occlusion off and on (`autoProbeFrames` each), the faster setting is kept
* for `autoHoldFrames`, then measured again. Within 3% (e.g. both capped by vsync) it stays off.
* Set false to keep it on whenever `enabled`.
*/
auto: boolean;
autoProbeFrames: number;
autoHoldFrames: number;
/** The auto setting currently in use (true: occlusion on). */
autoChoice: boolean;
private phase;
private phaseFrames;
private samples;
private offMean;
private lastTick;
/** Depth resolution relative to the drawing buffer. Lower is cheaper and culls a little less. */
resolutionScale: number;
/** Largest HZB dimension in texels (memory ~1.33 * maxSize^2 * 4 bytes). */
readonly maxSize: number;
/** True when this frame's HZB holds at least one occluder (the cull kernels test against it only then). */
active: boolean;
/** 1 while this frame's HZB is valid; the cull kernels only occlusion-cull then. */
readonly activeNode: THREE.UniformNode<"uint", number>;
private readonly depthScene;
private readonly proxies;
private readonly projection;
private readonly near;
private readonly levelCapacity;
private readonly levelOffsets;
private readonly hzbAttribute;
private readonly hzbWrite;
private readonly hzbRead;
/** Copy + reduce kernels for the current depth size, dispatched with this frame's cut (one submission). */
private buildNodes;
private renderTarget;
private width;
private height;
private levels;
private readonly bufferSize;
constructor(projection: Node, near: Node, maxSize?: number);
/**
* True only when a sphere (view-space center, radius) is certainly hidden. Any doubt (touching the near
* plane, outside the screen) keeps it.
*/
occluded(viewCenter: Node, radius: Node): Node;
/** Called once per frame by the context: feeds the auto A/B measurement (see `auto`). */
tick(now: number): void;
private next;
private autoWanted;
/** Depth-only proxy of a mesh's camera cut, created on first use and kept in sync with its pool. */
private proxyOf;
unregisterMesh(mesh: VirtualMesh): void;
/**
* Whether occlusion culling runs this frame (enabled, a supported camera and depth mode, and the auto tuner
* wants it). Meshes get their per-mesh flag from this before the cut is computed.
*/
wantsFrame(renderer: THREE.WebGPURenderer, camera: THREE.Camera, _frame: number): boolean;
/**
* Renders the occluders' last camera cuts into the depth target and returns the compute kernels that turn
* it into the HZB; run them before the cut (the caller prepends them to the same compute submission).
* Sets `activeNode`, which the cull kernels check: without occluders this frame, nothing is occlusion culled.
*/
beginFrame(renderer: THREE.WebGPURenderer, camera: THREE.Camera, meshes: readonly VirtualMesh[]): THREE.ComputeNode[];
private resize;
dispose(): void;
}
export {};