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

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.

px
Mode Time (ms) # Objects First object Coordinate
Picking
Raycasting
Projection 1 -
100% © IGN

Picking is the action of determining what's underneath the mouse cursor. Various picking technique exist, to handle different use cases.

index.js
import TileWMS from 'ol/source/TileWMS.js';
import { Raycaster, Vector3, Vector2 } 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 Map from '@giro3d/giro3d/entities/Map.js';
import Tiles3D from '@giro3d/giro3d/entities/Tiles3D.js';
import TiledImageSource from '@giro3d/giro3d/sources/TiledImageSource.js';
import ColorLayer from '@giro3d/giro3d/core/layer/ColorLayer.js';
import ElevationLayer from '@giro3d/giro3d/core/layer/ElevationLayer.js';
import BilFormat from '@giro3d/giro3d/formats/BilFormat.js';
import PointCloudMaterial, { MODE } from '@giro3d/giro3d/renderer/PointCloudMaterial.js';
import Tiles3DSource from '@giro3d/giro3d/sources/Tiles3DSource.js';
import Inspector from '@giro3d/giro3d/gui/Inspector.js';



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 extent = new Extent('EPSG:3946', 1837816.94334, 1847692.32501, 5170036.4587, 5178412.82698);

const viewerDiv = document.getElementById('viewerDiv');

const instance = new Instance(viewerDiv, {
    crs: 'EPSG:3946',
    renderer: {
        clearColor: 0xcccccc,
    },
});

// Create the 3D tiles entity
const pointcloud = new Tiles3D(
    'pointcloud',
    new Tiles3DSource('https://3d.oslandia.com/3dtiles/lyon.3dtiles/tileset.json'),
    {
        material: new PointCloudMaterial({
            size: 4,
            mode: MODE.COLOR,
        }),
    },
);
pointcloud.material.transparent = true;
pointcloud.material.needsUpdate = true;
pointcloud.material.opacity = 0.5;
instance.add(pointcloud);

const map = new Map('map', { extent });
instance.add(map);

// Adds a WMS imagery layer
const colorSource = new TiledImageSource({
    source: new TileWMS({
        url: 'https://data.geopf.fr/wms-r',
        projection: 'EPSG:3946',
        params: {
            LAYERS: ['HR.ORTHOIMAGERY.ORTHOPHOTOS'],
            FORMAT: 'image/jpeg',
        },
    }),
});

const colorLayer = new ColorLayer({
    name: 'wms_imagery',
    extent: map.extent,
    source: colorSource,
});
map.addLayer(colorLayer);

// Adds a WMS elevation layer
const elevationSource = new TiledImageSource({
    source: new TileWMS({
        url: 'https://data.geopf.fr/wms-r',
        projection: 'EPSG:3946',
        crossOrigin: 'anonymous',
        params: {
            LAYERS: ['ELEVATION.ELEVATIONGRIDCOVERAGE.HIGHRES'],
            FORMAT: 'image/x-bil;bits=32',
        },
    }),
    format: new BilFormat(),
    noDataValue: -1000,
});

const elevationLayer = new ElevationLayer({
    name: 'wms_elevation',
    extent: map.extent,
    source: elevationSource,
});

map.addLayer(elevationLayer);

// Sets the camera position
instance.camera.camera3D.position.set(extent.west(), extent.south(), 2000);

// Creates controls
const controls = new MapControls(instance.camera.camera3D, instance.domElement);

// Then looks at extent's center
controls.target = extent.centerAsVector3();
controls.target.z = 200;
controls.saveState();

controls.enableDamping = true;
controls.dampingFactor = 0.2;

instance.useTHREEControls(controls);

Inspector.attach(document.getElementById('panelDiv'), instance);

const radiusSlider = document.getElementById('radiusSlider');
const limitSlider = document.getElementById('limitSlider');

let where = ['map'];
document.getElementById('pickSource').addEventListener('change', e => {
    const newMode = parseInt(e.target.value, 10);
    if (newMode === 1) {
        where = ['pointcloud'];
    } else if (newMode === 2) {
        where = ['map'];
    } else {
        where = undefined;
    }
});

const raycaster = new Raycaster();

function findEntityInParent(obj) {
    if (obj.userData.parentEntity) {
        return obj.userData.parentEntity;
    }
    if (obj.parent) {
        return findEntityInParent(obj.parent);
    }
    return null;
}

const tmp = { vec2: new Vector2() };

function raycast(evt) {
    const results = [];
    const pointer = instance.eventToNormalizedCoords(evt, tmp.vec2).clone();

    raycaster.setFromCamera(pointer, instance.camera.camera3D);
    const picked = raycaster.intersectObject(instance.scene, true);
    for (const inter of picked) {
        inter.entity = findEntityInParent(inter.object);
        results.push(inter);
    }
    return results;
}

function project(evt, zDefault = 0) {
    // Fallback to getting coordinates assuming click is on Z=zDefault
    const ndc = instance.eventToNormalizedCoords(evt, tmp.vec2).clone();
    const vec = new Vector3(ndc.x, ndc.y, 0.5);
    vec.unproject(instance.camera.camera3D);

    vec.sub(instance.camera.camera3D.position).normalize();

    const distance = (zDefault - instance.camera.camera3D.position.z) / vec.z;
    const scaled = vec.multiplyScalar(distance);
    return instance.camera.camera3D.position.clone().add(scaled);
}

const formatter = new Intl.NumberFormat();

instance.domElement.addEventListener('dblclick', e => {
    const elem = id => document.getElementById(id);
    let t0 = performance.now();
    function format(point) {
        return `x: ${formatter.format(point.x)}\n
                y: ${formatter.format(point.y)}\n
                z: ${formatter.format(point.z)}`;
    }
    const picked = instance.pickObjectsAt(e, {
        radius: parseInt(radiusSlider.value, 10),
        limit: limitSlider.value === '0' ? undefined : parseInt(limitSlider.value, 10),
        where,
        // Remove uncoherent points from result
        filter: p => !Number.isNaN(p.point.x) && !Number.isNaN(p.point.y) && p.point.z < 1000,
    });
    let t1 = performance.now();
    console.log('Picked', picked);
    elem('pickingTiming').innerHTML = `${t1 - t0}`;
    elem('pickingCount').innerHTML = `${picked.length}`;
    elem('pickingCoord').innerHTML = picked.length > 0 ? format(picked[0].point) : '-';
    elem('pickingFirstResult').innerHTML =
        picked.length > 0 ? `${picked[0].entity.id} (${picked[0].entity.type})` : '-';

    t0 = performance.now();
    const raycasted = raycast(e);
    t1 = performance.now();
    console.log('Raycasted', raycasted);

    elem('raycastingTiming').innerHTML = `${t1 - t0}`;
    elem('raycastingCount').innerHTML = `${raycasted.length}`;
    elem('raycastingCoord').innerHTML = raycasted.length > 0 ? format(raycasted[0].point) : '-';
    elem('raycastingFirstResult').innerHTML =
        raycasted.length > 0 ? `${raycasted[0].entity.id} (${raycasted[0].entity.type})` : '-';

    t0 = performance.now();
    const projected = project(e, controls.target.z);
    t1 = performance.now();
    console.log('Projected', projected);
    elem('projectingTiming').innerHTML = `${t1 - t0}`;
    elem('projectingCoord').innerHTML = format(projected);
});
index.html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Giro3D - Picking</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
  <style>
    body {
      padding: 0;
      margin: 0;
      width: 100vw;
      height: 100vh;
    }

    #viewerDiv {
      width: 100%;
      height: 100%;
    }

    #panelDiv {
      position: absolute;
      top: 0;
      left: 0;
    }
    
  </style>
</head>

<body>
  <div id="viewerDiv"></div>
  <div id="panelDiv"></div>
  <div class="m-2 position-absolute top-0 end-0">
    <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">
            <!-- Parameters -->
            <form>
                <div class="row">
                    <div class="dropdown">
                        <select class="btn btn-primary dropdown-toggle" id="pickSource">
                            <option value="0">Pick from whole scene</option>
                            <option value="1">Pick from pointcloud</option>
                            <option value="2" selected>Pick from map</option>
                        </select>
                    </div>
                </div>
                <div class="row">
                    <label for="radiusSlider" class="col-sm-8 col-form-label">Picking radius</label>
                    <div class="col-sm-4">
                        <div class="input-group">
                            <input
                                id="radiusSlider"
                                type="number"
                                min="0"
                                max="10"
                                value="0"
                                class="form-control"
                            />
                            <span class="input-group-text">px</span>
                        </div>
                    </div>
                </div>
                <div class="row">
                    <label for="limitSlider" class="col-sm-8 col-form-label"
                        >Number of objects (0 for all)</label
                    >
                    <div class="col-sm-4">
                        <input
                            id="limitSlider"
                            type="number"
                            min="0"
                            max="100"
                            value="0"
                            class="form-control"
                        />
                    </div>
                </div>
            </form>

            <!-- Result table -->
            <table class="table">
                <thead>
                    <tr>
                        <th scope="col">Mode</th>
                        <th scope="col">Time (ms)</th>
                        <th scope="col"># Objects</th>
                        <th scope="col">First object</th>
                        <th scope="col">Coordinate</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <th scope="row">Picking</th>
                        <td><span id="pickingTiming"></span></td>
                        <td><span id="pickingCount"></span></td>
                        <td><span id="pickingFirstResult"></span></td>
                        <td><span id="pickingCoord"></span></td>
                    </tr>
                    <tr>
                        <th scope="row">Raycasting</th>
                        <td><span id="raycastingTiming"></span></td>
                        <td><span id="raycastingCount"></span></td>
                        <td><span id="raycastingFirstResult"></span></td>
                        <td><span id="raycastingCoord"></span></td>
                    </tr>
                    <tr>
                        <th scope="row">Projection</th>
                        <td><span id="projectingTiming"></span></td>
                        <td><span id="projectingCount">1</span></td>
                        <td><span id="projectingFirstResult">-</span></td>
                        <td><span id="projectingCoord"></span></td>
                    </tr>
                </tbody>
            </table>
        </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.35.0"
  },
  "devDependencies": {
    "vite": "^3.2.3"
  },
  "scripts": {
    "start": "vite",
    "build": "vite build"
  }
}