Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"cSpell.words": [
"acumin",
"babylonjs",
"chirality",
"desaturated",
"fluentui",
"multilines",
Expand Down
6 changes: 3 additions & 3 deletions packages/dev/loaders/src/SPLAT/sog.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Scene } from "core/scene";
import type { IParsedPLY } from "./splatDefs";
import type { IParsedSplat } from "./splatDefs";
import { Mode } from "./splatDefs";
import { Scalar } from "core/Maths/math.scalar";
import type { AbstractEngine } from "core/Engines";
Expand Down Expand Up @@ -139,7 +139,7 @@ async function LoadWebpImageData(rootUrlOrData: string | Uint8Array, filename: s
return await promise;
}

async function ParseSogDatas(data: SOGRootData, imageDataArrays: IWebPImage[], scene: Scene): Promise<IParsedPLY> {
async function ParseSogDatas(data: SOGRootData, imageDataArrays: IWebPImage[], scene: Scene): Promise<IParsedSplat> {
const splatCount = data.count ? data.count : data.means.shape[0];
const rowOutputLength = 3 * 4 + 3 * 4 + 4 + 4; // 32
const buffer = new ArrayBuffer(rowOutputLength * splatCount);
Expand Down Expand Up @@ -375,7 +375,7 @@ async function ParseSogDatas(data: SOGRootData, imageDataArrays: IWebPImage[], s
* @param scene The Babylon.js scene
* @returns Parsed data
*/
export async function ParseSogMeta(dataOrFiles: SOGRootData | Map<string, Uint8Array>, rootUrl: string, scene: Scene): Promise<IParsedPLY> {
export async function ParseSogMeta(dataOrFiles: SOGRootData | Map<string, Uint8Array>, rootUrl: string, scene: Scene): Promise<IParsedSplat> {
let data: SOGRootData;
let files: Map<string, Uint8Array> | undefined;

Expand Down
6 changes: 5 additions & 1 deletion packages/dev/loaders/src/SPLAT/splatDefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const enum Mode {
/**
* A parsed buffer and how to use it
*/
export interface IParsedPLY {
export interface IParsedSplat {
data: ArrayBuffer;
mode: Mode;
faces?: number[];
Expand All @@ -20,4 +20,8 @@ export interface IParsedPLY {
trainedWithAntialiasing?: boolean;
compressed?: boolean;
rawSplat?: boolean;
safeOrbitCameraRadiusMin?: number;
safeOrbitCameraElevationMinMax?: [number, number];
upAxis?: "X" | "Y" | "Z";
chirality?: "LeftHanded" | "RightHanded";
}
78 changes: 68 additions & 10 deletions packages/dev/loaders/src/SPLAT/splatFileLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import type { SPLATLoadingOptions } from "./splatLoadingOptions";
import type { GaussianSplattingMaterial } from "core/Materials/GaussianSplatting/gaussianSplattingMaterial";
import { ParseSpz } from "./spz";
import { Mode } from "./splatDefs";
import type { IParsedPLY } from "./splatDefs";
import type { IParsedSplat } from "./splatDefs";
import { ParseSogMeta } from "./sog";
import type { SOGRootData } from "./sog";
import { Tools } from "core/Misc/tools";
import type { ArcRotateCamera } from "core/Cameras/arcRotateCamera";

declare module "core/Loading/sceneLoader" {
// eslint-disable-next-line jsdoc/require-jsdoc, @typescript-eslint/naming-convention
Expand Down Expand Up @@ -134,7 +135,7 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu
return true;
}

private static _BuildMesh(scene: Scene, parsedPLY: IParsedPLY): Mesh {
private static _BuildMesh(scene: Scene, parsedPLY: IParsedSplat): Mesh {
const mesh = new Mesh("PLYMesh", scene);

const uBuffer = new Uint8Array(parsedPLY.data);
Expand Down Expand Up @@ -201,7 +202,7 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu
private _parseAsync(meshesNames: any, scene: Scene, data: any, rootUrl: string): Promise<Array<AbstractMesh>> {
const babylonMeshesArray: Array<Mesh> = []; //The mesh for babylon

const makeGSFromParsedSOG = (parsedSOG: IParsedPLY) => {
const makeGSFromParsedSOG = (parsedSOG: IParsedSplat) => {
scene._blockEntityCollection = !!this._assetContainer;
const gaussianSplatting = new GaussianSplattingMesh("GaussianSplatting", null, scene, this._loadingOptions.keepInRam);
gaussianSplatting._parentContainer = this._assetContainer;
Expand Down Expand Up @@ -278,6 +279,7 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu
babylonMeshesArray.push(gaussianSplatting);
gaussianSplatting.updateData(parsedSPZ.data, parsedSPZ.sh);
scene._blockEntityCollection = false;
this.applyAutoCameraLimits(parsedSPZ, scene);
resolve(babylonMeshesArray);
});
})
Expand Down Expand Up @@ -325,12 +327,35 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu
throw new Error("Unsupported Splat mode");
}
scene._blockEntityCollection = false;
this.applyAutoCameraLimits(parsedPLY, scene);
resolve(babylonMeshesArray);
});
});
});
}

/**
* Applies camera limits based on parsed meta data
* @param meta parsed splat meta data
* @param scene
*/
private applyAutoCameraLimits(meta: IParsedSplat, scene: Scene): void {
if (this._loadingOptions.disableAutoCameraLimits) {
return;
}
if ((meta.safeOrbitCameraRadiusMin !== undefined || meta.safeOrbitCameraElevationMinMax !== undefined) && scene.activeCamera?.getClassName() === "ArcRotateCamera") {
const arcCam = scene.activeCamera as ArcRotateCamera;
if (meta.safeOrbitCameraElevationMinMax) {
arcCam.lowerBetaLimit = Math.PI * 0.5 - meta.safeOrbitCameraElevationMinMax[1];
arcCam.upperBetaLimit = Math.PI * 0.5 - meta.safeOrbitCameraElevationMinMax[0];
}

if (meta.safeOrbitCameraRadiusMin) {
arcCam.lowerRadiusLimit = meta.safeOrbitCameraRadiusMin;
}
}
}

/**
* Load into an asset container.
* @param scene The scene to load into
Expand Down Expand Up @@ -385,7 +410,7 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu
* @param data the .ply data to load
* @returns the loaded splat buffer
*/
private static _ConvertPLYToSplat(data: ArrayBuffer): Promise<IParsedPLY> {
private static _ConvertPLYToSplat(data: ArrayBuffer): Promise<IParsedSplat> {
const ubuf = new Uint8Array(data);
const header = new TextDecoder().decode(ubuf.slice(0, 1024 * 10));
const headerEnd = "end_header\n";
Expand Down Expand Up @@ -428,16 +453,20 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu
offset: number;
};

const enum ElementMode {
Vertex = 0,
Chunk = 1,
SH = 2,
}
const ElementMode: Record<string, number> = {
Vertex: 0,
Chunk: 1,
SH: 2,
Float_Tuple: 3,
Float: 4,
Uchar: 5,
};

let chunkMode = ElementMode.Chunk;
const vertexProperties: PlyProperty[] = [];
const chunkProperties: PlyProperty[] = [];
const filtered = header.slice(0, headerEndIndex).split("\n");
const metaData: Partial<IParsedSplat> = {};
for (const prop of filtered) {
if (prop.startsWith("property ")) {
const [, type, name] = prop.split(" ");
Expand All @@ -450,7 +479,21 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu
rowVertexOffset += offsets[type];
} else if (chunkMode == ElementMode.SH) {
vertexProperties.push({ name, type, offset: rowVertexOffset });
} else if (chunkMode == ElementMode.Float_Tuple) {
const view = new DataView(data, rowChunkOffset, offsets.float * 2);
metaData.safeOrbitCameraElevationMinMax = [view.getFloat32(0, true), view.getFloat32(4, true)];
} else if (chunkMode == ElementMode.Float) {
const view = new DataView(data, rowChunkOffset, offsets.float);
metaData.safeOrbitCameraRadiusMin = view.getFloat32(0, true);
} else if (chunkMode == ElementMode.Uchar) {
const view = new DataView(data, rowChunkOffset, offsets.uchar);
if (name == "up_axis") {
metaData.upAxis = view.getUint8(0) == 0 ? "X" : view.getUint8(0) == 1 ? "Y" : "Z";
} else if (name == "chirality") {
metaData.chirality = view.getUint8(0) == 0 ? "LeftHanded" : "RightHanded";
}
}

if (!offsets[type]) {
Logger.Warn(`Unsupported property type: ${type}.`);
}
Expand All @@ -462,6 +505,12 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu
chunkMode = ElementMode.Vertex;
} else if (type == "sh") {
chunkMode = ElementMode.SH;
} else if (type == "safe_orbit_camera_elevation_min_max_radians") {
chunkMode = ElementMode.Float_Tuple;
} else if (type == "safe_orbit_camera_radius_min") {
chunkMode = ElementMode.Float;
} else if (type == "up_axis" || type == "chirality") {
chunkMode = ElementMode.Uchar;
}
}
}
Expand Down Expand Up @@ -516,7 +565,16 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu
const currentMode = faceCount ? Mode.Mesh : hasMandatoryProperties ? Mode.Splat : Mode.PointCloud;
// parsed ready ready to be used as a splat
return await new Promise((resolve) => {
resolve({ mode: currentMode, data: splatsData.buffer, sh: splatsData.sh, faces: faces, hasVertexColors: !!propertyColorCount, compressed: false, rawSplat: false });
resolve({
...metaData,
mode: currentMode,
data: splatsData.buffer,
sh: splatsData.sh,
faces: faces,
hasVertexColors: !!propertyColorCount,
compressed: false,
rawSplat: false,
});
});
});
}
Expand Down
5 changes: 5 additions & 0 deletions packages/dev/loaders/src/SPLAT/splatLoadingOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,9 @@ export type SPLATLoadingOptions = {
* @example import * as fflate from 'fflate';
*/
fflate?: unknown;

/**
* Disable automatic camera limits from being applied if they exist in the splat file
*/
disableAutoCameraLimits?: boolean;
};
4 changes: 2 additions & 2 deletions packages/dev/loaders/src/SPLAT/spz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Scalar } from "core/Maths/math.scalar";
import type { Scene } from "core/scene";
import type { SPLATLoadingOptions } from "./splatLoadingOptions";
import { Mode } from "./splatDefs";
import type { IParsedPLY } from "./splatDefs";
import type { IParsedSplat } from "./splatDefs";

/**
* Parses SPZ data and returns a promise resolving to an IParsedPLY object.
Expand All @@ -12,7 +12,7 @@ import type { IParsedPLY } from "./splatDefs";
* @param loadingOptions Options for loading Gaussian Splatting files.
* @returns A promise resolving to the parsed SPZ data.
*/
export function ParseSpz(data: ArrayBuffer, scene: Scene, loadingOptions: SPLATLoadingOptions): Promise<IParsedPLY> {
export function ParseSpz(data: ArrayBuffer, scene: Scene, loadingOptions: SPLATLoadingOptions): Promise<IParsedSplat> {
const ubuf = new Uint8Array(data);
const ubufu32 = new Uint32Array(data.slice(0, 12)); // Only need ubufu32[0] to [2]
// debug infos
Expand Down