UNPKG

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
/** * 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 {};