playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
201 lines (200 loc) • 5.46 kB
JavaScript
import { Asset } from "../../framework/asset/asset.js";
import { http, Http } from "../../platform/net/http.js";
import { GSplatResource } from "../../scene/gsplat/gsplat-resource.js";
import { GSplatSogData } from "../../scene/gsplat/gsplat-sog-data.js";
import { GSplatSogResource } from "../../scene/gsplat/gsplat-sog-resource.js";
const combineProgress = (target, assets) => {
const map = /* @__PURE__ */ new Map();
const count = assets.length;
const fire = () => {
let loaded = 0;
let total = 0;
map.forEach((value) => {
loaded += value.loaded;
total += value.total;
});
const reporting = map.size;
if (reporting > 0 && reporting < count) {
total = Math.ceil(total * count / reporting);
}
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);
});
};
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;
};
class SogParser {
app;
maxRetries;
constructor(app, maxRetries) {
this.app = app;
this.maxRetries = maxRetries;
}
_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) {
const gsplatCentersEnabledAtLoad = this.app.scene?.gsplatCentersEnabled !== false;
if (meta.version !== 2) {
meta = upgradeMeta(meta);
}
const { assets } = this.app;
const subs = ["means", "quats", "scales", "sh0", "shN"];
const textures = {};
const promises = [];
const base = window.document?.baseURI ?? window.location.href;
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, base).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();
let unloaded = false;
asset.once("unload", () => {
unloaded = true;
textureAssets.forEach((t) => {
assets.remove(t);
t.unload();
});
});
combineProgress(asset, textureAssets);
textureAssets.forEach((t) => assets.load(t));
await Promise.allSettled(promises);
if (this._shouldAbort(asset, unloaded)) {
textureAssets.forEach((t) => {
assets.remove(t);
t.unload();
});
callback(null, null);
return;
}
const data = new GSplatSogData();
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 = GSplatSogData.calcBands(data.sh_centroids?.width);
const decompress = asset.data?.decompress;
if (!decompress) {
if (this._shouldAbort(asset, unloaded)) {
data.destroy();
callback(null, null);
return;
}
data.prepareCodebook();
if (gsplatCentersEnabledAtLoad) {
await data.prepareGpuData();
}
}
if (this._shouldAbort(asset, unloaded)) {
data.destroy();
callback(null, null);
return;
}
const prepareCenters = gsplatCentersEnabledAtLoad;
const resource = decompress ? new GSplatResource(this.app.graphicsDevice, await data.decompress(), { prepareCenters }) : new GSplatSogResource(this.app.graphicsDevice, data, { prepareCenters });
if (this._shouldAbort(asset, unloaded)) {
resource.destroy();
callback(null, null);
return;
}
callback(null, resource);
}
load(url, callback, asset) {
if (asset.data?.means) {
this.loadTextures(url, callback, asset, asset.data);
} else {
if (typeof url === "string") {
url = {
load: url,
original: url
};
}
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 {
SogParser
};