360度動画を使って、楽にInstant-NGPで3Dシーンを再現する話

# 概要
こんにちは！
VRChatを中心にXR関係の活動をしているサックーと申します。
この記事は、NVIDIAが公開した高速なNeRF技術である[Instant-NGP](https://github.com/NVlabs/instant-ngp)に360度動画を素材として使うことで、楽に3Dシーンの再現ができた話です。
新しい(？)部分としては、Unityを使って360度動画から2D画像を切り出しているところかなと思います。
また、これは[IwakenLabアドベントカレンダー](https://qiita.com/advent-calendar/2022/iwakenlab)6日目の記事です

### 使用技術
 - Unity 2021.3.4f1 BRP
### 撮影機材
 - insta360 X3

# ビルドを公開しました！(2023/3/18追記)
[こちら](https://github.com/sack-kazu/360VideoToPictures)からダウンロードしてお使いください。
![image.png](https://user-images.githubusercontent.com/52432227/225943348-062dffca-6830-4834-9007-a8069f4fd3ac.png)

# きっかけ
さて、件のリポジトリ公開後Twitterでいろんな人がInstant-NGPを試しているのを見て、自分もやってみたところ想像以上に簡単に高精度なものができて普通に感動していました。
しかし自分は登山が好きでして、そういった自然の大きな空間を3Dで再現したいなあと思ったのですが、そういった場所で大量の写真を撮るのは大変ですし山域によっては危険です。
そこで、動画からも画像を切り出して素材として使えると見たのでやってみたのがこちらです。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">去年撮った山の動画からこうなった<br>４K映像だったらもう少しきれいになるだろうか<br>とんでもない技術だ… <a href="https://t.co/YlnSxxrjKg">pic.twitter.com/YlnSxxrjKg</a></p>&mdash; サックー@XR (@VRC_Sakk) <a href="https://twitter.com/VRC_Sakk/status/1574780803074363392?ref_src=twsrc%5Etfw">September 27, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
過去に適当に撮った動画からこういった3D空間際限ができるのは、すごい可能性を感じました。

しかしこれもまだ弱点があります。
通常の動画では、歩きながら撮った場合に正面しか映らないため、出来上がる空間は歩いたのと同じ方向からしか見ることができないものになってしまうのです。
そこでついに登場するのが360度動画です。360度動画であれば全方向の見え方をカバーできるのではないかと。
検索してみたところ、すでに先駆者の方がいらっしゃいました。
フォトグラメトリやシェーダーなどを中心にXR領域で活躍されているVoxelKeiさんです。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">360度素材からの90度切り出し法でInstant NeRF出来た。撮影はinsta360 one X2。次は1inch版で試す。 <a href="https://t.co/u030srczC8">pic.twitter.com/u030srczC8</a></p>&mdash; VoxelKei (@VoxelKei) <a href="https://twitter.com/VoxelKei/status/1550043652105596930?ref_src=twsrc%5Etfw">July 21, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
360度動画から画像を切り出して適用することできれいに再現できていることが分かります。
ただその切り出しに使えるツールがあったわけでは無いようでした。
そこで自分でそんなツールを作ろうと思い立ち今回の件につながるわけです。

# 基本の考え

技術的にすごいことは何もなく、素朴な実装をしています。
1. 360度動画をRenderTextureに流す。
1. SkyboxのMaterialにそのRenderTextureを設定する。
1. シーンにカメラを4つ90度ずつ配置する。
1. 再生しながら等間隔でカメラの画をpngに保存する

とはいえそこまですんなりいかなかったとこもあり、詳細を書いていきます。

# 詳細
### Skyboxで360度動画を再生する
ここはすんなりいけました。
RenderTextureのSizeに映像の解像度を指定するくらいでしょうか。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2341187/0bea70a4-baa6-55bf-6209-e78557d43ab4.png)
Skybox用のMaterialのShaderはSkybox/Panoramicを使います
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2341187/0608f8b3-d79c-dcda-706d-f3feb3c2d7f2.png)
これをSkyboxにしてシーンを再生すれば全天球で動画が再生されます。

### 指定した間隔で画像を保存する
これが割と難敵でした。
カメラの画をpng保存する方法は[こちら](https://nekojara.city/unity-screenshot)などを参考にしましたが、どうやってもこの処理は重いです。
そしてそれを4つのカメラで同時にやるのでとても重いです。
最初は非同期処理で並列してやれば軽いのではと思いましたが、それぞれのカメラでキャプチャするタイミングがずれるので断念しました…
一番の問題は動作が重すぎて、動画の再生フレームとスクリプトの実行フレームがぐちゃぐちゃになり、等間隔での撮影ができないことでした。

そこで利用したのが[VideoPlayer.frameReady](https://docs.unity3d.com/ScriptReference/Video.VideoPlayer-frameReady.html)です。
これはVideoPlayerで各フレームの描画準備ができたときに発火するイベントになっています。
ただしこれ
```
videoPlayer.sendFrameReadyEvents = true;
```
をしないと発火しないので注意です。
あとは指定したフレームレートでキャプチャをできるようにして完成しました。
ただこれでも動作が怪しかったので、結局動画の再生速度を0.2倍にしてどうにかしました…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2341187/11d7f5fc-774e-acc3-57e5-2668d92018ba.png)
### カメラのFOVの設定機能
個々のカメラの水平視野角を設定できる機能も付けました。
エディタのInspectorでは水平の視野角を直接指定できますが、スクリプトからだと垂直視野角しか設定できないので変換してやる必要がありました。
[こちら](https://zenn.dev/ai2playgame/articles/unity-camera-fieldofview)の記事を参考にさせていただきました。


# できたもの
できたものがこちらです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2341187/1f6856ae-b9db-1874-20e9-ee549cc3fda3.png)
機能としては
- 画像書き出し先指定(直書きですが)
- 切り出し元動画指定
- キャプチャフレームレート指定
- カメラの水平視野角の指定
- 使用カメラの指定（一応何個でもできる）

があります。これを使うことで、360度動画からこのような連番画像を入手できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2341187/0a4e734d-375b-14ca-039c-3797314f94e0.png)
そしてこの画像から再現したのがこちらです。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ここの360度動画からNeRFできた！<br>どうやら徒歩スピードだと2fpsくらいでの切り出しが必要っぽいことが分かってきた <a href="https://t.co/ma1YoeMSXb">https://t.co/ma1YoeMSXb</a> <a href="https://t.co/Va6npSlhVU">pic.twitter.com/Va6npSlhVU</a></p>&mdash; サックー@XR (@VRC_Sakk) <a href="https://twitter.com/VRC_Sakk/status/1583674239554846721?ref_src=twsrc%5Etfw">October 22, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
元動画を見てもらえばわかるのですが、録画しながら普通に歩いただけのものからこうして再現ができました。
もちろん1枚1枚写真を撮ってやったほうがきれいにはなりますが、お手軽さはすごいと思います。

### おまけ　なぜカメラのFOV設定機能を付けたか
なぜカメラの水平視野角を設定する必要があるのかというと、この値が最終的な3Dシーンの精度に影響するからです。
最初単純に90度で切り出したところ、カメラ位置の推定がうまくいかず、同一店からの撮影として認識されませんでした。
これは画像間の重なりが一切なかったためと思われます。
そこで120度でやってみたところうまく推定してもらうことができました。
いくつか値を試すと95度でもうまくいき、120度の時よりきれいな気がしました。
広角にすればそれだけ絵もゆがむのでその影響もあるのかなと思います。
研究のしどころかもしれません。（他力本願）

### おまけ２　撮影時のポイント
ただ歩いて撮るだけと言いましたが、おそらくジグザグに歩いたほうが良いと思います。
元々の前提として同じものを複数の方向から撮るというのがあるので、ジグザグに歩くことで多少角度を補うことができる気がします。
上で紹介したVoxelKeiさんの動画でもそうされているように見えます。
自分は意識してなかったのですが、山だと傾斜を緩くするためにジグザグ歩くので、それが幸いした可能性があります。

# まとめ
広域のフォトグラとかすごいけど大変そうだなあなどと思ってしまうめんどくさがりでしたが、360度カメラとInstant-NGPによってお手軽に広い空間の3D再現ができました。
もし360度カメラと強いグラボをお持ちの方はぜひやってみてください！