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

基于Three.js实现城市3D建筑hover与点击交互的技术咨询

解答

1. 是否需要单独添加每栋建筑以实现可点击功能?

不需要重复单独添加建筑对象,但必须给每栋建筑的Mesh(或分组对象)设置唯一标识——比如通过userData属性存储建筑的名称、ID、描述等信息,这样射线检测时能快速区分不同建筑。

2. 能否为3D对象创建此类交互?

完全可以,Three.js中实现这类鼠标交互的核心方案就是Raycaster(射线投射),它能检测鼠标射线与3D对象的相交情况,以此触发悬停、点击等交互逻辑。

3. Raycaster的具体应用步骤(新手友好版)

下面是针对你的需求的完整实现思路和代码片段:

步骤1:初始化核心工具

先创建Raycaster实例和鼠标位置向量,用于后续射线检测:

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2(); // 存储标准化设备坐标(范围:x/-1~1,y/-1~1)

步骤2:给建筑对象添加标识

假设你已经加载或创建了4栋建筑的Mesh,给每个Mesh添加userData标记:

// 示例:给第一栋建筑加标识
const building1 = yourLoadedMesh1; // 替换成你的建筑Mesh
building1.userData = {
  isBuilding: true, // 标记这是可交互的建筑
  name: "商业综合楼",
  desc: "5层商业建筑,涵盖零售、餐饮业态"
};

// 其余3栋建筑同理,只需修改userData里的内容

步骤3:监听鼠标事件,更新坐标

将屏幕鼠标坐标转换为Three.js需要的标准化设备坐标:

window.addEventListener('mousemove', (e) => {
  mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
});

步骤4:实现悬停显示顶部文本

新手推荐用HTML元素做文本标签(比Three.js的3D文本更简单易维护):
首先在HTML中添加标签容器:

<div id="buildingLabel" style="position: absolute; display: none; background: rgba(0,0,0,0.8); color: #fff; padding: 4px 8px; border-radius: 4px; pointer-events: none; font-size: 14px;"></div>

然后在渲染循环中处理悬停逻辑:

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

// 提前计算建筑包围盒(如果没自动计算的话)
// building1.geometry.computeBoundingBox();

function animate() {
  requestAnimationFrame(animate);

  // 更新射线:从相机位置指向鼠标方向
  raycaster.setFromCamera(mouse, camera);

  // 检测场景中与射线相交的对象(第二个true表示递归检测子对象,适合分组的建筑)
  const intersects = raycaster.intersectObjects(scene.children, true);

  if (intersects.length > 0) {
    const targetObj = intersects[0].object;
    // 判断是否是标记过的建筑
    if (targetObj.userData.isBuilding) {
      // 获取建筑顶部的3D坐标
      const topPos = new THREE.Vector3(
        targetObj.position.x,
        targetObj.position.y + targetObj.geometry.boundingBox.max.y,
        targetObj.position.z
      );
      // 将3D坐标转换为屏幕坐标
      topPos.project(camera);
      const screenX = (topPos.x + 1) / 2 * window.innerWidth;
      const screenY = (-topPos.y + 1) / 2 * window.innerHeight;

      // 显示并定位标签
      buildingLabel.style.display = 'block';
      buildingLabel.style.left = `${screenX}px`;
      buildingLabel.style.top = `${screenY}px`;
      buildingLabel.textContent = targetObj.userData.name;
    } else {
      buildingLabel.style.display = 'none';
    }
  } else {
    buildingLabel.style.display = 'none';
  }

  renderer.render(scene, camera);
}
animate();

步骤5:实现点击弹出模态窗口

同样用HTML模态框实现:
先添加模态框HTML:

<div id="buildingModal" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 12px rgba(0,0,0,0.3); display: none; z-index: 999;">
  <h3 id="modalTitle"></h3>
  <p id="modalDesc"></p>
  <button onclick="closeModal()">关闭</button>
</div>

然后添加点击事件监听和关闭函数:

const buildingModal = document.getElementById('buildingModal');
const modalTitle = document.getElementById('modalTitle');
const modalDesc = document.getElementById('modalDesc');

window.addEventListener('click', () => {
  raycaster.setFromCamera(mouse, camera);
  const intersects = raycaster.intersectObjects(scene.children, true);
  
  if (intersects.length > 0) {
    const targetObj = intersects[0].object;
    if (targetObj.userData.isBuilding) {
      // 填充模态框内容并显示
      modalTitle.textContent = targetObj.userData.name;
      modalDesc.textContent = targetObj.userData.desc;
      buildingModal.style.display = 'block';
    }
  }
});

function closeModal() {
  buildingModal.style.display = 'none';
}

注意事项

  • 如果你的建筑是用Group分组的,确保射线检测时开启递归(intersectObjects的第二个参数设为true)
  • 提前调用geometry.computeBoundingBox()计算建筑包围盒,否则boundingBox会是undefined
  • 用HTML元素做UI比Three.js的3D文本更节省性能,也更易修改样式

内容的提问来源于stack exchange,提问作者Kinarin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 22:35:03