240 lines
6.9 KiB
TypeScript
240 lines
6.9 KiB
TypeScript
/* 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}` });
|
|
}
|
|
}
|
|
};
|