UNPKG

playcanvas

Version:

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

1,010 lines (1,007 loc) 39.1 kB
import { math } from '../../core/math/math.js'; import { Mat4 } from '../../core/math/mat4.js'; import { Vec3 } from '../../core/math/vec3.js'; import { GraphNode } from '../graph-node.js'; import { GSplatInfo } from './gsplat-info.js'; import { GSplatUnifiedSorter } from './gsplat-unified-sorter.js'; import { GSplatWorkBuffer } from './gsplat-work-buffer.js'; import { GSplatQuadRenderer } from './gsplat-quad-renderer.js'; import { GSplatComputeLocalRenderer } from './gsplat-compute-local-renderer.js'; import { GSplatOctreeInstance } from './gsplat-octree-instance.js'; import { GSplatOctreeResource } from './gsplat-octree.resource.js'; import { GSplatWorldState } from './gsplat-world-state.js'; import { GSplatPlacementStateTracker } from './gsplat-placement-state-tracker.js'; import { GSplatSortKeyCompute } from './gsplat-sort-key-compute.js'; import { GSplatIntervalCompaction } from './gsplat-interval-compaction.js'; import { ComputeRadixSort } from '../graphics/compute-radix-sort.js'; import { GSPLAT_RENDERER_COMPUTE, GSPLAT_RENDERER_RASTER_CPU_SORT, GSPLAT_RENDERER_RASTER_GPU_SORT, GSPLAT_DEBUG_SH_UPDATE, GSPLAT_DEBUG_LOD } from '../constants.js'; import { Color } from '../../core/math/color.js'; import { GSplatBudgetBalancer } from './gsplat-budget-balancer.js'; import { BlockAllocator } from '../../core/block-allocator.js'; const cameraPosition = new Vec3(); const cameraDirection = new Vec3(); const translation = new Vec3(); const _tempVec3 = new Vec3(); const invModelMat = new Mat4(); const tempNonOctreePlacements = new Set(); const tempOctreePlacements = new Set(); const _updatedSplats = []; const _splatsWithSH = []; const _changedColorAllocIds = new Set(); const _cameraDeltas = { translationDelta: 0 }; const _localCamPos = new Vec3(); const _closestPt = new Vec3(); const tempOctreesTicked = new Set(); const _queuedSplats = new Set(); const _lodColorsRaw = [ [ 1, 0, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ], [ 1, 1, 0 ], [ 1, 0, 1 ], [ 0, 1, 1 ], [ 1, 0.5, 0 ], [ 0.5, 0, 1 ] ]; [ new Color(1, 0, 0), new Color(0, 1, 0), new Color(0, 0, 1), new Color(1, 1, 0), new Color(1, 0, 1), new Color(0, 1, 1), new Color(1, 0.5, 0), new Color(0.5, 0, 1) ]; let _randomColorRaw = null; class GSplatManager { destroy() { this._destroyed = true; for (const [, worldState] of this.worldStates){ for (const splat of worldState.splats){ splat.resource.decRefCount(); } worldState.destroy(); } this.worldStates.clear(); for (const [, instance] of this.octreeInstances){ instance.destroy(); } this.octreeInstances.clear(); for (const instance of this.octreeInstancesToDestroy){ instance.destroy(); } this.octreeInstancesToDestroy.length = 0; this.destroyGpuSorting(); this.destroyCpuSorting(); this.workBuffer.destroy(); this.renderer.destroy(); } destroyGpuSorting() { this.keyGenerator?.destroy(); this.keyGenerator = null; this.gpuSorter?.destroy(); this.gpuSorter = null; const useCpuSort = false; this.renderer.setCpuSortedRendering(); this.destroyIntervalCompaction(useCpuSort); } destroyIntervalCompaction(useCpuSort = true) { if (this.intervalCompaction) { if (useCpuSort) { this.renderer.setCpuSortedRendering(); } this.intervalCompaction.destroy(); this.intervalCompaction = null; } } destroyCpuSorting() { this.cpuSorter?.destroy(); this.cpuSorter = null; } initGpuSorting() { if (!this.keyGenerator) { this.keyGenerator = new GSplatSortKeyCompute(this.device); } if (!this.gpuSorter) { this.gpuSorter = new ComputeRadixSort(this.device); } } initCpuSorting() { if (!this.cpuSorter) { this.cpuSorter = this.createSorter(); } const currentState = this.worldStates.get(this.sortedVersion); if (currentState) { currentState.sortParametersSet = false; currentState.sortedBefore = false; this.cpuSorter.updateCentersForSplats(currentState.splats); } this.renderer.setCpuSortedRendering(); } get material() { return this.renderer.material; } prepareForPicking(camera, width, height) { if (this.activeRenderer !== GSPLAT_RENDERER_COMPUTE) return null; const localRenderer = this.renderer; return localRenderer.dispatchPick(camera, width, height); } createSorter() { const sorter = new GSplatUnifiedSorter(this.scene); sorter.on('sorted', (count, version, orderData)=>{ this.onSorted(count, version, orderData); }); return sorter; } setRenderMode(renderMode) { this.renderMode = renderMode; this.renderer.setRenderMode(renderMode); } get canCull() { return this.activeRenderer !== GSPLAT_RENDERER_RASTER_CPU_SORT && this.workBuffer.frustumCuller.totalBoundsEntries > 0; } _createRenderer(mode) { if (mode === GSPLAT_RENDERER_COMPUTE) { this.renderer = new GSplatComputeLocalRenderer(this.device, this.node, this.cameraNode, this.layer, this.workBuffer); } else { this.renderer = new GSplatQuadRenderer(this.device, this.node, this.cameraNode, this.layer, this.workBuffer); if (mode === GSPLAT_RENDERER_RASTER_GPU_SORT) { this.initGpuSorting(); } else { this.initCpuSorting(); } } this.activeRenderer = mode; } prepareRendererMode() { const requested = this.scene.gsplat.currentRenderer; if (requested === this.activeRenderer) return; this.destroyGpuSorting(); this.destroyCpuSorting(); this.renderer.destroy(); this._createRenderer(requested); this.renderer.setRenderMode(this.renderMode); this._workBufferRebuildRequired = true; this.sortNeeded = true; } reconcile(placements) { tempNonOctreePlacements.clear(); for (const p of placements){ if (p.resource instanceof GSplatOctreeResource) { if (!this.octreeInstances.has(p)) { this.octreeInstances.set(p, new GSplatOctreeInstance(this.device, p.resource.octree, p)); this.hasNewOctreeInstances = true; } tempOctreePlacements.add(p); } else { tempNonOctreePlacements.add(p); } } for (const [placement, inst] of this.octreeInstances){ if (!tempOctreePlacements.has(placement)) { this.octreeInstances.delete(placement); this.layerPlacementsDirty = true; this._placementSetChanged = true; this.octreeInstancesToDestroy.push(inst); } } this.layerPlacementsDirty || (this.layerPlacementsDirty = this.layerPlacements.length !== tempNonOctreePlacements.size); if (!this.layerPlacementsDirty) { for(let i = 0; i < this.layerPlacements.length; i++){ const existing = this.layerPlacements[i]; if (!tempNonOctreePlacements.has(existing)) { this.layerPlacementsDirty = true; break; } } } this._placementSetChanged || (this._placementSetChanged = this.layerPlacementsDirty); this.layerPlacements.length = 0; for (const p of tempNonOctreePlacements){ this.layerPlacements.push(p); } tempNonOctreePlacements.clear(); tempOctreePlacements.clear(); } updateWorldState() { let stateChanged = this._stateTracker.hasChanges(this.layerPlacements); for (const [, inst] of this.octreeInstances){ if (this._stateTracker.hasChanges(inst.activePlacements)) { stateChanged = true; } } const placementsChanged = this.layerPlacementsDirty; const worldChanged = placementsChanged || stateChanged || this.worldStates.size === 0; if (worldChanged) { this.lastWorldStateVersion++; const splats = []; for (const p of this.layerPlacements){ p.ensureInstanceStreams(this.device); const splatInfo = new GSplatInfo(this.device, p.resource, p, p.consumeRenderDirty.bind(p)); splats.push(splatInfo); } for (const [, inst] of this.octreeInstances){ inst.activePlacements.forEach((p)=>{ if (p.resource) { p.ensureInstanceStreams(this.device); const octreeNodes = p.intervals.size > 0 ? inst.octree.nodes : null; const nodeInfos = octreeNodes ? inst.nodeInfos : null; const splatInfo = new GSplatInfo(this.device, p.resource, p, p.consumeRenderDirty.bind(p), octreeNodes, nodeInfos); splats.push(splatInfo); } }); } if (this.cpuSorter) { for (const splat of splats){ const resource = splat.resource; const lastVersion = this._centersVersions.get(resource.id); if (lastVersion !== resource.centersVersion) { this._centersVersions.set(resource.id, resource.centersVersion); this.cpuSorter.setCenters(resource.id, null); this.cpuSorter.setCenters(resource.id, resource.centers); } } } this.cpuSorter?.updateCentersForSplats(splats); const newState = new GSplatWorldState(this.device, this.lastWorldStateVersion, splats, this._allocator, this._allocationMap); for (const splat of newState.splats){ splat.resource.incRefCount(); } for (const [, inst] of this.octreeInstances){ if (inst.removedCandidates && inst.removedCandidates.size) { for (const fileIndex of inst.removedCandidates){ newState.pendingReleases.push([ inst.octree, fileIndex ]); } inst.removedCandidates.clear(); } } if (this.octreeInstancesToDestroy.length) { for (const inst of this.octreeInstancesToDestroy){ if (inst.removedCandidates && inst.removedCandidates.size) { for (const fileIndex of inst.removedCandidates){ newState.pendingReleases.push([ inst.octree, fileIndex ]); } inst.removedCandidates.clear(); } const toRelease = inst.getFileDecrements(); for (const fileIndex of toRelease){ newState.pendingReleases.push([ inst.octree, fileIndex ]); } inst.destroy(true); } this.octreeInstancesToDestroy.length = 0; } if (this._placementSetChanged) { newState.fullRebuild = true; } this.worldStates.set(this.lastWorldStateVersion, newState); this.layerPlacementsDirty = false; this._placementSetChanged = false; this.sortNeeded = true; } } onSorted(count, version, orderData) { this.cleanupOldWorldStates(version); this.sortedVersion = version; const worldState = this.worldStates.get(version); if (worldState) { if (!worldState.sortedBefore) { worldState.sortedBefore = true; this.rebuildWorkBuffer(worldState, count); } this.workBuffer.setOrderData(orderData); this.renderer.setOrderData(); } } rebuildWorkBuffer(worldState, count, forceFullRebuild = false) { const textureSize = worldState.textureSize; if (textureSize !== this.workBuffer.textureSize) { this.workBuffer.resize(textureSize); } if (this.activeRenderer !== GSPLAT_RENDERER_RASTER_CPU_SORT) { this.workBuffer.frustumCuller.updateBoundsData(worldState.boundsGroups); this.workBuffer.frustumCuller.updateTransformsData(worldState.boundsGroups); } const renderAll = forceFullRebuild || worldState.fullRebuild; const splatsToRender = renderAll ? worldState.splats : worldState.needsUpload; const changedAllocIds = renderAll ? null : worldState.needsUploadIds; if (splatsToRender.length > 0) { const totalBlocks = this._allocationMap.size; const uploadBlocks = renderAll ? totalBlocks : worldState.needsUploadIds.size; this.bufferCopyUploaded += uploadBlocks; this.bufferCopyTotal = totalBlocks; this.workBuffer.render(splatsToRender, this.cameraNode, this.getDebugColors(), changedAllocIds); } for(let i = 0; i < worldState.splats.length; i++){ worldState.splats[i].update(); } this.updateColorCameraTracking(); if (worldState.pendingReleases && worldState.pendingReleases.length) { const cooldownTicks = this.scene.gsplat.cooldownTicks; for (const [octree, fileIndex] of worldState.pendingReleases){ octree.decRefCount(fileIndex, cooldownTicks); } worldState.pendingReleases.length = 0; } this.renderer.update(count, textureSize); } cleanupOldWorldStates(newVersion) { const activeState = this.worldStates.get(newVersion); if (!activeState.fullRebuild) { for(let v = this.sortedVersion + 1; v < newVersion; v++){ if (this.worldStates.get(v)?.fullRebuild) { activeState.fullRebuild = true; break; } } } if (!activeState.fullRebuild) { const activeIds = activeState.needsUploadIds; const lookup = activeState.allocIdToSplat; for(let v = this.sortedVersion + 1; v < newVersion; v++){ const oldState = this.worldStates.get(v); if (oldState) { for (const allocId of oldState.needsUploadIds){ if (!activeIds.has(allocId)) { activeIds.add(allocId); const splat = lookup.get(allocId); if (splat && !_queuedSplats.has(splat)) { activeState.needsUpload.push(splat); _queuedSplats.add(splat); } } } } } _queuedSplats.clear(); } for(let v = this.sortedVersion; v < newVersion; v++){ const oldState = this.worldStates.get(v); if (oldState) { for (const splat of oldState.splats){ splat.resource.decRefCount(); } this.worldStates.delete(v); oldState.destroy(); } } } applyWorkBufferUpdates(state) { const { colorUpdateAngle } = this.scene.gsplat; const ratio = Math.tan(colorUpdateAngle * math.DEG_TO_RAD); const cameraPos = this.cameraNode.getPosition(); const { translationDelta } = this.calculateColorCameraDeltas(); const hasCameraMovement = translationDelta > 0; let uploadedBlocks = 0; state.splats.forEach((splat)=>{ if (splat.update()) { _updatedSplats.push(splat); uploadedBlocks += splat.intervalAllocIds.length; if (splat.nodeInfos) { for (const ni of splat.intervalNodeIndices){ splat.nodeInfos[ni].colorAccumulatedTranslation = 0; } } else { splat.colorAccumulatedTranslation = 0; } this.sortNeeded = true; } else if (hasCameraMovement && splat.hasSphericalHarmonics) { _splatsWithSH.push(splat); if (splat.nodeInfos) { const nodeIndices = splat.intervalNodeIndices; for(let j = 0; j < nodeIndices.length; j++){ const nodeInfo = splat.nodeInfos[nodeIndices[j]]; nodeInfo.colorAccumulatedTranslation += translationDelta; const threshold = ratio * Math.max(1, nodeInfo.worldDistance); if (nodeInfo.colorAccumulatedTranslation >= threshold) { _changedColorAllocIds.add(splat.intervalAllocIds[j]); nodeInfo.colorAccumulatedTranslation = 0; uploadedBlocks++; } } } else { splat.colorAccumulatedTranslation += translationDelta; invModelMat.copy(splat.node.getWorldTransform()).invert(); invModelMat.transformPoint(cameraPos, _localCamPos); splat.aabb.closestPoint(_localCamPos, _closestPt); const dist = _localCamPos.distance(_closestPt) * splat.node.getWorldTransform().getScale().x; const threshold = ratio * Math.max(1, dist); if (splat.colorAccumulatedTranslation >= threshold) { _changedColorAllocIds.add(splat.allocId); uploadedBlocks += splat.intervalAllocIds.length; splat.colorAccumulatedTranslation = 0; } } } }); this.bufferCopyUploaded += uploadedBlocks; this.bufferCopyTotal = this._allocationMap.size; if (_updatedSplats.length > 0) { this.workBuffer.render(_updatedSplats, this.cameraNode, this.getDebugColors()); _updatedSplats.length = 0; } if (_changedColorAllocIds.size > 0) { this.workBuffer.renderColor(_splatsWithSH, this.cameraNode, this.getDebugColors(), _changedColorAllocIds); _changedColorAllocIds.clear(); } _splatsWithSH.length = 0; } testCameraMovedForLod() { const distanceThreshold = this.scene.gsplat.lodUpdateDistance; const currentCameraPos = this.cameraNode.getPosition(); const cameraMoved = this.lastLodCameraPos.distance(currentCameraPos) > distanceThreshold; if (cameraMoved) { return true; } let cameraRotated = false; const lodUpdateAngleDeg = this.scene.gsplat.lodUpdateAngle; if (lodUpdateAngleDeg > 0) { if (Number.isFinite(this.lastLodCameraFwd.x)) { const currentCameraFwd = this.cameraNode.forward; const dot = Math.min(1, Math.max(-1, this.lastLodCameraFwd.dot(currentCameraFwd))); const angle = Math.acos(dot); const rotThreshold = lodUpdateAngleDeg * math.DEG_TO_RAD; cameraRotated = angle > rotThreshold; } else { cameraRotated = true; } } const currentFov = this.cameraNode.camera.fov; const fovChanged = this.lastLodCameraFov < 0 || Math.abs(currentFov - this.lastLodCameraFov) > this.lastLodCameraFov * 0.02; return cameraMoved || cameraRotated || fovChanged; } testCameraMovedForSort() { const epsilon = 0.001; if (this.scene.gsplat.radialSorting) { const currentCameraPos = this.cameraNode.getPosition(); return this.lastSortCameraPos.distance(currentCameraPos) > epsilon; } if (Number.isFinite(this.lastSortCameraFwd.x)) { const currentCameraFwd = this.cameraNode.forward; const dot = Math.min(1, Math.max(-1, this.lastSortCameraFwd.dot(currentCameraFwd))); return Math.acos(dot) > epsilon; } return true; } testFrustumChanged() { const epsilon = 0.001; if (!this.lastCullingProjMat.equals(this.cameraNode.camera.projectionMatrix)) { return true; } const currentCameraFwd = this.cameraNode.forward; const dot = Math.min(1, Math.max(-1, this.lastCullingCameraFwd.dot(currentCameraFwd))); return Math.acos(dot) > epsilon; } updateColorCameraTracking() { this.lastColorUpdateCameraPos.copy(this.cameraNode.getPosition()); } getDebugColors() { const debug = this.scene.gsplat.debug; if (debug === GSPLAT_DEBUG_SH_UPDATE) { _randomColorRaw ?? (_randomColorRaw = []); const r = Math.random(); const g = Math.random(); const b = Math.random(); for(let i = 0; i < _lodColorsRaw.length; i++){ var _randomColorRaw1, _i; (_randomColorRaw1 = _randomColorRaw)[_i = i] ?? (_randomColorRaw1[_i] = [ 0, 0, 0 ]); _randomColorRaw[i][0] = r; _randomColorRaw[i][1] = g; _randomColorRaw[i][2] = b; } return _randomColorRaw; } else if (debug === GSPLAT_DEBUG_LOD) { return _lodColorsRaw; } return undefined; } calculateColorCameraDeltas() { _cameraDeltas.translationDelta = 0; if (isFinite(this.lastColorUpdateCameraPos.x)) { const currentCameraPos = this.cameraNode.getPosition(); _cameraDeltas.translationDelta = this.lastColorUpdateCameraPos.distance(currentCameraPos); } return _cameraDeltas; } fireFrameReadyEvent() { const ready = this.sortedVersion === this.lastWorldStateVersion && !this._awaitingLodUpdate; let loadingCount = 0; for (const [, inst] of this.octreeInstances){ loadingCount += inst.pendingLoadCount; } this.director.eventHandler.fire('frame:ready', this.cameraNode.camera, this.renderer.layer, ready, loadingCount); } computeGlobalMaxDistance() { let maxDist = 0; cameraPosition.copy(this.cameraNode.getPosition()); for (const [, inst] of this.octreeInstances){ const worldTransform = inst.placement.node.getWorldTransform(); const aabb = inst.placement.aabb; worldTransform.transformPoint(aabb.center, _tempVec3); const scale = worldTransform.getScale().x; const dist = _tempVec3.distance(cameraPosition) + aabb.halfExtents.length() * scale; if (dist > maxDist) maxDist = dist; } return Math.max(maxDist, 1); } _enforceBudget(budget) { const textureWidth = this.workBuffer.textureSize; let fixedSplats = 0; let paddingEstimate = 0; for (const p of this.layerPlacements){ const resource = p.resource; if (resource) { const numSplats = resource.numSplats ?? 0; fixedSplats += numSplats; paddingEstimate += (textureWidth - numSplats % textureWidth) % textureWidth; } } const octreeBudget = Math.max(1, budget - fixedSplats); const globalMaxDistance = this.computeGlobalMaxDistance(); let totalOptimalSplats = 0; for (const [, inst] of this.octreeInstances){ totalOptimalSplats += inst.evaluateOptimalLods(this.cameraNode, this.scene.gsplat, this._budgetScale); for (const placement of inst.activePlacements){ const resource = placement.resource; const numSplats = resource?.numSplats ?? 0; paddingEstimate += (textureWidth - numSplats % textureWidth) % textureWidth; } } const adjustedBudget = Math.max(1, octreeBudget - paddingEstimate); if (totalOptimalSplats > 0) { const ratio = totalOptimalSplats / adjustedBudget; const budgetScaleDeadZone = 0.4; const budgetScaleBlendRate = 0.3; if (ratio > 1 + budgetScaleDeadZone || ratio < 1 - budgetScaleDeadZone) { const invCorrection = 1 / Math.sqrt(ratio); this._budgetScale *= 1 + (invCorrection - 1) * budgetScaleBlendRate; this._budgetScale = Math.max(0.01, Math.min(this._budgetScale, 100.0)); } } this._budgetBalancer.balance(this.octreeInstances, adjustedBudget, globalMaxDistance); for (const [, inst] of this.octreeInstances){ const maxLod = inst.octree.lodLevels - 1; inst.applyLodChanges(maxLod, this.scene.gsplat); } } handleFormatChange() { const currentFormat = this.scene.gsplat.format; if (this.workBuffer.format !== currentFormat) { this.workBuffer.destroy(); this.workBuffer = new GSplatWorkBuffer(this.device, currentFormat); this.renderer.setDataSource(this.workBuffer); this._workBufferFormatVersion = this.workBuffer.format.extraStreamsVersion; this._workBufferRebuildRequired = true; this.sortNeeded = true; } } update() { this.bufferCopyUploaded = 0; this.bufferCopyTotal = 0; this.handleFormatChange(); const wbFormatVersion = this.workBuffer.format.extraStreamsVersion; if (this._workBufferFormatVersion !== wbFormatVersion) { this._workBufferFormatVersion = wbFormatVersion; this.workBuffer.syncWithFormat(); this._workBufferRebuildRequired = true; this.sortNeeded = true; } this.prepareRendererMode(); if (this.cpuSorter) { this.cpuSorter.applyPendingSorted(); } const sorterAvailable = this.activeRenderer !== GSPLAT_RENDERER_RASTER_CPU_SORT || this.cpuSorter && this.cpuSorter.jobsInFlight < 3; let fullUpdate = false; this.framesTillFullUpdate--; if (this.framesTillFullUpdate <= 0) { this.framesTillFullUpdate = 10; if (sorterAvailable) { fullUpdate = true; } } const hasNewInstances = this.hasNewOctreeInstances && sorterAvailable; if (hasNewInstances) this.hasNewOctreeInstances = false; let anyInstanceNeedsLodUpdate = false; let anyOctreeMoved = false; let cameraMovedOrRotatedForLod = false; if (fullUpdate) { for (const [, inst] of this.octreeInstances){ const isDirty = inst.update(); this.layerPlacementsDirty || (this.layerPlacementsDirty = isDirty); this._placementSetChanged || (this._placementSetChanged = inst.consumePlacementSetChanged()); const instNeeds = inst.consumeNeedsLodUpdate(); anyInstanceNeedsLodUpdate || (anyInstanceNeedsLodUpdate = instNeeds); } const threshold = this.scene.gsplat.lodUpdateDistance; for (const [, inst] of this.octreeInstances){ const moved = inst.testMoved(threshold); anyOctreeMoved || (anyOctreeMoved = moved); } cameraMovedOrRotatedForLod = this.testCameraMovedForLod(); this._awaitingLodUpdate = false; } if (this.testCameraMovedForSort()) { this.sortNeeded = true; } if (this.intervalCompaction && !this.sortNeeded && this.testFrustumChanged()) { this.lastCullingCameraFwd.copy(this.cameraNode.forward); this.lastCullingProjMat.copy(this.cameraNode.camera.projectionMatrix); this.sortNeeded = true; } if (this.scene.gsplat.dirty) { this.layerPlacementsDirty = true; this.renderer.updateOverdrawMode(this.scene.gsplat); this._workBufferRebuildRequired = true; this.sortNeeded = true; if (this.octreeInstances.size > 0) { this._awaitingLodUpdate = true; } } if (cameraMovedOrRotatedForLod || anyOctreeMoved || this.scene.gsplat.dirty || anyInstanceNeedsLodUpdate || hasNewInstances) { for (const [, inst] of this.octreeInstances){ inst.updateMoved(); } const cameraNode = this.cameraNode; this.lastLodCameraPos.copy(cameraNode.getPosition()); this.lastLodCameraFwd.copy(cameraNode.forward); this.lastLodCameraFov = cameraNode.camera.fov; const budget = this.scene.gsplat.splatBudget; if (budget > 0) { this._enforceBudget(budget); } else { this._budgetScale = 1.0; for (const [, inst] of this.octreeInstances){ inst.updateLod(this.cameraNode, this.scene.gsplat); } } } this.updateWorldState(); const lastState = this.worldStates.get(this.lastWorldStateVersion); if (lastState) { if (this.cpuSorter && !lastState.sortParametersSet) { lastState.sortParametersSet = true; const payload = this.prepareSortParameters(lastState); this.cpuSorter.setSortParameters(payload); } } const sortedState = this.worldStates.get(this.sortedVersion); if (sortedState?.sortedBefore) { if (this._workBufferRebuildRequired) { const count = sortedState.totalActiveSplats; this.rebuildWorkBuffer(sortedState, count, true); this._workBufferRebuildRequired = false; this.renderer.setOrderData(); if (this.intervalCompaction) { this.intervalCompaction._uploadedVersion = -1; } } else { this.applyWorkBufferUpdates(sortedState); } } let gpuSortedThisFrame = false; if (this.sortNeeded && lastState) { if (this.activeRenderer === GSPLAT_RENDERER_COMPUTE) { this.compactGpu(lastState); gpuSortedThisFrame = true; } else if (this.activeRenderer === GSPLAT_RENDERER_RASTER_GPU_SORT) { this.sortGpu(lastState); gpuSortedThisFrame = true; } else { this.sortCpu(lastState); } this.sortNeeded = false; this.lastSortCameraPos.copy(this.cameraNode.getPosition()); this.lastSortCameraFwd.copy(this.cameraNode.forward); this.lastCullingCameraFwd.copy(this.cameraNode.forward); this.lastCullingProjMat.copy(this.cameraNode.camera.projectionMatrix); } if (this.activeRenderer !== GSPLAT_RENDERER_COMPUTE && this.intervalCompaction && !gpuSortedThisFrame) { this.refreshIndirectDraw(); } const fogParams = this.scene.gsplat.useFog ? this.cameraNode.camera.fogParams ?? this.scene.fog : null; this.renderer.frameUpdate(this.scene.gsplat, this.scene.exposure, fogParams); if (sortedState?.sortedBefore) { this.updateColorCameraTracking(); } if (this.octreeInstances.size) { const cooldownTicks = this.scene.gsplat.cooldownTicks; for (const [, inst] of this.octreeInstances){ const octree = inst.octree; if (!tempOctreesTicked.has(octree)) { tempOctreesTicked.add(octree); octree.updateCooldownTick(cooldownTicks); } } tempOctreesTicked.clear(); } this.fireFrameReadyEvent(); if (this.scene.gsplat.dirty) { for (const [, inst] of this.octreeInstances){ inst.needsLodUpdate = true; } } return sortedState ? sortedState.totalActiveSplats : 0; } sortGpu(worldState) { const keyGenerator = this.keyGenerator; const gpuSorter = this.gpuSorter; if (!keyGenerator || !gpuSorter) return; const elementCount = worldState.totalActiveSplats; if (elementCount === 0) return; if (!this.intervalCompaction) { this.intervalCompaction = new GSplatIntervalCompaction(this.device); } if (!worldState.sortedBefore) { worldState.sortedBefore = true; this.cleanupOldWorldStates(worldState.version); this.sortedVersion = worldState.version; this.rebuildWorkBuffer(worldState, elementCount); } this.intervalCompaction.uploadIntervals(worldState); if (this.canCull) { const state = this.worldStates.get(this.sortedVersion); if (state) { this._runFrustumCulling(state); } } const numIntervals = worldState.totalIntervals; const totalActiveSplats = worldState.totalActiveSplats; this.intervalCompaction.dispatchCompact(this.workBuffer.frustumCuller, numIntervals, totalActiveSplats, this.renderer.fisheyeProj.enabled); this.allocateAndWriteIntervalIndirectArgs(numIntervals); const compactedSplatIds = this.intervalCompaction.compactedSplatIds; const numBits = Math.max(10, Math.min(20, Math.round(Math.log2(elementCount / 4)))); const roundedNumBits = Math.ceil(numBits / 4) * 4; const { minDist, maxDist } = this.computeDistanceRange(worldState); const sortedIndices = this.dispatchGpuSort(elementCount, roundedNumBits, minDist, maxDist, compactedSplatIds); this.applyGpuSortResults(worldState, sortedIndices); } compactGpu(worldState) { if (!this.intervalCompaction) { this.intervalCompaction = new GSplatIntervalCompaction(this.device); } const elementCount = worldState.totalActiveSplats; if (elementCount === 0) return; if (!worldState.sortedBefore) { worldState.sortedBefore = true; this.cleanupOldWorldStates(worldState.version); this.sortedVersion = worldState.version; this.rebuildWorkBuffer(worldState, elementCount); } this.intervalCompaction.uploadIntervals(worldState); if (this.canCull) { const state = this.worldStates.get(this.sortedVersion); if (state) { this._runFrustumCulling(state); } } const numIntervals = worldState.totalIntervals; const totalActiveSplats = worldState.totalActiveSplats; this.intervalCompaction.dispatchCompact(this.workBuffer.frustumCuller, numIntervals, totalActiveSplats, this.renderer.fisheyeProj.enabled); this.allocateAndWriteIntervalIndirectArgs(numIntervals); const ic = this.intervalCompaction; const localRenderer = this.renderer; localRenderer.setCompactedData(ic.compactedSplatIds, ic.sortElementCountBuffer, worldState.textureSize, totalActiveSplats); } allocateAndWriteIntervalIndirectArgs(numIntervals) { this.indirectDrawSlot = this.device.getIndirectDrawSlot(1); this.indirectDispatchSlot = this.device.getIndirectDispatchSlot(2); const ic = this.intervalCompaction; ic.writeIndirectArgs(this.indirectDrawSlot, this.indirectDispatchSlot, numIntervals); this.lastCompactedNumIntervals = numIntervals; } dispatchGpuSort(elementCount, roundedNumBits, minDist, maxDist, compactedSplatIds) { const keyGenerator = this.keyGenerator; const gpuSorter = this.gpuSorter; const ic = this.intervalCompaction; const keysBuffer = keyGenerator.generateIndirect(this.workBuffer, this.cameraNode, this.scene.gsplat.radialSorting, elementCount, roundedNumBits, minDist, maxDist, compactedSplatIds, ic.sortElementCountBuffer, this.indirectDispatchSlot); return gpuSorter.sortIndirect(keysBuffer, elementCount, roundedNumBits, this.indirectDispatchSlot + 1, ic.sortElementCountBuffer, compactedSplatIds, true); } applyGpuSortResults(worldState, sortedIndices) { const ic = this.intervalCompaction; this.renderer.setGpuSortedRendering(this.indirectDrawSlot, sortedIndices, ic.numSplatsBuffer, worldState.textureSize); } _runFrustumCulling(worldState) { this.workBuffer.frustumCuller.updateTransformsData(worldState.boundsGroups); const cam = this.cameraNode.camera; this.workBuffer.frustumCuller.computeFrustumPlanes(cam.projectionMatrix, cam.viewMatrix); const gsplat = this.scene.gsplat; const fp = this.renderer.fisheyeProj; fp.update(gsplat.fisheye, cam.fov, cam.projectionMatrix); if (fp.enabled) { this.workBuffer.frustumCuller.setFisheyeData(this.cameraNode.getPosition(), this.cameraNode.forward, fp.maxTheta); } } refreshIndirectDraw() { const sortedState = this.worldStates.get(this.sortedVersion); if (!sortedState || !this.intervalCompaction) return; this.allocateAndWriteIntervalIndirectArgs(this.lastCompactedNumIntervals); const gpuSorter = this.gpuSorter; const ic = this.intervalCompaction; this.renderer.setGpuSortedRendering(this.indirectDrawSlot, gpuSorter.sortedIndices, ic.numSplatsBuffer, sortedState.textureSize); } computeDistanceRange(worldState) { const cameraNode = this.cameraNode; const cameraMat = cameraNode.getWorldTransform(); cameraMat.getTranslation(cameraPosition); cameraMat.getZ(cameraDirection).normalize(); const radialSort = this.scene.gsplat.radialSorting; let minDist = radialSort ? 0 : Infinity; let maxDist = radialSort ? 0 : -Infinity; for (const splat of worldState.splats){ const modelMat = splat.node.getWorldTransform(); const aabbMin = splat.aabb.getMin(); const aabbMax = splat.aabb.getMax(); for(let i = 0; i < 8; i++){ _tempVec3.x = i & 1 ? aabbMax.x : aabbMin.x; _tempVec3.y = i & 2 ? aabbMax.y : aabbMin.y; _tempVec3.z = i & 4 ? aabbMax.z : aabbMin.z; modelMat.transformPoint(_tempVec3, _tempVec3); if (radialSort) { const dist = _tempVec3.distance(cameraPosition); if (dist > maxDist) maxDist = dist; } else { const dist = _tempVec3.sub(cameraPosition).dot(cameraDirection); if (dist < minDist) minDist = dist; if (dist > maxDist) maxDist = dist; } } } if (maxDist === 0 || maxDist === -Infinity) { return { minDist: 0, maxDist: 1 }; } return { minDist, maxDist }; } sortCpu(lastState) { if (!this.cpuSorter) return; const cameraNode = this.cameraNode; const cameraMat = cameraNode.getWorldTransform(); cameraMat.getTranslation(cameraPosition); cameraMat.getZ(cameraDirection).normalize(); const sorterRequest = []; lastState.splats.forEach((splat)=>{ const modelMat = splat.node.getWorldTransform(); invModelMat.copy(modelMat).invert(); const uniformScale = modelMat.getScale().x; const transformedDirection = invModelMat.transformVector(cameraDirection).normalize(); const transformedPosition = invModelMat.transformPoint(cameraPosition); modelMat.getTranslation(translation); const offset = translation.sub(cameraPosition).dot(cameraDirection); const aabbMin = splat.aabb.getMin(); const aabbMax = splat.aabb.getMax(); sorterRequest.push({ transformedDirection, transformedPosition, offset, scale: uniformScale, modelMat: modelMat.data.slice(), aabbMin: [ aabbMin.x, aabbMin.y, aabbMin.z ], aabbMax: [ aabbMax.x, aabbMax.y, aabbMax.z ] }); }); this.cpuSorter.setSortParams(sorterRequest, this.scene.gsplat.radialSorting); } prepareSortParameters(worldState) { return { command: 'intervals', textureSize: worldState.textureSize, totalActiveSplats: worldState.totalActiveSplats, version: worldState.version, ids: worldState.splats.map((splat)=>splat.resource.id), pixelOffsets: worldState.splats.map((splat)=>splat.intervalOffsets), intervals: worldState.splats.map((splat)=>splat.intervals) }; } constructor(device, director, layer, cameraNode){ this.node = new GraphNode('GSplatManager'); this.worldStates = new Map(); this.lastWorldStateVersion = 0; this.cpuSorter = null; this.keyGenerator = null; this.gpuSorter = null; this.intervalCompaction = null; this.indirectDrawSlot = -1; this.indirectDispatchSlot = -1; this.lastCompactedNumIntervals = 0; this.sortedVersion = 0; this._awaitingLodUpdate = false; this._workBufferFormatVersion = -1; this._workBufferRebuildRequired = false; this.bufferCopyUploaded = 0; this.bufferCopyTotal = 0; this._stateTracker = new GSplatPlacementStateTracker(); this._centersVersions = new Map(); this.framesTillFullUpdate = 0; this.lastLodCameraPos = new Vec3(Infinity, Infinity, Infinity); this.lastLodCameraFwd = new Vec3(Infinity, Infinity, Infinity); this.lastLodCameraFov = -1; this.lastSortCameraPos = new Vec3(Infinity, Infinity, Infinity); this.lastSortCameraFwd = new Vec3(Infinity, Infinity, Infinity); this.lastCullingCameraFwd = new Vec3(Infinity, Infinity, Infinity); this.lastCullingProjMat = new Mat4(); this.sortNeeded = true; this._budgetBalancer = new GSplatBudgetBalancer(); this._budgetScale = 1.0; this._allocationMap = new Map(); this.lastColorUpdateCameraPos = new Vec3(Infinity, Infinity, Infinity); this.layerPlacements = []; this.layerPlacementsDirty = false; this._placementSetChanged = false; this.octreeInstances = new Map(); this.octreeInstancesToDestroy = []; this.hasNewOctreeInstances = false; this.device = device; this.scene = director.scene; this.director = director; this.cameraNode = cameraNode; const allocatorGrowMultiplier = 1.15; const budget = this.scene.gsplat.splatBudget; this._allocator = new BlockAllocator(budget > 0 ? Math.ceil(budget * allocatorGrowMultiplier) : 0, allocatorGrowMultiplier); this.workBuffer = new GSplatWorkBuffer(device, this.scene.gsplat.format); this.layer = layer; this._createRenderer(this.scene.gsplat.currentRenderer); this._workBufferFormatVersion = this.workBuffer.format.extraStreamsVersion; } } export { GSplatManager };