UNPKG

playcanvas

Version:

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

223 lines (222 loc) 8.22 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 { Asset } from "../../framework/asset/asset.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 parseZipArchive = (data) => { const dataView = new DataView(data); const u16 = (offset2) => dataView.getUint16(offset2, true); const u32 = (offset2) => dataView.getUint32(offset2, true); const extractEocd = (offset2) => { return { magic: u32(offset2), numFiles: u16(offset2 + 8), cdSizeBytes: u32(offset2 + 12), cdOffsetBytes: u32(offset2 + 16) }; }; const extractCdr = (offset2) => { const filenameLength = u16(offset2 + 28); const extraFieldLength = u16(offset2 + 30); const fileCommentLength = u16(offset2 + 32); return { magic: u32(offset2), compressionMethod: u16(offset2 + 10), compressedSizeBytes: u32(offset2 + 20), uncompressedSizeBytes: u32(offset2 + 24), lfhOffsetBytes: u32(offset2 + 42), filename: new TextDecoder().decode(new Uint8Array(data, offset2 + 46, filenameLength)), recordSizeBytes: 46 + filenameLength + extraFieldLength + fileCommentLength }; }; const extractLfh = (offset2) => { const filenameLength = u16(offset2 + 26); const extraLength = u16(offset2 + 28); return { magic: u32(offset2), offsetBytes: offset2 + 30 + filenameLength + extraLength }; }; const eocd = extractEocd(dataView.byteLength - 22); if (eocd.magic !== 101010256) { throw new Error("Invalid zip file: EOCDR not found"); } if (eocd.cdOffsetBytes === 4294967295 || eocd.cdSizeBytes === 4294967295) { throw new Error("Invalid zip file: Zip64 not supported"); } const result = []; let offset = eocd.cdOffsetBytes; for (let i = 0; i < eocd.numFiles; i++) { const cdr = extractCdr(offset); if (cdr.magic !== 33639248) { throw new Error("Invalid zip file: CDR not found"); } const lfh = extractLfh(cdr.lfhOffsetBytes); if (lfh.magic !== 67324752) { throw new Error("Invalid zip file: LFH not found"); } result.push({ filename: cdr.filename, compression: { 0: "none", 8: "deflate" }[cdr.compressionMethod] ?? "unknown", data: new Uint8Array(data, lfh.offsetBytes, cdr.compressedSizeBytes) }); offset += cdr.recordSizeBytes; } return result; }; const inflate = async (compressed) => { const ds = new DecompressionStream("deflate-raw"); const out = new Blob([compressed]).stream().pipeThrough(ds); const ab = await new Response(out).arrayBuffer(); return new Uint8Array(ab); }; const downloadArrayBuffer = async (url, asset) => { const response = await (asset.file?.contents ?? fetch(url.load)); if (!response) { throw new Error("Error loading resource"); } if (response instanceof Response) { if (!response.ok) { throw new Error(`Error loading resource: ${response.status} ${response.statusText}`); } const totalLength = parseInt(response.headers.get("content-length") ?? "0", 10); if (!response.body || !response.body.getReader) { const buf = await response.arrayBuffer(); asset.fire("progress", buf.byteLength, totalLength); return buf; } const reader = response.body.getReader(); const chunks = []; let totalReceived = 0; try { while (true) { const { done, value } = await reader.read(); if (done) { break; } chunks.push(value); totalReceived += value.byteLength; asset.fire("progress", totalReceived, totalLength); } } finally { reader.releaseLock(); } return new Blob(chunks).arrayBuffer(); } return response; }; class SogBundleParser { constructor(app, maxRetries = 3) { /** @type {AppBase} */ __publicField(this, "app"); /** @type {number} */ __publicField(this, "maxRetries"); this.app = app; this.maxRetries = maxRetries; } /** * @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. */ async load(url, callback, asset) { const gsplatCentersEnabledAtLoad = this.app.scene?.gsplatCentersEnabled !== false; try { const arrayBuffer = await downloadArrayBuffer(url, asset); const files = parseZipArchive(arrayBuffer); for (const file of files) { if (file.compression === "deflate") { file.data = await inflate(file.data); } } const metaFile = files.find((f) => f.filename === "meta.json"); if (!metaFile) { callback("Error: meta.json not found"); return; } let meta; try { meta = JSON.parse(new TextDecoder().decode(metaFile.data)); } catch (err) { callback(`Error parsing meta.json: ${err}`); return; } const filenames = ["means", "scales", "quats", "sh0", "shN"].map((key) => meta[key]?.files ?? []).flat(); const textures = {}; const promises = []; for (const filename of filenames) { const file = files.find((f) => f.filename === filename); let texture; if (file) { texture = new Asset(filename, "texture", { url: `${url.load}/${filename}`, filename, contents: file.data }, { mipmaps: false }, { crossOrigin: "anonymous" }); } else { const url2 = new URL(filename, new URL(filename, window.location.href).toString()).toString(); texture = new Asset(filename, "texture", { url: url2, filename }, { mipmaps: false }, { crossOrigin: "anonymous" }); } const promise = new Promise((resolve, reject) => { texture.on("load", () => resolve(null)); texture.on("error", (err) => reject(err)); }); this.app.assets.add(texture); textures[filename] = texture; promises.push(promise); } Object.values(textures).forEach((t) => this.app.assets.load(t)); await Promise.allSettled(promises); const { assets } = this.app; asset.once("unload", () => { Object.values(textures).forEach((t) => { assets.remove(t); t.unload(); }); }); const decompress = asset.data?.decompress; const data = new GSplatSogData(); data.url = url.original; data.meta = meta; data.numSplats = meta.count; data.means_l = textures[meta.means.files[0]].resource; data.means_u = textures[meta.means.files[1]].resource; data.quats = textures[meta.quats.files[0]].resource; data.scales = textures[meta.scales.files[0]].resource; data.sh0 = textures[meta.sh0.files[0]].resource; data.sh_centroids = textures[meta.shN?.files[0]]?.resource; data.sh_labels = textures[meta.shN?.files[1]]?.resource; data.shBands = GSplatSogData.calcBands(data.sh_centroids?.width); if (!decompress) { data.prepareCodebook(); if (gsplatCentersEnabledAtLoad) { await data.prepareGpuData(); } } const prepareCenters = gsplatCentersEnabledAtLoad; const resource = decompress ? new GSplatResource(this.app.graphicsDevice, await data.decompress(), { prepareCenters }) : new GSplatSogResource(this.app.graphicsDevice, data, { prepareCenters }); callback(null, resource); } catch (err) { callback(err); } } } export { SogBundleParser };