VRChatでのカメラの描画順とイベント関数

※VRChatに限らない話ですが、VRChat以外ならCommandBufferを使ったり、SRPならレンダリングパイプライン側で制御などもあると思います。

カメラの描画順

シーンにカメラが複数ある場合、そのレンダリング順はdepthというパラメータで管理されています。 depthが小さいほど先にレンダリングされます。

Camera-depth - Unity スクリプトリファレンス

よくある状況として、シーン内を撮影しているカメラ(便宜上サブカメラと呼びます)があって、その映像がRenderTextureに出力されて、それを張り付けたスクリーンなどに描画される、みたいな場合があると思います。

この場合、サブカメラはメインカメラ(=アバターの目)よりも先にレンダリングされる必要があります。

  1. サブカメラがSceneを描画
  2. サブカメラのレンダリング結果がRenderTextureに書き込まれる
  3. メインカメラがスクリーンをレンダリング
  4. スクリーンのシェーダーがRenderTextureの色を使う(2の状態が反映されている)

1~2と3~4が逆になっていると、スクリーンに映される映像は1フレーム前のものになるので、注意が必要です。

カメラの描画結果をスクリプトで読み込む

カメラの描画結果(や、シェーダーの計算結果など)をU#から参照したくなるときがあると思います。 この場合、まずカメラの出力先をRenderTextureにして、RenerTextureの内容をU#スクリプトから読み込みます。

RenderTexture -> U#の読み込みには、間にTexture2Dを挟んで、 Texture2D.ReadPixelsTexture2D.GetPixelsを使います。

こんな感じで情報が流れます。

Camera --> RenderTexture -(ReadPixels)-> Texture2D -(GetPixels)-> U# Script

この辺はちょっと重めの関数なので、毎フレーム呼ぶ場合は解像度やオブジェクトの数を気にした方がよさそうです。

ドキュメントに書かれていますが、このメソッドは"アクティブなRenderTexure"を読み込みます。 アクティブなRenderTextureは常にひとつだけなので、一般的には下記の記事で説明されているように、 読み込みたいRenderTextureをアクティブにしてからReadPixelsを実行する、みたいなことをして使います。

UdonではRenderTexture.activeがサポートされていないので、「読み込みたいRenderTextureがアクティブになっているときにReadPixelsを実行する」必要があります。RenderTexture.activeがどのタイミングで何がセットされてるのかはあまりちゃんとわかってないんですが、

gyazo.com イベント関数の実行順序 - Unity マニュアル

↑のSceneRenderingのグループに含まれるイベント関数の中で実行すれば、いい感じにカメラの出力とかがactiveになってそうな感じがあります。

で、複数あるけど、どれを使うの?という話になるので、それについて書きます。

まず、これらのイベント関数は以下の2パターンが存在します。

  • 描画される側のレンダラーとかにアタッチされた場合に実行されるもの
  • 描画する側のカメラにアタッチされた場合に実行されるもの

前者は例えばOnRenderObjectとかOnWillRenderObjectとかが該当します。 これらは全カメラに対して実行されるので、

if (Camera.active == Camera.main)

とかでどのカメラをフィルターにかけるのですが、UdonではCamera.activeがサポートされていません。 代わりにカメラのレイヤーマスク機能で絞り込みをするとかがいいかもです。

後者はカメラごとに実行されるので、特定のカメラの描画結果を使う、みたいなときいは使いやすいです。 描画結果を使う場合は、OnPostRenderとかOnRenderImageが使えます。 Graphics.BlitがUdonで使えないので、やるならOnPostRenderがいいのかなと思います。

MainCameraに張り付けたUdonが動かない?

VRChatのワールドでは、VRC_SceneDescriptorのReference Cameraという項目で、「このカメラを使うよ」と宣言することができます。 これにより、ユーザーの目のカメラにポスプロがかかったりしているはずです。

これがシーンのメインカメラに相当するので、このカメラのレンダリング前にやりたいこと、みたいなのは結構需要があると思うのですが、このカメラに自前のU#スクリプトを張り付けてもどうも動かないっぽいです。無念。


KanikamaGIでのカメラ周り

--- 追記 ---

アスペクト比の調整はCamera.aspectを変更することで キャプチャカメラ=サンプリングカメラにできるようでした。

感謝。

ちょろっと試した感じ大丈夫そうだったので、v2.0.0とかに入れるかと思います。

→ ver 1.4.0でリリースされました。ありがたい! モニター簡略化 by shivaduke28 · Pull Request #68 · shivaduke28/kanikama · GitHub

---追記終わり---

https://user-images.githubusercontent.com/45098240/131208676-49e704dd-60f0-4c86-8c23-c4b6cc9c5858.png

モニターの設定 · shivaduke28/kanikama Wiki · GitHub

KanikamaGIでモニターの映像をGIに反映される場合、メインとは別に3つのサブカメラが使われます。

  • キャプチャーカメラ(depth -3)
  • サンプリングカメラ(depth -2)
  • 合成用ダミーカメラ(depth -1)
  • メインカメラ(depth 0)

キャプチャーカメラ

キャプチャーカメラは、モニターの前に配置されていて毎フレーム最初にカメラをレンダリングします。 レンダリング結果はアスペクト比がモニターと同じRenderTexture(大体短い辺が256くらい)に出力されます。

サンプリングカメラ

キャプチャカメラの出力先のRenderTextureを256x256のRawImageに張り付けて、サンプリングカメラが256x256のRenderTextureに出力します。

サンプリングカメラのOnRenderImage(本当はOnPostRenderでいい)で、256x256のTexture2Dを使ってReadPixels + GetPixelsを行い、色をU#の世界に持っていきます。このときにどうしてもmipmapを使いたかったので、テクスチャの解像度を2の冪(256)にする必要があり、そのためのRawImageを挟んでいます。(モニターの形が正方形ならキャプチャーカメラ=モニターカメラにできます。)

mipmapレベルの選択はモニターの分割数とかに依存していて、例えば3x2分割の場合だと、mipmapレベル6で読み込んでColor[16]を取得した後に、U#側で3x2になるように計算しなおしています。

kanikama/KanikamaColorSampler.cs at main · shivaduke28/kanikama · GitHub

ここの実装は気合で書かれていて大変厳しい。

合成用ダミーカメラ

depth-1の合成用ダミーカメラは、カメラとしての機能は一切使用しないので、1x1のRenderTextureに出力するようになっていて、 OnPreCullのタイミングで、ワールド内の光源やサンプリングカメラなどから色を収集します。 収集した色は、GIを受け取るRendererにMaterialPropertyBlockとして配ります。

モニターが1つの場合は、サンプリングカメラと合成用ダミーカメラを同じにすることができますが、モニターが複数ある場合や、それ以外のカメラを使って色を計算する仕組みがでてきても、depth=-2のカメラの後ろですべて終わらせておけば、後ろで合成されるようになっています。

ver 1.3.1でモニター映像の色の平均色をリアルタイムライトに反映させる仕組みを入れたのですが、このライトの更新もモニターの色確定とメインカメラの描画の間である、このタイミングで行う必要があります。

メインカメラ

depth 0のメインカメラがKanikamaGIの影響を受けるオブジェクトをレンダリングするときには、-1の時点で色が配られているので、GIが遅延なしで反映されるようになっています。