Three js 360 degree image annotation

283 Views Asked by At

I have a 360 degree panoramic image and i am displaying this image using three js. Now i wanted to display a marker on that 360 degree panoramic image (marker: just like the location marker on map). On zoom in or zoom out the 360 degree image should zomm in or zoom out but the marker mainatain its ideal constant location. The marker should not be zoom in or zoom out. For better understanding the what the actual beheviour i want you can look at the location marker on the google map. Here is the code of How i am displaying the 360 degree image using three js.

import React, { useRef, useEffect, useState } from 'react';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
import { EVENTS, TYPES, ThreeJsDrawing } from './threeJsDrawing';
import { setThreeJsData } from './mouseEvents';
import panImg from "../assets/shot.jpg";
import panImg2 from "../assets/shot2.jpg";
import panImg3 from "../assets/shot3.png";
import panImg4 from "../assets/shot4.png";

let scene, camera, controls, sphere, polygons = [], drawingTool,
  images = [
    {
      url: panImg,
      location: {
        x: 369794,
        y: 2212918,
        lat: 20.008508,
        lng: 73.755165

      }
    },
    {
      url: panImg2,
      location: {
        x: 369779,
        y: 2212922,
        lat: 20.008382,
        lng: 73.755117
      }
    },
    {
      url: panImg3,
      location: {
        x: 369769,
        y: 2212912,
        lat: 20.008251,
        lng: 73.755074
      }
    },
    {
      url: panImg4,
      location: {
        x: 369762,
        y: 2212895,
        lat: 20.008226,
        lng: 73.755165
      }
    }
  ]
const PanoramaViewer = () => {
  const [activeImage, setActiveImage] = useState(null)
  const [renderer, setRenderer] = useState(null)

  useEffect(() => {
    if (activeImage || activeImage === 0) loadImage(activeImage)

  }, [activeImage])

  const [loading, setLoading] = useState(true)

  const containerRef = useRef(null);


  const loadImage = (index, initCall) => {
    const img = images[index]
    const [x, y] = [0, 0]
    // Load the panorama texture
    const loader = new THREE.TextureLoader();
    const texture = loader.load(img.url, (texture) => {
      // Texture loaded successfully
      // Set the texture wrapping and flipping options
      texture.wrapS = THREE.RepeatWrapping;
      // texture.repeat.x = -1;
      sphere.material.map = texture;
      sphere.material.needsUpdate = true; // Ensure material updates
      // controls.object.rotation.set(-2.3553, 1.57, 2.3553)
      // Optional: Clean up the previous texture resources
      sphere.material.map.dispose();
      setLoading(false)
    });


    if (initCall) {
      // Create a sphere geometry
      const geometry = new THREE.SphereGeometry(5, 32, 32);
      geometry.scale(-1, 1, 1);

      // Create a material with the panorama texture
      const material = new THREE.MeshBasicMaterial({ map: texture });

      // Create a mesh with the geometry and material
      sphere = new THREE.Mesh(geometry, material);
      sphere.position.set(x, y, 0);
      // Add the mesh to the scene
      scene.add(sphere);

      camera.position.set(x, y, 0.1);
      controls.target.set(x, y, 0)
      controls.update();
    }
  }

  const nextImage = () => {

    if (activeImage + 1 < images.length) {
      setLoading(true)
      setActiveImage(activeImage + 1)
    }

  }

  const prevImage = () => {

    if (activeImage - 1 >= 0) {
      setLoading(true)
      setActiveImage(activeImage - 1)
    }

  }



  const initThreeJs = () => {
    // Create a scene
    scene = new THREE.Scene();

    // Create a camera
    camera = new THREE.PerspectiveCamera(
      75,
      containerRef.current.clientWidth / containerRef.current.clientHeight,
      1,
      1000
    );

    // Create a renderer
    const renderer = new THREE.WebGLRenderer();
    setRenderer(renderer)
    renderer.setSize(containerRef.current.clientWidth, containerRef.current.clientHeight);
    containerRef.current.appendChild(renderer.domElement);

    // Add orbit controls for rotation
    controls = new OrbitControls(camera, renderer.domElement);
    controls.enableZoom = true;
    controls.rotateSpeed = -0.25; // Adjust the value as needed
    loadImage(0, true)
    setThreeJsData(renderer, scene, camera, controls)



    // Render the scene
    const animate = () => {
      requestAnimationFrame(animate);
      renderer.render(scene, camera);
    };
    animate();


    // Clean up
    return () => {
      // containerRef.current.removeEventListener('click', handleClick);
      containerRef.current.removeChild(renderer.domElement);
      controls.dispose();
    };
  }
  useEffect(initThreeJs, []);

  const Loader = () => {
    return loading ? <div style={{ position: 'absolute', width: "100%", height: "100%", display: "flex", justifyContent: "center", alignItems: "center", color: 'white' }}>Loading...</div> : <></>
  }

  const markIssue = (type) => {
    if (drawingTool) {
      console.log("if called", drawingTool)
      drawingTool.enableDrawing(type)
      drawingTool.on(EVENTS.GEOMETRY_ADDED, (geometry => {
        polygons.push(geometry)
      }))
    }
    else {
      //activate drawing tool
      console.log("else called")
      console.log("scene:", scene, "camera:", camera, "sphere:", [sphere])
      drawingTool = new ThreeJsDrawing(scene, camera, [sphere], false)
      drawingTool.enableDrawing(type)
      drawingTool.on(EVENTS.GEOMETRY_ADDED, (geometry => {
        console.log("geometry", geometry)
        polygons.push(geometry)
      }))
      console.log("polygon arrayyy:", polygons)
    }

  }

  return <>
    <Loader />
    <div style={{ position: "absolute", display: "flex", width: "100%", height: "40px", alignItems: 'center', justifyContent: 'space-around', background: "#00000082", color: 'white' }}>
      <div onClick={prevImage} style={{ cursor: "pointer" }}>prevImage</div>
      <div onClick={nextImage} style={{ cursor: "pointer" }}>nextImage</div>
    </div>
    <div style={{ position: "absolute", display: "flex", width: "100%", height: "40px", bottom: 0, alignItems: 'center', justifyContent: 'space-around', background: "#00000082", color: 'white', cursor: "pointer" }}>
      <div onClick={() => {
        markIssue("Polygon");
        console.log("polygon click")
      }}>
        polygon
      </div>
      <div onClick={() => {
        markIssue("Rectangle");
        console.log("Rectangle click")
      }}>
        Rectangle
      </div>
      <div onClick={() => {
        markIssue("Hotspot");
        console.log("Hotspot clcik")
      }}>
        Hotspot
      </div>
    </div>
    <div ref={containerRef} style={{ width: '100%', height: '100vh' }} />
  </>;
};

export default PanoramaViewer;

I tried using sphere, css2Dobject, texture as a marker but it wont work.

1

There are 1 best solutions below

2
On

To display a marker on a 360-degree panoramic image using Three.js and ensure that the marker maintains a constant position when you zoom in or out, you can follow these steps:

  1. Create a marker using Three.js geometry and material.
  2. Set the marker's position relative to the camera's position.
  3. Update the marker's position when the camera zooms in or out.

Here's an updated version of your code with the necessary modifications:

import React, { useRef, useEffect, useState } from 'react';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

let scene, camera, controls, sphere, marker, polygons = [];

const PanoramaViewer = () => {
  // ... (previous code remains the same)

  const initThreeJs = () => {
    // ... (previous code remains the same)

    // Create a marker geometry and material
    const markerGeometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
    const markerMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
    marker = new THREE.Mesh(markerGeometry, markerMaterial);

    // Add the marker to the scene
    scene.add(marker);

    // Render the scene
    const animate = () => {
      requestAnimationFrame(animate);
      renderer.render(scene, camera);

      // Update the marker's position relative to the camera
      const markerPosition = new THREE.Vector3();
      markerPosition.set(0, 0, -5); // Adjust the position as needed
      markerPosition.applyMatrix4(camera.matrixWorld);
      marker.position.copy(markerPosition);
    };
    animate();

    // Clean up
    return () => {
      containerRef.current.removeChild(renderer.domElement);
      controls.dispose();
    };
  };

  // ... (rest of the code remains the same)

  return (
    <>
      <Loader />
      {/* ... (previous JSX code remains the same) */}
    </>
  );
};

export default PanoramaViewer;

In this code, we create a simple cube marker and add it to the scene. We update the marker's position relative to the camera's position in the animation loop. This ensures that the marker maintains a constant position regardless of zooming in or out.

You can customize the marker's geometry and material to suit your needs. Make sure to adjust the marker's position and size as necessary.