I have the following code which works great to “hover” objects, however it is possible for multiple objects to be hovered at once if their meshes overlap at the point of the mouse. I understand that the intersections array is sorted such that the first element is the closest, but I can’t figure out how to use this to my advantage given the following code. Any help is appreciated.
Live demo with code: https://playcode.io/1436752
import * as THREE from 'three';
import { OrbitControls } from 'three-controls';
// ----------------------- SCENE
const scene = new THREE.Scene();
// ----------------------- CAMERA
const camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
10000
);
camera.position.set(0, 0, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xfff000);
document.body.appendChild(renderer.domElement);
renderer.setPixelRatio(window.devicePixelRatio);
// ----------------------- CONTROLS
const controls = new OrbitControls(camera, renderer.domElement);
controls.addEventListener('change', render);
// ----------------------- RAYCASTING
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
var raycastLayer = [];
let hovered = {};
let intersects = [];
window.addEventListener('pointermove', e => {
pointer.set(
(e.clientX / window.innerWidth) * 2 - 1,
-(e.clientY / window.innerHeight) * 2 + 1
);
raycaster.setFromCamera(pointer, camera);
intersects = raycaster.intersectObjects(raycastLayer, true);
// if a previously hovered item is not among the hits we must call onPointerOut
Object.keys(hovered).forEach(key => {
const hit = intersects.find(hit => hit.object.uuid === key);
if (hit === undefined) {
const hoveredItem = hovered[key];
if (hoveredItem.object.onPointerOut)
hoveredItem.object.onPointerOut(hoveredItem);
delete hovered[key];
}
});
if (intersects.length > 0) {
[intersects[0]].forEach(hit => {
// if a hit has not been flagged as hovered we must call onPointerOver
if (!hovered[hit.object.uuid]) {
hovered[hit.object.uuid] = hit;
if (hit.object.onPointerOver) hit.object.onPointerOver(hit);
}
// call onPointerMove
if (hit.object.onPointerMove) hit.object.onPointerMove(hit);
});
}
render();
});
window.addEventListener('click', e => {
if (e.target == renderer.domElement) {
// update the picking ray with the camera and pointer position
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(raycastLayer, true);
intersects.forEach(hit => {
if (hit.object.onClick) {
hit.object.onClick(hit);
}
});
}
});
// ----------------------- LIGHTING
const ambLight = new THREE.AmbientLight(0xffffff, 0.7); // soft white light
scene.add(ambLight);
scene.background = new THREE.Color(0xffff00);
// ----------------------- GLYPHS
const activeGlyphScale = 1.25;
class Glyph extends THREE.Mesh {
constructor() {
super();
this.position.set(
Math.random() * 5 - 2.5,
Math.random() * 5 - 2.5,
Math.random() * 5 - 2.5
);
this.isActive = false;
let randCol = new THREE.Color(0xffffff);
randCol.setHex(Math.random() * 0xffffff);
this.geometry = new THREE.PlaneGeometry(1, 1);
this.material = new THREE.MeshStandardMaterial({
side: 2,
color: randCol,
emissiveIntensity: 1,
});
this.scale.setScalar(1);
}
onPointerOver() {
this.scale.setScalar(1.5);
}
onPointerOut() {
this.scale.setScalar(1);
}
onClick(e) {}
}
let glyphs = [];
for (let i = 0; i < 15; i++) {
glyphs[i] = new Glyph();
scene.add(glyphs[i]);
raycastLayer.push(glyphs[i]);
}
// ----------------------- RENDER
render();
function render() {
renderer.render(scene, camera);
}
animate();
function animate() {
requestAnimationFrame(animate);
}
2
Answers
As you said it yourself, the objects are sorted based on the intersection point distance from ray origin. Just pick the one that is at the index zero.
I using somethink like this to pick first object. If you want intersect only some group of objects you can use layers for this.