diff --git a/docs/docs/splat-mesh.md b/docs/docs/splat-mesh.md index 177342a..dfb4702 100644 --- a/docs/docs/splat-mesh.md +++ b/docs/docs/splat-mesh.md @@ -97,6 +97,13 @@ Creates a new splat with the provided parameters (all values in "float" space, i This method iterates over all splats in this instance's `packedSplats`, invoking the provided callback with `index: number` in `0..=(this.numSplats-1)`, `center: THREE.Vector3`, `scales: THREE.Vector3`, `quaternion: THREE.Quaternion`, `opacity: number` (0..1), and `color: THREE.Color` (rgb values in 0..1). Note that the objects passed in as `center` etc. are the same for every callback invocation: they are reused for efficiency. *Changing these values has no effect* as they are decoded/unpacked copies of the underlying data. To update the `packedSplats`, call `.packedSplats.setSplat(index, center, scales, quaternion, opacity, color)`. + +## `getBoundingBox(centers_only=true)` + +This method returns a `THREE.Box3` representing the axis-aligned bounding box of all splats in the mesh. +The parameter `centers_only` (boolean, default: `true`) controls whether we calculate the bounding box using only splat center positions, or include the full extent of each splat by considering their scales and orientations. The latter gives a slightly more accurate but more computationally expensive bounding box. +Note that this function will raise an error if called before splats are initialized. + ## `updateGenerator()` Call this whenever something changes in the splat processing pipeline, for example changing `maxSh` or updating `objectModifier` or `worldModifier`. Compiled generators are cached for efficiency and re-used when the same graph structure emerges after successive changes. diff --git a/examples/editor/index.html b/examples/editor/index.html index 887e68b..946ab9d 100644 --- a/examples/editor/index.html +++ b/examples/editor/index.html @@ -145,6 +145,7 @@ reversePointerDir: false, reversePointerSlide: false, backgroundColor: "#000000", + viewBoundingBox: false, openFiles: () => { fileInput.click(); }, @@ -340,7 +341,7 @@ async function loadFiles(splatFiles) { if (guiOptions.resetOnLoad) { - const toRemove = frame.children.filter((child) => child instanceof SplatMesh); + const toRemove = frame.children.filter((child) => child instanceof SplatMesh || child instanceof THREE.Box3Helper); for (const child of toRemove) { frame.remove(child); } @@ -403,6 +404,7 @@ } frame.add(splatMesh); console.log(`Loaded ${fileName} with ${splatMesh.numSplats} splats`); + addBoundingBoxHelper(splatMesh); const splatFolder = splatsFolder.addFolder(fileName).close(); splatFolder.add(splatMesh, "opacity", 0, 1, 0.01).name("Opacity").listen(); @@ -433,6 +435,14 @@ canvas.focus(); } + async function addBoundingBoxHelper(splatMesh) { + await splatMesh.initialized; + const box = splatMesh.getBoundingBox(); + const boxHelper = new THREE.Box3Helper(box, 0x00ff00); + boxHelper.visible = guiOptions.viewBoundingBox; + frame.add(boxHelper); + } + secondGui.add(guiOptions, "resetOnLoad").name("Reset on load"); secondGui.add(guiOptions, "loadOffset", -2, 2, 0.01).name("Loading offset"); secondGui.add(guiOptions, "openFiles").name("Select Files"); @@ -502,6 +512,13 @@ const debugFolder = gui.addFolder("Debug").close(); const normalColor = dyno.dynoBool(false); debugFolder.add(normalColor, "value").name("Normal color").onChange(() => updateFrameSplats()); + debugFolder.add(guiOptions, "viewBoundingBox").name("View bounding boxes").onChange((viewBoundingBox) => { + frame.children.forEach((child) => { + if (child instanceof THREE.Box3Helper) { + child.visible = viewBoundingBox; + } + }); + }); debugFolder.add(spark, "maxStdDev", 0.1, 3.0, 0.01).name("Max Gsplat stddev").listen(); debugFolder.add(spark, "falloff", 0, 1, 0.01).name("Gaussian falloff").listen(); @@ -688,6 +705,7 @@ } const instructions = makeInstructions(); + addBoundingBoxHelper(instructions); frame.add(instructions); // Load URLs from query parameters if any diff --git a/src/SplatMesh.ts b/src/SplatMesh.ts index 4a696db..2940b16 100644 --- a/src/SplatMesh.ts +++ b/src/SplatMesh.ts @@ -319,6 +319,53 @@ export class SplatMesh extends SplatGenerator { this.packedSplats.dispose(); } + // Returns axis-aligned bounding box of the SplatMesh. If centers_only is true, + // only the centers of the splats are used to compute the bounding box. + // IMPORTANT: This should only be called after the SplatMesh is initialized. + getBoundingBox(centers_only = true) { + if (!this.initialized) { + throw new Error( + "Cannot get bounding box before SplatMesh is initialized", + ); + } + const minVec = new THREE.Vector3( + Number.POSITIVE_INFINITY, + Number.POSITIVE_INFINITY, + Number.POSITIVE_INFINITY, + ); + const maxVec = new THREE.Vector3( + Number.NEGATIVE_INFINITY, + Number.NEGATIVE_INFINITY, + Number.NEGATIVE_INFINITY, + ); + const corners = new THREE.Vector3(); + const signs = [-1, 1]; + this.packedSplats.forEachSplat( + (_index, center, scales, quaternion, _opacity, _color) => { + if (centers_only) { + minVec.min(center); + maxVec.max(center); + } else { + // Get the 8 corners of the AABB in local space + for (const x of signs) { + for (const y of signs) { + for (const z of signs) { + corners.set(x * scales.x, y * scales.y, z * scales.z); + // Transform corner by rotation and position + corners.applyQuaternion(quaternion); + corners.add(center); + minVec.min(corners); + maxVec.max(corners); + } + } + } + } + }, + ); + const box = new THREE.Box3(minVec, maxVec); + return box; + } + constructGenerator(context: SplatMeshContext) { const { transform, viewToObject, recolor } = context; const generator = dynoBlock(