UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

549 lines (548 loc) 20.6 kB
/** * GSplatManager manages the rendering of splats using a work buffer, where all active splats are * stored and rendered from. * * Shared culling + compaction (GPU sorting and compute renderer, WebGPU only): * Interval compaction operates on contiguous intervals of splats (one per octree node). * 1. Cull + count (compute): each interval's bounding sphere is tested against frustum * planes (or a fisheye cone). The pass writes the interval's splat count (or 0 if * culled) into a count buffer. * 2. Prefix sum: exclusive prefix sum over the count buffer produces output offsets. * The last element gives visibleCount. * 3. Scatter (compute): one workgroup per interval expands visible intervals into * compactedSplatIds (flat list of work-buffer pixel indices). * * Raster renderer — GPU sorting (WebGPU, {@link GSplatQuadRenderer}): * Uses shared steps 1-3 above, then: * 4. Generate sort keys: an indirect compute dispatch (visibleCount threads) reads each * compactedSplatIds[i] to look up the splat's depth and writes a sort key to keysBuffer. * 5. Radix sort: an indirect GPU radix sort over keysBuffer, with compactedSplatIds supplied * as initial values, produces a buffer of sorted splat IDs directly. * 6. Render: the vertex shader reads sortedSplatIds[vertexId] → splatId. * * Raster renderer — CPU sorting (WebGPU and WebGL, {@link GSplatQuadRenderer}): * 1. Sort on worker: camera position and splat centers are sent to a web worker which * performs a counting sort and returns the sorted order as orderBuffer. * 2. Render: the vertex shader reads orderBuffer[vertexId] → splatId. * No culling or compaction is used. * * Compute tiled renderer (WebGPU only, {@link GSplatComputeLocalRenderer}): * Uses shared steps 1-3 above, then runs a fully compute-based tiled pipeline: * project splats into a cache, bin into screen tiles, sort per-tile by depth, and rasterize * front-to-back. See {@link GSplatComputeLocalRenderer} for the full pass breakdown. * * @ignore */ export class GSplatManager { /** * @param {GraphicsDevice} device - The graphics device. * @param {GSplatDirector} director - The director. * @param {Layer} layer - The layer. * @param {GraphNode} cameraNode - The camera node. */ constructor(device: GraphicsDevice, director: GSplatDirector, layer: Layer, cameraNode: GraphNode); /** @type {GraphicsDevice} */ device: GraphicsDevice; /** @type {GraphNode} */ node: GraphNode; /** @type {GSplatWorkBuffer} */ workBuffer: GSplatWorkBuffer; /** @type {GSplatRenderer} */ renderer: GSplatRenderer; /** * A map of versioned world states, keyed by version. * * @type {Map<number, GSplatWorldState>} */ worldStates: Map<number, GSplatWorldState>; /** * The version of the last world state. * * @type {number} */ lastWorldStateVersion: number; /** * The currently active renderer mode. Starts as undefined so the first * prepareRendererMode() call always creates the appropriate resources. * * @type {number|undefined} */ activeRenderer: number | undefined; /** * CPU-based sorter (when not using GPU sorting). * * @type {GSplatUnifiedSorter|null} */ cpuSorter: GSplatUnifiedSorter | null; /** * GPU-based key generator (when using GPU sorting). * * @type {GSplatSortKeyCompute|null} */ keyGenerator: GSplatSortKeyCompute | null; /** * GPU-based radix sorter (when using GPU sorting). * * @type {ComputeRadixSort|null} */ gpuSorter: ComputeRadixSort | null; /** * Interval-based GPU compaction (always-on for GPU sort path). * * @type {GSplatIntervalCompaction|null} */ intervalCompaction: GSplatIntervalCompaction | null; /** * Indirect draw slot index for the current frame (-1 when not using indirect draw). * * @type {number} */ indirectDrawSlot: number; /** * Indirect dispatch slot index for GPU-sort indirect dispatch args. * Slot +0 = key gen, slot +1 = sort. The compute local renderer builds * its own indirect args in private buffers and does not use these slots. * * @type {number} */ indirectDispatchSlot: number; /** * Total intervals from the last interval compaction dispatch. Needed for * writeIndirectArgs to index into the prefix sum buffer for visible count. * * @type {number} */ lastCompactedNumIntervals: number; /** @type {number} */ sortedVersion: number; /** * When true, suppresses ready=true in frame:ready until a fullUpdate cycle runs. * Only set when octreeInstances exist and params change (dirty). * * @type {boolean} * @private */ private _awaitingLodUpdate; /** * Cached work buffer format version for detecting extra stream changes. * * @type {number} * @private */ private _workBufferFormatVersion; /** * Flag set when the work buffer needs a full rebuild due to format changes. * * @type {boolean} * @private */ private _workBufferRebuildRequired; /** * Number of blocks uploaded to the work buffer this frame. * * @type {number} */ bufferCopyUploaded: number; /** * Total number of blocks in the work buffer this frame. * * @type {number} */ bufferCopyTotal: number; /** * Tracks placement state changes (format version, modifier hash, numSplats, centersVersion). * * @type {GSplatPlacementStateTracker} * @private */ private _stateTracker; /** * Tracks last seen centersVersion per resource ID for detecting centers updates. * * @type {Map<number, number>} * @private */ private _centersVersions; /** @type {number} */ framesTillFullUpdate: number; /** @type {Vec3} */ lastLodCameraPos: Vec3; /** @type {Vec3} */ lastLodCameraFwd: Vec3; /** @type {number} */ lastLodCameraFov: number; /** @type {Vec3} */ lastSortCameraPos: Vec3; /** @type {Vec3} */ lastSortCameraFwd: Vec3; /** @type {Vec3} */ lastCullingCameraFwd: Vec3; /** @type {Mat4} */ lastCullingProjMat: Mat4; /** @type {boolean} */ sortNeeded: boolean; /** * Budget balancer for global splat budget enforcement. * * @type {GSplatBudgetBalancer} * @private */ private _budgetBalancer; /** * Dynamic scale factor applied to LOD parameters during budget enforcement. Shifts all * LOD boundaries uniformly to bring the initial estimate closer to the budget target, * reducing balancer work. Applied directly to lodBaseDistance and gently to lodMultiplier. * Values > 1 push boundaries outward (more splats), values < 1 pull them inward * (fewer splats). * * @type {number} * @private */ private _budgetScale; /** * Persistent block allocator for work buffer pixel allocations. Grows on demand. * * @type {BlockAllocator} * @private */ private _allocator; /** * Maps allocId (from GSplatPlacement) to the corresponding MemBlock in the allocator. * Shared with GSplatWorldState constructors which mutate it during diff. * * @type {Map<number, MemBlock>} * @private */ private _allocationMap; /** @type {Vec3} */ lastColorUpdateCameraPos: Vec3; /** @type {GraphNode} */ cameraNode: GraphNode; /** @type {Scene} */ scene: Scene; /** * Layer placements, only non-octree placements are included. * * @type {GSplatPlacement[]} */ layerPlacements: GSplatPlacement[]; /** @type {boolean} */ layerPlacementsDirty: boolean; /** * True when placements have been added or removed since the last world state was created. * Triggers a full work buffer rebuild so boundsBaseIndex stays consistent. * * @type {boolean} * @private */ private _placementSetChanged; /** @type {Map<GSplatPlacement, GSplatOctreeInstance>} */ octreeInstances: Map<GSplatPlacement, GSplatOctreeInstance>; /** * Octree instances scheduled for destruction. We collect their releases and destroy them * when creating the next world state * * @type {GSplatOctreeInstance[]} */ octreeInstancesToDestroy: GSplatOctreeInstance[]; /** * Flag set when new octree instances are added, to trigger immediate LOD evaluation. * * @type {boolean} */ hasNewOctreeInstances: boolean; /** * Bitmask flags controlling which render passes this manager participates in. * * @type {number|undefined} */ renderMode: number | undefined; director: GSplatDirector; layer: Layer; destroy(): void; _destroyed: boolean; /** * Destroys GPU sorting resources (key generator, radix sorter, compaction). * * @private */ private destroyGpuSorting; /** * Destroys interval compaction resources. * * @param {boolean} [useCpuSort] - Whether to switch the renderer to CPU-sorted mode. * @private */ private destroyIntervalCompaction; /** * Destroys CPU sorting resources (worker-based sorter). * * @private */ private destroyCpuSorting; /** * Creates GPU sorting resources (key generator, radix sorter) if not already present. * * @private */ private initGpuSorting; /** * Creates the CPU sorter and prepares it for the current world state. Disables any * GPU-side indirect draw and hides the mesh until the first sort result arrives. * * @private */ private initCpuSorting; get material(): import("../materials/shader-material.js").ShaderMaterial; /** * Dispatches compute pick pipeline and returns the configured pick mesh instance. * Only works when the local compute renderer is active. * * @param {object} camera - The camera. * @param {number} width - Pick target width. * @param {number} height - Pick target height. * @returns {import('../mesh-instance.js').MeshInstance|null} The pick mesh instance, or null. */ prepareForPicking(camera: object, width: number, height: number): import("../mesh-instance.js").MeshInstance | null; /** * Creates the CPU sorter (Web Worker based). * * @returns {GSplatUnifiedSorter} The created sorter. */ createSorter(): GSplatUnifiedSorter; /** * Sets the render mode for this manager and its renderer. * * @param {number} renderMode - Bitmask flags controlling render passes (GSPLAT_FORWARD, GSPLAT_SHADOW, or both). * @ignore */ setRenderMode(renderMode: number): void; /** * True when frustum culling can run (bounds data available). * * @type {boolean} * @private */ private get canCull(); /** * Creates the renderer and sort resources for the given mode. Used at init time. * * @param {number} mode - The GSPLAT_RENDERER_* constant. * @private */ private _createRenderer; /** * Checks whether the resolved renderer mode has changed and transitions to the new mode. * Handles both sort-mode transitions (CPU <-> GPU sort) and full renderer swaps * (quad <-> compute). * * @private */ private prepareRendererMode; /** * Supply the manager with the placements to use. This is used to update the manager when the * layer's placements have changed, called infrequently. * * @param {GSplatPlacement[]} placements - The placements to reconcile with. */ reconcile(placements: GSplatPlacement[]): void; updateWorldState(): void; onSorted(count: any, version: any, orderData: any): void; /** * Rebuilds the work buffer for a world state on its first sort. * Resizes buffer, renders changed splats, syncs transforms, and handles pending releases. * * @param {GSplatWorldState} worldState - The world state to rebuild for. * @param {number} count - The number of splats. * @param {boolean} [forceFullRebuild] - Force rendering all splats (e.g. format change). */ rebuildWorkBuffer(worldState: GSplatWorldState, count: number, forceFullRebuild?: boolean): void; /** * Cleans up old world states between the last sorted version and the new version. * Merges upload requirements from skipped states into the active state, then * decrements ref counts and destroys old states. * * @param {number} newVersion - The new version to clean up to. */ cleanupOldWorldStates(newVersion: number): void; /** * Applies incremental work buffer updates for splats that have changed. * Detects transform changes and color update thresholds, then batch renders updates. * Sets sortNeeded = true when splats move. * * @param {GSplatWorldState} state - The world state to update. */ applyWorkBufferUpdates(state: GSplatWorldState): void; /** * Tests if the camera has moved or rotated enough to require LOD update. * * @returns {boolean} True if camera moved/rotated over thresholds, otherwise false. */ testCameraMovedForLod(): boolean; /** * Tests if the camera has moved enough to require re-sorting. * - For radial sorting: only position matters (rotation doesn't affect sort order) * - For directional sorting: only forward direction matters (position doesn't affect sort order) * * @returns {boolean} True if camera moved enough to require re-sorting, otherwise false. */ testCameraMovedForSort(): boolean; /** * Tests if the camera frustum has changed since the last sort or compaction. Checks both * projection matrix and camera rotation. Used to trigger re-culling/compaction independently of * sort-key changes. * * @returns {boolean} True if the frustum changed. */ testFrustumChanged(): boolean; /** * Updates the camera tracking state for color accumulation calculations. * Called after any render that updates colors (full or color-only). */ updateColorCameraTracking(): void; /** * Determines the colorization mode for rendering based on debug flags. * * @returns {Array<number[]>|undefined} Color array for debug visualization, or undefined for normal rendering */ getDebugColors(): Array<number[]> | undefined; /** * Calculates camera translation delta since last color update. * Updates and returns the shared _cameraDeltas object. * * @returns {{ translationDelta: number }} Shared camera movement deltas object */ calculateColorCameraDeltas(): { translationDelta: number; }; /** * Fires the frame:ready event with current sorting and loading state. */ fireFrameReadyEvent(): void; /** * Computes max world-space distance across all octree instances. Used for sqrt-based bucket * distribution in budget balancing. Non-octree placements are excluded since they have fixed * splat counts and don't participate in LOD-based budget balancing. * * @returns {number} Maximum world-space distance, minimum 1 to avoid division by zero. * @private */ private computeGlobalMaxDistance; /** * Enforces global splat budget across all octree instances using phased approach. * * @param {number} budget - Target splat budget from GSplatParams.splatBudget. * @private */ private _enforceBudget; /** * Detects if the work buffer format has been replaced (e.g. dataFormat changed) and * recreates the work buffer if needed. * * @private */ private handleFormatChange; update(): number; /** * Sorts the splats using GPU compute shaders * * @param {GSplatWorldState} worldState - The world state to sort. */ sortGpu(worldState: GSplatWorldState): void; /** * Runs frustum culling and interval compaction on the GPU, then passes the compacted * splat ID buffer directly to the local compute renderer (no key generation or radix sort). * * @param {GSplatWorldState} worldState - The world state to compact. * @private */ private compactGpu; /** * Allocates per-frame indirect draw and dispatch slots and runs writeIndirectArgs * for interval compaction. * * @param {number} numIntervals - Total interval count (index into prefix sum for visible count). * @private */ private allocateAndWriteIntervalIndirectArgs; /** * Generates sort keys and runs GPU radix sort using indirect dispatch * (sorting only the visible splat count determined by interval compaction). * * @param {number} elementCount - Total number of splats. * @param {number} roundedNumBits - Number of sort bits (rounded to multiple of 4). * @param {number} minDist - Minimum distance for key normalization. * @param {number} maxDist - Maximum distance for key normalization. * @param {StorageBuffer|null} compactedSplatIds - Compacted splat IDs from interval compaction. * @returns {StorageBuffer} The sorted indices buffer. * @private */ private dispatchGpuSort; /** * Applies GPU sort results to the renderer with indirect draw from interval compaction. * The sortedIndices buffer already contains actual splat IDs (single indirection) because * compactedSplatIds were fed as initial values to the radix sort. * * @param {GSplatWorldState} worldState - The world state being sorted. * @param {StorageBuffer} sortedIndices - Buffer containing sorted splat IDs. * @private */ private applyGpuSortResults; /** * Prepares frustum culling data: updates the GPU transform buffers and computes * frustum planes from the camera. The actual culling test runs inline in the * interval compaction compute shader. * * @param {GSplatWorldState} worldState - The world state whose splats provide transforms. * @private */ private _runFrustumCulling; /** * Refreshes indirect draw parameters on non-sort frames. * Allocates a new per-frame draw slot and re-runs writeIndirectArgs to write * draw args from the visibleCount that was established during the last sort. * Does NOT re-run compaction (the compacted buffer must stay stable). * * @private */ private refreshIndirectDraw; /** * Computes the min/max effective distances for the current world state. * * @param {GSplatWorldState} worldState - The world state. * @returns {{minDist: number, maxDist: number}} The distance range. */ computeDistanceRange(worldState: GSplatWorldState): { minDist: number; maxDist: number; }; /** * Sorts the splats using CPU worker (asynchronous). * * @param {GSplatWorldState} lastState - The last world state. */ sortCpu(lastState: GSplatWorldState): void; /** * Prepares sort parameters data for the sorter worker. * * @param {GSplatWorldState} worldState - The world state containing all needed data. * @returns {object} - Data for sorter worker. */ prepareSortParameters(worldState: GSplatWorldState): object; } import type { GraphicsDevice } from '../../platform/graphics/graphics-device.js'; import { GraphNode } from '../graph-node.js'; import { GSplatWorkBuffer } from './gsplat-work-buffer.js'; import type { GSplatRenderer } from './gsplat-renderer.js'; import { GSplatWorldState } from './gsplat-world-state.js'; import { GSplatUnifiedSorter } from './gsplat-unified-sorter.js'; import { GSplatSortKeyCompute } from './gsplat-sort-key-compute.js'; import { ComputeRadixSort } from '../graphics/compute-radix-sort.js'; import { GSplatIntervalCompaction } from './gsplat-interval-compaction.js'; import { Vec3 } from '../../core/math/vec3.js'; import { Mat4 } from '../../core/math/mat4.js'; import type { Scene } from '../scene.js'; import type { GSplatPlacement } from './gsplat-placement.js'; import { GSplatOctreeInstance } from './gsplat-octree-instance.js'; import type { GSplatDirector } from './gsplat-director.js'; import type { Layer } from '../layer.js';