如何利用Raycast解决多重叠对象同时被悬停的问题
解决Three.js中重叠对象悬停时仅触发最近对象的问题
你的核心问题是:原代码在清理已悬停对象时,只要对象存在于intersects数组中就保留悬停状态,但intersects包含所有被射线命中的重叠对象,导致多个对象同时处于悬停状态。利用Raycaster返回结果按距离排序的特性,我们只需要保留数组第一个元素(最近对象),并将其他所有已悬停对象移出即可。
修改后的关键逻辑说明
我们需要调整pointermove事件中的处理逻辑,核心是只聚焦最近的命中对象,并清理所有非目标的悬停状态:
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); // 仅保留最近的命中对象(Raycaster返回数组按距离由近到远排序) const currentHoverTarget = intersects.length > 0 ? intersects[0] : null; // 清理所有非当前目标的已悬停对象 Object.keys(hovered).forEach(key => { const hoveredObj = hovered[key].object; // 如果没有目标,或已悬停对象不是当前目标,则触发移出事件并删除 if (!currentHoverTarget || hoveredObj.uuid !== currentHoverTarget.object.uuid) { if (hoveredObj.onPointerOut) hoveredObj.onPointerOut(hovered[key]); delete hovered[key]; } }); // 处理当前最近对象的悬停逻辑 if (currentHoverTarget) { const targetObj = currentHoverTarget.object; if (!hovered[targetObj.uuid]) { hovered[targetObj.uuid] = currentHoverTarget; if (targetObj.onPointerOver) targetObj.onPointerOver(currentHoverTarget); } if (targetObj.onPointerMove) targetObj.onPointerMove(currentHoverTarget); } render(); });
核心修改点
- 锁定最近目标:通过
currentHoverTarget提取intersects的第一个元素,确保只处理距离摄像头最近的对象。 - 精准清理悬停状态:遍历已悬停对象时,仅保留与
currentHoverTarget匹配的对象,其他全部触发onPointerOut并从hovered集合中移除,彻底避免多对象同时悬停。 - 简化目标处理:直接针对
currentHoverTarget处理悬停进入和移动事件,逻辑更清晰。
完整修改后的代码
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); // 仅保留最近的命中对象 const currentHoverTarget = intersects.length > 0 ? intersects[0] : null; // 清理所有非当前目标的已悬停对象 Object.keys(hovered).forEach(key => { const hoveredObj = hovered[key].object; if (!currentHoverTarget || hoveredObj.uuid !== currentHoverTarget.object.uuid) { if (hoveredObj.onPointerOut) hoveredObj.onPointerOut(hovered[key]); delete hovered[key]; } }); // 处理当前最近对象的悬停逻辑 if (currentHoverTarget) { const targetObj = currentHoverTarget.object; if (!hovered[targetObj.uuid]) { hovered[targetObj.uuid] = currentHoverTarget; if (targetObj.onPointerOver) targetObj.onPointerOver(currentHoverTarget); } if (targetObj.onPointerMove) targetObj.onPointerMove(currentHoverTarget); } render(); }); window.addEventListener('click', e => { if (e.target == renderer.domElement) { raycaster.setFromCamera(pointer, camera); const intersects = raycaster.intersectObjects(raycastLayer, true); // 点击仅触发最近对象的事件(保持交互一致性) if (intersects.length > 0) { const hit = intersects[0]; 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); }
注:点击事件也同步修改为仅触发最近对象的事件,保持交互逻辑的一致性,你可以根据需求调整。
内容的提问来源于stack exchange,提问作者prismspecs
相关产品推荐
相关产品推荐

