Skip to content
Open
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
8 changes: 5 additions & 3 deletions packages/Debug/src/PointCloudDebug.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ function setupControllerVisibily(gui, displayMode, sizeMode) {
}

sizeMode = parseInt(sizeMode, 10);
if (sizeMode === PNTS_SIZE_MODE.VALUE) {
if ([PNTS_SIZE_MODE.VALUE, PNTS_SIZE_MODE.ADAPTIVE].includes(sizeMode)) {
getController(gui, 'pointSize').show();
getController(gui, 'minAttenuatedSize').hide();
getController(gui, 'maxAttenuatedSize').hide();
} else {
getController(gui, 'pointSize').hide();
getController(gui, 'minAttenuatedSize').show();
getController(gui, 'maxAttenuatedSize').show();
}
Expand Down Expand Up @@ -297,9 +299,8 @@ export default {
}

styleUI.add(layer, 'opacity', 0, 1).name('Layer opacity').onChange(update);
styleUI.add(layer, 'pointSize', 0, 15).name('Point size').onChange(update);
if (layer.material.sizeMode != undefined && view.camera.camera3D.isPerspectiveCamera) {
styleUI.add(layer.material, 'sizeAttenuation').name('Size attenuation')
styleUI.add(layer.material, 'sizeMode', PNTS_SIZE_MODE).name('Size mode')
.onChange(update);
styleUI.add(layer.material, 'minAttenuatedSize', 0, 15).name('Min size')
.onChange((value) => {
Expand All @@ -318,6 +319,7 @@ export default {
update();
});
}
styleUI.add(layer, 'pointSize', 0, 15).name('Point size').onChange(update);

if (layer.material.picking != undefined) {
styleUI.add(layer.material, 'picking').name('Display picking id').onChange(update);
Expand Down
20 changes: 12 additions & 8 deletions packages/Main/src/Core/PointCloudNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,18 @@ abstract class PointCloudNode extends THREE.EventDispatcher {
const y = this.y * 2;
const z = this.z * 2;

this.findAndCreateChild(depth, x, y, z);
this.findAndCreateChild(depth, x + 1, y, z);
this.findAndCreateChild(depth, x, y + 1, z);
this.findAndCreateChild(depth, x + 1, y + 1, z);
this.findAndCreateChild(depth, x, y, z + 1);
this.findAndCreateChild(depth, x + 1, y, z + 1);
this.findAndCreateChild(depth, x, y + 1, z + 1);
this.findAndCreateChild(depth, x + 1, y + 1, z + 1);
// Order follows ascending Potree child index (4*dx + 2*dy + dz),
// i.e. 0→7, so that node.children is always in Potree index order.
// This is required by the visibility texture encoding used for
// adaptive point-size rendering in the shader.
this.findAndCreateChild(depth, x, y, z); // 0
this.findAndCreateChild(depth, x, y, z + 1); // 1
this.findAndCreateChild(depth, x, y + 1, z); // 2
this.findAndCreateChild(depth, x, y + 1, z + 1); // 3
this.findAndCreateChild(depth, x + 1, y, z); // 4
this.findAndCreateChild(depth, x + 1, y, z + 1); // 5
this.findAndCreateChild(depth, x + 1, y + 1, z); // 6
this.findAndCreateChild(depth, x + 1, y + 1, z + 1); // 7
this._childrenCreated = true;
}

Expand Down
5 changes: 4 additions & 1 deletion packages/Main/src/Layer/C3DTilesLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ class C3DTilesLayer extends GeometryLayer {
* Only 'COLOR' or 'CLASSIFICATION' are possible. COLOR uses RGB colors of the points,
* CLASSIFICATION uses a classification property of the batch table to color points.
* @param {string} [config.pntsShape= PNTS_SHAPE.CIRCLE] Point cloud point shape. Only 'CIRCLE' or 'SQUARE' are possible.
* @param {string} [config.pntsSizeMode= PNTS_SIZE_MODE.VALUE] {@link PointsMaterial} Point cloud size mode. Only 'VALUE' or 'ATTENUATED' are possible. VALUE use constant size, ATTENUATED compute size depending on distance from point to camera.
* @param {string} [config.pntsSizeMode= PNTS_SIZE_MODE.VALUE] {@link PointsMaterial} Point cloud size mode.
* Only 'VALUE' or 'ATTENUATED' or 'ADAPTIVE' are possible. VALUE use constant size, ATTENUATED compute size depending on distance
* from point to camera and ADAPTIVE computes the point size on a per-point basis rather than on a per-node basis.
* **Warning:** ADAPTIVE work only if point cloud is an octree
* @param {number} [config.pntsMinAttenuatedSize=3] Minimum scale used by 'ATTENUATED' size mode
* @param {number} [config.pntsMaxAttenuatedSize=10] Maximum scale used by 'ATTENUATED' size mode
* @param {Style} [config.style=null] - style used for this layer
Expand Down
5 changes: 3 additions & 2 deletions packages/Main/src/Layer/OGC3DTilesLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,8 +370,9 @@ class OGC3DTilesLayer extends GeometryLayer {
* @param {string} [config.pntsShape = PNTS_SHAPE.CIRCLE] Point cloud point shape. Only 'CIRCLE' or 'SQUARE' are possible.
* (passed to {@link PointsMaterial}).
* @param {string} [config.pntsSizeMode = PNTS_SIZE_MODE.VALUE] {@link PointsMaterial} Point cloud size mode (passed to {@link PointsMaterial}).
* Only 'VALUE' or 'ATTENUATED' are possible. VALUE use constant size, ATTENUATED compute size depending on distance
* from point to camera.
* Only 'VALUE' or 'ATTENUATED' or 'ADAPTIVE' are possible. VALUE use constant size, ATTENUATED compute size depending on distance
* from point to camera and ADAPTIVE computes the point size on a per-point basis rather than on a per-node basis.
* **Warning:** ADAPTIVE work only if point cloud is an octree
* @param {number} [config.pntsMinAttenuatedSize = 3] Minimum scale used by 'ATTENUATED' size mode.
* @param {number} [config.pntsMaxAttenuatedSize = 10] Maximum scale used by 'ATTENUATED' size mode.
*/
Expand Down
71 changes: 70 additions & 1 deletion packages/Main/src/Layer/PointCloudLayer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as THREE from 'three';
import TinyQueue from 'tinyqueue';
import GeometryLayer from 'Layer/GeometryLayer';
import PointsMaterial, { PNTS_MODE } from 'Renderer/PointsMaterial';
import PointsMaterial, { PNTS_MODE, PNTS_SIZE_MODE } from 'Renderer/PointsMaterial';
import Picking from 'Core/Picking';
import { computeVisibilityTextureData } from 'Utils/PointCloudUtils';

import type PointCloudNode from 'Core/PointCloudNode';

Expand Down Expand Up @@ -228,6 +229,8 @@ abstract class PointCloudLayer<S extends PointCloudSource = PointCloudSource>

private _candidateNodes: TinyQueue<PointCloudNode>;
private _visibleNodes = new Set<PointCloudNode>();
private _visibilityTextureNeedsUpdate = true;
private _visibilityTextureData: { data: Uint8Array; nodeToIndex: Map<PointCloudNode, number> } | undefined;

/**
* Constructs a new instance of point cloud layer.
Expand Down Expand Up @@ -333,6 +336,7 @@ abstract class PointCloudLayer<S extends PointCloudSource = PointCloudSource>
node.notVisibleSince = Date.now();
node.sse = -1;
}
this._visibilityTextureNeedsUpdate = true;
}

preUpdate(context: Context) {
Expand Down Expand Up @@ -535,6 +539,71 @@ abstract class PointCloudLayer<S extends PointCloudSource = PointCloudSource>
}
}

// @ts-expect-error PointsMaterial is not typed yet
if (this.root && (this.material.sizeMode === PNTS_SIZE_MODE.ADAPTIVE)) {
if (this._visibilityTextureNeedsUpdate) {
this._visibilityTextureData =
computeVisibilityTextureData(this.root, Array.from(this._visibleNodes));
this._visibilityTextureNeedsUpdate = false;
}

if (this._visibilityTextureData === undefined) {
return;
}

// @ts-expect-error PointsMaterial is not typed yet
const vnt = this.material.visibleNodes;
const data = vnt.image.data;
data.fill(0);
data.set(this._visibilityTextureData.data);
Comment on lines +556 to +558

@Desplandis Desplandis Jun 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const data = vnt.image.data;
data.set(this._visibilityTextureData.data);
const data = vnt.image.data;
data.fill(0);
data.set(this._visibilityTextureData.data);

I think there are still some stale entries from the previous updates, I recommend you to zero out all the buffer. It is weird however that we access those part of the buffer but it shall fix the issue for now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, good point.

vnt.needsUpdate = true;

// Use box3D (projected in the local geometry coordinate frame) for
// octreeSize and bboxMin, because the vertex shader positions are
// expressed in that same local frame after CRS reprojection and
// quaternion rotation applied by the parsers.
// Using natBox (native source CRS) would mix coordinate systems
// (e.g. Lambert93 metres vs. ECEF metres) and produce wrong LODs
// for point clouds whose source CRS differs from the scene CRS.
const rootSize = this.root.voxelOBB.box3D.getSize(new THREE.Vector3());

for (const pts of this.group.children) {
const node = pts.userData.node;
const depth = node.depth;
const nodeStartOffset = this._visibilityTextureData.nodeToIndex.get(node);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beware that nodeStartOffset may be undefined! This is can happen because the rendered group this.group.children do not guarantee that all points are visible. They are in the rendered group (were visible once) but remain hidden until disposal (10s later).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I continue to next iteration if nodeStartOffset is undefined

const octreeSpacing = node.source.spacing;

if (nodeStartOffset === undefined) {
continue;
}

// Compute the bounding box min of the node's octree cell in
// the local coordinate frame used by the THREE.Points geometry.
// The parsers store each point as:
// quat * (forward(nativePos) - centerZ0)
// where centerZ0 = voxelOBB.position and quat is computed at
// that same position. projOBB() builds box3D with the exact
// same origin and the same quaternion, so box3D.min is already
// expressed in the geometry's local frame — no further
// transformation is needed.
const bboxMin = node.voxelOBB.box3D.min.clone();

pts.onBeforeRender = (_renderer, _scene, _camera, _geometry, material) => {
// @ts-expect-error Material is not typed yet
material.uniforms.nodeStartOffset.value = nodeStartOffset;
// @ts-expect-error Material is not typed yet
material.uniforms.octreeSize.value.copy(rootSize);
// @ts-expect-error Material is not typed yet
material.uniforms.octreeSpacing.value = octreeSpacing;
// @ts-expect-error Material is not typed yet
material.uniforms.nodeDepth.value = depth;
// @ts-expect-error Material is not typed yet
material.uniforms.nodeBBoxMin.value.copy(bboxMin);
// @ts-expect-error Material is not typed yet
material.uniformsNeedUpdate = true;
};
}
}
this.dispatchEvent({ type: 'post-update' });
}

Expand Down
1 change: 1 addition & 0 deletions packages/Main/src/Main.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export { default as Feature2Mesh } from 'Converter/Feature2Mesh';
export { default as FeaturesUtils } from 'Utils/FeaturesUtils';
export { default as DEMUtils } from 'Utils/DEMUtils';
export { default as CameraUtils } from 'Utils/CameraUtils';
export { computeVisibilityTextureData } from 'Utils/PointCloudUtils';
export { default as ShaderChunk } from 'Renderer/Shader/ShaderChunk';
export { getMaxColorSamplerUnitsCount, colorLayerEffects } from 'Renderer/LayeredMaterial';
export { default as Capabilities } from 'Core/System/Capabilities';
Expand Down
18 changes: 17 additions & 1 deletion packages/Main/src/Renderer/PointsMaterial.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const PNTS_SHAPE = {
export const PNTS_SIZE_MODE = {
VALUE: 0,
ATTENUATED: 1,
ADAPTIVE: 2,
};

const white = new THREE.Color(1.0, 1.0, 1.0);
Expand Down Expand Up @@ -161,7 +162,9 @@ class PointsMaterial extends THREE.ShaderMaterial {
* @param {Scheme} [options.discreteScheme] LUT for other discret point values colorization.
* @param {string} [options.gradient] Descrition of the gradient to use for continuous point values.
* (Default value will be the 'SPECTRAL' gradient from Utils/Gradients)
* @param {number} [options.sizeMode=PNTS_SIZE_MODE.VALUE] point cloud size mode. Only 'VALUE' or 'ATTENUATED' are possible. VALUE use constant size, ATTENUATED compute size depending on distance from point to camera.
* @param {number} [options.sizeMode=PNTS_SIZE_MODE.VALUE] point cloud size mode. Only 'VALUE' or 'ATTENUATED' or 'ADAPTIVE' are possible.
* VALUE use constant size, ATTENUATED compute size depending on distance from point to camera and ADAPTIVE computes the point size on a per-point basis rather than on a per-node basis.
* **Warning:** ADAPTIVE work only if point cloud is an octree
* @param {number} [options.minAttenuatedSize=3] minimum scale used by 'ATTENUATED' size mode
* @param {number} [options.maxAttenuatedSize=10] maximum scale used by 'ATTENUATED' size mode
*
Expand Down Expand Up @@ -245,6 +248,13 @@ class PointsMaterial extends THREE.ShaderMaterial {
CommonMaterial.setUniformProperty(this, 'gamma', gamma);
CommonMaterial.setUniformProperty(this, 'ambientBoost', ambientBoost);

// Adaptive point size uniforms
CommonMaterial.setUniformProperty(this, 'octreeSpacing', 1.0);
CommonMaterial.setUniformProperty(this, 'octreeSize', new THREE.Vector3());
CommonMaterial.setUniformProperty(this, 'nodeDepth', 0.0);
CommonMaterial.setUniformProperty(this, 'nodeStartOffset', 0.0);
CommonMaterial.setUniformProperty(this, 'nodeBBoxMin', new THREE.Vector3());

// add classification texture to apply classification lut.
const data = new Uint8Array(256 * 4);
const texture = new THREE.DataTexture(data, 256, 1, THREE.RGBAFormat);
Expand All @@ -267,6 +277,12 @@ class PointsMaterial extends THREE.ShaderMaterial {
textureVisi.magFilter = THREE.NearestFilter;
CommonMaterial.setUniformProperty(this, 'visibilityTexture', textureVisi);

const dataNodes = new Uint8Array(2048 * 4);
const visibleNodesTexture = new THREE.DataTexture(dataNodes, 2048, 1, THREE.RGBAFormat);
visibleNodesTexture.needsUpdate = true;
visibleNodesTexture.magFilter = THREE.NearestFilter;
CommonMaterial.setUniformProperty(this, 'visibleNodes', visibleNodesTexture);

// Classification and other discrete values scheme
this.classificationScheme = classificationScheme;
this.discreteScheme = discreteScheme;
Expand Down
77 changes: 74 additions & 3 deletions packages/Main/src/Renderer/Shader/PointsVS.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ uniform int sizeMode;
uniform float minAttenuatedSize;
uniform float maxAttenuatedSize;

// Adaptive point size uniforms
uniform sampler2D visibleNodes;
uniform float octreeSpacing;
uniform vec3 octreeSize;
uniform float nodeDepth;
uniform float nodeStartOffset;
uniform vec3 nodeBBoxMin;

attribute vec4 unique_id;
attribute float intensity;
attribute float classification;
Expand All @@ -40,6 +48,61 @@ attribute float returnNumber;
attribute float numberOfReturns;
attribute float scanAngle;

// Adaptive point size calculation functions (from Potree)
/**
* number of 1-bits up to inclusive index position
* number is treated as if it were an integer in the range 0-255
*
*/
int numberOfOnes(int number, int index) {
int numOnes = 0;
int tmp = 128;
for (int i = 7; i >= 0; i--) {
if (number >= tmp) {
number = number - tmp;
if (i <= index) {
numOnes++;
}
}
tmp = tmp / 2;
}
return numOnes;
}

float getLOD() {
// Transform position from local space (relative to node origin)
// to octree space (relative to node bbox min)
vec3 pos = position - nodeBBoxMin;
vec3 offset = vec3(0.0, 0.0, 0.0);
int iOffset = int(nodeStartOffset);
float depth = nodeDepth;
for (float i = 0.0; i <= 30.0; i++) {
vec3 nodeSizeAtLevel = octreeSize / pow(2.0, i + nodeDepth);

vec3 index3d = (pos - offset) / nodeSizeAtLevel;
index3d = clamp(floor(index3d + 0.5), 0.0, 1.0);
int index = int(round(4.0 * index3d.x + 2.0 * index3d.y + index3d.z));

vec4 value = texture2D(visibleNodes, vec2((float(iOffset) + 0.5) / 2048.0, 0.0));
int mask = int(round(value.r * 255.0));
bool childNodeExist = bool(((mask >> index) & 1) != 0);

if (childNodeExist) {
int greenChannelOffset = int(round(value.g * 255.0)) * 256;
int blueChannelOffset = int(round(value.b * 255.0));
int childIndexOffset = numberOfOnes(mask, index - 1);
int totalOffset = greenChannelOffset + blueChannelOffset + childIndexOffset;
iOffset = iOffset + totalOffset;
depth++;
} else {
// no more visible child nodes at this position
return depth;
}
offset = offset + (vec3(1.0, 1.0, 1.0) * nodeSizeAtLevel * 0.5) * index3d;
}
return depth;
}

void main() {
vec2 uv = vec2(classification/255., 0.5);

Expand Down Expand Up @@ -117,13 +180,21 @@ void main() {

gl_PointSize = size;

if (sizeMode == PNTS_SIZE_MODE_ATTENUATED) {
bool isPerspective = isPerspectiveMatrix(projectionMatrix);
bool isPerspective = isPerspectiveMatrix(projectionMatrix);
float depthAttenuationFactor = scale / -mvPosition.z;

if (sizeMode == PNTS_SIZE_MODE_ATTENUATED) {
if (isPerspective) {
gl_PointSize *= scale / -mvPosition.z;
gl_PointSize *= depthAttenuationFactor;
gl_PointSize = clamp(gl_PointSize, minAttenuatedSize, maxAttenuatedSize);
}
} else if (sizeMode == PNTS_SIZE_MODE_ADAPTIVE) {
if (isPerspective) {
float r = octreeSpacing * 1.7;
float pointSizeAttenuation = pow(2.0, getLOD());
float worldSpaceSize = size * r / pointSizeAttenuation;
gl_PointSize = worldSpaceSize * depthAttenuationFactor;
}
}

#include <logdepthbuf_vertex>
Expand Down
Loading
Loading