moved to full client

This commit is contained in:
Andreas Wilms
2026-03-09 16:12:01 +01:00
parent 8a1f133e50
commit 7293839a19
5 changed files with 149 additions and 676 deletions

View File

@@ -1,56 +1,117 @@
/* 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();
self.postMessage({ type: "LOG", message: `FETCH: ${url}` });
if (url.includes("webp.wasm")) {
self.postMessage({
type: "LOG",
message: `INTERCEPTED → /workers/webp.wasm`,
});
const res = await originalFetch("/workers/webp.wasm", init);
self.postMessage({
type: "LOG",
message: `WASM response status: ${res.status}`,
});
return res;
}
if (url.includes("webp.wasm"))
return originalFetch("/workers/webp.wasm", init);
return originalFetch(input, init);
};
// Intercept XMLHttpRequest (Emscripten uses this in Workers)
if (typeof XMLHttpRequest !== "undefined") {
const originalOpen = XMLHttpRequest.prototype.open;
// @ts-ignore
XMLHttpRequest.prototype.open = function (
method: string,
url: string | URL,
...rest: any[]
) {
if (typeof url === "string" && url.includes("webp.wasm")) {
url = "/workers/webp.wasm";
}
return originalOpen.apply(this, [method, url, ...rest] as any);
};
}
self.onmessage = async (e: MessageEvent) => {
const { type, filesData, mainLccName, fileName } = e.data;
if (type === "START_CONVERSION") {
try {
self.postMessage({ type: "LOG", message: "Initialisiere..." });
const generatedFiles: { name: string; blob: Blob }[] = [];
// Emscripten's native locateFile hook
// @ts-ignore
globalThis.Module = globalThis.Module || {};
// @ts-ignore
globalThis.Module.locateFile = function (path: string) {
if (path.endsWith(".wasm")) {
return new URL("/webp.wasm", self.location.origin).href;
}
return path;
};
// 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,
@@ -61,122 +122,78 @@ self.onmessage = async (e: MessageEvent) => {
} = await import("@playcanvas/splat-transform");
const readFs = new MemoryReadFileSystem();
self.postMessage({
type: "LOG",
message: "Lade Dateien in den virtuellen Speicher...",
});
for (const file of filesData) {
readFs.set(file.name, new Uint8Array(file.buffer));
}
const readOptions = {
iterations: 0,
lodSelect: [0, 1, 2, 3, 4], // we have captured a total level of 5
const commonOptions = {
iterations: 10,
lodSelect: [0, 1, 2, 3, 4],
unbundled: false,
lodChunkCount: 0,
lodChunkExtent: 0,
};
self.postMessage({ type: "LOG", message: "Lese LCC und Binärdaten..." });
const tables = await readFile({
filename: mainLccName,
fileSystem: readFs,
inputFormat: getInputFormat(mainLccName),
params: [],
options: readOptions,
options: { ...commonOptions, iterations: 0 },
});
const mainTable = tables[0];
if (!mainTable) throw new Error("Keine Splat-Daten gefunden.");
const generatedFiles: { name: string; blob: Blob }[] = [];
// PASS 1: Generate Single High-Quality SOG
self.postMessage({ type: "LOG", message: "Kompiliere Single SOG..." });
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();
const singleOutputName = `${fileName}.sog`;
const singleOptions = {
...readOptions,
iterations: 10,
unbundled: false,
};
await writeFile(
{
filename: singleOutputName,
filename: `${fileName}.sog`,
outputFormat: "sog-bundle",
dataTable: mainTable,
options: singleOptions,
options: commonOptions,
},
writeFsSingle,
);
const singleSogData = writeFsSingle.results.get(singleOutputName);
if (singleSogData) {
const singleData = writeFsSingle.results.get(`${fileName}.sog`);
if (singleData) {
generatedFiles.push({
name: singleOutputName,
blob: new Blob([new Uint8Array(singleSogData).slice().buffer], {
type: "application/octet-stream",
}),
name: `${fileName}.sog`,
blob: new Blob([new Uint8Array(singleData).buffer]),
});
}
// ==========================================
// PASS 2: Generate Unbundled LOD SOGs + JSON
// ==========================================
self.postMessage({ type: "LOG", message: "Kompiliere LOD Chunks..." });
// PASS: LOD Chunks
self.postMessage({ type: "LOG", message: "Compiling LOD Chunks..." });
const writeFsLods = new MemoryFileSystem();
// MUST be exactly "meta.json" for unbundled SOG format
const lodsOutputName = "meta.json";
const lodOptions = {
...readOptions,
iterations: 10,
unbundled: true,
lodChunkCount: 512,
lodChunkExtent: 16,
};
await writeFile(
{
filename: lodsOutputName,
filename: "meta.json",
outputFormat: "sog",
dataTable: mainTable,
options: lodOptions,
options: {
...commonOptions,
unbundled: true,
lodChunkCount: 512,
lodChunkExtent: 16,
},
},
writeFsLods,
);
// Jetzt iterieren wir über alle generierten Dateien im System
for (const [generatedName, data] of writeFsLods.results.entries()) {
const mimeType = generatedName.endsWith(".json")
? "application/json"
: "application/octet-stream";
for (const [name, data] of writeFsLods.results.entries()) {
generatedFiles.push({
name: generatedName,
blob: new Blob([new Uint8Array(data).slice().buffer], {
type: mimeType,
}),
name,
blob: new Blob([new Uint8Array(data).buffer]),
});
}
// Send all Data to Frontend
self.postMessage({
type: "DONE",
data: {
files: generatedFiles,
},
});
self.postMessage({ type: "DONE", data: { files: generatedFiles } });
} catch (err: any) {
self.postMessage({ type: "LOG", message: `Fehler: ${err.message}` });
console.error(err);
self.postMessage({ type: "LOG", message: `Error: ${err.message}` });
}
}
};