UNPKG

playcanvas

Version:

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

521 lines (520 loc) 19.8 kB
/** * 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';