Unity のシェーダ内でスクリーンの色と深度を取得するときの注意点

Screen Space で色々やることになったときのメモ。慣れてなかったのでめちゃ混乱しました。

基本

画面の色をテクスチャで取得する

よく知られているように GrabPass を使います。GrabPass {} と書くと _GrabTexture に、GrabPass { "Name" } と書くと Name に画面の色が入ったテクスチャが格納されます。名前を付けた場合は、複数のオブジェクトで結果が共有されます。

描画されるオブジェクトの位置に対応するテクスチャ座標を得るには、ComputeGrabScreenPos を経由する必要があります。これはプラットフォームによって正しいテクスチャ座標が異なるためです。間違えると上下反転したり、VRで隣の目のデータに突っ込んだりする可能性があります。

画面の深度をテクスチャで取得する

_CameraDepthTexture という名前のテクスチャを使います。このテクスチャには深度バッファの生データが入っています。

深度テクスチャを宣言する際は UNITY_DECLARE_DEPTH_TEXTURE を使用する必要があります。これも深度テクスチャの型がプラットフォームによって異なるためです。テクスチャの名前は _CameraDepthTexture で固定なので、シェーダ内では

UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);

と書くことになります。

深度テクスチャの参照は、スクリーン座標で行います。スクリーン座標を得るには、ComputeScreenPos を使用する必要があります。これもプラットフォーム依存のためです。

さらに、同じ理由で参照の際は SAMPLE_DEPTH_TEXTURE を使用する必要があります。引数は tex2D の場合と一緒で、返り値は float です。tex2Dprojtex2Dlod に対応する SAMPLE_DEPTH_TEXTURE_PROJSAMPLE_DEPTH_TEXTURE_LOD もあります。

得られたデータは多くの場合非線形なので、利用する前に線形なデータに変換する必要があります。LinearEyeDepth を使うと非線形のデータをビュー座標系におけるカメラからの距離に変換できます。一方、Linear01Depth を使うとニアクリップ面で 0、ファークリップ面で 1 になる値に変換できます。

中身をいじる必要が出てきた場合

単純に対応するピクセルの値を参照したい場合は上記の方法に従っていればいいんですが、反射や屈折のエフェクトを実装する場合は ComputeGrabScreenPos やら ComputeScreenPos やらで得られた値をいじって使う必要が出てきます。このとき中身がどうなってるか知らないと困ることになります。

ComputeGrabScreenPos で得られた値

直ちに tex2D で画面の色をサンプリングできる値が入っています。つまり、それまでに生じた環境依存の変換が全て詰まっています。具体的には、

  • UNITY_UV_STARTS_AT_TOP が定義されている場合、Y 方向の値が反転する
  • _ProjectionParams.x の値が -1 の場合、Y 方向の値が反転する
  • UNITY_SINGLE_PASS_STEREO が定義されている場合、現在描画中の目に依存して X 方向の値が変換される

があります。自分の環境では UNITY_UV_STARTS_AT_TOP が定義されていてかつ _ProjectionParams.x が -1 だったので、一見反転してないように見えていました。

下は XY を RG に割り当てた図です。Unity Editor 内では左下が原点に見えます。

一方同じシェーダを VRChat の中で見るとこうなります。_ProjectionParams.x の値が 1 になって、Y 方向が反転しています。

ComputeScreenPos で得られた値

これは自分の知る限り、環境に依存する反転は生じません。ただし、UNITY_SINGLE_PASS_STEREO が定義されている場合は X 方向の値が現在描画中の目によって変換されます。

しかし、UnityCG.cginc 内にある ComputeScreenPos の定義を見てみると、なんと Y 方向を _ProjectionParams.x の値によって反転させているではありませんか。

実は、これはそもそもクリップ座標系の Y 座標が反転していることがあるためで、それを元に戻すための変換を行っています。したがって、_ProjectionParams.x が -1 の環境では、反転が生じないからといって自前でクリップ座標からスクリーン座標を愚直に計算しようとすると Y 方向が反転します。しました。

まとめると次のようになります。

  • クリップ座標は環境によって Y 方向がひっくり返っていることがある(実は Z 方向はもっと酷いことになっているが、幸い気にしなくて済む)
  • スクリーン座標は左下原点が基本だが、UNITY_SINGLE_PASS_STEREO が定義されているときは目ごとに X 座標が変わる
  • ComputeGrabScreenPos で得た値はもう Y 方向がめちゃめちゃ入れ替わる、X 座標も UNITY_SINGLE_PASS_STEREO の影響を受ける
  • GrabPass で得たテクスチャを参照する座標と _CameraDepthTexture を参照する座標を一緒にしてはいけない

よって、一つ左のピクセルの色と深度を得たい、みたいなときは非常に慎重な作業が要求されます。Unity 側で「常に左下が (0, 0) で、右上が (1, 1)」みたいな正規化された座標系からの相互変換が行えるマクロを用意してくれていると楽なんですが、残念ながら存在しないようです。

おわり

どこか間違っている点があったら、恐らく後で困ることになると思うので Twitter で教えてもらえると助かります。

このエントリーをはてなブックマークに追加