playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
210 lines (209 loc) • 6.84 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;
maxRetries;
constructor(app, maxRetries = 3) {
this.app = app;
this.maxRetries = maxRetries;
}
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
};