UNPKG

itowns

Version:

A JS/WebGL framework for 3D geospatial data visualization

263 lines (253 loc) 9.06 kB
import * as THREE from 'three'; import RenderMode from "../Renderer/RenderMode.js"; import { unpack1K } from "../Renderer/LayeredMaterial.js"; import { Coordinates } from '@itowns/geographic'; const depthRGBA = new THREE.Vector4(); // TileMesh picking support function function screenCoordsToNodeId(view, tileLayer, viewCoords) { let radius = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; const dim = view.mainLoop.gfxEngine.getWindowSize(); viewCoords = viewCoords || new THREE.Vector2(Math.floor(dim.x / 2), Math.floor(dim.y / 2)); const restore = tileLayer.level0Nodes.map(n => RenderMode.push(n, RenderMode.MODES.ID)); const buffer = view.mainLoop.gfxEngine.renderViewToBuffer({ camera: view.camera, scene: tileLayer.object3d }, { x: viewCoords.x - radius, y: viewCoords.y - radius, width: 1 + radius * 2, height: 1 + radius * 2 }); restore.forEach(r => r()); const ids = []; traversePickingCircle(radius, (x, y) => { const idx = (y * 2 * radius + x) * 4; const data = buffer.slice(idx, idx + 4 || undefined); depthRGBA.fromArray(data).divideScalar(255.0); const unpack = unpack1K(depthRGBA, 256 ** 3); const _id = Math.round(unpack); if (!ids.includes(_id)) { ids.push(_id); } }); return ids; } function traversePickingCircle(radius, callback) { // iterate on radius so we get closer to the mouse // results first. // Result traversal order for radius=2 // --3-- // -323- // 32123 // -323 // --3-- let prevSq; for (let r = 0; r <= radius; r++) { const sq = r * r; for (let x = -r; x <= r; x++) { const sqx = x * x; for (let y = -r; y <= r; y++) { const dist = sqx + y * y; // skip if too far if (dist > sq) { continue; } // skip if belongs to previous if (dist <= prevSq) { continue; } if (callback(x, y) === false) { return; } } } prevSq = sq; } } function findLayerInParent(obj) { if (obj.layer) { return obj.layer; } if (obj.parent) { return findLayerInParent(obj.parent); } } const raycaster = new THREE.Raycaster(); const normalized = new THREE.Vector2(); const pointPos = new THREE.Vector3(); const pointPosCoord = new Coordinates('EPSG:4978'); // default crs, will be set to view crs when used const cameraPos = new THREE.Vector3(); const cameraPosCoord = new Coordinates('EPSG:4978'); // default crs, will be set to view crs when used /** * @module Picking * * Implement various picking methods for geometry layers. * These methods are not meant to be used directly, see View.pickObjectsAt * instead. * * All the methods here takes the same parameters: * - the View instance * - view coordinates (in pixels) where picking should be done * - radius (in pixels) of the picking circle * - layer: the geometry layer used for picking */ export default { pickTilesAt(view, viewCoords, radius, layer) { let results = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : []; const _ids = screenCoordsToNodeId(view, layer, viewCoords, radius); const extractResult = node => { if (_ids.includes(node.id) && node.isTileMesh) { results.push({ object: node, layer }); } }; for (const n of layer.level0Nodes) { n.traverse(extractResult); } return results; }, pickPointsAt(view, viewCoords, radius, layer) { let result = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : []; if (!layer.root) { return; } // enable picking mode for points material layer.object3d.traverse(o => { if (o.isPoints && o.baseId) { o.material.enablePicking(true); } }); // render 1 pixel // TODO: support more than 1 pixel selection const buffer = view.mainLoop.gfxEngine.renderViewToBuffer({ camera: view.camera, scene: layer.object3d }, { x: viewCoords.x - radius, y: viewCoords.y - radius, width: 1 + radius * 2, height: 1 + radius * 2 }); const candidates = []; traversePickingCircle(radius, (x, y) => { // x, y are offset from the center of the picking circle, // and pixels is a square where 0, 0 is the top-left corner. // So we need to shift x,y by radius. const idx = ((y + radius) * (radius * 2 + 1) + (x + radius)) * 4; const data = buffer.slice(idx, idx + 4); // see PotreeProvider and the construction of unique_id const objId = data[0] << 8 | data[1]; const index = data[2] << 8 | data[3]; const r = { objId, index }; for (let i = 0; i < candidates.length; i++) { if (candidates[i].objId == r.objId && candidates[i].index == r.index) { return; } } candidates.push(r); }); layer.object3d.traverse(o => { if (o.isPoints && o.baseId) { // disable picking mode o.material.enablePicking(false); // Skip if geometry is invalid or missing position attribute if (!o.geometry || !o.geometry.attributes || !o.geometry.attributes.position) { return; } // if baseId matches objId, the clicked point belongs to `o` for (let i = 0; i < candidates.length; i++) { if (candidates[i].objId == o.baseId) { // Get point position: get the picked point from the buffer geometry and apply local to world // transform of the picked object pointPos.fromBufferAttribute(o.geometry.attributes.position, candidates[i].index); o.localToWorld(pointPos); // Compute distance to the camera pointPosCoord.setCrs(view.referenceCrs); pointPosCoord.setFromVector3(pointPos); view.camera3D.getWorldPosition(cameraPos); cameraPosCoord.setCrs(view.referenceCrs); cameraPosCoord.setFromVector3(cameraPos); const dist = pointPosCoord.spatialEuclideanDistanceTo(cameraPosCoord); result.push({ object: o, point: pointPos.clone(), // the position of the point in the 3D view. Same name and value than what's returned by pickObjectsAt index: candidates[i].index, distance: dist, layer }); } } } }); return result; }, /* * Default picking method. Uses THREE.Raycaster */ pickObjectsAt(view, viewCoords, radius, object) { let target = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : []; // Raycaster use NDC coordinate view.viewToNormalizedCoords(viewCoords, normalized); if (radius < 0) { raycaster.setFromCamera(normalized, view.camera3D); const intersects = raycaster.intersectObject(object, true); for (const inter of intersects) { inter.layer = findLayerInParent(inter.object); target.push(inter); } return target; } // Instead of doing N raycast (1 per x,y returned by traversePickingCircle), // we force render the zone of interest. // Then we'll only do raycasting for the pixels where something was drawn. const zone = { x: viewCoords.x - radius, y: viewCoords.y - radius, width: 1 + radius * 2, height: 1 + radius * 2 }; const pixels = view.mainLoop.gfxEngine.renderViewToBuffer({ scene: object, camera: view.camera }, zone); const clearColor = new THREE.Color(); view.mainLoop.gfxEngine.renderer.getClearColor(clearColor); const clearR = Math.round(255 * clearColor.r); const clearG = Math.round(255 * clearColor.g); const clearB = Math.round(255 * clearColor.b); // Raycaster use NDC coordinate const tmp = normalized.clone(); traversePickingCircle(radius, (x, y) => { // x, y are offset from the center of the picking circle, // and pixels is a square where 0, 0 is the top-left corner. // So we need to shift x,y by radius. const offset = ((y + radius) * (radius * 2 + 1) + (x + radius)) * 4; const r = pixels[offset]; const g = pixels[offset + 1]; const b = pixels[offset + 2]; // Use approx. test to avoid rounding error or to behave // differently depending on hardware rounding mode. if (Math.abs(clearR - r) <= 1 && Math.abs(clearG - g) <= 1 && Math.abs(clearB - b) <= 1) { // skip because nothing has been rendered here return; } // Perform raycasting tmp.setX(normalized.x + x / view.camera.width).setY(normalized.y + y / view.camera.height); raycaster.setFromCamera(tmp, view.camera3D); const intersects = raycaster.intersectObject(object, true); for (const inter of intersects) { inter.layer = findLayerInParent(inter.object); target.push(inter); } // Stop at first hit return target.length == 0; }); return target; } };