はじめに
こんばんは、代表の堂前です!
というより、明けましておめでとうございます。
堂前個人としてはだいぶ間が空いてしまいましたが、新年初のブログを書こうと思います!
(昨年末は地獄の進行でなかなか時間が取れませんでした。。。)
今回のテーマですが、Unityでのビュー&プロジェクション行列についてまとめていこうと思います。
これを書こうと思ったきっかけですが、これら周りでスクリプトやシェーダを作成する際に非常に混乱することが度々おきまして・・・。
早い話、自分のメモとして残しておこうと思いました。
※検証に利用したのはUnity5.5.0f3になります。
何がややこしいのか?
先程、「Unityのビュー&プロジェクション行列がややこしい」的なことを書きました。
今回の話を進めるにあたり、自分が何に対してややこしいと思っているかを先にまとめます。
★ビュー行列
・UnityのScene上は左手座標系が原則だが、シェーダ内の行列(UNITY_MATRIX_V)では右手座標系になっているという情報がある。
・それらにおいて、カメラの前方(Z軸)はプラスかマイナスかがたまに混乱する。
★プロジェクション行列
・シェーダ内でのnear〜farが「-1〜1」か「0〜1」かで混乱する。(プラットフォーム依存?)
・更に5.5.0からnearとfarが反転しているらしい?(つまり「0〜1」ではなく「1〜0」となる)
・シェーダではなくスクリプトでも上記ルールが適応されるか?
それぞれについて検証しましたが、そのためのプロジェクト&シーン構成は以下のようにしています。
・「New Scene」で作った状態で、カメラに対してスクリプトを付与。
・↑のスクリプトは各種行列やグラフィックスAPIなどの情報を画面に表示して確認。
・カメラ自体は初期状態。(near=0.3, far=1000)
・実行解像度は640×480固定。(念のため)
Editor | 実行画面 |
それでは結果を見てみましょう!
ビュー行列
まず、ビュー行列(カメラ行列)というのはどういうものかというのを軽く書きます。
カメラはオブジェクトと同様、ワールドに(世界)に配置されます。
そこから捉えた絵を最終的にディスプレイに表示するのですが、その計算都合のため、カメラを原点に置いた座標系に一旦変換するための行列です。
「New Scene」でのカメラの初期座標は(0, 1, -10)ですが、これをビュー行列で変換すると(0, 0, 0)になります。
座標だけではなく、方向も特定の方向を向きますが、Unityの場合はワールドのZ軸に沿った方向を向くような変換になります。
というわけで本題に入りますが、Unityのスクリプト上でビュー行列を取得する方法は(把握している限り)以下になります。
・カメラのGameObject(Transform)のTransform.worldToLocalMatrixを利用。
・CameraコンポーネントのCamera.worldToCameraMatrixを利用。
結論から言えば、後者(Camera.worldToCameraMatrix)はシェーダに渡されるビュー行列(UNITY_MATRIX_V)となります。
そしてそれぞれを画面表示した結果が以下です。
(上がTransform.worldToLocalMatrix、下がCamera.worldToCameraMatrixになります。)
両者とも(0, 1, -10)を乗算すると原点(0, 0, 0)になります。
が、下図の様なカメラ前方にある(0, 1, -5)の物体について考えます。
カメラを原点に持ってくるということは、物体も相対的に移動するはずです。
上図はシーン上ですが、Z軸がプラス方向に物体があるので、変換後はプラスになることが予想されます。
が、結果としては上が(0, 0, 5)に対し、下が(0, 0, -5)と、Zの値がプラスマイナス逆になります。
あと具体例は割愛しますが、x,yに関しては値(方向)が変わりません。
これにより、シェーダとそれ以外で座標系が異なることがわかります。
以下、一覧としてまとめます。
座標系 | 前方(Z軸) | スクリプト算出方法 | |
シェーダ以外 | 左手座標系 | プラス | Transform.worldToLocalMatrix |
シェーダ内 | 右手座標系 | マイナス | Camera.worldToCameraMatrix |
ビュー行列に関してはプラットフォームには依存しません。(のハズです)
普段は左手座標で考えてスクリプトを組んで良いと思いますが、シェーダにカメラを考慮したマトリクスを渡す際などは気を付ける必要があります。
プロジェクション行列(シェーダ編)
プロジェクション行列についても軽く書きます。
カメラから捉えた絵をディスプレイなどの2Dに投影しますが、そのための計算を行うための行列になります。
2Dなので(x,y,z)で(x,y)が残るイメージですが、実はZ値も大きな要素ですし、今回の話のポイントにもなっています。
そしてその算出後のZ値(厳密に言えばW値で除算)ですが、カメラのnear〜farに対応した値になります。
下図のイメージで考えると良いです。
これはシェーダ内でプロジェクション行列(UNITY_MATRIX_P)を乗算して得られますが、Z値に関しては結論から言えばプラットフォームとUnityのバージョンでマチマチでした。
大きく分けて3種類ですので、相関を表としてまとめてみました。
(プラットフォームはビルトインシェーダ内のSHADER_API_xxxxの形式で記しています。(xxxxの部分のみ表記))
上記結果の行列がシェーダ内のプロジェクション行列(UNITY_MATRIX_P)に相当します。
(GL.GetGPUProjectionMatrixで算出しました。)
基本は「near〜far」で「0〜1」もしくは「-1〜1」という、「手前から奥にかけて値が増える」のが基本です。
シェーダ内でnearをあてにした計算には注意が必要ですが、更に5.5.0以降では「1〜0」という逆方向のものも存在します。
(ざっくりというと、描画精度向上のための変更です。)
逆方向かどうかとnearの値はビルトインシェーダで以下の様に定義されています。
シェーダ定義 | 備考 | |
nearの値 | UNITY_NEAR_CLIP_VALUE | |
逆方向か? | UNITY_REVERSED_Z | スクリプトでは SystemInfo.usesReversedZBuffer でも情報を取得できる。 |
シェーダで処理を書く際は、直値などではなく上記を利用して処理を行うのが良いでしょう。
プロジェクション行列(シーン編)
ここまでのプロジェクション行列の説明は、あくまでシェーダ処理内(UNITY_MATRIX_P)での話です。
ビュー行列と同様、シェーダ以外では意味合いなど変わってきます!
まず先程の行列表示をもう一度掲載します。
(near〜far)→(0〜1) | (near〜far)→(-1〜1) | (near〜far)→(1〜0) |
行列を2つ表示していて、下の方の行列がシェーダ側の行列(UNITY_MATRIX_P)に相当しています。
これはGL.GetGPUProjectionMatrixで算出していて、引数はCamera.projectionMatrixを渡しています。
そして結果の上の行列はそのCamera.projectionMatrixを渡しているのですが、それはプラットフォームでの違いがなく、(near〜far)→(-1〜1)相当であると言えます。
ちなみにCamera.projectionMatrixではなくMatrix4x4.Perspectiveで算出した時もプラットフォームによる違いが出ず、(near〜far)→(-1〜1)となりました。
(試していないですがMatrix4x4.Orthoも恐らくそうです。)
ここでシェーダ内とそれ以外でまとめます。
near〜far範囲 | スクリプト算出方法 | |
シェーダ以外 | -1〜1のみ | Camera.projectionMatrix Matrix4x4.Perspective Matrix4x4.Ortho |
シェーダ内 | 0〜1 もしくは -1〜1 もしくは 1〜0 (プラットフォームで変わる) |
GL.GetGPUProjectionMatrixで算出 |
スクリプトで算出して処理する時は-1〜1のみで考えて良いですが、スクリプトからシェーダに直に値を渡す時などは十分に注意が必要です。
まとめ
シェーダーでトリッキーな処理をする時にスクリプトからビュー&プロジェクション行列の結果を活用したり、シェーダ内でも前後関係などを把握する時などがたまにありますが、その時に意外とハマり易いのがこの部分です。
プラットフォーム依存もあり、不具合の特定が遅れるのもありますので、普段から内部ではこの様になっているのを意識すると良いかなと思います。
【免責事項】
本サイトでの情報を利用することによる損害等に対し、株式会社ロジカルビートは一切の責任を負いません。