Display a classified point cloud.

Giro3D version
THREE.js version
OpenLayers version
CRS
Memory usage (CPU)
Memory usage (GPU)
Frames
Clear color
Clear alpha
Status
Local clipping enabled
Capabilities
WebGL 2
Max texture units
Max texture size
Precision
Max fragment shader uniforms
Logarithmic depth buffer
Max shader attributes
Check shader errors
EXT_clip_control
EXT_color_buffer_float
EXT_color_buffer_half_float
EXT_conservative_depth
EXT_depth_clamp
EXT_float_blend
EXT_polygon_offset_clamp
EXT_texture_compression_bptc
EXT_texture_compression_rgtc
EXT_texture_filter_anisotropic
EXT_texture_mirror_clamp_to_edge
EXT_texture_norm16
NV_shader_noperspective_interpolation
OES_draw_buffers_indexed
OES_sample_variables
OES_shader_multisample_interpolation
OES_texture_float_linear
OVR_multiview2
WEBGL_clip_cull_distance
WEBGL_compressed_texture_astc
WEBGL_compressed_texture_etc
WEBGL_compressed_texture_etc1
WEBGL_compressed_texture_s3tc
WEBGL_compressed_texture_s3tc_srgb
WEBGL_debug_renderer_info
WEBGL_debug_shaders
WEBGL_lose_context
WEBGL_multi_draw
WEBGL_polygon_mode
WEBGL_stencil_texturing
MSAA
EDL
EDL Radius
EDL Strength
Inpainting
Inpainting steps
Inpainting depth contrib.
Point cloud occlusion
Type
FOV
Automatic plane computation
Far plane
Near plane
Max far plane
Min near plane
Width (pixels)
Height (pixels)
x
y
z
x
y
z
color
Enable cache
Default TTL (seconds)
Capacity (MB)
Capacity (entries)
Entries
Memory usage (approx)
Pending requests
Memory tracker
Identifier
Memory usage (CPU)
Memory usage (GPU)
Status
Render order
Enable
Plane normal X
Plane normal Y
Plane normal Z
Distance
Helper size
Negate plane
Visible
Freeze updates
Opacity
Show volumes
Cast shadow
Receive shadow
Error target
Point size
Brightness
Contrast
Saturation
Enabled
Mode
Elevation
Lower bound
Upper bound
Layers
Show helpers
Show hidden objects
Name filter
Hierarchy
Properties
isObject3D
uuid
name
type
matrixAutoUpdate
matrixWorldAutoUpdate
matrixWorldNeedsUpdate
visible
castShadow
receiveShadow
frustumCulled
renderOrder
x
y
z
x
y
z
Options
Classifications
100% © IGN

To make use of classification data available in 3D Tiles point cloud, set pointCloudMode to MODE.CLASSIFICATION. Updating a classification (color and visibility) is done directly on the entity through the pointCloudClassifications property.

index.js
import { Color } from "three";
import { MapControls } from "three/examples/jsm/controls/MapControls.js";
import { CSS2DObject } from "three/examples/jsm/renderers/CSS2DRenderer.js";

import Instance from "@giro3d/giro3d/core/Instance.js";
import Tiles3D from "@giro3d/giro3d/entities/Tiles3D.js";
import Inspector from "@giro3d/giro3d/gui/Inspector.js";
import {
  ASPRS_CLASSIFICATIONS,
  MODE,
} from "@giro3d/giro3d/renderer/PointCloudMaterial.js";

function bindColorPicker(id, onChange) {
  const element = document.getElementById(id);
  if (!(element instanceof HTMLInputElement)) {
    throw new Error(
      "invalid binding element: expected HTMLInputElement, got: " +
        element.constructor.name,
    );
  }

  element.oninput = function oninput() {
    // Let's change the classification color with the color picker value
    const hexColor = element.value;
    onChange(new Color(hexColor));
  };

  const externalFunction = (v) => {
    element.value = `#${new Color(v).getHexString()}`;
    onChange(element.value);
  };

  return [externalFunction, new Color(element.value), element];
}

function bindSlider(id, onChange) {
  const element = document.getElementById(id);
  if (!(element instanceof HTMLInputElement)) {
    throw new Error(
      "invalid binding element: expected HTMLInputElement, got: " +
        element.constructor.name,
    );
  }

  element.oninput = function oninput() {
    onChange(element.valueAsNumber);
  };

  const setValue = (v, min, max, step) => {
    if (min != null && max != null) {
      element.min = min.toString();
      element.max = max.toString();

      if (step != null) {
        element.step = step;
      }
    }
    element.valueAsNumber = v;
    onChange(element.valueAsNumber);
  };

  const initialValue = element.valueAsNumber;

  return [setValue, initialValue, element];
}

function bindToggle(id, onChange) {
  const element = document.getElementById(id);
  if (!(element instanceof HTMLInputElement)) {
    throw new Error(
      "invalid binding element: expected HTMLButtonElement, got: " +
        element.constructor.name,
    );
  }

  element.oninput = function oninput() {
    onChange(element.checked);
  };

  const callback = (v) => {
    element.checked = v;
    onChange(element.checked);
  };

  return [callback, element.checked, element];
}

Instance.registerCRS(
  "EPSG:2154",
  "+proj=lcc +lat_0=46.5 +lon_0=3 +lat_1=49 +lat_2=44 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs",
);

const instance = new Instance({
  target: "view",
  crs: "EPSG:2154",
  backgroundColor: null, // To make the canvas transparent and show the actual CSS background
});

// Enables post-processing effects to improve readability of point cloud.
instance.renderingOptions.enableEDL = true;
instance.renderingOptions.enableInpainting = true;
instance.renderingOptions.enablePointCloudOcclusion = true;

instance.view.camera.position.set(227137, 6876151, 128);

const controls = new MapControls(instance.view.camera, instance.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.2;
controls.target.set(227423, 6876442, 0);
controls.saveState();
instance.view.setControls(controls);

const classifications = ASPRS_CLASSIFICATIONS.map((c) => c.clone());

// The default classifications provide colors for classifications 0-63,
// i.e the reserved range for ASPRS classifications.
// Classifications in the 64-255 range are user-defined.
classifications[64].color = new Color(0x94a770); // Classification "Sursol pérenne"
classifications[65].color = new Color(0xd3ff00); // Classification "Artefacts"
classifications[66].color = new Color(0x00ff8d); // Classification "Points virtuels"

// Original dataset extracted from the French IGN LIDAR HD
// database (https://geoservices.ign.fr/lidarhd#telechargementclassifiees),
// then converted to 3D Tiles with py3dtiles (https://gitlab.com/py3dtiles/py3dtiles)
const url =
  "https://3d.oslandia.com/giro3d/3d-tiles/LHD_FXX_0227_6877_PTS_C_LAMB93_IGN69/tileset.json";

const pointcloud = new Tiles3D({
  url,
  pointCloudMode: MODE.CLASSIFICATION,
  errorTarget: 14,
  classifications,
});

instance.add(pointcloud);

const classificationNames = new Array(32);

// GUI controls for classification handling

function addClassification(number, name) {
  const currentColor =
    pointcloud.pointCloudClassifications[number].color.getHexString();

  const template = `
    <div class="form-check form-switch">
        <input
            class="form-check-input"
            type="checkbox"
            checked
            role="switch"
            id="class-${number}"
            autocomplete="off"
        />
        <label class="form-check-label w-100" for="class-${number}">
            <div class="row">
                <div class="col" >${name}</div>
                <div class="col-auto">
                    <input
                        type="color"
                        style="height: 1.5rem"
                        class="form-control form-control-color float-end"
                        id="color-${number}"
                        value="#${currentColor}"
                        title="Classification color"
                    />
                </div>
            </div>
        </label>
    </div>
    `;

  const node = document.createElement("div");
  node.innerHTML = template;
  document.getElementById("classifications").appendChild(node);

  // Let's change the classification color with the color picker value
  bindColorPicker(`color-${number}`, (v) => {
    // Parse it into a THREE.js color
    const color = new Color(v);

    pointcloud.pointCloudClassifications[number].color = color;

    instance.notifyChange();
  });

  classificationNames[number] = name;

  bindToggle(`class-${number}`, (enabled) => {
    // By toggling the .visible property of a classification,
    // all points that have this classification are hidden/shown.
    pointcloud.pointCloudClassifications[number].visible = enabled;
    instance.notifyChange();
  });
}

// Standard ASPRS classifications found in the dataset
addClassification(1, "Unclassified");
addClassification(2, "Ground");
addClassification(3, "Low vegetation");
addClassification(4, "Medium vegetation");
addClassification(5, "High vegetation");
addClassification(6, "Building");
addClassification(9, "Water");

// Dataset-specific classifications
addClassification(64, "Permanent above-ground structures");
addClassification(65, "Artifacts");
addClassification(67, "Virtual points");

const labelElement = document.createElement("div");
labelElement.classList.value = "badge rounded-pill text-bg-light";
labelElement.style.marginTop = "2rem";

const classifName = document.createElement("span");
classifName.style.marginLeft = "0.5rem";

const classifColor = document.createElement("span");
classifColor.classList.value = "badge rounded-pill";
classifColor.style.color = "white";
classifColor.style.background = "red";
classifColor.style.width = "1rem";
classifColor.innerText = " ";

labelElement.appendChild(classifColor);
labelElement.appendChild(classifName);

const label = new CSS2DObject(labelElement);

instance.add(label);

// Let's query the classification of the picked point and display it in the label.
function updateLabel(mouseEvent) {
  const results = instance.pickObjectsAt(mouseEvent, { radius: 6 });

  // Reset label visibility
  label.visible = false;

  if (results && results.length > 0) {
    for (const result of results) {
      const { object, point, index } = result;

      const classificationIndex = object.geometry
        .getAttribute("classification")
        .getX(index);

      const classification =
        pointcloud.pointCloudClassifications[classificationIndex];

      // Let's ignore hidden classifications
      if (classification && classification.visible) {
        const color = classification.color.getHexString();
        classifColor.style.background = `#${color}`;

        classifName.innerText = classificationNames[classificationIndex];

        label.visible = true;
        label.position.copy(point);
        label.updateMatrixWorld(true);

        break;
      }
    }
  }

  instance.notifyChange();
}

bindSlider("pointSize", (v) => {
  pointcloud.pointSize = v;
  instance.notifyChange(pointcloud);
});

bindToggle("postProcessingEffects", (v) => {
  instance.renderingOptions.enableEDL = v;
  instance.renderingOptions.enableInpainting = v;
  instance.renderingOptions.enablePointCloudOcclusion = v;
  instance.notifyChange(pointcloud);
});

instance.domElement.addEventListener("mousemove", updateLabel);

Inspector.attach("inspector", instance);
index.html
<!doctype html>
<html lang="en">
  <head>
    <title>Point cloud classification</title>
    <meta charset="UTF-8" />
    <meta name="name" content="point_cloud_classification" />
    <meta name="description" content="Display a classified point cloud." />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <link rel="icon" href="https://giro3d.org/images/favicon.svg" />
    <link
      href="https://giro3d.org/assets/bootstrap-custom.css"
      rel="stylesheet"
    />
    <script src="https://giro3d.org/assets/bootstrap.bundle.min.js"></script>
    <link
      rel="stylesheet"
      type="text/css"
      href="https://giro3d.org/latest/examples/css/example.css"
    />

    <style>
      #view canvas {
        background: rgb(132, 170, 182);
        background: radial-gradient(
          circle,
          rgba(132, 170, 182, 1) 0%,
          rgba(37, 44, 48, 1) 100%
        );
      }
    </style>
  </head>

  <body>
    <div id="view" class="m-0 p-0 w-100 h-100"></div>
    <div
      id="inspector"
      class="position-absolute top-0 start-0 mh-100 overflow-auto"
    ></div>

    <div class="side-pane-with-status-bar" style="width: 21rem">
      <div class="mh-100 overflow-y-auto">
        <div class="card mb-1">
          <div class="card-header">Options</div>

          <div class="card-body">
            <fieldset>
              <!-- Point size -->
              <div class="input-group mb-2">
                <label for="pointSize" class="form-label">Point size</label>
                <input
                  type="range"
                  min="0"
                  max="10"
                  value="0"
                  class="form-range"
                  id="pointSize"
                  autocomplete="off"
                />
              </div>

              <!-- Enable point cloud effects -->
              <div class="form-check form-switch">
                <input
                  class="form-check-input"
                  type="checkbox"
                  checked="true"
                  role="switch"
                  id="postProcessingEffects"
                  autocomplete="off"
                />
                <label class="form-check-label" for="postProcessingEffects"
                  >Post-processing effects</label
                >
              </div>
            </fieldset>
          </div>
        </div>

        <div class="card mb-1">
          <div class="card-header">Classifications</div>

          <div class="card-body">
            <fieldset id="classifications">
              <!-- Classifications are added dynamically from the JS example -->
            </fieldset>
          </div>
        </div>
      </div>
    </div>

    <script type="module" src="index.js"></script>
    <script>
      /* activate popovers */
      const popoverTriggerList = [].slice.call(
        document.querySelectorAll('[data-bs-toggle="popover"]'),
      );
      popoverTriggerList.map(
        // bootstrap is used as script in the template, disable warning about undef
        // eslint-disable-next-line no-undef
        (popoverTriggerEl) =>
          new bootstrap.Popover(popoverTriggerEl, {
            trigger: "hover",
            placement: "left",
            content: document.getElementById(
              popoverTriggerEl.getAttribute("data-bs-content"),
            ).innerHTML,
            html: true,
          }),
      );
    </script>
  </body>
</html>
package.json
{
    "name": "point_cloud_classification",
    "dependencies": {
        "@giro3d/giro3d": "0.42.4"
    },
    "devDependencies": {
        "vite": "^3.2.3"
    },
    "scripts": {
        "start": "vite",
        "build": "vite build"
    }
}