【React】Three.jsを使ってBlednerで作った3Dモデルを手軽に表示する

# はじめに
本記事ではReactでThree.jsを使用し3DモデルをWebページ上に表示します。
3DモデルはBlenderで作成した地球の3Dモデルを使用します。

# 完成イメージ

![three_gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1247619/a0b3ba89-728b-40d9-5706-4271634ed778.gif)

# 対象の方
- ReactでThree.jsを使ってみたい方
- Blenderで作成した3DモデルをReactを使って描画してみたい方
- タイトルが気になった方


本記事のコードは対象の3Dモデル部分を変えていただければ、基本的にコピペでも動くと思います。

# 環境
- vite：5.1
- React：18.2
- three：0.161

# 3Dモデルについて
本記事のタイトルの通り3DはBlenderで作成することを想定しています。
Blenderからgltf形式でエクスポートした3Dモデルを使用。
今回は以下のような惑星を使用しています。
何かしら3Dモデルを準備していてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1247619/ab55c5f9-9068-a2e2-9d24-fe938417497a.png)


# プロジェクトの作成

今回はViteを利用しプロジェクトを作成する。
以下のコマンドでプロジェクトを作成。
```
npm create vite@latest
```

作成されたプロジェクトにおいて、
`public`直下の`assets`フォルダにBlednerで作成した3Dモデルを格納する。

```
例：　public
      └assets
       └3Dモデル
```

:::note
補足
`src`直下の`assets`フォルダに3Dモデルを格納すると参照できないため注意
:::

# パッケージのインストール

```
npm i three
```

```
npm i --save-dev @types/three
```


# ソース

###  ◆シーンの作成

空のシーンを新しく作成します。
このシーンに光源、ライト、3Dモデル等を追加していきます。
```tsx
 // シーン
const scene = new THREE.Scene();
```

###  ◆canvasのサイズを取得
画面のサイズを取得し画面全体にcanvasを表示させるよう設定します。

```tsx
// 画面のサイズを取得
const sizes = {
  width: window.innerWidth,
  height: window.innerHeight,
};
 ```

### ◆光源の配置
Three.jsの空間に光源を追加します。
光源に関連した項目（色・強さ・照射距離・減衰率）設定し座標を設定を宣言している。
今回は`PointLight`を使用し光源を配置している。

```tsx
// 光源の設定
const color = 0xFFFFFF;  // 白色
const intensity = 300;  // 強さ
const distance = 300;  // 照射距離
const decay = 2;  // 減衰率

// 光源の座標を作成
const vertices = [
    new THREE.Vector3(-3, -3, -3), 
    new THREE.Vector3(3, -3, -3),  
    new THREE.Vector3(3, 3, -3),   
    new THREE.Vector3(-3, 3, -3),  
    new THREE.Vector3(-3, -3, 3),  
    new THREE.Vector3(3, -3, 3),   
    new THREE.Vector3(3, 3, 3),    
    new THREE.Vector3(-3, 3, 3)    
];

// 光源を配置
for (const vertex of vertices) {
    const light = new THREE.PointLight(color, intensity, distance, decay);
    light.position.copy(vertex);
    scene.add(light);
}
```

:::note
補足
今回は`PointLight`を使用したが、Thre.jsには他にも光源の種類がいくつかあります。
気になる方はドキュメントをご確認ください。
- ドキュメント：https://threejs.org/docs/#api/en/lights/PointLight
:::


### ◆カメラの配置

カメラとコントロール追加します。
`OrbitControls`を使用してカメラを制御（マウスやタッチ操作）するためのコントロールを作成しています。

```tsx
// カメラ
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100);
camera.position.set(0, 1, 5);
scene.add(camera);

const controls = new OrbitControls(camera, canvasRef.current!);
controls.enableDamping = true
```

:::note warn
注意：OrbitControlsの引数について
※`OrbitControls`の2つめの引数に`body`を指定してしまうと、
canvas外の部分をクリックしても`OrbitControls`の操作が反応してしまうので注意してください。
:::

### ◆ヘルパーの表示
空間内での座標が分かりにくいので基準となる平面を作成します。
`GridHelper`は平面状のグリッドを生成し作成するヘルパークラスです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1247619/880bffe2-871f-f750-1c27-76d041297f97.png)


```tsx
// ヘルパー
const gridHelper = new THREE.GridHelper( 20, 10 );
gridHelper.position.set(0,-1,0);
scene.add( gridHelper );
```

### ◆レンダリング設定
`WebGLRenderer`で3Dシーンをレンダリングします。
デフォルトではシーンの背景色が真っ黒なので色も変更しておきます。

```tsx
// レンダラー
const renderer = new THREE.WebGLRenderer({
  canvas: canvasRef.current!,
});
renderer.setSize(sizes.width, sizes.height);

// 背景色を水色に設定
renderer.setClearColor('#001021');
```

<details><summary><font color="red">ここまでの全体のコード（renderCanvas.tsx）</font></summary>

```tsx:renderCanvas.tsx
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/Addons.js';

// シーンの作成
const createScene = (canvasRef: React.RefObject<HTMLCanvasElement>) => {

    // 画面のサイズを取得
    const sizes = {
      width: window.innerWidth,
      height: window.innerHeight,
    };

    // シーン
    const scene = new THREE.Scene();

    // ライト
    const color = 0xFFFFFF; 
    const intensity = 300;
    const distance = 300;
    const decay = 2;

    // 光源の座標を作成
    const vertices = [
        new THREE.Vector3(-3, -3, -3), 
        new THREE.Vector3(3, -3, -3),  
        new THREE.Vector3(3, 3, -3),   
        new THREE.Vector3(-3, 3, -3),  
        new THREE.Vector3(-3, -3, 3),  
        new THREE.Vector3(3, -3, 3),   
        new THREE.Vector3(3, 3, 3),    
        new THREE.Vector3(-3, 3, 3)    
    ];

    // 光源の配置
    for (const vertex of vertices) {
        const light = new THREE.PointLight(color, intensity, distance, decay);
        light.position.copy(vertex);
        scene.add(light);
    }

    // カメラ
    const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100);
    camera.position.set(0, 1, 5);
    scene.add(camera);

    const controls = new OrbitControls(camera, canvasRef.current!);
    controls.enableDamping = true
    
    // ヘルパー
    const gridHelper = new THREE.GridHelper( 20, 10 );
    gridHelper.position.set(0,-1,0);
    scene.add( gridHelper );

    // レンダラー
    const renderer = new THREE.WebGLRenderer({
      canvas: canvasRef.current!,
    });
    renderer.setSize(sizes.width, sizes.height);

    // 背景色を水色に設定
    renderer.setClearColor('#001021');

    return {  scene, camera, renderer };
};

export default createScene;
```
</details>



### ◆3Dモデルの読み込み
以下のコードで`assets`フォルダに格納した3Dモデルを読み込んでいる。
読み込み後には3Dモデルの位置を設定しシーンに追加・更新を行う。
また読み込んだ3DモデルはThree.jsの空間内で`Y軸`方向にゆっくり回り続けるように設定してある。

```tsx
let model: THREE.Object3D;
const loader = new GLTFLoader();

// モデルの読み込み
loader.load('assets/earth2.glb', (gltf) => {
  model = gltf.scene;
  // 3Dモデルの位置
  model.position.set(0,-1,0);
  // 3Dモデルをシーンに追加
  scene.add(model);

  // シーンの更新
  const animate = () => {
    requestAnimationFrame(animate);

    // モデルの回転
    if (model) {
      model.rotation.y += 0.001;
    }
    renderer.render(scene, camera);
  };
  animate();
});
```

<details><summary><font color="red">3Dモデルの表示に関する全体のコード（App.tsx）</font></summary>


```tsx:App.tsx
import { useEffect, useRef } from 'react';
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import createScene from './renderCanvas';

const App = () => {

  // canvasを扱う、DOMを扱うから初期値null
  const canvasRef = useRef<HTMLCanvasElement | null>(null);

  useEffect(() => {
    const { scene, camera, renderer } = createScene(canvasRef);

    // 3Dモデルの読み込み
    let model: THREE.Object3D;
    const loader = new GLTFLoader();
    loader.load('assets/earth2.glb', (gltf) => {
      model = gltf.scene;
      model.position.set(0,-1,0);
      scene.add(model);

      // カメラの位置を設定
      camera.position.set(0, 1, 5);

      // シーンの更新
      const animate = () => {
        requestAnimationFrame(animate);

        // モデルの回転
        if (model) {
          model.rotation.y += 0.001;
        }
        renderer.render(scene, camera);
      };
      animate();
    });
  }, []);

  return <canvas ref={canvasRef} className='canvasContaier'/>;
}

export default App
```

</details>



<details><summary>main.tsx</summary>


```tsx:main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)
```
</details>

# 実行結果
`npm run dev`で起動しWebページ上の描画すると、
以下の画像用のように問題なくBlnderで作成した3Dモデルが描画されていることを確認。


![three_gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1247619/008a05c1-2a90-18c8-f1c9-e776cbee6dbb.gif)