Files
XgridConverter/app/workers/converter.worker.ts
2026-03-09 16:12:01 +01:00

200 lines
6.1 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
*/
function parseEnvironment(buffer: ArrayBuffer): Blob {
const view = new DataView(buffer);
const POINT_SIZE = 44;
const numPoints = Math.floor(buffer.byteLength / POINT_SIZE);
let ply = `ply\nformat ascii 1.0\nelement vertex ${numPoints}\nproperty float x\nproperty float y\nproperty float z\nproperty float scale_0\nproperty float scale_1\nproperty float scale_2\nproperty float rot_0\nproperty float rot_1\nproperty float rot_2\nproperty float rot_3\nproperty float opacity\nend_header\n`;
for (let i = 0; i < numPoints; i++) {
const offset = i * POINT_SIZE;
const row = [];
for (let j = 0; j < 11; j++) {
row.push(view.getFloat32(offset + j * 4, true).toFixed(6));
}
ply += row.join(" ") + "\n";
}
return new Blob([ply], { 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}` });
}
}
};