OpenGLとDirectXで行列の形式が違うとか、row-majorとcolumn-majorのどちらが良いのか、など行列表現に関して情報が混乱していたので、行列の表現と実装について個人的にまとめてみました。なお、話を簡単にするため、変換行列とベクトルの乗算のみを考えます。
数学での行列
行列は横(行、row)と縦(列、column)に要素(成分)を並べて
と表記します。行数が1のものは行ベクトル、列数が1のものは列ベクトルと呼ばれます。
行ベクトルはのように行列に対して左から掛けます。この表記はDirectXでよく使われます。例えば、平行移動の行列は
と表記します。一方、列ベクトルはのように行列に対して右から掛けます。この表記はOpenGLでよく使われます。例えば、平行移動の行列は
と表記します。
つまり、行ベクトルと列ベクトルの関係を考えると、それらの変換行列は転置の関係にあることが分かります。
プログラムでの行列
二次元である行列を一次元であるメモリに格納するには、マッピングを決める必要があります。その方法にはrow-majorとcolumn-majorの2種類があります。row-majorはのように行を優先して配置します。一方、column-majorはのように列を優先して配置します。
つまり、row-majorとcolumn-majorの関係を考えると、ベクトルのときと同様に、転置の関係にあることが分かります。
実装
行列の積は積和かドット積のいずれかで実装できます。row-major行列では、行ベクトルとの積は積和で、列ベクトルとの積はドット積で実装できます。逆に、column-major行列では、行ベクトルとの積はドット積で、列ベクトルとの積は積和で実装できます。
これらの違いを大雑把に言うと、積和による実装は計算効率に優れ、ドット積による実装は空間効率に優れると考えて良さそうです。積和による実装では、ベクトルがと自明であるときに行列の最終段との乗算を省略できます。また、融合積和(FMA)を使うことで計算の誤差を比較的少なくすることができます。そしてなにより、積和命令のほうがドット積命令より高速に動作するかもしれません。一方、ドット積による実装では、行列の最終段がと自明であるときにそれを省略してメモリを節約できます。
実例
OpenGLは列ベクトルを使って変換行列を表記します。また、そのアプリケーションでよく使われる数学ライブラリであるglmはcolumn-majorで行列クラスを定義します。すると、メモリ上ではa[12],a[13],a[14]に平行移動成分があるように見えるはずです。そして、そのシェーダー言語であるGLSLは行列がcolumn-majorでメモリに格納されているとみなすので、ベクトルを右から掛けることになります。
一方、DirectXは行ベクトルを使って変換行列を表記します。また、そのアプリケーションでよく使われる数学ライブラリであるDirectXMathはrow-majorで行列クラスを定義します。すると、メモリ上では、OpenGLのときと同様に、a[12],a[13],a[14]に平行移動成分があるように見えるはずです。そして、そのシェーダー言語であるHLSLは既定では行列がcolumn-majorでメモリに格納されているとみなすので、OpenGLのときと同様に、ベクトルを右から掛けることになります。1
つまり、OpenGLとDirectXの行列表現は数学上では異なるもののプログラム上ではどちらの実装も同じになります。
まとめ
row-majorとcolumn-majorは二次元から一次元へのマッピングの話であって、それ自体に良い悪いは特にありません。row-majorとcolumn-majorは積の実装に違いがありますが、行ベクトルを掛けるか列ベクトルを掛けるかによって対応関係が逆転するので、それ自体に固有の意味合いはありません。また、OpenGLとDirectXの行列はインターフェイス上では異なりますが、諸々が打ち消し合って最終的にプログラム上では同じになります。
参考文献
Footnotes
-
コンパイラオプションでrow-majorにすることもできます。その場合、ベクトルは左から掛けることになります。 ↩