행렬과 변환

소개

본 튜토리얼을 읽기 전에 :ref:`doc_vector_math`에 대한 이전 튜토리얼을 읽어 보는 것이 좋습니다.

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

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

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

어떻게 이런 일이 일어난거죠? 우주선을 우주에서 잃어버렸는데 지금은 돌아왔어요!

이상하게 보일지 모르지만, 그것은 많은 논리를 가지고 있습니다. :ref:`doc_vector_math`에서 보았던 바와 같이 X축까지의 거리와 Y축까지의 거리가 계산되었다는 것을 기억하세요. 방향 또는 평면에서 거리를 계산하는 것은 외적을 사용하는 방법 중 하나입니다. 이것은 우주선 안의 모든 지점에 대한 설계 좌표를 다시 얻기에 충분했습니다.

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

Basis(기초)

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

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

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

../../_images/tutomat10.png

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

Godot 에서의 변환

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

Transform2D(변형2D)

ref=class_Transform2D는 3x2 행렬입니다. 이것은 3Vector2 원소들이 있고 2D에 사용됩니다. "X" 축은 원소 0, "Y" 축은 원소 1이고 "원점"은 요소 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에도 동일한 논리가 적용됩니다.

항등성

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

  • 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

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

If instead, translation is desired in local coordinates of the matrix (towards where the basis is oriented), there is the Transform2D.translated() method:

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

더군다나, 다음 섹션에서 읽을 수 있는 것처럼 이것에 대한 도우미 기능도 있다.

로컬에서 글로벌 좌표로 또는 그 반대로

로컬 좌표와 전역 좌표 사이를 변환하는 도우미 방법이 있습니다.

There are Node2D.to_local() and Node2D.to_global() for 2D as well as Spatial.to_local() and Spatial.to_global() for 3D.

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에는 두 개의 멤버가 있습니다:

모든 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)