playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
549 lines (548 loc) • 20.6 kB
TypeScript
/**
* 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';