窗口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
相关产品推荐
相关产品推荐

