🐛

React Three FiberでOrbitControlsのカメラを操作できない問題への対処

2021/11/24に公開

ハマったのでメモ。
React Three Fiber を使って React で Three.js を使用している場合にうまくカメラを同期させないと、useEffect とかでカメラのパラメーターが操作できなくてハマりましたという話。

最初のコード

以下のソースだと、うまく OrbitControls がうまく作動しない。
useFrameとかで camera の ID を見ると、Drei.PerspectiveCameraOrbitControls.objectの ID と異なっていることがわかる。
結果としてコンポーネントに渡している cameraState によるカメラ操作が適用されない。

const CustomOrbitControls: React.FC<Props> = ({ cameraState }) => {
  const {
    gl: { domElement },
  } = useThree();

  const ref = useRef<OrbitControlsImpl>(null);
  const cameraRef = useRef<PerspectiveCamera>(null);

  useLayoutEffect(() => {
    // カメラの初期ポジション
    if (cameraRef && cameraRef.current) {
      cameraRef.current.position.set(50, 100, 300);
    }
  }, []);

  useEffect(() => {
    if (cameraRef && cameraRef.current) {
      // 親からもらったstateでカメラを操作したい
      cameraRef.current.fov = cameraState.fov;
    }
  }, [cameraState]);

  return (
    <>
      <Drei.PerspectiveCamera ref={cameraRef} {...cameraState} />
      <OrbitControls
        ref={ref}
        args={[cameraRef.current, domElement]}
        camera={cameraRef.current}
      />
    </>
  );
};
export default CustomOrbitControls;

修正後のコード

Drei.PerspectiveCameramakeDefaultをつけ、default のカメラを同期する。
drei コンポーネントは内部的にuseThreeを使っており、default のカメラを制御できないと操作できなくて死ぬ。

const CustomOrbitControls: React.FC<Props> = ({ cameraState }) => {
  const {
    gl: { domElement },
  } = useThree();

  const ref = useRef<OrbitControlsImpl>(null);
  const cameraRef = useRef<PerspectiveCamera>(null);

  useLayoutEffect(() => {
    if (cameraRef && cameraRef.current) {
      cameraRef.current.position.set(50, 100, 300);
    }
  }, []);

  useEffect(() => {
    if (cameraRef && cameraRef.current) {
      cameraRef.current.fov = cameraState.fov;
    }
  }, [cameraState]);

  return (
    <>
      {/* makeDefaultをつける!! */}
      <Drei.PerspectiveCamera makeDefault ref={cameraRef} {...cameraState} />
      <OrbitControls
        ref={ref}
        args={[cameraRef.current, domElement]}
        camera={cameraRef.current}
      />
    </>
  );
};
export default CustomOrbitControls;

まとめ

Three.js はもともと副作用のある処理が多く、React で利用する際はuseRefを使いこなせないとかなり難易度が高い。

追記(2021-11-25)

drei のコミッターの方にモット短く書ける方法をアドバイスしてもらいました。

const CustomOrbitControls: React.FC<Props> = ({ cameraState }) => {
  const ref = useRef<OrbitControlsImpl>(null);
  const cameraRef = useRef<PerspectiveCamera>(null);

  useLayoutEffect(() => {
    if (cameraRef && cameraRef.current) {
      cameraRef.current.position.set(50, 100, 300);
    }
  }, []);

  useEffect(() => {
    if (cameraRef && cameraRef.current) {
      cameraRef.current.fov = cameraState.fov;
    }
  }, [cameraState]);

  return (
    <>
      {/* makeDefaultをつける!! */}
      <Drei.PerspectiveCamera makeDefault ref={cameraRef} {...cameraState} />
      <OrbitControls ref={ref} makeDefault />
    </>
  );
};
export default CustomOrbitControls;

Discussion