Three.jsとreact-three-fiberの実装を比較する

# はじめに

React ベースの Web アプリで Three.js を実装する方法として、ピュアな Three.js を使う方法と`react-three-fiber`というライブラリを使う方法が挙げられます。
今回はこの`react-three-fiber`と Three.js で実装した時を比べてみます。

# 実装する内容

3D モデルを読み込んで表示します。
アニメーションで上下に動かします。
デバッグ UI も用意します。

![Three実装.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/d34cb1aa-c574-8310-4a7e-44ac3a5e2b94.gif)


# Three.js

純粋な Three.js の実装を見たいので Vanilla で実装していきます。

# 初期構築

vite で構築します

```bash
yarn create vite

✔ Select a framework: › Vanilla
✔ Select a variant: › JavaScript
```

## package install

```bash
yarn add three lil-gui
```

## css の修正

画面幅いっぱいまで描画されるようにします。

```css:style.css
* {
  margin: 0;
  padding: 0;
}
```

## Three の実装

デバッグ UI には`lil-gui`を使います。
3D モデルはプロジェクト直下に `models`というディレクトリを作って置いています。
3D モデルは fbx ファイルを使います。

```js:main.js
import * as THREE from "three";
import * as dat from "lil-gui";
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

const gui = new dat.GUI();
const animationFolder = gui.addFolder("animation");
const ambientLightFolder = gui.addFolder("ambientLight");
const directionalLightFolder = gui.addFolder("directionalLight");

// canvasの設定
const canvas = document.getElementById("canvas");

// sceneの設定
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);

// cameraの設定
const camera = new THREE.PerspectiveCamera(
  35,
  innerWidth / innerHeight,
  0.1,
  2000
);
camera.position.z = 80;
camera.position.y = 7.2;
scene.add(camera);

// ライト
const ambientLight = new THREE.AmbientLight();
ambientLight.color.set(0xffffff);
ambientLight.intensity = 0.5;
scene.add(ambientLight);
ambientLightFolder
  .add(ambientLight, "intensity")
  .min(0)
  .max(1)
  .step(0.1)
  .name("ambIntensity");
ambientLightFolder.addColor(ambientLight, "color");

const directionalLight = new THREE.DirectionalLight(0xffffff, 0.4);
directionalLight.position.set(1, 0.55, 5);
scene.add(directionalLight);

directionalLightFolder
  .add(directionalLight, "intensity")
  .min(0)
  .max(1)
  .step(0.1)
  .name("dirIntensity");
directionalLightFolder.addColor(directionalLight, "color");

// オブジェクト
let drone;
const fbxLoader = new FBXLoader();
fbxLoader.setResourcePath("./models/Drone_Costum/Teturizer/");
fbxLoader.load("./models/Drone_Costum/Material/drone_costum.fbx", (obj) => {
  obj.scale.set(0.025, 0.025, 0.025);
  scene.add(obj);
  drone = obj;
});

// レンダリング設定
const renderer = new THREE.WebGLRenderer({
  canvas,
});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(devicePixelRatio);

//コントロール
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

// ブラウザのリサイズの操作
window.addEventListener("resize", () => {
  // サイズのアップデート
  sizes.width = window.innerWidth;
  sizes.height = window.innerHeight;

  // カメラのアップデート
  camera.aspect = sizes.width / sizes.height;
  camera.updateProjectionMatrix();

  // レンダラーのアップデート
  renderer.setSize(sizes.width, sizes.height);
  renderer.setPixelRatio(window.devicePixelRatio);
});

const clock = new THREE.Clock();

const animation = {
  isAnimation: true,
  frequency: 5,
  depth: 0.5,
};
animationFolder.add(animation, "isAnimation").name("animate");
animationFolder.add(animation, "frequency").min(0).max(10).step(0.1);
animationFolder.add(animation, "depth").min(0).max(10).step(0.1);

// アニメーション設定
const animate = () => {
  if (drone) {
    if (animation.isAnimation)
      drone.position.y =
        Math.sin(clock.getElapsedTime() * animation.frequency) *
        animation.depth;
  }
  renderer.render(scene, camera);
  window.requestAnimationFrame(animate);
};

animate();
```

## 実装結果

![Three実装.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/d34cb1aa-c574-8310-4a7e-44ac3a5e2b94.gif)

# react-three-fiber

React ベースで実装します。

# 初期構築

vite で構築します

```bash
yarn create vite

✔ Select a framework: › React
✔ Select a variant: › TypeScript
```

## package install

```bash
yarn add three leva @react-three/drei @react-three/fiber
yarn add -D @types/three
```

## css の修正

画面幅いっぱいまで描画されるようにします。
`index.css`の中身は全部削除します。

```css:App.css
body {
  margin: 0;
  padding: 0;
}
```

## react-three-fiber の実装

デバッグ UI には`leva`を使います。
3D モデルは`public`に `models`というディレクトリを作って置いています。
3D モデルは fbx ファイルを使います。

```tsx:App.tsx
import {
  Html,
  OrbitControls,
  PerspectiveCamera,
  useProgress,
} from "@react-three/drei";
import { Canvas, useFrame, useLoader } from "@react-three/fiber";
import { Suspense, useRef } from "react";
import "./App.css";
import * as THREE from "three";
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";
import { OrbitControls as OrbitControlImpl } from "three-stdlib";
import { Mesh } from "three";
import { useControls } from "leva";

const App = () => {
  const fbx = useLoader(
    FBXLoader,
    "/models/Drone_Costum/Material/drone_costum.fbx",
    (loader) => {
      loader.setResourcePath("/models/Drone_Costum/Teturizer/");
    }
  );
  const controlRef = useRef<OrbitControlImpl>({} as OrbitControlImpl);
  const model = useRef({} as Mesh);

  const { animate, frequency, depth } = useControls("animation", {
    animate: true,
    frequency: { value: 5, min: 0, max: 10, step: 0.1 },
    depth: { value: 0.5, min: 0, max: 10, step: 0.1 },
  });
  const { ambIntensity, ambColor } = useControls("ambientLight", {
    ambIntensity: { value: 0.5, min: 0, max: 1, step: 0.1 },
    ambColor: { value: "#fff", label: "color" },
  });
  const { dirIntensity, dirColor } = useControls("dirientLight", {
    dirIntensity: { value: 0.4, min: 0, max: 1, step: 0.1 },
    dirColor: { value: "#fff", label: "color" },
  });

  // アニメーション設定
  useFrame(({ clock }) => {
    if (animate)
      model.current.position.y =
        Math.sin(clock.getElapsedTime() * frequency) * depth;
  });

  return (
    <>
      {/* cameraの設定 */}
      <PerspectiveCamera
        makeDefault
        args={[35, innerWidth / innerHeight, 0.1, 2000]}
        position={[0, 7.2, 80]}
      />
      {/* ライト */}
      <ambientLight
        color={new THREE.Color(ambColor)}
        intensity={ambIntensity}
      />
      <directionalLight
        color={new THREE.Color(dirColor)}
        intensity={dirIntensity}
        position={[1, 0.55, 5]}
      />
      {/* オブジェクト */}
      <Suspense fallback={<Loading />}>
        <primitive object={fbx} scale={[0.025, 0.025, 0.025]} ref={model} />
      </Suspense>
      {/* コントロール */}
      <OrbitControls makeDefault ref={controlRef} />
    </>
  );
};

export default () => (
  // canvasの設定 scene・レンダリング設定込み
  <Canvas
    onCreated={({ gl, scene }) => {
      scene.background = new THREE.Color(0xffffff);
    }}
    flat
    style={{
      height: innerHeight,
      width: innerWidth,
    }}
  >
    <App />
  </Canvas>
);

const Loading = () => {
  const { progress } = useProgress();
  return <Html center>{progress} % loaded</Html>;
};
```

## 実装結果

![r3f実装.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/3392a2f7-27be-3333-0270-cd3f799e8813.gif)


# まとめ

react-three-fiber ではシーン設定やカメラ設定など、Three.js の実装の中で面倒な部分をライブラリの内部でやってくれます。
よって、そのまま Three.js で実装するよりコード量が少なくなります。
また、React ライクな記述方法になるので React で 3D 描画を実装する際は積極的に取り入れていきたいですね。

| 比較項目          | Three.js                                        | react-three-fiber                 |
| ----------------- | ----------------------------------------------- | --------------------------------- |
| コード量          | -                                               | (Three.jsと比較して)少なくなる                        |
| デバッグ UI       | lil-gui                                         | leva                              |
| アニメーション    | requestAnimationFrame の中で実装                | useFrame の中で実装               |
| 3D モデルのロード | FBXLoader + 引数内の関数で scene にモデルを追加 | useLoader + suspense で遅延ロード |

# 参考

[Three.js 公式](https://threejs.org/)
[react-three-fiber 公式 Doc](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction)