javascriptreactjsthree.jsreact-three-fiberreact-three-drei

Allow complete rotation of model in Three with React


I just started with 'three', '@react-three/drei' and '@react-three/fiber' to help me visualize jaw/teeth in 3D. I the result is what I expected except on 2 point, as display on the video, I am stuck at one point in one axis for the rotation. and the 2nd point is that the initial position is not facing the teeth like I expect.

I added the axe helper to help me visualize what I'm doing and I'm afraid that I'm stuck because of my lack of spatial awareness regarding this project.

This is a GIF of the initial position and the part where I'm stuck rotating the model.

enter image description here

And here is my current code:

/* eslint-disable */
import React, { useState, useRef, useEffect, useMemo } from 'react';
// libraries
import { Canvas, useThree, useLoader } from '@react-three/fiber';
import { OrbitControls, PerspectiveCamera } from '@react-three/drei';
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
import * as THREE from 'three';
// mui
import { Button, Stack } from '@mui/material';

// ----------------------------------------------------------------------

const STLModel = ({ geometry }) => {
  useEffect(() => {
    if (geometry) {
      geometry.computeVertexNormals();
    }
  }, [geometry]);

  return (
    <mesh geometry={geometry} receiveShadow castShadow>
      <meshPhongMaterial
        color="#078DEE"
        specular="#ffffff"
        shininess={100}
        side={THREE.DoubleSide}
        receiveShadow 
        castShadow
      />
    </mesh>
  );
};

// ----------------------------------------------------------------------

const Scene = ({ geometries, currentFileIndex, cameraPosition, onCameraChange }) => {
  const controlsRef = useRef();
  const { camera } = useThree();

  useEffect(() => {
    if (controlsRef.current) {
      controlsRef.current.addEventListener('change', () => {
        onCameraChange({
          position: camera.position.toArray(),
          target: controlsRef.current.target.toArray(),
        });
      });
      // I Removed this two line to allow full rotation, but it did not work
      // controlsRef.current.maxPolarAngle = Math.PI;
      // controlsRef.current.minPolarAngle = 0;
      controlsRef.current.enablePan = true;
    }
  }, [camera, onCameraChange]);

  useEffect(() => {
    if (cameraPosition && controlsRef.current) {
      camera.position.fromArray(cameraPosition.position);
      controlsRef.current.target.fromArray(cameraPosition.target);
      controlsRef.current.update();
    } else {
      camera.position.set(0, 0, 100);
      controlsRef.current.target.set(0, 0, 0);
      controlsRef.current.update();
    }
  }, [camera, cameraPosition]);

  return (
    <>
      <PerspectiveCamera makeDefault position={[0, 0, 100]} />
      <STLModel geometry={geometries[currentFileIndex]} />
      <OrbitControls ref={controlsRef} />
      <ambientLight intensity={0.5} />
      <pointLight position={[10, 10, 10]} intensity={0.8} />
      <pointLight position={[-10, -10, -10]} intensity={0.5} />
      <directionalLight position={[0, 0, 5]} intensity={0.5} />
      <axesHelper scale={[10, 10, 10]} />
    </>
  );
};

// ----------------------------------------------------------------------

const TreatmentPlan = () => {
  const [currentFileIndex, setCurrentFileIndex] = useState(0);
  const [cameraPosition, setCameraPosition] = useState(null);

  const url = "https://myurl/";
  const fileUrls = useMemo(() => Array.from({ length: 15 }, (_, i) => `${url}file${i + 1}.stl`), [url]);

  const geometries = useLoader(STLLoader, fileUrls);

  const handleNext = () => {
    setCurrentFileIndex((prevIndex) => (prevIndex + 1) % geometries.length);
  };

  const handlePrevious = () => {
    setCurrentFileIndex((prevIndex) => (prevIndex - 1 + geometries.length) % geometries.length);
  };

  return (
    <div>
      <Stack direction="row" spacing={2} mb={2}>
        <Button variant="contained" onClick={handlePrevious} disabled={geometries.length === 0}>Previous</Button>
        <Button variant="contained" onClick={handleNext} disabled={geometries.length === 0}>Next</Button>
      </Stack>
      {geometries.length > 0 && (
        <div style={{ width: '50vw', height: '50vh' }}>
          <Canvas>
            <Scene
              geometries={geometries}
              currentFileIndex={currentFileIndex}
              cameraPosition={cameraPosition}
              onCameraChange={setCameraPosition}
            />
          </Canvas>
        </div>
      )}
    </div>
  );
};

export default TreatmentPlan;

Thank you in advance for any help/advice


Solution

  • The problem you are facing does not result from your code, but only from the limitations and philosophy of OrbitControls. OC maintains a constant upward vector, hence the limitation. What you write about, or specifically what you expect, can be found in TrackballControls, where this vector is not constant. Also, as your code shows, it doesn't matter whether you remove the lines you commented out, because they are the default values.

    TrackballControls is similar to OrbitControls. However, it does not maintain a constant camera up vector. That means if the camera orbits over the “north” and “south” poles, it does not flip to stay "right side up".

    In this thread, pailhead explained this issue quite... graphically.

    https://discourse.threejs.org/t/solved-orbitcontrols-max-polar-angle-infinity/2629

    Take a look on difference

    OrbitControls

    <script type="importmap">
      {
    "imports": {
      "three": "https://unpkg.com/three@0.166.0/build/three.module.js",
      "three/addons/": "https://unpkg.com/three@0.166.0/examples/jsm/"
    }
      }
    </script>
    
    <script type="module">
    import * as THREE from "three";
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
    
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
    
    const planeGeometry = new THREE.PlaneGeometry(5, 5);
    const planeMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00, side: THREE.DoubleSide });
    const plane = new THREE.Mesh(planeGeometry, planeMaterial);
    scene.add(plane);
    
    camera.position.z = 5;
    
    const controls = new OrbitControls(camera, renderer.domElement);
    
    function animate() {
      requestAnimationFrame(animate);
      controls.update();
      renderer.render(scene, camera);
    }
    
    animate();
    
    </script>

    TrackballControls

    <script type="importmap">
      {
    "imports": {
      "three": "https://unpkg.com/three@0.166.0/build/three.module.js",
      "three/addons/": "https://unpkg.com/three@0.166.0/examples/jsm/"
    }
      }
    </script>
    
    <script type="module">
    import * as THREE from "three";
    
    import { TrackballControls } from 'three/addons/controls/TrackballControls.js';
    
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
    
    const planeGeometry = new THREE.PlaneGeometry(5, 5);
    const planeMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00, side: THREE.DoubleSide });
    const plane = new THREE.Mesh(planeGeometry, planeMaterial);
    scene.add(plane);
    
    camera.position.z = 5;
    
    const controls = new TrackballControls(camera, renderer.domElement);
    
    function animate() {
      requestAnimationFrame(animate);
      controls.update();
      renderer.render(scene, camera);
    }
    
    animate();
    
    </script>

    Just try switch OrbitControls on TrackballControls, maybe you’ll get what you expect.

    import { TrackballControls } from '@react-three/drei';
    
    //…
    <TrackballControls />