playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
521 lines (520 loc) • 19.8 kB
TypeScript
/**
* GSplatManager manages the rendering of splats using a work buffer, where all active splats are
* stored and rendered from.
*
* Shared culling + compaction (raster GPU-sort path 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 — 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.
*/
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;
/**
* When true, {@link updateWorldState} must rebuild the splat set.
*
* @type {boolean}
* @private
*/
private _worldStateDirty;
/**
* CPU-based sorter (when not using GPU sorting).
*
* @type {GSplatUnifiedSorter|null}
*/
cpuSorter: GSplatUnifiedSorter | null;
/**
* GPU-based radix sorter (raster GPU sort path).
*
* @type {ComputeRadixSort|null}
*/
gpuSorter: ComputeRadixSort | null;
/**
* Interval-based GPU compaction (raster GPU sort / compute paths).
*
* @type {GSplatIntervalCompaction|null}
*/
intervalCompaction: GSplatIntervalCompaction | null;
/**
* Compute projector + sort keys for {@link GSPLAT_RENDERER_RASTER_GPU_SORT}.
*
* @type {GSplatProjector|null}
*/
projector: GSplatProjector | null;
/**
* Indirect draw slot index for the current frame (-1 when not using indirect draw).
*/
indirectDrawSlot: number;
/**
* Indirect dispatch slot for raster GPU sort (projector + radix) and compute paths.
* The compute local renderer builds its own indirect args in private buffers
* and does not use these slots.
*/
indirectDispatchSlot: number;
/**
* Total intervals from the last interval compaction dispatch. Needed for
* writeIndirectArgs to index into the prefix sum buffer for visible count.
*/
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).
*
* @private
*/
private _awaitingLodUpdate;
/**
* Cached work buffer format version for detecting extra stream changes.
*
* @private
*/
private _workBufferFormatVersion;
/**
* Flag set when the work buffer needs a full rebuild due to format changes.
*
* @private
*/
private _workBufferRebuildRequired;
/**
* Number of blocks uploaded to the work buffer this frame.
*/
bufferCopyUploaded: number;
/**
* Total number of blocks in the work buffer this frame.
*/
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).
*
* @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.
*
* @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.
*/
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 (radix sorter, projector, 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;
/**
* GPU radix sort + projector for hybrid raster (no separate sort-key compute pass).
*
* @private
*/
private initHybridSorting;
/**
* 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 a renderer-specific pick pipeline and returns the configured pick mesh instance.
* The local compute renderer renders to pick textures; the hybrid renderer refreshes its
* shared projector/sort buffers for the picker camera and returns a transient pick mesh.
*
* @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 renderer mode transitions (CPU raster, hybrid, compute)
* (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;
/**
* Hybrid GPU path: interval compaction, projector (keys + proj cache), indirect radix sort
* over projector keys (indices are dense in projCache), then hybrid raster bindings.
*
* @param {GSplatWorldState} worldState - The world state to sort.
*/
sortGpuHybrid(worldState: GSplatWorldState): void;
/**
* Runs the shared hybrid projector + indirect radix sort path for a specific camera.
*
* @param {GSplatWorldState} worldState - The world state to sort.
* @param {GraphNode} cameraNode - Camera node used for projection and sort keys.
* @param {number} viewportWidth - Projection viewport width in pixels.
* @param {number} viewportHeight - Projection viewport height in pixels.
* @param {number} alphaClip - Projector producer alpha threshold.
* @param {boolean} pickMode - Whether projector writes pcId into the cache.
* @returns {StorageBuffer|null} The sorted cache indices, or null if no work was dispatched.
* @private
*/
private sortGpuHybridForCamera;
/**
* 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;
/**
* Applies hybrid GPU sort results to the renderer with indirect draw from interval compaction.
*
* @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.
* @param {GraphNode} [cameraNode] - Camera node to cull against.
* @private
*/
private _runFrustumCulling;
/**
* Computes the min/max effective distances for the current world state.
*
* @param {GSplatWorldState} worldState - The world state.
* @param {GraphNode} [cameraNode] - Camera node to measure distances from.
* @returns {{minDist: number, maxDist: number}} The distance range.
*/
computeDistanceRange(worldState: GSplatWorldState, cameraNode?: GraphNode): {
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 { ComputeRadixSort } from '../graphics/radix-sort/compute-radix-sort.js';
import { GSplatIntervalCompaction } from './gsplat-interval-compaction.js';
import { GSplatProjector } from './gsplat-projector.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';