Unity でステレオ VR 動画を作成する (ほぼ完全 (?) 版)

## はじめに

この記事は [Unity 2018.1 で VR180 動画を作成する](https://qiita.com/tan-y/items/c4b09d2f92a60fda6113#_reference-511d4328583a8209a292) の続きです。

[Unity 2018.1 でステレオ VR 動画を作成する](https://qiita.com/tan-y/items/ea824684077b0a544421) のまとめで

>実は一番はまってたのは今回記事にした向きの問題ではなく cubemap 作成の方なのですが、解消していません (記事でも触れていません) 。今回の記事の例ではあまり顕在化しないようにしています。こちらはできればまた改めて記事にしたいと思います。

と書いた件について、解消するためにあれこれ調査、実験をした結果のまとめ記事になります。コード的には VR180 対応も含んでいます。

で結果的に Unity 2018.1 の新機能は使い物にならず、全く使わずに目的を達してしまっています (のでタイトルから 2018 を外しました) 。よって 2018 より前でも (おそらく Unity 5.6 以降) この記事の手法であれば適用可能と思います。がサンプルコードは以前の記事の 2018 の機能を使うコードも残したままにしている (選択可能) ので、もし以前の Unity で使いたい場合は適宜修正が必要になると思います。

2018 より前のバージョンでも動く、ということは旧来から存在している方法と基本的には同じかと思いますが、その手法の解説記事でもあります。今後の Unity 本体や Recorder アセットのバージョンアップによって状況は変わっていくとは思いますが、その時は「こんな話もあった」程度の認識を持っていていただければと。

今回は (も) ソースとして "Candy Rock Star" を使わさせてもらっています。というか "Candy Rock Star をちゃんと Equirectangluar でキャプチャする" のが目的の記事です(ぇ。

実装したコードは [サンプルプロジェクト](https://github.com/aosoft/UnityVRVideoRecordingTest) で公開していますが、そちらから必要なコードを unitychan-crs へコピーすることで試すことが可能です。

* [Unity-Chan "Candy Rock Star" Public Repository](https://github.com/unity3d-jp/unitychan-crs)

この記事は技術解説記事なので、技術解説を先に書いています。今回作成したコードを使って VR 動画の作成をしたい方は "サンプルプロジェクトの使い方" まで飛ばしてください。

使用している Unity のバージョンは 2018.1.0f2 です。 2018.2.0b7 も軽く確認してみましたが 2018.2.0b7 ではレンダリング内容が少し改善しているようですが、本質的には解消していないようです。本記事の画像は全て 2018.1.0f2 でのものです。 Recorder アセットは 2018/1/18 時点のものです。

Recorder アセットの "360 view" でキャプチャするとこんな感じ (2018.1 でのもの) ↓

![180610_1.jpg](https://qiita-image-store.s3.amazonaws.com/0/211064/4e525fd3-bea8-ed14-5ef3-c978596dbb15.jpeg)


これを本記事のサンプルプロジェクトで実装したコードを使ってレンダリング、キャプチャすると↓

![180610_2.jpg](https://qiita-image-store.s3.amazonaws.com/0/211064/6a0138ca-70cd-83d0-17b2-4ed42ecbd906.jpeg)

© Unity Technologies Japan/UCL

※パノラマ映像用にカメラ位置など元のプロジェクトから調整をしていますが、この記事のテーマであるパノラマ映像のレンダリング設定以外は全て同一です。

## これまでで解決できていない問題点と解決方法

Candy Rock Star をソースにすると、向きの問題以外にうまくいかない事が多々あります。

### PNG でキャプチャできない (透明になっている)

PNG でキャプチャしようとすると透明な映像がキャプチャされています。 JPEG 等にすると大丈夫。

これは簡単な話で、単にアルファチャンネルが 0 になっているだけで RGB チャンネルの方は色が入っています。ので単純にシェーダーで アルファを 1.0 にして出力すれば OK でした。

```glsl
float3 color = texCUBE(_MainTex, mul(_Matrix, unit).xyz);
return float4(color, 1.0);
```

ただ、これは Recorder アセットで直接 360 view 出力をした場合は解決できないです。そうするならアセット側の改造が必要。

これ以降はサンプルプログラムを用いて RenderTexture に VR 映像をレンダリングしたものを Recorder アセットでキャプチャしていく、これまでの記事に則った方法で対応します。

### 色がおかしい (暗い) 場合がある

前回記事までの状態でレンダリングしたものがこちら。

![180610_3.jpg](https://qiita-image-store.s3.amazonaws.com/0/211064/a73ccb8a-7654-6aa5-e265-330b221cda7c.jpeg)

全般的に暗めの色で描画されていて、明らかに正しそうな感じがしません。

Frame Debugger で通常のレンダリングの動作を追ってみたりすると、途中まではキャプチャ結果と同じような色合いになっていて、最後のタイミングで正しく見える色になっています。

![180610_8.jpg](https://qiita-image-store.s3.amazonaws.com/0/211064/45823c4c-bb4a-601a-1709-8a49cbc2a0b4.jpeg)

つまり Linear カラーの HDR がそのまま出力されてしまっているってことになります。

であるならこれの解決も簡単で sRGB にガンマ変換してしまえばよいわけです。ここでは CubemapToOtherProjection.shader の Fragment Shader でテクスチャーから取得した色を Linear カラーから sRGB カラーに変換します。

```glsl
float3 color = texCUBE(_MainTex, mul(_Matrix, unit).xyz);
color = color < 0.0031308 ? 12.92 * color : 1.055 * pow(color, 1.0 / 2.4) - 0.055;
return float4(color, 1.0);
```

色とガンマについては興味のある方は別途調べてみてほしいです。ものすごく簡単にいってしまうと、通常 PC で RGB 値を指定している場合、それは sRGB という規格に基づいた色の RGB 値で、 HDR レンダリングでは Linear カラーに基づいた色で処理、出力されるので、変換しないでそれをそのまま出力してしまうと Linear カラーの色を sRGB の色とみなして表示されてしまうのでおかしな色になってしまうのです。

* [ガンマについて](http://compojigoku.blog.fc2.com/blog-entry-23.html)

Unity では Player Settings の "Other Settings - Color Space" が "Linear" になっていた場合、ディスプレイに出力される最後のレンダリングパスでガンマ変換をして戻しているようですが、 **Camera.Render や Caemra.RenderToCubemap の場合、ディスプレイ表示の最終レンダリングパスではないのでガンマ変換がされないようです。**

![180610_5.png](https://qiita-image-store.s3.amazonaws.com/0/211064/cb529857-2aeb-8b76-e73e-e57dff99750f.png)

まあ実際、ここでガンマ変換されても困るので正しいのですが。この設定を "Gamma" にすれば、上記の対策をしなくてもそれっぽい色の絵が出てきますが、そもそも  Color Space が "Linear" に設定されているということはレンダリング処理が Linear 空間前提になっているので、これでは根本的に間違ってしまっています。

本題と若干ずれますが、サンプルコードでは sRGB の他に BT.709 に変換するコードも実装しています。 PC では原則的には sRGB ですが、 SDR の動画ファイルは (多くの場合) BT.709 ですので最終的に動画を生成することを考えると最初から BT.709 にしておいた方が適切かもしれません (エンコーダーが sRGB → BT.709 変換をするなら問題ないですが、そこまで配慮していない可能性もあります) 。これ言い出すと解像度が HD か SD かでもガンマ違うんじゃない？とかキリがなくなってくるところですが気になる場合は調べてみてください。

ちなみにこの件は Recorder アセットの GitHub レポジトリの Issue にもあがってました。

* [360 stereo video captures are too dark](https://github.com/Unity-Technologies/GenericFrameRecorder/issues/65)

### レンダリング結果に全ての処理が反映しきれていない

色の問題が解決した状態がこちら。まだおかしいですね。

![180610_4.jpg](https://qiita-image-store.s3.amazonaws.com/0/211064/65c64c5a-d3b9-793f-6bd4-8ea5621d77ca.jpeg)

通常のレンダリング結果と比べると何か描画が仕切れていないように見えることがあります。 unitychan-crs では床の反射や天井、バックスクリーンの映像などが描画されていません。

まず気が付いたポイントとして **Unity のゲームループの実行位置によってレンダリング結果が変わる** ということです。

* [イベント関数の実行順](https://docs.unity3d.com/ja/current/Manual/ExecutionOrder.html)

Recorder アセットでは共通処理として "yield WaitForEndOfFrame" のタイミング (コルーチンで yield return new WaitForEndOfFrame(); をした直後) でキャプチャ処理が動作するようになっているようですが、 (少なくとも unitychan-crs の場合) このタイミングで Camera.Render / Camera.RenderToCubemap するとかなり処理が抜けた描画結果になってしまっているようです。 Render, RenderToCubemap をするなら Update か LateUpdate のタイミングの方がまだ適切なような感じでした。ただこれはタイミングによって結果が異なるのは理解できる (MonoBehaviour の処理が行われているかどうかで内容が変わるのは当然) のですが、 yield WaitForEndOfFrame はタイミング的には最後の最後なので、LateUpdate などよりこちらの方が問題のない結果が得られそうに思うのでちょっと謎です。

この動作違いは CubemapToOtherProjection を使う場合、 LateUpdate の実装をちょっと変えると確認できるようにしています。

```cs
private void LateUpdate()
{
    if (RenderTarget != null)
    {
        //StartCoroutine(InternalUpdateAsync());
        InternalUpdate();
    }
}
```
コメントアウトしている "StartCorutine" している方を解除し、 InterlUpdate を直接呼んでいる方をコメントアウトしてください。

このタイミングを変えるやり方、 1 フレーム前後ズレる可能性もあるかなーと思いましたが、非リアルタイムレンダリングのキャプチャーを前提としているので、まあそこまで問題ではないかなと思います。音声とのズレがあった場合は後処理で調整しても問題ないでしょう。

こうやってタイミングを変えてで RenderToCubemap をしてみましたが、結果は変わっても望むような結果は得られませんでした。つまり RenderToCubemap では目的を達せられなかったということです。これが普通の Render メソッドだと望む結果が得られている様子。つまり **Render メソッドを使って自前で Cubemap を作る** しかないようです。

Cubemap とは任意位置からの周囲 6 方向それぞれの向きの平面正方形映像を組みとし、周囲の環境の映像として利用するものです。

* [Cubemap](https://docs.unity3d.com/jp/current/Manual/class-Cubemap.html)

ただ今回は環境マップとして使うのが目的ではなく、 VR 動画を作るのが目的です。なので Cubemap にせずに 6 方向の普通のテクスチャにレンダリングし、 Equirectangular 等に展開する際にその 6 枚をソースにしてレンダリングをすればよい、のですが過去記事で Cubemap からレンダリングするフローを確立しているので、ここでは Cubemap を作成することにします。

Cubemap へのレンダリングですが、いろいろ試してみたのですがうまくいったのが **Command Buffer を利用して最終レンダリング結果が得られたタイミングで Cubemap に転写する** という方法です。

* Cubemap へ直接 Render することができなかった。
* RenderTexture から Graphics クラスを使って Cubemap への転写ができそうな感じはしたのですが、これもうまくいかなかった。

CommandBuffer を使って最終レンダリング結果を得る方法は Recorder アセットの [CBRenderTextureInput](https://github.com/Unity-Technologies/GenericFrameRecorder/blob/master/source/FrameRecorder/Inputs/CBRenderTexture/Engine/CBRenderTextureInput.cs) クラスでやってるので参考にさせてもらいました。ここでのポイントは CommandBuffer では Cubemap テクスチャの任意の面に対してレンダリングをする命令が存在して、うまく機能することです。

Cubemap を自前で作る大きなメリットは RenderToCubemap の致命的な問題点である [任意の方向を正面にできない](https://qiita.com/tan-y/items/ea824684077b0a544421#%E4%BB%BB%E6%84%8F%E3%81%AE%E6%96%B9%E5%90%91%E3%82%92%E6%AD%A3%E9%9D%A2%E3%81%AB%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84) 件が Cubemap 作成の段階で解消できる点です。これにより Equirectanglur や Fisheye 画像作るフェーズで補正をする必要がなくなるので

* Equirectanglur の作成に "RenderTexture.ConvertToEquirect" を使えるようになる。 (VR180 には使えないですが)
* VR180 動画の場合、真後ろの映像のレンダリングは省略できる (真後ろは完全に視界の外だから) 。

ということになります。

#### CommandBuffer の機能を使って Cubemap を作成する

CubemapRenderer.cs, CubemapRenderer.shader で実装しています。

まず 6 方向分の CommandBuffer を定義します。

```cs
_commandBuffers = new CommandBuffer[_faces.Length];
for (int i = 0; i < _faces.Length; i++)
{
    var commandBuffer = new CommandBuffer();
    commandBuffer.SetGlobalTexture(tid, BuiltinRenderTextureType.CameraTarget);
    commandBuffer.SetRenderTarget(_cubemap, 0, _faces[i].Face);
    commandBuffer.DrawMesh(_mesh, Matrix4x4.identity, _material, 0, 0);
    _commandBuffers[i] = commandBuffer;
}
```

CommandBuffer で直接 Cubemap の指定の面に対してレンダリングを行うことが可能なので、そこだけ変えた CommandBuffer を 6 つ作成します。

DrawMesh で指定している Material の中のシェーダー (基本的にはただのコピー) で必要に応じてガンマ変換をするようにしています。

```cs
for (int i = 0; i < _commandBuffers.Length; i++)
{
    camera.AddCommandBuffer(CameraEvent.AfterEverything, _commandBuffers[i]);
    try
    {
        camera.transform.localRotation = orgLocalRotation * _faces[i].Rotate;
        camera.Render();
    }
    finally
    {
        camera.RemoveCommandBuffer(CameraEvent.AfterEverything, _commandBuffers[i]);
    }
}
```

面毎にレンダリング処理を行います。

1. CameraEvent.AfterEverything が全てのレンダリング処理が終わった後のタイミングになるのでここに登録をする。
2. 描画したい面の向きにカメラの向きを変える。
3. Render で描画し、 CommandBuffer の登録を外す。

Cubemap の描画は "正方形に対して視野角 90 度の Perspective カメラ" で行いますので、事前にそのような設定をします。

```cs
camera.orthographic = false;
camera.aspect = 1.0f;
camera.fieldOfView = 90.0f;
```

これまでの設定は元設定をキャッシュしておき、処理が終わったら戻すようにしています。

ここまでで冒頭の完成版の映像が取得できるようになりました。

#### ステレオレンダリング

Unity 2018.1 の RenderToCubemap メソッドはステレオレンダリングに対応していますが、ここでは自前で Cubemap のレンダリングをしているのでステレオの対応も自前でやる必要があります。

![180610_9.png](https://qiita-image-store.s3.amazonaws.com/0/211064/6a5fcdd6-2788-a2ba-f81f-3f16f2a72815.png)

ステレオの対応は単純に考えてしまえばカメラの位置を中心に IPD の幅だけ離した位置で左右それぞれで映像をキャプチャすればよい、ということになります。

試してみると一見これで問題ないように見えますが、この状態で今まで通りそれぞれの目を中心にぐるっとまわしてみると、後ろ方向になった時に左右の目が逆になってしまっていますし、真横の向きを見てもおかしなことになっています。

![180610_10.png](https://qiita-image-store.s3.amazonaws.com/0/211064/7c209ecc-ed55-c774-9bb6-fa1c15cc8c2e.png)

これはちょっと考えればわかることですが、実空間を見る向きを変える時、目の位置が固定されていて向きだけ変わる、なんて事態は起きるわけがないのです。向きを変える時は顔の向き (カメラだったらカメラ自体の向き) が見たい方向の向きに変わっています。これにより後ろを向いた場合、自然と目の位置が入れ替わっているわけです。

![180610_11.png](https://qiita-image-store.s3.amazonaws.com/0/211064/abe60102-9fd4-7ec9-dfec-61200f4b5204.png)

これをレンダリング結果に反映させるのが難しい。試しにレンダリングを実行 (6 方向のそれぞれの向きで描画) するタイミングでカメラ位置を微調整してみました。結果的にはかなり大きな線の分断が視認される状態となりました。 90 度単位で向きだけではなく目の位置も少しずつ変わるので当然だったわけですが (やってみたら意外と目立たないかなーと期待したのですが考えが甘かった) 。

![180610_7.jpg](https://qiita-image-store.s3.amazonaws.com/0/211064/80e0ee35-9530-53aa-7279-3d038e1e5715.jpeg)

なので正しくやるには Transform クラスの範囲での調整は難しくて、シェーダーの改変 (プロジェクト毎に) が必要になるかと思いまして、そうなるとなかなか厳しいのではないかと思います (これまでのところはあくまで Transform クラスでの調整のみなので影響はほぼない) 。

・・・と思ってたのですが、 [Unity の Beta ダウンロード](https://unity3d.com/jp/unity/beta-download) から 2018.2 のリリースノートを見ると非常に気になる一文がありまして

>* XR: Render to Cubemap in stereo eye mode now takes account of camera rotations (1009982)

素直に受け取ると Camera.RenderToCubemap でステレオの Cubemap をレンダリングする際にカメラ回転の考慮が入る、ってことのようですがこれが本当なら上記の補正処理が入るということになるかなと思いまして、非常に気になるところです (これを試すところまで手が回ってない) 。

## サンプルプロジェクトの使い方

基本的には過去記事と同じですが、改めて説明をしていきたいと思います。

サンプルプロジェクトで直接試すと効果が確認できないと思いますので、是非 HDR なプロジェクトで試してみてください。

1. [サンプルプロジェクト](https://github.com/aosoft/UnityVRVideoRecordingTest) をチェックアウトしてください。ご自分のプロジェクトでキャプチャをしたい場合、下記ファイルをプロジェクトにコピーしてください。
    * CubemapRenderer.cs
    * CubemapToOtherProjection.cs
    * CubemapRenderer.shader
    * CubemapToOtherProjection.shader
2. [Recorder アセット](https://assetstore.unity.com/packages/essentials/beta-projects/recorder-94079) を import してください。
3. 撮影対象となる Camera に CubemapToOtherProjection を追加します。
4. CubemapToOtherProjection の設定をします。
5. RenderTexture を用意します。 RenderTexture のサイズは任意ですが、アスペクト比は配慮が必要です。
    * 単眼 (Mono) 向けの場合、 Equirectangular_360 (360 度全天球) は 2:1 、それ以外は 1:1 となるようにする必要があります。
    * ステレオ向けの場合 (Render In Stereo にチェック) の場合、 Equirectangular_360 は 1:1 (上下に並ぶ) 、それ以外は 2:1 (左右に並ぶ) となるようにする必要があります。
    ただし、単眼の時と比べ面積としては倍にした方がよいでしょう。
6. CubemapToOtherProjection の Render Target に 5. で作成した RenderTexture をセットします。
7. メニュー "Tools - Recorder - Video" で Recorder アセットの設定パネルが表示されるので、下記のように設定します。
    * "Selected reorder" は "Image Sequence" で連番静止画で取得した後、別途ビデオエンコーダーで MP4 にすることをお勧めします。
    * "Collection method" は "Render Texture" にし、 5. で作成した RenderTexture をセットします。
    * Output(s) は環境に合わせて任意の設定をしてください。
8. "Start Recording" を押して録画を開始します。

![180610_6.png](https://qiita-image-store.s3.amazonaws.com/0/211064/26acc06e-3987-8c86-bd4e-a6508d7e5d1d.png)

図のように同じ RenderTexture を CubemapToOtherProjection と Recorder アセットに指定します。

### CubemapToOtherProjection の設定項目

| フィールド名 | 内容 |
|:--:|:--:|
|RenderTarget|レンダリング先となるテクスチャです。 Recorder アセットの録画対象と指定するものと同じです。|
|CubemapSize|作成する Cubemap の一辺の長さです。|
|ProjectionType|VR (パノラマ) 映像として展開する射影のタイプです。通常は Equirectangular_360 か Equirectangular_180 のとぢらか。|
|FishEyeType|ProjectionType で FishEye_Circumference か FishEye_Diagonal を指定した時の魚眼タイプです。|
|UseUnityInternalCubemapRenderer|true にすると Camera.RenderToCubemap で Cubemap のレンダリングをします。 false では本記事で解説した自前描画をします。 false の方が多くの場合適切です。|
|GammaConvertType|レンダリング結果が Linear カラーだったとして、そこからガンマ変換して出力する時に変換の種類を指定します。|
|RenderInStereo|ステレオレンダリングする場合は true|
|StereoSeparation|ステレオレンダリングする際の IPD 値そのもの|
|CorrectCameraPositionInStereoRendering|true にすると 90 度単位で目の位置を補正します (ステレオレンダリングの項を参照) 。実験実装なので通常は false のままで。|

* Projection Type
    * Equirectangular_360 ( [正距円筒図法 (Equirectangular)](https://ja.wikipedia.org/wiki/正距円筒図法) の全天球 (360 度) 用)
    * Equirectangular_180 (正距円筒図法 の前面半天球 (180 度) 用)
    * FishEye_Circumference (円周魚眼レンズ)
    * FishEye_Diagonal (対角線魚眼レンズ)
* FishEyeType
    * Equidistance ( [等距離射影方式 (正距方位図法)](https://ja.wikipedia.org/wiki/正距方位図法) )
    * EquisolidAngle (等立体角射影方式)
    * Orthogonal (正射影方式)
* GammaConvertType
    * None (変換なし)
    * Linear_to_sRGB (Linear → sRGB 変換)
    * Linear_to_BT709 (Linear → BT.709 変換)

### Candy Rock Star で試す方法

1. [Candy Rock Star](https://github.com/unity3d-jp/unitychan-crs) のリポジトリをチェックアウトしてプロジェクトを開きます。
2. サンプルプロジェクトから必要なソースコード (CubemapRenderer.*, CubemapToOtherProjection.*) をコピーします。
3. Recorder アセットをインポートします。
4. unitychan-crs のプロジェクトから "Main Camera Rig" という Prefab を検索し、 Hierachy に登録します。
5. その中の "Camera Switcher - Main Camera" に対して CubemapToOtherProjection を追加します。
6. CubemapToOtherProjection に必要な設定をします。
7. Prefab を Apply して反映し、 Hierarchy から削除します。

ちなみに Main Camera 自体もある程度調整した方がよいです。例えば "Screen Overlay" は最初の Unity ロゴ表示なのでこれを無効化したり、 "Depth Of Field Scatter" や Camera の位置などは初期値では不適当でした。

"Main Camera Rig" はオリジナルをいじるよりコピーしたものをいじった方がよいでしょう。コピーを使う場合は Hierarchy の "Stage Director" オブジェクトの Stage Directory スクリプトで "Main Camera Rig Prefab" というフィールドがあるので、ここをコピーして編集した Prefab に切り替えてください。 



## おわりに

この VR 動画を作るシリーズ (になってしまってた) 、当初は Unity 2018 の新機能の検証だったわけですが、思うようなレンダリング結果が得られずにもやもやしていました。今回、解消をすることができたものの、ある意味当初の目的を思いっきり逸脱してしまったわけですが、手段より得られる処理結果の方が大事なわけですからまあ結果よかったかなと。いろいろ勉強にもなりましたし。

今回の Cubemap を自前で生成してから Equirectanglur を得る方法、 Cubemap を挟むことが処理の手数としては本来不必要なので、 Cubemap にしないか Cubemap に直接レンダリングする方法を見出すかのどちらかをするべきでしょう。あとステレオレンダリングの問題を解決できたら完全版かな？　Cubemap に直接レンダリングする方法は結局分からなかったので、できるなら知りたいところです。