このレッスンの前半で、ベクトル(または点)は行列(1行3列)として書き表すことができると説明しました。 しかし、行列(3行1列)としても書くことができたことに注意しましょう。 技術的には、点やベクトルを行列として表現するこれら2つの方法は完全に有効であり、どちらを選ぶかは単なる慣習の問題に過ぎない
ベクトルを行列として書く。 \( V=begin{bmatrix}x & y & z}end{bmatrix})
Vector written as matrix: \(V=begin{bmatrix}x} {}y}zend{bmatrix}}
最初の例( matrix )では、ベクトルまたは点を、いわゆる行頭順で表現しています。 2 番目の例では、点またはベクトルは列の長さの順序で書かれていると言います。ベクトルまたは点の 3 つの座標を列として垂直に書きます。
点やベクトルを行列として表現して、変換行列(簡単にするために、行列ではなく)で乗じることを覚えておいてください。 また、行列の乗算は、左の行列の列の数と右の行列の行の数が同じでなければできないことも学びました。 つまり、行列と行列は互いに掛け合わせることができますが、行列と行列は掛け合わせることができないのです。 ベクトルを行列と書くと行列と掛け合わせることができますが(この行列が掛け算の右内側にあると仮定して)、このベクトルを行列と書くと行列と掛け合わせることができないことに注意しましょう。 このことは、次の例で説明します。 乗算に関係する行列の内次元(3と3)は同じ(緑色)なので、この乗算は有効です(そして結果は行列の形で書かれた変換された点です):
$$* = \begin{bmatrix}x & y & zend{bmatrix} * \begin{bmatrix}c_{00}&c_{01}&{c_{02}}\\c_{10}&c_{11}&{c_{12}}\\c_{20}&c_{21}&{c_{22}}\\\end{bmatrix} =\begin{bmatrix}x’&y’&z’\end{bmatrix}$$
The 乗算に関係する行列の内次元(1 と 3)が同じでない(赤色)ので、この乗算は不可能である。
$$* \begin{bmatrix}x yzentaend{bmatrix}. * \begin{bmatrix} c_{00}&c_{01}&{c_{02}} c_{10}&c_{11}&{c_{12}} ◇ c_{20}&c_{21}&{c_{22}} ◇end{bmatrix}$$
ではどうしたらいいのか…。 この問題の解決策は、ベクトルや点に行列をかけるのではなく、行列MにベクトルVをかけることです。 つまり、乗算の内側で点またはベクトルを右に移動させるのです。
$$* \begin{bmatrix} c_{00}&c_{01}&{c_{02}} ◇ c_{10}&c_{11}&{c_{12}} ◇ c_{20}&c_{21}&{c_{22}} ◇end{bmatrix} ◇c_{20}{8820>{bmatrix} ◇c_{22}} ◇c_{01}{8820}{8820} ◇c_{02}{8820}◇c_{0120 * \begin{bmatrix}x’\y’\z’\end{bmatrix} = \begin{bmatrix}x’\y’\z’\end{bmatrix}$$
この演算の結果は、点を変換して行列形式で記述している点に注意してください。 つまり、最初に点を得て、最後に変換された点を得ることができるのです。 問題は解決しました。 まとめると、慣習的にベクトルや点を行長順()で表現する場合、乗算記号の左側に点を、右側に点を置く必要があります。 これを数学では、左掛け、前掛けと呼びます。 代わりにベクトルを列の長順で書くことにした場合()、行列を乗算の左側に、ベクトルまたは点を右側に置く必要があります。 これを右掛け、後掛けと呼びます。
これらの用語が実際にどのように使用されるかについて、私たちは注意する必要があります。 たとえば、Maya のドキュメントには「Maya では行列は後乗算される」と書かれています。 たとえば、点 P をオブジェクト空間からワールド空間 (P’) に変換するには、worldMatrix を後乗せする必要があります。 (P’ = P x WM)」とありますが、これは実際には事前乗算なので混乱しますが、この特定のケースでは点に対するマトリックスの位置について話しているのです。 実はこれは用語の使い方として間違っているんです。 Maya では、点とベクトルは行長ベクトルとして表現され、したがってそれらは事前乗算される(乗算で点またはベクトルが行列の前に現れるという意味)と書くべきでした。
次の表は、2 つの規約の違いをまとめたものです(ここで P、V、M はそれぞれ点、ベクトル、行列の略)。
Row-major order |
(P/V=begin{bmatrix}x & y & zend{bmatrix}) |
Left or pre->Left or pre-乗算 |
P/V * M |
列-。major order |
(P/V=BEGIN{Bmatrix}x \ y zend{Bmatrix}) |
Right or post->7343乗算 |
M * P/V |
さて、この二つの規約について学んだところで、「それは単に紙にものを書くだけの話ではないのか」と思われるかもしれません。”. 2 つの行列 A と B の積を計算する方法を知っています。A の現在の行内の各係数に、B の現在の列内の関連する要素を掛け、その結果を合計してください。 この式を 2 つの方法で適用し、結果を比較してみましょう。
Row-major order | |
---|---|
${ \begin{bmatrix}x & y & zend{bmatrix} * \begin{bmatrix}a & b & c \d & e & f \g & h & i end{bmatrix}. }$$ |
${ \begin{array}{l}x’ = x * a + y * d + z * gy’ = x * b + y * e + z * h }$$ |
Column-major order | |
${ \begin{bmatrix} a & b & c \d & e & f \g & h & i \end{bmatrix} * \begin{bmatrix}x\\y\\z\end{bmatrix} }$ |
${begin{array}{l}x’ = a * x + b * y + c * z}y’ = d * x + e * y + f * z}z’ = g * x + h * y + i * zend{array} 7342> |
点またはベクトルに行列を掛けると、行順でも列順でも同じ結果になるはずです。 3Dアプリケーションで、ある点をz軸の周りにある角度だけ回転させる場合、開発者が点やベクトルを表現するためにどんな内部規則を使ったとしても、回転後の点はある位置にあることを期待します。 しかし、上の表を見ればわかるように、行長と列長の点(またはベクトル)に同じ行列を掛けても、同じ結果にはならないことは明らかです。 気を取り直して、x’、y’、z’が同じであることを確認するために、列方向にメジャーな乗算で使用した行列を転置する必要があります(行列の転置とは何かを覚えておく必要がある場合は、行列演算の章を参照してください)。
Row-major order | |
---|---|
$${ \begin{bmatrix}x & y & zend{bmatrix} * \begin{bmatrix}a & b & c \d & e & f \g & h & i end{bmatrix}. }$ |
${begin{array}{l}x’ = x * a + y * d + z * g y’ = x * b + y * e + z * h ◇z’ = x * c + y * f + z * iend{array} }$$ |
Column-major order | |
${ \begin{bmatrix} a & d & g \b & e & h \c & f & iend{bmatrix} {$$$$$ |
${begin{array}{l}x’ = a * x + d * y + g * z }y’ = b * x + e * y + h * z }z’ = c * x + f * y + i * z }end{array} }$$ |
結論として、行長順から列長順にすることは、乗算の点またはベクトルと行列を入れ替えるだけでなく、行列を転置し、両方の規約が同じ結果を与える(逆もまた同じ)ことを保証することが必要です。
これらのことから、行-長順の規約が使われているときに点またはベクトルに適用される一連の変換は、順次順序(または読み順)で書くことができることがわかります。 例えば、点Pを行列Tで平行移動し、Rzでz軸の周りに回転させ、Ryでy軸の周りに回転させたいとする。
$P’=P * T * R_z * R_y$
もし列挙記法を使うなら、変換を逆順に呼び出す必要があります (これは直感に反すると思われます):
$P’=R_y * R_z * T * P$
そこで、「あるシステムと別のシステムを好む理由があるはず」と考えるかもしれません。 実際、どちらの方式も正しく、同じ結果が得られますが、技術的な理由から、数学と物理の教科書では、一般的にベクトルを列ベクトルとして扱います。
行順行列を使用する場合の変換の順序は、数学では関数の評価と合成の書き方に似ています。
しかしながら、行順行列規約は行列を教えやすく、それがスクラッチ ピクセル(Maya や DirectX と同様に)でそれを使用する理由です。 また、RenderMan の仕様でも標準として定義されています)。 しかし、OpenGL などの一部の 3D API では、列指向の規約が使用されています。 しかし、これは、慣習そのものや、1 つが他よりどれだけ実用的であるかということとは、実際には何の関係もありません。 これは、コンピュータとその動作に関係することなのです。 これから行列を扱うことを思い出してください。 一般に、C++ における行列の実装は次のようになります。
見てわかるように、行列の 16 の係数は float(必要な精度によっては double)の二次元配列に保存されています。 C++のMatrixクラスはテンプレートです)。 つまり,メモリ上では 16 個の係数は次のように配置されます: c00, c01, c02, c03, c10, c11, c12, c13, c20, c21, c22, c23, c30, c31, c32, c33.このように,16 個の係数は 2 次元配列に格納されています. つまり、メモリ上に連続的に配置されているのです。 では、ベクトルが行頭順で記述されるベクトル行列の乗算では、これらの係数はどのようにアクセスされるのでしょうか。
x’ に対する行列の要素が順にアクセスしないことがわかると思います。 つまり、x’を計算するためには、行列の16個の浮動小数点数の配列の1番目、5番目、9番目の浮動小数点数が必要です。 y’を計算するためには、この配列の2番目、6番目、10番目の浮動小数点にアクセスする必要があります。 そして最後に z’ を計算するには、配列の 3 番目、7 番目、11 番目の浮動小数点にアクセスする必要があります。 計算機の世界では、配列の要素に順不同でアクセスすることは必ずしも良いことではありません。 実際、CPUのキャッシュ性能を低下させる可能性があります。 ここではあまり詳しく説明しませんが、CPUがアクセスできる最も近いメモリはキャッシュと呼ばれています。 このキャッシュは、アクセスは非常に高速ですが、非常に限られた数のデータしか保存することができません。 CPUはあるデータにアクセスする必要があるとき、まずそのデータがキャッシュに存在するかどうかをチェックする。 存在すればCPUはすぐにそのデータにアクセスするが(キャッシュヒット)、存在しなければ(キャッシュミス)、まずキャッシュにエントリーを作成し、メインメモリからこの場所にデータをコピーする必要がある。 この処理は、データがすでにキャッシュに存在する場合よりも明らかに時間がかかるため、理想的にはキャッシュミスをできるだけ避けたいところです。 CPUはメインメモリから特定のデータをコピーするだけでなく、そのすぐ隣にあるデータの塊(例えば次の24バイト)もコピーします。これは、例えばコードが配列の要素にアクセスする必要がある場合、その直後にその次の要素にアクセスする可能性が高いとハードウェアエンジニアが考えたためです。 実際、プログラムでは配列の要素に対して順次ループをかけることが多いので、この仮定は正しいと思われます。 今回の行列の問題に当てはめると、行列の係数に順不同でアクセスすることが問題になる可能性があります。 CPU が要求された float とその隣の 3 つの float をキャッシュにロードすると仮定すると、x’ y’ と z’ を計算するために使用される係数は配列内で 5 つの float に分かれているため、現在の実装では多くのキャッシュミスが発生する可能性があります。 一方、カラム・メジャー・オーダー表記を使用する場合、例えば x’ を計算するには、行列の第 1、第 2、第 3 要素にアクセスする必要があります。
係数は順次アクセスするので、CPU キャッシュ機構も有効に使えます(この例ではキャッシュミスは 9 回ではなく、3 回だけです)。 結論として、プログラミングの観点からは、ポイントまたはベクトル行列の乗算を列挙型の順序で実装した方が、行型の順序で実装した場合よりも性能的に優れていると言えます。 しかし、実際には、そのようなことはなく(最適化フラグ -O、-O2、-O3 を使用してプログラムをコンパイルすると、コンパイラは多次元配列に対するループを最適化してくれます)、同じコードの行メジャー順序の実装を使用したバージョンと比較してパフォーマンスを低下させずに成功したことがあります。
Row-major and Column-Major Order in Computing
完全性のために、行メジャーおよび列メジャーという用語は、多次元配列の要素がメモリ内にレイアウトされる方法を記述するためにコンピューティングでも使用できることに触れておきましょう。 行頭順とは、多次元配列の要素を左から右、上から下へと次々に並べる方法です。 C/C++ではこの方式が採用されている。 例えば、行列:
$M = \begin{bmatrix}1&2&3&5&6{bmatrix}$
は、C/C++では次のように書けます。
そしてこの配列の要素は線形メモリに連続的に配置され、次のように記述されるでしょう。
FORTRANやMATLABなどの言語で使用されている列挙型の順序では、行列の要素は上から下、左から右へとメモリに格納されます。 同じ行列の例を使用すると、行列の要素は次のようにメモリに格納(およびアクセス)されます:
行列の要素がメモリにどのようにレイアウトされているかを知ることは、特にポインタオフセットと for ループ最適化を使用してアクセスしようとする場合に重要です(この章で以前 CPU キャッシュ性能に影響があることを解説しています)。 しかし、私たちはプログラミング言語として C/C++ しか考えないので、(コンピューティングに適用される)カラムメジャー順序は私たちにとって大きな関心事ではありません。 私たちは、この用語が使用される文脈によって、2つの異なるものを記述する可能性があることを認識するために、コンピューティングにおける用語の意味について言及しているに過ぎません。 混同しないように注意しなければならない。 数学の文脈では、ベクトル(または点)を座標の行として扱うか、列として扱うかを記述し、コンピューティングの文脈では、あるプログラミング言語が多次元配列(行列がそうです)の要素をメモリに保存し、アクセスする方法を記述します。 GL が最初に作成されたとき、開発者は行メジャー ベクトル規約を選択しました。 しかし、OpenGLを拡張した開発者たちは、列メジャーベクターに戻すべきだと考え、それを実行しました。 しかし、互換性の観点から、点と行列の掛け算のコードを変えたくなく、代わりに行列の係数がメモリに格納される順番を変えることにしました。 言い換えれば、OpenGLは列-長順で係数を保存します。これは、列-長ベクトルを使った行列からの変換係数m03、m13、m23は、行-長ベクトルを使った行列からの変換係数m30、m31、m32と同様に、float配列のインデックス13、14、15を持つことを意味します。
まとめ
2つの規約の違いを次の表にまとめました。
行-major vector (Mathematics) | 列-major vector (Column-)メジャーベクトル (Mathematics) |
---|---|
(P/V=begin{bmatrix}x & y & zend{bmatrix}) |
(P/V=begin{bmatrix}x \ y zend{bmatrix}}) |
Pre-…multiplication \(vM) |
Post-multiplication \(Mv) |
Call order と transform の適用順は同じです。 「take P, transform by T, transform by Rz, transform by Ry” は \(P’=P*T*R_z*R_y} |
Call order is the reverse of the order the transform are applied: “take P, transform by T, transform by Rz, transform by Ry” は、 \(P’=R_y*R_z*T*P) |
API.JP と記述します。 Direct X、Maya |
API: OpenGL, PBRT, Blender |
行列の行は、座標系のベース(または軸)を表します(赤:X軸、緑:Y軸、青:Z軸) ${begin{bmatrix}$${bmatrix}${bmatrix}${bmatrix}$${bmatrix} ${bmatrix} ${begin{bmatrix}${bmatrix}${bmatrix}${bmatrix}${bmatrix \color{red}{c_{00}}& \color{red}{c_{01}}&\color{red}{c_{02}}&0\\ \color{green}{c_{10}}& \color{green}{c_{11}}&\color{green}{c_{12}}&0\\ \color{blue}{c_{20}}& \color{blue}{c_{21}}&\color{blue}{c_{22}}&0\\0&0&0&1 \end{bmatrix} } $$ |
行列の列は座標系の底(または軸)を表します(赤:X軸、緑:Y軸、青:Z軸) ${ \begin{bmatrix}}… \color{red}{c_{00}}& \color{green}{c_{01}}&\color{blue}{c_{02}}&0\\ \color{red}{c_{10}}& \color{green}{c_{11}}&\color{blue}{c_{12}}&0\\ \color{red}{c_{20}}& \color{green}{c_{21}}&\color{blue}{c_{22}}&0\\0&0&0&1\end{bmatrix} }$$ |
翻訳値は、c30、c31、c32要素に格納されます。 $${\begin{bmatrix}1&0&0&0\\0&1&0&0\\0&0&1&0\\Tx&Ty&Tz&1\end{bmatrix} }$$ |
翻訳値はc03、c13、c23要素に格納される。 ${begin{bmatrix}1&0&0&Tx0&1&0&Ty0&0&1&Tz}0&0&0&1 |
行列を転置して列-長順序行列として使用 |
行列を転置して行-長順序行列として使用。7342> |
行長行列(計算) | 列長行列(計算) |
API: Direct X、Maya、PBRT |
API: OpenGL |
読者が Stackoverflow に質問を投稿し、上の表はわかりにくいという指摘がありました。 このトピックは混乱しやすく、私たちがこの問題に光を当てようと最善を尽くしても、多くの人がまだこの問題で混乱したままです。 私たちは、Stackoverflow での私たちの回答が、この質問に対する別の洞察をもたらすことを期待できると考えました。
あなたには理論(数学でペンと紙を使って行うこと)と実装(C++)で行うことがあるのです。 これらは 2 つの異なる問題です。
数学: 列または行の主要な 2 つの表記を使用できます。 行メジャー ベクトルでは、紙の上に、ベクトルと行列の乗算 vM を書く必要があります。ここで v は行ベクトル (1×4) で、M はあなたの 4×4 行列です。 なぜでしょうか? 数学的には * としか書けず、その逆はできないからです。 同様に、列を使う場合は、ベクトルを縦に書き下すか、表記法(4行1列)にする必要があります。 したがって、行列との掛け算は次のようにしか書けません。 . 行列はベクトルの前に置かれることに注意してください。 Mv. 最初の表記は左または前乗算(ベクトルが積の左側にあるため)と呼ばれ、2番目の表記(Mv)は右または後乗算(ベクトルが積の右側にあるため)と呼ばれます。
さて、ベクトル(または点)を変換する必要がある場合、紙に書き出すときに、乗算の順序に注意する必要があります。 行列Tで平行移動して、Rで回転して、Sで拡大縮小する場合、列方向の世界ではv’ = S * R * T * vと書き、行方向の世界ではv’ = v * T * R * Sと書く必要があるのです。 7342>
コンピュータ:これをC++で実装しようと思ったときがポイントになります。 これについての良い点は、C++は何についてもあなたに何も課さないということです。 行列の係数の値を好きなようにメモリにマッピングできますし、行列の掛け算を別の行列で実行するコードも好きなように書くことができます。 同様に、ベクトルと行列の掛け算の係数にどのようにアクセスするかは、完全にあなた次第です。メモリ上で係数をどのようにマッピングするか、数学的な観点からベクトルを表現するためにどのような規則を使用する必要があるかを明確に区別する必要があります。 これらは2つの独立した問題です。 この部分を行/列メジャー レイアウトと呼ぶことにします。
たとえば、行列クラスをたとえば 16 個の連続した浮動小数点数の配列として宣言することができます。 それは結構なことです。 係数 m14, m24, m34 は行列の並進部分 (Tx, Ty, Tz) を表し、列方向と言われる OpenGL 行列の規約を使うように言われても、あなたの「規約」は行方向と仮定しています。 ここで考えられる混乱は、メモリ上の係数のマッピングが、あなたが「列方向にメジャー」な行列を表現しているときとは異なるという事実に起因しています。 7342>
重要なのは、行列を3つの軸と並進によって定義される座標系の表現として見ることです。 このデータをどこに、どのようにメモリに格納するかは、完全にあなた次第です。 座標系の3軸を表す3つのベクトルをAX(x,y,z), AY(x,y,z), AZ(x,y,z) とし、並進ベクトルを (Tx, Ty, Tz) とすると、数学的には、列ベクトルを使うと、
$M = \begin{bmatrix} となりまっせ。 AXx & AYx & AZx & Tx\ AXy & AYy & AZy & Ty \ AXz & AYz & AZz & Tz \ 0 & 0 & 1 & 1 end{bmatrix}$$
座標軸は垂直方向に書かれています。 今、if you have if you use row-major:
$$M = \begin{bmatrix}. AXx & AXy & AXz & 0 \ AYx & AYy & AYz & 0 \ AZx & AZy & AZz & 0 \ Tx & Ty & Tz & 1 UNEPend{bmatrix}$$
座標軸は水平方向に記述されます。 さて、コンピュータの世界で問題になるのは、この係数をどのようにメモリに格納するかである。 7342>
どの規則を使うかは教えてくれていますか。 いいえ。 7342>
または、以下のように記述することも可能です。
これでも特にどちらの「数学」規約を使っているかはわからないのですが、「数学」規約を使うと、以下のようになります。 あなたは16個の係数を異なる方法でメモリに保存しているだけで、その方法が何であるかを知っていれば、後で適切にアクセスすることができるので、全く問題ありません。 ベクトルと行列の掛け算は、行数表記でも列数表記でも同じベクトルになることを覚えておいてください。 したがって、本当に重要なのは、ベクトルの (x,y,z) 座標に行列の正しい係数を掛けることであり、これには「あなた」が行列の係数をどのようにメモリに格納することにしたかを知っている必要があります。
この関数は、ベクトル*行列の乗算の結果は、どの表記法を使っても、ベクトルの入力座標と座標系の軸座標 AX, AY, AZ との乗算と加算に過ぎないことを強調するために書きました(どの表記法を使い、どのようにメモリに格納しているかは関係ありません)。 使用する場合:
呼び出す必要があります:
使用する場合:
あなたは:
これで、どの規約を使うかわかりますか? いいえ、vec * mat の乗算をするときに、正しい場所で正しい係数を呼び出す必要があるだけです。 さて、mat * mat の乗算に関しては、少し事情が異なります。 行列を掛ける順番は同じではないと考えてよいでしょう。 つまり、R * S * T と T * S * R は同じではありません。順序は確かに重要です。 ここで再び「行のメジャー」を使用する場合、数学的には次のように書く必要があります。
ここで ml は左手の行列、mr は右手の行列です。 これは、1次元の配列に格納された要素にアクセスしていると思わせないためです。 C++でこれを書きたい場合は、上で提案したように係数をメモリにどのように保存したかによります。