カメラの画面でUnityアプリ!? THETA XでUnityを使ったプラグイン開発。

## はじめに
リコーの[YuukiS](https://qiita.com/Yuuki_S)です。
弊社ではRICOH THETAという全周囲360度撮れるカメラを出しています。
RICOH THETA V, RICOH THETA Z1, RICOH THETA Xは、OSにAndroidを採用しています。Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます（詳細は本記事の末尾を参照）。

## ついにTHETAに液晶ディスプレイが！
2022年7月22日に発売された[THETA X](https://theta360.com/ja/about/theta/x.html)には、タッチ可能なディスプレイが搭載されています。
これにより、これまでのTHETAでは出来なかったカメラ単体でのライブビューやプレビューが可能になり、撮影体験が向上しています。
さらに、従来のTHETA VやZ1と同様にAndroidを搭載していますので、Androidアプリも画面に表示し操作する事が可能です。

せっかくAndroidが動いてタッチパネルも搭載されたTHETA X。
どうせならリッチな3D表現もしてみたい、と思い立ってUnityアプリを動かす試行錯誤をしてみました。
![THETAXでUnity.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/89e12cd3-e4c5-bd5b-d468-636d6d18bd5b.gif)
その結果が、この通り。
無事Unityで作ったアプリケーションが動作しました。
ただ、なかなか工夫したポイントもいくつかあり、今回はそのポイントを紹介します。
※Unityの表示確認に[SDユニティちゃん](https://unity-chan.com/download/releaseNote.php?id=SDUnityChan)を利用させて頂きました。(© Unity Technologies Japan/UCL)

## Unity as a Libraryを使う
今回、まず最初にUnity単体でビルドしたアプリをインストールし動かしてみました。
その結果、アプリの動作は問題ないもののTHETA固有のハードウェアボタンの押し込みを検出できないことが判明しました。
(Input.GetKeyDown等で色々試したのですが駄目だったので、もし動作した方がいましたらコメント下さい)
THETA Xの標準のプラグインは、Modeボタンの長押しで終了する仕様になっているので、このボタンが検出できないとアプリ終了動作を共通に出来ません。
なので、今回は描画部分にのみUnityを利用するUnity as a Libraryという枠組みを利用することにしました。
### Unity as a Libraryとは
Unityの機能をフレームワークとしてネイティブアプリから読み出して利用できる機能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/74b7e2f6-ad3f-9b96-28ce-8b3ced58aa83.png)
→[公式ページリンク](https://unity.com/ja/features/unity-as-a-library)

これを利用すると例えばUI部分はユーザが慣れ親しんだiOSやAndroidのネイティブ表示を利用しつつ、複雑な3D描画はUnityを利用するといったことが可能になります。

今回は下図の様にAndroidのネイティブアプリを新規で構成し、THETAの一般機能の制御にはTHETA Plug-in Library、画面描画はUnity as a Libraryを用いる構成にしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/6d431cad-85bd-50e2-50d0-e2a75c0e76b8.png)

これにより、Modeボタン等の振る舞いはネイティブアプリのTHETA Plug-in Libraryで管理することが出来るので、前述したアプリ終了動作の問題が解消できます。

### Unity as a Libraryの利用準備
Unity as a Libraryでは、Unity側のシーンはUnityで構築し、それをエクスポートしてネイティブアプリの開発環境(Android Studio)で取り込んでビルドします。
今回、この一連の流れは@cha84rakanalさんの以下の記事を大変参考にさせて頂きました。
[AndroidアプリにUnity as a Libraryでリッチな3Dを簡単に組み込む](https://qiita.com/cha84rakanal/items/4a1e4b242ec40e3e8121) 

ほぼ上記の記事を踏襲すれば準備は完了ですが、自分の環境では下記エラーが出てAndroid Studioでビルドが通りませんでした。
```
Could not get unknown property 'unityStreamingAssets' for object of type com.android.build.gradle.internal.dsl.AaptOptions
```
調べると、どうやらUnity2020以降で発生するエラーのようでgradle.propertiesファイルに以下の1行を追加することで解消されました。
```
unityStreamingAssets=.unity3d
```

### THETA Plug-in Libraryの導入
THETAの各機能を便利に利用するため、THETA Plug-in Libraryを導入します。
導入の方法は、[GitHub](https://github.com/ricohapi/theta-plugin-library)のHow to Useに従えば大丈夫です。
### MainActivityの実装
以上でUnity側のシーンの利用とTHETA側のプラグイン利用の準備が整ったので、MainActivityを実装していきます。
と言っても大部分は下記の記事を踏襲します。
[AndroidアプリにUnity as a Libraryでリッチな3Dを簡単に組み込む](https://qiita.com/cha84rakanal/items/4a1e4b242ec40e3e8121) 

THETA向けの変更点としては、継承するクラスをPluginActivityに変更し、下記のようなTHETA特有の制御を入れました。
- **onResume()で notificationWebApiCameraOpen()を呼び出し**
    今回、Unity側からのTHETA制御にはTHETA Web APIを利用しました。
    [”RICOH THETA X におけるプラグイン開発”](https://qiita.com/KA-2/items/35d7ef550d2aead50cb4)の記事にある通り、THETA XではプラグインからのWeb API利用/終了には手続きが追加されました。
    よって、onResume時にこの手続きを行い、Unity側からWeb APIを利用できるようにしています。
- **onPause()で、notificationWebApiCameraClose()を呼び出し**
    同様に終了時の手続きも行います。
- **onDestroy() でunityPlayer?.quit()を呼び出し**
    Modeボタンが長押しされた場合呼ばれるonDestroy()内で、Unity側の処理も終了する様にしています。

以上でMainActivityの実装も完了です。

<details><summary>(参考までにMainActivity全体のコードも折り畳んで記載しておきます)</summary><div>

```MainActivity.kt 
package com.yuukis.uaal4thetax

import android.os.Bundle
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import com.theta360.pluginlibrary.activity.PluginActivity
import com.unity3d.player.UnityPlayer

class MainActivity : PluginActivity() {

    var unityPlayer: UnityPlayer? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        unityPlayer = UnityPlayer(this)
        window.clearFlags(1024)

        findViewById<ConstraintLayout>(R.id.unity)?.addView(
            unityPlayer, ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
            )
        )
        unityPlayer?.requestFocus()
    }

    // Notify Unity of the focus change.
    override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        unityPlayer?.windowFocusChanged(hasFocus)
    }

    override fun onResume() {
        super.onResume()
        unityPlayer?.resume()
        notificationWebApiCameraOpen()
    }

    override fun onPause() {
        super.onPause()
        unityPlayer?.pause()
        notificationWebApiCameraClose()
    }

    override fun onDestroy() {
        super.onDestroy()
        unityPlayer?.quit()
    }
}
```
</div></details>

### Unity側からTHETAの制御をおこなう
ここまでで、ネイティブアプリ側からTHETA Plug-in Libraryを利用しつつUnityのシーンが描画出来るようになりました。
ただ、これだけでは単にディスプレイ上に表示が出るのみなので、画面のボタンを押したらTHETAの撮影が出来るようにしてみましょう。

これには、ネイティブアプリ側の画面にボタンを追加しPlug-in Libraryから撮影をする方法と、Unityシーン側にボタンを追加しWeb APIから撮影する2通りの方法が存在します。
今回はUnity側でUIを完結したかったため、後者を選択しました。
(ちなみにUnity as a LibraryではネイティブとUnity間で通信する仕組みもあるので、これを利用してUnityシーン側のボタンが押されたことをネイティブアプリ側に通知し、ネイティブアプリ側でPlug-in Libraryから撮影をすることも可能だと思います。)

UnityからWeb APIを利用する例は、過去の記事["UnityちゃんとTHETAでMixedRealityな記念撮影を"](https://qiita.com/Yuuki_S/items/0cd99e51678fe55ba7a7)で紹介しています。

具体的なコードは以下の通りです。
```c#
 IEnumerator sendTakePictureSignal()
    {
        string url = "http://127.0.0.1:8080/osc/commands/execute";
        takepicObject.name = "camera.takePicture";
        settedJson = JsonUtility.ToJson(takepicObject);
        byte[] postData_takePic = System.Text.Encoding.UTF8.GetBytes(settedJson);
        var request_takePic = new UnityWebRequest(url, "POST");
        request_takePic.uploadHandler = (UploadHandler)new UploadHandlerRaw(postData_takePic);
        request_takePic.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
        request_takePic.SetRequestHeader("Content-Type", "application/json");
        yield return request_takePic.SendWebRequest();
        if (request_takePic.isNetworkError || request_takePic.isHttpError)
        {
            Debug.Log(request_takePic.error);
            Debug.Log(request_takePic.downloadHandler.text);
        }
        else
        {
            Debug.Log(request_takePic.downloadHandler.text);
        }
    }
```
この関数を画面に配置したボタンのonClickに割り当てて、完成です。
![コンポ 1_2_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/55dc775e-08c1-67c5-8ff8-a3ef12bd2497.gif)
GIFでは分かりにくいですが、画面のボタンを押すとシャッターが切れています。
(せっかくなので、同時にユニティちゃんにジャンプしてもらいました)

## まとめ
今回は、THETA XでUnityを活用する試みを紹介しました。
THETAで撮影できる全天球画像と3Dコンテンツは相性が良いので、色々出来ることもありそうです。
皆さんもぜひ、遊んで試してみてください。

# RICOH THETAプラグインパートナープログラムについて 
THETAプラグインをご存じない方は[こちら](https://api.ricoh/products/theta-plugin/)をご覧ください。
パートナープログラムへの登録方法は[こちら](https://qiita.com/mShiiina/items/55d98f366e650ca42251)にもまとめてあります。
Qiitaの[RICOH THETAプラグイン開発者コミュニティ TOPページ「About」](https://qiita.com/organizations/theta-plugin)に便利な記事リンク集もあります。
興味を持たれた方は[Twitter](https://twitter.com/thetaplugin)のフォローと[THETAプラグイン開発コミュニティ(Slack)](https://join.slack.com/t/thetadeveloper/shared_invite/enQtNzcxODI2MTk2ODg0LWY5YmU2ZTIxNGYwNzlmNWZjMWMyMWFlMzY3ZmQ3NGU3ZjZiZjI0Njk4MGIxMDU5NDJiMzQ4Y2FkNDEzYmFlNGU)への参加もよろしくおねがいします。