mirror of
https://github.com/storytold/spark.git
synced 2026-10-12 23:49:01 +00:00
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.
This commit is contained in:
committed by
GitHub
parent
cd40e139d1
commit
4266db3259
@@ -63,7 +63,7 @@
|
||||
<canvas id="canvas" tabindex="0"></canvas>
|
||||
<div id="main-gui"></div>
|
||||
<div id="second-gui"></div>
|
||||
<input id="file-input" type="file" accept=".ply,.spz,.splat,.ksplat" multiple="true" style="display: none;" />
|
||||
<input id="file-input" type="file" accept=".ply,.spz,.splat,.ksplat,.zip" multiple="true" style="display: none;" />
|
||||
<script type="module">
|
||||
import * as THREE from "three";
|
||||
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
||||
@@ -154,7 +154,7 @@
|
||||
loadFiles(urls);
|
||||
guiOptions.loadFromText = ""; // Clear after loading
|
||||
} else {
|
||||
alert("No valid URLs found in text. URLs must start with http:// or https:// and end with .ply, .spz, .splat, .ksplat, or .json");
|
||||
alert("No valid URLs found in text. URLs must start with http:// or https:// and end with .ply, .spz, .splat, .ksplat, .json, or .zip");
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -683,7 +683,8 @@
|
||||
file.name.toLowerCase().endsWith('.ply') ||
|
||||
file.name.toLowerCase().endsWith('.spz') ||
|
||||
file.name.toLowerCase().endsWith('.splat') ||
|
||||
file.name.toLowerCase().endsWith('.ksplat')
|
||||
file.name.toLowerCase().endsWith('.ksplat') ||
|
||||
file.name.toLowerCase().endsWith('.zip')
|
||||
);
|
||||
|
||||
if (splatFiles.length > 0) {
|
||||
@@ -693,7 +694,7 @@
|
||||
|
||||
// Parse URLs from text (handles single URLs, multiple lines, mixed content)
|
||||
function parseURLsFromText(text) {
|
||||
const supportedExtensions = [".ply", ".spz", ".splat", ".ksplat", ".json"];
|
||||
const supportedExtensions = [".ply", ".spz", ".splat", ".ksplat", ".zip", ".json"];
|
||||
const urls = [];
|
||||
|
||||
// Split by lines, commas, and semicolons
|
||||
|
||||
@@ -347,7 +347,7 @@
|
||||
</div>
|
||||
<div class="drop-zone">Drag and drop a splat file here</div>
|
||||
<label for="file-input" class="button upload"><img class="upload-icon" src="upload-icon.svg" alt="upload" />Choose file</label>
|
||||
<input id="file-input" class="hidden" accept=".ply,.spz,.splat,.ksplat" type="file" />
|
||||
<input id="file-input" class="hidden" accept=".ply,.spz,.splat,.ksplat,.zip" type="file" />
|
||||
<form class="url-form">
|
||||
<input class="url-input" type="text" placeholder="Copy splat URL" />
|
||||
</form>
|
||||
|
||||
+55
-1
@@ -1,3 +1,4 @@
|
||||
import { unzipSync } from "fflate";
|
||||
import { FileLoader, Loader, type LoadingManager } from "three";
|
||||
import { PackedSplats } from "./PackedSplats";
|
||||
import { SplatMesh } from "./SplatMesh";
|
||||
@@ -138,6 +139,7 @@ export enum SplatFileType {
|
||||
SPLAT = "splat",
|
||||
KSPLAT = "ksplat",
|
||||
PCSOGS = "pcsogs",
|
||||
PCSOGSZIP = "pcsogszip",
|
||||
}
|
||||
|
||||
export function getSplatFileType(
|
||||
@@ -157,6 +159,14 @@ export function getSplatFileType(
|
||||
// Unknown Gzipped file type
|
||||
return undefined;
|
||||
}
|
||||
if (view.getUint32(0, true) === 0x04034b50) {
|
||||
// PKZip file
|
||||
if (tryPcSogsZip(fileBytes)) {
|
||||
return SplatFileType.PCSOGSZIP;
|
||||
}
|
||||
// Unknown PKZip file type
|
||||
return undefined;
|
||||
}
|
||||
// Unknown file type
|
||||
return undefined;
|
||||
}
|
||||
@@ -218,7 +228,7 @@ export type PcSogsJson = {
|
||||
maxs: number[];
|
||||
files: string[];
|
||||
};
|
||||
shN: {
|
||||
shN?: {
|
||||
shape: number[];
|
||||
dtype: string;
|
||||
mins: number;
|
||||
@@ -277,6 +287,37 @@ export function tryPcSogs(
|
||||
}
|
||||
}
|
||||
|
||||
export function tryPcSogsZip(
|
||||
input: ArrayBuffer | Uint8Array,
|
||||
): { name: string; json: PcSogsJson } | undefined {
|
||||
try {
|
||||
const fileBytes =
|
||||
input instanceof ArrayBuffer ? new Uint8Array(input) : input;
|
||||
let metaFilename: string | null = null;
|
||||
|
||||
const unzipped = unzipSync(fileBytes, {
|
||||
filter: ({ name }) => {
|
||||
const filename = name.split(/[\\/]/).pop() as string;
|
||||
if (filename === "meta.json") {
|
||||
metaFilename = name;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
if (!metaFilename) {
|
||||
return undefined;
|
||||
}
|
||||
const json = tryPcSogs(unzipped[metaFilename]);
|
||||
if (!json) {
|
||||
return undefined;
|
||||
}
|
||||
return { name: metaFilename, json };
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function unpackSplats({
|
||||
input,
|
||||
extraFiles,
|
||||
@@ -373,6 +414,19 @@ export async function unpackSplats({
|
||||
return { packedArray, numSplats, extra };
|
||||
});
|
||||
}
|
||||
case SplatFileType.PCSOGSZIP: {
|
||||
return await withWorker(async (worker) => {
|
||||
const { packedArray, numSplats, extra } = (await worker.call(
|
||||
"decodePcSogsZip",
|
||||
{ fileBytes },
|
||||
)) as {
|
||||
packedArray: Uint32Array;
|
||||
numSplats: number;
|
||||
extra: Record<string, unknown>;
|
||||
};
|
||||
return { packedArray, numSplats, extra };
|
||||
});
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown splat file type: ${splatFileType}`);
|
||||
}
|
||||
|
||||
+58
-3
@@ -1,4 +1,5 @@
|
||||
import type { PcSogsJson } from "./SplatLoader";
|
||||
import { unzip } from "fflate";
|
||||
import { type PcSogsJson, tryPcSogsZip } from "./SplatLoader";
|
||||
import {
|
||||
computeMaxSplats,
|
||||
encodeSh1Rgb,
|
||||
@@ -11,14 +12,13 @@ import {
|
||||
} from "./utils";
|
||||
|
||||
export async function unpackPcSogs(
|
||||
fileBytes: Uint8Array,
|
||||
json: PcSogsJson,
|
||||
extraFiles: Record<string, ArrayBuffer>,
|
||||
): Promise<{
|
||||
packedArray: Uint32Array;
|
||||
numSplats: number;
|
||||
extra: Record<string, unknown>;
|
||||
}> {
|
||||
const json = JSON.parse(new TextDecoder().decode(fileBytes)) as PcSogsJson;
|
||||
if (json.quats.encoding !== "quaternion_packed") {
|
||||
throw new Error("Unsupported quaternion encoding");
|
||||
}
|
||||
@@ -204,3 +204,58 @@ async function decodeImageRgba(fileBytes: ArrayBuffer) {
|
||||
const { rgba } = await decodeImage(fileBytes);
|
||||
return rgba;
|
||||
}
|
||||
|
||||
export async function unpackPcSogsZip(fileBytes: Uint8Array): Promise<{
|
||||
packedArray: Uint32Array;
|
||||
numSplats: number;
|
||||
extra: Record<string, unknown>;
|
||||
}> {
|
||||
const nameJson = tryPcSogsZip(fileBytes);
|
||||
if (!nameJson) {
|
||||
throw new Error("Invalid PC SOGS zip file");
|
||||
}
|
||||
const { name, json } = nameJson;
|
||||
// Find path prefix, will be -1 if no / or \
|
||||
const lastSlash = name.lastIndexOf("/");
|
||||
const lastBackslash = name.lastIndexOf("\\");
|
||||
const prefix = name.slice(0, Math.max(lastSlash, lastBackslash) + 1);
|
||||
|
||||
const fileMap = new Map<string, string>();
|
||||
const refFiles = [
|
||||
...json.means.files,
|
||||
...json.scales.files,
|
||||
...json.quats.files,
|
||||
...json.sh0.files,
|
||||
...(json.shN?.files ?? []),
|
||||
];
|
||||
for (const file of refFiles) {
|
||||
fileMap.set(prefix + file, file);
|
||||
}
|
||||
|
||||
const unzipped = await new Promise<Record<string, ArrayBuffer>>(
|
||||
(resolve, reject) => {
|
||||
unzip(
|
||||
fileBytes,
|
||||
{
|
||||
filter: ({ name }) => {
|
||||
return fileMap.has(name);
|
||||
},
|
||||
},
|
||||
(err, files) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(files);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const extraFiles: Record<string, ArrayBuffer> = {};
|
||||
for (const [full, name] of fileMap.entries()) {
|
||||
extraFiles[name] = unzipped[full];
|
||||
}
|
||||
|
||||
return await unpackPcSogs(json, extraFiles);
|
||||
}
|
||||
|
||||
+17
-3
@@ -1,9 +1,9 @@
|
||||
import init_wasm, { sort_splats } from "spark-internal-rs";
|
||||
import type { TranscodeSpzInput } from "./SplatLoader";
|
||||
import type { PcSogsJson, TranscodeSpzInput } from "./SplatLoader";
|
||||
import { unpackAntiSplat } from "./antisplat";
|
||||
import { SCALE_MIN, WASM_SPLAT_SORT } from "./defines";
|
||||
import { unpackKsplat } from "./ksplat";
|
||||
import { unpackPcSogs } from "./pcsogs";
|
||||
import { unpackPcSogs, unpackPcSogsZip } from "./pcsogs";
|
||||
import { PlyReader } from "./ply";
|
||||
import { SpzReader, transcodeSpz } from "./spz";
|
||||
import {
|
||||
@@ -86,7 +86,21 @@ async function onMessage(event: MessageEvent) {
|
||||
fileBytes: Uint8Array;
|
||||
extraFiles: Record<string, ArrayBuffer>;
|
||||
};
|
||||
const decoded = await unpackPcSogs(fileBytes, extraFiles);
|
||||
const json = JSON.parse(
|
||||
new TextDecoder().decode(fileBytes),
|
||||
) as PcSogsJson;
|
||||
const decoded = await unpackPcSogs(json, extraFiles);
|
||||
result = {
|
||||
id,
|
||||
numSplats: decoded.numSplats,
|
||||
packedArray: decoded.packedArray,
|
||||
extra: decoded.extra,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "decodePcSogsZip": {
|
||||
const { fileBytes } = args as { fileBytes: Uint8Array };
|
||||
const decoded = await unpackPcSogsZip(fileBytes);
|
||||
result = {
|
||||
id,
|
||||
numSplats: decoded.numSplats,
|
||||
|
||||
Reference in New Issue
Block a user