UnityちゃんとTHETAでMixedRealityな記念撮影を

#はじめに
リコーの[Yuuki_S](https://qiita.com/Yuuki_S)です。
弊社では[RICOH THETA](https://theta360.com/ja/)という全周囲360度撮れるカメラを出しています。
RICOH THETA VやTHETA Z1は、OSにAndroidを採用しており、Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます。(詳細は本記事の末尾を参照）。
***
さて、THETAの様な全天球カメラは360度撮影できるのでVRとの相性が良いです。
撮影した全天球画像をHMDに入れれば、そのまま撮影した瞬間の空間にダイブするが如く見ることが出来ます。

ところで、VRというと3D CGのキャラクターを利用したコンテンツが多くあります。
共にVRと相性の良い全天球画像と3D CG、一見すると組み合わせ易そうですが、実はそうではありません。
全天球画像に3D CGのキャラクターを合成しようとすると、これが中々難しいです。

難しい理由は、画像に奥行き情報が存在していないため。
全天球画像はHMDで見ていると一見奥行きもあるように感じますが、あくまで画像は2次元情報でそれを球体に展開しているだけです。(世界地図と地球儀の関係)

残念ながら現状のTHETAには奥行きを検出する手段はありません。
ですが、プラグインやAPIを介することで他のデバイスの力を借りる事ができます。
今回は、奥行きを取得できるデバイスとしてHololensを利用し、好きな場所にキャラクターを配置しMixedRealityな記念撮影ができるアプリを作ってみました。


まずは、動作の様子と撮影結果をご覧ください。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">◤Qiita記事公開◢<br>THETAでUnityちゃんとMixed Realityな記念撮影を。<a href="https://t.co/BbCi9JvJAa">https://t.co/BbCi9JvJAa</a><a href="https://twitter.com/hashtag/THETA?src=hash&amp;ref_src=twsrc%5Etfw">#THETA</a> <a href="https://twitter.com/hashtag/thetaplugin?src=hash&amp;ref_src=twsrc%5Etfw">#thetaplugin</a> <a href="https://twitter.com/hashtag/Unity?src=hash&amp;ref_src=twsrc%5Etfw">#Unity</a> <a href="https://twitter.com/hashtag/MixedReality?src=hash&amp;ref_src=twsrc%5Etfw">#MixedReality</a> <a href="https://twitter.com/hashtag/xR?src=hash&amp;ref_src=twsrc%5Etfw">#xR</a> <a href="https://twitter.com/hashtag/Hololens?src=hash&amp;ref_src=twsrc%5Etfw">#Hololens</a> <a href="https://t.co/PgRybViCef">pic.twitter.com/PgRybViCef</a></p>&mdash; THETAプラグイン開発者コミュニティ (@thetaplugin) <a href="https://twitter.com/thetaplugin/status/1240465608342269952?ref_src=twsrc%5Etfw">March 19, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

撮影結果
<blockquote data-width="500" data-height="375" class="ricoh-theta-spherical-image" >Post from RICOH THETA. - <a href="https://theta360.com/s/cAnk5tzqcHWGcB0B1NGwfBK52" target="_blank">Spherical Image - RICOH THETA</a></blockquote>
<script async src="https://theta360.com/widgets.js" charset="utf-8"></script>


#システム構成と仕組み
![system_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/5356191f-395b-e977-9edd-ab6d9f427eb3.png)
今回の欲しい空間の3D情報取得は、Hololensの基本機能であるSpatialMappingを利用します。
SpatialMappingは、Hololensのセンサで空間の奥行き取得し、下図の様に3Dのメッシュデータとして保存、利用するものです。

これにより、現実空間に整合してキャラクターを配置したり、位置関係による隠面消去(オクルージョン)表現が可能になります。
![spatialmapping_example.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/3f17110a-6ebf-9baf-50e6-08d14d7b34d1.jpeg)
[UnityのHololensマニュアルより引用](https://docs.unity3d.com/ja/2018.4/Manual/SpatialMapping.html)

そして、空間にCGキャラクターを配置した後、現実空間のTHETAに撮影トリガを飛ばし、
撮影画像を受信すると同時にHololens側でもTHETAの位置から見たCGキャラクターを含む仮想全天球画像を取得、それらを合成することで、CGキャラクターとの記念撮影が実現できます。
![THETA.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/91be00e6-fecc-879f-62d9-3b66fea06e8e.png)



#Hololens側の実装
開発はUnityを利用します。
細かな環境構築に関しては割愛しますが、SpatialMappingや入力まわりはMRTK(Microsoft Mixed Reality Toolkit)をインポートして使っています。

目的であるCGキャラクターとの記念撮影を実現するためには、
現実のTHETAと同じ位置関係でCGキャラクターが写った仮想の全天球画像を得る必要があります。

そのために、今回はTHETAの位置をHololens側のUIで指定する方法を用いました。
具体的には、Hololens側に仮想THETAを表示し、その位置を現実のTHETAに重ねるという力技です。
(ちなみに仮想THETAモデルは20分ぐらいかけてBlenderで作りました。)
![THETA位置合わせ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/cd7c0f8c-e039-80af-8f30-20a9900f71d6.png)

これで、現実世界のTHETAの位置をHololensのSpatialMapping上で把握することが出来ましたので、
あとは撮影ボタンを押したらTHETAの位置から仮想全天球画像を取得する仕組みを作ります。

Unityでは、任意の位置からキューブマップという全方位が写った立方体テクスチャを得ることが出来ます。
これをTHETAの画像と同じequirectangularに変換すれば仮想全天球画像が得られます。
このアイディアを実装しようと調査した所、まさにそのものを公開されている方が居たので利用させて頂きました。
@mechamogeraさんの[Unityでカメラのequirectangular画像を作成してみる](https://qiita.com/mechamogera/items/0b47e5947f1eee2467da)


#THETAとの通信
今回、THETAとの通信内容は、撮影トリガと撮影画像の受信のみです。
なので、THETAのAndroidプラグインではなく[THETA Web API](https://api.ricoh/docs/#ricoh-theta-api)を利用しました。
THETA Web APIは他の機器からTHETAをリモート制御する時に利用でき、撮影に加え、THETA内のAPIサーバーにアクセスして撮影画像も受け取れます。
APIのやり取りに利用するJSONですが、UnityではJsonUtilityという便利な仕組みがあるので今回はそれを利用します。

撮影トリガとその後の画像取得は下記の通りです。一定の時間がかかるのでコルーチンによる非同期処理を行っています。

```cs
...
 IEnumerator StartThetaTask()
    {
        //撮影開始トリガ
        url = "http://192.168.1.1:80/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.Send();

        //結果反映まで待つ
        yield return new WaitForSeconds(3);

        //確認
        url = "http://192.168.1.1:80/osc/checkForUpdates";
        checkUpdateObject.stateFingerprint = "FIG_0003";
        settedJson = JsonUtility.ToJson(checkUpdateObject);
        byte[] postData_checkUpdata = System.Text.Encoding.UTF8.GetBytes(settedJson);
        var request_chckUpdate = new UnityWebRequest(url, "POST");
        request_chckUpdate.uploadHandler = (UploadHandler)new UploadHandlerRaw(postData_checkUpdata);
        request_chckUpdate.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
        request_chckUpdate.SetRequestHeader("Content-Type", "application/json");
        yield return request_chckUpdate.Send();

        //撮影画像の取得
        url = "http://192.168.1.1:80/osc/state";
        var request_state = new UnityWebRequest(url, "POST");
        request_state.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
        request_state.SetRequestHeader("Content-Type", "application/json");
        yield return request_state.Send();
        //gettedObject = JsonUtility.FromJson<gettedState>(request_state.downloadHandler.text);
        int appear_http = request_state.downloadHandler.text.LastIndexOf("_latestFileUrl");
        int appear_JPG = request_state.downloadHandler.text.LastIndexOf(".JPG");
        DataURL = request_state.downloadHandler.text.Substring(appear_http + 17, appear_JPG - 13 - appear_http);

        url = DataURL;
        var request_download = new UnityWebRequest(url, "GET");
        request_download.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
        request_download.SetRequestHeader("Content-Type", "application/json");
        yield return request_download.Send();
        Debug.Log(request_download.responseCode);
        Debug.Log(request_download.error);
        string directorypath = "";

        //画像の保存
#if WINDOWS_UWP
        //HoloLens上での動作の場合、LocalAppData/AppName/LocalStateフォルダを参照する
        directorypath = Windows.Storage.ApplicationData.Current.LocalFolder.Path;
#else
        // Unity上での動作の場合、Assets/StreamingAssetsフォルダを参照する
        directorypath = UnityEngine.Application.streamingAssetsPath;
#endif
        File.WriteAllBytes(directorypath + "/" + datetimeStr + "_RealEquirePIC" + ".JPG", request_download.downloadHandler.data);

        THETAPicTexture.LoadImage(request_download.downloadHandler.data);
        TargetMaterial_THETA.SetTexture("_MainTex", THETAPicTexture);
        TargetMaterial.SetTexture("_MainTex", THETAPicTexture);
    }
```

#実際の動作
実際の撮影は以下の④ステップでおこないます。
それぞれの動作はボタンを押すことで操作できるUIにしました。
①現実のTHETAの位置を決め、その位置に仮想THETAを重なるように設置します。
![設置.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/a3c86801-9d62-885c-7fe6-5eb11c0c8001.png)

②キャラクターを配置します。
今回はせっかくUnityを使用しているのでUnityちゃん(© Unity Technologies Japan/UCL)を利用しました。
視線の先が先程の仮想THETAに自動で向く様にスクリプトを追加しています。
![設置2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/8dca73ab-891a-eab8-29af-49a529d65185.png)

③撮影ボタンを押して撮影します。
すぐに撮影されるとHololensを操作している姿勢のまま撮れてしまうので、3秒セルフタイマーにしました。
![設置3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/d0e68145-43ca-1abf-c77c-2354da6c2a4a.png)

④少し待つとTHETAから撮影画像が送られてきます。
その画像とアプリ内で取得した仮想全天球画像を重ねた画像がプレビュー表示され、同時にそれぞれが保存されます。
![設置4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/ff541e96-2420-e43e-3a6f-4075577c0b8c.png)

上記の4ステップで目的の記念撮影が出来ました！
SpatialMappingの奥行き情報によって、Unityちゃんの椅子に隠れた部分はちゃんと非表示になっているのが分かると思います。
![完成.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/7809cff3-4ad2-cdbd-84bb-2473a3005cf0.png)


#まとめ 
今回は、CGのキャラクターと記念撮影できる全天球合成画像生成アプリを作ってみました。
現時点では、オクルージョンを中心とした配置の整合性のみ考慮していますが、せっかくTHETAの全天球画像を受け取っているので、それを利用して光源推定(Image-based lighting)をおこなうと、MixedRealityらしい溶け込んだCG表現になると思います。

また、今回はHololensを利用しましたが、Kinectに代表されるデプスセンサと組み合わせても(範囲は限定されますが)同様のことが出来ると思います。



#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)への参加もよろしくおねがいします｡