playcanvas
Version:
PlayCanvas WebGL game engine
208 lines (205 loc) • 6.91 kB
JavaScript
import { Asset } from '../asset/asset.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';
const parseZipArchive = (data)=>{
const dataView = new DataView(data);
const u16 = (offset)=>dataView.getUint16(offset, true);
const u32 = (offset)=>dataView.getUint32(offset, true);
const extractEocd = (offset)=>{
return {
magic: u32(offset),
numFiles: u16(offset + 8),
cdSizeBytes: u32(offset + 12),
cdOffsetBytes: u32(offset + 16)
};
};
const extractCdr = (offset)=>{
const filenameLength = u16(offset + 28);
const extraFieldLength = u16(offset + 30);
const fileCommentLength = u16(offset + 32);
return {
magic: u32(offset),
compressionMethod: u16(offset + 10),
compressedSizeBytes: u32(offset + 20),
uncompressedSizeBytes: u32(offset + 24),
lfhOffsetBytes: u32(offset + 42),
filename: new TextDecoder().decode(new Uint8Array(data, offset + 46, filenameLength)),
recordSizeBytes: 46 + filenameLength + extraFieldLength + fileCommentLength
};
};
const extractLfh = (offset)=>{
const filenameLength = u16(offset + 26);
const extraLength = u16(offset + 28);
return {
magic: u32(offset),
offsetBytes: offset + 30 + filenameLength + extraLength
};
};
const eocd = extractEocd(dataView.byteLength - 22);
if (eocd.magic !== 0x06054b50) {
throw new Error('Invalid zip file: EOCDR not found');
}
if (eocd.cdOffsetBytes === 0xffffffff || eocd.cdSizeBytes === 0xffffffff) {
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 !== 0x02014b50) {
throw new Error('Invalid zip file: CDR not found');
}
const lfh = extractLfh(cdr.lfhOffsetBytes);
if (lfh.magic !== 0x04034b50) {
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){
this.app = app;
this.maxRetries = maxRetries;
}
async load(url, callback, asset) {
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
});
} else {
const url = new URL(filename, new URL(filename, window.location.href).toString()).toString();
texture = new Asset(filename, 'texture', {
url,
filename
}, {
mipmaps: false
});
}
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 data = new GSplatSogsData();
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;
const decompress = asset.data?.decompress;
if (!decompress) {
await data.prepareGpuData();
}
const resource = decompress ? new GSplatResource(this.app.graphicsDevice, await data.decompress()) : new GSplatSogsResource(this.app.graphicsDevice, data);
callback(null, resource);
} catch (err) {
callback(err);
}
}
}
export { SogBundleParser };