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