Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cloud Optimised Point Cloud (COPC) support #2110

Merged
merged 4 commits into from
Apr 30, 2024
Merged
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
4 changes: 3 additions & 1 deletion docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"OrientedImageLayer",
"PointCloudLayer",
"PotreeLayer",
"CopcLayer",
"C3DTilesLayer",
"LabelLayer",
"GlobeLayer",
Expand All @@ -72,7 +73,8 @@
"OrientedImageSource",
"PotreeSource",
"VectorTilesSource",
"EntwinePointTileSource"
"EntwinePointTileSource",
"CopcSource"
],

"Provider": [
Expand Down
3 changes: 2 additions & 1 deletion examples/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"potree_3d_map": "Potree 3D map",
"laz_dragndrop": "LAS/LAZ viewer",
"entwine_simple_loader": "Entwine loader",
"entwine_3d_loader": "Entwine 3D loader"
"entwine_3d_loader": "Entwine 3D loader",
"copc_simple_loader": "COPC loader"
},

"Vector tiles": {
Expand Down
128 changes: 128 additions & 0 deletions examples/copc_simple_loader.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<html>
<head>
<meta charset="UTF-8">

<title>Itowns - COPC loader</title>

<link rel="stylesheet" type="text/css" href="css/example.css">
<link rel="stylesheet" type="text/css" href="css/LoadingScreen.css">

<style>
#description {
z-index: 2;
left: 10px;
}
</style>

<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.6/dat.gui.min.js"></script>
</head>
<body>
<div id="description">Specify the URL of a COPC file to load:
<input type="text" id="url" />
<button onclick="readURL()">Load</button>
<div>
<button onClick="loadAutzen()">Load Autzen Stadium (80mb)</button>
<button onClick="loadSofi()">Load SoFI Stadium (2.3gb)</button>
<button onClick="loadMillsite()">Load Millsite (1.9gb)</button>
<div id="share"></div>
</div>
</div>
<div id="viewerDiv"></div>

<script src="../dist/itowns.js"></script>
<script src="js/GUI/LoadingScreen.js"></script>
<script src="../dist/debug.js"></script>
<script type="text/javascript">
let layer; // COPCLayer

const uri = new URL(location);

const gui = new dat.GUI();

const viewerDiv = document.getElementById('viewerDiv');
const view = new itowns.View('EPSG:4326', viewerDiv);
const controls = new itowns.PlanarControls(view);
view.mainLoop.gfxEngine.renderer.setClearColor(0xdddddd);

setUrl(uri.searchParams.get('copc'));

function onLayerReady(layer) {
const camera = view.camera.camera3D;

const lookAt = new itowns.THREE.Vector3();
const size = new itowns.THREE.Vector3();
layer.root.bbox.getSize(size);
layer.root.bbox.getCenter(lookAt);

camera.far = 2.0 * size.length();

controls.groundLevel = layer.root.bbox.min.z;
const position = layer.root.bbox.min.clone().add(
size.multiply({ x: 1, y: 1, z: size.x / size.z }),
);

camera.position.copy(position);
camera.lookAt(lookAt);
camera.updateProjectionMatrix();

view.notifyChange(camera);
}


function readURL() {
const url = document.getElementById('url').value;

if (url) {
setUrl(url);
}
}

function setUrl(url) {
if (!url) return;

const input_url = document.getElementById('url');
if (!input_url) return;

uri.searchParams.set('copc', url);
history.replaceState(null, null, `?${uri.searchParams.toString()}`);

input_url.value = url;
load(url);
}


function load(url) {
const source = new itowns.CopcSource({ url });

if (layer) {
gui.removeFolder(layer.debugUI);
view.removeLayer('COPC');
view.notifyChange();
layer.delete();
}

layer = new itowns.CopcLayer('COPC', {
source,
crs: view.referenceCrs,
sseThreshold: 2,
pointBudget: 3000000,
});
view.addLayer(layer).then(onLayerReady);
debug.PotreeDebug.initTools(view, layer, gui);
}

function loadAutzen() {
setUrl("https://s3.amazonaws.com/hobu-lidar/autzen-classified.copc.laz");
Desplandis marked this conversation as resolved.
Show resolved Hide resolved
}

function loadSofi() {
setUrl("https://s3.amazonaws.com/hobu-lidar/sofi.copc.laz");
}

function loadMillsite() {
setUrl("https://s3.amazonaws.com/data.entwine.io/millsite.copc.laz");
}
</script>
</body>
</html>
194 changes: 194 additions & 0 deletions src/Core/CopcNode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import * as THREE from 'three';
import { Hierarchy } from 'copc';
import PointCloudNode from 'Core/PointCloudNode';

const size = new THREE.Vector3();
const position = new THREE.Vector3();
const translation = new THREE.Vector3();

function buildId(depth, x, y, z) {
return `${depth}-${x}-${y}-${z}`;
}

class CopcNode extends PointCloudNode {
/**
* Constructs a new instance of a COPC Octree node
*
* @param {number} depth - Depth within the octree
* @param {number} x - X position within the octree
* @param {number} y - Y position within the octree
* @param {number} z - Z position with the octree
* @param {number} entryOffset - Offset from the beginning of the file of
* the node entry
* @param {number} entryLength - Size of the node entry
* @param {CopcLayer} layer - Parent COPC layer
* @param {number} [numPoints=0] - Number of points given by this entry
*/
constructor(depth, x, y, z, entryOffset, entryLength, layer, numPoints = 0) {
super(numPoints, layer);
this.isCopcNode = true;

this.entryOffset = entryOffset;
this.entryLength = entryLength;
this.layer = layer;
this.depth = depth;
this.x = x;
this.y = y;
this.z = z;
}

get id() {
return buildId(this.depth, this.x, this.y, this.z);
}

get octreeIsLoaded() {
return this.numPoints >= 0;
}

/**
* @param {number} offset
* @param {number} size
*/
async _fetch(offset, size) {
return this.layer.source.fetcher(this.layer.source.url, {
...this.layer.source.networkOptions,
headers: {
...this.layer.source.networkOptions.headers,
range: `bytes=${offset}-${offset + size - 1}`,
},
});
Desplandis marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Create an (A)xis (A)ligned (B)ounding (B)ox for the given node given
* `this` is its parent.
* @param {CopcNode} node - The child node
*/
createChildAABB(node) {
// factor to apply, based on the depth difference (can be > 1)
const f = 2 ** (node.depth - this.depth);

// size of the child node bbox (Vector3), based on the size of the
// parent node, and divided by the factor
this.bbox.getSize(size).divideScalar(f);

// initialize the child node bbox at the location of the parent node bbox
node.bbox.min.copy(this.bbox.min);

// position of the parent node, if it was at the same depth as the
// child, found by multiplying the tree position by the factor
position.copy(this).multiplyScalar(f);

// difference in position between the two nodes, at child depth, and
// scale it using the size
translation.subVectors(node, position).multiply(size);

// apply the translation to the child node bbox
node.bbox.min.add(translation);

// use the size computed above to set the max
node.bbox.max.copy(node.bbox.min).add(size);
}

/**
* Create a CopcNode from the provided subtree and add it as child
* of the current node.
* @param {number} depth - Child node depth in the octree
* @param {number} x - Child node x position in the octree
* @param {number} y - Child node y position in the octree
* @param {number} z - Child node z position in the octree
* @param {Hierarchy.Subtree} hierarchy - Octree's subtree
* @param {CopcNode[]} stack - Stack of node candidates for traversal
*/
findAndCreateChild(depth, x, y, z, hierarchy, stack) {
const id = buildId(depth, x, y, z);

let pointCount;
let offset;
let byteSize;

const node = hierarchy.nodes[id];
if (node) {
pointCount = node.pointCount;
offset = node.pointDataOffset;
byteSize = node.pointDataLength;
Desplandis marked this conversation as resolved.
Show resolved Hide resolved
} else {
const page = hierarchy.pages[id];
if (!page) { return; }
pointCount = -1;
offset = page.pageOffset;
byteSize = page.pageLength;
}

const child = new CopcNode(
depth,
x,
y,
z,
offset,
byteSize,
this.layer,
pointCount,
);
this.add(child);
stack.push(child);
}

async loadOctree() {
// Load hierarchy
const buffer = await this._fetch(this.entryOffset, this.entryLength);
const hierarchy = await Hierarchy.parse(new Uint8Array(buffer));

// Update current node entry from loaded subtree
const node = hierarchy.nodes[this.id];
if (!node) {
return Promise.reject('[CopcNode]: Ill-formed data, entry not found in hierarchy.');
}
this.numPoints = node.pointCount;
this.entryOffset = node.pointDataOffset;
this.entryLength = node.pointDataLength;

// Load subtree entries
const stack = [];
stack.push(this);
while (stack.length) {
const node = stack.shift();
const depth = node.depth + 1;
const x = node.x * 2;
const y = node.y * 2;
const z = node.z * 2;

node.findAndCreateChild(depth, x, y, z, hierarchy, stack);
node.findAndCreateChild(depth, x + 1, y, z, hierarchy, stack);
node.findAndCreateChild(depth, x, y + 1, z, hierarchy, stack);
node.findAndCreateChild(depth, x + 1, y + 1, z, hierarchy, stack);
node.findAndCreateChild(depth, x, y, z + 1, hierarchy, stack);
node.findAndCreateChild(depth, x + 1, y, z + 1, hierarchy, stack);
node.findAndCreateChild(depth, x, y + 1, z + 1, hierarchy, stack);
node.findAndCreateChild(depth, x + 1, y + 1, z + 1, hierarchy, stack);
mgermerie marked this conversation as resolved.
Show resolved Hide resolved
}
}

/**
* Load the COPC Buffer geometry for this node.
* @returns {Promise<THREE.BufferGeometry>}
*/
async load() {
if (!this.octreeIsLoaded) {
await this.loadOctree();
}

const buffer = await this._fetch(this.entryOffset, this.entryLength);
const geometry = await this.layer.source.parser(buffer, {
in: {
...this.layer.source,
pointCount: this.numPoints,
},
out: this.layer,
});

return geometry;
}
}

export default CopcNode;
Loading
Loading