UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

208 lines (205 loc) 6.91 kB
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 };