Unityのシェーダーに関するお話です。
皆さんはRendererコンポーネントがlocalToWorldMatrixというプロパティを持っているのをご存知だろうか? 僕はつい最近この存在に気づいた。なぜRendererなんてメジャーなコンポーネントをリファレンスでちゃんと確認していなかったのだろう、と後になって思うのだけれど、それまでは当然のようにTransformのlocalToWorldMatrixを使っていた。
なんでRendererがlocalToWorldMatrixなんてプロパティを持っているのかと言えば、たぶんUnityがDrawコールを減らすためにバッチ処理するからだと思う。リファレンスには
This property MUST be used instead of Transform.localToWorldMatrix, if you’re setting shader parameters.
と書いてある。
でも、Dynamic Batchが有効になりそうな状況でRendererのlocalToWorldMatrixを覗いてみたけど、別に単位行列とかになってなく、普通の値だったんだよね。うーん。。。
そして、Unityはバッチ処理以外にもメッシュの頂点をいじくっているのをご存知だろうか? これもつい最近気付いたことなのだが、Unityはオブジェクトに一様でないスケールがかかっているときは、あらかじめメッシュの頂点にスケールをかけていて、頂点シェーダーにはスケールが適用済みの頂点が渡されているのであった。
それならば、RendererのlocalToWorldMatrixにはスケールが取り除かれたマトリクスが入っているだろうと思いきや、別にそういうわけではなかった。じゃあRendererのlocalToWorldMatrixは何のためにあるんだ! と声を大にして言いたい。が、言ったところで何も変わらないので、じゃあどうしよう、というのが今回の内容。ちょっと前置きが長くなってしまった。
やろうとしていることは、例えばオブジェクトに何かテクスチャを投影して貼り付けるというようなこと。この投影のマトリクスがMatrix4x4 uvProjection
という変数で、ワールド座標系からUV座標に投影するものだとしよう。頂点シェーダーで無駄な処理をしたくないので、この投影マトリクスをローカル座標系からUV座標系に1発で変換するものにしたい。
そこで、
1 |
Matrix4x4 localToUVProjection = uvProjection * renderer.localToWorldMatrix; |
というマトリクスを作ってシェーダーに渡すわけである。
ところが、上でも書いたように、頂点シェーダーに渡される頂点にはスケールがかかっている場合がある。これは常にというわけではなく、一様でないスケール(つまり、X軸, Y軸, Z軸の各方向に対するスケールが同じじゃない)場合、もしくはスケールの値が負の場合にあらかじめ頂点にスケールがかかってしまう。スケールがきっかり一様、もしくはだいたい一様で、かつスケールの値が正の場合にはスケールかかからない。なので、上のようなマトリクスを使った場合、一様ではないスケールがかかったときにおかしな結果になってしまう。
そこで、色々実験した結果、次のようにするとうまく行くようになる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
Matrix4x4 GetLocalToUVProjectionMatrix() { Matrix4x4 localToWorld = renderer.localToWorldMatrix; if (IsVertexPrescaled()) { localToWorld.SetColumn(0, localToWorld.GetColumn(0).normalized); localToWorld.SetColumn(1, localToWorld.GetColumn(1).normalized); localToWorld.SetColumn(2, localToWorld.GetColumn(2).normalized); } return uvProjectionMatrix * localToWorld; } bool IsVertexPrescaled() { #if (UNITY_4_3 || UNITY_4_4 || UNITY_4_5 || UNITY_4_6) // support Unity 4.3 or later Vector3 scale = transform.localScale; float max = Mathf.Max (scale.x, Mathf.Max (scale.y, scale.z)); float min = Mathf.Min (scale.x, Mathf.Min (scale.y, scale.z)); return 0.00001f * max < (max - min); #else return false; #endif } |
#if
でUnityのバージョンで処理を変えているのは、この件についてUnity Answersで質問してみたところ、Unity 5では頂点にスケールをかけるのをやめるらしい、という回答をもらったから。ソースは未確認。そしてUnity 5も持ってないので実際にどうなのかも未確認です。
しかし、こんなことするくらいなら、Unityのビルトインのマトリクスを使った方が良いんじゃないかとも思う。上のIsVertexPrescaled()
を使うのはちょっと危うい気もするし、ビルトインのマトリクスを使えばDynamic Batchが使えるというメリットもある。
例えば、頂点シェーダーでワールド座標系の頂点座標を計算するのであれば、uvProjection
をそのまま渡して使ってしまえばいい。ただ、ワールド座標系を経由すると、ワールドが巨大な場合に丸め誤差が大きくなって精度が落ちてしまうという問題がある。UV座標の計算ならそんなに精度は必要ないけど、原点から遠い場所でちょっとテクスチャがずれてしまうなんてことが起こってしまう可能性はあると思う。それに、頂点シェーダーでワールド座標系の頂点座標を使うついでがなければ、ワールド座標系の頂点座標を計算するのはムダである。
別の方法としては、透視変換後の座標を使うという手がある。透視変換なら頂点シェーダーで必ずやる必要があるので、ムダな処理にはならないし、ワールド座標系を経由しないので、精度が落ちる心配もない。例えば頂点シェーダーのコードはこんな風になるだろう。
1 2 3 |
v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); o.uv = mul(_uvProj, o.pos); |
そして、_uvProj
は次のように計算すればいい。
1 2 3 4 |
// (訂正) // Matrix4x4 cameraProjection = Camera.current.projectionMatrix * Camera.current.worldToCameraMatrix; Matrix4x4 cameraProjection = GL.GetGPUProjectionMatrix(Camera.current.projectionMatrix) * Camera.current.worldToCameraMatrix; Matrix4x4 uvProj = uvProjection * cameraProjection.inverse; |
(訂正内容についてはこちらを参照)
ただし、この処理はカメラ毎にやる必要があるので、OnWillRenderObject
で行なう必要がある。
それにしても、なぜUnityは頂点にいちいちスケールをかけるのだろう? 一様じゃないときにのみかかるので、法線ベクトルとかを正しく変換したかったんだろうけど、それなら_World2Object
を転置してかければいいだけなので、頂点シェーダーでやっても問題ない。それに、あらかじめ頂点にスケールをかけるということは、頂点バッファをコピーしなきゃいけないので、メモリの無駄遣いにもなる。法線ベクトルやタンジェントを別の意味で使いたい人もいるだろうし、勝手にスケールをかけられては困る場合だってあるはずだ。まあ、そんなわけでUnity 5ではスケールを勝手にかけるのをやめるのだろう。
結論としては、localToWorldMatrixはそのままでは使えないし、Dynamic Batchを有効にする意味でも、localToWorldMatrixは使わずになるべくビルトインのマトリクスを使った方が良い。
でも、ビルトインのマトリクスを使うとシェーダーのコードがややこしくなったり、別にDynamic Batchは必要ないという場合は、どうせUnity 5ではスケールがかからなくなる(らしい)ので、とりあえず上に書いたIsVertexPrescaled()
を使って、localToWorldMatrixを利用するという手もある。もちろんスケールなんて使わないぜっていう人は気にせずlocalToWorldMatrixを使っても良い。僕の場合はAsset Storeに出すためのアセットを作っていたので、対策をせざるを得なかった。
Pingback: スクリプトでUNITY_MATRIX_MVPを計算する | Nyahoon Games Pte. Ltd.