スクリーンショット_2018-10-18_12

【Unity】Camera.projectionMatrixの罠まとめ

先日、会社の人に相談を受けてCamera.projectionMatrixまわりの操作を説明しました。

説明しながら「罠が多い!そういえばCameraクラスからはたどり着きにくい関連するメソッドもいくつかある!」と思い、これはどっかに網羅されてると役立つのでは!?と書いてみることにしました。

全体の流れはこんな感じです。

・Camera.projectionMatrixとは
・ベーシックな求め方とそれに関わるプロパティ
・直接求めるときの罠
 ・罠1:一度代入すると各プロパティは連動しなくなる
 ・罠2:OpenGL座標系
 ・罠3:GPUに渡ってくる行列が違う
・関連するメソッド
 ・GeometryUtility.CalculateFrustumPlanes()
 ・Matrix4x4.Frustum()
 ・Camera.CalculateObliqueMatrix()
 ・Camera.CalculateFrustumCorners()


Camera.projectionMatrixとは

射影行列というやつでカメラ座標系からクリップ座標系への変換を行います。幾何学的にはカメラの投影範囲表す視錐台(Frustum)で表現できます。視錐台についてはUnity公式のドキュメントの説明があります。

視錐台(Frustum)


ベーシックな求め方

普段はCameraクラスの機能を使うことでprojectionMatrixを直接求めることはあまりないと思います。この場合Cameraは内部的に以下の変数からprojectionMatrixを求めています。


Camera.aspect
Camera.nearClipPlane
Camera.farClipPlane
Camera.fieldOfView (Camera.orthographic == false のとき)
Camera.orthographicSize (Camera.orthographic == true のとき)

またUnity2018.2からPhysical Cameraが追加されました。
テラシュールブログさんの説明がわかりやすいです。

この場合はfieldOfViewsensorSize,focalLenghと連動し、新たにlensShiftが加わります。

Camera.aspect
Camera.nearClipPlane
Camera.farClipPlane
Camera.fieldOfView (Camera.usePhysicalProperties == true のときは次の2変数から求める)
 Camera.sensorSize
 Camera.focalLength
Camera.lensShift

レンズシフト(LensShift)とは、現実世界ではレンズをずらすことで投影面を平行移動させることを指します。これをCG上でやる場合、最終的にprojectionMatrixの値をいじることになります。Camera.lensShiftを使わずに直接projectionMatrixを書き換える方法がUnity公式で説明されています。


ProjectionMatrixを直接求めるときの罠

Cameraクラスの機能を使わずに自前でprojectionMatrixを求めてCamera.projectionMatrixに代入して使うことも可能です。

上記の機能では対応できない変態的な射影をしたかったり、なんらかの条件を満たすのに直接代入するほうが楽な場合などにこの方法を使います。

以前はレンズシフトをするのにPhysical Cameraがなくこの方法しかやりようがありませんでした。

しかしCamera.projectionMatrix直接操作にはいくつか罠があるので注意が必要です。


罠1:一度代入すると各プロパティは連動しなくなる

Camera.projectionMatrixに自前の値を代入すると上記ベーシックな求め方で出てきた変数と連動しなくなります。おそらく値によっては各変数への逆演算ができなくなるからだと思います。

連動を復活させるには

Camera.ResetProjectionMatrix()

を呼ぶ必要があります。


罠2:OpenGL座標系

自分で射影行列を求める際には座標系にも注意が必要です。Camera.projectionMatrixはOpenGL座標系での射影行列となっています。

In Unity, projection matrices follow OpenGL convention. 

https://docs.unity3d.com/ja/current/ScriptReference/GL.GetGPUProjectionMatrix.html

Unityの感覚ではZ+方向が前方ですがOpenGL座標系ではZ-方向が前方になります。つまり、カメラ座標系でZ+の値を射影するとクリップ座標系ではZ-の値になるようにprojectionMatrixを作る必要があります。

OpenGLと同じ向きのBlenderのカメラ。Z軸(青い矢印)が後ろ向き


罠3:GPUに渡ってくる行列が違う

Unityのシェーダー内ではCamera.projectionMatrixの値はそのまま渡っては来ず、各プラットフォームに合わせた値に変換されてUNITY_MATRIX_Pなどに入ってきます。
C#側では

GL.GetGPUProjectionMatrix()

でCamera.projectionMatrixから求めることができます。


ProjectionMatrixと関連するメソッド

projectionMatrixと関連性の高いメソッドがいくつかあるのですがなかなか見つけづらいです。ここでゴソッとまとめてみます。

GeometryUtility.CalculateFrustumPlanes()

Cameraから視錐台を構成する6面をゲットするメソッドです。自前でカリング処理などをするときなどに使えます。ロジカルビートさんのブログがわかりやすいです。


Matrix4x4.Frustum()

nearClipPlaneの上下左右の幅と、zNear、zFarから直接、視錐台を求める関数です。これもOpenGL座標系でそのままCamera.projectionMatrixに代入して使えます。


Camera.CalculateObliqueMatrix()

カメラのnearClipPlaneを斜め(Z軸に垂直ではない平面)にしたprojectionMatrixを求めるメソッドです。もともとは水面に反射した像を作るためのもののようです。

「【Unite 2017 Tokyo】スマートフォンでどこまでできる?3Dゲームをぐりぐり動かすテクニック講座」より

引数の意味がわかりにくいですが、与えたいnearClip面の法線xyzと法線方向への距離wをVector4型で渡します。

Vector4 clip_plane = new Vector4(cnormal.x, cnormal.y, cnormal.z, -Vector3.Dot(cpos, cnormal));
reflection_camera.worldToCameraMatrix = local_reflection_matrix;
reflection_camera.projectionMatrix = camera.CalculateObliqueMatrix(clip_plane);

WaveShooterデモ(C) Unity Technologies Japan/UCLより

また、このメソッドで求めた行列をCamera.projectionMatrixに代入して使えますがシーンビューの視錐台表示には反映されませんので少し注意が必要です。

ちなみにPortalをUnityで実装してみた際にもこのメソッドに助けられました。


Camera.CalculateFrustumCorners()

ビューポートの矩形と深度を指定して、カメラから矩形頂点までのベクトルを求めます。ピクセルの位置からワールド座標を求めるのに便利らしいです。


まとめ

Camera.projectionMatrixについて気づきにくい点を網羅できるようにまとめてみました。

・Camera.projectionMatrixとは
・ベーシックな求め方とそれに関わるプロパティ
・直接求めるときの罠
 ・罠1:一度代入すると各プロパティは連動しなくなる
 ・罠2:OpenGL座標系
 ・罠3:GPUに渡ってくる行列が違う
・関連するメソッド
 ・GeometryUtility.CalculateFrustumPlanes()
 ・Matrix4x4.Frustum()
 ・Camera.CalculateObliqueMatrix()
 ・Camera.CalculateFrustumCorners()

もし「これもあるよ!」とか「これは違うんじゃない?」とみたいなのがありましたらご連絡いただけるとうれしいです


もしお役に立ちましたらスキ(♡マーク)をお願いします!!! noteアカウントがなくても大丈夫です。サポートもお待ちしています!