解决React Three Fiber中跟随鼠标的球体透视畸变问题
解决跟随鼠标球体的屏幕边缘畸变问题
看起来你遇到的是透视相机的典型透视畸变问题——当物体处于屏幕边缘时,透视投影会导致物体形状拉伸变形,尤其是在FOV设置较大或者物体离相机相对近的情况下。咱们一步步来解决这个问题:
问题根源
你当前使用的是透视相机(PerspectiveCamera),它的视锥体是梯形结构,屏幕边缘的物体因为投影关系会产生明显的透视变形。加上你是直接通过mouse.x * viewport.width / 2这种线性方式计算球体位置,没有考虑透视投影的非线性特性,所以边缘的畸变会被放大。
解决方案1:改用正交相机(彻底消除畸变)
如果你的场景不需要透视效果(比如这种跟随鼠标的UI元素),正交相机(OrthographicCamera)是最佳选择——它没有透视变形,屏幕上任何位置的物体大小、形状都保持一致。
修改Canvas组件的相机配置:
const Canvas: NextPage<CanvasProps> = ({ children }) => { const { width, height } = useThree((state) => state.viewport); const aspectRatio = width / height; const cameraZoom = 2; // 调整这个值控制整体视野大小 // 配置正交相机参数 const camera = { type: 'orthographic', left: -aspectRatio * cameraZoom, right: aspectRatio * cameraZoom, top: cameraZoom, bottom: -cameraZoom, near: 0.1, far: 1000, position: [0, 0, 5] } return ( <ThreeCanvas className="bg-gray-900" camera={camera}> {children} <Mouse /> </ThreeCanvas> ) }
简化Mouse组件的位置计算:
正交相机下,鼠标归一化坐标可以直接映射到世界坐标,无需复杂计算:
useFrame(({ mouse }) => { if (!ref.current) return; // 直接用鼠标归一化值乘以相机缩放系数 const x = mouse.x * cameraZoom; const y = mouse.y * cameraZoom; ref.current.position.set(x, y, 0); // 如果不需要旋转效果可以直接移除这行 ref.current.rotation.set(-y * 0.5, x * 0.5, 0); })
注意:这里的cameraZoom要和Canvas组件里的保持一致,或者可以通过useThree获取相机实例的zoom属性来同步。
解决方案2:使用射线投射修正透视相机的位置计算
如果你需要保留透视效果,可以用Raycaster来计算鼠标对应的世界坐标——它会基于相机的投影矩阵,准确计算出鼠标射线与指定平面的交点,避免线性计算带来的畸变。
修改Mouse组件:
const Mouse: NextPage<MouseProps> = ({}) => { const [active, setActive] = useState(false) const { camera, mouse } = useThree() const { scale } = useSpring({ scale: active ? 0.7 : 1 }) useEffect(() => { if (active) { setTimeout(() => setActive(false), 200) } }, [active]) const ref = useRef<THREE.Mesh>(null); const raycaster = useRef(new THREE.Raycaster()); // 定义一个在相机前方的平面(这里选z=0的平面,你可以调整距离) const targetPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0); const intersectionPoint = new THREE.Vector3(); useFrame(() => { if (!ref.current) return; // 更新射线指向当前鼠标位置 raycaster.current.setFromCamera(mouse, camera); // 计算射线与平面的交点 raycaster.current.ray.intersectPlane(targetPlane, intersectionPoint); // 将球体移动到交点位置 ref.current.position.copy(intersectionPoint); ref.current.rotation.set(-intersectionPoint.y, intersectionPoint.x, 0); }) return ( <> <ambientLight /> <pointLight position={[10, 10, 10]} /> <animated.mesh scale={scale} onClick={() => setActive(!active)} ref={ref}> <sphereGeometry args={[0.15, 32, 32]} /> <meshStandardMaterial color="beige" transparent /> </animated.mesh> </> ) }
额外优化建议
- 如果你保留了旋转效果,可以适当降低旋转系数(比如
-y * 0.5),避免边缘区域旋转幅度过大看起来奇怪。 - 透视相机下,你也可以尝试降低FOV值(比如从60调到45),能一定程度减少边缘畸变,但不如前两种方案彻底。
内容的提问来源于stack exchange,提问作者Herbie Vine
相关产品推荐
相关产品推荐

