mirror of
https://github.com/storytold/spark.git
synced 2026-10-21 08:06:45 +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>
|
<canvas id="canvas" tabindex="0"></canvas>
|
||||||
<div id="main-gui"></div>
|
<div id="main-gui"></div>
|
||||||
<div id="second-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">
|
<script type="module">
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
loadFiles(urls);
|
loadFiles(urls);
|
||||||
guiOptions.loadFromText = ""; // Clear after loading
|
guiOptions.loadFromText = ""; // Clear after loading
|
||||||
} else {
|
} 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('.ply') ||
|
||||||
file.name.toLowerCase().endsWith('.spz') ||
|
file.name.toLowerCase().endsWith('.spz') ||
|
||||||
file.name.toLowerCase().endsWith('.splat') ||
|
file.name.toLowerCase().endsWith('.splat') ||
|
||||||
file.name.toLowerCase().endsWith('.ksplat')
|
file.name.toLowerCase().endsWith('.ksplat') ||
|
||||||
|
file.name.toLowerCase().endsWith('.zip')
|
||||||
);
|
);
|
||||||
|
|
||||||
if (splatFiles.length > 0) {
|
if (splatFiles.length > 0) {
|
||||||
@@ -693,7 +694,7 @@
|
|||||||
|
|
||||||
// Parse URLs from text (handles single URLs, multiple lines, mixed content)
|
// Parse URLs from text (handles single URLs, multiple lines, mixed content)
|
||||||
function parseURLsFromText(text) {
|
function parseURLsFromText(text) {
|
||||||
const supportedExtensions = [".ply", ".spz", ".splat", ".ksplat", ".json"];
|
const supportedExtensions = [".ply", ".spz", ".splat", ".ksplat", ".zip", ".json"];
|
||||||
const urls = [];
|
const urls = [];
|
||||||
|
|
||||||
// Split by lines, commas, and semicolons
|
// Split by lines, commas, and semicolons
|
||||||
|
|||||||
@@ -347,7 +347,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="drop-zone">Drag and drop a splat file here</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>
|
<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">
|
<form class="url-form">
|
||||||
<input class="url-input" type="text" placeholder="Copy splat URL" />
|
<input class="url-input" type="text" placeholder="Copy splat URL" />
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+55
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import { unzipSync } from "fflate";
|
||||||
import { FileLoader, Loader, type LoadingManager } from "three";
|
import { FileLoader, Loader, type LoadingManager } from "three";
|
||||||
import { PackedSplats } from "./PackedSplats";
|
import { PackedSplats } from "./PackedSplats";
|
||||||
import { SplatMesh } from "./SplatMesh";
|
import { SplatMesh } from "./SplatMesh";
|
||||||
@@ -138,6 +139,7 @@ export enum SplatFileType {
|
|||||||
SPLAT = "splat",
|
SPLAT = "splat",
|
||||||
KSPLAT = "ksplat",
|
KSPLAT = "ksplat",
|
||||||
PCSOGS = "pcsogs",
|
PCSOGS = "pcsogs",
|
||||||
|
PCSOGSZIP = "pcsogszip",
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSplatFileType(
|
export function getSplatFileType(
|
||||||
@@ -157,6 +159,14 @@ export function getSplatFileType(
|
|||||||
// Unknown Gzipped file type
|
// Unknown Gzipped file type
|
||||||
return undefined;
|
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
|
// Unknown file type
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -218,7 +228,7 @@ export type PcSogsJson = {
|
|||||||
maxs: number[];
|
maxs: number[];
|
||||||
files: string[];
|
files: string[];
|
||||||
};
|
};
|
||||||
shN: {
|
shN?: {
|
||||||
shape: number[];
|
shape: number[];
|
||||||
dtype: string;
|
dtype: string;
|
||||||
mins: number;
|
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({
|
export async function unpackSplats({
|
||||||
input,
|
input,
|
||||||
extraFiles,
|
extraFiles,
|
||||||
@@ -373,6 +414,19 @@ export async function unpackSplats({
|
|||||||
return { packedArray, numSplats, extra };
|
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: {
|
default: {
|
||||||
throw new Error(`Unknown splat file type: ${splatFileType}`);
|
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 {
|
import {
|
||||||
computeMaxSplats,
|
computeMaxSplats,
|
||||||
encodeSh1Rgb,
|
encodeSh1Rgb,
|
||||||
@@ -11,14 +12,13 @@ import {
|
|||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
|
||||||
export async function unpackPcSogs(
|
export async function unpackPcSogs(
|
||||||
fileBytes: Uint8Array,
|
json: PcSogsJson,
|
||||||
extraFiles: Record<string, ArrayBuffer>,
|
extraFiles: Record<string, ArrayBuffer>,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
packedArray: Uint32Array;
|
packedArray: Uint32Array;
|
||||||
numSplats: number;
|
numSplats: number;
|
||||||
extra: Record<string, unknown>;
|
extra: Record<string, unknown>;
|
||||||
}> {
|
}> {
|
||||||
const json = JSON.parse(new TextDecoder().decode(fileBytes)) as PcSogsJson;
|
|
||||||
if (json.quats.encoding !== "quaternion_packed") {
|
if (json.quats.encoding !== "quaternion_packed") {
|
||||||
throw new Error("Unsupported quaternion encoding");
|
throw new Error("Unsupported quaternion encoding");
|
||||||
}
|
}
|
||||||
@@ -204,3 +204,58 @@ async function decodeImageRgba(fileBytes: ArrayBuffer) {
|
|||||||
const { rgba } = await decodeImage(fileBytes);
|
const { rgba } = await decodeImage(fileBytes);
|
||||||
return rgba;
|
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 init_wasm, { sort_splats } from "spark-internal-rs";
|
||||||
import type { TranscodeSpzInput } from "./SplatLoader";
|
import type { PcSogsJson, TranscodeSpzInput } from "./SplatLoader";
|
||||||
import { unpackAntiSplat } from "./antisplat";
|
import { unpackAntiSplat } from "./antisplat";
|
||||||
import { SCALE_MIN, WASM_SPLAT_SORT } from "./defines";
|
import { SCALE_MIN, WASM_SPLAT_SORT } from "./defines";
|
||||||
import { unpackKsplat } from "./ksplat";
|
import { unpackKsplat } from "./ksplat";
|
||||||
import { unpackPcSogs } from "./pcsogs";
|
import { unpackPcSogs, unpackPcSogsZip } from "./pcsogs";
|
||||||
import { PlyReader } from "./ply";
|
import { PlyReader } from "./ply";
|
||||||
import { SpzReader, transcodeSpz } from "./spz";
|
import { SpzReader, transcodeSpz } from "./spz";
|
||||||
import {
|
import {
|
||||||
@@ -86,7 +86,21 @@ async function onMessage(event: MessageEvent) {
|
|||||||
fileBytes: Uint8Array;
|
fileBytes: Uint8Array;
|
||||||
extraFiles: Record<string, ArrayBuffer>;
|
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 = {
|
result = {
|
||||||
id,
|
id,
|
||||||
numSplats: decoded.numSplats,
|
numSplats: decoded.numSplats,
|
||||||
|
|||||||
Reference in New Issue
Block a user