259 lines
8.4 KiB
TypeScript
259 lines
8.4 KiB
TypeScript
/* eslint-disable no-restricted-globals */
|
|
import JSZip from "jszip";
|
|
|
|
// --- BINARY PARSERS ---
|
|
|
|
function parseLci(buffer: ArrayBuffer): Blob {
|
|
const view = new DataView(buffer);
|
|
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({
|
|
dataOffset: Number(view.getBigUint64(offset + 8, true)),
|
|
vertexNum: view.getUint32(offset + 24, true),
|
|
faceNum: view.getUint32(offset + 28, true),
|
|
});
|
|
}
|
|
let vStr = "";
|
|
let fStr = "";
|
|
let vOff = 0;
|
|
for (const m of meshes) {
|
|
let pos = m.dataOffset;
|
|
for (let j = 0; j < m.vertexNum; j++) {
|
|
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++) {
|
|
fStr += `3 ${view.getUint32(pos, true) + vOff} ${view.getUint32(pos + 4, true) + vOff} ${view.getUint32(pos + 8, true) + vOff}\n`;
|
|
pos += 12;
|
|
}
|
|
vOff += m.vertexNum;
|
|
}
|
|
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" });
|
|
}
|
|
|
|
function parseEnvironment(buffer: ArrayBuffer): Blob {
|
|
const view = new DataView(buffer);
|
|
const POINT_SIZE = 44;
|
|
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 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",
|
|
"property float rot_0",
|
|
"property float rot_1",
|
|
"property float rot_2",
|
|
"property float rot_3",
|
|
"property float opacity",
|
|
"end_header",
|
|
"",
|
|
].join("\n");
|
|
|
|
const rows: string[] = [];
|
|
|
|
for (let i = 0; i < numPoints; i++) {
|
|
const offset = i * POINT_SIZE;
|
|
|
|
// 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";
|
|
|
|
// 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);
|
|
|
|
// 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")], {
|
|
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 zip = new JSZip();
|
|
self.postMessage({ type: "LOG", message: "Initialisiere Pipeline..." });
|
|
|
|
// 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));
|
|
|
|
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 f of filesData) readFs.set(f.name, new Uint8Array(f.buffer));
|
|
|
|
// 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 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...`,
|
|
});
|
|
|
|
// 5. Combine all tagged LOD tables
|
|
const combined = combine(taggedTables);
|
|
|
|
// 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 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: "lod-meta.json",
|
|
outputFormat,
|
|
dataTable: combined,
|
|
options: {
|
|
iterations: 10,
|
|
lodSelect: lodSelectAll,
|
|
unbundled: true,
|
|
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()) {
|
|
zip.file(name, data);
|
|
}
|
|
|
|
// 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: `Fehler: ${err.message}` });
|
|
}
|
|
}
|
|
};
|