UnityのシーンをエクスポートしてThree.jsで使用する

Unityで作成したシーンをエクスポートして、Three.jsで使用してみます。

手順は次のようになります。

1. Unityでシーンを作成する
1. FBX ExporterでFBXをエクスポートする
1. FBX2glTFでFBXをglTFに変換する
1. Three.jsでglTFを読み込む

UnityからエクスポートしたFBXを直接Three.jsで使用してもよいのですが、FBXよりもglTFの方が扱いが楽なのでglTFに変換しています。

この記事で使用するUnity、three.jsのバージョンは次のとおりです。

- **Unity:** 2021.2.10f1
- **Three.js:** r139

## Unityでシーンを作成する

まず、エクスポートするシーンをUnityで作成します。今回は複数のメッシュとカメラを配置します。カメラにはアニメーションをつけておきます。エクスポート対象を一つのゲームオブジェクト(`Root`)にまとめておきます。
![UnityScene.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/a3880728-3c9a-3388-9ce1-dc5cb7a0dc81.png)
カメラアニメーションはこんな感じです。
![UnityScene.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/c029b24a-9bb8-dc81-eb34-01a988b94cc6.gif)

## FBX ExporterでFBXをエクスポートする

UnityにFBX Exporterをインストールします。FBX ExporterはPackage ManagerのUnity Registryに存在しています。

https://docs.unity3d.com/Packages/com.unity.formats.fbx@4.1/manual/index.html

FBX Exporterをインストール後、エクスポート対象をまとめたゲームオブジェクトを選択状態にして`Menu > GameObject > Export to FBX...`をクリック(またはゲームオブジェクトを右クリックして、`Export to FBX...`をクリック)します。`Export Options`が表示されるので、次のように設定して`Export`ボタンを押すと、指定したパスにFBXがエクスポートされます。
![FbxExporterSetting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/0117a71a-a47e-6563-12f7-8a640112026d.png)

## FBX2glTFでFBXをglTFに変換する

UnityからエクスポートしたFBXをFBX2glTFでglTFに変換します。

https://github.com/facebookincubator/FBX2glTF

自分でビルドしてもいいですが、[Releaseページ](https://github.com/facebookincubator/FBX2glTF/releases)から各OSごとにビルド済みの実行ファイルを取得することができるので、今回はこちらを使用します。

Macの場合はターミナルから次のようにglTFに変換します。FBX2glTFではdroco圧縮などの多数のオプションがありますが、今回は単純にバイナリ形式(GLB)に変換するだけにします。実行すると`UnityToThreejs.glb`が同じディレクトリに生成されます。

```
$ chmod +x ./FBX2glTF-darwin-x64 
$ ./FBX2glTF-darwin-x64 --binary ./UnityToThreejs.fbx 
```

## Three.jsでglTFを読み込む

作成したglTFをThree.jsで読み込みます。読み込みにはGLTFLoaderを使用します。

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Unity to Three.js</title>
    <style>
      body {
        margin: 0;
        overflow: hidden;
      }
    </style>
  </head>
  <body>
    <script src="./lib/threejs/three.min.js"></script>
    <script src="./lib/threejs/examples/loaders/GLTFLoader.js"></script>
    <script>
      const renderer = new THREE.WebGLRenderer();
      renderer.setSize(window.innerWidth, window.innerHeight);
      renderer.setPixelRatio(window.devicePixelRatio);
      document.body.appendChild(renderer.domElement);

      const scene = new THREE.Scene();
      scene.background = new THREE.Color(0x999999);

      const dirLight = new THREE.DirectionalLight(0xffffff, 1.0);
      dirLight.position.set(-1.0, 3.0, -1.0);
      scene.add(dirLight);
      const ambLight = new THREE.AmbientLight(0x333333);
      scene.add(ambLight);

      let camera = null;
      let mixer = null;

      const loader = new THREE.GLTFLoader();
      loader.load('./assets/UnityToThreejs.glb', (gltf) => {
        // アニメーションの再生
        mixer = new THREE.AnimationMixer(gltf.scene);
        gltf.animations.forEach((animation) => {
          mixer.clipAction(animation).play();
        });

        const gltfCamera = gltf.cameras[0];
        camera = new THREE.PerspectiveCamera(
          gltfCamera.fov,
          window.innerWidth / window.innerHeight,
          // カメラのnearとfarはFBXに変換したときにセンチメートル単位になり、
          // FBX2glTFではメートルに変換してくれないので、ここでメートル単位に変換する
          gltfCamera.near * 0.01,
          gltfCamera.far * 0.01
        );
        // カメラアニメーションに存在する謎回転に対処するために、
        // 相殺する回転をかけてglTFカメラの子要素に追加する
        camera.rotation.set(0, -0.5 * Math.PI, 0);
        gltfCamera.add(camera);

        scene.add(gltf.scene);
      });


      function resize() {
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(window.innerWidth, window.innerHeight);
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
      }
      window.addEventListener('resize', resize);

      const clock = new THREE.Clock();

      function animate() {
        requestAnimationFrame(animate);

        // アニメーションの更新
        if (mixer) {
          mixer.update(clock.getDelta());
        }

        if (camera) {
          renderer.render(scene, camera);
        }
      }
      animate();
    </script>
  </body>
</html>
```

カメラアニメーションがなぜかY軸に対して90度回転するので、glTFで読み込んだカメラを直接使わずに、回転を相殺するように`rotation`を設定したカメラを子要素に追加して、そのカメラで描画するようにしています。
(カメラがY軸90度回転する問題はFBXを読み込んだときにも発生し、FBXの場合はアニメーションの有無に関わらず発生します。glTFの場合はアニメーションがなければ発生しません。おそらくFBX2glTFがアニメーションがない場合の回転は修正するが、アニメーションの回転は修正しないのではないかと思っています。)

Three.jsの描画は次のようになります。これでUnityのシーンをエクスポートしてThree.jsで描画することができました。
![ThreejsScene.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/44d3049c-8dfb-58cf-a686-97a942ab615d.gif)

## 比較
上がUnity、下がThree.jsの描画です。ライティングやマテリアルなどの違いがあるので全く同じようには描画できていませんが、モデルの配置やカメラワークは同じようにすることができました。
![UnityScene.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/c029b24a-9bb8-dc81-eb34-01a988b94cc6.gif)
![ThreejsScene.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/44d3049c-8dfb-58cf-a686-97a942ab615d.gif)