Looking Glass Portrait を手に入れたので Unity 向けの仕組みを調べてみた - 凹みTips

凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Looking Glass Portrait を手に入れたので Unity 向けの仕組みを調べてみた

はじめに

遅ればせながら Looking Glass Portrait を手に入れました。

lookingglassfactory.com

クラウドファンディング開始からしばらくして気づいてバックしたので入手できるタイミングが第一陣の方々に比べ遅くなってしまいましたが…、ようやくといった感じです。自分は以前のバージョンは買っておらず、イベントなどで見る限りでしたが改めて家でじっくり見てみると裸眼で 3 次元に見えるのは面白いですね。

本記事では、Unity で Looking Glass Portrait を使ってサンプルを動かしていく上で気づいた点をメモ書きしていきます。

とりあえず遊んでみた

レイマーチング

uRaymarching のサンプルシーンを動かしてみました。

f:id:hecomi:20210810013457p:plain

パラメタ調整は必要ですが特別なことは何もせずに Holoplay Capture オブジェクトを置くだけで見れます。裸眼でも立体感あるのは面白いですね。

ボリュームレンダリング

ボリュームレンダリングしたシーンも見てみました。

f:id:hecomi:20210810013151p:plain

破綻なく立体に見えるので思ったより中に物が入ってる感ありました。

セットアップ

learn.lookingglassfactory.com

こちらの動画に従ってセットアップしていくと特に難しい設定もなく終わります。インストールする HoloPlay Service を入れると WebSocket 経由でブラウザ上のボタンから直接 Looking Glass 上に画像を表示するプレビューが出来たりします。HoloPlay Service はどんなデバイスが接続されているかを管理し、その情報(外部ディスプレイとして認識されるので画面位置など)を各種アプリケーションと通信して提供してくれるようです。良く出来てますね~。

Looking Glass 概要

Looking Glass の仕組みは以下にまとまっています(英日両方あります)。

docs.lookingglassfactory.com

docs-ja.lookingglassfactory.com

まず、キルト(Quilt: マス目状の生地)と呼ばれる N x M のタイルの画像を作成します。例えば次のような画像です。

f:id:hecomi:20210801180025p:plain

これを入力としてレンチキュラーを通じて立体視できるよう、画像を加工します。

f:id:hecomi:20210801181056p:plain

この画像を Looking Glass を通じてみると立体視できる感じですね。

HoloPlay Unity Plugin

セットアップ

Unity プラグインは .unitypackage の形で配布されています(要ディベロッパー登録)。

docs.lookingglassfactory.com

セットアップや各コンポーネントについては以下のページで詳しく紹介されています。

learn.lookingglassfactory.com

また、ドキュメントは日本語も用意されており以下で詳しく説明されています。

docs-ja.lookingglassfactory.com

動作の仕組み

サンプルシーンを開くと次のような感じになります。

f:id:hecomi:20210801231033p:plain

Holoplay Capture というゲームオブジェクトにアタッチされた Holoplay というスクリプトがコアとなっています。このコンポーネントによってシーン上に描画範囲が緑で、中間プレーン(Zero-Parallax Plane)が紫で表示されています(色はインスペクタ上から変えられます)。ただ、一見カメラもシーンに見当たりません。ちょっとトリッキーなシーン作りになってます。

Holoplay では GameObject.hideFlags を設定し、内部でインスペクタに表示されないゲームオブジェクトを生成します。

docs.unity3d.com

ここにデフォルトでは何もレンダリングしないよう設定(Clear Flags を Don't Clear、Culling Mask を Nothing)した Camera コンポーネントを追加しています。同オブジェクトに LightfieldPostProcess というコンポーネントを更に追加し、この中の OnRenderImage()Camera コンポーネントがあると呼び出される MonoBehaviour のコールバック)を通じて諸々の処理を行っています。具体的には、カメラを少しずつずらしながら枚数分(Looking Glass Portrait なら 8 x 6 の 48 枚)分のレンダリングを行いキルト画像を作成、これを専用のシェーダでレンチキュラー用のイメージへと変換しています。Frame Debugger で見てみると、ループが回ってるのがわかりますね。

f:id:hecomi:20210802001146p:plain

より詳細には 1 枚毎にカメラのビュー行列とプロジェクション行列をいじって枚数分のレンダリングを行います。レンダーターゲットは 3360 x 3360 なので、8 x 6 だと 420 x 560 ですね。なお、Optimization > View Interpolation の項目をいじると枚数を減らしてレンダリングし、間の画をコンピュートシェーダを使って補間する、という処理が走ります。例えば Every Other を選ぶと半分の 24 枚、Every 8 を選ぶと 1/8 の描画オブジェクト数になります。

f:id:hecomi:20210802002647p:plain

ただ、あくまで補間の計算を行うので補間できないような遮蔽領域は黒くなってしまうので使い所は注意が必要です。

f:id:hecomi:20210807224529g:plain

パフォーマンスについての考察

前述のようにカメラを動かしてキルトを生成しています。これは具体的には次のようなコードで行われています(一部改変)。

// インスペクタからは見えないカメラ
[System.NonSerialized] public Camera cam;

// LightfieldPostProcess の OnRenderImage() から呼ばれる
public void RenderQuilt(...)
{
    ...
    var centerViewMatrix = cam.worldToCameraMatrix;
    var centerProjMatrix = cam.projectionMatrix;
    ...
    for (int i = 0; i < quiltSettings.numViews; i++)
    {
        // 各ビュー毎のレンダーターゲットを作成
        viewRT = RenderTexture.GetTemporary(...);
        viewRTDepth = RenderTexture.GetTemporary(...);
        cam.SetTargetBuffers(viewRT.colorBuffer, viewRTDepth.depthBuffer);
        ...
        // レンチキュラーを考慮しビュー・プロジェクション行列を動かす
        var viewMatrix = centerViewMatrix;
        var projMatrix = centerProjMatrix;
        float currentViewLerp = (float)i / (quiltSettings.numViews - 1) - 0.5f;
        viewMatrix.m03 += currentViewLerp * viewConeSweep;
        projMatrix.m02 += currentViewLerp * viewConeSweep * projModifier;
        ...
        // セットしたレンダーターゲットへ通常のレンダリング
        cam.Render();
        // レンダリング結果をキルト画像へ集約
        CopyViewToQuilt(i, viewRT, quiltRT);
        ...
    }
}

途中で、cam.Render() という形でカメラ描画が行われています。中ではオブジェクトの描画だけでなくシャドウマップ作成といったことも複数回行われています。

f:id:hecomi:20210807225637p:plain

この描画方法は、Oculus DK1 登場時のレンダリング手法に似ています。当時は Oculus DK1 も外部ディスプレイとして接続され、左目・右目用にそれぞれ 2 つのカメラを使って描画を行っていました(= マルチカメラ)。

ここから幾つか最適化がされていきます。まず、ビューカリングやシャドウマップ生成はカメラの視点が近いことを利用して共通化できることがわかりました。この結果マルチパスという手法が登場しました。副作用がほとんど無く簡単に対応できるのがメリットです。

ただ依然としてマルチパスでもその名の通り複数回描画のループが回ります。これを解決するためにシングルパスと呼ばれる手法が次に取り入れられました。Unity の実装では 2 倍の幅を持つレンダーターゲットを用意し、ビューポートをスイッチすることで左目・右目を切り替えていました。内部的には 1 つのオブジェクトに対しドローコールが依然として 2 回走りますが、コンスタントバッファなどはそのまま使うことができるためその分のオーバーヘッド削減が可能となりました。ただ、ビュー・プロジェクション行列は異なるため配列として詰めて、現在どちらのビューをレンダリングしているかのインデックスをハンドルするためシェーダに改変が必要となりました。

最後に描画 API の更新とともにシングルパスのインスタンシング版が登場しました。ビューポートもスイッチすること無くインスタンシングを使って 1 回の描画で左右のどちらの目に対してもオブジェクトが描画できるようになりました。

より詳細は以下の記事にまとめています。

tips.hecomi.com

さて、Looking Glass では現状はマルチカメラ相当の実装となっています。この手法は副作用がなくパフォーマンスが許すならば多様な既存プロジェクトに組み込むことができるためとても柔軟性が高いです。一方で、マルチパスやシングルパスのようなレンダリングパイプラインが実装できればパフォーマンスが大幅に改善できる余地が残っています。しかしながら現状 Looking Glass がサポートしているビルトインパイプラインではこれらの対応は出来ません。マルチパスやシングルパスは Unity の組み込みの機能となっており、ユーザ側には提供されていないコードで行われているからです。

一方で SRP を使用することでマルチパスやシングルパス相当のレンダリングパイプラインは組み立てられるかもしれません。ただ URP 向けに拡張したり今後の変更に対してメンテしたりサポートするコストをトータルで考えると難しくはありそうですね。。

その他

キルト画像のカスタマイズ

上述のようにループが回っているので、ここに手を入れてあげれば面白い効果が色々作れるかもしれません。@koichi3 さんのチームは第 1 回 Looking Glass ハッカソンで左右で視点の違うゲームを作られていました。

ループのインデックスの前半か後半かを見て、使用するカメラの位置にオフセットをかませてあげることでこういった効果が作れそうです。他に余りいいアイディアは思い浮かばないですが...、こうしたキルト画像を改変する面白いアイディアは他にもあるかもしれませんね。

諦めたアイディア

ジャイロセンサと組み合わせて中でボールがコロコロ、みたいなデモを作ろうかと思いましたが、レンチキュラーなので左右の傾きはいい感じかもしれませんが前後の傾きは見え方は変わらないので微妙ということが分かり諦めました…。ちょっと変な面白いアイディア何かないか、また考えてみようと思います。

メインディスプレイの DPI スケーリング設定によるズレ

Windows でメインの方のディスプレイの DPI スケーリングが効いている際はプレビューが画面に合わない感じになってしまいます(自分は 4K ディスプレイ x 2 で両方 150% 設定で使っています)。対症療法としては、いったんメインのディスプレイの方の DPI スケーリングを 100% にし、Reload Calibration を行って位置を合わせたあと、DPI スケーリングをもとに戻す、という感じです(戻したあとは Reload Calibration しない)。ただシーンを切り替えたりして Holoplay コンポーネントが切り替わると再度同じ手順を踏まないとならないので我慢して 100% で作業するのが良いかもしれません…。

おわりに

SDK も良く出来ていて簡単に使えますし、作ってきたものをとりあえず映して眺めているだけでも楽しいです。2018 年の初期版発売当初に買ってビッグウェーブに乗らなかったことを後悔...