Illustrates colorization of point clouds with multiple modes.

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
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
Mode
Color ramp
100% © IGN

Point clouds can be colorized from different methods : using the original color in the point cloud, coloring using an elevation gradient, or coloring using an external image source, such as a WMS layer.

index.js
import colormap from "colormap";

import { Vector3 } from "three";
import { MapControls } from "three/examples/jsm/controls/MapControls.js";

import Extent from "@giro3d/giro3d/core/geographic/Extent.js";
import Instance from "@giro3d/giro3d/core/Instance.js";
import ColorMap from "@giro3d/giro3d/core/ColorMap.js";
import ColorLayer from "@giro3d/giro3d/core/layer/ColorLayer.js";
import Tiles3D from "@giro3d/giro3d/entities/Tiles3D.js";
import Inspector from "@giro3d/giro3d/gui/Inspector.js";
import { MODE } from "@giro3d/giro3d/renderer/PointCloudMaterial.js";
import WmtsSource from "@giro3d/giro3d/sources/WmtsSource.js";

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

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

  const lower = min;
  const upper = max;

  callback(lower.valueAsNumber, upper.valueAsNumber);

  function updateLabels() {
    document.getElementById("minLabel").innerText =
      `Lower bound: ${Math.round(lower.valueAsNumber)}m`;
    document.getElementById("maxLabel").innerText =
      `Upper bound: ${Math.round(upper.valueAsNumber)}m`;
  }

  lower.oninput = function oninput() {
    const rawValue = lower.valueAsNumber;
    const clampedValue = MathUtils.clamp(
      rawValue,
      Number.parseFloat(lower.min),
      upper.valueAsNumber - 1,
    );
    lower.valueAsNumber = clampedValue;
    callback(lower.valueAsNumber, upper.valueAsNumber);
    updateLabels();
  };

  upper.oninput = function oninput() {
    const rawValue = upper.valueAsNumber;
    const clampedValue = MathUtils.clamp(
      rawValue,
      lower.valueAsNumber + 1,
      Number.parseFloat(upper.max),
    );
    upper.valueAsNumber = clampedValue;
    callback(lower.valueAsNumber, upper.valueAsNumber);
    updateLabels();
  };

  const externalInput = (min, max) => {
    lower.min = min;
    lower.max = max;
    upper.min = min;
    upper.max = max;
    lower.valueAsNumber = min;
    upper.valueAsNumber = max;
    callback(lower.valueAsNumber, upper.valueAsNumber);
    updateLabels();
  };

  return externalInput;
}

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

  element.onchange = () => {
    onChange(element.value);
  };

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

  const setOptions = (options) => {
    const items = options.map(
      (opt) =>
        `<option value=${opt.id} ${opt.selected ? "selected" : ""}>${opt.name}</option>`,
    );
    element.innerHTML = items.join("\n");
  };

  return [callback, element.value, element, setOptions];
}

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

  element.onchange = () => {
    onChange(parseInt(element.value));
  };

  const callback = (v) => {
    element.value = v.toString();
    onChange(parseInt(element.value));
  };

  return [callback, parseInt(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];
}

function makeColorRamp(
  preset,
  discrete = false,
  invert = false,
  mirror = false,
) {
  let nshades = discrete ? 10 : 256;

  const values = colormap({ colormap: preset, nshades });

  const colors = values.map((v) => new Color(v));

  if (invert) {
    colors.reverse();
  }

  if (mirror) {
    const mirrored = [...colors, ...colors.reverse()];
    return mirrored;
  }

  return colors;
}

const colorRamps = {};

function makeColorRamps() {
  colorRamps.viridis = makeColorRamp("viridis");
  colorRamps.jet = makeColorRamp("jet");
  colorRamps.blackbody = makeColorRamp("blackbody");
  colorRamps.earth = makeColorRamp("earth");
  colorRamps.bathymetry = makeColorRamp("bathymetry");
  colorRamps.magma = makeColorRamp("magma");
  colorRamps.par = makeColorRamp("par");

  colorRamps.slope = makeColorRamp("RdBu");
}

makeColorRamps();

const tmpVec3 = new Vector3();

Instance.registerCRS(
  "EPSG:3946",
  "+proj=lcc +lat_1=45.25 +lat_2=46.75 +lat_0=46 +lon_0=3 +x_0=1700000 " +
    "+y_0=5200000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs",
);

const instance = new Instance({
  target: "view",
  crs: "EPSG:3946",
  backgroundColor: 0xcccccc,
});

// Create the 3D tiles entity
const pointcloud = new Tiles3D({
  url: "https://3d.oslandia.com/3dtiles/lyon.3dtiles/tileset.json",
  colorMap: new ColorMap({ colors: colorRamps["viridis"], min: 100, max: 600 }),
  errorTarget: 15,
});

let colorLayer;

function placeCamera(position, lookAt) {
  instance.view.camera.position.set(position.x, position.y, position.z);
  instance.view.camera.lookAt(lookAt);
  // create controls
  const controls = new MapControls(instance.view.camera, instance.domElement);
  controls.target.copy(lookAt);
  controls.enableDamping = true;
  controls.dampingFactor = 0.25;

  instance.view.setControls(controls);

  instance.notifyChange(instance.view.camera);
}

// add pointcloud to scene
function initializeCamera() {
  const bbox = pointcloud.getBoundingBox();

  instance.view.camera.far = 2.0 * bbox.getSize(tmpVec3).length();

  const ratio = bbox.getSize(tmpVec3).x / bbox.getSize(tmpVec3).z;
  const position = bbox.min
    .clone()
    .add(bbox.getSize(tmpVec3).multiply(new Vector3(0, 0, ratio * 0.5)));
  const lookAt = bbox.getCenter(tmpVec3);
  lookAt.z = bbox.min.z;

  const extent = Extent.fromBox3("EPSG:3946", bbox);

  placeCamera(position, lookAt);

  const url =
    "https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetCapabilities";

  // Let's build the color layer from the WMTS capabilities
  WmtsSource.fromCapabilities(url, {
    layer: "HR.ORTHOIMAGERY.ORTHOPHOTOS",
  })
    .then((orthophotoWmts) => {
      colorLayer = new ColorLayer({
        name: "color",
        extent,
        source: orthophotoWmts,
      });
      pointcloud.setColorLayer(colorLayer);
    })
    .catch(console.error);

  instance.renderingOptions.enableEDL = true;
  instance.renderingOptions.enableInpainting = true;
  instance.renderingOptions.enablePointCloudOcclusion = true;

  pointcloud.pointCloudMode = MODE.TEXTURE;
}

instance.add(pointcloud).then(initializeCamera);

Inspector.attach("inspector", instance);
instance.domElement.addEventListener("dblclick", (e) =>
  console.log(
    instance.pickObjectsAt(e, {
      // Specify a radius around where we click so we don't have to precisely be on a point
      // to select it
      radius: 5,
      // Limit the number of results for better performances
      limit: 10,
      // Some points are incoherent in the pointcloud, don't pick them
      filter: (p) => !Number.isNaN(p.point.z) && p.point.z < 1000,
    }),
  ),
);

instance.notifyChange();

bindToggle("edl-enable", (v) => {
  instance.renderingOptions.enableEDL = v;
  instance.notifyChange();
});
bindToggle("occlusion-enable", (v) => {
  instance.renderingOptions.enablePointCloudOcclusion = v;
  instance.notifyChange();
});
bindToggle("inpainting-enable", (v) => {
  instance.renderingOptions.enableInpainting = v;
  instance.notifyChange();
});
bindSlider("edl-radius", (v) => {
  instance.renderingOptions.EDLRadius = v;
  instance.notifyChange();
});
bindSlider("edl-intensity", (v) => {
  instance.renderingOptions.EDLStrength = v;
  instance.notifyChange();
});
bindSlider("inpainting-steps", (v) => {
  instance.renderingOptions.inpaintingSteps = v;
  instance.notifyChange();
});
bindSlider("opacity", (v) => {
  pointcloud.opacity = v;
  document.getElementById("opacityLabel").innerText =
    `Point cloud opacity: ${Math.round(v * 100)}%`;
  instance.notifyChange(pointcloud);
});

bindColorMapBounds((min, max) => {
  pointcloud.colorMap.min = min;
  pointcloud.colorMap.max = max;
  instance.notifyChange(pointcloud);
});

const colorMapGroup = document.getElementById("colormapGroup");

bindNumericalDropDown("pointcloud_mode", (newMode) => {
  pointcloud.pointCloudMode = newMode;

  if (newMode === MODE.ELEVATION) {
    colorMapGroup.classList.remove("d-none");
  } else {
    colorMapGroup.classList.add("d-none");
  }

  instance.notifyChange(pointcloud);
  if (colorLayer) {
    colorLayer.visible = newMode === MODE.TEXTURE;
    instance.notifyChange(colorLayer);
  }
});

bindDropDown("colormap", (newRamp) => {
  pointcloud.colorMap.colors = colorRamps[newRamp];
  instance.notifyChange(pointcloud);
});
index.html
<!doctype html>
<html lang="en">
  <head>
    <title>Colorized 3D Tiles Point Cloud</title>
    <meta charset="UTF-8" />
    <meta name="name" content="colorized_pointcloud" />
    <meta
      name="description"
      content="Illustrates colorization of point clouds with multiple modes."
    />
    <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"
    />
  </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">
      <!-- Top color layer -->
      <div class="card">
        <div class="card-header">Options</div>

        <div class="card-body" id="top-options">
          <!-- Coloring mode -->
          <div class="input-group">
            <span class="input-group-text flex-grow-1">Mode</span>
            <select
              class="btn btn-outline-primary btn-sm"
              id="pointcloud_mode"
              autocomplete="off"
            >
              <option value="0">point cloud colors</option>
              <option value="5">elevation gradient</option>
              <option value="4" selected>color layer</option>
            </select>
          </div>

          <!-- Color ramp -->
          <div class="d-none my-2" id="colormapGroup">
            <div class="input-group">
              <span class="input-group-text flex-grow-1">Color ramp</span>
              <select
                class="btn btn-outline-primary btn-sm"
                id="colormap"
                autocomplete="off"
              >
                <option value="viridis" selected>Viridis</option>
                <option value="jet">Jet</option>
                <option value="blackbody">Blackbody</option>
                <option value="earth">Earth</option>
                <option value="bathymetry">Bathymetry</option>
                <option value="magma">Magma</option>
                <option value="par">Par</option>
              </select>
            </div>

            <div class="input-group my-2">
              <label for="min" class="form-label" id="minLabel"
                >Lower bound: 100m</label
              >
              <div class="input-group">
                <input
                  type="range"
                  min="0"
                  max="1000"
                  value="100"
                  class="form-range"
                  id="min"
                  autocomplete="off"
                />
              </div>
            </div>

            <div class="input-group my-2">
              <label for="max" class="form-label" id="maxLabel"
                >Upper bound: 600m</label
              >
              <div class="input-group">
                <input
                  type="range"
                  min="0"
                  max="1000"
                  value="600"
                  class="form-range"
                  id="max"
                  autocomplete="off"
                />
              </div>
            </div>
          </div>

          <!-- Opacity -->
          <div class="input-group my-2">
            <label for="opacity" class="form-label" id="opacityLabel"
              >Point cloud opacity: 100%</label
            >
            <div class="input-group">
              <input
                type="range"
                min="0"
                max="1"
                step="0.05"
                value="1"
                class="form-range"
                id="opacity"
                autocomplete="off"
              />
            </div>
          </div>

          <!-- Activate EDL -->
          <div class="input-group my-2">
            <div>
              <div class="form-check form-switch">
                <input
                  class="form-check-input"
                  checked
                  type="checkbox"
                  role="switch"
                  id="edl-enable"
                  autocomplete="off"
                />
                <label class="form-check-label" for="edl-enable"
                  >Eye dome lighting (EDL)</label
                >
              </div>
            </div>
          </div>

          <!-- EDL intensity -->
          <div class="input-group my-2">
            <label for="edl-intensity" class="form-label">EDL intensity</label>
            <div class="input-group">
              <input
                type="range"
                min="0"
                step="0.05"
                max="2"
                value="0.7"
                class="form-range"
                id="edl-intensity"
                autocomplete="off"
              />
            </div>
          </div>

          <!-- EDL radius -->
          <div class="input-group my-2">
            <label for="edl-radius" class="form-label">EDL radius</label>
            <div class="input-group">
              <input
                type="range"
                min="0.1"
                step="0.05"
                max="5"
                value="1.5"
                class="form-range"
                id="edl-radius"
                autocomplete="off"
              />
            </div>
          </div>

          <!-- Activate occlusion -->
          <div class="input-group my-2">
            <div>
              <div class="form-check form-switch">
                <input
                  class="form-check-input"
                  checked
                  type="checkbox"
                  role="switch"
                  id="occlusion-enable"
                  autocomplete="off"
                />
                <label class="form-check-label" for="occlusion-enable"
                  >Point occlusion effect</label
                >
              </div>
            </div>
          </div>

          <!-- Activate inpainting -->
          <div class="input-group my-2">
            <div>
              <div class="form-check form-switch">
                <input
                  class="form-check-input"
                  checked
                  type="checkbox"
                  role="switch"
                  id="inpainting-enable"
                  autocomplete="off"
                />
                <label class="form-check-label" for="inpainting-enable"
                  >Inpainting</label
                >
              </div>
            </div>
          </div>

          <!-- Inpainting steps -->
          <div class="input-group my-2">
            <label for="inpainting-steps" class="form-label"
              >Inpainting steps</label
            >
            <div class="input-group">
              <input
                type="range"
                min="1"
                step="1"
                max="10"
                value="2"
                class="form-range"
                id="inpainting-steps"
                autocomplete="off"
              />
            </div>
          </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": "colorized_pointcloud",
    "dependencies": {
        "colormap": "^2.3.2",
        "@giro3d/giro3d": "0.42.4"
    },
    "devDependencies": {
        "vite": "^3.2.3"
    },
    "scripts": {
        "start": "vite",
        "build": "vite build"
    }
}