Skip to content

Commit 40b5fb7

Browse files
committed
chore(pointcloud): Add adaptive size mode
1 parent 1d4b404 commit 40b5fb7

File tree

4 files changed

+229
-10
lines changed

4 files changed

+229
-10
lines changed

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: 103 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,37 @@ 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 visibilityTextureData = this.computeVisibilityTextureData(this.group.children);
513+
514+
// @ts-expect-error PointsMaterial is not typed yet
515+
const vnt = this.material.visibleNodes;
516+
const data = vnt.image.data;
517+
data.set(visibilityTextureData.data);
518+
vnt.needsUpdate = true;
519+
520+
const octreeSize = this.root!.voxelOBB.box3D.getSize(new THREE.Vector3()).x;
521+
522+
for (const pts of this.group.children) {
523+
const node = pts.userData.node;
524+
const depth = node.depth;
525+
const nodeStartOffset = visibilityTextureData.offsets.get(node);
526+
const octreeSpacing = node.source.spacing;
527+
528+
pts.onBeforeRender = (_renderer, _scene, _camera, _geometry, material) => {
529+
// @ts-expect-error Material is not typed yet
530+
material.uniforms.nodeStartOffset.value = nodeStartOffset;
531+
// @ts-expect-error Material is not typed yet
532+
material.uniforms.octreeSize.value = octreeSize;
533+
// @ts-expect-error Material is not typed yet
534+
material.uniforms.octreeSpacing.value = octreeSpacing;
535+
// @ts-expect-error Material is not typed yet
536+
material.uniforms.nodeDepth.value = depth;
537+
};
538+
}
539+
}
509540
}
510541

511542
// @ts-expect-error Layer and Picking are not typed yet
@@ -529,6 +560,77 @@ abstract class PointCloudLayer<S extends PointCloudSource = PointCloudSource>
529560
}
530561
}
531562
}
563+
564+
// Encoding the octree hierarchy in bread-first order
565+
// into a texture for adaptive point size rendering
566+
// Explanation p36: https://www.cg.tuwien.ac.at/research/publications/2016/SCHUETZ-2016-POT/SCHUETZ-2016-POT-thesis.pdf
567+
computeVisibilityTextureData(children: THREE.Object3D[]) {
568+
// copy array
569+
const orderedChildren = children.slice();
570+
571+
// Helper function to get octree child index from node
572+
const getChildIndex = (node: PointCloudNode): number => {
573+
if (!node.parent) {
574+
return 0;
575+
}
576+
const parent = node.parent;
577+
// @ts-expect-error PointCloudNode has x properties
578+
const dx = node.x - parent.x * 2;
579+
// @ts-expect-error PointCloudNode has y properties
580+
const dy = node.y - parent.y * 2;
581+
// @ts-expect-error PointCloudNode has z properties
582+
const dz = node.z - parent.z * 2;
583+
// Octree child index (Potree convention): 4*x + 2*y + z
584+
return 4 * dx + 2 * dy + dz;
585+
};
586+
587+
// sort by level and hierarchy order
588+
const sort = function sortNodes(a: THREE.Object3D, b: THREE.Object3D) {
589+
const na = a.userData.node;
590+
const nb = b.userData.node;
591+
if (na.depth !== nb.depth) { return na.depth - nb.depth; }
592+
if (na.z !== nb.z) { return na.z - nb.z; }
593+
if (na.y !== nb.y) { return na.y - nb.y; }
594+
return na.x - nb.x;
595+
};
596+
orderedChildren.sort(sort);
597+
598+
const data = new Uint8Array(orderedChildren.length * 4);
599+
const visibleNodeTextureOffsets = new Map();
600+
const offsetsToChild: number[] = new Array(orderedChildren.length).fill(Infinity);
601+
602+
for (let nodeIndex = 0; nodeIndex < orderedChildren.length; nodeIndex++) {
603+
const node = orderedChildren[nodeIndex].userData.node;
604+
visibleNodeTextureOffsets.set(node, nodeIndex);
605+
606+
if (node.parent) {
607+
const childIndex = getChildIndex(node);
608+
const parentIndex = visibleNodeTextureOffsets.get(node.parent);
609+
610+
const parentOffsetToChild = nodeIndex - parentIndex;
611+
const offsetToFirstChild =
612+
Math.min(offsetsToChild[parentIndex], parentOffsetToChild);
613+
offsetsToChild[parentIndex] = offsetToFirstChild;
614+
615+
// The 8 bits of the red value indicate
616+
// which of the children are visible
617+
data[parentIndex * 4] = data[parentIndex * 4] | (1 << childIndex);
618+
// Offset to child is stored on 2 bytes,
619+
// so it can support up to 65536 nodes per subtree.
620+
// The green channel contains the relative offset
621+
// to the node’s first child (8 most significant bits)
622+
data[parentIndex * 4 + 1] = offsetToFirstChild >> 8;
623+
// The blue channel contains the relative offset
624+
// to the node’s first child (8 least significant bits)
625+
data[parentIndex * 4 + 2] = offsetToFirstChild % 256;
626+
}
627+
}
628+
629+
return {
630+
data,
631+
offsets: visibleNodeTextureOffsets,
632+
};
633+
}
532634
}
533635

534636
export default PointCloudLayer;

packages/Main/src/Renderer/PointsMaterial.js

Lines changed: 55 additions & 1 deletion
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,12 @@ 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+
248255
// add classification texture to apply classification lut.
249256
const data = new Uint8Array(256 * 4);
250257
const texture = new THREE.DataTexture(data, 256, 1, THREE.RGBAFormat);
@@ -262,11 +269,16 @@ class PointsMaterial extends THREE.ShaderMaterial {
262269
// add texture to apply visibility.
263270
const dataVisi = new Uint8Array(256 * 1);
264271
const textureVisi = new THREE.DataTexture(dataVisi, 256, 1, THREE.RedFormat);
265-
266272
textureVisi.needsUpdate = true;
267273
textureVisi.magFilter = THREE.NearestFilter;
268274
CommonMaterial.setUniformProperty(this, 'visibilityTexture', textureVisi);
269275

276+
const dataNodes = new Uint8Array(2048 * 4);
277+
const visibleNodesTexture = new THREE.DataTexture(dataNodes, 2048, 1, THREE.RGBAFormat);
278+
visibleNodesTexture.needsUpdate = true;
279+
visibleNodesTexture.magFilter = THREE.NearestFilter;
280+
CommonMaterial.setUniformProperty(this, 'visibleNodes', visibleNodesTexture);
281+
270282
// Classification and other discrete values scheme
271283
this.classificationScheme = classificationScheme;
272284
this.discreteScheme = discreteScheme;
@@ -395,6 +407,48 @@ class PointsMaterial extends THREE.ShaderMaterial {
395407
this.uniforms.ambientBoost.value = ambientBoost;
396408
}
397409

410+
// Adaptive point size properties
411+
412+
/** @returns {number} */
413+
get octreeSpacing() {
414+
return this.uniforms.octreeSpacing.value;
415+
}
416+
417+
/** @param {number} octreeSpacing */
418+
set octreeSpacing(octreeSpacing) {
419+
this.uniforms.octreeSpacing.value = octreeSpacing;
420+
}
421+
422+
/** @returns {number} */
423+
get octreeSize() {
424+
return this.uniforms.octreeSize.value;
425+
}
426+
427+
/** @param {number} octreeSize */
428+
set octreeSize(octreeSize) {
429+
this.uniforms.octreeSize.value = octreeSize;
430+
}
431+
432+
/** @returns {number} */
433+
get nodeDepth() {
434+
return this.uniforms.nodeDepth.value;
435+
}
436+
437+
/** @param {number} depth */
438+
set nodeDepth(depth) {
439+
this.uniforms.nodeDepth.value = depth;
440+
}
441+
442+
/** @returns {number} */
443+
get nodeStartOffset() {
444+
return this.uniforms.nodeStartOffset.value;
445+
}
446+
447+
/** @param {number} offset */
448+
set nodeStartOffset(offset) {
449+
this.uniforms.nodeStartOffset.value = offset;
450+
}
451+
398452
recomputeClassification() {
399453
recomputeTexture(this.classificationScheme, this.classificationTexture, 256);
400454
this.dispatchEvent({

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

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ 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+
3441
attribute vec4 unique_id;
3542
attribute float intensity;
3643
attribute float classification;
@@ -40,6 +47,58 @@ attribute float returnNumber;
4047
attribute float numberOfReturns;
4148
attribute float scanAngle;
4249

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

@@ -117,13 +176,17 @@ void main() {
117176

118177
gl_PointSize = size;
119178

120-
if (sizeMode == PNTS_SIZE_MODE_ATTENUATED) {
121-
bool isPerspective = isPerspectiveMatrix(projectionMatrix);
122-
123-
if (isPerspective) {
124-
gl_PointSize *= scale / -mvPosition.z;
125-
gl_PointSize = clamp(gl_PointSize, minAttenuatedSize, maxAttenuatedSize);
126-
}
179+
bool isPerspective = isPerspectiveMatrix(projectionMatrix);
180+
181+
if (isPerspective && (sizeMode == PNTS_SIZE_MODE_ATTENUATED)) {
182+
gl_PointSize *= scale / -mvPosition.z;
183+
gl_PointSize = clamp(gl_PointSize, minAttenuatedSize, maxAttenuatedSize);
184+
} else if (isPerspective && (sizeMode == PNTS_SIZE_MODE_ADAPTIVE)) {
185+
float r = octreeSpacing * 1.7;
186+
float pointSizeAttenuation = pow(2.0, getLOD());
187+
float worldSpaceSize = size * r / pointSizeAttenuation;
188+
float projFactor = scale / -mvPosition.z;
189+
gl_PointSize = worldSpaceSize * projFactor;
127190
}
128191

129192
#include <logdepthbuf_vertex>

0 commit comments

Comments
 (0)