【React】react-three-fibarで3D表現をする

<!-- ======================================================== -->
# 概要
<!-- ======================================================== -->
Three.jsのReact用ライブラリ **react-three-fibar** を使用して、簡単な3D表現を実装します。

![output(video-cutter-js.com) (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/afaab38d-e16e-c1c8-3d26-0f01aa4062c2.gif)

公式ドキュメント

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

https://threejs.org/docs/index.html#manual/en/introduction/Creating-a-scene

:::note info
react-three-fibarは、Three.jsのReact用パッケージで、ほぼ100％の互換性があります。
Three.jsのドキュメントはかなり充実しているので、react-three-fibarのドキュメントに載っていないことがあれば、そちらを参照しましょう。
:::

<!-- ======================================================== -->
# 環境
<!-- ======================================================== -->
* react － 17.0.2
* typescript － 4.4.3
* three － 0.132.2
* react-three/fiber － 7.0.7
* react-three/drei － 7.8.2

<!-- ======================================================== -->
# インストール
<!-- ======================================================== -->
いつものように、プロジェクトフォルダを用意して`create-react-app`でプロジェクトを作成します。

```:cmd
npx create-react-app . --template typescript --use-npm
```

必要なパッケージをインストールします。

```:cmd
npm i three @react-three/fiber @react-three/drei
npm i -D @types/three
```

:::note info
<strong>react-three/drei</strong>は、react-three-fiberに対して便利なヘルパー関数を提供します。
:::

<!-- ======================================================== -->
# 実装
<!-- ======================================================== -->
### キャンバスの作成・オブジェクトの配置

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/ae0ac533-a39b-c53f-7a52-d50531fe35ff.png" width="500">

```.tsx
import React, { VFC } from 'react';
import { DoubleSide } from 'three';
import { OrbitControls } from '@react-three/drei';
import { Canvas } from '@react-three/fiber';

export const ThreeDemo: VFC = () => {
	return (
		<Canvas camera={{ fov: 50, position: [0, 3, 10] }}>
			<Contents />
		</Canvas>
	)
}

const Contents: VFC = () => {
	return (
		<>
			{/* control */}
			<OrbitControls />

			{/* light */}
			<directionalLight position={[5, 5, 5]} />

			{/* box 1 */}
			<mesh position={[0, 2, 0]}>
				<boxGeometry args={[1, 1, 1]} />
				<meshPhongMaterial color="blue" />
			</mesh>

			{/* box 2 */}
			<mesh position={[1, 3, 2]} scale={0.5}>
				<boxGeometry args={[1, 1, 1]} />
				<meshPhongMaterial color="red" />
			</mesh>

			{/* floor */}
			<mesh position={[0, 0, 0]} rotation={[-Math.PI / 2, 0, 0]}>
				<planeGeometry args={[10, 10]} />
				<meshStandardMaterial color="#E5E5E5" side={DoubleSide} />
			</mesh>
		</>
	)
}

```

#### [Canvas](https://docs.pmnd.rs/react-three-fiber/API/canvas)
視野角（fov）やカメラ位置（position）を設定します。ほかに背景色やDPRなどを設定できます。

#### [OrbitControls](https://docs.pmnd.rs/drei/controls/orbit)
このコンポーネントを追加すると、マウスでカメラの回転、移動、ズームを行うことができます。
どれかしらの機能だけを使う/使わないの設定もできます。

#### [directionalLight](https://threejs.org/docs/index.html?q=light#api/en/lights/DirectionalLight)
ライトです。位置や強さ、色などを設定することができます。

#### オブジェクト
[**geometry**](https://threejs.org/docs/index.html?q=geo#api/en/geometries/BoxGeometry)ではオブジェクトの形状を、[**material**](https://threejs.org/docs/index.html?q=material#api/en/materials/MeshPhongMaterial)では色や材質を定義します。
それを**mesh**タグで囲います。**mesh**では、オブジェクトの位置や回転を定義します。

```.tsx
<mesh position={[0, 2, 0]}>
	<boxGeometry args={[1, 1, 1]} />
	<meshPhongMaterial color="blue" />
</mesh>
```

:::note warn
実装例では、Contentsコンポーネントを作成してCanvasタグの中で参照しています。
これは意図的にやっていて、Canvas内のコンポーネントでしか使用できないカスタムフックがあるためです。（後ほど紹介しています）
:::

---------------------------------------------------
### 影
オブジェクト間に落ちる影も簡単に実装することができます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/aafd52d4-5e5d-6a75-5b0a-018c8acce9ad.png" width="500">

```.tsx
import React, { VFC } from 'react';
import { DoubleSide } from 'three';
import { OrbitControls } from '@react-three/drei';
import { Canvas } from '@react-three/fiber';

export const ThreeDemo: VFC = () => {
	return (
		<Canvas camera={{ fov: 50, position: [0, 3, 10] }} shadows>
			<Contents />
		</Canvas>
	)
}

const Contents: VFC = () => {
	return (
		<>
			{/* control */}
			<OrbitControls />

			{/* light */}
			<directionalLight
				position={[5, 5, 5]}
				intensity={1} // 光の強さ
				shadow-mapSize-width={2048} // 描画精度
				shadow-mapSize-height={2048}
				castShadow
			/>

			{/* box 1 */}
			<mesh position={[0, 2, 0]} castShadow receiveShadow>
				<boxGeometry args={[1, 1, 1]} />
				<meshPhongMaterial color="blue" />
			</mesh>

			{/* box 2 */}
			<mesh position={[1, 3, 2]} scale={0.5} castShadow receiveShadow>
				<boxGeometry args={[1, 1, 1]} />
				<meshPhongMaterial color="red" />
			</mesh>

			{/* floor */}
			<mesh position={[0, 0, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
				<planeGeometry args={[10, 10]} />
				<meshStandardMaterial color="#E5E5E5" side={DoubleSide} />
			</mesh>
		</>
	)
}

```

影を入れるためには、まず**Canvas**に`shadows`プロパティを追加します。

次に、lightやオブジェクトに、`castShadow`・`receiveShadow`を追加します。
`castShadow`は、それが影を他に落とすかの設定です。`receiveShadow`は、それが影を受けるかの設定です。
例えば、directionalLightは、影を他に落とすけど自分自身は影を受けないので、`castShadow`だけ設定します。boxオブジェクトは、影を他に落とすし自分自身も影を受けるので、`castShadow`・`receiveShadow`どちらも設定しています。

:::note info
このため、<font color="blue">Box1</font>には<font color="red">Box2</font>の影が落ちていて、<font color="blue">Box1</font>自身も床に影を落としています。
:::

影の解像度が低いときは、lightの`shadow-mapSize-width`・`shadow-mapSize-height`で調整します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/a2f05d70-7af2-e949-31e8-bb7f86b5d112.png" width="500">

---------------------------------------------------
### オブジェクトの回転
```.tsx
import React, { useRef, VFC } from 'react';
import { DoubleSide } from 'three';
import { OrbitControls } from '@react-three/drei';
import { Canvas, useFrame } from '@react-three/fiber';

export const ThreeDemo: VFC = () => {
	return (
		<Canvas camera={{ fov: 50, position: [0, 3, 10] }} shadows>
			<Contents />
		</Canvas>
	)
}

const Contents: VFC = () => {
	const boxRef = useRef<any>(null)

	useFrame(({ clock }) => {
		const a = clock.getElapsedTime()
		boxRef.current.rotation.x = a * 1
		boxRef.current.rotation.y = a * 1
		boxRef.current.rotation.z = a * 0
	})

	return (
		<>
			{/* control */}
			・・・

			{/* light */}
			・・・

			{/* box 1 */}
			<mesh ref={boxRef} position={[0, 2, 0]} castShadow receiveShadow>
				<boxGeometry args={[1, 1, 1]} />
				<meshPhongMaterial color="blue" />
			</mesh>

			{/* box 2 */}
			<mesh position={[1, 3, 2]} scale={0.5} castShadow receiveShadow>
				<boxGeometry args={[1, 1, 1]} />
				<meshPhongMaterial color="red" />
			</mesh>

			{/* floor */}
			・・・
		</>
	)
}
```
フレームアニメーションは、[**useFrame**](https://docs.pmnd.rs/react-three-fiber/tutorials/basic-animations)を使うことで実装できます。
<font color="blue">Box1</font>のmeshに**boxRef**を指定することで、<font color="blue">Box1</font>が回転します。

:::note warn
useFrameは、Canvas内のコンポーネントでのみ使用できます。
:::

---------------------------------------------------
### ヘルパーオブジェクト
Three.jsには、グリッドやライトを視覚化するためのヘルパーオブジェクトが用意されています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/38930b72-ddff-2f01-b088-c9d21f6d37ee.png" width="500">

```.tsx
import React, { useRef, VFC } from 'react';
import { DirectionalLightHelper, DoubleSide } from 'three';
import { OrbitControls, useHelper } from '@react-three/drei';
import { Canvas, useFrame } from '@react-three/fiber';

export const ThreeDemo: VFC = () => {
	return (
		<Canvas camera={{ fov: 50, position: [0, 3, 10] }} shadows>
			<Contents />
		</Canvas>
	)
}

const Contents: VFC = () => {
	const lightRef = useRef()
	useHelper(lightRef, DirectionalLightHelper)

	・・・

	return (
		<>
			{/* control */}
			・・・

			{/* light */}
			<directionalLight
				ref={lightRef}
				position={[5, 5, 5]}
				intensity={1} // 光の強さ
				shadow-mapSize-width={2048} // 描画精度
				shadow-mapSize-height={2048}
				castShadow
			/>

			{/* box 1 */}
			・・・

			{/* box 2 */}
			・・・

			{/* floor */}
			・・・

			{/* grid */}
			<gridHelper position={[0, 0.01, 0]} args={[10, 10, 'red', '#4C4C4C']} />
		</>
	)
}
```

#### [DirectionalLightHelper](https://threejs.org/docs/?q=DirectionalLightHelper#api/en/helpers/DirectionalLightHelper)
`lightRef `を`directionalLight`のプロパティに追加することで、ライトを可視化します。
ヘルパーの生成には、[useHelper](https://docs.pmnd.rs/drei/misc/use-helper)を使用します。

:::note warn
useHelperは、Canvas内のコンポーネントでのみ使用できます。
:::

#### [gridHelper](https://threejs.org/docs/?q=gridhelper#api/en/helpers/GridHelper)
`args`では、`[大きさ, 分割数, 真ん中の線の色, 全体の線の色]`を指定しています。

<!-- ======================================================== -->
# まとめ
<!-- ======================================================== -->
react-three-fiberを使うと、簡単に動く3D表現ができてとても楽しいです。
ですが、Three.jsのラッパーパッケージなので、Three.jsでの情報はあってもreact-three-fiberの情報がなかなかないです...:rolling_eyes:

実装例は、CodeSandBoxを参照するといいと思います。

https://codesandbox.io/search?query=&page=1&configure%5BhitsPerPage%5D=12&refinementList%5Btags%5D%5B0%5D=r3f&refinementList%5Btags%5D%5B1%5D=react-three-fiber&refinementList%5Bnpm_dependencies.dependency%5D=