react-three-fiberを使って3Dモデルを動かしてみた！

# はじめに
こちらは[ANGEL Calendar](https://qiita.com/organizations/2024-angel-dojo)27日目の記事になります！
Amplifyで作ったアプリに3Dモデルを表示させてみたのですが、楽しいですねこれ。
本記事では3Dモデルを動かすところをご紹介します。

今回作成する完成イメージはこちら
![メディア1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3875372/7c600c7f-6e1c-98b3-1a80-bea984ca3a6a.gif)



Amplify Gen2の環境が既にある状態から始めていきます。
Amplifyを詳しく紹介している記事があるのでまずはそちらをご覧ください！

https://qiita.com/hohe096/items/94caf06a39ba80db2a3e

https://qiita.com/yuri-0123/items/5a81494608ae8fe86691


# Three.jsとは
[Three.js](https://threejs.org/)は3Dコンテンツを作成できるJavaScriptライブラリです。
canvas要素を作成して図形を描画することができます。
Reactと組み合わせてThree.jsを利用する場合、[react-three-fiber](https://r3f.docs.pmnd.rs/getting-started/introduction)というライブラリが便利です。
このライブラリを使用するメリットは、Reactのコンポーネントを利用して宣言的に記述できるため、コードの量を減らすことができます。
今回はReactでreact-three-fiberを使用して作成していきます。


# 試してみた
react-three-fiberのドキュメントを参考に作成していきます

https://r3f.docs.pmnd.rs/getting-started/introduction

まずはパッケージをインストールします。
```
npm install three @types/three @react-three/fiber
```

※以下[Quickstart](https://docs.amplify.aws/react/start/quickstart/)から持ってきたApp.tsxをごっそり書き換えています。
```react:App.tsx
import { createRoot } from 'react-dom/client';
import { Canvas } from '@react-three/fiber';

function Box(props) {

  return (
    <mesh
      {...props}>
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color={'orange'} />
    </mesh>
  );
}

const App: React.FC = () => {
  return (
    <Canvas>
      <ambientLight intensity={Math.PI / 2} />
      <spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />
      <pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />
      <Box position={[-1.2, 0, 0]} />
      <Box position={[1.2, 0, 0]} />
    </Canvas>
  );
}

createRoot(document.getElementById('root')!).render(<App />);

export default App;


createRoot(document.getElementById('root')!).render(<App />);

export default App;
```
ローカルでサーバーを起動して画面を確認します。
```
npm run dev
```
このような箱が2つできていれば成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3875372/ce7707d7-458f-353d-b16f-89a57cc3f503.png)

# コードの解説

```
<boxGeometry args={[1, 1, 1]} />
```
[BoxGeometry](https://threejs.org/docs/#api/en/geometries/BoxGeometry)で箱の形を決めています。幅と高さ、奥行きを設定します。今回は全て1にしているので立方体が作成されています。

```
<Canvas>
  <ambientLight intensity={Math.PI / 2} />
  <spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />
  <pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />
  <Box position={[-1.2, 0, 0]} />
  <Box position={[1.2, 0, 0]} />
</Canvas>
```
メインとなるCanvas要素です。
[AmbientLight](https://threejs.org/docs/#api/en/lights/AmbientLight)、[SpotLight](https://threejs.org/docs/#api/en/lights/SpotLight)、[PointLight](https://threejs.org/docs/#api/en/lights/PointLight)はその名の通り3Dモデルの光の当たり具合を調整します。これらが設定されていない場合は真っ黒な箱のできあがりです。

ちなみにrotationを追加するとx,y,z軸でモデルを回転させることができます。
```
<mesh
  {...props}
  rotation={[4, 4, 0]}>
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3875372/21be7334-c5cf-9b9d-6b2b-96bbc4fcaf1f.png)

# 完成イメージに近づける
箱にカーソルが当たった時ピンク色に変更し、クリックしたら箱が大きくなるように以下を追加します。
```
import React, { useRef, useState } from 'react';
```

```
const [hovered, setHover] = useState(false);
const [active, setActive] = useState(false);
```
hoveredはカーソルが箱の上にあるか判定し、activeは箱がクリックされているかを判定します。

3Dオブジェクトを定義するmesh要素は以下のように変更してください。
```
<mesh
  {...props}
  scale={active ? 1.5 : 1}
  onClick={() => setActive(!active)}
  onPointerOver={() => setHover(true)}
  onPointerOut={() => setHover(false)}>
  <boxGeometry args={[1, 1, 1]} />
  <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
</mesh>
```
[MeshStandardMaterial](https://threejs.org/docs/#api/en/materials/MeshStandardMaterial)でカーソルが箱の上にある場合、ピンク色に変更します。

クリックしてカーソルを当てると以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3875372/5c9dab85-39ae-21fe-f1a6-f084aceceb0f.png)

最後に箱を回転させます。以下を追加してmesh要素を変更してください。
```
const meshRef = useRef();

useFrame((state, delta) => (meshRef.current.rotation.x += delta));
```
```
<mesh
  {...props}
  ref={meshRef}
  scale={active ? 1.5 : 1}
  onClick={() => setActive(!active)}
  onPointerOver={() => setHover(true)}
  onPointerOut={() => setHover(false)}>
  <boxGeometry args={[1, 1, 1]} />
  <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
</mesh>
```
毎フレーム少しずつx軸方向に少しずつ回転させています。deltaは前フレームからの経過時間を設定します。

2を掛けてあげると回転の速度が2倍になります。
```
useFrame((state, delta) => (meshRef.current.rotation.x += delta * 2));
```

完成しました！
![メディア1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3875372/faf56f5b-3d0a-fd4a-c902-a3ccd2241498.gif)

最終的なコードは以下
```react:App.tsx
import { createRoot } from 'react-dom/client';
import React, { useRef, useState } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';

function Box(props) {
  const meshRef = useRef();
  const [hovered, setHover] = useState(false);
  const [active, setActive] = useState(false);
  
  useFrame((state, delta) => (meshRef.current.rotation.x += delta));

  return (
    <mesh
      {...props}
      ref={meshRef}
      scale={active ? 1.5 : 1}
      onClick={() => setActive(!active)}
      onPointerOver={() => setHover(true)}
      onPointerOut={() => setHover(false)}>
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
    </mesh>
  );
}

const App: React.FC = () => {
  return (
    <Canvas>
      <ambientLight intensity={Math.PI / 2} />
      <spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />
      <pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />
      <Box position={[-1.2, 0, 0]} />
      <Box position={[1.2, 0, 0]} />
    </Canvas>
  );
}

createRoot(document.getElementById('root')!).render(<App />);

export default App;
```


# おわりに
この記事ではAmplifyの良さはお伝えできませんでしたが、簡単に開発できるところに感動しました。
ReactやThree.js、react-three-fiberも初めて使いましたが、触っていくうちにあれもこれもできるのでは？とわくわくしながら作成できました。
他にも学びや人との繋がりができて、ANGEL Dojo参加してよかった～