UNPKG

playcanvas

Version:

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

351 lines (350 loc) 11.4 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { GSplatOctreeNode } from "./gsplat-octree-node.js"; import { path } from "../../core/path.js"; import { Debug } from "../../core/debug.js"; import { Tracing } from "../../core/tracing.js"; import { TRACEID_OCTREE_RESOURCES } from "../../core/constants.js"; const _toDelete = []; class GSplatOctree { /** * @param {string} assetFileUrl - The file URL of the container asset. * @param {Object} data - The parsed JSON data containing info, filenames and tree. */ constructor(assetFileUrl, data) { /** * @type {GSplatOctreeNode[]} */ __publicField(this, "nodes"); /** * Packed per-node axis-aligned bounds in octree local space for CPU hot paths (e.g. LOD). * Length is {@link GSplatOctree.nodes}.length * 6. For node index `i`, base `b = i * 6`: * `[minX, minY, minZ, maxX, maxY, maxZ]` matching {@link GSplatOctreeNode.bounds}. * * @type {Float32Array} */ __publicField(this, "nodeBoundsMinMax"); /** * @type {{ url: string, lodLevel: number }[]} */ __publicField(this, "files"); /** * @type {number} */ __publicField(this, "lodLevels"); /** * The file URL of the container asset, used as the base for resolving relative URLs. * * @type {string} */ __publicField(this, "assetFileUrl"); /** * Resources of individual files, identified by their file index. * * @type {Map<number, GSplatResource>} */ __publicField(this, "fileResources", /* @__PURE__ */ new Map()); /** * Reference counts for each file by file index. Index is fileIndex, value is reference count. * When a file reaches zero references, it is scheduled for cooldown and unload. * * @type {Int32Array} */ __publicField(this, "fileRefCounts"); /** * Cooldown timers for files that reached zero references. Key is fileIndex, value is ticks * remaining. * * @type {Map<number, number>} */ __publicField(this, "cooldowns", /* @__PURE__ */ new Map()); /** * Optional environment asset URL. * * @type {string|null} */ __publicField(this, "environmentUrl", null); /** * Loaded environment resource. * * @type {GSplatResource|null} */ __publicField(this, "environmentResource", null); /** * Reference count for environment usage. */ __publicField(this, "environmentRefCount", 0); /** * Asset loader used for loading/unloading resources. * * @type {GSplatAssetLoaderBase|null} */ __publicField(this, "assetLoader", null); /** * Whether this octree has been destroyed. */ __publicField(this, "destroyed", false); /** * Number of update ticks before unloading unused file resources. Set from GSplatParams. * * @private */ __publicField(this, "cooldownTicks", 100); this.lodLevels = data.lodLevels; this.assetFileUrl = assetFileUrl; const baseDir = path.getDirectory(assetFileUrl); this.files = data.filenames.map((url) => ({ url: path.isRelativePath(url) ? path.join(baseDir, url) : url, lodLevel: -1 })); this.fileRefCounts = new Int32Array(this.files.length); if (data.environment) { this.environmentUrl = path.isRelativePath(data.environment) ? path.join(baseDir, data.environment) : data.environment; } const leafNodes = []; this._extractLeafNodes(data.tree, leafNodes); this.nodes = leafNodes.map((nodeData) => { const lods = []; for (let i = 0; i < this.lodLevels; i++) { const lodData = nodeData.lods[i.toString()]; if (lodData) { lods.push({ file: this.files[lodData.file].url || "", fileIndex: lodData.file, offset: lodData.offset || 0, count: lodData.count || 0 }); this.files[lodData.file].lodLevel = i; } else { lods.push({ file: "", fileIndex: -1, offset: 0, count: 0 }); } } return new GSplatOctreeNode(lods, nodeData.bound); }); const nodeCount = this.nodes.length; const boundsFlat = new Float32Array(nodeCount * 6); for (let i = 0; i < nodeCount; i++) { const bounds = this.nodes[i].bounds; const mn = bounds.getMin(); const mx = bounds.getMax(); const b = i * 6; boundsFlat[b + 0] = mn.x; boundsFlat[b + 1] = mn.y; boundsFlat[b + 2] = mn.z; boundsFlat[b + 3] = mx.x; boundsFlat[b + 4] = mx.y; boundsFlat[b + 5] = mx.z; } this.nodeBoundsMinMax = boundsFlat; } /** * Destroys the octree and clears internal state. Does not force-unload resources as they may * still be referenced by managers. Resources will be cleaned up when their reference counts * reach zero through the normal cleanup mechanisms. */ destroy() { this.destroyed = true; this.fileResources.clear(); this.cooldowns.clear(); this.assetLoader?.destroy(); this.assetLoader = null; this.environmentResource = null; } /** * Trace out per-LOD counts of currently loaded file resources. * * @private */ _traceLodCounts() { Debug.call(() => { if (!Tracing.get(TRACEID_OCTREE_RESOURCES)) return; const loadedCounts = /* @__PURE__ */ new Map(); for (const fileIndex of this.fileResources.keys()) { const lod = this.files[fileIndex].lodLevel; loadedCounts.set(lod, (loadedCounts.get(lod) || 0) + 1); } const maxLod = Math.max(0, this.lodLevels - 1); const loadedSummary = Array.from({ length: maxLod + 1 }, (_, i) => loadedCounts.get(i) || 0).join(" / "); Debug.trace(TRACEID_OCTREE_RESOURCES, `${this.assetFileUrl}: LOD resources in memory: ${loadedSummary}`); }); } /** * Recursively extracts leaf nodes (nodes with 'lods' property) from the hierarchical tree. * * @param {Object} node - The current tree node to process. * @param {Array} leafNodes - Array to collect leaf nodes. * @private */ _extractLeafNodes(node, leafNodes) { if (node.lods) { leafNodes.push({ lods: node.lods, bound: node.bound }); } else if (node.children) { for (const child of node.children) { this._extractLeafNodes(child, leafNodes); } } } getFileResource(fileIndex) { return this.fileResources.get(fileIndex); } /** * Increments reference count for a file by index and cancels any pending cooldown. * * @param {number} fileIndex - Index of the file in `files` array. */ incRefCount(fileIndex) { Debug.assert(fileIndex >= 0 && fileIndex < this.files.length); const count = this.fileRefCounts[fileIndex] + 1; this.fileRefCounts[fileIndex] = count; this.cooldowns.delete(fileIndex); } /** * Decrements reference count for a file by index. When it reaches zero, either unload * immediately (if cooldownTicks is 0) or schedule for cooldown. * * @param {number} fileIndex - Index of the file in `files` array. * @param {number} cooldownTicks - Number of update ticks before unloading when unused. If 0, * unload immediately. */ decRefCount(fileIndex, cooldownTicks) { Debug.assert(fileIndex >= 0 && fileIndex < this.files.length); const count = this.fileRefCounts[fileIndex] - 1; this.fileRefCounts[fileIndex] = count; Debug.assert(count >= 0); if (count === 0) { if (cooldownTicks === 0) { this.unloadResource(fileIndex); } else { this.cooldowns.set(fileIndex, cooldownTicks); } } } /** * Unloads a resource for a file index if currently loaded. * * @param {number} fileIndex - Index of the file in `files` array. */ unloadResource(fileIndex) { Debug.assert(fileIndex >= 0 && fileIndex < this.files.length); if (!this.assetLoader) { return; } const fullUrl = this.files[fileIndex].url; this.assetLoader.unload(fullUrl); if (this.fileResources.has(fileIndex)) { this.fileResources.delete(fileIndex); this._traceLodCounts(); } } /** * Advances cooldowns for zero-ref files and unloads those whose timers expired. * * @param {number} cooldownTicks - Number of ticks for new cooldowns, synced from GSplatParams. */ updateCooldownTick(cooldownTicks) { this.cooldownTicks = cooldownTicks; if (this.cooldowns.size > 0) { this.cooldowns.forEach((remaining, fileIndex) => { if (remaining <= 1) { if (this.fileRefCounts[fileIndex] === 0) { this.unloadResource(fileIndex); } _toDelete.push(fileIndex); } else { this.cooldowns.set(fileIndex, remaining - 1); } }); _toDelete.forEach((idx) => this.cooldowns.delete(idx)); _toDelete.length = 0; } } /** * Ensures a file resource is loaded and available. This function: * - Starts loading if not already started * - Checks if loading completed and stores the resource if available * * @param {number} fileIndex - The index of the file in the `files` array. */ ensureFileResource(fileIndex) { Debug.assert(fileIndex >= 0 && fileIndex < this.files.length); Debug.assert(this.assetLoader); if (this.fileResources.has(fileIndex)) { return; } const fullUrl = this.files[fileIndex].url; const res = this.assetLoader?.getResource(fullUrl); if (res) { this.fileResources.set(fileIndex, res); res.releaseTextureSources?.(); if (this.fileRefCounts[fileIndex] === 0) { this.cooldowns.set(fileIndex, this.cooldownTicks); } this._traceLodCounts(); return; } this.assetLoader?.load(fullUrl); } /** * Increments reference count for environment. */ incEnvironmentRefCount() { this.environmentRefCount++; } /** * Decrements reference count for environment. When it reaches zero, immediately unload. */ decEnvironmentRefCount() { this.environmentRefCount--; Debug.assert(this.environmentRefCount >= 0); if (this.environmentRefCount === 0) { this.unloadEnvironmentResource(); } } /** * Ensures environment resource is loaded and available. */ ensureEnvironmentResource() { if (!this.assetLoader) { return; } if (!this.environmentUrl) { return; } if (this.environmentResource) { return; } const res = this.assetLoader.getResource(this.environmentUrl); if (res) { this.environmentResource = res; if (this.environmentRefCount === 0) { this.unloadEnvironmentResource(); } return; } this.assetLoader.load(this.environmentUrl); } /** * Unloads environment resource if currently loaded. */ unloadEnvironmentResource() { if (!this.assetLoader) { return; } if (this.environmentResource && this.environmentUrl) { this.assetLoader.unload(this.environmentUrl); this.environmentResource = null; } } } export { GSplatOctree };