行列と変換

はじめに

このチュートリアルを読むには、まずベクトルの知識が必要なので、ベクトル演算 チュートリアルをよく読んで理解することをお勧めします。

このチュートリアルでは、*変換*と、行列を使用してGodotでそれらを表現する方法について説明します。行列の詳細なガイドではありません。変換はほとんどの場合、平行移動、回転、スケールとして適用されるため、これらを行列で表現する方法に焦点を当てます。

このガイドのほとんどは、Transform2DVector2 を使用して2Dに焦点を当てていますが、3Dでの動作は非常に似ています。

注釈

前のチュートリアルで述べたように、Godotでは、Y軸が2Dで を指していることに注意することが重要です。これは、ほとんどの学校が線形代数を教える方法の反対で、学校の授業ではY軸は上向きです。

注釈

慣例では、X軸は赤、Y軸は緑、Z軸は青です。このチュートリアルは、これらの規則に合わせて色分けされていますが、原点ベクトルも青色で表します。

行列成分と単位行列

単位行列は、平行移動、回転、スケールのないtransformを表します。単位行列と、その成分が視覚的にどのように表示されるかを確認することから始めましょう。

../../_images/identity.png

行列には行と列があり、変換行列にはそれぞれの動作に関する特定の規則があります。

上の画像では、赤のXベクトルは行列の最初の列で表され、緑のYベクトルは同様に2番目の列で表されています。列を変更すると、これらのベクトルが変更されます。次のいくつかの例で、それらの操作方法を説明します。

通常は列を操作するため、行を直接操作することを心配する必要はありません。ただし、マトリックスの行は、特定の方向への移動に寄与するベクトルを示すものと考えることができます。

When we refer to a value such as t.x.y, that's the Y component of the X column vector. In other words, the bottom-left of the matrix. Similarly, t.x.x is top-left, t.y.x is top-right, and t.y.y is bottom-right, where t is the Transform2D.

変換行列のスケーリング

スケールの適用は、理解するのが最も簡単な操作の1つです。まず、ベクトルの下にGodotロゴを配置して、オブジェクトに対する効果を視覚的に確認できるようにします:

../../_images/identity-godot.png

行列をスケーリングするために必要なことは、各成分に必要なスケールを乗算することだけです。 2倍に拡大しましょう。1 x 2が2になり、0 x 2が0になるため、次のようになります:

../../_images/scale.png

コードでこれを行うには、各ベクトルを単純に乗算します:

var t = Transform2D()
# Scale
t.x *= 2
t.y *= 2
transform = t # Change the node's transform to what we just calculated.
Transform2D t = Transform2D.Identity;
// Scale
t.x *= 2;
t.y *= 2;
Transform = t; // Change the node's transform to what we just calculated.

元のスケールに戻したい場合は、各成分に0.5を掛けます。変換行列のスケーリングはこれでほぼすべてです。

既存の変換行列からオブジェクトのスケールを計算するには、各列ベクトルで length() を使用できます。

注釈

実際のプロジェクトでは、scaled() メソッドを使用してスケーリングを実行できます。

変換行列の回転

単位行列の下にGodotロゴを配置して、以前と同じ方法で開始します:

../../_images/identity-godot.png

例として、Godotロゴを時計回りに90度回転させたいとしましょう。現在、X軸は右向き、Y軸は下向きです。これらを頭の中で回転させると、論理的には新しいX軸が下を向き、新しいY軸が左を指すはずです。

Godotのロゴとそのベクトルの両方をつかみ、それを中央で回転させると想像できます。回転を終了するたびに、ベクトルの向きによって行列が決まります。

通常の座標で「下」と「左」を表す必要があるため、Xを (0, 1)に、Yを (-1, 0)に設定します。これらは、Vector2.DOWN および Vector2.LEFT の値でもあります。これを行うと、オブジェクトを回転させるという望ましい結果が得られます:

../../_images/rotate1.png

If you have trouble understanding the above, try this exercise: Cut a square of paper, draw X and Y vectors on top of it, place it on graph paper, then rotate it and note the endpoints.

To perform rotation in code, we need to be able to calculate the values programmatically. This image shows the formulas needed to calculate the transformation matrix from a rotation angle. Don't worry if this part seems complicated, I promise it's the hardest thing you need to know.

../../_images/rotate2.png

注釈

Godotはすべての回転を度ではなくラジアンで表します。 1回転は TAU または PI*2 ラジアンで、90度の4分の1回転は TAU/4 または PI/2 ラジアンです。TAU を使用すると、通常、コードが読みやすくなります。

注釈

おもしろい事実: GodotではYが であることに加えて、回転は時計回りに表されます。これにより、これらの違いが「キャンセルされる」ため、すべての数学およびトリガ関数が"Y-is-up CCW"(Yが上で反時計回り)システムと同じように動作することを意味します。両方のシステムの回転は「XからY」であると考えることができます。

0.5ラジアン(約28.65度)の回転を実行するには、0.5の値を上記の式に差し込み、実際の値がどうあるべきかを評価します:

../../_images/rotate3.png

コードでそれを行う方法を次に示します(スクリプトをNode2Dに配置します):

var rot = 0.5 # The rotation to apply.
var t = Transform2D()
t.x.x = cos(rot)
t.y.y = cos(rot)
t.x.y = sin(rot)
t.y.x = -sin(rot)
transform = t # Change the node's transform to what we just calculated.
float rot = 0.5f; // The rotation to apply.
Transform2D t = Transform2D.Identity;
t.x.x = t.y.y = Mathf.Cos(rot);
t.x.y = t.y.x = Mathf.Sin(rot);
t.y.x *= -1;
Transform = t; // Change the node's transform to what we just calculated.

既存の変換行列からオブジェクトの回転を計算するには、atan2(t.x.y, t.x.x) を使用できます。tはTransform2Dです。

注釈

実際のプロジェクトでは、rotated() メソッドを使用して回転を実行できます。

変換行列の基底

これまでは、回転、スケール、および/またはせん断(高度な内容で、最後に扱います)の表現を担当するベクトル x および y のみを使用してきました。 XおよびYベクトルを合わせて変換行列の 基底 と呼ばれます。「基底」および「基底ベクトル」という用語は重要です。

Transform2D には、実際には3つの Vector2 値があります: x、` y`、および origin です。origin 値は基底の一部ではありませんが、transformの一部であり、位置を表すために必要です。これ以降、すべての例で原点ベクトルを追跡します。originは別の列と考えることができますが、多くの場合、完全に独立していると考える方が良いでしょう。

3Dの場合、コードが複雑になる可能性があり、それを class_Transform`(これは、原点用に1つの :ref: class_Basis` と、1つの追加の Vector3 で構成されます)から分離する方が理にかなってるため、Godoには基底の3つの Vector3 値を保持するための別個の :ref:` class_Basis` 構造を持っています。

変換行列の平行移動

origin ベクトルを変更することは、変換行列の 並行移動 と呼ばれます。並行移動とは、基本的にオブジェクトを「動かす」ための専門用語ですが、明示的には回転を伴いません。

Let's work through an example to help understand this. We will start with the identity transform like last time, except we will keep track of the origin vector this time.

../../_images/identity-origin.png

オブジェクトを(1, 2)の位置に移動させたい場合は、単にその origin ベクトルを(1, 2)に設定する必要があります:

../../_images/translate.png

また、origin を直接追加または変更するために異なる操作を実行する translated() メソッドもあります。translated() メソッドはオブジェクトを それ自身の回転に関連して 変換します。たとえば、時計回りに90度回転したオブジェクトは、Vector2.UP`translated() ` を使用すると右に移動します。

注釈

Godotの2Dはピクセルに基づく座標を使用するため、実際のプロジェクトでは数百という単位で並行移動を行う必要があります。

すべてをまとめる

We're going to apply everything we mentioned so far onto one transform. To follow along, create a simple project with a Sprite node and use the Godot logo for the texture resource.

平行移動を(350, 150)に設定し、-0.5 radで回転し、3でスケーリングしましょう。スクリーンショットと再現コードを投稿しましたが、コードを見ずにスクリーンショットを再現することをおすすめします!

../../_images/putting-all-together.png
var t = Transform2D()
# Translation
t.origin = Vector2(350, 150)
# Rotation
var rot = -0.5 # The rotation to apply.
t.x.x = cos(rot)
t.y.y = cos(rot)
t.x.y = sin(rot)
t.y.x = -sin(rot)
# Scale
t.x *= 3
t.y *= 3
transform = t # Change the node's transform to what we just calculated.
Transform2D t = Transform2D.Identity;
// Translation
t.origin = new Vector2(350, 150);
// Rotation
float rot = -0.5f; // The rotation to apply.
t.x.x = t.y.y = Mathf.Cos(rot);
t.x.y = t.y.x = Mathf.Sin(rot);
t.y.x *= -1;
// Scale
t.x *= 3;
t.y *= 3;
Transform = t; // Change the node's transform to what we just calculated.

変換行列のせん断(高度な内容)

注釈

変換行列を 使用 する方法のみを探している場合は、チュートリアルのこのセクションをスキップしてください。このセクションでは、変換行列の理解を深める目的で、一般的に使用されない側面について説明します。

transformは、上記のアクションの組み合わせよりも自由度が高いことに気づいたかもしれません。 2D変換行列の基底には、2つの Vector2 値に合計4つの数値がありますが、回転値とスケールのVector2には3つの数値しかありません。欠落している自由度の高い概念は、せん断と呼ばれます。

Normally, you will always have the basis vectors perpendicular to each other. However, shearing can be useful in some situations, and understanding shearing helps you understand how transforms work.

視覚的にどのように見えるかを示すために、グリッドをGodotロゴにオーバーレイしましょう:

../../_images/identity-grid.png

このグリッド上の各ポイントは、基底ベクトルを加算することにより取得されます。右下隅はX + Y、右上隅はX - Yです。基底ベクトルを変更すると、グリッドが基底ベクトルで構成されるため、グリッド全体が一緒に移動します。基底ベクトルにどのような変更を加えても、現在平行なグリッド上のすべての線は平行のままです。

例として、Yを(1, 1)に設定しましょう:

../../_images/shear.png
var t = Transform2D()
# Shear by setting Y to (1, 1)
t.y = Vector2.ONE
transform = t # Change the node's transform to what we just calculated.
Transform2D t = Transform2D.Identity;
// Shear by setting Y to (1, 1)
t.y = Vector2.One;
Transform = t; // Change the node's transform to what we just calculated.

注釈

エディタでTransform2Dの生の値を設定することはできません。そのため、オブジェクトをせん断したい場合はコードを使用する 必要 があります。

ベクトルが垂直ではなくなったため、オブジェクトはせん断化されました。グリッドの下部中央は、本体に対して(0, 1)であり、それが(1, 1)のワールド位置に配置されています。

オブジェクト内の座標は、テクスチャではUV座標と呼ばれます。そのため、ここではその用語を借用しましょう。相対位置からワールド位置を見つける式はU * X + V * Yです。ここで、UとVは数値で、XとYは基底ベクトルです。

グリッドの右下隅は常に(1, 1)のUV位置にあり、(2, 1)のワールド位置にあります。これは、X*1 + Y*1すなわち (1, 0) + (1, 1)、 (1 + 1, 0 + 1)、または (2, 1)です。これは、画像の右下隅の位置に関する観察結果と一致します。

同様に、グリッドの右上隅は常に(1, -1)のUV位置にあり、X*1 + Y*-1すなわち(1, 0) - (1, 1)、(1 - 1, 0 - 1)、または(0, -1)から計算されるワールド位置(0, -1)にあります。これは、画像の右上隅の位置に関する観察結果と一致します。

変換行列がオブジェクトにどのように影響するか、基底ベクトル間の関係、およびオブジェクトの"UV"または「内部座標」のワールド位置がどのように変化するかを完全に理解できたと思います。

注釈

Godotでは、すべての変換計算は親ノードを基準にして行われます。 「ワールド位置」を参照する場合、ノードに親がある場合は、そのノードの親に対する相対的な位置になります。

追加の説明が必要な場合は、線形変換に関する3Blue1Brownの優れたビデオをご覧ください: https://www.youtube.com/watch?v=kYB8IZa5AuE

変換の実用的な応用

実際のプロジェクトでは、通常、複数の Node2D または :ref:` class_Spatial` ノードを相互にペアレント化することにより、transform内で変換を操作します。

ただし、必要な値を手動で計算すると便利な場合があります。Transform2D または :ref:` class_Transform` を使用して手動でノードの変換を計算する方法について説明します。

transform間の位置の変換

transformの内外で位置を変換したい場合が多くあります。たとえば、プレイヤーに対する相対的な位置があり、そのワールド(親相対)位置を検索する場合、またはワールド位置があり、そのプレイヤーとの相対的な位置を知りたい場合です。

"xform"メソッドを使用して、プレイヤーに対する相対的なベクトルのワールド空間内での値を得る事ができます:

# World space vector 100 units below the player.
print(transform.xform(Vector2(0, 100)))
// World space vector 100 units below the player.
GD.Print(Transform.Xform(new Vector2(0, 100)));

そして、"xform_inv"メソッドを使用して、ワールド空間内の位置をプレイヤーに対する相対的な位置に変換した結果を得ることができます:

# Where is (0, 100) relative to the player?
print(transform.xform_inv(Vector2(0, 100)))
// Where is (0, 100) relative to the player?
GD.Print(Transform.XformInv(new Vector2(0, 100)));

注釈

transfomが(0, 0)に配置されていることが事前にわかっている場合は、代わりに"basis_xform"または"basis_xform_inv"メソッドを使用できます。これらのメソッドは、平行移動の処理をスキップします。

オブジェクトをそれ自身に対して相対的に移動する

特に3Dゲームでの一般的な操作は、オブジェクトをそれ自体に対して移動することです。たとえば、一人称シューティングゲームでは、W を押すと、キャラクターが前方(-Z軸)に移動します。

基底ベクトルは親に対する相対的な方向であり、原点ベクトルは親に対する相対的な位置であるため、単純に複数の基底ベクトルを加算して、オブジェクト自体を相対的に移動できます。

次のコードは、オブジェクトを 100単位右に移動します:

transform.origin += transform.x * 100
Transform2D t = Transform;
t.origin += t.x * 100;
Transform = t;

3Dで移動する場合は、"x"を"basis.x"に置き換える必要があります。

注釈

実際のプロジェクトでは、3Dで translate_object_local を使用するか、2Dで move_local_xmove_local_y を使用してこれを行うことができます。

transformへの変換の適用

transformについて知っておくべき最も重要なことの1つは、transformのいくつかを一緒に使用する方法です。親ノードのtransformは、そのすべての子に影響します。例を見てみましょう。

この図では、子ノードは成分名の後に "2" があり、親ノードと区別しています。非常に多くの数字で少し圧倒されるように見えるかもしれませんが、各数字は2回(矢印の横とマトリックスにも)表示され、数字のほぼ半分がゼロであることに注意してください。

../../_images/apply.png

ここで行われている唯一の変換は、親ノードに(2, 1)のスケールが与えられ、子に(0.5, 0.5)のスケールが与えられ、両方のノードに位置が与えられていることです。

すべての子の変換は、親の変換の影響を受けます。子のスケールは(0.5, 0.5)であるため、1:1の比率の正方形であることが予想されますが、これは親に対してのみです。子のXベクトルは、親の基底ベクトルによってスケーリングされるため、ワールド空間で(1, 0)になります。同様に、子ノードの origin ベクトルは(1, 1)に設定されますが、親ノードの基底ベクトルにより、実際にはワールド空間の(2, 1)に移動します。

子transformからワールド空間transformへの手動計算には、次のコードを使用します:

# Set up transforms just like in the image, except make positions be 100 times bigger.
var parent = Transform2D(2, 0, 0, 1, 100, 200)
var child = Transform2D(0.5, 0, 0, 0.5, 100, 100)

# Calculate the child's world space transform
# origin = (2, 0) * 100 + (0, 1) * 100 + (100, 200)
var origin = parent.x * child.origin.x + parent.y * child.origin.y + parent.origin
# basis_x = (2, 0) * 0.5 + (0, 1) * 0
var basis_x = parent.x * child.x.x + parent.y * child.x.y
# basis_y = (2, 0) * 0 + (0, 1) * 0.5
var basis_y = parent.x * child.y.x + parent.y * child.y.y

# Change the node's transform to what we just calculated.
transform = Transform2D(basis_x, basis_y, origin)
// Set up transforms just like in the image, except make positions be 100 times bigger.
Transform2D parent = new Transform2D(2, 0, 0, 1, 100, 200);
Transform2D child = new Transform2D(0.5f, 0, 0, 0.5f, 100, 100);

// Calculate the child's world space transform
// origin = (2, 0) * 100 + (0, 1) * 100 + (100, 200)
Vector2 origin = parent.x * child.origin.x + parent.y * child.origin.y + parent.origin;
// basisX = (2, 0) * 0.5 + (0, 1) * 0 = (0.5, 0)
Vector2 basisX = parent.x * child.x.x + parent.y * child.x.y;
// basisY = (2, 0) * 0 + (0, 1) * 0.5 = (0.5, 0)
Vector2 basisY = parent.x * child.y.x + parent.y * child.y.y;

// Change the node's transform to what we just calculated.
Transform = new Transform2D(basisX, basisY, origin);

実際のプロジェクトでは、* 演算子を使用して、あるtransformを別のtransformに適用することで、子のワールドtransformを得ることができます:

# Set up transforms just like in the image, except make positions be 100 times bigger.
var parent = Transform2D(Vector2(2, 0), Vector2(0, 1), Vector2(100, 200))
var child = Transform2D(Vector2(0.5, 0), Vector2(0, 0.5), Vector2(100, 100))

# Change the node's transform to what would be the child's world transform.
transform = parent * child
// Set up transforms just like in the image, except make positions be 100 times bigger.
Transform2D parent = new Transform2D(2, 0, 0, 1, 100, 200);
Transform2D child = new Transform2D(0.5f, 0, 0, 0.5f, 100, 100);

// Change the node's transform to what would be the child's world transform.
Transform = parent * child;

注釈

行列を乗算する場合、順序が重要です!それらを混同しないでください。

最後に、恒等変換の適用は常に何も行いません。

追加の説明が必要な場合は、行列の構成に関する3Blue1Brownの優れたビデオをご覧ください: https://www.youtube.com/watch?v=XkY2DOUCWMU

変換行列の反転

"affine_inverse"関数は、前の変換を「元に戻す」transformを返します。これは状況によっては役立つ場合がありますが、いくつかの例を提供する方が簡単です。

逆transformに通常のtransformを乗算すると、すべての変換が取り消されます:

var ti = transform.affine_inverse()
var t = ti * transform
# The transform is the identity transform.
Transform2D ti = Transform.AffineInverse();
Transform2D t = ti * Transform;
// The transform is the identity transform.

transformとその逆のtransformによって位置を変換すると、同じ位置になります( "xform_inv"と同じ):

var ti = transform.affine_inverse()
position = transform.xform(position)
position = ti.xform(position)
# The position is the same as before.
Transform2D ti = Transform.AffineInverse();
Position = Transform.Xform(Position);
Position = ti.Xform(Position);
// The position is the same as before.

3Dではどのように機能しますか?

One of the great things about transformation matrices is that they work very similarly between 2D and 3D transformations. All the code and formulas used above for 2D work the same in 3D, with 3 exceptions: the addition of a third axis, that each axis is of type Vector3, and also that Godot stores the Basis separately from the Transform, since the math can get complex and it makes sense to separate it.

3Dでの平行移動、回転、拡大縮小、およびせん断の仕組みに関するすべての概念は、2Dと比較してすべて同じです。スケーリングするには、各成分を取得して乗算します。回転するには、各基底ベクトルが指す場所を変更します。並行移動するには、原点を操作します。そしてせん断するために、基底ベクトルを非直交に変更します。

../../_images/3d-identity.png

必要に応じて、transformを操作して、それらがどのように機能するかを理解することをお勧めします。Godotを使用すると、インスペクタから直接3D変換行列を編集できます。Basis ベクトルと原点を2Dと3Dの両方で視覚化するのに役立つ色付きの線とキューブを含むこのプロジェクトをダウンロードできます: https://github.com/godotengine/godot-demo-projects/tree/master/misc/matrix_transform

注釈

Godot 3.2.xのインスペクタにあるSpatialの"Matrix"セクションでは、行列が横の列と縦の列で転置された行列として表示されます。これは、Godotの将来のリリースで混乱が少なくなるように変更される可能性があります。

注釈

Node2Dの変換行列をGodot 3.2.xのインスペクタで直接編集することはできません。これは、Godotの将来のリリースで変更される可能性があります。

追加の説明が必要な場合は、3Blue1Brownの3D線形変換に関する優れたビデオをご覧ください: https://www.youtube.com/watch?v=rHLEWRxRGiM

3Dでの回転の表現(高度な内容)

2Dと3D変換行列の最大の違いは、基底ベクトルなしで回転をそれ自体で表現する方法です。

With 2D, we have an easy way (atan2) to switch between a transformation matrix and an angle. In 3D, we can't simply represent rotation as one number. There is something called Euler angles, which can represent rotations as a set of 3 numbers, however, they are limited and not very useful, except for trivial cases.

3Dでは、通常、角度は使用せず、変換基底(Godotのほぼどこでも使用)を使用するか、クォータニオンを使用します。Godotは、Quat 構造体を使用してクォータニオンを表すことができます。あなたへの私の提案は、それらが非常に複雑で直感的でないため、それらが内部でどのように機能するかは完全に無視することです。

ただし、実際にその仕組みを知っておく必要がある場合は、次の優れたリソースを参照してください:

https://www.youtube.com/watch?v=mvmuCPvRoWQ

https://www.youtube.com/watch?v=d4EgbgTm0Bg

https://eater.net/quaternions