UNPKG

playcanvas

Version:

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

406 lines (405 loc) 18.1 kB
/** * Owns the gsplat "world": the work buffer, versioned world states, allocation, octree/LOD * evaluation, streaming, budget enforcement, and the work-buffer bake. A single primary camera * (passed in to {@link GSplatWorld#update} / {@link GSplatWorld#bake}) drives LOD and color. * * The dependency is one-way: {@link GSplatManager} reads world data through getters and drives * mutation through explicit methods. The world never calls into the renderer, sorters, interval * compaction, or projector — instead it writes results into caller-owned result objects, and the * manager reacts (e.g. rebinding the renderer's data source on a work-buffer recreation). * * @ignore */ export class GSplatWorld { /** * @param {GraphicsDevice} device - The graphics device. * @param {Scene} scene - The scene. */ constructor(device: GraphicsDevice, scene: Scene); /** @type {GraphicsDevice} */ _device: GraphicsDevice; /** @type {Scene} */ _scene: Scene; /** @type {GSplatWorkBuffer} */ _workBuffer: GSplatWorkBuffer; /** @type {Map<number, GSplatWorldState>} */ _worldStates: Map<number, GSplatWorldState>; /** @type {number} */ _lastWorldStateVersion: number; /** * The render-ready version: the version the work buffer is baked to and rendered from. Advanced * exclusively via {@link GSplatWorld#markSorted}. (Formerly `GSplatManager.sortedVersion`.) * * @type {number} */ _currentVersion: number; /** @type {boolean} */ _worldStateDirty: boolean; /** @type {number} */ _workBufferFormatVersion: number; /** @type {boolean} */ _workBufferRebuildRequired: boolean; /** @type {number} */ _bufferCopyUploaded: number; /** @type {number} */ _bufferCopyTotal: number; /** @type {GSplatPlacementStateTracker} */ _stateTracker: GSplatPlacementStateTracker; /** @type {number} */ _framesTillFullUpdate: number; /** * Latched request for a full LOD update, raised by the 10-frame cadence metronome and cleared * when fulfilled (when the back-pressure gate allows). Decouples "a full update is due" from * "we may run it this frame", so a tick deferred by back-pressure fires on the next available * frame rather than being lost until the next 10-frame mark. * * @type {boolean} */ _lodUpdateRequested: boolean; /** @type {Vec3} */ _lastLodCameraPos: Vec3; /** @type {Vec3} */ _lastLodCameraFwd: Vec3; /** @type {number} */ _lastLodCameraFov: number; /** @type {GSplatBudgetBalancer} */ _budgetBalancer: GSplatBudgetBalancer; /** @type {number} */ _budgetScale: number; /** @type {BlockAllocator} */ _allocator: BlockAllocator; /** @type {Map<number, MemBlock>} */ _allocationMap: Map<number, MemBlock>; /** @type {Vec3} */ _lastColorUpdateCameraPos: Vec3; /** @type {GSplatPlacement[]} */ _layerPlacements: GSplatPlacement[]; /** @type {boolean} */ _layerPlacementsDirty: boolean; /** @type {boolean} */ _placementSetChanged: boolean; /** @type {Map<GSplatPlacement, GSplatOctreeInstance>} */ _octreeInstances: Map<GSplatPlacement, GSplatOctreeInstance>; /** @type {GSplatOctreeInstance[]} */ _octreeInstancesToDestroy: GSplatOctreeInstance[]; /** @type {boolean} */ _hasNewOctreeInstances: boolean; /** * Suppresses ready=true in frame:ready until a fullUpdate cycle runs. Only set when octree * instances exist and params change (dirty). * * @type {boolean} */ _awaitingLodUpdate: boolean; destroy(): void; /** @type {GSplatWorkBuffer} */ get workBuffer(): GSplatWorkBuffer; /** @type {number} */ get currentVersion(): number; /** @type {number} */ get lastWorldStateVersion(): number; /** @type {number} */ get bufferCopyUploaded(): number; /** @type {number} */ get bufferCopyTotal(): number; /** @type {boolean} */ get awaitingLodUpdate(): boolean; /** @type {boolean} */ get hasOctreeInstances(): boolean; /** * Total pending loads across all octree instances (including environment). * * @type {number} */ get pendingLoadCount(): number; /** * The render-ready world state, or undefined if not yet created. * * @type {GSplatWorldState|undefined} */ get currentState(): GSplatWorldState | undefined; /** * Looks up a world state by version. * * @param {number} version - The world state version. * @returns {GSplatWorldState|undefined} The world state, or undefined. */ getState(version: number): GSplatWorldState | undefined; /** * True when frustum culling can run for the given renderer (bounds data available). The renderer * type gate stays on the manager; this only reports bounds availability. * * @type {boolean} */ get hasBounds(): boolean; /** * Resets per-frame buffer-copy stats. Must run before any bake/rebuild of the frame (including * the CPU sorter's async onSorted path, which the manager applies before {@link GSplatWorld#update}). */ resetFrameStats(): void; /** * Marks the world state and/or work buffer as needing a rebuild. Called by the manager on * renderer-mode transitions (the manager must not poke the private flags directly). * * @param {object} [opts] - Options. * @param {boolean} [opts.worldState] - Force a world-state rebuild. * @param {boolean} [opts.workBuffer] - Force a full work-buffer rebuild. */ invalidate({ worldState, workBuffer }?: { worldState?: boolean; workBuffer?: boolean; }): void; /** * Resets the render-ready state's sort bookkeeping so the next sort triggers a full rebuild * (used when switching to the CPU-sort renderer). Returns the state's splats for the manager to * feed its CPU sorter, or null if no state exists. * * @returns {GSplatInfo[]|null} The current state's splats, or null. */ invalidateSortState(): GSplatInfo[] | null; /** * Detects work-buffer format changes and recreates / syncs the work buffer. Must run before the * CPU sorter's pending results are applied (so onSorted rebuilds into the current buffer). * * @param {{ bufferRecreated: boolean, sortNeeded: boolean }} result - Caller-owned result object. * @returns {{ bufferRecreated: boolean, sortNeeded: boolean }} The populated result. */ syncFormat(result: { bufferRecreated: boolean; sortNeeded: boolean; }): { bufferRecreated: boolean; sortNeeded: boolean; }; /** * Supply the placements to use. Updates octree instances and the non-octree placement list, * flagging dirtiness. Called infrequently (when the layer's placements change). * * @param {GSplatPlacement[]} placements - The placements to reconcile with. */ reconcile(placements: GSplatPlacement[]): void; /** * Per-frame LOD/streaming pass: evaluates LOD against the primary camera (subject to the * back-pressure gate), enforces budget, and creates a new world-state version when needed. * * @param {GraphNode} camera - The primary camera driving LOD/streaming. * @param {boolean} allowLodUpdate - Back-pressure gate (false when the CPU sorter is busy). * @param {boolean} requireCenters - Whether resources without a centers buffer must be skipped * (CPU sort path). * @param {{ newVersion: boolean, overdrawDirty: boolean, sortNeeded: boolean }} result - * Caller-owned result object the manager reacts to. * @returns {{ newVersion: boolean, overdrawDirty: boolean, sortNeeded: boolean }} The populated result. */ update(camera: GraphNode, allowLodUpdate: boolean, requireCenters: boolean, result: { newVersion: boolean; overdrawDirty: boolean; sortNeeded: boolean; }): { newVersion: boolean; overdrawDirty: boolean; sortNeeded: boolean; }; /** * Creates a new world state version when placements/resources changed. Returns whether a new * version was created. Does NOT feed the CPU sorter (the manager does that on a new version). * * @param {boolean} requireCenters - Whether resources without centers must be skipped. * @returns {boolean} True if a new world-state version was created. * @private */ private _updateWorldState; /** * Advances the render-ready version to `version` (cleaning up older states) and, on the first * sort of that version, rebuilds the work buffer. The manager calls this from the GPU sort * paths and the CPU onSorted callback. Atomic: cleanup + version-advance happen together. * * @param {number} version - The version that has been sorted. * @param {number} count - The splat count for the work-buffer rebuild / renderer update. * @param {GraphNode} camera - The primary camera (for color bake). * @param {boolean} updateBounds - Whether to upload frustum-culling bounds (false for CPU sort). * @param {{ rebuilt: boolean, count: number, textureSize: number }} result - Caller-owned result. * @returns {{ rebuilt: boolean, count: number, textureSize: number }} The populated result. When * `rebuilt` is true the manager must call `renderer.update(count, textureSize)`. */ markSorted(version: number, count: number, camera: GraphNode, updateBounds: boolean, result: { rebuilt: boolean; count: number; textureSize: number; }): { rebuilt: boolean; count: number; textureSize: number; }; /** * Applies a completed CPU sort: advances the render-ready version (via markSorted) and uploads * the sorted order texture. The manager rebinds the renderer afterwards. * * @param {number} version - The sorted version. * @param {number} count - The sorted splat count. * @param {Uint32Array} orderData - The sorted order data. * @param {GraphNode} camera - The primary camera (for color bake on first sort). * @param {boolean} updateBounds - Whether to upload frustum-culling bounds (false for CPU sort). * @param {{ rebuilt: boolean, count: number, textureSize: number }} result - Caller-owned result. * @returns {{ rebuilt: boolean, count: number, textureSize: number }} The populated result. */ onSorted(version: number, count: number, orderData: Uint32Array, camera: GraphNode, updateBounds: boolean, result: { rebuilt: boolean; count: number; textureSize: number; }): { rebuilt: boolean; count: number; textureSize: number; }; /** * Materializes the work buffer for the given (render-ready) version: a full rebuild when one is * pending, otherwise an incremental update. Refreshes color tracking. Camera drives the SH color * bake. * * @param {number} version - The render-ready version to bake. * @param {GraphNode} camera - The primary camera (for color bake). * @param {boolean} updateBounds - Whether to upload frustum-culling bounds (false for CPU sort). * @param {{ rebuilt: boolean, count: number, textureSize: number, sortNeeded: boolean }} result - * Caller-owned result. When `rebuilt` is true the manager must call `renderer.update(count, * textureSize)`, `renderer.setOrderData()` and `intervalCompaction.invalidateUpload()`. When * `sortNeeded` is true (a splat moved during the incremental update) the manager must re-sort. * @returns {{ rebuilt: boolean, count: number, textureSize: number, sortNeeded: boolean }} The populated result. */ bake(version: number, camera: GraphNode, updateBounds: boolean, result: { rebuilt: boolean; count: number; textureSize: number; sortNeeded: boolean; }): { rebuilt: boolean; count: number; textureSize: number; sortNeeded: boolean; }; /** * Rebuilds the work buffer for a world state: resizes if needed, renders changed (or all) splats, * syncs transforms, and applies pending file-release requests. Does NOT touch the renderer — the * caller updates the renderer's count/textureSize from {@link GSplatWorld#bake} / * {@link GSplatWorld#markSorted} results. * * @param {GSplatWorldState} worldState - The world state to rebuild for. * @param {number} count - The number of splats (unused here; surfaced via the result for the renderer). * @param {boolean} forceFullRebuild - Force rendering all splats (e.g. format change). * @param {GraphNode} camera - The primary camera (for color bake). * @param {boolean} updateBounds - Whether to upload bounds/transforms for frustum culling. False * for the CPU-sort renderer, whose frustum-culler storage buffers are not allocated. * @private */ private rebuildWorkBuffer; /** * Cleans up old world states between the last render-ready version and the new version. Merges * upload requirements from skipped states into the active state, then decrements ref counts and * destroys old states. Note: reads the current `_currentVersion` (not yet advanced to newVersion). * * @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. Reports whether any splat moved (the * manager uses this to set sortNeeded). * * @param {GSplatWorldState} state - The world state to update. * @param {GraphNode} camera - The primary camera (for color delta + bake). * @returns {boolean} True if any splat moved (requires re-sort). */ applyWorkBufferUpdates(state: GSplatWorldState, camera: GraphNode): boolean; /** * Tests if the camera has moved or rotated enough to require LOD update. * * @param {GraphNode} camera - The primary camera. * @returns {boolean} True if camera moved/rotated over thresholds, otherwise false. */ testCameraMovedForLod(camera: GraphNode): boolean; /** * Updates the camera tracking state for color accumulation calculations. * * @param {GraphNode} camera - The primary camera. */ updateColorCameraTracking(camera: GraphNode): 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. * * @param {GraphNode} camera - The primary camera. * @returns {{ translationDelta: number }} Shared camera movement deltas object. */ calculateColorCameraDeltas(camera: GraphNode): { translationDelta: number; }; /** * Computes max world-space distance across all octree instances. Used for sqrt-based bucket * distribution in budget balancing. * * @param {GraphNode} camera - The primary camera. * @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 a phased approach. * * @param {number} budget - Target splat budget from GSplatParams.splatBudget. * @param {GraphNode} camera - The primary camera. * @private */ private _enforceBudget; /** * Computes the world-space union of all placement AABBs. Returns the shared bounding box, or * null if there are no placements. The manager applies it to the renderer's mesh instance. * * @returns {BoundingBox|null} The aggregate AABB, or null. */ computeAggregateAabb(): BoundingBox | null; /** * Accumulates a placement's transformed AABB into the running mesh-instance AABB. * * @param {GSplatPlacement} placement - The placement. * @param {boolean} initialized - Whether the running AABB has been initialized. * @returns {boolean} The updated initialized flag. * @private */ private _accumulatePlacementAabb; /** * Ticks octree cooldown timers once per frame per unique octree. */ tickCooldowns(): void; /** * Forces LOD re-evaluation on all octree instances (e.g. after frame:ready listeners changed * params). */ markInstancesNeedLodUpdate(): void; /** * Prepares sort parameters data for the sorter worker. Reads world-state data; the manager owns * the sorter and posts the result. * * @param {GSplatWorldState} worldState - The world state containing all needed data. * @returns {object} Data for the sorter worker. */ prepareSortParameters(worldState: GSplatWorldState): object; } import type { GraphicsDevice } from '../../platform/graphics/graphics-device.js'; import type { Scene } from '../scene.js'; import { GSplatWorkBuffer } from './gsplat-work-buffer.js'; import { GSplatWorldState } from './gsplat-world-state.js'; import { GSplatPlacementStateTracker } from './gsplat-placement-state-tracker.js'; import { Vec3 } from '../../core/math/vec3.js'; import { GSplatBudgetBalancer } from './gsplat-budget-balancer.js'; import { BlockAllocator } from '../../core/block-allocator.js'; import type { MemBlock } from '../../core/block-allocator.js'; import type { GSplatPlacement } from './gsplat-placement.js'; import { GSplatOctreeInstance } from './gsplat-octree-instance.js'; import { GSplatInfo } from './gsplat-info.js'; import type { GraphNode } from '../graph-node.js'; import { BoundingBox } from '../../core/shape/bounding-box.js';