/** * Shared GPU buffers and compute passes for many VirtualMeshes. * * Every mesh's vertices, meshlets, level table and instances are appended to one set of storage buffers, and one * fixed set of compute passes selects the cut for all of them. CPU cost, shader count and pipeline compilation * therefore do not grow with the number of unique meshes: a level with hundreds of different props costs the * same 12 dispatches per frame as a scene with one. Each mesh still gets its own indexed indirect draw (its own * material), into a region of the pool's shared index buffer laid out each frame by a prefix sum. * * Per frame (all on the GPU): * 0. reset - clears the counters. * 1. cells - one thread per cell (CELL_SIZE instances of one mesh, Morton-ordered): frustum, sub-pixel and * draw-distance tests on the cell's bounding sphere. Visible cells are appended to a list. * 2. instance - one workgroup per visible cell: per-instance tests, occlusion, then the LOD level range per * cut (camera and shadow). Instances whose few meshlets are all certain to pass emit them * directly (fast path); the others go to a work list. * 3. meshlet - one workgroup per work item: per-meshlet frustum/occlusion culling and the LOD test * `parentError > t && error <= t`, widened to the blend band [t, t * k] when the threshold is high * enough for level switches to show (see lodBlend.ts). Selected meshlets get a draw slot, their blend range and a range inside their * mesh's region of the index buffer. * 4. prefix - one thread: lays the meshes' regions out one after another and writes each mesh's draw args. * 5. expand - one workgroup per drawn meshlet: writes 3 indices per triangle. An index encodes * `drawSlot * 128 + meshletLocalVertex`; the material's positionNode decodes it and pulls the * vertex from storage buffers, so any three.js NodeMaterial works unchanged. * * A pool is bounded by WebGPU's storage-buffer binding size; VirtualGeometry opens another pool when one is full. * Buffers grow by reallocation (the nodes are rebuilt, and the pipelines recompiled in the background). * * Based on nanite-webgpu (MIT, Marcin Matuszczyk): cullMeshletsPass + rasterizeHwPass. */ import * as THREE from 'three/webgpu'; import type { VirtualMeshData } from '../preprocess/buildVirtualMesh.js'; import type { VirtualGeometry } from './VirtualGeometry.js'; type Node = any; /** World-space normal of the current vertex, usable in material color/roughness nodes. */ export declare const vgWorldNormal: THREE.PropertyNode<"vec3">; export declare const vgMeshletVarying: THREE.PropertyNode<"float">; export declare const vgLodVarying: THREE.PropertyNode<"float">; export declare const vgInstanceVarying: THREE.PropertyNode<"float">; export declare const vgTintVarying: THREE.PropertyNode<"vec3">; /** World position of the current vertex's instance origin (vertex stage): e.g. to make whole trees sway from their base. */ export declare const vgInstanceOrigin: THREE.PropertyNode<"vec3">; /** Bits of an index used for the meshlet-local vertex; the rest is the draw slot. */ export declare const LOCAL_VERTEX_BITS: number; /** Instances per cell. A cell is a contiguous range of one mesh's Morton-sorted instances. */ export declare const CELL_SIZE = 128; /** Fraction of `maxDrawDistance` over which instances shrink to nothing before they are culled. */ export declare const DRAW_DISTANCE_FADE = 0.15; /** Stand-in for an infinite draw distance on the GPU. */ export declare const NO_DRAW_DISTANCE = 1e+30; export interface PoolCapacity { /** Max meshlets drawn per frame, per cut. */ meshlets: number; /** Max triangles drawn per frame by the camera cut (its index buffer). */ triangles: number; /** Max triangles drawn per frame into shadow maps. */ shadowTriangles: number; } /** CPU-side record of one mesh's place in the pool. */ export interface PoolEntry { slot: number; data: VirtualMeshData; vertexBase: number; meshletBase: number; meshletVertexBase: number; triangleBase: number; levelBase: number; levelCount: number; instanceBase: number; instanceCount: number; cellBase: number; cellCount: number; uvScale: [number, number]; uvOffset: [number, number]; } /** Growable typed array with a GPU attribute; growth replaces the attribute (the pool then rebuilds its nodes). */ declare class GrowBuffer { private readonly make; readonly itemSize: number; /** Largest capacity (items) whose binding stays under the pool's byte limit. */ private readonly maxItems; array: T; attribute: THREE.StorageBufferAttribute; used: number; constructor(make: (n: number) => T, itemSize: number, capacity: number, /** Largest capacity (items) whose binding stays under the pool's byte limit. */ maxItems?: number); get capacity(): number; /** Reserves `count` items; returns the first, or -1 when the buffer must grow first. */ reserve(count: number): number; /** Reallocates to hold at least `needed` items (keeps the contents). */ grow(needed: number): void; /** Marks items [first, first + count) for upload. */ touch(first: number, count: number): void; } export declare class GeometryPool { private readonly context; readonly entries: (PoolEntry | null)[]; /** Highest used mesh slot + 1 (the prefix pass loops over this many). */ readonly meshSlots: THREE.UniformNode<"uint", number>; readonly cellCount: THREE.UniformNode<"uint", number>; readonly maxBytes: number; readonly capacity: PoolCapacity; readonly vertexData: GrowBuffer; readonly vertexAttrs: GrowBuffer; readonly meshletBounds: GrowBuffer; readonly meshletInfo: GrowBuffer; readonly meshletVertices: GrowBuffer; readonly meshletTriangles: GrowBuffer; readonly meshTable: GrowBuffer; readonly instances: GrowBuffer; readonly cells: GrowBuffer; private meshSlotCapacity; private levelUsed; private countersAttribute; /** First word of the capacity controller's state in the counters buffer (see S_SCALE). */ private lodStateBase; /** From the last readback: the GPU controller is still moving its scale, so the cut must be redone every frame. */ controllerActive: boolean; drawArgsAttribute: THREE.IndirectStorageBufferAttribute; cameraIndex: THREE.StorageBufferAttribute; shadowIndex: THREE.StorageBufferAttribute; computeNodes: THREE.ComputeNode[]; positionNode: Node; /** Bumped on every rebuild: meshes re-point their geometry and material when it changes. */ version: number; private needsRebuild; private indexBuffersReady; pipelineState: 'none' | 'compiling' | 'ready'; /** The cut needs recomputing (camera moved, instances or settings changed). */ dirty: boolean; /** True once a cut was computed (its camera index buffer can then serve as occluders). */ hasCut: boolean; /** Holds one mesh too large for a shared pool, sized for it (no other mesh joins). */ dedicated: boolean; /** Set when the device cannot bind this pool's buffers: it is skipped entirely and its meshes are hidden. */ disabled: boolean; constructor(context: VirtualGeometry, capacity: PoolCapacity, maxBytes: number); /** Bytes the geometry of `data` takes in this pool (the largest single binding is what must fit). */ static bytesOf(data: VirtualMeshData): number; /** True when `data` (and `instanceCount` instances) still fit under the binding-size limit. */ fits(data: VirtualMeshData, instanceCount: number): boolean; /** Appends a mesh's geometry and instances. Returns its entry (`slot` is its index in the mesh table). */ add(data: VirtualMeshData, matrices: Float32Array, colors: Float32Array | undefined, gpuToInput: Uint32Array): PoolEntry; /** Frees a mesh's slot (its buffer ranges are reclaimed when the pool is next repacked). */ remove(entry: PoolEntry): void; private growMeshSlots; private writeGeometry; /** Writes a mesh record: bounding spheres, levers, level table location, uv packing, flags. */ writeMeshRecord(e: PoolEntry, o: { maxDrawDistance: number; minPixelRadius: number; enabled: number; shadow: number; occlusion: number; }): void; /** Updates some of a mesh's per-frame levers (only what changed is uploaded). */ writeFlags(e: PoolEntry, o: { maxDrawDistance?: number; minPixelRadius?: number; enabled?: number; shadow?: number; occlusion?: number; }): void; readFlag(e: PoolEntry, index: 'maxDrawDistance' | 'minPixelRadius' | 'enabled' | 'shadow' | 'occlusion'): number; private writeInstances; /** Writes one instance matrix (GPU order index within the mesh). Call `commitInstances` afterwards. */ setMatrix(e: PoolEntry, gpuIndex: number, matrix: THREE.Matrix4): void; /** Uploads changed instances and refreshes the bounds of the given cells. */ commitInstances(e: PoolEntry, cells: Iterable): void; /** * Bounding sphere of one cell's instances: the AABB of their world-space object spheres gives the center, and * the radius reaches every instance sphere. Both radii are inflated a little so float32 rounding on the GPU can * never make the cell test stricter than the per-instance test. */ private computeCell; /** * Index buffers are written by compute and read by draws. three.js picks GPU buffer usage from the first use, * so create them as index buffers (INDEX | STORAGE) before any compute. Also rebuilds the nodes after growth. */ prepare(renderer: THREE.WebGPURenderer): void; /** Builds the nodes if the pool's buffers grew since the last build (or it was never built). */ ensureBuilt(): void; /** Recreates the per-frame buffers and every node (after the pool's buffers grew). */ build(): void; /** Stats of the last computed cut, from a readback of the counters buffer. */ readCounters(renderer: THREE.WebGPURenderer): Promise<{ camera: { selected: number; drawn: number; survived: number; requestedTriangles: number; }; shadow: { selected: number; drawn: number; survived: number; requestedTriangles: number; }; occluded: number; perMesh: (c: number, slot: number) => number; scale: number[]; fill: number[]; }>; /** * Byte offset of a mesh's draw args for a cut (0: camera, 1: shadow) and part (0: meshlets drawn over every pixel, * 1: meshlets inside an LOD blend band, drawn with the blend mask) in `drawArgsAttribute`. */ drawArgsOffset(cut: number, slot: number, part?: number): number; dispose(): void; } /** * World-space bounding sphere of the instance whose column-major matrix starts at `base`, computed the same way * the instance pass does: center = model * sphere center, radius = sphere radius * max column length. */ export declare function instanceSphere(m: ArrayLike, base: number, sphere: number[], out: Float64Array): void; export declare function encodeOctNormal(nx: number, ny: number, nz: number): number; /** xyz as float bits + octahedral-encoded normal, 4 x u32 per vertex. */ export declare function packVertices(positions: Float32Array, normals: Float32Array): Uint32Array; /** * uv per vertex as two unorm16 in one u32, quantized over the bounds of all UVs (also tiling UVs outside 0..1). * Decode: unpackUnorm2x16(packed) * scale + offset. One step is 1/65535 of the range: well below a texel of a 4K * texture for 0..1 UVs. */ export declare function packUvs(uvs: Float32Array): { packed: Uint32Array; scale: [number, number]; offset: [number, number]; }; /** * Instance indices sorted along a 30-bit Morton curve over the instance centers, so that consecutive slots are * spatially close and a fixed-size run of slots forms a compact cell. */ export declare function mortonOrder(matrices: Float32Array, sphere: number[]): Uint32Array; export {};