UNPKG

playcanvas

Version:

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

353 lines (352 loc) 12.8 kB
import { Mat4 } from "../../core/math/mat4.js"; import { Vec3 } from "../../core/math/vec3.js"; import { GraphNode } from "../graph-node.js"; import { GSplatUnifiedSorter } from "./gsplat-unified-sorter.js"; import { GSplatWorld } from "./gsplat-world.js"; import { GSplatQuadRenderer } from "./gsplat-quad-renderer.js"; import { GSplatHybridRenderer } from "./gsplat-hybrid-renderer.js"; import { GSplatHybridRendererScratch } from "./gsplat-hybrid-renderer-scratch.js"; import { GSplatShadowRenderer } from "./gsplat-shadow-renderer.js"; import { BoundingBox } from "../../core/shape/bounding-box.js"; import { GSPLAT_RENDERER_RASTER_GPU_SORT, GSPLAT_FORWARD, GSPLAT_SHADOW, GSPLAT_DEBUG_AABBS } from "../constants.js"; import { Color } from "../../core/math/color.js"; const cameraPosition = new Vec3(); const cameraDirection = new Vec3(); const translation = new Vec3(); const invModelMat = new Mat4(); const _lodColors = [ 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) ]; class GSplatManager { device; node = new GraphNode("GSplatManager"); world; renderer; shadowRenderer = null; _hybridScratch = null; activeRenderer; cpuSorter = null; _centersVersions = /* @__PURE__ */ new Map(); lastSortCameraPos = new Vec3(Infinity, Infinity, Infinity); lastSortCameraFwd = new Vec3(Infinity, Infinity, Infinity); sortNeeded = true; _deviceRestoredEvent = null; cameraNode; scene; renderMode; _updateResult = { newVersion: false, overdrawDirty: false, sortNeeded: false }; _bakeResult = { rebuilt: false, count: 0, textureSize: 0, sortNeeded: false }; _markResult = { rebuilt: false, count: 0, textureSize: 0 }; _formatResult = { bufferRecreated: false, sortNeeded: false }; _lastStreamToken = -1; _streamAdvanced = false; _renderViewParams = {}; _pickParams = {}; constructor(device, director, layer, cameraNode) { this.device = device; this.scene = director.scene; this.director = director; this.cameraNode = cameraNode; this.world = new GSplatWorld(device, this.scene); this.layer = layer; this._createRenderer(this.scene.gsplat.currentRenderer); this._deviceRestoredEvent = this.device.on("devicerestored", this._onDeviceRestored, this); } destroy() { this._destroyed = true; this._deviceRestoredEvent?.off(); this._deviceRestoredEvent = null; this.destroyCpuSorting(); this.world.destroy(); this.renderer.destroy(); this.shadowRenderer?.destroy(); this.shadowRenderer = null; this._hybridScratch?.destroy(); this._hybridScratch = null; } _onDeviceRestored() { if (this.world.hasOctreeInstances) return; this.world.invalidate({ workBuffer: true }); this.sortNeeded = true; } destroyCpuSorting() { this.cpuSorter?.destroy(); this.cpuSorter = null; } initCpuSorting() { if (!this.cpuSorter) { this.cpuSorter = this.createSorter(); } const splats = this.world.invalidateSortState(); if (splats) { this.cpuSorter.updateCentersForSplats(splats); } this.renderer.setCpuSortedRendering(); } get material() { return this.renderer.material; } get bufferCopyUploaded() { return this.world.bufferCopyUploaded; } get bufferCopyTotal() { return this.world.bufferCopyTotal; } get hasPendingSort() { return !!this.cpuSorter?.pendingSorted; } prepareForPicking(camera, width, height) { if (!this.renderer.usesGpuSort || !camera.node) return null; const sortedState = this.world.getState(this.world.currentVersion); if (!sortedState?.sortedBefore) return null; return this.renderer.preparePickingView(this.world, sortedState, this._fillPickParams(camera, width, height)); } _writeGsplatParams(p) { const gsplat = this.scene.gsplat; p.radialSorting = gsplat.radialSorting; p.alphaClip = gsplat.alphaClip; p.alphaClipForward = gsplat.alphaClipForward; p.minPixelSize = gsplat.minPixelSize; p.minContribution = gsplat.minContribution; p.foveationStrength = gsplat.foveationStrength; p.foveationCenter = gsplat.foveationCenter; p.antiAlias = gsplat.antiAlias; p.fisheye = gsplat.fisheye; p.material = gsplat.material; p.varyings = gsplat.varyings; } _fillRenderViewParams() { const p = this._renderViewParams; this._writeGsplatParams(p); p.cameraNode = this.cameraNode; return p; } _fillPickParams(camera, width, height) { const p = this._pickParams; this._writeGsplatParams(p); p.cameraNode = camera.node; p.width = width; p.height = height; return p; } 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); this._syncShadowRenderer(); } _syncShadowRenderer() { const wantShadow = !!(this.renderMode & GSPLAT_SHADOW) && this.renderer.usesGpuSort; if (wantShadow && !this.shadowRenderer) { this.shadowRenderer = new GSplatShadowRenderer(this.device, this.node, this.cameraNode, this.layer, this.world, this._hybridScratch); } else if (!wantShadow && this.shadowRenderer) { this.shadowRenderer.destroy(); this.shadowRenderer = null; } if (!this.renderer.usesGpuSort && this._hybridScratch) { this._hybridScratch.destroy(); this._hybridScratch = null; } } _createRenderer(mode) { const workBuffer = this.world.workBuffer; if (mode === GSPLAT_RENDERER_RASTER_GPU_SORT) { this._hybridScratch ?? (this._hybridScratch = new GSplatHybridRendererScratch(this.device)); this.renderer = new GSplatHybridRenderer(this.device, this.node, this.cameraNode, this.layer, workBuffer, this._hybridScratch); } else { this.renderer = new GSplatQuadRenderer(this.device, this.node, this.cameraNode, this.layer, workBuffer); this.initCpuSorting(); } this.activeRenderer = mode; } prepareRendererMode() { const requested = this.scene.gsplat.currentRenderer; if (requested === this.activeRenderer) return; this.world.invalidate({ worldState: true }); this.destroyCpuSorting(); this.renderer.destroy(); this._createRenderer(requested); this.renderer.setRenderMode(this.renderMode); this._syncShadowRenderer(); this.world.invalidate({ workBuffer: true }); this.sortNeeded = true; } reconcile(placements) { this.world.reconcile(placements); } onSorted(count, version, orderData) { const updateBounds = this.renderer.requiresBounds; const result = this.world.onSorted(version, count, orderData, this.cameraNode, updateBounds, this._markResult); if (result.rebuilt) { this.renderer.update(result.count, result.textureSize); } this.renderer.setOrderData(); } _markSortedIfNeeded(worldState) { if (!worldState.sortedBefore) { this.world.markSorted(worldState.version, worldState.totalActiveSplats, this.cameraNode, true, this._markResult); if (this._markResult.rebuilt) { this.renderer.update(this._markResult.count, this._markResult.textureSize); } } } testCameraMovedForSort() { const epsilon = 1e-3; 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; } fireFrameReadyEvent() { const ready = this.world.currentVersion === this.world.lastWorldStateVersion && !this.world.awaitingLodUpdate; const loadingCount = this.world.pendingLoadCount; this.director.eventHandler.fire("frame:ready", this.cameraNode.camera, this.renderer.layer, ready, loadingCount); } updateStreaming(token) { if (token === this._lastStreamToken) return this._streamAdvanced; this._lastStreamToken = token; this.world.syncFormat(this._formatResult); if (this._formatResult.bufferRecreated) { this.renderer.setDataSource(this.world.workBuffer); this.shadowRenderer?.setDataSource(this.world.workBuffer); } if (this._formatResult.sortNeeded) this.sortNeeded = true; this.prepareRendererMode(); const allowLodUpdate = !this.renderer.requiresCpuSort || this.cpuSorter && this.cpuSorter.jobsInFlight < 3; this.world.update(this.cameraNode, allowLodUpdate, !!this.cpuSorter, this._updateResult); if (this._updateResult.overdrawDirty) this.renderer.updateOverdrawMode(this.scene.gsplat); if (this._updateResult.sortNeeded) this.sortNeeded = true; if (this._updateResult.newVersion) this._feedCpuSorterCenters(); this.world.tickCooldowns(); this._streamAdvanced = this._updateResult.newVersion || this._formatResult.bufferRecreated; return this._streamAdvanced; } update() { this.world.resetFrameStats(); this.updateStreaming(this.director._streamToken); if (this.cpuSorter) { this.cpuSorter.applyPendingSorted(); } if (this.testCameraMovedForSort()) { this.sortNeeded = true; } const lastState = this.world.getState(this.world.lastWorldStateVersion); if (lastState) { if (this.cpuSorter && !lastState.sortParametersSet) { lastState.sortParametersSet = true; const payload = this.world.prepareSortParameters(lastState); this.cpuSorter.setSortParameters(payload); } } const updateBounds = this.renderer.requiresBounds; this.world.bake(this.world.currentVersion, this.cameraNode, updateBounds, this._bakeResult); if (this._bakeResult.rebuilt) { this.renderer.update(this._bakeResult.count, this._bakeResult.textureSize); this.renderer.setOrderData(); this.renderer.invalidateCullUpload(); } if (this._bakeResult.sortNeeded) this.sortNeeded = true; if (lastState) { if (this.renderer.usesGpuSort) { this._markSortedIfNeeded(lastState); if (this.renderMode & GSPLAT_FORWARD) { this.renderer.prepareRenderView(this.world, lastState, this._fillRenderViewParams()); } } else if (this.sortNeeded) { this.sortCpu(lastState); } if (this.sortNeeded) { this.sortNeeded = false; this.lastSortCameraPos.copy(this.cameraNode.getPosition()); this.lastSortCameraFwd.copy(this.cameraNode.forward); } } this.shadowRenderer?.syncLights(); const aggregateAabb = this.world.computeAggregateAabb(); this.renderer?.meshInstance?.setCustomAabb(aggregateAabb); this.shadowRenderer?.setCastersAabb(aggregateAabb); this.fireFrameReadyEvent(); if (this.scene.gsplat.dirty) { this.world.markInstancesNeedLodUpdate(); } const fogParams = this.scene.gsplat.useFog ? this.cameraNode.camera.fogParams ?? this.scene.fog : null; this.renderer.frameUpdate(this.scene.gsplat, this.scene.exposure, fogParams); const sortedState = this.world.getState(this.world.currentVersion); return sortedState ? sortedState.totalActiveSplats : 0; } updateShadows() { this.shadowRenderer?.cull(this.scene.gsplat); } _feedCpuSorterCenters() { if (!this.cpuSorter) return; const state = this.world.getState(this.world.lastWorldStateVersion); if (!state) return; const splats = state.splats; 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); } 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); } } export { GSplatManager };