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

窗口resize时如何更新three.js Raycaster修正物体选择偏差?

解决 Three.js Raycaster 窗口 resize 后选中异常的问题

这个问题我太熟了!之前做项目的时候踩过一模一样的坑,核心原因是窗口尺寸变化后,相机的投影矩阵没有更新,同时 Raycaster 的射线计算依赖的相机状态和鼠标坐标都是旧的,导致射线“瞄不准”实际的物体位置。

关键修复步骤

1. 监听窗口 resize 事件,同步更新相机与渲染器

窗口大小改变后,相机的宽高比(aspect)已经不匹配新窗口了,必须更新并重新计算投影矩阵:

  • 首先更新渲染器的尺寸,适配新窗口
  • 然后更新相机的宽高比
  • 最重要的是调用 camera.updateProjectionMatrix(),强制相机重新生成投影矩阵(很多人会漏掉这一步,直接导致 Raycaster 失效)

代码示例:

window.addEventListener('resize', () => {
  // 更新渲染器尺寸
  renderer.setSize(window.innerWidth, window.innerHeight);
  
  // 更新相机宽高比
  camera.aspect = window.innerWidth / window.innerHeight;
  
  // 强制更新相机投影矩阵 —— 这一步是核心!
  camera.updateProjectionMatrix();
});

2. 确保点击时使用当前窗口尺寸计算鼠标坐标

Raycaster 的射线是基于**标准化设备坐标(NDC)**计算的,这个坐标依赖当前窗口的宽高。每次点击时都要重新计算,不能用缓存的旧尺寸:

window.addEventListener('click', (event) => {
  // 计算当前窗口下的标准化鼠标坐标
  const mouse = new THREE.Vector2();
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
  
  // 根据当前相机状态更新 Raycaster 射线
  raycaster.setFromCamera(mouse, camera);
  
  // 检测与地板的碰撞
  const intersects = raycaster.intersectObjects([floor]);
  if (intersects.length > 0) {
    console.log('成功选中地板!');
    // 这里添加你的选中逻辑
  }
});

3. (可选)同步更新地板的尺寸/位置(如果需要适配窗口)

如果你的地板需要铺满窗口,窗口 resize 后还要调整地板的缩放或几何体参数,确保它在新窗口的可视区域内:
比如用透视相机时,你可以根据窗口宽高比调整地板的缩放:

window.addEventListener('resize', () => {
  // ... 前面的相机和渲染器更新代码
  
  // 适配地板到新窗口宽高比
  const aspectRatio = window.innerWidth / window.innerHeight;
  floor.scale.set(aspectRatio * 10, 1, 10); // 这里的数值根据你的初始地板大小调整
});

完整可运行示例片段

// 初始化基础组件
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 5, 10);
camera.lookAt(0, 0, 0);

const scene = new THREE.Scene();
const raycaster = new THREE.Raycaster();

// 创建地板
const floorGeometry = new THREE.BoxGeometry(10, 0.1, 10);
const floorMaterial = new THREE.MeshLambertMaterial({ color: 0x888888 });
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.position.y = -0.05; // 让地板贴紧底部
scene.add(floor);

// 添加光源(方便看清地板)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 10, 7.5);
scene.add(directionalLight);

// 窗口 resize 监听
window.addEventListener('resize', () => {
  renderer.setSize(window.innerWidth, window.innerHeight);
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  
  // 适配地板缩放
  const aspectRatio = window.innerWidth / window.innerHeight;
  floor.scale.set(aspectRatio * 10, 1, 10);
});

// 点击监听
window.addEventListener('click', (event) => {
  const mouse = new THREE.Vector2();
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
  
  raycaster.setFromCamera(mouse, camera);
  const intersects = raycaster.intersectObjects([floor]);
  
  if (intersects.length > 0) {
    // 选中后改变地板颜色做反馈
    floor.material.color.setHex(Math.random() * 0xffffff);
    console.log('地板被选中!');
  }
});

// 渲染循环
function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
animate();

避坑提醒

  • 绝对不能忘记调用 camera.updateProjectionMatrix():相机的投影矩阵不会自动更新,必须手动触发,否则相机的视口还是旧窗口的尺寸。
  • 不要缓存窗口宽高:每次计算鼠标坐标时都要用实时的 window.innerWidth 和 window.innerHeight,缓存的数值在 resize 后会失效。
  • 如果地板没有适配窗口:即使 Raycaster 正常工作,你的地板可能已经不在新窗口的可视区域内,导致点击不到——这种情况可以通过调整地板的缩放/位置解决。

内容的提问来源于stack exchange,提问作者Tiarnan Guinée

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.09 10:17:40