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

如何在Three.js中让导入的3D模型可点击?

解决Three.js导入模型的点击交互问题

原代码核心问题

你当前代码存在两个关键问题,导致射线检测无法正常工作:

  1. 每个行星加载时都定义了独立的animate函数,多个渲染循环同时运行,不仅浪费性能,还会干扰交互逻辑。
  2. 没有集中管理可交互的行星模型,射线检测时无法高效遍历目标对象。

以下是完整的解决方案,包含点击检测、相机聚焦和图片显示功能:


步骤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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 15:50:37