playcanvas
Version:
PlayCanvas WebGL game engine
208 lines (205 loc) • 8.79 kB
JavaScript
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';
// Temporary array reused to avoid allocations during cooldown ticking
const _toDelete = [];
/**
* @import { GSplatResource } from '../gsplat/gsplat-resource.js'
* @import { GSplatOctreeNodeLod } from './gsplat-octree-node.js'
* @import { GSplatAssetLoaderBase } from './gsplat-asset-loader-base.js'
*/ 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){
/**
* Resources of individual files, identified by their file index.
*
* @type {Map<number, GSplatResource>}
*/ this.fileResources = new Map();
/**
* Cooldown timers for files that reached zero references. Key is fileIndex, value is ticks
* remaining.
*
* @type {Map<number, number>}
*/ this.cooldowns = new Map();
this.lodLevels = data.lodLevels;
this.assetFileUrl = assetFileUrl;
// expand all file paths to full URLs upfront to avoid repeated joins later
const baseDir = path.getDirectory(assetFileUrl);
this.files = data.filenames.map((url)=>({
url: path.isRelativePath(url) ? path.join(baseDir, url) : url,
lodLevel: -1
}));
// initialize per-file ref counts
this.fileRefCounts = new Int32Array(this.files.length);
// Extract leaf nodes from hierarchical tree structure
const leafNodes = [];
this._extractLeafNodes(data.tree, leafNodes);
// Create nodes from the extracted leaf nodes
this.nodes = leafNodes.map((nodeData)=>{
/** @type {GSplatOctreeNodeLod[]} */ const lods = [];
// Ensure we have exactly lodLevels entries
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
});
// record LOD level for the file index
this.files[lodData.file].lodLevel = i;
} else {
// Missing LOD entry - fill with defaults
lods.push({
file: '',
fileIndex: -1,
offset: 0,
count: 0
});
}
}
return new GSplatOctreeNode(lods, nodeData.bound);
});
}
/**
* Trace out per-LOD counts of currently loaded file resources.
* @private
*/ _traceLodCounts() {
Debug.call(()=>{
if (!Tracing.get(TRACEID_OCTREE_RESOURCES)) return;
const loadedCounts = new Map();
for (const fileIndex of this.fileResources.keys()){
const lod = this.files[fileIndex].lodLevel;
loadedCounts.set(lod, (loadedCounts.get(lod) || 0) + 1);
}
// report all LODs from 0..lodLevels-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) {
// This is a leaf node with LOD data
leafNodes.push({
lods: node.lods,
bound: node.bound
});
} else if (node.children) {
// This is a branch node, recurse into 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;
// cancel any pending cooldown
this.cooldowns.delete(fileIndex);
}
/**
* Decrements reference count for a file by index. When it reaches zero, start cooldown.
*
* @param {number} fileIndex - Index of the file in `files` array.
* @param {number} cooldownTicks - Number of update ticks before unloading when unused.
*/ 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);
// unload cooldown
if (count === 0) {
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.
* @param {GSplatAssetLoaderBase} assetLoader - Asset loader used to unload the resource.
*/ unloadResource(fileIndex, assetLoader) {
Debug.assert(fileIndex >= 0 && fileIndex < this.files.length);
if (this.fileResources.has(fileIndex)) {
const fullUrl = this.files[fileIndex].url;
assetLoader.unload(fullUrl);
this.fileResources.delete(fileIndex);
// trace updated LOD counts after change
this._traceLodCounts();
}
}
/**
* Advances cooldowns for zero-ref files and unloads those whose timers expired.
*
* @param {GSplatAssetLoaderBase} assetLoader - Asset loader used to unload expired resources.
*/ updateCooldownTick(assetLoader) {
if (this.cooldowns.size > 0) {
this.cooldowns.forEach((remaining, fileIndex)=>{
if (remaining <= 1) {
// just a safety to avoid unloading a file that was re-referenced
if (this.fileRefCounts[fileIndex] === 0) {
this.unloadResource(fileIndex, assetLoader);
}
_toDelete.push(fileIndex);
} else {
// decrement cooldown timer
this.cooldowns.set(fileIndex, remaining - 1);
}
});
// delete them from the cooldowns map
_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.
* @param {GSplatAssetLoaderBase} assetLoader - The asset loader.
*/ ensureFileResource(fileIndex, assetLoader) {
Debug.assert(fileIndex >= 0 && fileIndex < this.files.length);
// resource already loaded
if (this.fileResources.has(fileIndex)) {
return;
}
// Check if the resource is now available from the asset loader
const fullUrl = this.files[fileIndex].url;
const res = assetLoader.getResource(fullUrl);
if (res) {
this.fileResources.set(fileIndex, res);
// if the file finished loading and is no longer needed, schedule a cooldown
if (this.fileRefCounts[fileIndex] === 0) {
this.cooldowns.set(fileIndex, assetLoader.cooldownTicks);
}
// trace updated LOD counts after change
this._traceLodCounts();
return;
}
// Start/continue loading (asset loader handles duplicates internally)
assetLoader.load(fullUrl);
}
}
export { GSplatOctree };