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
2 changes: 2 additions & 0 deletions examples/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
},

"Pointcloud": {
"pointcloud_loader": "Pointcloud loader",
"pointcloud_loader_globe": "Pointcloud loader (globe)",
"potree_25d_map": "Potree 2.5D map",
"potree2_25d_map": "Potree 2.5D map 2.0 format",
"potree_3d_map": "Potree 3D map",
Expand Down
80 changes: 80 additions & 0 deletions examples/css/pointcloud_loader.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
:root {
--itowns-color-bg: #000000;
--itowns-color-bg-field: #232323;
--itowns-color-primary: #ffffff;
--itowns-color-muted: #bbbbbb;
--itowns-color-accent: #0077BB;
--itowns-color-red: #cc3311;

--itowns-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
--itowns-text-sm: 11px;

--itowns-spacing-xs: 8px;
--itowns-spacing-md: 16px;

--itowns-radius: 6px;
}

#load-form {
position: absolute;
z-index: 2;
display: grid;
grid-template-columns: auto 1fr;
left: var(--itowns-spacing-md);
bottom: var(--itowns-spacing-md);
gap: var(--itowns-spacing-xs);
padding: var(--itowns-spacing-xs);
width: 100%;
max-width: 245px;
background: var(--itowns-color-bg);
color: var(--itowns-color-primary);
border-radius: var(--itowns-radius);
font-family: var(--itowns-font);
font-size: var(--itowns-text-sm);
}

#load-form .form-text {
grid-column: 1 / -1;
margin: 0;
}

#load-form label {
align-self: center;
}

#load-form input[type="text"],
#load-form input[list],
#load-form select {
background: var(--itowns-color-bg-field);
padding: var(--itowns-spacing-xs);
border: 0px;
border-radius: var(--itowns-radius);
color: var(--itowns-color-primary);
font: inherit;
}

#load-form input::placeholder {
color: var(--itowns-color-muted);
}

#load-form button {
grid-column: 1 / -1;
padding: var(--itowns-spacing-xs);
background: var(--itowns-color-accent);
border: 0;
border-radius: var(--itowns-radius);
color: var(--itowns-color-primary);
font: inherit;
font-weight: bold;
cursor: pointer;
}

#error-message {
display: none;
grid-column: 1 / -1;
color: var(--itowns-color-red);
}

#load-form.error #error-message {
display: block;
}
83 changes: 83 additions & 0 deletions examples/jsm/PointCloudHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Vector3 } from 'three';
import {
Coordinates,
CopcSource, CopcLayer,
EntwinePointTileSource, EntwinePointTileLayer,
PotreeSource, PotreeLayer,
Potree2Source, Potree2Layer,
} from 'itowns';

class Potree extends PotreeSource {
constructor(source) {
const url = new URL(source.url);
const file = url.pathname.split('/').pop();
super({ ...source, url: new URL('.', url.href).href, file });
}
}

class Potree2 extends Potree2Source {
constructor(source) {
const url = new URL(source.url);
const file = url.pathname.split('/').pop();
super({ ...source, url: new URL('.', url.href).href, file });
}
}

export function getFormat(url, format) {
if (format !== 'auto') { return format; }
if (url.includes('.copc.laz')) { return 'copc'; }
if (url.includes('ept.json')) { return 'ept'; }
if (url.includes('cloud.js')) { return 'potree'; }
if (url.includes('metadata.json')) { return 'potree2'; }
throw new Error(`Cannot infer format from URL ${url}`);
}

export function getPointCloudClass(format) {
if (format === 'copc') {
return { Source: CopcSource, Layer: CopcLayer };
}
if (format === 'ept') {
return { Source: EntwinePointTileSource, Layer: EntwinePointTileLayer };
}
if (format === 'potree') {
return { Source: Potree, Layer: PotreeLayer };
}
if (format === 'potree2') {
return { Source: Potree2, Layer: Potree2Layer };
}

throw new Error(`Unsupported format ${format}`);
}

export function zoomToLayer(view, layer) {
const camera = view.camera3D;
const obb = layer.root.voxelOBB;

const center = obb.box3D.getCenter(new Vector3());
obb.localToWorld(center);
const length = obb.box3D.getSize(new Vector3()).length();

const fov = camera.fov * (Math.PI / 180);
const radius = length / 2;
const distance = radius / Math.tan(fov / 2);

const up = new Vector3(0, 0, 1);
camera.position.copy(center).addScaledVector(up, distance);
camera.far = 2 * distance;
camera.lookAt(center);
camera.updateProjectionMatrix();
view.notifyChange(camera);
}

export function zoomToLayerGlobe(view, layer) {
const lookAt = layer.root.clampOBB.position;
const coordLookAt = new Coordinates(view.referenceCrs).setFromVector3(lookAt);

const size = new Vector3();
layer.root.voxelOBB.box3D.getSize(size);

view.controls.lookAtCoordinate({
coord: coordLookAt,
range: 2 * size.length(),
}, false);
}
170 changes: 170 additions & 0 deletions examples/jsm/postprocessing/EDLPass.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import {
NoBlending,
ShaderMaterial,
UniformsUtils,
AlwaysDepth,
} from 'three';
// eslint-disable-next-line
import { Pass, FullScreenQuad } from 'three/addons/postprocessing/Pass.js';
// eslint-disable-next-line import/extensions
import { EDLShader } from '../shaders/EDLShader.js';

/**
* Generates a kernel of evenly distributed 2D sample directions around a circle.
* Used for sampling neighbor depths in the EDL algorithm.
*
* @param {number} kernelSize - Number of sample directions
* @returns {Float32Array} Flat array of (x, y) pairs
*/
function generateKernel(kernelSize) {
const kernel = new Float32Array(kernelSize * 2);

for (let i = 0; i < kernelSize; ++i) {
const angle = (2 * Math.PI * i) / kernelSize;
kernel[(i * 2) + 0] = Math.cos(angle);
kernel[(i * 2) + 1] = Math.sin(angle);
}

return kernel;
}

// Algorithm by Christian Boucheny. See:
// - Phd thesis (page 115-127, french):
// https://tel.archives-ouvertes.fr/tel-00438464/document
// - Implementation in Cloud Compare (last update 2022):
// https://github.com/CloudCompare/CloudCompare/tree/master/plugins/core/GL/qEDL/shaders/EDL
// Parameters by Markus Schuetz (Potree). See:
// - Master thesis (pages 38-41):
// https://www.cg.tuwien.ac.at/research/publications/2016/SCHUETZ-2016-POT/SCHUETZ-2016-POT-thesis.pdf
// - Implementation in Potree (last update 2019):
// https://github.com/potree/potree/blob/develop/src/materials/shaders/edl.fs

class EDLPass extends Pass {
/**
* @param {PerspectiveCamera | OrthographicCamera} camera
* @param {number} width
* @param {number} height
* @param {number} kernelSize
*/
constructor(camera, width = 256, height = 256, kernelSize = 8) {
super();

/**
* The width of the render target.
*
* @type {number}
* @default 256
*/
this.width = width;

/**
* The height of the render target.
*
* @type {number}
* @default 256
*/
this.height = height;

/**
* Overwritten to true to ensure the render target is cleared.
*
* @type {boolean}
* @default true
*/
this.clear = true;

/** @type {THREE.PerspectiveCamera | THREE.OrthographicCamera} */
this.camera = camera;

/** @type {Float32Array} */
this._kernel = generateKernel(kernelSize);

// edl material
// depthWrite: true enables gl_FragDepth output for depth compositing
// depthTest: true with AlwaysDepth ensures all fragments are processed
// while still allowing depth buffer writes (some drivers ignore depthWrite when depthTest=false)
this.edlMaterial = new ShaderMaterial({
defines: { ...EDLShader.defines },
uniforms: UniformsUtils.clone(EDLShader.uniforms),
vertexShader: EDLShader.vertexShader,
fragmentShader: EDLShader.fragmentShader,
blending: NoBlending,
depthWrite: true,
depthTest: true,
depthFunc: AlwaysDepth,
});

this.edlMaterial.defines.KERNEL_SIZE = kernelSize;
// Set camera type define: 1 for perspective, 0 for orthographic
this.edlMaterial.defines.PERSPECTIVE_CAMERA = camera.isPerspectiveCamera ? 1 : 0;

const uniforms = this.edlMaterial.uniforms;
uniforms.kernel.value = this._kernel;
uniforms.resolution.value.set(this.width, this.height);
uniforms.cameraNear.value = this.camera.near;
uniforms.cameraFar.value = this.camera.far;

this._fsQuad = new FullScreenQuad(this.edlMaterial);
}

/**
* @param {number} width
* @param {number} height
*/
setSize(width, height) {
this.width = width;
this.height = height;

this.edlMaterial.uniforms.resolution.value.set(width, height);
}

/**
* EDL strength controls the intensity of the edge darkening effect.
* Higher values produce more pronounced edges.
* @type {number}
* @default 6000.0
*/
get strength() {
return this.edlMaterial.uniforms.edlStrength.value;
}

set strength(value) {
this.edlMaterial.uniforms.edlStrength.value = value;
}

/**
* Kernel radius in pixels for neighbor sampling.
* Larger values sample further neighbors, creating thicker edges.
* @type {number}
* @default 0.7
*/
get kernelRadius() {
return this.edlMaterial.uniforms.kernelRadius.value;
}

set kernelRadius(value) {
this.edlMaterial.uniforms.kernelRadius.value = value;
}

/**
* @param {WebGLRenderer} renderer
* @param {WebGLRenderTarget} writeBuffer
* @param {WebGLRenderTarget} readBuffer
*/
render(renderer, writeBuffer, readBuffer) {
this.edlMaterial.uniforms.cameraNear.value = this.camera.near;
this.edlMaterial.uniforms.cameraFar.value = this.camera.far;

this.edlMaterial.uniforms.tDepth.value = readBuffer.depthTexture;
this.edlMaterial.uniforms.tDiffuse.value = readBuffer.texture;

renderer.setRenderTarget(this.renderToScreen ? null : writeBuffer);
this._fsQuad.render(renderer);
}

dispose() {
this._fsQuad.dispose();
}
}

export { EDLPass };
Loading
Loading