mirror of
https://github.com/storytold/spark.git
synced 2026-10-14 23:52:59 +00:00
SPZ writing, exporting, & viewer. Creates a new npm run to-spz which runs scripts/to-spz.js to convert any Gsplat input file to compress .spz. Also includes a configurable viewer examples/viewer that can load splats via drag-n-drop, from url query param, the URL text list, can edit sets of SplatMesh, and export composite .SPZ with some filters. Refactored some loading code from worker.ts to loader files so they can be called from Nodejs. Created api transcodeSpz with options to combine multiple source Gsplat files into one without losing coding quality along the way.
This commit is contained in:
committed by
Diego Marcos Segura
parent
b4257bcd64
commit
156bbf400e
+258
-73
@@ -118,6 +118,28 @@ export function getFileExtension(pathOrUrl: string): string {
|
||||
return filename.slice(lastDot + 1).toLowerCase();
|
||||
}
|
||||
|
||||
export function getSplatFileTypeFromPath(
|
||||
pathOrUrl: string,
|
||||
): SplatFileType | undefined {
|
||||
const extension = getFileExtension(pathOrUrl);
|
||||
if (extension === "ply") {
|
||||
return SplatFileType.PLY;
|
||||
}
|
||||
if (extension === "wlg") {
|
||||
return SplatFileType.WLG0;
|
||||
}
|
||||
if (extension === "spz") {
|
||||
return SplatFileType.SPZ;
|
||||
}
|
||||
if (extension === "splat") {
|
||||
return SplatFileType.SPLAT;
|
||||
}
|
||||
if (extension === "ksplat") {
|
||||
return SplatFileType.KSPLAT;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function unpackSplats({
|
||||
input,
|
||||
fileType,
|
||||
@@ -137,82 +159,245 @@ export async function unpackSplats({
|
||||
if (!fileType) {
|
||||
splatFileType = getSplatFileType(fileBytes);
|
||||
if (!splatFileType && pathOrUrl) {
|
||||
const extension = getFileExtension(pathOrUrl);
|
||||
if (extension === "ply") {
|
||||
splatFileType = SplatFileType.PLY;
|
||||
} else if (extension === "wlg") {
|
||||
splatFileType = SplatFileType.WLG0;
|
||||
} else if (extension === "spz") {
|
||||
splatFileType = SplatFileType.SPZ;
|
||||
} else if (extension === "splat") {
|
||||
splatFileType = SplatFileType.SPLAT;
|
||||
} else if (extension === "ksplat") {
|
||||
splatFileType = SplatFileType.KSPLAT;
|
||||
}
|
||||
splatFileType = getSplatFileTypeFromPath(pathOrUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if (splatFileType === SplatFileType.WLG0) {
|
||||
return await withWorker(async (worker) => {
|
||||
const { packedArray, numSplats } = (await worker.call("decodeWlg", {
|
||||
fileBytes,
|
||||
})) as { packedArray: Uint32Array; numSplats: number };
|
||||
return { packedArray, numSplats };
|
||||
});
|
||||
}
|
||||
if (splatFileType === SplatFileType.PLY) {
|
||||
const ply = new PlyReader({ fileBytes });
|
||||
await ply.parseHeader();
|
||||
const numSplats = ply.numSplats;
|
||||
const maxSplats = getTextureSize(numSplats).maxSplats;
|
||||
const args = { fileBytes, packedArray: new Uint32Array(maxSplats * 4) };
|
||||
return await withWorker(async (worker) => {
|
||||
const { packedArray, numSplats, extra } = (await worker.call(
|
||||
"unpackPly",
|
||||
args,
|
||||
)) as {
|
||||
packedArray: Uint32Array;
|
||||
numSplats: number;
|
||||
extra: Record<string, unknown>;
|
||||
};
|
||||
return { packedArray, numSplats, extra };
|
||||
});
|
||||
}
|
||||
if (splatFileType === SplatFileType.SPZ) {
|
||||
return await withWorker(async (worker) => {
|
||||
const { packedArray, numSplats, extra } = (await worker.call(
|
||||
"decodeSpz",
|
||||
{
|
||||
switch (splatFileType) {
|
||||
case SplatFileType.WLG0:
|
||||
return await withWorker(async (worker) => {
|
||||
const { packedArray, numSplats } = (await worker.call("decodeWlg", {
|
||||
fileBytes,
|
||||
},
|
||||
)) as {
|
||||
packedArray: Uint32Array;
|
||||
numSplats: number;
|
||||
extra: Record<string, unknown>;
|
||||
};
|
||||
return { packedArray, numSplats, extra };
|
||||
});
|
||||
})) as { packedArray: Uint32Array; numSplats: number };
|
||||
return { packedArray, numSplats };
|
||||
});
|
||||
case SplatFileType.PLY: {
|
||||
const ply = new PlyReader({ fileBytes });
|
||||
await ply.parseHeader();
|
||||
const numSplats = ply.numSplats;
|
||||
const maxSplats = getTextureSize(numSplats).maxSplats;
|
||||
const args = { fileBytes, packedArray: new Uint32Array(maxSplats * 4) };
|
||||
return await withWorker(async (worker) => {
|
||||
const { packedArray, numSplats, extra } = (await worker.call(
|
||||
"unpackPly",
|
||||
args,
|
||||
)) as {
|
||||
packedArray: Uint32Array;
|
||||
numSplats: number;
|
||||
extra: Record<string, unknown>;
|
||||
};
|
||||
return { packedArray, numSplats, extra };
|
||||
});
|
||||
}
|
||||
case SplatFileType.SPZ: {
|
||||
return await withWorker(async (worker) => {
|
||||
const { packedArray, numSplats, extra } = (await worker.call(
|
||||
"decodeSpz",
|
||||
{
|
||||
fileBytes,
|
||||
},
|
||||
)) as {
|
||||
packedArray: Uint32Array;
|
||||
numSplats: number;
|
||||
extra: Record<string, unknown>;
|
||||
};
|
||||
return { packedArray, numSplats, extra };
|
||||
});
|
||||
}
|
||||
case SplatFileType.SPLAT: {
|
||||
return await withWorker(async (worker) => {
|
||||
const { packedArray, numSplats } = (await worker.call(
|
||||
"decodeAntiSplat",
|
||||
{
|
||||
fileBytes,
|
||||
},
|
||||
)) as { packedArray: Uint32Array; numSplats: number };
|
||||
return { packedArray, numSplats };
|
||||
});
|
||||
}
|
||||
case SplatFileType.KSPLAT:
|
||||
return await withWorker(async (worker) => {
|
||||
const { packedArray, numSplats, extra } = (await worker.call(
|
||||
"decodeKsplat",
|
||||
{ fileBytes },
|
||||
)) as {
|
||||
packedArray: Uint32Array;
|
||||
numSplats: number;
|
||||
extra: Record<string, unknown>;
|
||||
};
|
||||
return { packedArray, numSplats, extra };
|
||||
});
|
||||
default: {
|
||||
throw new Error(`Unknown splat file type: ${splatFileType}`);
|
||||
}
|
||||
}
|
||||
if (splatFileType === SplatFileType.SPLAT) {
|
||||
return await withWorker(async (worker) => {
|
||||
const { packedArray, numSplats } = (await worker.call("decodeAntiSplat", {
|
||||
fileBytes,
|
||||
})) as { packedArray: Uint32Array; numSplats: number };
|
||||
return { packedArray, numSplats };
|
||||
});
|
||||
}
|
||||
if (splatFileType === SplatFileType.KSPLAT) {
|
||||
return await withWorker(async (worker) => {
|
||||
const { packedArray, numSplats, extra } = (await worker.call(
|
||||
"decodeKsplat",
|
||||
{ fileBytes },
|
||||
)) as {
|
||||
packedArray: Uint32Array;
|
||||
numSplats: number;
|
||||
extra: Record<string, unknown>;
|
||||
};
|
||||
return { packedArray, numSplats, extra };
|
||||
});
|
||||
}
|
||||
throw new Error("Unknown splat file type");
|
||||
}
|
||||
|
||||
export class SplatData {
|
||||
numSplats: number;
|
||||
maxSplats: number;
|
||||
centers: Float32Array;
|
||||
scales: Float32Array;
|
||||
quaternions: Float32Array;
|
||||
opacities: Float32Array;
|
||||
colors: Float32Array;
|
||||
sh1?: Float32Array;
|
||||
sh2?: Float32Array;
|
||||
sh3?: Float32Array;
|
||||
|
||||
constructor({ maxSplats = 1 }: { maxSplats?: number } = {}) {
|
||||
this.numSplats = 0;
|
||||
this.maxSplats = getTextureSize(maxSplats).maxSplats;
|
||||
this.centers = new Float32Array(this.maxSplats * 3);
|
||||
this.scales = new Float32Array(this.maxSplats * 3);
|
||||
this.quaternions = new Float32Array(this.maxSplats * 4);
|
||||
this.opacities = new Float32Array(this.maxSplats);
|
||||
this.colors = new Float32Array(this.maxSplats * 3);
|
||||
}
|
||||
|
||||
pushSplat(): number {
|
||||
const index = this.numSplats;
|
||||
this.ensureIndex(index);
|
||||
this.numSplats += 1;
|
||||
return index;
|
||||
}
|
||||
|
||||
unpushSplat(index: number) {
|
||||
if (index === this.numSplats - 1) {
|
||||
this.numSplats -= 1;
|
||||
} else {
|
||||
throw new Error("Cannot unpush splat from non-last position");
|
||||
}
|
||||
}
|
||||
|
||||
ensureCapacity(numSplats: number) {
|
||||
if (numSplats > this.maxSplats) {
|
||||
const targetSplats = Math.max(numSplats, this.maxSplats * 2);
|
||||
const newCenters = new Float32Array(targetSplats * 3);
|
||||
const newScales = new Float32Array(targetSplats * 3);
|
||||
const newQuaternions = new Float32Array(targetSplats * 4);
|
||||
const newOpacities = new Float32Array(targetSplats);
|
||||
const newColors = new Float32Array(targetSplats * 3);
|
||||
newCenters.set(this.centers);
|
||||
newScales.set(this.scales);
|
||||
newQuaternions.set(this.quaternions);
|
||||
newOpacities.set(this.opacities);
|
||||
newColors.set(this.colors);
|
||||
this.centers = newCenters;
|
||||
this.scales = newScales;
|
||||
this.quaternions = newQuaternions;
|
||||
this.opacities = newOpacities;
|
||||
this.colors = newColors;
|
||||
|
||||
if (this.sh1) {
|
||||
const newSh1 = new Float32Array(targetSplats * 9);
|
||||
newSh1.set(this.sh1);
|
||||
this.sh1 = newSh1;
|
||||
}
|
||||
if (this.sh2) {
|
||||
const newSh2 = new Float32Array(targetSplats * 15);
|
||||
newSh2.set(this.sh2);
|
||||
this.sh2 = newSh2;
|
||||
}
|
||||
if (this.sh3) {
|
||||
const newSh3 = new Float32Array(targetSplats * 21);
|
||||
newSh3.set(this.sh3);
|
||||
this.sh3 = newSh3;
|
||||
}
|
||||
|
||||
this.maxSplats = targetSplats;
|
||||
// console.log("Reallocated capacity", this.maxSplats);
|
||||
}
|
||||
}
|
||||
|
||||
ensureIndex(index: number) {
|
||||
this.ensureCapacity(index + 1);
|
||||
}
|
||||
|
||||
setCenter(index: number, x: number, y: number, z: number) {
|
||||
this.centers[index * 3] = x;
|
||||
this.centers[index * 3 + 1] = y;
|
||||
this.centers[index * 3 + 2] = z;
|
||||
}
|
||||
|
||||
setScale(index: number, scaleX: number, scaleY: number, scaleZ: number) {
|
||||
this.scales[index * 3] = scaleX;
|
||||
this.scales[index * 3 + 1] = scaleY;
|
||||
this.scales[index * 3 + 2] = scaleZ;
|
||||
}
|
||||
|
||||
setQuaternion(index: number, x: number, y: number, z: number, w: number) {
|
||||
this.quaternions[index * 4] = x;
|
||||
this.quaternions[index * 4 + 1] = y;
|
||||
this.quaternions[index * 4 + 2] = z;
|
||||
this.quaternions[index * 4 + 3] = w;
|
||||
}
|
||||
|
||||
setOpacity(index: number, opacity: number) {
|
||||
this.opacities[index] = opacity;
|
||||
}
|
||||
|
||||
setColor(index: number, r: number, g: number, b: number) {
|
||||
this.colors[index * 3] = r;
|
||||
this.colors[index * 3 + 1] = g;
|
||||
this.colors[index * 3 + 2] = b;
|
||||
}
|
||||
|
||||
setSh1(index: number, sh1: Float32Array) {
|
||||
if (!this.sh1) {
|
||||
this.sh1 = new Float32Array(this.maxSplats * 9);
|
||||
// console.log("setSh1 creating sh1", this.sh1.length);
|
||||
}
|
||||
for (let j = 0; j < 9; ++j) {
|
||||
this.sh1[index * 9 + j] = sh1[j];
|
||||
}
|
||||
}
|
||||
|
||||
setSh2(index: number, sh2: Float32Array) {
|
||||
if (!this.sh2) {
|
||||
this.sh2 = new Float32Array(this.maxSplats * 15);
|
||||
// console.log("setSh2 creating sh2", this.sh2.length);
|
||||
}
|
||||
for (let j = 0; j < 15; ++j) {
|
||||
this.sh2[index * 15 + j] = sh2[j];
|
||||
}
|
||||
}
|
||||
|
||||
setSh3(index: number, sh3: Float32Array) {
|
||||
if (!this.sh3) {
|
||||
this.sh3 = new Float32Array(this.maxSplats * 21);
|
||||
// console.log("setSh3 creating sh3", this.sh3.length);
|
||||
}
|
||||
for (let j = 0; j < 21; ++j) {
|
||||
this.sh3[index * 21 + j] = sh3[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function transcodeSpz(
|
||||
input: TranscodeSpzInput,
|
||||
): Promise<{ input: TranscodeSpzInput; fileBytes: Uint8Array }> {
|
||||
return await withWorker(async (worker) => {
|
||||
// console.log("withWorker calling transcodeSpz", input);
|
||||
const result = (await worker.call("transcodeSpz", input)) as {
|
||||
input: TranscodeSpzInput;
|
||||
fileBytes: Uint8Array;
|
||||
};
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
export type FileInput = {
|
||||
fileBytes: Uint8Array;
|
||||
fileType?: SplatFileType;
|
||||
pathOrUrl?: string;
|
||||
transform?: { translate?: number[]; quaternion?: number[]; scale?: number };
|
||||
};
|
||||
|
||||
export type TranscodeSpzInput = {
|
||||
inputs: FileInput[];
|
||||
maxSh?: number;
|
||||
clipXyz?: { min: number[]; max: number[] };
|
||||
fractionalBits?: number;
|
||||
opacityThreshold?: number;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user