エフアンダーバー

個人開発の記録

【Unity】 Matrix4x4の罠

Matrix4x4でとてつもなくしょうもないバグにハマったのでメモ。

問題コード

何気なくこんな感じのコードを書いて、「あれ?」となりました。

using UnityEngine;

public class MatrixSample : MonoBehaviour
{
    private void Start()
    {
        transform.position = Vector3.one;
        transform.rotation = Quaternion.identity;
        transform.localScale = Vector3.one;

        Vector3 result = transform.localToWorldMatrix * Vector3.zero;

        Debug.Log(result);   // (0.0, 0.0, 0.0)
    }
}

なんとなくわかると思いますが、期待していた結果は(1.0, 1.0, 1.0)です。

行列演算であることを考えれば当たり前のことなのですが、 Unityで普段Matrix4x4で計算することがあまりないので、 ついQuaternionと同じ感覚で書いてしまいました。

原因

Matrix4x4Vector3との間には乗算が定義されていません。 また、Vector3にはVector4への暗黙的な型変換が定義されています。

この結果として右辺のVector3はw値として0が設定されたVector4に暗黙的に型変換され、 Matrix4x4Vector4との間に定義された乗算が呼び出されます。 この演算結果はVector4ですが、Vector4からVector3への暗黙的な型変換も存在するため、 w値が捨てられたVector3が結果として代入されます。

つまり、

Vector3 result = transform.localToWorldMatrix * Vector3.zero;

は実際には、

Vector3 result = (Vector3)(transform.localToWorldMatrix * (Vector4)Vector3.zero);

として実行されます。

行列演算時のw値が0なので、当然平行移動は一切適用されません。

正しい記述

w値に1が入れば正しく計算できるため、明示的に指定してやれば正しい結果となります。

Vector3 result = transform.localToWorldMatrix * new Vector4(0.0f, 0.0f, 0.0f, 1.0f);

ただ、Matrix4x4にはMultiplyPointという専用のメソッドが用意されているので、そちらを利用した方がいいでしょう。

Vector3 result = transform.localToWorldMatrix.MultiplyPoint(Vector3.zero);

localToWorldMatrixworldToLocalMatrixのように4行目の要素を利用しないのであれば、 MultiplyPoint3x4を利用すると不要な計算をスキップできます。

Vector3 result = transform.localToWorldMatrix.MultiplyPoint3x4(Vector3.zero);

おわりに

わかってしまえば本当にしょうもないことなのですが、正しいはずと思い込むと結構ハマります。 ベクトルの変換、せめて次元を増やすときは明示的でよかったんじゃないかな・・・。

今までなるべく短い記事というのは避けていたのですが、 特にこれ以上書くこともないし、記事にしないよりはマシかなということで投稿。 今後はちょくちょくこんな感じの記事も書いていくかもしれません。



執筆時のUnityのバージョン:5.6.0f3