行列と変換

はじめに

このチュートリアルの前に、この前回にあたるベクトル演算をまず読まれたほうが良いでしょう。

このチュートリアルでは変換についてと、加えて行列についても少し扱います(深くは掘り下げません)。

変換はほとんどの場合、移動、回転、拡大に応用されるので、ここでは優先的に扱います。

直交座標系(OCS)

宇宙のどこかに宇宙船があると想像してみてください。Godotではこれは簡単です、ちょうどどこかに船を移動し、それを回転させます:

../../_images/tutomat1.png

さて、2Dでは、これは単純に見えます。位置と回転の角度です。しかし、私たちはここから成長し、角度を使用しないことを覚えておいてください(プラス、角度は3Dで作業する場合にはあまり役に立ちません)。

ある時点で、誰かがこの宇宙船を設計したことを理解する必要があります。Paint.net、Gimp、Photoshopなどの図面で2Dを作成するか、Blender、Max、Mayaなどの3D DCCツールを使用して3Dを作成します。

設計時には、回転していませんでした。独自の座標系で設計されました。

../../_images/tutomat2.png

これは、船の先端に座標があり、フィンに別の座標があることを意味します。ピクセル(2D)または頂点(3D)のいずれかです。

それでは、船が宇宙のどこかにあったことをもう一度思い出しましょう:

../../_images/tutomat3.png

どうやってそこに行きましたか?何が移動して、設計された場所から現在の位置に回転したのでしょうか?答えは...変換です。船は元の位置から新しい位置に変換されました。これにより、船舶を現在の場所に表示できます。

しかし、変換はこのプロセスを説明するには一般的な用語です。このパズルを解決するために、船の元の設計位置を現在の位置に重ね合わせます:

../../_images/tutomat4.png

そのため、デザインスペース も変化していることがわかります。この変換をどのように最もよく表現できますか?これに3つのベクトル(2D)を使用してみましょう。Xの正の方向を指す単位ベクトル、Yの正の方向を指す単位ベクトル、および平行移動です。

../../_images/tutomat5.png

3つのベクトルを"X"、"Y"、"Origin(原点)"と呼び、さらにそれらを船の上に重ねて、より意味のあるものにしましょう:

../../_images/tutomat6.png

さて、これは良い感じですが、まだ意味をなしていません。X、Y、Originは船がそこに着いた方法と何の関係があるのでしょうか?

では、船の先端のポイントを参考にしてみましょう:

../../_images/tutomat7.png

そして、次の操作をそれに適用してみましょう(そして船内のすべてのポイントにも適用しますが、基準点として先端を追跡します):

var new_pos = pos - origin
var newPosition = pos - origin;

選択したポイントにこれを行うと、中心に戻ります:

../../_images/tutomat8.png

これは予想されていましたが、それからもっと面白いことをしましょう。Xと点の内積を使用し、Yと点の内積に追加します。

var final_pos = Vector2(x.dot(new_pos), y.dot(new_pos))
var finalPosition = new Vector2(x.Dot(newPosition), y.Dot(newPosition));

そして、私たちが持っているのは...ちょっと待ってください、それは元の設計位置にある船です!

../../_images/tutomat9.png

この黒魔術はどのようして起きたのですか?船は宇宙から消え、今では家に戻っています!

奇妙に見えるかもしれませんが、多くのロジックがあります。ベクトル演算 で見たように、X軸までの距離とY軸までの距離が計算されたことを思い出してください。方向または平面での距離の計算は、内積の用途の1つです。これは、船内のすべてのポイントの設計座標を取り戻すのに十分でした。

そのため、これまで(X、Y、およびOriginを使用して)取り組んできたのは *指向座標系(OCS)*です。 XとYは**基準軸(基底)**で、* Origin*はオフセットです。

Basis

Originが何であるかを知っています。これは、新しい位置に変換された後、設計座標系の0,0(原点)が終わった場所です。これが*Origin*と呼ばれる理由ですが、実際には、新しい位置へのオフセットにすぎません。

基準軸はもっと面白いです。基準軸は、新しい変換された場所からのOCSのXおよびYの方向です。変更内容を2Dまたは3Dで通知します。 Origin(オフセット)とBasis(方向)は、次のように伝えます「デザインの元のX軸とY軸はここにあり、これらの方向を指しています」

それでは、基準軸の表現を変更しましょう。 2つのベクトルの代わりに、matrixを使用しましょう。

../../_images/tutomat10.png

ベクトルは、行列(matrix/マトリックス)内の水平方向にあります。次の問題は、それです..この行列って何ですか?さて、行列について聞いたことがないと仮定します。

Godotで変換(幾何学変換)

このチュートリアルでは、行列の数学(およびその演算)については詳しく説明せず、実用的な使い方のみを説明します。そのための資料はたくさんありますが、このチュートリアルを完了すると、理解しやすくなります。変換(Transform)の使用方法を説明します。

Transform2D

class_Transform2D は3x2行列です。 3つのVector2要素があり、2Dに使用されます。"X"軸は要素0、"Y"軸は要素1、"Origin"は要素2です。その単純さのため、便宜上、基準軸/原点で分割されていません。

var m = Transform2D()
var x = m[0] # 'X'
var y = m[1] # 'Y'
var o = m[2] # 'Origin'
var m = new Transform2D();
Vector2 x = m[0]; // 'X'
Vector2 y = m[1]; // 'Y'
Vector2 o = m[2]; // 'Origin'

ほとんどの操作はこのデータ型(Transform2D)で説明されますが、同じロジックが3Dにも適用されます。

単位

重要なTransformは「単位」行列です。これの意味は:

  • 'X'ポイント 右: Vector2(1,0)
  • 'Y'ポイント 上(またはピクセルの下): Vector2(0,1)
  • 'Origin'は原点 Vector2(0,0)
../../_images/tutomat11.png

It's easy to guess that an identity matrix is just a matrix that aligns the transform to its parent coordinate system. It's an OCS that hasn't been translated, rotated or scaled.

# The Transform2D constructor will default to Identity
var m = Transform2D()
print(m)
# prints: ((1, 0), (0, 1), (0, 0))
// Due to technical limitations on structs in C# the default
// constructor will contain zero values for all fields.
var defaultTransform = new Transform2D();
GD.Print(defaultTransform);
// prints: ((0, 0), (0, 0), (0, 0))

// Instead we can use the Identity property.
var identityTransform = Transform2D.Identity;
GD.Print(identityTransform);
// prints: ((1, 0), (0, 1), (0, 0))

操作

回転

Transform2Dの回転は、"rotated"関数を使用して行われます:

var m = Transform2D()
m = m.rotated(PI/2) # rotate 90°
var m = Transform2D.Identity;
m = m.Rotated(Mathf.Pi / 2); // rotate 90°
../../_images/tutomat12.png

翻訳

Transform2Dの処理を翻訳するには2つの方法があります。最初の方法は原点を移動することです:

# Move 2 units to the right
var m = Transform2D()
m = m.rotated(PI/2) # rotate 90°
m[2] += Vector2(2,0)
// Move 2 units to the right
var m = Transform2D.Identity;
m = m.Rotated(Mathf.Pi / 2); // rotate 90°
m[2] += new Vector2(2, 0);
../../_images/tutomat13.png

これは常にグローバル座標で機能します。

代わりに、行列のlocal座標(基準軸が向いている方向)での変換が必要な場合、Transform2D.translated() メソッドがあります:

# Move 2 units towards where the basis is oriented
var m = Transform2D()
m = m.rotated(PI/2) # rotate 90°
m = m.translated( Vector2(2,0) )
// Move 2 units towards where the basis is oriented
var m = Transform2D.Identity;
m = m.Rotated(Mathf.Pi / 2); // rotate 90°
m = m.Translated(new Vector2(2, 0));
../../_images/tutomat14.png

グローバル座標をローカル座標に手動で変換することもできます:

var local_pos = m.xform_inv(point)
var localPosition = m.XformInv(point);

しかし、さらに良いことに、次のセクションに書かれているように、このためのヘルパー関数があります。

ローカル座標からグローバル座標、およびその逆

ローカル座標とグローバル座標を変換するためのヘルパーメソッドがあります。

2Dの場合は Node2D.to_local()Node2D.to_global()、3Dには Spatial.to_local()、および :ref:` Spatial.to_global() <class_Spatial_method_to_global>` があります。

Scale

行列も拡大縮小できます。スケーリングは、基底ベクトル(基準軸)にベクトルを乗算します(Xベクトルとスケールのx要素、Yベクトルをスケールのy要素で乗算します)。このばあい原点はそのまま残します:

# Make the basis twice its size.
var m = Transform2D()
m = m.scaled( Vector2(2,2) )
// Make the basis twice its size.
var m = Transform2D.Identity;
m = m.Scaled(new Vector2(2, 2));
../../_images/tutomat15.png

行列内のこの種の演算は累積的です。これは、すべてが前のものに関連して開始することを意味します。この惑星に十分長く住んでいる人にとって、変換がどのように機能するかについての良いリファレンスはこれです:

../../_images/tutomat16.png

行列はタートルと同様に使用されます。カメはおそらく内部に行列を持っていました(そして、おそらくサンタが本物ではないことを発見した、あなたはこの何年も学んでいるでしょう)。訳注: 海外のプログラミング教育でLOGOなどのタートルグラフィックスが使用されている事に関連した文だと思われます。

幾何学変換(変形)

変換とは、座標系を切り替えることです。位置(2Dまたは3D)を「デザイナー」座標系からOCSに変換するには、"xform"メソッドが使用されます。

var new_pos = m.xform(pos)
var newPosition = m.Xform(position);

そして、基準軸のみ(変換なし):

var new_pos = m.basis_xform(pos)
var newPosition = m.BasisXform(position);

逆変換

逆の操作(ロケットで行ったこと)を行うには、"xform_inv"メソッドが使用されます:

var new_pos = m.xform_inv(pos)
var newPosition = m.XformInv(position);

基準軸のみ:

var new_pos = m.basis_xform_inv(pos)
var newPosition = m.BasisXformInv(position);

直交行列

ただし、行列がスケーリングされている場合(ベクトルが単位長ではありません)、または基底ベクトルが直交(90°で交差)していない場合、逆変換は機能しません。

言い換えれば、逆変換は直交行列でのみ有効です。このため、これらの場合、アフィン変換の逆行列を計算する必要があります。

変換、または単位行列の逆変換は、変更されていない位置を返します:

# Does nothing, pos is unchanged
pos = Transform2D().xform(pos)
// Does nothing, position is unchanged
position = Transform2D.Identity.Xform(position);

アフィン逆行列

アフィン逆行列は、行列にスケールがあるか、軸ベクトルが直交していないかに関係なく、別の行列の逆演算を行う行列です。アフィン逆行列は、affine_inverse()メソッドを使用して計算されます:

var mi = m.affine_inverse()
pos = m.xform(pos)
pos = mi.xform(pos)
# pos is unchanged
var mi = m.AffineInverse();
position = m.Xform(position);
position = mi.Xform(position);
// position is unchanged

行列が正規直交の場合には:

# if m is orthonormal, then
pos = mi.xform(pos)
# is the same is
pos = m.xform_inv(pos)
// if m is orthonormal, then
position = mi.Xform(position);
// is the same is
position = m.XformInv(position);

行列乗算

行列は乗算できます。 2つの行列の乗算は、変換を「結合」(連結)します。

ただし、慣例に従って、乗算は逆の順序で行われます。

例:

var m = more_transforms * some_transforms
var m = moreTransforms * someTransforms;

もう少し明確にするために、次の手順を実行します:

pos = transform1.xform(pos)
pos = transform2.xform(pos)
position = transform1.Xform(position);
position = transform2.Xform(position);

次の場合と同じです:

# note the inverse order
pos = (transform2 * transform1).xform(pos)
// note the inverse order
position = (transform2 * transform1).Xform(position);

ただし、これは同じではありません:

# yields a different results
pos = (transform1 * transform2).xform(pos)
// yields a different results
position = (transform1 * transform2).Xform(position);

行列演算では、A * B は B * A と同じではないからです。

逆行列による乗算

行列に逆行列を掛けると、次の結果が得られます:

# No matter what A is, B will be identity
var B = A.affine_inverse() * A
// No matter what A is, B will be identity
var B = A.AffineInverse() * A;

単位行列による乗算

行列を単位行列で乗算すると、変更されない行列が生まれます:

# B will be equal to A
B = A * Transform2D()
// B will be equal to A
var B = A * Transform2D.Identity;

行列のヒント

階層変換を使用する場合、行列の乗算が逆になることに注意してください!階層のグローバル変換を取得するには、次を実行します:

var global_xform = parent_matrix * child_matrix
var globalTransform = parentMatrix * childMatrix;

3つのレベルの場合:

var global_xform = gradparent_matrix * parent_matrix * child_matrix
var globalTransform = grandparentMatrix * parentMatrix * childMatrix;

親を基準にして行列を作成するには、アフィン逆行列(または正規直交行列の場合は通常の逆行列)を使用します。

# transform B from a global matrix to one local to A
var B_local_to_A = A.affine_inverse() * B
// transform B from a global matrix to one local to A
var bLocalToA = A.AffineInverse() * B;

上記の例と同じように元に戻します:

# transform back local B to global B
B = A * B_local_to_A
// transform back local B to global B
B = A * bLocalToA;

さて、うまくいけばこれで十分でしょう! 3Dマトリックスに移動してチュートリアルを完了しましょう。

3Dの行列と変換

前に述べたように、3Dの場合、回転行列用に3つの Vector3 ベクトルを処理し、原点用に追加のベクトルを処理します。

Basis

Godotには、Basis という名前の3x3行列用の特別な型があります。 3Dの回転とスケールを表すために使用できます。サブベクトルには次のようにアクセスできます:

var m = Basis()
var x = m[0] # Vector3
var y = m[1] # Vector3
var z = m[2] # Vector3
var m = new Basis();
Vector3 x = m[0];
Vector3 y = m[1];
Vector3 z = m[2];

または、次のようにします:

var m = Basis()
var x = m.x # Vector3
var y = m.y # Vector3
var z = m.z # Vector3
var m = new Basis();
Vector3 x = m.x;
Vector3 y = m.y;
Vector3 z = m.z;

単位基底(基準軸/Basis)には次の値があります:

../../_images/tutomat17.png

そして、次のようにアクセスできます:

# The Basis constructor will default to Identity
var m = Basis()
print(m)
# prints: ((1, 0, 0), (0, 1, 0), (0, 0, 1))
// Due to technical limitations on structs in C# the default
// constructor will contain zero values for all fields.
var defaultBasis = new Basis();
GD.Print(defaultBasis);
// prints: ((0, 0, 0), (0, 0, 0), (0, 0, 0))

// Instead we can use the Identity property.
var identityBasis = Basis.Identity;
GD.Print(identityBasis);;
// prints: ((1, 0, 0), (0, 1, 0), (0, 0, 1))

3Dでの回転

3Dの回転は、2Dよりも複雑です(平行移動とスケールは同じです)。3Dで回転するには、を選択する必要があります。回転はこの軸の周りに発生します。

回転の軸は正規化ベクトルでなければなりません。なので、任意の方向を指すことができるベクトルですが、長さは1(1.0)でなければなりません。

#rotate in Y axis
var m3 = Basis()
m3 = m3.rotated( Vector3(0,1,0), PI/2 )
// rotate in Y axis
var m3 = Basis.Identity;
m3 = m3.Rotated(new Vector3(0, 1, 0), Mathf.Pi / 2);

幾何学変換(変形)

ミックスに最後の要素を追加するために、Godotは Transform タイプを提供します。 Transformには2つのメンバーがあります:

  • basis (タイプ Basisの)
  • origin (タイプ Vector3の)

どの3D変換もTransformで表すことができ、基底と原点を分離することで、移動/変形と回転を別々に簡単に行うことができます。

例:

var t = Transform()
pos = t.xform(pos) # transform 3D position
pos = t.basis.xform(pos) # (only rotate)
pos = t.origin + pos # (only translate)
var t = new Transform(Basis.Identity, Vector3.Zero);
position = t.Xform(position); // transform 3D position
position = t.basis.Xform(position); // (only rotate)
position = t.origin + position; // (only translate)