はじめに
遅ればせながら Looking Glass Portrait を手に入れました。
クラウドファンディング開始からしばらくして気づいてバックしたので入手できるタイミングが第一陣の方々に比べ遅くなってしまいましたが…、ようやくといった感じです。自分は以前のバージョンは買っておらず、イベントなどで見る限りでしたが改めて家でじっくり見てみると裸眼で 3 次元に見えるのは面白いですね。
やったー!やっと Looking Glass Portrait を手に入れたゾ #LookingGlassPortrait pic.twitter.com/q87pA9dV3v
— 凹 (@hecomi) 2021年7月31日
本記事では、Unity で Looking Glass Portrait を使ってサンプルを動かしていく上で気づいた点をメモ書きしていきます。
とりあえず遊んでみた
レイマーチング
uRaymarching のサンプルシーンを動かしてみました。
パラメタ調整は必要ですが特別なことは何もせずに Holoplay Capture オブジェクトを置くだけで見れます。裸眼でも立体感あるのは面白いですね。
#LookingGlassPortrait #LookingGlass pic.twitter.com/RJDEKwVLdp
— 凹 (@hecomi) 2021年8月9日
ボリュームレンダリング
ボリュームレンダリングしたシーンも見てみました。
破綻なく立体に見えるので思ったより中に物が入ってる感ありました。
#LookingGlassPortrait #LookingGlass pic.twitter.com/nFDK6X6Fik
— 凹 (@hecomi) 2021年8月9日
#LookingGlassPortrait #LookingGlass pic.twitter.com/Uw94QR02DE
— 凹 (@hecomi) 2021年8月9日
セットアップ
こちらの動画に従ってセットアップしていくと特に難しい設定もなく終わります。インストールする HoloPlay Service を入れると WebSocket 経由でブラウザ上のボタンから直接 Looking Glass 上に画像を表示するプレビューが出来たりします。HoloPlay Service はどんなデバイスが接続されているかを管理し、その情報(外部ディスプレイとして認識されるので画面位置など)を各種アプリケーションと通信して提供してくれるようです。良く出来てますね~。
Looking Glass 概要
Looking Glass の仕組みは以下にまとまっています(英日両方あります)。
docs-ja.lookingglassfactory.com
まず、キルト(Quilt: マス目状の生地)と呼ばれる N x M のタイルの画像を作成します。例えば次のような画像です。
これを入力としてレンチキュラーを通じて立体視できるよう、画像を加工します。
この画像を Looking Glass を通じてみると立体視できる感じですね。
HoloPlay Unity Plugin
セットアップ
Unity プラグインは .unitypackage の形で配布されています(要ディベロッパー登録)。
セットアップや各コンポーネントについては以下のページで詳しく紹介されています。
また、ドキュメントは日本語も用意されており以下で詳しく説明されています。
docs-ja.lookingglassfactory.com
動作の仕組み
サンプルシーンを開くと次のような感じになります。
Holoplay Capture
というゲームオブジェクトにアタッチされた Holoplay
というスクリプトがコアとなっています。このコンポーネントによってシーン上に描画範囲が緑で、中間プレーン(Zero-Parallax Plane)が紫で表示されています(色はインスペクタ上から変えられます)。ただ、一見カメラもシーンに見当たりません。ちょっとトリッキーなシーン作りになってます。
Holoplay
では GameObject.hideFlags
を設定し、内部でインスペクタに表示されないゲームオブジェクトを生成します。
ここにデフォルトでは何もレンダリングしないよう設定(Clear Flags を Don't Clear、Culling Mask を Nothing)した Camera
コンポーネントを追加しています。同オブジェクトに LightfieldPostProcess
というコンポーネントを更に追加し、この中の OnRenderImage()
(Camera
コンポーネントがあると呼び出される MonoBehaviour
のコールバック)を通じて諸々の処理を行っています。具体的には、カメラを少しずつずらしながら枚数分(Looking Glass Portrait なら 8 x 6 の 48 枚)分のレンダリングを行いキルト画像を作成、これを専用のシェーダでレンチキュラー用のイメージへと変換しています。Frame Debugger で見てみると、ループが回ってるのがわかりますね。
より詳細には 1 枚毎にカメラのビュー行列とプロジェクション行列をいじって枚数分のレンダリングを行います。レンダーターゲットは 3360 x 3360 なので、8 x 6 だと 420 x 560 ですね。なお、Optimization > View Interpolation の項目をいじると枚数を減らしてレンダリングし、間の画をコンピュートシェーダを使って補間する、という処理が走ります。例えば Every Other
を選ぶと半分の 24 枚、Every 8
を選ぶと 1/8 の描画オブジェクト数になります。
ただ、あくまで補間の計算を行うので補間できないような遮蔽領域は黒くなってしまうので使い所は注意が必要です。
パフォーマンスについての考察
前述のようにカメラを動かしてキルトを生成しています。これは具体的には次のようなコードで行われています(一部改変)。
// インスペクタからは見えないカメラ [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()
という形でカメラ描画が行われています。中ではオブジェクトの描画だけでなくシャドウマップ作成といったことも複数回行われています。
この描画方法は、Oculus DK1 登場時のレンダリング手法に似ています。当時は Oculus DK1 も外部ディスプレイとして接続され、左目・右目用にそれぞれ 2 つのカメラを使って描画を行っていました(= マルチカメラ)。
ここから幾つか最適化がされていきます。まず、ビューカリングやシャドウマップ生成はカメラの視点が近いことを利用して共通化できることがわかりました。この結果マルチパスという手法が登場しました。副作用がほとんど無く簡単に対応できるのがメリットです。
ただ依然としてマルチパスでもその名の通り複数回描画のループが回ります。これを解決するためにシングルパスと呼ばれる手法が次に取り入れられました。Unity の実装では 2 倍の幅を持つレンダーターゲットを用意し、ビューポートをスイッチすることで左目・右目を切り替えていました。内部的には 1 つのオブジェクトに対しドローコールが依然として 2 回走りますが、コンスタントバッファなどはそのまま使うことができるためその分のオーバーヘッド削減が可能となりました。ただ、ビュー・プロジェクション行列は異なるため配列として詰めて、現在どちらのビューをレンダリングしているかのインデックスをハンドルするためシェーダに改変が必要となりました。
最後に描画 API の更新とともにシングルパスのインスタンシング版が登場しました。ビューポートもスイッチすること無くインスタンシングを使って 1 回の描画で左右のどちらの目に対してもオブジェクトが描画できるようになりました。
より詳細は以下の記事にまとめています。
さて、Looking Glass では現状はマルチカメラ相当の実装となっています。この手法は副作用がなくパフォーマンスが許すならば多様な既存プロジェクトに組み込むことができるためとても柔軟性が高いです。一方で、マルチパスやシングルパスのようなレンダリングパイプラインが実装できればパフォーマンスが大幅に改善できる余地が残っています。しかしながら現状 Looking Glass がサポートしているビルトインパイプラインではこれらの対応は出来ません。マルチパスやシングルパスは Unity の組み込みの機能となっており、ユーザ側には提供されていないコードで行われているからです。
一方で SRP を使用することでマルチパスやシングルパス相当のレンダリングパイプラインは組み立てられるかもしれません。ただ URP 向けに拡張したり今後の変更に対してメンテしたりサポートするコストをトータルで考えると難しくはありそうですね。。
その他
キルト画像のカスタマイズ
上述のようにループが回っているので、ここに手を入れてあげれば面白い効果が色々作れるかもしれません。@koichi3 さんのチームは第 1 回 Looking Glass ハッカソンで左右で視点の違うゲームを作られていました。
幣チームの作品
— 蔵岡 恒一郎 (@koichi3) 2019年3月31日
1台で左右 2視点で対戦できる野球ゲーム。M5Stackでスイング、投球が可能#るきはく #LookingGlass pic.twitter.com/NimqIoM2w8
ループのインデックスの前半か後半かを見て、使用するカメラの位置にオフセットをかませてあげることでこういった効果が作れそうです。他に余りいいアイディアは思い浮かばないですが...、こうしたキルト画像を改変する面白いアイディアは他にもあるかもしれませんね。
諦めたアイディア
ジャイロセンサと組み合わせて中でボールがコロコロ、みたいなデモを作ろうかと思いましたが、レンチキュラーなので左右の傾きはいい感じかもしれませんが前後の傾きは見え方は変わらないので微妙ということが分かり諦めました…。ちょっと変な面白いアイディア何かないか、また考えてみようと思います。
メインディスプレイの DPI スケーリング設定によるズレ
Windows でメインの方のディスプレイの DPI スケーリングが効いている際はプレビューが画面に合わない感じになってしまいます(自分は 4K ディスプレイ x 2 で両方 150% 設定で使っています)。対症療法としては、いったんメインのディスプレイの方の DPI スケーリングを 100% にし、Reload Calibration を行って位置を合わせたあと、DPI スケーリングをもとに戻す、という感じです(戻したあとは Reload Calibration しない)。ただシーンを切り替えたりして Holoplay コンポーネントが切り替わると再度同じ手順を踏まないとならないので我慢して 100% で作業するのが良いかもしれません…。
おわりに
SDK も良く出来ていて簡単に使えますし、作ってきたものをとりあえず映して眺めているだけでも楽しいです。2018 年の初期版発売当初に買ってビッグウェーブに乗らなかったことを後悔...