likes
comments
collection
share

全景看图前端3D组件三部曲

作者站长头像
站长
· 阅读数 2

在实际场景中,实现全景看图功能通常会结合使用React和Three.js等3D库。以下是一个简化版的基于React + TypeScript创建一个基础全景查看器组件的例子:

1.基础版

// 首先确保安装了必要的依赖:
// npm install three @types/three react-three-fiber

import React, { Suspense } from 'react';
import { Canvas, useLoader } from 'react-three-fiber';
import { TextureLoader } from 'three';

interface PanoramaProps {
  src: string; // 全景图片资源路径
}

const Panorama: React.FC<PanoramaProps> = ({ src }) => {
  const texture = useLoader(TextureLoader, src);

  return (
    <Canvas>
      <ambientLight />
      <pointLight position={[10, 10, 10]} />
      <mesh rotation={[-Math.PI / 2, 0, 0]}>
        <sphereGeometry args={[500, 60, 40]} />
        <meshBasicMaterial map={texture} />
      </mesh>
    </Canvas>
  );
};

export default function App() {
  return (
    <div style={{ height: '100vh', width: '100%' }}>
      <Suspense fallback={<div>Loading...</div>}>
        <Panorama src="/path/to/your/panorama.jpg" />
      </Suspense>
    </div>
  );
}

上述代码创建了一个基本的全景查看器组件Panorama,它使用Three.js中的纹理加载器加载全景图片,并将其映射到一个球体上以模拟360度全景视图效果。同时,添加了环境光和点光源以提供光照。

为了实现交互式的全景查看,你可能还需要处理鼠标或触摸事件来改变视角。这可以通过监听Canvas组件上的onPointerMove事件并调整相机方向来实现,但此处并未包含具体的交互逻辑部分。

注意:在实际应用中,为了更好的全景浏览体验,可能需要更复杂的全景图像投影方式(如equirectangular投影)以及平滑的视角变换动画等。

2.优化版

为了实现全景图像的平滑浏览和交互控制,我们可以在上述组件中添加更多的功能。以下是一个包含了基本视角拖拽控制的增强版全景查看器组件:

import React, { useState, useRef } from 'react';
import { Canvas, useLoader, useFrame } from 'react-three-fiber';
import { TextureLoader, PerspectiveCamera } from 'three';

interface PanoramaProps {
  src: string;
}

const Panorama: React.FC<PanoramaProps> = ({ src }) => {
  const texture = useLoader(TextureLoader, src);
  const [mouseX, setMouseX] = useState(0);
  const [mouseY, setMouseY] = useState(0);
  const [isDragging, setIsDragging] = useState(false);
  const canvasRef = useRef<HTMLCanvasElement>(null);

  // 获取到canvas元素上的鼠标事件
  const handleMouseMove = (event: React.MouseEvent<HTMLCanvasElement>) => {
    if (isDragging) {
      setMouseX(event.clientX - canvasRef.current!.offsetLeft);
      setMouseY(event.clientY - canvasRef.current!.offsetTop);
    }
  };

  const handleMouseDown = () => setIsDragging(true);
  const handleMouseUp = () => setIsDragging(false);

  // 在每一帧更新相机角度以响应鼠标移动
  useFrame(() => {
    if (isDragging) {
      const camera = canvasRef.current!.getBoundingClientRect();
      const x = ((mouseX / camera.width) * 2 - 1) * Math.PI;
      const y = ((mouseY / camera.height) * 2 - 1) * Math.PI;

      // 根据鼠标的相对位置调整相机方向
      // 注意:这里假设已经有一个初始设置好的camera(如在Canvas组件内设置)
      const cameraObject = canvasRef.current!.parentElement!.getObjectByName('camera') as PerspectiveCamera;
      cameraObject.position.set(0, 0, 500);
      cameraObject.lookAt(0, 0, 0);
      cameraObject.rotation.x = x;
      cameraObject.rotation.y = y;
    }
  });

  return (
    <Canvas
      ref={canvasRef}
      onPointerMove={handleMouseMove}
      onPointerDown={handleMouseDown}
      onPointerUp={handleMouseUp}
      camera={{ position: [0, 0, 500], fov: 75 }}
    >
      <ambientLight />
      <pointLight position={[10, 10, 10]} />
      <mesh rotation={[-Math.PI / 2, 0, 0]}>
        <sphereGeometry args={[500, 60, 40]} />
        <meshBasicMaterial map={texture} />
      </mesh>
      {/* 添加一个隐藏的物体作为相机的目标点 */}
      <mesh name="cameraTarget" position={[0, 0, 0]} visible={false} />
    </Canvas>
  );
};

export default function App() {
  return (
    <div style={{ height: '100vh', width: '100%' }}>
      <Panorama src="/path/to/your/panorama.jpg" />
    </div>
  );
}

在这个版本中,我们添加了对鼠标拖拽事件的监听,并在每一帧更新时根据鼠标的位置旋转相机,从而实现了全景视图的拖动交互。同时,在Canvas中指定了一个名为cameraTarget的隐藏对象作为相机的注视目标。

请注意,实际应用中可能需要针对移动端触摸事件进行优化,并且在某些情况下可能还需要更复杂的摄像机控制逻辑来处理3D空间中的旋转和平移。此外,这里的示例使用了透视相机(PerspectiveCamera),它更适合全景效果,但需确保设置了合适的视野(fov)值。

3.完善版

为了进一步优化全景查看器,我们还可以加入鼠标滚轮缩放、双击重置视角等功能,并确保在移动设备上支持触摸操作。以下是一个更完整的示例:

import React, { useState, useRef } from 'react';
import { Canvas, useLoader, useFrame, useThree } from 'react-three-fiber';
import { TextureLoader, PerspectiveCamera, Vector2 } from 'three';

interface PanoramaProps {
  src: string;
}

const Panorama: React.FC<PanoramaProps> = ({ src }) => {
  const texture = useLoader(TextureLoader, src);
  const [isDragging, setIsDragging] = useState(false);
  const [startPosition, setStartPosition] = useState(new Vector2());
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const { camera } = useThree();

  // 鼠标/触摸事件处理
  const handleMouseDown = (event: React.MouseEvent<HTMLCanvasElement>) => {
    event.preventDefault();
    setIsDragging(true);
    setStartPosition(new Vector2(event.clientX, event.clientY));
  };

  const handleMouseMove = (event: React.MouseEvent<HTMLCanvasElement>) => {
    if (isDragging) {
      const movementX = event.clientX - startPosition.x;
      const movementY = event.clientY - startPosition.y;

      camera.position.x += movementX * 0.01;
      camera.position.y -= movementY * 0.01;
      camera.lookAt(0, 0, 0);

      setStartPosition(new Vector2(event.clientX, event.clientY));
    }
  };

  const handleMouseUp = () => setIsDragging(false);
  const handleMouseLeave = () => setIsDragging(false);

  const handleWheel = (event: React.WheelEvent<HTMLCanvasElement>) => {
    event.preventDefault();
    const scaleDelta = event.deltaY > 0 ? 0.95 : 1.05;
    camera.position.z *= scaleDelta;
  };

  const handleDoubleClick = () => {
    camera.position.set(0, 0, 500);
    camera.lookAt(0, 0, 0);
  };

  useEffect(() => {
    // 添加移动端触摸事件支持
    const touchStartHandler = (event: TouchEvent) => {
      setIsDragging(true);
      const touch = event.touches[0];
      setStartPosition(new Vector2(touch.clientX, touch.clientY));
    };
    
    const touchMoveHandler = (event: TouchEvent) => {
      if (isDragging) {
        const touch = event.touches[0];
        const movementX = touch.clientX - startPosition.x;
        const movementY = touch.clientY - startPosition.y;
        
        camera.position.x += movementX * 0.01;
        camera.position.y -= movementY * 0.01;
        camera.lookAt(0, 0, 0);
        
        setStartPosition(new Vector2(touch.clientX, touch.clientY));
      }
    };
    
    const touchEndHandler = () => setIsDragging(false);
    
    canvasRef.current?.addEventListener('mousedown', handleMouseDown);
    canvasRef.current?.addEventListener('mousemove', handleMouseMove);
    canvasRef.current?.addEventListener('mouseup', handleMouseUp);
    canvasRef.current?.addEventListener('mouseleave', handleMouseLeave);
    canvasRef.current?.addEventListener('wheel', handleWheel);
    canvasRef.current?.addEventListener('dblclick', handleDoubleClick);
    canvasRef.current?.addEventListener('touchstart', touchStartHandler);
    canvasRef.current?.addEventListener('touchmove', touchMoveHandler);
    canvasRef.current?.addEventListener('touchend', touchEndHandler);

    return () => {
      canvasRef.current?.removeEventListener('mousedown', handleMouseDown);
      canvasRef.current?.removeEventListener('mousemove', handleMouseMove);
      canvasRef.current?.removeEventListener('mouseup', handleMouseUp);
      canvasRef.current?.removeEventListener('mouseleave', handleMouseLeave);
      canvasRef.current?.removeEventListener('wheel', handleWheel);
      canvasRef.current?.removeEventListener('dblclick', handleDoubleClick);
      canvasRef.current?.removeEventListener('touchstart', touchStartHandler);
      canvasRef.current?.removeEventListener('touchmove', touchMoveHandler);
      canvasRef.current?.removeEventListener('touchend', touchEndHandler);
    };
  }, [camera]);

  return (
    <Canvas
      ref={canvasRef}
      camera={{ position: [0, 0, 500], fov: 75 }}
    >
      <ambientLight />
      <pointLight position={[10, 10, 10]} />
      <mesh rotation={[-Math.PI / 2, 0, 0]}>
        <sphereGeometry args={[500, 60, 40]} />
        <meshBasicMaterial map={texture} />
      </mesh>
    </Canvas>
  );
};

export default function App() {
  return (
    <div style={{ height: '100vh', width: '100%' }}>
      <Panorama src="/path/to/your/panorama.jpg" />
    </div>
  );
}

在这个版本中,我们添加了鼠标滚轮缩放功能(handleWheel)、双击重置视角功能(handleDoubleClick)以及对移动设备触屏的兼容(handleTouchStart、handleTouchMove 和 handleTouchEnd)。同时使用了useEffect来正确地添加和移除事件监听器,以防止内存泄漏。