React-three-fiber unable to rotate camera

    <Canvas
      invalidateFrameloop
      colorManagement
      shadowMap
      onCreated={({ camera, gl, scene }) => {
        gl.setPixelRatio(window.devicePixelRatio);
        gl.outputEncoding = sRGBEncoding;
        gl.physicallyCorrectLights = true;
        gl.shadowMap.enabled = true;
        gl.shadowMap.type = PCFSoftShadowMap;
        gl.toneMapping = ACESFilmicToneMapping;
        console.log({ camera });
        const yAxis = new Vector3(0, 1, 0);
        const xAxis = new Vector3(1, 0, 0);
        camera.setRotationFromAxisAngle(yAxis, 60);
        camera.lookAt([0, -50, 0]);
        // camera.rotateY(Math.PI);
        camera.updateProjectionMatrix();
      }}
    >
      <PerspectiveCamera
        makeDefault
        // rotation={[Math.PI, 0, 0]}
        fov={75}
        position={[240, -420, 240]}
        near={1}
        far={1000}
      ></PerspectiveCamera>
      <Suspense fallback="loading">
        <Model {...props} {...position} />
      </Suspense>
    </Canvas>

I have this above code but for some reason the camera is still looking at the model straight ahead. I want it to look at the model a little from the top. I tried adding rotation through props and through the onCreated function as well, but still no dice. Not sure what simple thing I am missing here.

May or may not be the exact cause - but you are mixing controlled and reactive behaviour (ie. first onCreate is called, then if at any point React rerenders your component and it’s children - it will reset the camera to the state defined by props.)

Consider either using useEffect together with useRef (referencing the imperatively controlled camera) in place of onCreate - or use props to adjust the camera rotation :thinking:

1 Like

I refactored the code to be like this, where camera is its own function component. With this the camera is set to center (inside) of the object. None of the values seems to have been applied through the useEffect

function SceneCamera() {
 const { camera } = useThree();

 useEffect(() => {
  camera.fov = 75;
  camera.near = 1;
  camera.far = 1000;
  camera.position.set([240, -420, 180]);
  camera.rotateY(60);
  camera.updateProjectionMatrix();
 }, []);
 return <PerspectiveCamera makeDefault></PerspectiveCamera>;
 }

 <Canvas
   invalidateFrameloop
   colorManagement
   shadowMap
   onCreated={({ gl }) => {
     gl.setPixelRatio(window.devicePixelRatio);
     gl.outputEncoding = sRGBEncoding;
     gl.physicallyCorrectLights = true;
     gl.shadowMap.enabled = true;
     gl.shadowMap.type = PCFSoftShadowMap;
     gl.toneMapping = ACESFilmicToneMapping;
   }}
 >
  <SceneCamera />
  <Suspense fallback="loading">
    <Model {...props} {...position} />
  </Suspense>
 </Canvas>
1 Like

The strange thing with rotation in props like this is that everything except rotation is applied. Whether I use it like this

rotation={[0, 0, Math.PI]}

or

rotation={[0, Math.PI, 0]}

or

rotateY={180}

It is all the same, no impact whatsoever :man_shrugging:

function SceneCamera() {

return (
 <PerspectiveCamera
  makeDefault
  rotation={[0, 0, Math.PI]}
  fov={75}
  position={[240, -420, 240]}
  near={1}
  far={1000}
  ></PerspectiveCamera>
);
}
1 Like

most likely you use controls (orbit, etc) ?

something is overwriting your camera like @mjurczyk said. the oncreate, or the rotation prop, all of this seems fine and will rotate the camera. tried it here:

function Test() {
  const camera = useThree(state => state.camera)
  useEffect(() => {
    camera.rotation.set(0, 0.5, 0)
    camera.updateProjectionMatrix()
  }, [])
  return null
}

<Canvas>
  <Test />

don’t think the updateProjectionMatrix is necessary, but it rotates

also tried the declarative cameras from drei:

<PerspectiveCamera makeDefault position={[0, 0, 10]} fov={50} rotation={[0, 0.4, 0]} />

same thing, rotates around the y axis.

wait, you are using invalidateframeloop, that means it will not render unless props have changed or you indicate that a change has occured by calling the “invalidate” function you get from either useThree or global imports. that is most likely the cause. invalidate is rendering on demand, it for usecases where you want to save battery, but you need to know how to use it. if you just mutate something deep inside threejs it’s not gonna know about this and trigger a frame. drei’s camera do this automatically. remove that line pls, “invalidateFrameloop”

btw, you also use outdated syntax:

ps.

import { Camera } from '@react-three/fiber'

<Canvas shadows frameloop='demand'>

it’s using srgb and tonemapping by default

Yes, I have orbit controls setup like so:

extend({ OrbitControls });
const Controls = props => {
 const { gl, camera } = useThree();
 const controls = useRef();

 useFrame(() => {
   if (controls.current) controls.current.update();
 });

 useEffect(() => {
  controls.current.enabled = false;
  camera.rotation.set(0, 0, 0.5);
  camera.updateProjectionMatrix();
  controls.current.enabled = true;
  controls.current.update();
 }, []);

 return <orbitControls ref={controls} args={[camera, gl.domElement]} {...props} />;
};

Note: I added useEffect inside my Controls function only now. First iteration was only camera object mutations. I have the second iteration shared up here, where I try to disable the controls, change camera settings and then re-enable them. This is how you do it in pure three.js. Like so in constructor:

this.controls = new OrbitControls(this.camera, this.element3D.current);

In gltfloader function:

this.controls.enabled = false;
this.controls.update();
this.camera.position.applyAxisAngle(yAxis, 0.2 * Math.PI);
this.camera.position.applyAxisAngle(xAxis, -0.05 * Math.PI);
this.camera.lookAt(root.position);
this.camera.updateProjectionMatrix();

// Enable orbit controls post camera rotation
this.controls.enabled = true;
this.controls.update();

I think with r3f’s route of applying props on every frame reveals this issue and that’s why I think this is unique to r3f implementation. With above threejs code, camera position changes on model load work swimmingly

useEffect fires after render (on screen), that means controls have already rendered out once before your effect is called. useLayoutEffect is called before render. if you want to apply something before it goes on screen that should be uLE.

I think with r3f’s route of applying props on every frame

props are only applied when you change them. it completely stays out of the frame rate. the only thing that runs every frame is what you put into useFrame.

The frameloop was added much later as I wanted to change model’s position right after loading. I used zustand to track code running once like this:

const [useStore] = create(set => ({
  ranOnce: 0,
  runOnce: () =>
    set(state => {
      if (state.ranOnce === 0) {
        return { ranOnce: 1 };
      }
      return { ranOnce: state.ranOnce };
    }),
}));

Inside model component generated by gltfjsx, I do the following:

const { ranOnce, runOnce } = useStore();

useFrame(state => {
    if (ranOnce) {
      invalidate();
    }
    if (!ranOnce) {
      groupRef.current.position.y = -50;
      runOnce();
    }
});

And the above works great to change position of the model. I tried adding this same logic to my SceneCamera function but I don’t have access to orbit controls object through useThree. Is there a way to do that?

useLayoutEffect inside Controls function had no impact unfortunately. Same behavior as earlier with useEffect