PointCloudLoader
Copying and pasting? We've got you covered! You can find the full source code of this tutorial here.
☁️ Reality capture: point clouds + BIM
The PointCloudLoader streams out-of-core point clouds (Potree 2.0 octrees) and
draws them through the deferred PostproductionRenderer, so they are correctly
occluded by BIM geometry. It handles georeferenced precision (a float64 anchor),
centres the cloud on its data centroid, and exposes the tuned point material as
live quality settings (overlap, detail, point budget, max size, EDL).
This example does not ship any point cloud: you load your own. Pick (or drag and
drop) the three files of a Potree 2.0 dataset: metadata.json, hierarchy.bin
and octree.bin. You can produce them from a .las file with
buildPotreeOctree from @thatopen/components (dist/realityCapture), or with
PotreeConverter 2.
The loader needs potree-core installed. It is an optional peer dependency,
imported only when a cloud is loaded.
🖖 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 point 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 you can see points hidden behind its walls once your cloud is placed next to 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);
});
fragments.list.onItemSet.add(({ value: model }) => {
model.useCamera(world.camera.three);
world.scene.three.add(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 point cloud
Get the PointCloudLoader and give it the world. The loader fetches its bytes
through a requestManager, so the transport is yours: here it serves the files
you picked straight from memory, honouring the HTTP Range requests the octree
streaming makes.
const pcl = components.get(OBF.PointCloudLoader);
pcl.world = world;
const localFilesRequestManager = (
files: Map<string, File>,
): OBF.PointCloudRequestManager => ({
getUrl: (url: string) => url,
async fetch(input: RequestInfo | URL, init?: RequestInit) {
const url = typeof input === "string" ? input : input.toString();
const name = url.split(/[\\/]/).pop()!.split("?")[0];
const file = files.get(name);
if (!file) return new Response(null, { status: 404 });
const range = new Headers(init?.headers).get("Range");
const match = range?.match(/bytes=(\d+)-(\d*)/);
if (!match) return new Response(file);
const start = Number(match[1]);
const end = match[2] ? Number(match[2]) + 1 : file.size;
return new Response(file.slice(start, end), { status: 206 });
},
});
const clipper = components.get(OBC.Clipper);
clipper.enabled = true;
let section: string | null = null;
const loadFiles = async (list: FileList | File[]) => {
const files = new Map<string, File>();
for (const file of Array.from(list)) files.set(file.name, file);
const required = ["metadata.json", "hierarchy.bin", "octree.bin"];
const missing = required.filter((name) => !files.has(name));
if (missing.length) {
alert(`Missing Potree 2.0 files: ${missing.join(", ")}`);
return;
}
// `fitCamera` because this tutorial is ABOUT the cloud: it defaults off so
// the loader never moves a camera an app has placed.
const cloud = await pcl.load(
{ url: "local", requestManager: localFilesRequestManager(files) },
{ id: `local-${pcl.list.size}`, fitCamera: true },
);
// Widen near/far so the anchored cloud isn't clipped at any scale.
const diag = cloud.box.getSize(new THREE.Vector3()).length() || 100;
world.camera.three.near = Math.max(diag / 5000, 0.01);
world.camera.three.far = diag * 200;
world.camera.three.updateProjectionMatrix();
// A horizontal clipping plane through the middle of the cloud. The loader
// hands the active planes to its point material, so the cut happens in the
// point shader and a cloud and a model share one plane.
if (section) await clipper.delete(world, section);
section = clipper.createFromNormalAndCoplanarPoint(
world,
new THREE.Vector3(0, 1, 0),
cloud.box.getCenter(new THREE.Vector3()),
);
};
const pickFiles = () => {
const input = document.createElement("input");
input.type = "file";
input.multiple = true;
input.accept = ".json,.bin";
input.addEventListener("change", () => {
if (input.files) void loadFiles(input.files);
});
input.click();
};
// Drag and drop the three files onto the viewer works too.
container.addEventListener("dragover", (event) => event.preventDefault());
container.addEventListener("drop", (event) => {
event.preventDefault();
if (event.dataTransfer?.files) void loadFiles(event.dataTransfer.files);
});
⏱️ 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 files and to wire the point-cloud quality
settings to a panel.