【React】react-three-fibarで3D表現をする（文字表示）

<!-- ==================================================== -->
# 概要
<!-- ==================================================== -->
Three.jsのReact用ライブラリ **react-three-fibar** を使用して、文字の表示方法を3パターン実装します。

パッケージのバージョンやインストール方法は、以下を参照してください。

https://qiita.com/nemutas/items/60587fb3c67450ecff6d

<!-- ==================================================== -->
# 実装
<!-- ==================================================== -->
### 立体感のある文字
厚みのある文字です。Boxオブジェクトなどと同様に影を落としたりできます。

![スクリーンショット 2021-09-17 224341.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/d6d40956-e9c6-254d-7782-e659964fa1ed.png)

```.tsx
import React, { VFC } from 'react';
import { FontLoader, TextGeometry } from 'three';
import threeFontJson from 'three/examples/fonts/helvetiker_bold.typeface.json';
import { OrbitControls } from '@react-three/drei';
import { TDirectionalLight } from './objects/TDirectionalLight';
import { TFloorPlane } from './objects/TFloorPlane';
import { TCanvas } from './TCanvas';

export const Text1: VFC = () => {
	return (
		<TCanvas>
			<OrbitControls />
			<TDirectionalLight position={[-1, 3, 5]} />

			<TextObject text="Hello! Three.js" />

			<TFloorPlane />
		</TCanvas>
	)
}

// ----------------------------------------------
const TextObject: VFC<{ text: string }> = ({ text }) => {
	// 位置の調整
	const textGeo = new TextGeometry(text, {
		font: new FontLoader().parse(threeFontJson),
		size: 1,
		height: 0.1
	})
	textGeo.computeBoundingBox()
	const centerOffset = -(textGeo.boundingBox!.max.x - textGeo.boundingBox!.min.x) / 2

	return (
		<mesh position={[centerOffset, 1, 0]} args={[textGeo]} castShadow receiveShadow>
			{/* <textGeometry args={['Hello! Three.js', config]} /> */}
			<meshPhongMaterial color="royalblue" />
		</mesh>
	)
}

```
:::note warn
TCanvas、TDirectionalLight、TFloorPlaneは、それぞれreact-three-fiberのCanvas、directionalLight、planeGeometryをラップしたコンポーネントです。
実装の詳細は、<strong>おまけ</strong>載せています。
:::

テキストの表示位置の調整のために、TextGeometryのインスタンスを作成して、それを使ってオフセットを割り出しています。
`[x, y] = [0, 0]`の位置がテキストの左・下になっているので、オフセットを使用して中心・下に調整します。
この調整が必要ない場合は、コメントアウトしている**textGeometry**が使えます。

**元ネタ**

https://threejs.org/examples/#webgl_geometry_text

--------------------------
### 平面な文字
厚みのない平面な文字です。

![スクリーンショット 2021-09-17 224415.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/cf316f5e-ef75-fba1-dff9-40c11dd20d61.png)

```.tsx
import { loremIpsum } from 'lorem-ipsum';
import React, { VFC } from 'react';
import { Text } from '@react-three/drei';
import { useRotate } from './hooks/useRotate';
import { TCanvas } from './TCanvas';

export const Text2: VFC = () => {
	const lorem = loremIpsum({ count: 6 })

	return (
		<TCanvas position={[0, 0, 10]}>
			<TextObject text={lorem} />
		</TCanvas>
	)
}

// ----------------------------------------------
const TextObject: VFC<{ text: string }> = ({ text }) => {
	const textRef = useRotate([0, 0.1, 0])

	return (
		<Text
			ref={textRef}
			color="#24ACF2"
			fontSize={0.5}
			maxWidth={10}
			lineHeight={1}
			letterSpacing={0.02}
			textAlign={'left'}
			font="https://fonts.gstatic.com/s/raleway/v14/1Ptrg8zYS_SKggPNwK4vaqI.woff"
			anchorX="center"
			anchorY="middle">
			{text}
		</Text>
	)
}
```

:::note warn
useRotateは、オブジェクトをフレーム単位で回転させるためのカスタムフックです。
実装の詳細は、<strong>おまけ</strong>載せています。
:::

**元ネタ**

https://docs.pmnd.rs/drei/abstractions/text

--------------------------
### オブジェクトに追従する文字
オブジェクトに追従して、常に前面を向いている文字です。
また、カメラとオブジェクトの距離によって文字サイズも変化します。

![output(video-cutter-js.com) (3).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/1ee656a3-a57d-2343-dc83-244a430e959c.gif)

```.tsx
import React, { VFC } from 'react';
import { css } from '@emotion/css';
import { Html, OrbitControls } from '@react-three/drei';
import { useRotate } from './hooks/useRotate';
import { TDirectionalLight } from './objects/TDirectionalLight';
import { TFloorPlane } from './objects/TFloorPlane';
import { TCanvas } from './TCanvas';

export const Text3: VFC = () => {
	return (
		<TCanvas>
			<OrbitControls />
			<TDirectionalLight position={[5, 5, 5]} />

			<CubeWithText text="Hello! Three.js" />

			<TFloorPlane />
		</TCanvas>
	)
}

// ----------------------------------------------

const CubeWithText: VFC<{ text: string }> = ({ text }) => {
	const ref = useRotate()
	const groupRef = useRotate([0, 0.5, 0])

	return (
		<group ref={groupRef}>
			<mesh ref={ref} position={[2, 2, 2]} castShadow>
				<boxGeometry args={[1, 1, 1]} />
				<meshPhongMaterial color="royalblue" />
				<Html className={styles.text} center distanceFactor={10}>
					{text}
				</Html>
			</mesh>
		</group>
	)
}

const styles = {
	text: css`
		white-space: nowrap;
		font-size: 2rem;
		color: red;
		margin-top: 100px;
		user-select: none;
	`
}
```
:::note info
meshに回転アニメーションのrefを追加すると、そのオブジェクトが回転（自転）します。
meshをgroupで囲って、そのgroupに回転アニメーションのrefを追加すると、そのオブジェクトが中心軸まわりに回転（公転）します。
:::

**元ネタ**

https://docs.pmnd.rs/drei/misc/html

<!-- ==================================================== -->
# まとめ
<!-- ==================================================== -->
文字の扱い方についてまとめました。（実装はほぼコピーですが...）
また違う表現方法を見つけたら追加します。

<!-- ==================================================== -->
# おまけ
<!-- ==================================================== -->
### TCanvas
Canvasのラッパーコンポーネントです。

```.tsx
import React, { VFC } from 'react';
import { css } from '@emotion/css';
import { Canvas } from '@react-three/fiber';

type Props = {
	children: React.ReactNode
	fov?: number
	position?: [number, number, number]
}

export const TCanvas: VFC<Props> = props => {
	const { children, fov = 50, position = [0, 3, 10] } = props
	return (
		<Canvas className={styles.canvas} camera={{ fov, position }} dpr={[1, 2]} shadows>
			{children}
		</Canvas>
	)
}

const styles = {
	canvas: css`
		width: 100%;
		height: 100%;
	`
}
```

### TDirectionalLight
光源です。オプションでHelperを表示することができます。

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

type Props = {
	position: [number, number, number]
	isHelper?: boolean
}

export const TDirectionalLight: VFC<Props> = props => {
	const { position, isHelper = false } = props

	const lightRef = useRef()
	useHelper(lightRef, DirectionalLightHelper)

	return (
		<>
			{isHelper ? (
				<directionalLight
					ref={lightRef}
					position={position}
					intensity={1} // 光の強さ
					shadow-mapSize-width={2048} // 描画精度
					shadow-mapSize-height={2048}
					castShadow
				/>
			) : (
				<directionalLight
					position={position}
					intensity={1} // 光の強さ
					shadow-mapSize-width={2048} // 描画精度
					shadow-mapSize-height={2048}
					castShadow
				/>
			)}
		</>
	)
}

```

### TFloorPlane
床パネルです。

```.tsx
import React, { VFC } from 'react';
import { DoubleSide } from 'three';

type Props = {
	size?: [number, number]
	color?: string
	isGridHelper?: boolean
}

export const TFloorPlane: VFC<Props> = props => {
	const { size = [10, 10], color = 'white', isGridHelper = false } = props

	return (
		<>
			<mesh position={[0, 0, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
				<planeGeometry args={size} />
				<meshPhongMaterial color={color} side={DoubleSide} />
			</mesh>

			{isGridHelper && (
				<gridHelper position={[0, 0.01, 0]} args={[size[0], size[0], 'red', 'black']} />
			)}
		</>
	)
}
```

### useRotate
オブジェクトを回転させるためのカスタムフックです。

```.tsx
import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';

export const useRotate = (speed = [1, 1, 1]) => {
	const ref = useRef<any>(null)
	useFrame(({ clock }) => {
		const a = clock.getElapsedTime()
		ref.current.rotation.x = a * speed[0]
		ref.current.rotation.y = a * speed[1]
		ref.current.rotation.z = a * speed[2]
	})
	return ref
}
```