SplatLoader
Copying and pasting? We've got you covered! You can find the full source code of this tutorial here.
✨ Reality capture: gaussian splats + BIM
The SplatLoader renders 3D Gaussian splats (.ply / .compressed.ply) through
the deferred PostproductionRenderer, so they are correctly occluded by BIM
geometry. The splats are drawn by an in-house WebGL2 renderer (EWA covariance
billboards + a worker global depth sort) into the deferred occlusion layer and
depth-tested against the BIM depth: no external splat engine.
This example does not ship any capture: you load your own. Pick (or drag and
drop) a gaussian splat .ply (INRIA 3DGS) or .compressed.ply file.
🖖 Importing our Libraries
import * as THREE from "three";
import * as OBC from "@thatopen/components";
import Stats from "stats.js";
import * as BUI from "@thatopen/ui";
// You have to import * as OBF from "@thatopen/components-front"
import * as OBF from "../..";
🌎 Setting up the scene
A simple world with the PostproductionRenderer in DEFERRED mode: the pipeline
the splat loader draws through.
const components = new OBC.Components();
const worlds = components.get(OBC.Worlds);
const world = worlds.create<
OBC.SimpleScene,
OBC.OrthoPerspectiveCamera,
OBF.PostproductionRenderer
>();
world.scene = new OBC.SimpleScene(components);
world.scene.setup();
world.scene.three.background = null;
const container = document.getElementById("container")!;
world.renderer = new OBF.PostproductionRenderer(components, container);
world.camera = new OBC.OrthoPerspectiveCamera(components);
components.init();
const grids = components.get(OBC.Grids);
const grid = grids.create(world);
grid.config.color.set(0x666666);
world.renderer.postproduction.enabled = true;
world.renderer.postproduction.mode = OBF.PostproductionMode.DEFERRED;
world.renderer.postproduction.style =
OBF.PostproductionAspect.COLOR_PEN_SHADOWS;
world.renderer.postproduction.basePass.isolatedMaterials.push(grid.material);
world.dynamicAnchor = false;
🏢 A BIM model as occluder
We load a sample Fragments model so splats behind its walls get hidden once
your capture is placed next to it. The FragmentsManager is initialised
regardless: the splat loader registers a pick contributor on the world's
FastModelPicker, which relies on it.
const workerUrl = await OBC.FragmentsManager.getWorker();
const fragments = components.get(OBC.FragmentsManager);
fragments.init(workerUrl);
world.camera.controls.addEventListener("update", () => fragments.core.update());
world.onCameraChanged.add((camera) => {
for (const [, model] of fragments.list) model.useCamera(camera.three);
fragments.core.update(true);
});
const bimObjects: THREE.Object3D[] = [];
fragments.list.onItemSet.add(({ value: model }) => {
model.useCamera(world.camera.three);
world.scene.three.add(model.object);
bimObjects.push(model.object);
fragments.core.update(true);
});
const fragFile = await fetch(
"https://thatopen.github.io/engine_components/resources/frags/school_arq.frag",
);
await fragments.core.load(await fragFile.arrayBuffer(), {
modelId: "school_arq",
});
✨ Loading your splats
Get the SplatLoader, give it the world, and hand it the bytes of your file.
Captured splats are often non-metric: gaussianScale scales every dataset so
you can bring yours to true size next to the BIM.
const splats = components.get(OBF.SplatLoader);
splats.world = world;
let current: OBF.LoadedSplat | null = null;
const frameSplat = async () => {
if (!current) return;
const center = current.box.getCenter(new THREE.Vector3());
const radius = current.box.getSize(new THREE.Vector3()).length() / 2 || 8;
await world.camera.controls.setLookAt(
center.x + radius,
center.y + radius * 0.7,
center.z + radius,
center.x,
center.y,
center.z,
true,
);
};
const loadFile = async (file: File) => {
const fileBytes = new Uint8Array(await file.arrayBuffer());
if (current) splats.remove(current.id);
current = await splats.load({ fileBytes }, { id: file.name });
await frameSplat();
};
const pickFile = () => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".ply";
input.addEventListener("change", () => {
const file = input.files?.[0];
if (file) void loadFile(file);
});
input.click();
};
// Drag and drop the file onto the viewer works too.
container.addEventListener("dragover", (event) => event.preventDefault());
container.addEventListener("drop", (event) => {
event.preventDefault();
const file = event.dataTransfer?.files?.[0];
if (file) void loadFile(file);
});
⏱️ Measuring the performance (optional)
const stats = new Stats();
stats.showPanel(2);
document.body.append(stats.dom);
stats.dom.style.left = "0px";
stats.dom.style.zIndex = "unset";
world.renderer.onBeforeUpdate.add(() => stats.begin());
world.renderer.onAfterUpdate.add(() => stats.end());
🧩 Adding some UI
We use @thatopen/ui to load your file and to wire the splat settings to a
panel.