如何在Three.js中让导入的3D模型可点击?
解决Three.js导入模型的点击交互问题
原代码核心问题
你当前代码存在两个关键问题,导致射线检测无法正常工作:
- 每个行星加载时都定义了独立的
animate函数,多个渲染循环同时运行,不仅浪费性能,还会干扰交互逻辑。 - 没有集中管理可交互的行星模型,射线检测时无法高效遍历目标对象。
以下是完整的解决方案,包含点击检测、相机聚焦和图片显示功能:
步骤1:添加全局交互变量
在代码开头添加射线检测和行星管理所需的全局变量:
// 射线检测工具 const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); // 存储所有可交互行星 const planets = []; // 行星对应图片(替换为你的实际图片路径) const planetImages = { sun: 'sun.jpg', mercury: 'mercury.jpg', venus: 'venus.jpg', earth: 'earth.jpg', mars: 'mars.jpg', jupiter: 'jupiter.jpg', saturn: 'saturn.jpg', uranus: 'uranus.jpg', neptune: 'neptune.jpg' };
步骤2:重构行星加载逻辑
统一所有行星的加载代码,给行星命名并添加到planets数组,删除每个行星内部的独立animate函数:
// 示例:太阳加载(其他行星同理修改) loader.load('sun.glb', function (gltf) { let sun = gltf.scene; sun.name = 'sun'; // 命名用于后续识别 sun.scale.set(0.2, 0.2, 0.2); sun.position.set(-300, 0, 0); planets.push(sun); // 添加到行星数组 scene.add(sun); }, undefined, function (error) { console.error(error); });
步骤3:统一全局渲染循环
修改全局animate函数,统一处理所有行星的旋转逻辑:
function animate() { requestAnimationFrame(animate); // 遍历所有行星执行旋转 planets.forEach(planet => { planet.rotation.y += 0.002; }); controls.update(); // 必须在这里更新控制器 renderer.render(scene, camera); }
步骤4:实现点击检测逻辑
添加鼠标点击事件监听器,将屏幕坐标转换为Three.js标准化坐标,执行射线检测:
function onMouseClick(event) { // 转换鼠标坐标为Three.js标准化设备坐标(范围-1到1) mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; // 更新射线起点和方向 raycaster.setFromCamera(mouse, camera); // 检测射线与行星的交点(true表示递归检测子对象) const intersects = raycaster.intersectObjects(planets, true); if (intersects.length > 0) { const clickedPlanet = intersects[0].object.parent; // 获取行星根对象 handlePlanetClick(clickedPlanet); } } window.addEventListener('click', onMouseClick);
步骤5:实现相机聚焦和图片显示
编写点击后的处理函数,完成相机聚焦和图片展示:
function handlePlanetClick(planet) { // 相机聚焦到行星:平滑过渡到行星位置 controls.target.copy(planet.position); // 根据行星大小动态调整相机偏移 const cameraOffset = new THREE.Vector3(0, planet.scale.y * 500, planet.scale.z * 800); camera.position.copy(planet.position.clone().add(cameraOffset)); controls.update(); // 显示行星图片 showPlanetImage(planet); } function showPlanetImage(planet) { // 创建或获取图片元素 let imgElement = document.getElementById('planet-image'); if (!imgElement) { imgElement = document.createElement('img'); imgElement.id = 'planet-image'; imgElement.style.position = 'absolute'; imgElement.style.zIndex = '100'; imgElement.style.width = '200px'; imgElement.style.height = 'auto'; imgElement.style.border = '2px solid white'; document.body.appendChild(imgElement); } // 设置图片源 imgElement.src = planetImages[planet.name]; // 将行星3D坐标转换为屏幕坐标 const screenPos = planet.position.clone(); screenPos.project(camera); // 转换为DOM坐标并定位图片 const domX = (screenPos.x * 0.5 + 0.5) * window.innerWidth; const domY = (-screenPos.y * 0.5 + 0.5) * window.innerHeight; imgElement.style.left = `${domX + 20}px`; imgElement.style.top = `${domY}px`; }
完整修改后的代码
import * as THREE from 'three'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; const renderer = new THREE.WebGLRenderer({ canvas: document.querySelector('#bg'), }); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 20000); scene.add(camera) const directionalLight = new THREE.DirectionalLight(0xffffff, 0); directionalLight.position.set(0,1,0); scene.add(directionalLight) renderer.setClearColor(0x000000) renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); camera.position.set(-700, 1000, 600); // 射线检测相关 const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); // 存储所有可交互行星 const planets = []; // 行星对应图片路径 const planetImages = { sun: 'sun.jpg', mercury: 'mercury.jpg', venus: 'venus.jpg', earth: 'earth.jpg', mars: 'mars.jpg', jupiter: 'jupiter.jpg', saturn: 'saturn.jpg', uranus: 'uranus.jpg', neptune: 'neptune.jpg' }; // Loader const loader = new GLTFLoader(); // 太阳 loader.load('sun.glb', function (gltf) { let sun = gltf.scene; sun.name = 'sun'; sun.scale.set(0.2, 0.2, 0.2); sun.position.set(-300, 0, 0); planets.push(sun); scene.add(sun); }, undefined, function (error) { console.error(error); }); // 水星 loader.load('mercury.glb', function (gltf) { let mercury = gltf.scene; mercury.name = 'mercury'; mercury.scale.set(0.03, 0.03, 0.03); mercury.position.set(-100, 0, 0); planets.push(mercury); scene.add(mercury); }, undefined, function (error) { console.error(error); }); // 金星 loader.load('venus.glb', function (gltf) { let venus = gltf.scene; venus.name = 'venus'; venus.scale.set(0.04, 0.04, 0.04); venus.position.set(30, 0, 0); planets.push(venus); scene.add(venus); }, undefined, function (error) { console.error(error); }); // 地球 loader.load('earth.glb', function (gltf) { let earth = gltf.scene; earth.name = 'earth'; earth.scale.set(0.04, 0.04, 0.04); earth.position.set(140, 0, 0); planets.push(earth); scene.add(earth); }, undefined, function (error) { console.error(error); }); // 火星 loader.load('mars.glb', function (gltf) { let mars = gltf.scene; mars.name = 'mars'; mars.scale.set(0.015, 0.015, 0.015); mars.position.set(230, 0, 0); planets.push(mars); scene.add(mars); }, undefined, function (error) { console.error(error); }); // 木星 loader.load('jupiter.glb', function (gltf) { let jupiter = gltf.scene; jupiter.name = 'jupiter'; jupiter.scale.set(0.08, 0.08, 0.08); jupiter.position.set(350, 0, 0); planets.push(jupiter); scene.add(jupiter); }, undefined, function (error) { console.error(error); }); // 土星 loader.load('saturn.glb', function (gltf) { let saturn = gltf.scene; saturn.name = 'saturn'; saturn.scale.set(0.07, 0.07, 0.07); saturn.position.set(530, 0, 0); planets.push(saturn); scene.add(saturn); }, undefined, function (error) { console.error(error); }); // 天王星 loader.load('uranus.glb', function (gltf) { let uranus = gltf.scene; uranus.name = 'uranus'; uranus.scale.set(0.05, 0.05, 0.05); uranus.position.set(700, 0, 0); planets.push(uranus); scene.add(uranus); }, undefined, function (error) { console.error(error); }); // 海王星 loader.load('neptune.glb', function (gltf) { let neptune = gltf.scene; neptune.name = 'neptune'; neptune.scale.set(0.045, 0.045, 0.045); neptune.position.set(850, 0, 0); planets.push(neptune); scene.add(neptune); }, undefined, function (error) { console.error(error); }); // 生成星星 function addstar() { const geometry = new THREE.SphereGeometry(0.25, 24, 24); const material = new THREE.MeshStandardMaterial({ color: 0xffffff }) const star = new THREE.Mesh(geometry, material); const [x, y, z] = Array(3).fill().map(() => THREE.MathUtils.randFloatSpread(3000)); star.position.set(x,y,z); scene.add(star) } Array(1000).fill().forEach(addstar) // 背景 const background = new THREE.TextureLoader().load('background.jpg') scene.background = background // 控制器 let controls = new OrbitControls(camera, renderer.domElement) controls.update() // 环境光 let light = new THREE.AmbientLight(0xffffff, 2) scene.add(light) // 窗口 resize 处理 function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } window.addEventListener('resize', onWindowResize, false); // 鼠标点击事件处理 function onMouseClick(event) { mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(planets, true); if (intersects.length > 0) { const clickedPlanet = intersects[0].object.parent; handlePlanetClick(clickedPlanet); } } window.addEventListener('click', onMouseClick); // 点击行星后的处理逻辑 function handlePlanetClick(planet) { // 相机聚焦 controls.target.copy(planet.position); const cameraOffset = new THREE.Vector3(0, planet.scale.y * 500, planet.scale.z * 800); camera.position.copy(planet.position.clone().add(cameraOffset)); controls.update(); // 显示图片 showPlanetImage(planet); } // 显示行星图片 function showPlanetImage(planet) { let imgElement = document.getElementById('planet-image'); if (!imgElement) { imgElement = document.createElement('img'); imgElement.id = 'planet-image'; imgElement.style.position = 'absolute'; imgElement.style.zIndex = '100'; imgElement.style.width = '200px'; imgElement.style.height = 'auto'; imgElement.style.border = '2px solid white'; document.body.appendChild(imgElement); } imgElement.src = planetImages[planet.name]; // 3D坐标转屏幕坐标 const screenPos = planet.position.clone(); screenPos.project(camera); const domX = (screenPos.x * 0.5 + 0.5) * window.innerWidth; const domY = (-screenPos.y * 0.5 + 0.5) * window.innerHeight; imgElement.style.left = `${domX + 20}px`; imgElement.style.top = `${domY}px`; } // 全局渲染循环 function animate() { requestAnimationFrame(animate); planets.forEach(planet => { planet.rotation.y += 0.002; }); controls.update(); renderer.render(scene, camera); } animate();
关键说明
- 统一渲染循环:避免多个
animate函数冲突,提升性能。 - 射线检测递归:
intersectObjects的第二个参数设为true,确保检测到GLTF模型的子网格。 - 动态相机偏移:根据行星缩放比例调整相机位置,适配不同大小的行星。
- 坐标转换:通过
project方法将3D坐标转为屏幕坐标,让图片始终跟随行星位置。
内容的提问来源于stack exchange,提问作者Francesco
相关产品推荐
相关产品推荐

