행렬과 변환

소개

본 튜토리얼을 읽기 전에 벡터 에 대한 이전 튜토리얼을 읽어 보는 것이 좋습니다.

본 튜토리얼에서는 변환 에 대해 다루며 행렬에 대해 설명합니다(심층적이지는 않음).

변환은 변환, 회전 및 척도로 사용되는 대부분의 시간이기 때문에 여기서는 변환이 우선으로 간주됩니다.

OCS(주요 좌표계)

우주 어딘가에 우주선이 있다고 상상해 보세요. Godot에서 이것은 쉽습니다. 배를 어딘가로 옮겨서 돌리십시오:

../../_images/tutomat1.png

2D에서는 단순한 위치 및 회전각으로 보입니다. 하지만 기억하세요, 우리는 여기서 더 나아갔고, 각도는 사용하지 않습니다.

우리는 언젠가 누군가가 이 우주선을 설계 했다는 것을 깨달아야 합니다. Paint.net, Gimp, Photoshop 등과 같은 도면에서 2D로 제작하거나 Blender, Max, Maya 등과 같은 3D DCC 도구를 통해 3D로 제작해야 합니다.

이것은 설계 시 회전하지 않았습니다. 그것은 자체적인 좌표계 시스템 으로 설계되었습니다.

../../_images/tutomat2.png

이것은 우주선의 끝이 좌표를 가지고 있고 판부분은 다른 것을 가지고 있다는 것을 의미합니다. 픽셀(2D) 또는 정점(3D)으로 지정합니다.

그러면, 우주 어딘가에 우주선이 있었다는 것을 회상해봅시다:

../../_images/tutomat3.png

그건 어떻게 그곳에 도착했나요? 무엇을 움직여서 디자인된 위치에서 현재 위치로 회전시킨건가요? 답은... 변환 입니다. 우주선은 원래 위치에서 새 위치로 변환 되었습니다. 이렇게 하면 우주선이 있는 곳에 표시될 수 있습니다.

그러나 변환은 이 과정을 설명하기에는 너무 일반적인 용어입니다. 이 퍼즐을 해결하기 위해 우주선의 원래 설계 위치를 현재 위치에 겹치게 됩니다:

../../_images/tutomat4.png

"설계된 우주"도 바뀌었습니다. 이러한 변화를 가장 잘 표현할 수 있는 방법은 무엇일까요? 이를 위해 (2D 단위) 3개의 벡터를 사용하겠습니다. X 양수를 가리키는 단위 벡터, Y 양수를 가리키는 단위 벡터 및 변환을 사용합니다.

../../_images/tutomat5.png

이 세 벡터를 "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축까지의 거리가 계산되었다는 것을 기억하세요. 방향 또는 평면에서 거리를 계산하는 것은 내적을 사용하는 방법 중 하나입니다. 이것은 우주선 안의 모든 지점에 대한 설계 좌표를 다시 얻기에 충분했습니다.

지금까지 X, Y, 원점과 함께 작업한 것은 지향적 좌표계 입니다. X, Y는 Basis 이고 원점 은 오프셋입니다.

Basis

우리는 원점이 무엇인지 압니다. 그것은 설계 좌표계의 0,0(원래)이 새로운 위치로 변환된 후에 종료되는 곳입니다. 이것이 바로 *원점*이라고 불리는 이유지만 실제로는 새로운 위치로의 상쇄에 불과합니다.

기저는 더 흥미롭습니다. 기저는 새로운 변환된 위치에서 OCS의 X와 Y의 방향입니다. 그것은 2D나 3D로 무엇이 바뀌었는지 말해줍니다. 원점(오프셋)과 베이스(방향)는 "너의 설계된 원래 X와 Y 축은 바로 여기에 있어 이러한 방향 을 가리킵니다."

그러면, 기저의 표현을 바꿔보겠습니다. 2개의 벡터 대신에 matrix 를 사용합니다.

../../_images/tutomat10.png

벡터는 위쪽에 수평으로 있습니다. 다음 문제는.. 이 매트릭스란 무엇일까요?? 음, 우리는 당신이 매트릭스에 대해 들어본 적이 없다고 가정하겠습니다.

Godot 에서의 변환

본 튜토리얼에서는 행렬 수학(및 그 연산)을 실제로 사용하기만 하고 자세히 설명하지는 않습니다. 이 튜토리얼을 완수한 후 이해하기 훨씬 더 간단할 수 있습니다. 변환 사용법에 대해 설명하겠습니다.

Transform2D

class_Transform2D is a 3x2 matrix. It has 3 Vector2 elements and it's used for 2D. The "X" axis is the element 0, "Y" axis is the element 1 and "Origin" is element 2. It's not divided in basis/origin for convenience, due to its simplicity.

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에도 동일한 논리가 적용됩니다.

항등성

중요한 변환은 항등행렬이다. 이것의 의미는:

  • X점 오른쪽: 벡터2(1.0)
  • 'Y' 점 위(또는 픽셀 단위 아래): Vector2(0,1)
  • '원점'은 원점 벡터2(0,0)입니다
../../_images/tutomat11.png

항등 행렬은 상위 좌표계에 대한 변환을 조정하는 행렬일 뿐입니다. *OCS*는 변환, 회전 또는 스케일링되지 않았습니다.

# 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))

연산

Rotation

Rotated(회전) 기능을 사용하여 Transform2D를 회전합니다:

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를 변환하는 두 가지 방법이 있습니다. 첫 번째 방법은 원점을 이동하는 것입니다:

# 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

이 기능은 항상 전반적인 좌표에 적용됩니다.

대신 행렬의 지역 좌표에 변환을 원하는 경우(기저 가 있는 위치), :ref:`Transform2D.translated() <class_Transform2D_method_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()Spatial.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

행렬은 거북이와 비슷하게 사용됩니다. 거북이는 안에 행렬을 가지고 있을 가능성이 높습니다. (그리고 여러분은 산타는 진짜가 아니라는 것을 알게 된 후 수년 동안 이 사실을 배우게 될 것입니다).

변형

변환은 좌표계 사이를 전환하는 행위입니다. 위치(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);

아핀 역

아핀 역은 행렬에 축척이 있거나 축 벡터가 직교하지 않더라도 다른 행렬의 역 연산을 수행하는 행렬입니다. 아핀 역은 the 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);

행렬 곱

행렬은 곱해질 수 있습니다. 두 행렬의 곱은 그들의 변환을 연결합니다.

그러나 관례에 따라 곱은 역순으로 진행됩니다.

예시:

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의 경우 Vector3 회전 행렬용 벡터 3개와 원점용 벡터 1개를 추가로 취급합니다.

Basis

Godot has a special type for a 3x3 matrix, named Basis. It can be used to represent a 3D rotation and scale. Sub vectors can be accessed as:

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;

항등 기저는 다음과 같은 값을 가집니다:

../../_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 회전은 2D보다 더 복잡합니다(변환 및 배율은 동일). 3D로 회전하려면 을 선택해야 합니다. 그런 다음 이 축을 중심으로 회전합니다.

회전 축은 법선 벡터 이어야 합니다. 임의의 방향을 가리킬 수 있지만 길이는 하나(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);

변형

믹스에 최종 구성요소를 추가하기 위해 고도는 :ref:"Transform <class_Transform> 유형을 제공합니다. Transform에는 두 개의 멤버가 있습니다:

  • 기저 (of type Basis)
  • 원점 (of type 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)