Skip to content

Commit 5a70f87

Browse files
committed
chore(pointcloud): Add adaptive size mode
1 parent e83d835 commit 5a70f87

4 files changed

Lines changed: 229 additions & 5 deletions

File tree

packages/Debug/src/PointCloudDebug.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ export default {
276276
styleUI.add(layer, 'opacity', 0, 1).name('Layer opacity').onChange(update);
277277
styleUI.add(layer, 'pointSize', 0, 15).name('Point size').onChange(update);
278278
if (layer.material.sizeMode != undefined && view.camera.camera3D.isPerspectiveCamera) {
279-
styleUI.add(layer.material, 'sizeAttenuation').name('Size attenuation')
279+
styleUI.add(layer.material, 'sizeMode', PNTS_SIZE_MODE).name('Size mode')
280280
.onChange(update);
281281
styleUI.add(layer.material, 'minAttenuatedSize', 0, 15).name('Min size')
282282
.onChange((value) => {

packages/Main/src/Layer/PointCloudLayer.ts

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as THREE from 'three';
22
import GeometryLayer from 'Layer/GeometryLayer';
3-
import PointsMaterial, { PNTS_MODE } from 'Renderer/PointsMaterial';
3+
import PointsMaterial, { PNTS_MODE, PNTS_SIZE_MODE } from 'Renderer/PointsMaterial';
44
import Picking from 'Core/Picking';
55

66
import type PointCloudNode from 'Core/PointCloudNode';
@@ -506,6 +506,52 @@ abstract class PointCloudLayer<S extends PointCloudSource = PointCloudSource>
506506
this.dispatchEvent({ type: 'dispose-model', scene: obj, tile: obj.userData.node });
507507
}
508508
}
509+
510+
// @ts-expect-error PointsMaterial is not typed yet
511+
if (this.material.sizeMode === PNTS_SIZE_MODE.ADAPTIVE) {
512+
const nodes = this.getNodes(this.group.children);
513+
const visibilityTextureData = this.computeVisibilityTextureData(nodes);
514+
515+
// @ts-expect-error PointsMaterial is not typed yet
516+
const vnt = this.material.visibleNodes;
517+
const data = vnt.image.data;
518+
data.set(visibilityTextureData.data);
519+
vnt.needsUpdate = true;
520+
521+
const rootSize = this.root!.voxelOBB.box3D.getSize(new THREE.Vector3());
522+
const octreeSize = Math.max(rootSize.x, rootSize.y, rootSize.z);
523+
524+
for (const pts of this.group.children) {
525+
const node = pts.userData.node;
526+
const depth = node.depth;
527+
const nodeStartOffset = visibilityTextureData.offsets.get(node.voxelKey);
528+
const octreeSpacing = node.source.spacing;
529+
530+
// Compute the bounding box min of the node in the local
531+
// space of the THREE.Points object, for getLOD() to work
532+
// correctly. The geometry.boundingBox is already in local
533+
// space (positions relative to node.origin with rotation).
534+
const geomBBox = (pts as THREE.Points).geometry.boundingBox;
535+
const bboxMin = geomBBox
536+
? geomBBox.min.clone()
537+
: new THREE.Vector3();
538+
539+
pts.onBeforeRender = (_renderer, _scene, _camera, _geometry, material) => {
540+
// @ts-expect-error Material is not typed yet
541+
material.uniforms.nodeStartOffset.value = nodeStartOffset;
542+
// @ts-expect-error Material is not typed yet
543+
material.uniforms.octreeSize.value = octreeSize;
544+
// @ts-expect-error Material is not typed yet
545+
material.uniforms.octreeSpacing.value = octreeSpacing;
546+
// @ts-expect-error Material is not typed yet
547+
material.uniforms.nodeDepth.value = depth;
548+
// @ts-expect-error Material is not typed yet
549+
material.uniforms.nodeBBoxMin.value.copy(bboxMin);
550+
// @ts-expect-error Material is not typed yet
551+
material.uniformsNeedUpdate = true;
552+
};
553+
}
554+
}
509555
}
510556

511557
// @ts-expect-error Layer and Picking are not typed yet
@@ -529,6 +575,99 @@ abstract class PointCloudLayer<S extends PointCloudSource = PointCloudSource>
529575
}
530576
}
531577
}
578+
579+
getNodes(children: THREE.Object3D[]) {
580+
const nodes = new Set<PointCloudNode>();
581+
// Sometimes child node is loaded before parent node, so we need to
582+
// make sure to add all the parent nodes of the visible nodes
583+
const collectNodeParentRecursively = (node: PointCloudNode): void => {
584+
if (nodes.has(node)) {
585+
return;
586+
}
587+
nodes.add(node);
588+
if (node.parent) {
589+
collectNodeParentRecursively(node.parent);
590+
}
591+
};
592+
children.forEach(child => collectNodeParentRecursively(child.userData.node));
593+
return Array.from(nodes);
594+
}
595+
596+
// Encoding the octree hierarchy in breadth-first order
597+
// into a texture for adaptive point size rendering
598+
// Explanation p36: https://www.cg.tuwien.ac.at/research/publications/2016/SCHUETZ-2016-POT/SCHUETZ-2016-POT-thesis.pdf
599+
computeVisibilityTextureData(nodes: PointCloudNode[]) {
600+
// sort by level and hierarchy order
601+
const sort = function sortNodes(a: PointCloudNode, b: PointCloudNode) {
602+
if (a.depth !== b.depth) { return a.depth - b.depth; }
603+
// @ts-expect-error PointCloudNode has x properties
604+
if (a.x !== b.x) { return a.x - b.x; }
605+
// @ts-expect-error PointCloudNode has y properties
606+
if (a.y !== b.y) { return a.y - b.y; }
607+
// @ts-expect-error PointCloudNode has z properties
608+
return a.z - b.z;
609+
};
610+
// breadth-first order
611+
const orderedNodes = nodes.toSorted(sort);
612+
613+
const data = new Uint8Array(orderedNodes.length * 4);
614+
const visibleNodeTextureOffsets = new Map<string, number>();
615+
const offsetsToChild: number[] = new Array(orderedNodes.length).fill(Infinity);
616+
617+
// Helper function to get octree child index from node
618+
const getChildIndex = (node: PointCloudNode): number => {
619+
if (!node.parent) {
620+
return 0;
621+
}
622+
const parent = node.parent;
623+
// @ts-expect-error PointCloudNode has x properties
624+
const dx = node.x - parent.x * 2;
625+
// @ts-expect-error PointCloudNode has y properties
626+
const dy = node.y - parent.y * 2;
627+
// @ts-expect-error PointCloudNode has z properties
628+
const dz = node.z - parent.z * 2;
629+
// Octree child index (Potree convention): 4*x + 2*y + z
630+
return 4 * dx + 2 * dy + dz;
631+
};
632+
633+
for (let nodeIndex = 0; nodeIndex < orderedNodes.length; nodeIndex++) {
634+
const node = orderedNodes[nodeIndex];
635+
// @ts-expect-error PointCloudNode has voxelKey properties
636+
visibleNodeTextureOffsets.set(node.voxelKey, nodeIndex);
637+
638+
if (node.parent) {
639+
const childIndex = getChildIndex(node);
640+
// @ts-expect-error PointCloudNode has voxelKey properties
641+
const parentIndex = visibleNodeTextureOffsets.get(node.parent.voxelKey);
642+
643+
if (parentIndex === undefined) {
644+
continue;
645+
}
646+
647+
const parentOffsetToChild = nodeIndex - parentIndex;
648+
const offsetToFirstChild =
649+
Math.min(offsetsToChild[parentIndex], parentOffsetToChild);
650+
offsetsToChild[parentIndex] = offsetToFirstChild;
651+
652+
// The 8 bits of the red value indicate
653+
// which of the children are visible
654+
data[parentIndex * 4] = data[parentIndex * 4] | (1 << childIndex);
655+
// Offset to child is stored on 2 bytes,
656+
// so it can support up to 65536 nodes per subtree.
657+
// The green channel contains the relative offset
658+
// to the node’s first child (8 most significant bits)
659+
data[parentIndex * 4 + 1] = offsetToFirstChild >> 8;
660+
// The blue channel contains the relative offset
661+
// to the node’s first child (8 least significant bits)
662+
data[parentIndex * 4 + 2] = offsetToFirstChild % 256;
663+
}
664+
}
665+
666+
return {
667+
data,
668+
offsets: visibleNodeTextureOffsets,
669+
};
670+
}
532671
}
533672

534673
export default PointCloudLayer;

packages/Main/src/Renderer/PointsMaterial.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const PNTS_SHAPE = {
2525
export const PNTS_SIZE_MODE = {
2626
VALUE: 0,
2727
ATTENUATED: 1,
28+
ADAPTIVE: 2,
2829
};
2930

3031
const white = new THREE.Color(1.0, 1.0, 1.0);
@@ -245,6 +246,13 @@ class PointsMaterial extends THREE.ShaderMaterial {
245246
CommonMaterial.setUniformProperty(this, 'gamma', gamma);
246247
CommonMaterial.setUniformProperty(this, 'ambientBoost', ambientBoost);
247248

249+
// Adaptive point size uniforms
250+
CommonMaterial.setUniformProperty(this, 'octreeSpacing', 1.0);
251+
CommonMaterial.setUniformProperty(this, 'octreeSize', 1.0);
252+
CommonMaterial.setUniformProperty(this, 'nodeDepth', 0.0);
253+
CommonMaterial.setUniformProperty(this, 'nodeStartOffset', 0.0);
254+
CommonMaterial.setUniformProperty(this, 'nodeBBoxMin', new THREE.Vector3());
255+
248256
// add classification texture to apply classification lut.
249257
const data = new Uint8Array(256 * 4);
250258
const texture = new THREE.DataTexture(data, 256, 1, THREE.RGBAFormat);
@@ -267,6 +275,12 @@ class PointsMaterial extends THREE.ShaderMaterial {
267275
textureVisi.magFilter = THREE.NearestFilter;
268276
CommonMaterial.setUniformProperty(this, 'visibilityTexture', textureVisi);
269277

278+
const dataNodes = new Uint8Array(2048 * 4);
279+
const visibleNodesTexture = new THREE.DataTexture(dataNodes, 2048, 1, THREE.RGBAFormat);
280+
visibleNodesTexture.needsUpdate = true;
281+
visibleNodesTexture.magFilter = THREE.NearestFilter;
282+
CommonMaterial.setUniformProperty(this, 'visibleNodes', visibleNodesTexture);
283+
270284
// Classification and other discrete values scheme
271285
this.classificationScheme = classificationScheme;
272286
this.discreteScheme = discreteScheme;

packages/Main/src/Renderer/Shader/PointsVS.glsl

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ uniform int sizeMode;
3131
uniform float minAttenuatedSize;
3232
uniform float maxAttenuatedSize;
3333

34+
// Adaptive point size uniforms
35+
uniform sampler2D visibleNodes;
36+
uniform float octreeSpacing;
37+
uniform float octreeSize;
38+
uniform float nodeDepth;
39+
uniform float nodeStartOffset;
40+
uniform vec3 nodeBBoxMin;
41+
3442
attribute vec4 unique_id;
3543
attribute float intensity;
3644
attribute float classification;
@@ -40,6 +48,61 @@ attribute float returnNumber;
4048
attribute float numberOfReturns;
4149
attribute float scanAngle;
4250

51+
// Adaptive point size calculation functions (from Potree)
52+
/**
53+
* number of 1-bits up to inclusive index position
54+
* number is treated as if it were an integer in the range 0-255
55+
*
56+
*/
57+
int numberOfOnes(int number, int index) {
58+
int numOnes = 0;
59+
int tmp = 128;
60+
for (int i = 7; i >= 0; i--) {
61+
if (number >= tmp) {
62+
number = number - tmp;
63+
if (i <= index) {
64+
numOnes++;
65+
}
66+
}
67+
tmp = tmp / 2;
68+
}
69+
return numOnes;
70+
}
71+
72+
float getLOD() {
73+
// Transform position from local space (relative to node origin)
74+
// to octree space (relative to node bbox min)
75+
vec3 pos = position - nodeBBoxMin;
76+
vec3 offset = vec3(0.0, 0.0, 0.0);
77+
int iOffset = int(nodeStartOffset);
78+
float depth = nodeDepth;
79+
for (float i = 0.0; i <= 30.0; i++) {
80+
float nodeSizeAtLevel = octreeSize / pow(2.0, i + nodeDepth);
81+
82+
vec3 index3d = (pos - offset) / nodeSizeAtLevel;
83+
index3d = clamp(floor(index3d + 0.5), 0.0, 1.0);
84+
int index = int(round(4.0 * index3d.x + 2.0 * index3d.y + index3d.z));
85+
86+
vec4 value = texture2D(visibleNodes, vec2((float(iOffset) + 0.5) / 2048.0, 0.5));
87+
int mask = int(round(value.r * 255.0));
88+
bool childNodeExist = bool(((mask >> index) & 1) != 0);
89+
90+
if (childNodeExist) {
91+
int greenChannelOffset = int(round(value.g * 255.0)) * 256;
92+
int blueChannelOffset = int(round(value.b * 255.0));
93+
int childIndexOffset = numberOfOnes(mask, index - 1);
94+
int totalOffset = greenChannelOffset + blueChannelOffset + childIndexOffset;
95+
iOffset = iOffset + totalOffset;
96+
depth++;
97+
} else {
98+
// no more visible child nodes at this position
99+
return depth;
100+
}
101+
offset = offset + (vec3(1.0, 1.0, 1.0) * nodeSizeAtLevel * 0.5) * index3d;
102+
}
103+
return depth;
104+
}
105+
43106
void main() {
44107
vec2 uv = vec2(classification/255., 0.5);
45108

@@ -117,13 +180,21 @@ void main() {
117180

118181
gl_PointSize = size;
119182

120-
if (sizeMode == PNTS_SIZE_MODE_ATTENUATED) {
121-
bool isPerspective = isPerspectiveMatrix(projectionMatrix);
183+
bool isPerspective = isPerspectiveMatrix(projectionMatrix);
184+
float depthAttenuationFactor = scale / -mvPosition.z;
122185

186+
if (sizeMode == PNTS_SIZE_MODE_ATTENUATED) {
123187
if (isPerspective) {
124-
gl_PointSize *= scale / -mvPosition.z;
188+
gl_PointSize *= depthAttenuationFactor;
125189
gl_PointSize = clamp(gl_PointSize, minAttenuatedSize, maxAttenuatedSize);
126190
}
191+
} else if (sizeMode == PNTS_SIZE_MODE_ADAPTIVE) {
192+
if (isPerspective) {
193+
float r = octreeSpacing * 1.7;
194+
float pointSizeAttenuation = pow(2.0, getLOD());
195+
float worldSpaceSize = size * r / pointSizeAttenuation;
196+
gl_PointSize = worldSpaceSize * depthAttenuationFactor;
197+
}
127198
}
128199

129200
#include <logdepthbuf_vertex>

0 commit comments

Comments
 (0)