diff --git a/examples/editor/index.html b/examples/editor/index.html
index fc5a851..0505ce4 100644
--- a/examples/editor/index.html
+++ b/examples/editor/index.html
@@ -753,7 +753,8 @@
file.name.toLowerCase().endsWith('.spz') ||
file.name.toLowerCase().endsWith('.splat') ||
file.name.toLowerCase().endsWith('.ksplat') ||
- file.name.toLowerCase().endsWith('.zip')
+ file.name.toLowerCase().endsWith('.zip') ||
+ file.name.toLowerCase().endsWith('.sog')
);
if (splatFiles.length > 0) {
diff --git a/src/SplatLoader.ts b/src/SplatLoader.ts
index 8f01bbe..c30f537 100644
--- a/src/SplatLoader.ts
+++ b/src/SplatLoader.ts
@@ -282,6 +282,9 @@ export function getSplatFileTypeFromPath(
if (extension === "ksplat") {
return SplatFileType.KSPLAT;
}
+ if (extension === "sog") {
+ return SplatFileType.PCSOGSZIP;
+ }
return undefined;
}
@@ -318,6 +321,32 @@ export type PcSogsJson = {
};
};
+export type PcSogsV2Json = {
+ version: 2;
+ count: number;
+ antialias?: boolean;
+ means: {
+ mins: number[];
+ maxs: number[];
+ files: string[];
+ };
+ scales: {
+ codebook: number[];
+ files: string[];
+ };
+ quats: { files: string[] };
+ sh0: {
+ codebook: number[];
+ files: string[];
+ };
+ shN?: {
+ count: number;
+ bands: number;
+ codebook: number[];
+ files: string[];
+ };
+};
+
export function isPcSogs(input: ArrayBuffer | Uint8Array | string): boolean {
// Returns true if the input seems to be a valid PC SOGS file
return tryPcSogs(input) !== undefined;
@@ -325,7 +354,7 @@ export function isPcSogs(input: ArrayBuffer | Uint8Array | string): boolean {
export function tryPcSogs(
input: ArrayBuffer | Uint8Array | string,
-): PcSogsJson | undefined {
+): PcSogsJson | PcSogsV2Json | undefined {
// Try to parse input as SOGS JSON and see if it's valid
try {
let text: string;
@@ -345,6 +374,8 @@ export function tryPcSogs(
if (!json || typeof json !== "object" || Array.isArray(json)) {
return undefined;
}
+ const isVersion2 = json.version === 2;
+
for (const key of ["means", "scales", "quats", "sh0"]) {
if (
!json[key] ||
@@ -353,15 +384,33 @@ export function tryPcSogs(
) {
return undefined;
}
- if (!json[key].shape || !json[key].files) {
- return undefined;
- }
- if (key !== "quats" && (!json[key].mins || !json[key].maxs)) {
- return undefined;
+ if (isVersion2) {
+ // Expect files
+ if (!json[key].files) {
+ return undefined;
+ }
+
+ // Scales and sh0 should have codebooks
+ if ((key === "scales" || key === "sh0") && !json[key].codebook) {
+ return undefined;
+ }
+ // Means should have mins and maxs defined
+ if (key === "means" && (!json[key].mins || !json[key].maxs)) {
+ return undefined;
+ }
+ } else {
+ // Expect shape and files
+ if (!json[key].shape || !json[key].files) {
+ return undefined;
+ }
+ // Besides 'quats' all other properties have mins and maxs
+ if (key !== "quats" && (!json[key].mins || !json[key].maxs)) {
+ return undefined;
+ }
}
}
// This is probably a PC SOGS file
- return json as PcSogsJson;
+ return json as PcSogsJson | PcSogsV2Json;
} catch {
return undefined;
}
@@ -388,6 +437,8 @@ export function tryPcSogsZip(
if (!metaFilename) {
return undefined;
}
+
+ // Check for PC SOGS V1 and V2 (aka SOG)
const json = tryPcSogs(unzipped[metaFilename]);
if (!json) {
return undefined;
diff --git a/src/pcsogs.ts b/src/pcsogs.ts
index f6a34cc..f82230f 100644
--- a/src/pcsogs.ts
+++ b/src/pcsogs.ts
@@ -1,6 +1,10 @@
import { unzip } from "fflate";
import type { SplatEncoding } from "./PackedSplats";
-import { type PcSogsJson, tryPcSogsZip } from "./SplatLoader";
+import {
+ type PcSogsJson,
+ type PcSogsV2Json,
+ tryPcSogsZip,
+} from "./SplatLoader";
import {
computeMaxSplats,
encodeSh1Rgb,
@@ -13,7 +17,7 @@ import {
} from "./utils";
export async function unpackPcSogs(
- json: PcSogsJson,
+ json: PcSogsJson | PcSogsV2Json,
extraFiles: Record,
splatEncoding: SplatEncoding,
): Promise<{
@@ -21,11 +25,13 @@ export async function unpackPcSogs(
numSplats: number;
extra: Record;
}> {
- if (json.quats.encoding !== "quaternion_packed") {
+ const isVersion2 = "version" in json;
+
+ if (!isVersion2 && json.quats.encoding !== "quaternion_packed") {
throw new Error("Unsupported quaternion encoding");
}
- const numSplats = json.means.shape[0];
+ const numSplats = isVersion2 ? json.count : json.means.shape[0];
const maxSplats = computeMaxSplats(numSplats);
const packedArray = new Uint32Array(maxSplats * 4);
const extra: Record = {};
@@ -54,30 +60,41 @@ export async function unpackPcSogs(
const scalesPromise = decodeImageRgba(extraFiles[json.scales.files[0]]).then(
(scales) => {
- const xLookup = new Array(256)
- .fill(0)
- .map(
- (_, i) =>
- json.scales.mins[0] +
- (json.scales.maxs[0] - json.scales.mins[0]) * (i / 255),
- )
- .map((x) => Math.exp(x));
- const yLookup = new Array(256)
- .fill(0)
- .map(
- (_, i) =>
- json.scales.mins[1] +
- (json.scales.maxs[1] - json.scales.mins[1]) * (i / 255),
- )
- .map((x) => Math.exp(x));
- const zLookup = new Array(256)
- .fill(0)
- .map(
- (_, i) =>
- json.scales.mins[2] +
- (json.scales.maxs[2] - json.scales.mins[2]) * (i / 255),
- )
- .map((x) => Math.exp(x));
+ let xLookup: number[];
+ let yLookup: number[];
+ let zLookup: number[];
+
+ if (isVersion2) {
+ xLookup =
+ yLookup =
+ zLookup =
+ json.scales.codebook.map((x) => Math.exp(x));
+ } else {
+ xLookup = new Array(256)
+ .fill(0)
+ .map(
+ (_, i) =>
+ json.scales.mins[0] +
+ (json.scales.maxs[0] - json.scales.mins[0]) * (i / 255),
+ )
+ .map((x) => Math.exp(x));
+ yLookup = new Array(256)
+ .fill(0)
+ .map(
+ (_, i) =>
+ json.scales.mins[1] +
+ (json.scales.maxs[1] - json.scales.mins[1]) * (i / 255),
+ )
+ .map((x) => Math.exp(x));
+ zLookup = new Array(256)
+ .fill(0)
+ .map(
+ (_, i) =>
+ json.scales.mins[2] +
+ (json.scales.maxs[2] - json.scales.mins[2]) * (i / 255),
+ )
+ .map((x) => Math.exp(x));
+ }
for (let i = 0; i < numSplats; ++i) {
const i4 = i * 4;
@@ -118,38 +135,51 @@ export async function unpackPcSogs(
const sh0Promise = decodeImageRgba(extraFiles[json.sh0.files[0]]).then(
(sh0) => {
const SH_C0 = 0.28209479177387814;
- const rLookup = new Array(256)
- .fill(0)
- .map(
- (_, i) =>
- json.sh0.mins[0] +
- (json.sh0.maxs[0] - json.sh0.mins[0]) * (i / 255),
- )
- .map((x) => SH_C0 * x + 0.5);
- const gLookup = new Array(256)
- .fill(0)
- .map(
- (_, i) =>
- json.sh0.mins[1] +
- (json.sh0.maxs[1] - json.sh0.mins[1]) * (i / 255),
- )
- .map((x) => SH_C0 * x + 0.5);
- const bLookup = new Array(256)
- .fill(0)
- .map(
- (_, i) =>
- json.sh0.mins[2] +
- (json.sh0.maxs[2] - json.sh0.mins[2]) * (i / 255),
- )
- .map((x) => SH_C0 * x + 0.5);
- const aLookup = new Array(256)
- .fill(0)
- .map(
- (_, i) =>
- json.sh0.mins[3] +
- (json.sh0.maxs[3] - json.sh0.mins[3]) * (i / 255),
- )
- .map((x) => 1.0 / (1.0 + Math.exp(-x)));
+ let rLookup: number[];
+ let gLookup: number[];
+ let bLookup: number[];
+ let aLookup: number[];
+
+ if (isVersion2) {
+ rLookup =
+ gLookup =
+ bLookup =
+ json.sh0.codebook.map((x) => SH_C0 * x + 0.5);
+ aLookup = new Array(256).fill(0).map((_, i) => i / 255);
+ } else {
+ rLookup = new Array(256)
+ .fill(0)
+ .map(
+ (_, i) =>
+ json.sh0.mins[0] +
+ (json.sh0.maxs[0] - json.sh0.mins[0]) * (i / 255),
+ )
+ .map((x) => SH_C0 * x + 0.5);
+ gLookup = new Array(256)
+ .fill(0)
+ .map(
+ (_, i) =>
+ json.sh0.mins[1] +
+ (json.sh0.maxs[1] - json.sh0.mins[1]) * (i / 255),
+ )
+ .map((x) => SH_C0 * x + 0.5);
+ bLookup = new Array(256)
+ .fill(0)
+ .map(
+ (_, i) =>
+ json.sh0.mins[2] +
+ (json.sh0.maxs[2] - json.sh0.mins[2]) * (i / 255),
+ )
+ .map((x) => SH_C0 * x + 0.5);
+ aLookup = new Array(256)
+ .fill(0)
+ .map(
+ (_, i) =>
+ json.sh0.mins[3] +
+ (json.sh0.maxs[3] - json.sh0.mins[3]) * (i / 255),
+ )
+ .map((x) => 1.0 / (1.0 + Math.exp(-x)));
+ }
for (let i = 0; i < numSplats; ++i) {
const i4 = i * 4;
@@ -168,9 +198,15 @@ export async function unpackPcSogs(
const promises = [meansPromise, scalesPromise, quatsPromise, sh0Promise];
if (json.shN) {
- const useSH3 = json.shN.shape[1] >= 48 - 3;
- const useSH2 = json.shN.shape[1] >= 27 - 3;
- const useSH1 = json.shN.shape[1] >= 12 - 3;
+ const useSH3 = isVersion2
+ ? json.shN.bands >= 3
+ : json.shN.shape[1] >= 48 - 3;
+ const useSH2 = isVersion2
+ ? json.shN.bands >= 2
+ : json.shN.shape[1] >= 27 - 3;
+ const useSH1 = isVersion2
+ ? json.shN.bands >= 1
+ : json.shN.shape[1] >= 12 - 3;
if (useSH1) extra.sh1 = new Uint32Array(numSplats * 2);
if (useSH2) extra.sh2 = new Uint32Array(numSplats * 4);
@@ -185,9 +221,12 @@ export async function unpackPcSogs(
decodeImage(extraFiles[json.shN.files[0]]),
decodeImage(extraFiles[json.shN.files[1]]),
]).then(([centroids, labels]) => {
- const lookup = new Array(256)
- .fill(0)
- .map((_, i) => shN.mins + (shN.maxs - shN.mins) * (i / 255));
+ const lookup =
+ "codebook" in shN
+ ? shN.codebook
+ : new Array(256)
+ .fill(0)
+ .map((_, i) => shN.mins + (shN.maxs - shN.mins) * (i / 255));
for (let i = 0; i < numSplats; ++i) {
const i4 = i * 4;