
本文详解如何使用 three.js 的 raycaster 实现城市场景中多个 3d 建筑的鼠标悬停(显示标签)和点击(弹出模态框)交互,涵盖坐标转换、射线检测、状态管理及事件分发全流程。
本文详解如何使用 three.js 的 raycaster 实现城市场景中多个 3d 建筑的鼠标悬停(显示标签)和点击(弹出模态框)交互,涵盖坐标转换、射线检测、状态管理及事件分发全流程。
在 Three.js 中,为多个 3D 模型(如城市中的四栋建筑)添加交互能力完全可行,且无需将每个模型拆分为独立场景——关键在于统一管理可交互对象 + 精确射线检测 + 状态驱动响应。核心工具是 THREE.Raycaster,它通过从相机向鼠标位置发射一条虚拟射线,检测其与指定 3D 对象的交点,从而实现“点击”与“悬停”的底层逻辑。
✅ 基础准备:初始化 Raycaster 与鼠标坐标映射
首先创建射线投射器,并建立鼠标屏幕坐标到标准化设备坐标(NDC)的映射关系:
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
// 监听鼠标移动,实时更新 mouse.x/mouse.y(范围:[-1, 1])
window.addEventListener('mousemove', (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
});⚠️ 注意:sizes.width/height 在原文中指代视口尺寸,实际开发中建议使用 window.innerWidth/innerHeight 或监听 resize 事件动态更新,确保响应式兼容。
✅ 悬停检测:模拟 mouseenter / mouseleave
在渲染循环(如 requestAnimationFrame 回调)中执行射线检测,并维护当前交点状态以触发进出事件:
详细的 Three.js 3D 图形参考,涵盖场景设置、相机、几何体、材质、光照、动画、控制器、加载器、数学工具和调试。
let currentIntersect = null; // 全局状态:记录当前悬停的物体
function render() {
// 更新射线起点与方向(基于相机和鼠标位置)
raycaster.setFromCamera(mouse, camera);
// 指定待检测的建筑模型数组(确保它们已添加至 scene)
const buildings = [building1, building2, building3, building4];
const intersects = raycaster.intersectObjects(buildings);
if (intersects.length > 0) {
const hoveredObject = intersects[0].object;
if (currentIntersect === null) {
// 首次悬停:显示浮动标签(例如 DOM tooltip 或 3D 文本)
showTooltip(hoveredObject, getBuildingName(hoveredObject));
console.log('进入建筑:', hoveredObject.name);
}
currentIntersect = hoveredObject;
} else {
if (currentIntersect !== null) {
// 悬停离开:隐藏标签
hideTooltip();
console.log('离开建筑:', currentIntersect.name);
}
currentIntersect = null;
}
renderer.render(scene, camera);
}? 提示:showTooltip() 可通过 document.createElement('div') 动态创建 HTML 标签并定位到屏幕坐标(需将 3D 交点投影为屏幕像素),或使用 CSS2DRenderer 实现更稳定的 3D 空间标签。
✅ 点击响应:精准识别目标并触发模态框
点击事件独立监听,复用 currentIntersect 判断是否处于有效悬停状态,再通过 switch 或 Map 分发对应逻辑:
window.addEventListener('click', () => {
if (!currentIntersect) return;
// 推荐:用 Map 存储模型与业务逻辑的映射,更易维护
const buildingActions = new Map([
[building1, () => openModal('金融中心', '这是一座50层的地标写字楼...')],
[building2, () => openModal('科技园区', '聚焦AI研发的创新孵化基地...')],
[building3, () => openModal('文化广场', '集剧院、美术馆与公共空间于一体...')],
[building4, () => openModal('生态住宅', '零碳设计的智能宜居社区...')]
]);
const action = buildingActions.get(currentIntersect);
if (action) action();
});✅ openModal(title, content) 可封装为标准模态框函数(如基于 Bootstrap 或自定义 CSS+JS),确保 UI 层与 3D 逻辑解耦。
? 关键注意事项
- 性能优化:避免在 render() 中重复创建数组(如 buildings),应提前声明为常量;若模型数量庞大,考虑使用 Octree 或分组 Group 提升射线检测效率。
- 模型要求:确保每栋建筑为独立 Mesh 实例(而非合并几何体),且 mesh.userData 可附加元数据(如 mesh.userData.id = 'building-1'),便于后续逻辑扩展。
- 交互反馈:悬停时建议同步改变材质颜色(mesh.material.emissive.set(0xffaa00))或缩放,增强视觉提示。
- 移动端适配:需补充 touchstart / touchmove 事件,并将 event.touches[0] 替换 event.clientX/Y。
通过以上结构化实现,你不仅能为多栋建筑赋予一致的交互体验,还能轻松扩展至数十甚至上百个可点击对象——Raycaster 是 Three.js 交互的基石,而状态管理与模块化设计,才是工程化落地的关键。

















