/* eslint-disable no-restricted-globals */ /** * Converts XGrids .lci binary to PLY Mesh */ function parseLci(buffer: ArrayBuffer): Blob { const view = new DataView(buffer); const LCI_MAGIC = 0x6c6c6f63; // 'coll' if (view.getUint32(0, true) !== LCI_MAGIC) { throw new Error("Invalid LCI Magic Number"); } const meshNum = view.getUint32(44, true); const meshes = []; for (let i = 0; i < meshNum; i++) { const offset = 48 + i * 40; meshes.push({ dataOffset: Number(view.getBigUint64(offset + 8, true)), vertexNum: view.getUint32(offset + 24, true), faceNum: view.getUint32(offset + 28, true), }); } let verticesStr = ""; let facesStr = ""; let vOffset = 0; for (const m of meshes) { let pos = m.dataOffset; for (let j = 0; j < m.vertexNum; j++) { verticesStr += `${view.getFloat32(pos, true).toFixed(6)} ${view.getFloat32(pos + 4, true).toFixed(6)} ${view.getFloat32(pos + 8, true).toFixed(6)}\n`; pos += 12; } for (let j = 0; j < m.faceNum; j++) { facesStr += `3 ${view.getUint32(pos, true) + vOffset} ${view.getUint32(pos + 4, true) + vOffset} ${view.getUint32(pos + 8, true) + vOffset}\n`; pos += 12; } vOffset += m.vertexNum; } const header = `ply\nformat ascii 1.0\nelement vertex ${vOffset}\nproperty float x\nproperty float y\nproperty float z\nelement face ${meshes.reduce((a, b) => a + b.faceNum, 0)}\nproperty list uchar int vertex_indices\nend_header\n`; return new Blob([header + verticesStr + facesStr], { type: "application/octet-stream", }); } /** * Converts XGrids environment.bin to PlayCanvas Gaussian Splat PLY */ /** * Converts XGrids environment.bin to a Full Gaussian Splatting PLY * Extracts: Position, Scale, Rotation (Quaternions), and Opacity */ function parseEnvironment(buffer: ArrayBuffer): Blob { const view = new DataView(buffer); const POINT_SIZE = 44; // 11 floats * 4 bytes const numPoints = Math.floor(buffer.byteLength / POINT_SIZE); let plyHeader = [ "ply", "format ascii 1.0", `element vertex ${numPoints}`, "property float x", "property float y", "property float z", "property float scale_0", "property float scale_1", "property float scale_2", "property float rot_0", "property float rot_1", "property float rot_2", "property float rot_3", "property float opacity", "end_header", "", ].join("\n"); // 2. Extract all 11 properties for every point // We use an array of strings to build the body efficiently const rows: string[] = []; for (let i = 0; i < numPoints; i++) { const offset = i * POINT_SIZE; const pointData = []; for (let j = 0; j < 11; j++) { // getFloat32(offset, littleEndian: true) const val = view.getFloat32(offset + j * 4, true); pointData.push(val.toFixed(6)); } rows.push(pointData.join(" ")); // Periodically clear memory pressure if the scene is massive if (i % 100000 === 0 && i > 0) { self.postMessage({ type: "LOG", message: `Processing Environment: ${i.toLocaleString()} splats...`, }); } } return new Blob([plyHeader + rows.join("\n")], { type: "application/octet-stream", }); } // --- WORKER INFRASTRUCTURE --- const originalFetch = globalThis.fetch; globalThis.fetch = async (input, init) => { const url = input instanceof Request ? input.url : input.toString(); if (url.includes("webp.wasm")) return originalFetch("/workers/webp.wasm", init); return originalFetch(input, init); }; self.onmessage = async (e: MessageEvent) => { const { type, filesData, mainLccName, fileName } = e.data; if (type === "START_CONVERSION") { try { const generatedFiles: { name: string; blob: Blob }[] = []; // 1. Process LCI (Collision) const lciData = filesData.find((f: any) => f.name === "collision.lci"); if (lciData) { self.postMessage({ type: "LOG", message: "Parsing Collision Mesh..." }); generatedFiles.push({ name: "collision_mesh.ply", blob: parseLci(lciData.buffer), }); } // 2. Process Environment (Point Cloud) const envData = filesData.find((f: any) => f.name === "environment.bin"); if (envData) { self.postMessage({ type: "LOG", message: "Parsing Environment Cloud...", }); generatedFiles.push({ name: "environment_reference.ply", blob: parseEnvironment(envData.buffer), }); } // 3. Process Splat Transformation (SOG / LODs) self.postMessage({ type: "LOG", message: "Initializing PlayCanvas Splat Transform...", }); const { readFile, writeFile, MemoryReadFileSystem, MemoryFileSystem, getInputFormat, } = await import("@playcanvas/splat-transform"); const readFs = new MemoryReadFileSystem(); for (const file of filesData) { readFs.set(file.name, new Uint8Array(file.buffer)); } const commonOptions = { iterations: 10, lodSelect: [0, 1, 2, 3, 4], unbundled: false, lodChunkCount: 0, lodChunkExtent: 0, }; const tables = await readFile({ filename: mainLccName, fileSystem: readFs, inputFormat: getInputFormat(mainLccName), params: [], options: { ...commonOptions, iterations: 0 }, }); const mainTable = tables[0]; if (!mainTable) throw new Error("No Splat data found."); // PASS: Single SOG self.postMessage({ type: "LOG", message: "Compiling High-Res SOG..." }); const writeFsSingle = new MemoryFileSystem(); await writeFile( { filename: `${fileName}.sog`, outputFormat: "sog-bundle", dataTable: mainTable, options: commonOptions, }, writeFsSingle, ); const singleData = writeFsSingle.results.get(`${fileName}.sog`); if (singleData) { generatedFiles.push({ name: `${fileName}.sog`, blob: new Blob([new Uint8Array(singleData).buffer]), }); } // PASS: LOD Chunks self.postMessage({ type: "LOG", message: "Compiling LOD Chunks..." }); const writeFsLods = new MemoryFileSystem(); await writeFile( { filename: "meta.json", outputFormat: "sog", dataTable: mainTable, options: { ...commonOptions, unbundled: true, lodChunkCount: 512, lodChunkExtent: 16, }, }, writeFsLods, ); for (const [name, data] of writeFsLods.results.entries()) { generatedFiles.push({ name, blob: new Blob([new Uint8Array(data).buffer]), }); } self.postMessage({ type: "DONE", data: { files: generatedFiles } }); } catch (err: any) { self.postMessage({ type: "LOG", message: `Error: ${err.message}` }); } } };