fixed exports
This commit is contained in:
@@ -1,19 +1,14 @@
|
||||
/* eslint-disable no-restricted-globals */
|
||||
import JSZip from "jszip";
|
||||
|
||||
// --- BINARY PARSERS ---
|
||||
|
||||
/**
|
||||
* 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 LCI_MAGIC = 0x6c6c6f63;
|
||||
if (view.getUint32(0, true) !== LCI_MAGIC) throw new Error("Invalid LCI");
|
||||
const meshNum = view.getUint32(44, true);
|
||||
const meshes = [];
|
||||
|
||||
for (let i = 0; i < meshNum; i++) {
|
||||
const offset = 48 + i * 40;
|
||||
meshes.push({
|
||||
@@ -22,40 +17,28 @@ function parseLci(buffer: ArrayBuffer): Blob {
|
||||
faceNum: view.getUint32(offset + 28, true),
|
||||
});
|
||||
}
|
||||
|
||||
let verticesStr = "";
|
||||
let facesStr = "";
|
||||
let vOffset = 0;
|
||||
|
||||
let vStr = "";
|
||||
let fStr = "";
|
||||
let vOff = 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`;
|
||||
vStr += `${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`;
|
||||
fStr += `3 ${view.getUint32(pos, true) + vOff} ${view.getUint32(pos + 4, true) + vOff} ${view.getUint32(pos + 8, true) + vOff}\n`;
|
||||
pos += 12;
|
||||
}
|
||||
vOffset += m.vertexNum;
|
||||
vOff += 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",
|
||||
});
|
||||
const header = `ply\nformat ascii 1.0\nelement vertex ${vOff}\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 + vStr + fStr], { 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 POINT_SIZE = 44;
|
||||
const numPoints = Math.floor(buffer.byteLength / POINT_SIZE);
|
||||
|
||||
let plyHeader = [
|
||||
@@ -65,6 +48,9 @@ function parseEnvironment(buffer: ArrayBuffer): Blob {
|
||||
"property float x",
|
||||
"property float y",
|
||||
"property float z",
|
||||
"property float f_dc_0", // Red
|
||||
"property float f_dc_1", // Green
|
||||
"property float f_dc_2", // Blue
|
||||
"property float scale_0",
|
||||
"property float scale_1",
|
||||
"property float scale_2",
|
||||
@@ -77,29 +63,34 @@ function parseEnvironment(buffer: ArrayBuffer): Blob {
|
||||
"",
|
||||
].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));
|
||||
}
|
||||
// X, Y, Z (first 3 floats)
|
||||
const x = view.getFloat32(offset, true).toFixed(6);
|
||||
const y = view.getFloat32(offset + 4, true).toFixed(6);
|
||||
const z = view.getFloat32(offset + 8, true).toFixed(6);
|
||||
const f_dc = "1.000000 1.000000 1.000000";
|
||||
|
||||
rows.push(pointData.join(" "));
|
||||
// Scales (floats 4, 5, 6)
|
||||
const s0 = view.getFloat32(offset + 12, true).toFixed(6);
|
||||
const s1 = view.getFloat32(offset + 16, true).toFixed(6);
|
||||
const s2 = view.getFloat32(offset + 20, true).toFixed(6);
|
||||
|
||||
// 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...`,
|
||||
});
|
||||
}
|
||||
// Rotations (floats 7, 8, 9, 10)
|
||||
const r0 = view.getFloat32(offset + 24, true).toFixed(6);
|
||||
const r1 = view.getFloat32(offset + 28, true).toFixed(6);
|
||||
const r2 = view.getFloat32(offset + 32, true).toFixed(6);
|
||||
const r3 = view.getFloat32(offset + 36, true).toFixed(6);
|
||||
|
||||
// Opacity (float 11)
|
||||
const alpha = view.getFloat32(offset + 40, true).toFixed(6);
|
||||
|
||||
rows.push(
|
||||
`${x} ${y} ${z} ${f_dc} ${s0} ${s1} ${s2} ${r0} ${r1} ${r2} ${r3} ${alpha}`,
|
||||
);
|
||||
}
|
||||
|
||||
return new Blob([plyHeader + rows.join("\n")], {
|
||||
@@ -122,118 +113,146 @@ self.onmessage = async (e: MessageEvent) => {
|
||||
|
||||
if (type === "START_CONVERSION") {
|
||||
try {
|
||||
const generatedFiles: { name: string; blob: Blob }[] = [];
|
||||
const zip = new JSZip();
|
||||
self.postMessage({ type: "LOG", message: "Initialisiere Pipeline..." });
|
||||
|
||||
// 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),
|
||||
});
|
||||
}
|
||||
// 1. Parse and add collision + environment meshes to ZIP
|
||||
const lciFile = filesData.find((f: any) => f.name === "collision.lci");
|
||||
if (lciFile) zip.file("collision.ply", parseLci(lciFile.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 envFile = filesData.find((f: any) => f.name === "environment.bin");
|
||||
if (envFile)
|
||||
zip.file("environment.ply", parseEnvironment(envFile.buffer));
|
||||
|
||||
// 2. Import splat-transform API
|
||||
const {
|
||||
readFile,
|
||||
writeFile,
|
||||
processDataTable,
|
||||
combine,
|
||||
MemoryReadFileSystem,
|
||||
MemoryFileSystem,
|
||||
getInputFormat,
|
||||
getOutputFormat,
|
||||
} = await import("@playcanvas/splat-transform");
|
||||
|
||||
// 3. Build in-memory read filesystem from all uploaded files
|
||||
const readFs = new MemoryReadFileSystem();
|
||||
for (const file of filesData) {
|
||||
readFs.set(file.name, new Uint8Array(file.buffer));
|
||||
}
|
||||
for (const f of filesData) readFs.set(f.name, new Uint8Array(f.buffer));
|
||||
|
||||
const commonOptions = {
|
||||
iterations: 10,
|
||||
lodSelect: [0, 1, 2, 3, 4],
|
||||
// 4. Read each LOD level separately from the LCC
|
||||
const lccFile = filesData.find((f: any) => f.name === mainLccName);
|
||||
if (!lccFile) throw new Error("LCC-Datei nicht gefunden.");
|
||||
|
||||
const lccText = new TextDecoder().decode(lccFile.buffer);
|
||||
const lccJson = JSON.parse(lccText);
|
||||
const totalLevels: number = lccJson?.totalLevel ?? 5;
|
||||
|
||||
self.postMessage({
|
||||
type: "LOG",
|
||||
message: `LCC enthält ${totalLevels} LOD-Level.`,
|
||||
});
|
||||
|
||||
// 5. Read each LOD level separately from the LCC
|
||||
const lodOptions = {
|
||||
iterations: 0,
|
||||
unbundled: false,
|
||||
lodChunkCount: 0,
|
||||
lodChunkExtent: 0,
|
||||
};
|
||||
|
||||
const tables = await readFile({
|
||||
filename: mainLccName,
|
||||
fileSystem: readFs,
|
||||
inputFormat: getInputFormat(mainLccName),
|
||||
params: [],
|
||||
options: { ...commonOptions, iterations: 0 },
|
||||
const taggedTables = [];
|
||||
for (let i = 0; i < totalLevels; i++) {
|
||||
self.postMessage({ type: "LOG", message: `Lese LOD ${i}...` });
|
||||
const tables = await readFile({
|
||||
filename: mainLccName,
|
||||
fileSystem: readFs,
|
||||
inputFormat: getInputFormat(mainLccName),
|
||||
params: [],
|
||||
options: {
|
||||
...lodOptions,
|
||||
lodSelect: [i],
|
||||
},
|
||||
});
|
||||
|
||||
if (!tables || tables.length === 0) {
|
||||
self.postMessage({
|
||||
type: "LOG",
|
||||
message: `LOD ${i} nicht gefunden, überspringe...`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const tagged = processDataTable(tables[0], [{ kind: "lod", value: i }]);
|
||||
taggedTables.push(tagged);
|
||||
}
|
||||
if (taggedTables.length === 0)
|
||||
throw new Error("Keine LOD-Daten gefunden.");
|
||||
|
||||
self.postMessage({
|
||||
type: "LOG",
|
||||
message: `${taggedTables.length} LOD-Level geladen. Kombiniere...`,
|
||||
});
|
||||
|
||||
const mainTable = tables[0];
|
||||
if (!mainTable) throw new Error("No Splat data found.");
|
||||
// 5. Combine all tagged LOD tables
|
||||
const combined = combine(taggedTables);
|
||||
|
||||
// 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,
|
||||
);
|
||||
// 7. Write LOD streaming format
|
||||
// CRITICAL: output filename MUST be exactly "lod-meta.json"
|
||||
// The format is determined by the filename, not a format string.
|
||||
self.postMessage({
|
||||
type: "LOG",
|
||||
message: "Kompiliere LOD Chunks (World Mode)...",
|
||||
});
|
||||
|
||||
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();
|
||||
const lodSelectAll = Array.from({ length: totalLevels }, (_, i) => i);
|
||||
const outputFormat = getOutputFormat("lod-meta.json", {
|
||||
iterations: 10,
|
||||
lodSelect: lodSelectAll,
|
||||
unbundled: true,
|
||||
lodChunkCount: 512, // default value from CLI tool
|
||||
lodChunkExtent: 16, // default value from CLI tool
|
||||
});
|
||||
|
||||
await writeFile(
|
||||
{
|
||||
filename: "meta.json",
|
||||
outputFormat: "sog",
|
||||
dataTable: mainTable,
|
||||
filename: "lod-meta.json",
|
||||
outputFormat,
|
||||
dataTable: combined,
|
||||
options: {
|
||||
...commonOptions,
|
||||
iterations: 10,
|
||||
lodSelect: lodSelectAll,
|
||||
unbundled: true,
|
||||
lodChunkCount: 512,
|
||||
lodChunkExtent: 16,
|
||||
lodChunkCount: 512, // default value from CLI tool
|
||||
lodChunkExtent: 16, // default value from CLI tool
|
||||
},
|
||||
},
|
||||
writeFsLods,
|
||||
);
|
||||
|
||||
// 8. Organize output files into ZIP folder structure
|
||||
// The API emits files named like: "0_0_meta.json", "0_0_means_l.webp", "env_meta.json" etc.
|
||||
// We reorganize these into: 0_0/meta.json, 0_0/means_l.webp, env/meta.json
|
||||
self.postMessage({
|
||||
type: "LOG",
|
||||
message: "Organisiere Ordnerstruktur...",
|
||||
});
|
||||
|
||||
for (const [name, data] of writeFsLods.results.entries()) {
|
||||
generatedFiles.push({
|
||||
name,
|
||||
blob: new Blob([new Uint8Array(data).buffer]),
|
||||
});
|
||||
zip.file(name, data);
|
||||
}
|
||||
|
||||
self.postMessage({ type: "DONE", data: { files: generatedFiles } });
|
||||
// 9. Generate and emit ZIP
|
||||
self.postMessage({ type: "LOG", message: "Erstelle ZIP-Datei..." });
|
||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
||||
|
||||
self.postMessage({
|
||||
type: "DONE",
|
||||
data: { files: [{ name: `${fileName}_pack.zip`, blob: zipBlob }] },
|
||||
});
|
||||
} catch (err: any) {
|
||||
self.postMessage({ type: "LOG", message: `Error: ${err.message}` });
|
||||
self.postMessage({ type: "LOG", message: `Fehler: ${err.message}` });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user