
本文详解如何使用three.js为多个3d建筑模型添加鼠标悬停显示文本、点击触发模态框等交互功能,核心基于raycaster实现精准拾取,并提供完整可运行的逻辑框架与注意事项。
本文详解如何使用three.js为多个3d建筑模型添加鼠标悬停显示文本、点击触发模态框等交互功能,核心基于raycaster实现精准拾取,并提供完整可运行的逻辑框架与注意事项。
在Three.js中,为多个3D模型(如城市中的4栋建筑)赋予交互能力——包括悬停显示标签、点击弹出详情模态框——完全可行,且无需将每个模型拆分为独立场景或复杂拓扑。关键在于正确使用 THREE.Raycaster 进行射线拾取(raycasting),它能高效判断鼠标位置是否“击中”指定的3D对象。
✅ 基础准备:统一管理可交互模型
首先,将所有需交互的建筑模型(假设已加载为 THREE.Mesh 实例)存入一个数组,便于统一检测:
const interactiveBuildings = [building1, building2, building3, building4]; // 确保每个模型都设置了唯一标识(推荐使用 .userData 或 .name) building1.userData.id = 'tower-a'; building2.userData.id = 'office-b'; // ... 其他同理
⚠️ 注意:务必为每个模型设置 castShadow: true 和 receiveShadow: true(若启用阴影),并确保其材质 material 的 transparent 和 opacity 设置不影响拾取(Raycaster 默认忽略不可见/透明度为0的对象)。
? 核心交互逻辑:Raycaster + 帧循环检测
在渲染循环(requestAnimationFrame)中实时更新射线方向并检测相交:
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
// 监听鼠标移动,归一化设备坐标 [-1, 1]
window.addEventListener('mousemove', (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
});
let currentIntersect = null;
function animate() {
requestAnimationFrame(animate);
// 更新射线:从相机指向鼠标方向
raycaster.setFromCamera(mouse, camera);
// 检测与可交互模型的交点(按距离排序,取最近的一个)
const intersects = raycaster.intersectObjects(interactiveBuildings);
if (intersects.length > 0) {
const hit = intersects[0];
if (!currentIntersect) {
// 鼠标进入:显示悬浮文本(例如用DOM元素定位到屏幕坐标)
showTooltip(hit.object.userData.id, hit.point);
}
currentIntersect = hit;
} else {
if (currentIntersect) {
// 鼠标离开:隐藏提示
hideTooltip();
}
currentIntersect = null;
}
renderer.render(scene, camera);
}
? 悬停提示:动态定位HTML Tooltip
利用 project() 将3D世界坐标转为屏幕像素位置,实现精准悬浮标签:
function showTooltip(buildingId, worldPosition) {
const vector = worldPosition.clone().project(camera);
const x = (vector.x * 0.5 + 0.5) * window.innerWidth;
const y = (-vector.y * 0.5 + 0.5) * window.innerHeight;
const tooltip = document.getElementById('tooltip');
tooltip.textContent = `Building: ${buildingId}`;
tooltip.style.left = `${x}px`;
tooltip.style.top = `${y}px`;
tooltip.style.display = 'block';
}
function hideTooltip() {
document.getElementById('tooltip').style.display = 'none';
}
? 点击事件:精准响应不同模型
监听全局点击事件,结合 currentIntersect 判断当前悬停对象并执行对应逻辑:
window.addEventListener('click', () => {
if (!currentIntersect) return;
const { object } = currentIntersect;
switch (object.userData.id) {
case 'tower-a':
openModal('Tower A Details', 'Tallest skyscraper in the district.');
break;
case 'office-b':
openModal('Office B', 'Modern co-working space with rooftop garden.');
break;
case 'mall-c':
openModal('Mall C', 'Shopping complex with 80+ retail stores.');
break;
case 'hotel-d':
openModal('Hotel D', '4-star boutique hotel with panoramic views.');
break;
default:
console.warn('Unknown building clicked:', object.userData.id);
}
});
function openModal(title, content) {
const modal = document.getElementById('modal');
modal.querySelector('.modal-title').textContent = title;
modal.querySelector('.modal-body').textContent = content;
modal.style.display = 'block';
}
? 关键注意事项
- 性能优化:避免在 intersectObjects() 中传入整个场景;仅传入明确需要交互的模型数组。
- Z-Fighting修复:若悬停提示文字闪烁,可在 showTooltip() 中对 worldPosition.z 微调(如 +0.1),确保文本始终在模型前方。
- 移动端适配:需补充 touchstart/touchmove 事件,并将 event.touches[0] 替代 event.clientX/Y。
- 模型层级:若建筑由多个子网格组成(如门窗、结构件),建议将 interactiveBuildings 设为父级 Group,或统一设置 traverse() 后的可交互子对象。
通过以上结构化实现,你不仅能轻松扩展至数十个建筑模型,还可无缝集成动画、音效、数据绑定等高级交互——这才是Three.js真正强大的地方。











