You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何利用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();
});

核心修改点

  1. 锁定最近目标:通过currentHoverTarget提取intersects的第一个元素,确保只处理距离摄像头最近的对象。
  2. 精准清理悬停状态:遍历已悬停对象时,仅保留与currentHoverTarget匹配的对象,其他全部触发onPointerOut并从hovered集合中移除,彻底避免多对象同时悬停。
  3. 简化目标处理:直接针对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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 08:47:05