Reactで3Dコンテンツを実装する - three.js入門

# はじめに

業務でブラウザ上での3Dコンテンツ実装する機会があり、
Reactのプロジェクトに[three.js](https://github.com/mrdoob/three.js)、[react-three-fiber](https://github.com/pmndrs/react-three-fiber)を導入して実装を行いました。
シンプルなものであれば、思っていたよりも簡単に3Dモデルを操作できて面白かったので記事にしてみました。
本記事では公式リポジトリのサンプルコードをReactプロジェクト上で動かしつつ、three.jsの諸概念を解説しようと思います。

# three.jsとは
three.jsはJavascriptでWebGLを使った3Dコンテンツを実装できるライブラリです。
3Dモデルの表示やアニメーションなどの表現を簡単に実装することができます。
本記事では、このライブラリの概要を説明します。

https://threejs.org/

https://github.com/mrdoob/three.js/

# React-Three-Fiberとは

Reactでthree.jsを扱うためのライブラリで、three.jsの諸要素をJSXコンポーネントとして扱うことができるようになります。
このライブラリを使用するためには、three.jsの知識が必要なので、こちらの説明は次の記事で扱う予定です。

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

https://github.com/pmndrs/react-three-fiber

# 早速使ってみる

## Reactアプリケーションの準備

[create-react-app](https://www.npmjs.com/package/create-react-app)を使って、Reactアプリケーションを作成します。

```
npx create-react-app react_three_js --template typescript
```

src/下にpages/threeSamples.tsxを作成し、サンプルページを実装し、src/App.tsxから呼び出します。

```tsx:src/pages/threeSample.tsx
import React, { useEffect } from "react"
import * as THREE from 'three'

export const ThreeSample = () => {
  return <div></div>
}
```

```tsx:src/App.tsx
import React from 'react';
import { ThreeSample } from './pages/threeSample';

function App() {
  return <ThreeSample />;
}

export default App;
```

サーバー立ち上げて、http://localhost:3000 にアクセスし、hogeと表示されれば完了です。
```
npm run start
```

![スクリーンショット 2022-12-19 23.13.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2599923/f5a66b73-bc09-7d01-5e91-beee4c536dbd.png)

## three.jsのインストール

npmでthree.jsをインストールします。今回はtypescriptで実装するので、一緒に@types/threeもインストールしています。
```
npm i --save three @types/three
```

## 公式リポジトリのサンプルを実装する

さて、準備ができたら早速、公式リポジトリに上がっているサンプルを実装してみましょう。
先程作成したthreeSample.tsxにuseEffect()を追加し、その中にサンプルコードをコピペし、
下記2点ほど修正します。
1. animation()関数の引数timeに型numberを追加する
1. ```<div>hoge</div>```としていた部分を```<></>```に変更

後ほど一つずつ解説しますので、今は中身を理解しなくて大丈夫です!

```tsx:src/pages/threeSample.tsx
import React, { useEffect } from "react"
import * as THREE from 'three'

export const ThreeSample = () => {
  useEffect(() => {
    // init
    const camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.01, 10 );
    camera.position.z = 1;

    const scene = new THREE.Scene();

    const geometry = new THREE.BoxGeometry( 0.2, 0.2, 0.2 );
    const material = new THREE.MeshNormalMaterial();

    const mesh = new THREE.Mesh( geometry, material );
    scene.add( mesh );

    const renderer = new THREE.WebGLRenderer( { antialias: true } );
    renderer.setSize( window.innerWidth, window.innerHeight );
    renderer.setAnimationLoop( animation );
    document.body.appendChild( renderer.domElement );

    // animation
    function animation( time: number // <= 1. timeにnumberの方を追加 ) {
      mesh.rotation.x = time / 2000;
      mesh.rotation.y = time / 1000;
      renderer.render( scene, camera );
    }
  }, [])

  return <></> // <= 2. divとhogeを削除
}

```

再度、 http://localhost:3000 にアクセスすると下記のようなアニメーションが表示されるはずです。

![threejs_sample_mv.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2599923/223d6de6-0420-a8c3-4bd4-2514ce7adbd8.gif)


いかがでしょうか、簡単にいい感じの3Dアニメーションが実装できてテンションが上がってきますよね。

:::note warn
ReactのStrictモードだと、useEffectが二回走るため、appendChildが二回実行され、二つのSceneが表示されてしまいます。
これを防止するためには、appendChildに条件付けをするか、Strictモードを解除する必要がありますが、
簡単のために本記事では後者で進めていきます。
具体的には、```src/index.tsx```の```<React.StrictMode>```をコメントアウトします。

```tsx:src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  // <React.StrictMode>
    <App />
  // </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
```

:::

# Three.jsの諸概念解説

上記のサンプルを踏まえつつ、three.jsを使う上で最低限知っておきたい諸概念を解説します。

## Scene(シーン)

3Dオブジェクトやライト、カメラなどを配置する3D空間のことです。
three.jsはこのシーンがないと何も描画することができません。
サンプル中でもthreeSample.tsxの10行目あたりに下記のような記述があります。

```ts
const scene = new THREE.Scene()
```

このsceneにMeshなどのオブジェクトなどを追加して、3D空間を実装していきます。
サンプルで該当するのは15, 16行目の部分です。
```ts
const mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );
```

## Camera(カメラ)

3D空間をどの視点から描画するか設定するための機能です。
カメラの中にも遠視投影([PerspectiveCamera](https://threejs.org/docs/index.html#api/en/cameras/PerspectiveCamera))や並行投影([OrthographicCamera](https://threejs.org/docs/index.html#api/en/cameras/OrthographicCamera))などの種類があります。
ざっくりですが、前者が遠近法あり、後者が遠近法なし(近くでも遠くでも同じ大きさ)という違いがあります。
サンプルで使用されているのは遠視投影のカメラです。

```ts
const camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000);
```
引数ではそれぞれ左から、

1. fov(視野角)
1. aspect(アスペクト比)
1. near(カメラからどのぐらい近くの位置まで描画するか)
1. far(カメラからどのぐらい離れた位置まで描画するか)

を指定しています。

```ts
camera.position.z = 1;
```
という部分では、カメラの位置設定を行っています。
positionは3D空間上の位置のことで、xyz座標で表されます。
three.jsではpositionを指定することで、オブジェクトやカメラ等を3D空間のどの位置に配置するか管理することができます。
ここでは、cameraのz軸上の位置を1に指定しています。

また、サンプルでは使用されていませんが、カメラの角度はrotationで表現されます。
```ts
camera.rotation.x = 0.5
```
この数値は°ではなくラジアンであることに注意してください。

## Renderer

Rendererはカメラとシーンを組み合わせて、実際に描画を行うものです。
こちらもいくつか種類がありますがサンプルで使用されているのは[WebGLRenderer](https://threejs.org/docs/index.html#api/en/renderers/WebGLRenderer)です。

```ts
const renderer = new THREE.WebGLRenderer( { antialias: true } );
```

ここでは引数に```{ antialias: true }```を指定しています。
アンチエイリアスは、斜線や曲線などを描画する際、ジャギーが目立たなくなるように段階的に中間色を配置するというものです。

https://e-words.jp/w/%E3%82%A2%E3%83%B3%E3%83%81%E3%82%A8%E3%82%A4%E3%83%AA%E3%82%A2%E3%82%B9.html

RendererにsetSize(）をすることで、描画するcanvasのサイズを設定することができます。
```ts
renderer.setSize( window.innerWidth, window.innerHeight );
```
ここでは、実際に使用している画面サイズに合わせる形でcanvasのサイズを設定しています。

諸々の設定が完了したのち、render()で引数にSceneとCameraを受け取って、canvasに描画を開始します。
```ts
renderer.render( scene, camera );
```

また、アニメーションの実装もrendererを使うのですが、こちらは後ほど解説いたします。
```ts
render.setAnimationLoop( animation )
```

## Light(ライト)

3D空間上に配置する光源のことです。
サンプルでは使用されていませんが、配置したオブジェクトを光を当てたり、影をつけたりするのに必要です。
ライトに関しては[こちら](https://ics.media/tutorial-three/light_variation/)の記事がとてもわかりやすかったので、公式ドキュメントと併せてご参照ください。

## Mesh

MeshはGeometory(形状)とMaterial(材質)を組み合わせてオブジェクトを生成するためのものです。
下記のようにMeshをSceneに追加しています。

```ts
const mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );
```
シンプルなので、特に説明は不要だと思います。
GeometoryとMaterialを個別に見ていけばイメージがつきやすいと思います。

## Geometory(形状)

オブジェクトの形状を表現するためのものです。
直方体、球のようなシンプルなものから、[トーラス結目](https://ja.wikipedia.org/wiki/%E3%83%88%E3%83%BC%E3%83%A9%E3%82%B9%E7%B5%90%E3%81%B3%E7%9B%AE)のような複雑な形まで表現できます。
サンプルでは[BoxGeometory(直方体)](https://threejs.org/docs/index.html#api/en/geometries/BoxGeometry)が使用されています。

```ts
const geometry = new THREE.BoxGeometry( 0.2, 0.2, 0.2 );
```
引数で、
1. 横幅
1. 高さ
1. 奥行き

を指定しています。

## Material(材質)

オブジェクトがどんな材質かを表現するためのものです。
Geometoryで定義した形状の表面が、どんな色をしているか、どんな質感か、ということを定義することができます。
サンプルでは、[MeshNormalMaterial](https://threejs.org/docs/index.html#api/en/materials/MeshNormalMaterial)を使用しています。

```ts
const material = new THREE.MeshNormalMaterial();
```

NormalMaterialはNormalMapping(法線マッピング)という技法が使われています。
法線ベクトルは、オブジェクトの面に向かって垂直に伸びるベクトルのことで、
x, y, z方向の法線ベクトルにそれぞれ, R, G, Bを割り当ててオブジェクトの表面の色を決定しています。

法線マッピングは光の反射を計算するために使用され、これを使うとポリゴン数を増やさなくても凹凸を表現することができます。

https://nodachisoft.com/common/jp/article/jp000050/

少し難解な概念なのでわかりづらいと思いますが、axesHelperを使ってみるとわかりやすいです。
axesHelperは3D空間上のx, y, z軸を表示してくれるヘルパーです。
threeSample.tsxに下記のように追記して見ましょう。
```ts
...
renderer.setAnimationLoop( animation );

const axesHelper = new THREE.AxesHelper( 5 ); // <= 追記
scene.add( axesHelper ); // <= 追記

document.body.appendChild( renderer.domElement );
...
```

再度、http://localhost:3000 にアクセスして見ましょう。

![sample_with_axes_helper.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2599923/00612b0b-245d-13ca-8b28-798762c1dc24.gif)


カメラのポジションがZ軸上にあるので、Z軸は見えませんが、X軸が赤色、Y軸が緑色で表示されたと思います。
X軸側に向いている面は赤色っぽく、Y軸側に向いている面が緑色っぽく、画面側、つまりZ軸側に向いている面が青っぽく表示されていることがわかります。

## その他

### アニメーションの付け方

最後に、アニメーションの解説をします。
まず、注目すべきは下記の部分です。
```ts
renderer.setAnimationLoop( animation );
```
setAnimationLoop()は[window.requestAnimationFrame()](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame)をthree.jsで扱うための組み込み関数で、
1フレーム(60分の1秒)ごとに引数に指定されたコールバック関数を呼び出します。
また、呼び出したコールバック関数には呼び出し時刻のtimestampを引数として渡します。
今回はコールバック関数として、animation()を呼び出しています。

次にanimation()の中身を見ていきましょう。
```ts
function animation( time: number // <= 1. timeにnumberの方を追加 ) {
  mesh.rotation.x = time / 2000;
  mesh.rotation.y = time / 1000;
  renderer.render( scene, camera );
}
```

Meshのrotationプロパティを現在時刻をもとに算出し、
renderer.render( scene, camera )で再描画を行っています。
これによって、時刻が進むごとに少しずつrotationが変わっていくことで、
Meshが回転しているようなアニメーションを実装することができます。

同じようにpositionを変更することで、立方体の位置を移動させることもできます。
rotationの部分をpositionに変更して見ましょう。

```ts
function animation( time: number // <= 1. timeにnumberの方を追加 ) {
  mesh.position.x = time / 2000;
  mesh.position.y = time / 1000;
  renderer.render( scene, camera );
}
```

すると、立方体が無限の彼方へ飛んでいきます。

![move_position.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2599923/afb3d4c3-9e33-599b-38fc-c9e3d94beabc.gif)

このように、setAnimationLoop()を使用すれば、簡単にアニメーションを実装することができます。

# おわりに

本記事では公式リポジトリのサンプルをもとにthree.jsの諸概念を解説していきました。
今回説明したものはごく一部ですが、それでもthree.jsの基礎はざっくり理解できたと思います。

ただ、勘の良い方はお気づきだと思いますがthree.jsそのものをReactで扱う場合、
componentとして分離しづらく、useEffectの中身が肥大化して大変なことになります。
そこで、各要素をJSXコンポーネントのように扱うことができるようにするためのライブラリがreact-three-fiberです。

というわけで次回はreact-three-fiberの概要を説明する記事を投稿予定なので、
よろしければそちらもご一読いただけると幸いです!