React Three Fiberに入門してみる

`react`を使った場合に3Dの表現ってどうやるんだろ？って気になり調べてみたら[react-three-fiber](https://github.com/pmndrs/react-three-fiber)という`Three.js`の`react`版みたいな良さげなライブラリがあったので触ってみたいと思います。

https://docs.pmnd.rs/react-three-fiber/getting-started/introduction

ドキュメントの[Examples](https://docs.pmnd.rs/react-three-fiber/getting-started/examples)を初めて見た時は結構テンションが上がりました。笑

また、[エコシステム](https://github.com/pmndrs/react-three-fiber#ecosystem)もあるのでそれらを組み合わせつつ、`react-three-fiber`に慣れれば`Three.js`で書くより開発速度があがるかもしれません。（Three.jsやWebGLの知識は必要なので自分のスキルに合わせて勉強していければと思います。。。）

## 開発環境を準備する
ビルドツールに関してはお好みのものをご使用いただければと思いますので割愛させていただきます。
※Next.jsの場合は[react-three-fiberの公式ドキュメント](https://docs.pmnd.rs/react-three-fiber/getting-started/installation#next-js)も参考にするといいと思います。

### react-three-fiberをインストール
本題の`react-three-fiber`と`Three.js`をインストールします。

```
npm i @react-three/fiber three
```
`TypeScript`ご使用の場合は`Three.js`の型定義ファイルもインストールします。

```
npm i @react-three/fiber three @types/three 
```

### react-three-fiberのエコシステムをインストール
- [r3f-perf](https://github.com/utsuboco/r3f-perf) - パフォーマンスモニタ FPSなどの確認
- [leva](https://github.com/pmndrs/leva) - dat.gui的なツール
- [@react-three/drei](https://github.com/pmndrs/drei) - react-three-fiberのヘルパー

```
npm i r3f-perf leva @react-three/drei
```

## 動かしてみる
Three.jsに用意されているBoxGeometryの形状とlevaでカラーを変更できるように試してみる。

```tsx:App.tsx
import { OrbitControls } from '@react-three/drei';
import { Canvas, useFrame } from '@react-three/fiber';
import { useControls } from 'leva';
import { Perf } from 'r3f-perf';
import { useEffect, useRef, useState, VFC } from 'react';
import type { Mesh } from 'three';

type BoxProps = {
  color?: ColorRepresentation;
};

const Box: VFC<BoxProps> = ({ color = '#0066ff' }) => {
  const mesh = useRef<Mesh>();

  useFrame(() => {
    if (mesh.current) {
      mesh.current.rotation.x = mesh.current.rotation.y += 0.01;
    }
  });

  return (
    <mesh ref={mesh}>
      <boxBufferGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color={color} />
    </mesh>
  );
};

function App() {
  const [width, setWidth] = useState(0);
  const [height, setHeight] = useState(0);
  const [dpr, setDpr] = useState(0);
  const { color } = useControls({ color: '#ff0000' });

  useEffect(() => {
    setWidth(window.innerWidth);
    setHeight(window.innerHeight);
    setDpr(Math.min(window.devicePixelRatio, 2));

    window.addEventListener('resize', () => {
      setWidth(window.innerWidth);
      setHeight(window.innerHeight);
      setDpr(Math.min(window.devicePixelRatio, 2));
    });
  }, []);

  const aspect = width / height;

  return (
    <Canvas
      dpr={dpr}
      camera={{ aspect, near: 0.1, far: 100, position: [0, 0, 4] }}>
      <ambientLight />
      <pointLight position={[10, 10, 0]} />
      <Box color={color} />
      <OrbitControls />
      <Perf position={'bottom-right'} showGraph={false} />
    </Canvas>
  );
}

export default App;
```

## シェーダーも動くか試してみる
シェーダーも問題なく動くか試してみます。
先ほどの`App.tsx`のBoxのマテリアルをシェダーにてカラー変更を行えるよう切り替えてみます。

```tsx:App.tsx
import { OrbitControls } from '@react-three/drei';
import { Canvas, useFrame } from '@react-three/fiber';
import { useControls } from 'leva';
import { Perf } from 'r3f-perf';
import { useEffect, useMemo, useRef, useState, VFC } from 'react';
import fragmentShader from 'shaders/fragment.glsl?raw';
import vertexShader from 'shaders/vertex.glsl?raw';
import { Color, ColorRepresentation, Mesh, ShaderMaterial } from 'three';

type BoxProps = {
  color?: ColorRepresentation;
};

const Box: VFC<BoxProps> = ({ color = '#0066ff' }) => {
  const mesh = useRef<Mesh>();
  const params = useMemo(
    () => ({
      uniforms: {
        uColor: { value: new Color(color) },
      },
      fragmentShader,
      vertexShader,
    }),
    []
  );

  useFrame(() => {
    if (mesh.current) {
      mesh.current.rotation.x = mesh.current.rotation.y += 0.01;
      (mesh.current.material as ShaderMaterial).uniforms.uColor.value =
        new Color(color);
    }
  });

  return (
    <mesh ref={mesh}>
      <boxBufferGeometry args={[1, 1, 1]} />
      <shaderMaterial {...params} />
    </mesh>
  );
};

function App() {
  const [width, setWidth] = useState(0);
  const [height, setHeight] = useState(0);
  const [dpr, setDpr] = useState(0);
  const { color } = useControls({ color: '#ff0000' });

  useEffect(() => {
    setWidth(window.innerWidth);
    setHeight(window.innerHeight);
    setDpr(Math.min(window.devicePixelRatio, 2));

    window.addEventListener('resize', () => {
      setWidth(window.innerWidth);
      setHeight(window.innerHeight);
      setDpr(Math.min(window.devicePixelRatio, 2));
    });
  }, []);

  const aspect = width / height;

  return (
    <Canvas
      dpr={dpr}
      camera={{ aspect, near: 0.1, far: 100, position: [0, 0, 4] }}>
      <ambientLight />
      <pointLight position={[10, 10, 0]} />
      <Box color={color} />
      <OrbitControls />
      <Perf position={'bottom-right'} showGraph={false} />
    </Canvas>
  );
}

export default App;
```

```glsl:vertex.glsl
void main() {
  vec4 worldPosition = modelMatrix * vec4(position, 1.0);
  vec4 mvPosition = viewMatrix * worldPosition;
  gl_Position = projectionMatrix * mvPosition;
}
```

```glsl:fragment.glsl
uniform vec3 uColor;

void main() {
  gl_FragColor = vec4(uColor, 1.0);
}
```

問題なさそう。。。
ざっくりとした感じではありましたが以上となります。


## まとめ

もう少し触ってみないとわからないことも多いですが、なんと言っても楽しいにつきます。