playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
256 lines (255 loc) • 7.96 kB
JavaScript
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 {
app;
constructor(app) {
this.app = app;
}
canParse(context) {
return context.ext === "sog";
}
_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 load(url, callback, asset) {
const gsplatCentersEnabledAtLoad = this.app.scene?.gsplatCentersEnabled !== false;
try {
const { assets } = this.app;
let unloaded = false;
let ownedByResource = false;
const textures = {};
asset.once("unload", () => {
unloaded = true;
Object.values(textures).forEach((t) => {
assets.remove(t);
if (!ownedByResource) {
t.unload();
} else {
assets._loader.clearCache(t.getFileUrl(), t.type);
}
});
});
const arrayBuffer = await downloadArrayBuffer(url, asset);
if (this._shouldAbort(asset, unloaded)) {
callback(null, null);
return;
}
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 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);
if (this._shouldAbort(asset, unloaded)) {
Object.values(textures).forEach((t) => {
assets.remove(t);
t.unload();
});
callback(null, null);
return;
}
Object.values(textures).forEach((t) => {
if (t.file) {
t.file.contents = null;
}
});
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) {
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 });
ownedByResource = !decompress;
if (this._shouldAbort(asset, unloaded)) {
resource.destroy();
callback(null, null);
return;
}
callback(null, resource);
} catch (err) {
callback(err);
}
}
}
export {
SogBundleParser
};