Pick objects with various methods (GPU picking, raycasting...)

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
Volume color
Discard no-data values
Sidedness
Front
Depth test
Visible tiles
Reachable tiles
Loaded tiles
Cast shadow
Receive shadow
Tile width (pixels)
Tile height (pixels)
Show grid
Background
Background opacity
Show tiles outlines
Tile outline color
Show tile info
Show extent
Extent color
Subdivision threshold
Deformation
Wireframe
Tile subdivisions
Show collider meshes
CPU terrain
Stitching
Geometry pool
Enabled
Mode
Hillshade
Hillshade intensity
Z factor
Hillshade zenith
Hillshade azimuth
Elevation layers only
Enable
Color
Opacity
X step
Y step
X Offset
Y Offset
Thickness
Enable
Color
Thickness
Opacity
Primary interval (m)
Secondary interval (m)
Brightness
Contrast
Saturation
Layer count
Render state
Normal
Layers
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
Picking
?

Picking lets you pick Giro3D and native THREE.js objects, and get their coordinates in the CRS of the instance.
Provides various filtering options to enhance precision and performance.

  • pixels
  • Pick latency: 12.1 ms
  • No result to display
100% © IGN

Picking is the action of determining what's underneath the mouse cursor.

index.js
import {
  AmbientLight,
  BoxGeometry,
  DirectionalLight,
  Group,
  Mesh,
  MeshLambertMaterial,
  SphereGeometry,
  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 ColorLayer from "@giro3d/giro3d/core/layer/ColorLayer.js";
import ElevationLayer from "@giro3d/giro3d/core/layer/ElevationLayer.js";
import Map from "@giro3d/giro3d/entities/Map.js";
import Tiles3D from "@giro3d/giro3d/entities/Tiles3D.js";
import BilFormat from "@giro3d/giro3d/formats/BilFormat.js";
import Inspector from "@giro3d/giro3d/gui/Inspector.js";
import WmtsSource from "@giro3d/giro3d/sources/WmtsSource.js";

import { bindDropDown } from "./widgets/bindDropDown";
import { bindSlider } from "./widgets/bindSlider";
import { bindToggle } from "./widgets/bindToggle";

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",
);
Instance.registerCRS(
  "IGNF:WGS84G",
  'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]',
);

const extent = new Extent(
  "EPSG:2154",
  -111629.52,
  1275028.84,
  5976033.79,
  7230161.64,
);

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

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

const map = new Map({
  extent,
  backgroundColor: "gray",
  lighting: {
    enabled: true,
    elevationLayersOnly: true,
  },
});

map.name = "map";

instance.add(map);

const noDataValue = -1000;

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

WmtsSource.fromCapabilities(capabilitiesUrl, {
  layer: "ELEVATION.ELEVATIONGRIDCOVERAGE.HIGHRES",
  format: new BilFormat(),
  noDataValue,
})
  .then((elevationWmts) => {
    map.addLayer(
      new ElevationLayer({
        name: "wmts_elevation",
        extent: map.extent,
        // We don't need the full resolution of terrain because we are not using any shading
        resolutionFactor: 0.25,
        minmax: { min: 0, max: 5000 },
        noDataOptions: {
          replaceNoData: false,
        },
        source: elevationWmts,
      }),
    );
  })
  .catch(console.error);

WmtsSource.fromCapabilities(capabilitiesUrl, {
  layer: "HR.ORTHOIMAGERY.ORTHOPHOTOS",
})
  .then((orthophotoWmts) => {
    map.addLayer(
      new ColorLayer({
        name: "wmts_orthophotos",
        extent: map.extent,
        source: orthophotoWmts,
      }),
    );
  })
  .catch(console.error);

// Create the 3D tiles entity
const pointcloud = new Tiles3D({
  url: "https://3d.oslandia.com/lidar_hd/tileset.json",
});

pointcloud.name = "point cloud";

instance.add(pointcloud);

// Add a sunlight
const sun = new DirectionalLight("#ffffff", 1.4);
sun.position.set(-1, -2, 1).normalize();
sun.updateMatrixWorld(true);
instance.scene.add(sun);

// We can look below the floor, so let's light also a bit there
const sun2 = new DirectionalLight("#ffffff", 0.5);
sun2.position.set(0, 1, 1);
sun2.updateMatrixWorld();
instance.scene.add(sun2);

// Add an ambient light
const ambientLight = new AmbientLight(0xffffff, 0.2);
instance.scene.add(ambientLight);

const cube = new Mesh(
  new BoxGeometry(300, 300, 300),
  new MeshLambertMaterial({ color: "blue" }),
);
cube.name = "cube";

cube.position.set(913741, 6459089, 369);
instance.add(cube);
cube.updateMatrixWorld(true);

const lookAt = new Vector3(913896, 6459191, 200);

instance.view.camera.position.set(
  913349.2364044407,
  6456426.459171033,
  1706.0108044011636,
);
instance.view.camera.lookAt(lookAt);

const controls = new MapControls(instance.view.camera, instance.domElement);
controls.target.copy(lookAt);
controls.saveState();
controls.enableDamping = true;
controls.dampingFactor = 0.2;
instance.view.setControls(controls);

const markerMaterial = new MeshLambertMaterial({
  color: "red",
});

const markerGroup = new Group();
instance.add(markerGroup);

const options = {
  gpuPicking: false,
  showMarkers: true,
  pickPointCloudOnly: false,
  pickMapOnly: false,
  radius: 0,
  limit: 0,
  pickEvent: "mousemove",
};

bindDropDown("pickEvent", (v) => (options.pickEvent = v));

bindToggle("gpuPicking", (v) => (options.gpuPicking = v));
bindToggle("showMarkers", (v) => {
  options.showMarkers = v;
  if (!v) {
    markerGroup.clear();
    instance.notifyChange();
  }
});
bindToggle("pickMap", (v) => (options.pickMapOnly = v));
bindToggle("pickPointCloud", (v) => (options.pickPointCloudOnly = v));

bindSlider("radius", (v) => (options.radius = v));
bindSlider("limit", (v) => (options.limit = v));

function updateResultTable(pickResults) {
  const table = document.getElementById("table");
  const resultList = document.getElementById("results");

  resultList.innerHTML = "";

  function column(content) {
    const col = document.createElement("td");
    col.innerHTML = content;
    return col;
  }

  const emptyWarning = document.getElementById("emptyWarning");
  if (pickResults.length > 0) {
    emptyWarning.style.display = "none";
    table.style.display = "unset";
  } else {
    emptyWarning.style.display = "unset";
    table.style.display = "none";
  }

  for (let index = 0; index < pickResults.length; index++) {
    const pickResult = pickResults[index];
    const tr = document.createElement("tr");

    // result #
    tr.appendChild(column(`${index}`));
    // entity
    const entity = pickResult.entity;
    tr.appendChild(
      column(entity ? `<code>${pickResult.entity?.name}</code>` : "none"),
    );
    // picked object
    const type = pickResult.object.type;
    tr.appendChild(
      column(`<span class="badge rounded-pill text-bg-primary">${type}</span>`),
    );

    const point = pickResult.point;

    // X, Y, Z coordinates of point
    tr.appendChild(column(point.x.toFixed(0)));
    tr.appendChild(column(point.y.toFixed(0)));
    tr.appendChild(column(point.z.toFixed(0)));

    resultList.appendChild(tr);
  }
}

const sphere = new SphereGeometry(8);

function performPicking(mouseEvent) {
  // Determine which entities to include
  let where = [];
  if (options.pickMapOnly) {
    where.push(map);
  }

  if (options.pickPointCloudOnly) {
    where.push(pointcloud);
  }

  const pickOptions = {
    limit: options.limit === 0 ? undefined : options.limit,
    radius: options.radius,
    gpuPicking: options.gpuPicking,
    where: where.length > 0 ? where : undefined,
    sortByDistance: true,
  };

  const start = performance.now();
  const results = instance.pickObjectsAt(mouseEvent, pickOptions);
  const end = performance.now();

  document.getElementById("latency").innerText =
    `Latency: ${(end - start).toFixed(1)} ms`;

  const noRaycast = () => {};

  if (options.showMarkers && results.length > 0) {
    const position = results[0].point;
    const marker = new Mesh(sphere, markerMaterial);
    // Disable raycasting on markers to avoid picking them.
    marker.raycast = noRaycast;
    if (markerGroup.children.length > 30) {
      const removed = markerGroup.children.splice(
        0,
        markerGroup.children.length - 30,
      );
      removed.forEach((item) => item.removeFromParent());
    }

    // - In the case of CPU picking, the Z value is simply the Z-coordinate of
    // the picked point, which itself is affected by the scale of the scene.
    //
    // - In the case of GPU picking, the Z value is sampled from the texture, unaffected
    // by the scale of the scene. That is why we have to apply the scene scale to obtain the
    // correct world space coordinate.
    if (options.gpuPicking) {
      position.multiply(instance.scene.scale);
    }

    marker.position.copy(position);
    // Notice we use attach instead of add so that world position is
    // preserved in case of non-default scale.
    markerGroup.attach(marker);
    marker.updateMatrixWorld(true);
    instance.notifyChange();
  }

  updateResultTable(results);
}

function onMouseMove(mouseEvent) {
  if (options.pickEvent === "mousemove") {
    performPicking(mouseEvent);
  }
}

function onMouseClick(mouseEvent) {
  if (options.pickEvent === "click") {
    performPicking(mouseEvent);
  }
}

bindSlider("zScaleSlider", (v) => {
  document.getElementById("zScaleLabel").innerText =
    `Z-scale = ${v.toFixed(1)}`;

  instance.scene.scale.setZ(v);
  instance.scene.updateMatrixWorld(true);
  instance.notifyChange(map);
});

instance.domElement.addEventListener("mousemove", onMouseMove);
instance.domElement.addEventListener("click", onMouseClick);

instance.scene.updateMatrixWorld(true);

instance.notifyChange();

Inspector.attach("inspector", instance);
index.html
<!doctype html>
<html lang="en">
  <head>
    <title>Picking</title>
    <meta charset="UTF-8" />
    <meta name="name" content="picking" />
    <meta
      name="description"
      content="Pick objects with various methods (GPU picking, raycasting...)"
    />
    <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" style="width: 30rem">
      <div class="card">
        <h5 class="card-header">Picking</h5>

        <!-- tooltip -->
        <span
          class="badge bg-secondary position-absolute top-0 end-0 m-2"
          data-bs-toggle="popover"
          data-bs-content="pickingHelper"
          >?</span
        >
        <p class="card-text d-none" id="pickingHelper">
          Picking lets you pick Giro3D and native THREE.js objects, and get
          their coordinates in the CRS of the instance.<br />
          Provides various filtering options to enhance precision and
          performance.
        </p>

        <div class="card-body p-0">
          <ul class="list-group list-group-flush">
            <li class="list-group-item">
              <!-- Parameters -->
              <form>
                <!-- Z-scale -->
                <div class="mb-2">
                  <label
                    for="zScaleSlider"
                    class="col-form-label"
                    id="zScaleLabel"
                    >Z-scale = 1</label
                  >
                  <div class="input-group">
                    <input
                      type="range"
                      min="0.1"
                      max="4"
                      step="0.1"
                      value="1"
                      class="form-range"
                      id="zScaleSlider"
                      autocomplete="off"
                    />
                  </div>
                </div>

                <!-- Pick events -->
                <div class="mb-3">
                  <label for="pickEvent" class="form-label"
                    >Pick on event</label
                  >
                  <select id="pickEvent" class="form-select">
                    <option value="click">Click</option>
                    <option value="mousemove" selected>Mouse move</option>
                  </select>
                </div>
                <!-- Picking radius -->
                <div class="mb-3">
                  <label for="radius" class="col-form-label"
                    >Picking radius</label
                  >
                  <div class="input-group">
                    <input
                      id="radius"
                      type="number"
                      min="0"
                      max="10"
                      value="0"
                      class="form-control"
                    />
                    <span class="input-group-text">pixels</span>
                  </div>
                </div>
                <!-- Limit -->
                <div class="mb-3">
                  <label for="limit" class="col-form-label"
                    >Number of objects (0 for all)</label
                  >
                  <input
                    id="limit"
                    type="number"
                    min="0"
                    max="100"
                    value="0"
                    class="form-control"
                  />
                </div>

                <div class="row">
                  <!-- Prefer raycasting -->
                  <div class="col">
                    <div class="form-check">
                      <input
                        class="form-check-input"
                        type="checkbox"
                        id="gpuPicking"
                      />
                      <label class="form-check-label" for="gpuPicking">
                        GPU picking
                      </label>
                    </div>
                  </div>

                  <!-- Picking markers -->
                  <div class="col">
                    <div class="form-check">
                      <input
                        class="form-check-input"
                        type="checkbox"
                        checked
                        id="showMarkers"
                        autocomplete="off"
                      />
                      <label class="form-check-label" for="showMarkers">
                        Show markers
                      </label>
                    </div>
                  </div>
                </div>
                <div class="row">
                  <!-- Pick map -->
                  <div class="col">
                    <div class="form-check">
                      <input
                        class="form-check-input"
                        type="checkbox"
                        id="pickMap"
                        autocomplete="off"
                      />
                      <label class="form-check-label" for="pickMap">
                        Pick only map
                      </label>
                    </div>
                  </div>
                  <!-- Pick point cloud -->
                  <div class="col">
                    <div class="form-check">
                      <input
                        class="form-check-input"
                        type="checkbox"
                        id="pickPointCloud"
                        autocomplete="off"
                      />
                      <label class="form-check-label" for="pickPointCloud">
                        Pick only point cloud
                      </label>
                    </div>
                  </div>
                </div>
              </form>
            </li>
            <li class="list-group-item">
              <span id="latency">Pick latency: 12.1 ms</span>
            </li>
            <li class="list-group-item">
              <div id="emptyWarning" class="p-4">
                <span class="fs-4 text-secondary">No result to display</span>
              </div>
              <!-- Result table -->
              <table id="table" class="table" style="display: none">
                <thead>
                  <tr>
                    <th scope="col">#</th>
                    <th scope="col">Entity</th>
                    <th scope="col">Object</th>
                    <th scope="col">X</th>
                    <th scope="col">Y</th>
                    <th scope="col">Z</th>
                  </tr>
                </thead>
                <tbody id="results">
                  <tr>
                    <td>?</td>
                    <td>?</td>
                    <td>?</td>
                    <td>?</td>
                    <td>?</td>
                    <td>?</td>
                  </tr>
                </tbody>
              </table>
            </li>
          </ul>
        </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": "picking",
    "dependencies": {
        "@giro3d/giro3d": "0.42.3"
    },
    "devDependencies": {
        "vite": "^3.2.3"
    },
    "scripts": {
        "start": "vite",
        "build": "vite build"
    }
}