Skip to content

Commit

Permalink
refactor(pointcloud): simplify SSE computation for pointcloud
Browse files Browse the repository at this point in the history
Project average point spacing on screen and compare to sse threshold.
It's a simplification of the approach proposed in #674.

This allows to drop a dependency (convexhull) and brings the SSE
compuation for pointcloud closer to what is done for other geometries.
  • Loading branch information
peppsac committed Apr 16, 2018
1 parent 5efcbd0 commit 279a857
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 1,128 deletions.
963 changes: 1 addition & 962 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
"flatbush": "^1.3.0",
"js-priority-queue": "^0.1.5",
"jszip": "^3.1.3",
"monotone-convex-hull-2d": "^1.0.1",
"text-encoding-utf-8": "^1.0.1",
"togeojson": "^0.16.0",
"url-polyfill": "^1.0.8",
Expand Down
241 changes: 86 additions & 155 deletions src/Process/PointCloudProcessing.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as THREE from 'three';
import convexHull from 'monotone-convex-hull-2d';
import CancelledCommandException from '../Core/Scheduler/CancelledCommandException';

// Draw a cube with lines (12 lines).
Expand Down Expand Up @@ -35,59 +34,6 @@ function cube(size) {
return geometry;
}

// TODO: move this function to Camera, as soon as it's good enough (see https://github.com/iTowns/itowns/pull/381#pullrequestreview-49107682)
const temp = {
points: [
new THREE.Vector3(),
new THREE.Vector3(),
new THREE.Vector3(),
new THREE.Vector3(),
new THREE.Vector3(),
new THREE.Vector3(),
new THREE.Vector3(),
new THREE.Vector3(),
],
box3: new THREE.Box3(),
matrix4: new THREE.Matrix4(),
};
function box3SurfaceOnScreen(camera, box3d, matrixWorld) {
if (box3d.isEmpty()) {
return 0;
}

temp.box3.copy(box3d);
if (matrixWorld) {
temp.matrix4.multiplyMatrices(camera._viewMatrix, matrixWorld);
} else {
temp.matrix4.copy(camera._viewMatrix);
}

// copy pasted / adapted from Box3.applyMatrix4
// NOTE: I am using a binary pattern to specify all 2^3 combinations below
temp.points[0].set(temp.box3.min.x, temp.box3.min.y, temp.box3.min.z).applyMatrix4(temp.matrix4); // 000
temp.points[1].set(temp.box3.min.x, temp.box3.min.y, temp.box3.max.z).applyMatrix4(temp.matrix4); // 001
temp.points[2].set(temp.box3.min.x, temp.box3.max.y, temp.box3.min.z).applyMatrix4(temp.matrix4); // 010
temp.points[3].set(temp.box3.min.x, temp.box3.max.y, temp.box3.max.z).applyMatrix4(temp.matrix4); // 011
temp.points[4].set(temp.box3.max.x, temp.box3.min.y, temp.box3.min.z).applyMatrix4(temp.matrix4); // 100
temp.points[5].set(temp.box3.max.x, temp.box3.min.y, temp.box3.max.z).applyMatrix4(temp.matrix4); // 101
temp.points[6].set(temp.box3.max.x, temp.box3.max.y, temp.box3.min.z).applyMatrix4(temp.matrix4); // 110
temp.points[7].set(temp.box3.max.x, temp.box3.max.y, temp.box3.max.z).applyMatrix4(temp.matrix4); // 111

for (const pt of temp.points) {
// translate/scale to [0, width]x[0, height]
pt.x = camera.width * (pt.x + 1) * 0.5;
pt.y = camera.height * (1 - pt.y) * 0.5;
pt.z = 0;
}

const indices = convexHull(temp.points.map(v => [v.x, v.y]));
const contour = indices.map(i => temp.points[i]);

const area = THREE.ShapeUtils.area(contour);

return Math.abs(area);
}

function initBoundingBox(elt, layer) {
const size = elt.tightbbox.getSize();
elt.obj.boxHelper = new THREE.LineSegments(
Expand All @@ -106,41 +52,18 @@ function initBoundingBox(elt, layer) {
elt.obj.boxHelper.updateMatrixWorld();
}

function shouldDisplayNode(context, layer, elt) {
let shouldBeLoaded = 0;

if (layer.octreeDepthLimit >= 0 && layer.octreeDepthLimit < elt.name.length) {
return { shouldBeLoaded, surfaceOnScreen: 0 };
function computeScreenSpaceError(context, layer, elt, distance) {
if (distance <= 0) {
return layer.sseThreshold;
}

const numPoints = elt.numPoints;

const cl = (elt.tightbbox ? elt.tightbbox : elt.bbox);

const visible = context.camera.isBox3Visible(cl, layer.object3d.matrixWorld);
const surfaceOnScreen = 0;

if (visible) {
if (cl.containsPoint(context.camera.camera3D.position)) {
shouldBeLoaded = 1;
} else {
const surfaceOnScreen = box3SurfaceOnScreen(context.camera, cl, layer.object3d.matrixWorld);

// no point indicates shallow hierarchy, so we definitely want to load its children
if (numPoints == 0) {
shouldBeLoaded = 1;
} else {
const count = layer.overdraw * (surfaceOnScreen / Math.pow(layer.pointSize, 2));
shouldBeLoaded = Math.min(count / numPoints, 1);
}

elt.surfaceOnScreen = surfaceOnScreen;
}
} else {
shouldBeLoaded = -1;
}

return { shouldBeLoaded, surfaceOnScreen };
const pointSpacing = layer.metadata.spacing / Math.pow(2, elt.name.length);
// Estimate the onscreen distance between 2 points
const onScreenSpacing = context.camera.preSSE * pointSpacing / distance;
// [ P1 ]--------------[ P2 ]
// <---------------------> = pointsSpacing (in world coordinates)
// ~ onScreenSpacing (in pixels)
// <------> = layer.pointSize (in pixels)
return Math.max(0.0, onScreenSpacing - layer.pointSize);
}

function markForDeletion(elt) {
Expand All @@ -155,7 +78,7 @@ function markForDeletion(elt) {

if (!elt.notVisibleSince) {
elt.notVisibleSince = Date.now();
elt.shouldBeLoaded = -1;
elt.sse = -1;
}
for (const child of elt.children) {
markForDeletion(child);
Expand All @@ -169,6 +92,11 @@ export default {
return [];
}

context.camera.preSSE =
context.camera.height /
(2 * Math.tan(THREE.Math.degToRad(context.camera.camera3D.fov) * 0.5));


if (changeSources.has(undefined) || changeSources.size == 0) {
return [layer.root];
}
Expand Down Expand Up @@ -212,74 +140,85 @@ export default {
},

update(context, layer, elt) {
const { shouldBeLoaded } = shouldDisplayNode(context, layer, elt);
elt.visible = false;

elt.shouldBeLoaded = shouldBeLoaded;
if (layer.octreeDepthLimit >= 0 && layer.octreeDepthLimit < elt.name.length) {
markForDeletion(elt);
return;
}

if (shouldBeLoaded > 0) {
elt.notVisibleSince = undefined;
// pick the best bounding box
const bbox = (elt.tightbbox ? elt.tightbbox : elt.bbox);
elt.visible = context.camera.isBox3Visible(bbox, layer.object3d.matrixWorld);
if (!elt.visible) {
markForDeletion(elt);
return;
}

// only load geometry if this elements has points
if (elt.numPoints > 0) {
if (elt.obj) {
elt.obj.material.visible = true;
if (__DEBUG__) {
elt.obj.material.uniforms.density.value = elt.density;
elt.notVisibleSince = undefined;

if (layer.bboxes.visible) {
if (!elt.obj.boxHelper) {
initBoundingBox(elt, layer);
}
elt.obj.boxHelper.visible = true;
elt.obj.boxHelper.material.color.r = 1 - shouldBeLoaded;
elt.obj.boxHelper.material.color.g = shouldBeLoaded;
// only load geometry if this elements has points
if (elt.numPoints > 0) {
if (elt.obj) {
elt.obj.material.visible = true;
elt.obj.material.uniforms.size.value = layer.pointSize;

if (__DEBUG__) {
if (layer.bboxes.visible) {
if (!elt.obj.boxHelper) {
initBoundingBox(elt, layer);
}
elt.obj.boxHelper.visible = true;
elt.obj.boxHelper.material.color.r = 1 - elt.sse;
elt.obj.boxHelper.material.color.g = elt.sse;
}
}
} else if (!elt.promise) {
const distance = Math.max(0.001, bbox.distanceToPoint(context.camera.camera3D.position));
const priority = computeScreenSpaceError(context, layer, elt, distance) / distance;
elt.promise = context.scheduler.execute({
layer,
requester: elt,
view: context.view,
priority,
redraw: true,
isLeaf: elt.childrenBitField == 0,
earlyDropFunction: cmd => !cmd.requester.visible,
}).then((pts) => {
if (layer.onPointsCreated) {
layer.onPointsCreated(layer, pts);
}
elt.obj.material.uniforms.size.value = layer.pointSize;
} else if (!elt.promise) {
// TODO:
// - add command cancelation support
// - rework priority
elt.promise = context.scheduler.execute({
layer,
requester: elt,
view: context.view,
priority: 1.0 / elt.name.length, // surfaceOnScreen,
redraw: true,
isLeaf: elt.childrenBitField == 0,
earlyDropFunction: cmd => cmd.requester.shouldBeLoaded <= 0,
}).then((pts) => {
if (layer.onPointsCreated) {
layer.onPointsCreated(layer, pts);
}

elt.obj = pts;
// store tightbbox to avoid ping-pong (bbox = larger => visible, tight => invisible)
elt.tightbbox = pts.tightbbox;
elt.obj = pts;
// store tightbbox to avoid ping-pong (bbox = larger => visible, tight => invisible)
elt.tightbbox = pts.tightbbox;

// make sure to add it here, otherwise it might never
// be added nor cleaned
layer.group.add(elt.obj);
elt.obj.updateMatrixWorld(true);
// make sure to add it here, otherwise it might never
// be added nor cleaned
layer.group.add(elt.obj);
elt.obj.updateMatrixWorld(true);

elt.obj.owner = elt;
elt.obj.owner = elt;
elt.promise = null;
}, (err) => {
if (err instanceof CancelledCommandException) {
elt.promise = null;
}, (err) => {
if (err instanceof CancelledCommandException) {
elt.promise = null;
}
});
}
}
});
}
} else {
// not visible / displayed
markForDeletion(elt);
}

if (shouldBeLoaded >= 0.9 && elt.children && elt.children.length) {
return elt.children;
if (elt.children && elt.children.length) {
const distance = bbox.distanceToPoint(context.camera.camera3D.position);
elt.sse = computeScreenSpaceError(context, layer, elt, distance) / layer.sseThreshold;
if (elt.sse >= 1) {
return elt.children;
} else {
for (const child of elt.children) {
markForDeletion(child);
}
}
}
return undefined;
},

postUpdate(context, layer) {
Expand All @@ -290,17 +229,9 @@ export default {
layer.displayedCount = 0;
for (const pts of layer.group.children) {
if (pts.material.visible) {
if (layer.metadata.customBinFormat) {
const count = Math.floor(pts.owner.shouldBeLoaded * pts.geometry.attributes.position.count);
if (count > 0) {
pts.geometry.setDrawRange(0, count);
layer.displayedCount += count;
} else {
pts.material.visible = false;
}
} else {
layer.displayedCount += pts.geometry.attributes.position.count;
}
const count = pts.geometry.attributes.position.count;
pts.geometry.setDrawRange(0, count);
layer.displayedCount += count;
}
}

Expand All @@ -326,7 +257,7 @@ export default {
// This format doesn't require points to be evenly distributed, so
// we're going to sort the nodes by "importance" (= on screen size)
// and display only the first N nodes
layer.group.children.sort((p1, p2) => p2.owner.surfaceOnScreen - p1.owner.surfaceOnScreen);
layer.group.children.sort((p1, p2) => p2.owner.sse - p1.owner.sse);

let limitHit = false;
layer.displayedCount = 0;
Expand Down
5 changes: 3 additions & 2 deletions src/Provider/PointCloudProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,15 @@ export default {
layer.bboxes = new THREE.Group();
layer.object3d.add(layer.bboxes);
layer.bboxes.updateMatrixWorld();
layer.bboxes.visible = false;
}

// default options
layer.fetchOptions = layer.fetchOptions || {};
layer.octreeDepthLimit = layer.octreeDepthLimit || -1;
layer.pointBudget = layer.pointBudget || 15000000;
layer.pointBudget = layer.pointBudget || 2000000;
layer.pointSize = layer.pointSize === 0 || !isNaN(layer.pointSize) ? layer.pointSize : 4;
layer.overdraw = layer.overdraw || 2;
layer.sseThreshold = 1;
layer.type = 'geometry';

// default update methods
Expand Down
1 change: 0 additions & 1 deletion src/Renderer/PointsMaterial.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ class PointsMaterial extends RawShaderMaterial {
this.uniforms.size = new Uniform(size);
this.uniforms.resolution = new Uniform(new Vector2(window.innerWidth, window.innerHeight));
this.uniforms.pickingMode = new Uniform(false);
this.uniforms.density = new Uniform(0.01);
this.uniforms.opacity = new Uniform(1.0);
this.uniforms.useCustomColor = new Uniform(false);
this.uniforms.customColor = new Uniform(new Color());
Expand Down
1 change: 0 additions & 1 deletion src/Renderer/Shader/PointsVS.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
uniform vec2 resolution;
uniform bool pickingMode;
uniform float density; // points per on screen pixels

attribute vec4 unique_id;
attribute vec3 color;
Expand Down
9 changes: 5 additions & 4 deletions test/pointcloudprocessing_unit_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import PointCloudProcessing from '../src/Process/PointCloudProcessing';
/* global describe, it */

const assert = require('assert');
const context = { camera: { height: 1, camera3D: { fov: 1 } } };

describe('preUpdate', function () {
it('should return root if no change source', () => {
const layer = { root: {} };
const sources = new Set();
assert.equal(
layer.root,
PointCloudProcessing.preUpdate(null, layer, sources)[0]);
PointCloudProcessing.preUpdate(context, layer, sources)[0]);
});

it('should return root if no common ancestors', () => {
Expand All @@ -21,7 +22,7 @@ describe('preUpdate', function () {
sources.add(elt2);
assert.equal(
layer.root,
PointCloudProcessing.preUpdate(null, layer, sources)[0]);
PointCloudProcessing.preUpdate(context, layer, sources)[0]);
});

it('should return common ancestor', () => {
Expand All @@ -36,7 +37,7 @@ describe('preUpdate', function () {
layer.root.findChildrenByName = (name) => {
assert.equal('12', name);
};
PointCloudProcessing.preUpdate(null, layer, sources);
PointCloudProcessing.preUpdate(context, layer, sources);
});

it('should not search ancestors if layer are different root if no common ancestors', () => {
Expand All @@ -49,6 +50,6 @@ describe('preUpdate', function () {
layer.root.findChildrenByName = (name) => {
assert.equal('12', name);
};
PointCloudProcessing.preUpdate(null, layer, sources);
PointCloudProcessing.preUpdate(context, layer, sources);
});
});
Loading

0 comments on commit 279a857

Please sign in to comment.