Skip to content

Commit 4266db3

Browse files
authored
PC SOGS decoding from .Zip archive (#100)
* Implement PC SOGS decoding from .zip file archive. Update viewer and editor to accept .zip files. * Restore GL WEBP decoding.
1 parent cd40e13 commit 4266db3

File tree

5 files changed

+136
-12
lines changed

5 files changed

+136
-12
lines changed

examples/editor/index.html

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
<canvas id="canvas" tabindex="0"></canvas>
6464
<div id="main-gui"></div>
6565
<div id="second-gui"></div>
66-
<input id="file-input" type="file" accept=".ply,.spz,.splat,.ksplat" multiple="true" style="display: none;" />
66+
<input id="file-input" type="file" accept=".ply,.spz,.splat,.ksplat,.zip" multiple="true" style="display: none;" />
6767
<script type="module">
6868
import * as THREE from "three";
6969
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
@@ -154,7 +154,7 @@
154154
loadFiles(urls);
155155
guiOptions.loadFromText = ""; // Clear after loading
156156
} else {
157-
alert("No valid URLs found in text. URLs must start with http:// or https:// and end with .ply, .spz, .splat, .ksplat, or .json");
157+
alert("No valid URLs found in text. URLs must start with http:// or https:// and end with .ply, .spz, .splat, .ksplat, .json, or .zip");
158158
}
159159
}
160160
},
@@ -683,7 +683,8 @@
683683
file.name.toLowerCase().endsWith('.ply') ||
684684
file.name.toLowerCase().endsWith('.spz') ||
685685
file.name.toLowerCase().endsWith('.splat') ||
686-
file.name.toLowerCase().endsWith('.ksplat')
686+
file.name.toLowerCase().endsWith('.ksplat') ||
687+
file.name.toLowerCase().endsWith('.zip')
687688
);
688689

689690
if (splatFiles.length > 0) {
@@ -693,7 +694,7 @@
693694

694695
// Parse URLs from text (handles single URLs, multiple lines, mixed content)
695696
function parseURLsFromText(text) {
696-
const supportedExtensions = [".ply", ".spz", ".splat", ".ksplat", ".json"];
697+
const supportedExtensions = [".ply", ".spz", ".splat", ".ksplat", ".zip", ".json"];
697698
const urls = [];
698699

699700
// Split by lines, commas, and semicolons

examples/viewer/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@
347347
</div>
348348
<div class="drop-zone">Drag and drop a splat file here</div>
349349
<label for="file-input" class="button upload"><img class="upload-icon" src="upload-icon.svg" alt="upload" />Choose file</label>
350-
<input id="file-input" class="hidden" accept=".ply,.spz,.splat,.ksplat" type="file" />
350+
<input id="file-input" class="hidden" accept=".ply,.spz,.splat,.ksplat,.zip" type="file" />
351351
<form class="url-form">
352352
<input class="url-input" type="text" placeholder="Copy splat URL" />
353353
</form>

src/SplatLoader.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { unzipSync } from "fflate";
12
import { FileLoader, Loader, type LoadingManager } from "three";
23
import { PackedSplats } from "./PackedSplats";
34
import { SplatMesh } from "./SplatMesh";
@@ -138,6 +139,7 @@ export enum SplatFileType {
138139
SPLAT = "splat",
139140
KSPLAT = "ksplat",
140141
PCSOGS = "pcsogs",
142+
PCSOGSZIP = "pcsogszip",
141143
}
142144

143145
export function getSplatFileType(
@@ -157,6 +159,14 @@ export function getSplatFileType(
157159
// Unknown Gzipped file type
158160
return undefined;
159161
}
162+
if (view.getUint32(0, true) === 0x04034b50) {
163+
// PKZip file
164+
if (tryPcSogsZip(fileBytes)) {
165+
return SplatFileType.PCSOGSZIP;
166+
}
167+
// Unknown PKZip file type
168+
return undefined;
169+
}
160170
// Unknown file type
161171
return undefined;
162172
}
@@ -218,7 +228,7 @@ export type PcSogsJson = {
218228
maxs: number[];
219229
files: string[];
220230
};
221-
shN: {
231+
shN?: {
222232
shape: number[];
223233
dtype: string;
224234
mins: number;
@@ -277,6 +287,37 @@ export function tryPcSogs(
277287
}
278288
}
279289

290+
export function tryPcSogsZip(
291+
input: ArrayBuffer | Uint8Array,
292+
): { name: string; json: PcSogsJson } | undefined {
293+
try {
294+
const fileBytes =
295+
input instanceof ArrayBuffer ? new Uint8Array(input) : input;
296+
let metaFilename: string | null = null;
297+
298+
const unzipped = unzipSync(fileBytes, {
299+
filter: ({ name }) => {
300+
const filename = name.split(/[\\/]/).pop() as string;
301+
if (filename === "meta.json") {
302+
metaFilename = name;
303+
return true;
304+
}
305+
return false;
306+
},
307+
});
308+
if (!metaFilename) {
309+
return undefined;
310+
}
311+
const json = tryPcSogs(unzipped[metaFilename]);
312+
if (!json) {
313+
return undefined;
314+
}
315+
return { name: metaFilename, json };
316+
} catch {
317+
return undefined;
318+
}
319+
}
320+
280321
export async function unpackSplats({
281322
input,
282323
extraFiles,
@@ -373,6 +414,19 @@ export async function unpackSplats({
373414
return { packedArray, numSplats, extra };
374415
});
375416
}
417+
case SplatFileType.PCSOGSZIP: {
418+
return await withWorker(async (worker) => {
419+
const { packedArray, numSplats, extra } = (await worker.call(
420+
"decodePcSogsZip",
421+
{ fileBytes },
422+
)) as {
423+
packedArray: Uint32Array;
424+
numSplats: number;
425+
extra: Record<string, unknown>;
426+
};
427+
return { packedArray, numSplats, extra };
428+
});
429+
}
376430
default: {
377431
throw new Error(`Unknown splat file type: ${splatFileType}`);
378432
}

src/pcsogs.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { PcSogsJson } from "./SplatLoader";
1+
import { unzip } from "fflate";
2+
import { type PcSogsJson, tryPcSogsZip } from "./SplatLoader";
23
import {
34
computeMaxSplats,
45
encodeSh1Rgb,
@@ -11,14 +12,13 @@ import {
1112
} from "./utils";
1213

1314
export async function unpackPcSogs(
14-
fileBytes: Uint8Array,
15+
json: PcSogsJson,
1516
extraFiles: Record<string, ArrayBuffer>,
1617
): Promise<{
1718
packedArray: Uint32Array;
1819
numSplats: number;
1920
extra: Record<string, unknown>;
2021
}> {
21-
const json = JSON.parse(new TextDecoder().decode(fileBytes)) as PcSogsJson;
2222
if (json.quats.encoding !== "quaternion_packed") {
2323
throw new Error("Unsupported quaternion encoding");
2424
}
@@ -204,3 +204,58 @@ async function decodeImageRgba(fileBytes: ArrayBuffer) {
204204
const { rgba } = await decodeImage(fileBytes);
205205
return rgba;
206206
}
207+
208+
export async function unpackPcSogsZip(fileBytes: Uint8Array): Promise<{
209+
packedArray: Uint32Array;
210+
numSplats: number;
211+
extra: Record<string, unknown>;
212+
}> {
213+
const nameJson = tryPcSogsZip(fileBytes);
214+
if (!nameJson) {
215+
throw new Error("Invalid PC SOGS zip file");
216+
}
217+
const { name, json } = nameJson;
218+
// Find path prefix, will be -1 if no / or \
219+
const lastSlash = name.lastIndexOf("/");
220+
const lastBackslash = name.lastIndexOf("\\");
221+
const prefix = name.slice(0, Math.max(lastSlash, lastBackslash) + 1);
222+
223+
const fileMap = new Map<string, string>();
224+
const refFiles = [
225+
...json.means.files,
226+
...json.scales.files,
227+
...json.quats.files,
228+
...json.sh0.files,
229+
...(json.shN?.files ?? []),
230+
];
231+
for (const file of refFiles) {
232+
fileMap.set(prefix + file, file);
233+
}
234+
235+
const unzipped = await new Promise<Record<string, ArrayBuffer>>(
236+
(resolve, reject) => {
237+
unzip(
238+
fileBytes,
239+
{
240+
filter: ({ name }) => {
241+
return fileMap.has(name);
242+
},
243+
},
244+
(err, files) => {
245+
if (err) {
246+
reject(err);
247+
} else {
248+
resolve(files);
249+
}
250+
},
251+
);
252+
},
253+
);
254+
255+
const extraFiles: Record<string, ArrayBuffer> = {};
256+
for (const [full, name] of fileMap.entries()) {
257+
extraFiles[name] = unzipped[full];
258+
}
259+
260+
return await unpackPcSogs(json, extraFiles);
261+
}

src/worker.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import init_wasm, { sort_splats } from "spark-internal-rs";
2-
import type { TranscodeSpzInput } from "./SplatLoader";
2+
import type { PcSogsJson, TranscodeSpzInput } from "./SplatLoader";
33
import { unpackAntiSplat } from "./antisplat";
44
import { SCALE_MIN, WASM_SPLAT_SORT } from "./defines";
55
import { unpackKsplat } from "./ksplat";
6-
import { unpackPcSogs } from "./pcsogs";
6+
import { unpackPcSogs, unpackPcSogsZip } from "./pcsogs";
77
import { PlyReader } from "./ply";
88
import { SpzReader, transcodeSpz } from "./spz";
99
import {
@@ -86,7 +86,21 @@ async function onMessage(event: MessageEvent) {
8686
fileBytes: Uint8Array;
8787
extraFiles: Record<string, ArrayBuffer>;
8888
};
89-
const decoded = await unpackPcSogs(fileBytes, extraFiles);
89+
const json = JSON.parse(
90+
new TextDecoder().decode(fileBytes),
91+
) as PcSogsJson;
92+
const decoded = await unpackPcSogs(json, extraFiles);
93+
result = {
94+
id,
95+
numSplats: decoded.numSplats,
96+
packedArray: decoded.packedArray,
97+
extra: decoded.extra,
98+
};
99+
break;
100+
}
101+
case "decodePcSogsZip": {
102+
const { fileBytes } = args as { fileBytes: Uint8Array };
103+
const decoded = await unpackPcSogsZip(fileBytes);
90104
result = {
91105
id,
92106
numSplats: decoded.numSplats,

0 commit comments

Comments
 (0)