UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

237 lines (234 loc) 8.55 kB
import { Asset } from '../asset/asset.js'; import { Debug } from '../../core/debug.js'; import { Http, http } from '../../platform/net/http.js'; import { GSplatResource } from '../../scene/gsplat/gsplat-resource.js'; import { GSplatSogsData } from '../../scene/gsplat/gsplat-sogs-data.js'; import { GSplatSogsResource } from '../../scene/gsplat/gsplat-sogs-resource.js'; // combine the progress updates from multiple assets // and fire progress events on the target const combineProgress = (target, assets)=>{ const map = new Map(); const fire = ()=>{ let loaded = 0; let total = 0; map.forEach((value)=>{ loaded += value.loaded; total += value.total; }); target.fire('progress', loaded, total); }; assets.forEach((asset)=>{ const progress = (loaded, total)=>{ map.set(asset, { loaded, total }); fire(); }; const done = ()=>{ asset.off('progress', progress); asset.off('load', done); asset.off('error', done); }; asset.on('progress', progress); asset.on('load', done); asset.on('error', done); }); }; // given a v1 meta.json, upgrade it to the v2 shape const upgradeMeta = (meta)=>{ const result = { version: 1, count: meta.means.shape[0], means: { mins: meta.means.mins, maxs: meta.means.maxs, files: meta.means.files }, scales: { mins: meta.scales.mins, maxs: meta.scales.maxs, files: meta.scales.files }, quats: { files: meta.quats.files }, sh0: { mins: meta.sh0.mins, maxs: meta.sh0.maxs, files: meta.sh0.files } }; if (meta.shN) { result.shN = { mins: meta.shN.mins, maxs: meta.shN.maxs, files: meta.shN.files }; } return result; }; /** * @import { AppBase } from '../app-base.js' * @import { ResourceHandlerCallback } from '../handlers/handler.js' */ class SogsParser { /** * @param {AppBase} app - The app instance. * @param {number} maxRetries - Maximum amount of retries. */ constructor(app, maxRetries){ this.app = app; this.maxRetries = maxRetries; } /** * Checks if loading should be aborted due to asset unload or invalid device. * * @param {Asset} asset - The asset being loaded. * @param {boolean} unloaded - Whether the asset was unloaded during async loading. * @returns {boolean} True if loading should be aborted. * @private */ _shouldAbort(asset, unloaded) { if (unloaded || !this.app.assets.get(asset.id)) return true; if (!this.app?.graphicsDevice || this.app.graphicsDevice._destroyed) return true; return false; } async loadTextures(url, callback, asset, meta) { // transform meta to latest shape if (meta.version !== 2) { Debug.deprecated('Loading SOG v1 data which is deprecated. Please recompress your scene with latest tools.'); meta = upgradeMeta(meta); } const { assets } = this.app; const subs = [ 'means', 'quats', 'scales', 'sh0', 'shN' ]; const textures = {}; const promises = []; subs.forEach((sub)=>{ const files = meta[sub]?.files ?? []; textures[sub] = files.map((filename)=>{ const texture = new Asset(filename, 'texture', { url: asset.options?.mapUrl?.(filename) ?? new URL(filename, new URL(url.load, window.location.href).toString()).toString(), filename }, { mipmaps: false }, { crossOrigin: 'anonymous' }); const promise = new Promise((resolve, reject)=>{ texture.on('load', ()=>resolve(null)); texture.on('error', (err)=>reject(err)); }); assets.add(texture); promises.push(promise); return texture; }); }); const textureAssets = subs.map((sub)=>textures[sub]).flat(); // Track if asset was unloaded during async loading let unloaded = false; // When the parent gsplat asset unloads, remove and unload child texture assets asset.once('unload', ()=>{ unloaded = true; textureAssets.forEach((t)=>{ // remove from registry assets.remove(t); // destroys resource t.unload(); }); }); combineProgress(asset, textureAssets); textureAssets.forEach((t)=>assets.load(t)); // wait for all textures to complete loading await Promise.allSettled(promises); if (this._shouldAbort(asset, unloaded)) { // Clean up texture assets that were created during the async load textureAssets.forEach((t)=>{ assets.remove(t); t.unload(); }); callback(null, null); return; } // construct the gsplat resource const data = new GSplatSogsData(); data.url = url.original; data.meta = meta; data.numSplats = meta.count; data.means_l = textures.means[0].resource; data.means_u = textures.means[1].resource; data.quats = textures.quats[0].resource; data.scales = textures.scales[0].resource; data.sh0 = textures.sh0[0].resource; data.sh_centroids = textures.shN?.[0]?.resource; data.sh_labels = textures.shN?.[1]?.resource; data.shBands = GSplatSogsData.calcBands(data.sh_centroids?.width); const decompress = asset.data?.decompress; const minimalMemory = asset.options?.minimalMemory ?? false; // Pass minimalMemory to data data.minimalMemory = minimalMemory; if (!decompress) { if (this._shouldAbort(asset, unloaded)) { data.destroy(); callback(null, null); return; } // no need to prepare gpu data if decompressing await data.prepareGpuData(); } if (this._shouldAbort(asset, unloaded)) { data.destroy(); callback(null, null); return; } const resource = decompress ? new GSplatResource(this.app.graphicsDevice, await data.decompress()) : new GSplatSogsResource(this.app.graphicsDevice, data); if (this._shouldAbort(asset, unloaded)) { resource.destroy(); callback(null, null); return; } callback(null, resource); } /** * @param {object} url - The URL of the resource to load. * @param {string} url.load - The URL to use for loading the resource. * @param {string} url.original - The original URL useful for identifying the resource type. * @param {ResourceHandlerCallback} callback - The callback used when * the resource is loaded or an error occurs. * @param {Asset} asset - Container asset. */ load(url, callback, asset) { if (asset.data?.means) { // user can specify meta.json in asset data this.loadTextures(url, callback, asset, asset.data); } else { // otherwise download meta.json using asset url if (typeof url === 'string') { url = { load: url, original: url }; } // we need to specify JSON for blob URLs const options = { retry: this.maxRetries > 0, maxRetries: this.maxRetries, responseType: Http.ResponseType.JSON }; http.get(url.load, options, (err, meta)=>{ if (this._shouldAbort(asset, false)) { callback(null, null); return; } if (!err) { this.loadTextures(url, callback, asset, meta); } else { callback(`Error loading gsplat meta: ${url.original} [${err}]`); } }); } } } export { SogsParser };