diff --git a/.vscode/settings.json b/.vscode/settings.json index 38c28e90111..35952d9c905 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "cSpell.words": [ "acumin", "babylonjs", + "chirality", "desaturated", "fluentui", "multilines", diff --git a/packages/dev/loaders/src/SPLAT/sog.ts b/packages/dev/loaders/src/SPLAT/sog.ts index 4585b76412e..2b61b28a272 100644 --- a/packages/dev/loaders/src/SPLAT/sog.ts +++ b/packages/dev/loaders/src/SPLAT/sog.ts @@ -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"; @@ -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 { +async function ParseSogDatas(data: SOGRootData, imageDataArrays: IWebPImage[], scene: Scene): Promise { 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); @@ -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, rootUrl: string, scene: Scene): Promise { +export async function ParseSogMeta(dataOrFiles: SOGRootData | Map, rootUrl: string, scene: Scene): Promise { let data: SOGRootData; let files: Map | undefined; diff --git a/packages/dev/loaders/src/SPLAT/splatDefs.ts b/packages/dev/loaders/src/SPLAT/splatDefs.ts index d1db9200361..04a00310c33 100644 --- a/packages/dev/loaders/src/SPLAT/splatDefs.ts +++ b/packages/dev/loaders/src/SPLAT/splatDefs.ts @@ -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[]; @@ -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"; } diff --git a/packages/dev/loaders/src/SPLAT/splatFileLoader.ts b/packages/dev/loaders/src/SPLAT/splatFileLoader.ts index ae72fa9c85e..e5fd61424d0 100644 --- a/packages/dev/loaders/src/SPLAT/splatFileLoader.ts +++ b/packages/dev/loaders/src/SPLAT/splatFileLoader.ts @@ -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 @@ -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); @@ -201,7 +202,7 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu private _parseAsync(meshesNames: any, scene: Scene, data: any, rootUrl: string): Promise> { const babylonMeshesArray: Array = []; //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; @@ -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); }); }) @@ -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 @@ -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 { + private static _ConvertPLYToSplat(data: ArrayBuffer): Promise { const ubuf = new Uint8Array(data); const header = new TextDecoder().decode(ubuf.slice(0, 1024 * 10)); const headerEnd = "end_header\n"; @@ -428,16 +453,20 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu offset: number; }; - const enum ElementMode { - Vertex = 0, - Chunk = 1, - SH = 2, - } + const ElementMode: Record = { + 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 = {}; for (const prop of filtered) { if (prop.startsWith("property ")) { const [, type, name] = prop.split(" "); @@ -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}.`); } @@ -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; } } } @@ -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, + }); }); }); } diff --git a/packages/dev/loaders/src/SPLAT/splatLoadingOptions.ts b/packages/dev/loaders/src/SPLAT/splatLoadingOptions.ts index 57fddaef28b..34359ef1bf0 100644 --- a/packages/dev/loaders/src/SPLAT/splatLoadingOptions.ts +++ b/packages/dev/loaders/src/SPLAT/splatLoadingOptions.ts @@ -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; }; diff --git a/packages/dev/loaders/src/SPLAT/spz.ts b/packages/dev/loaders/src/SPLAT/spz.ts index 49034cfeee5..4c65a05080f 100644 --- a/packages/dev/loaders/src/SPLAT/spz.ts +++ b/packages/dev/loaders/src/SPLAT/spz.ts @@ -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. @@ -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 { +export function ParseSpz(data: ArrayBuffer, scene: Scene, loadingOptions: SPLATLoadingOptions): Promise { const ubuf = new Uint8Array(data); const ubufu32 = new Uint32Array(data.slice(0, 12)); // Only need ubufu32[0] to [2] // debug infos