Home 7. 아핀공간
Post
Cancel

7. 아핀공간

아핀공간

행렬의 곱을 통한 이동

이동이라는것은 각각의 좌표에 특정값을 더한다고 볼 수 있습니다.

그렇다면 선형변환형태의 행렬꼴로 나타내면 다음과 같습니다.

Av=[abcd][xy]=[x+ey+f]=u\bold A\vec v =\begin{bmatrix} a & b\\ c & d \end{bmatrix} \begin{bmatrix} x\\ y \end{bmatrix} = \begin{bmatrix} x+e\\ y+f \end{bmatrix} = \bold u

해당 식을 만족하는 행렬 A\bold A 가 있을 수 있을까요? 아니요 불가능합니다.

그러면 해당 식을 만족할려면 어떡해야할까요?

간단합니다. 차원을 하나 추가합니다.

Av=[ab0cd0001][xy0]=[x+ey+f0]=u\bold A\vec v =\begin{bmatrix} a & b & 0 \\ c & d & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x\\ y\\ 0 \end{bmatrix} = \begin{bmatrix} x+e\\ y+f\\ 0 \end{bmatrix} = \bold u

이렇게 구조를 짠다면 해결할 수 있을까요?

기본적으로 identity 행렬에서 부터 생각해봅시다.

Av=[100010001][xy0]=[1x+0y+000x+1y+0000+00+10]=u\bold A\vec v =\begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x\\ y\\ 0 \end{bmatrix} = \begin{bmatrix} 1 \cdot x + 0 \cdot y + 0 \cdot 0\\ 0 \cdot x + 1 \cdot y + 0 \cdot 0\\ 0 \cdot 0 + 0 \cdot 0 + 1 \cdot 0 \end{bmatrix} = \bold u

즉 이 형태에서 x+ex+ey+fy+f 를 나타낼려면 a13=ea_{13}=ea23=fa_{23}=f 입니다.

다시 이걸 행렬로 바꾸게 된다면,

Av=[10e01f001][xy1]=[1x+0y+e10x+1y+f100+00+11]=[x+ey+f1]=u\bold A\vec v =\begin{bmatrix} 1 & 0 & e \\ 0 & 1 & f \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x\\ y\\ 1 \end{bmatrix} = \begin{bmatrix} 1 \cdot x + 0 \cdot y + e \cdot 1\\ 0 \cdot x + 1 \cdot y + f \cdot 1\\ 0 \cdot 0 + 0 \cdot 0 + 1 \cdot 1 \end{bmatrix} = \begin{bmatrix} x+e\\ y+f\\ 1 \end{bmatrix} = \bold u

즉 정리하자면 밀기변환은 하나의 축만은 움직일 수 없으므로, 가상의 축을 추가하여

해당 축을 제외한 밀기변환을 행한다고 볼 수 있습니다.

여기서 zz 가 1인 평면만을 사용하는 이유는, ( e,fe,f )만큼 밀기변환을 하였을 때,

( 1e,1f1\cdot e,1\cdot f )만큼만 이동하기 때문입니다. 다른 값 nn 을 가진다면 밀기변환이

( ne,nfn\cdot e,n\cdot f ) 만큼 이동을 하기 때문에 부정확해지기 때문이죠

아핀공간 및 점의 정의

z=1z=1 인 밀기변환이 정확히 평행이동되는 공간을 아핀 공간(Affine Space)이라고 부릅니다.

원점이 어디인지 알 수 없으며, 2차원 아핀공간의 경우, 3차원 벡터공간의 부분공간이라고 볼 수 있습니다.

여기서 그 아핀공간의 원소를 점(Point)이라고 하며, 점과 벡터를 같은군으로 해석합니다.

아핀공간에서의 벡터의 존재이유

아핀공간은 마지막차원, 편의상 z=1z=1 인 공간을 의미한다 하였습니다. 하지만 연산은 벡터와 같이 적용됩니다.

따라서 점과 점의 단순덧셈은 zz 값이 1이 아니게 되므로, 점간의 연산은 불가합니다.

그러므로 점의 이동을 행할 때에는 점이 아닌 이동벡터( z=0z=0 )을 통해 결과가 여전히 z=1z=1 인 점을 이동할 수 있습니다. 이동벡터( z=0z=0 ) 간의 연산 역시 가능하겠죠. 이후 이동벡터를 잠시 단순히 벡터라고 서술하겠습니다.

정리해본다면

  • 점 간의 덧셈은 불가능하며,
  • 점과 벡터와의 연산은 가능하며 벡터와 벡터간의 연산도 가능합니다.

따라서 아핀공간에서도 아핀공간의 원소인 점 뿐만 아니라 벡터가 필요하게 됩니다.

점과 벡터의 연산

아까 점과 점의 단순덧셈연산은 불가능 하다고 하였습니다.

점과 벡터의 연산은 점을 만듭니다.

그러면 점에서 점을 빼면 벡터가 나오지 않을까요?

P1=(x1,y1,1), P2=(x2,y2,1)P1P2=v(x1x2,y1y2,0)P_1 = (x_1,y_1,1),\space P_2 = (x_2,y_2,1)\\ P_1 - P_2 = v(x_1 - x_2,y_1-y_2,0)

두 점간의 위치 차이. 전 이것을 조금 더 정확하게 이해하기 위해 ‘변위(displacement,變位)’라고 표현하겠습니다.

두 점간의 변위는 한 점에 다른점을 빼면 됩니다. 그 결과로 변위벡터(displacement Vector), 즉 아까 말한 이동벡터가 나오게 됩니다.

그리고 이 변위벡터는 아까 서술하였듯, 마지막 차원의 성분이 0을 갖습니다.

아핀조합

벡터공간에서는 벡터를 통해 새로운 벡터를 만들어내는것, 즉 스팬(Span)하는 것을 선형조합이라고 하였습니다. 그렇다면 점과 점을 통해 새로운 점을 생성하는 것 역시 있지 않을까요?

여기서 문제점은 다른부분은 벡터와 동일하다 할 때, 마지막차원의 성분,

즉 2차원에서의 zz 성분이 문제입니다.

그래서 계산 결과값의 zz 성분이 1이 되도록 해야하죠.

이미 구현으로 단련된 사람들은 이 해답을 알고있다고 생각합니다.

어떻게 하냐면, 더했을때 1이 나오는 특정 비율의 곱으로 나타내면 됩니다.

P=(1a)P1+aP2;P = (1-a)P_1 + aP_2;

여기서 볼 수 있는 것은, 다른걸 다 제외하고 P1P_1 의 마지막차원, zz 의 성분은 언제나 1이기 때문에 zz 에 대해서만 본다면 z=(1a)+a=1z = (1-a) +a = 1 이라는 겁니다.

그리고 이것을 벡터의 덧셈과 스칼라 곱셈만으로 이룰 수 있고, 이미 체계에서 닫힘임을 약속받은 연산들이기 때문에 선형성을 유지한다고 볼 수 있습니다.

그리고 이 방법을 대게 linear interpolation 및 extrapolation 라고 칭하죠

P=Lerp(P1,P2,a)P=(1a)P1+aP2;P = Lerp(P_1,P_2,a)\\ P = (1-a)*P_1 + a*P_2;

보통 0a10\leq a \leq 1 의 경우를 interpolate(보간), 그 나머지 경우를 extrapolate(보외) 라고 합니다.

점과 점 사이에 속하냐, 아니냐의 대한 부분이죠.

직선, 반직선, 선분

그리고 이렇게 생성될 수 있는 점의 집합을 모은다면 어떻게 될까요?

아마 예쁜 선이 나올 것 입니다. 정보의 집합이 예쁜 선을 만든다니. 생각만 해도 행복하지 않나요?

아무튼 위에서 사용한 수식의 선형성을 먼저 생각해봅시다.

왜냐하면 점 역시 벡터와 같은 군으로 생각하기 때문에, 조합을 위해선 선형적이여야합니다.

이미 선형적이라고 생각하지만 더 제대로 생각해 봅시다.

P=(1a)P1+aP2;=P1aP1+aP2 PP1=a(P2P1)vpp1=a(vp2p1)P = (1-a)*P_1 + a*P_2;\\ =P_1-a * P_1 + a*P_2\\ \space \\ P-P_1 = a(P_2-P_1)\\ v_{p-p1} = a(v_{p2-p1})

두 점의 차는 변위벡터라고 아까 설명하였기 때문에, 식을 전개하면 벡터 = 스칼라 * 벡터 의 꼴이 나옵니다. 이렇게 선형성을 만족하는것을 확인하였으니 다음으로 넘어갑시다.

이렇게 아핀조합으로 생성된 점의 집합을 선 이라고 생각할 수 있습니다.

그리고 아핀조합을 좀 더 편하게 부르기 위해 다음과같이 정의합시다.

P=Lerp(P1,P2,a)P=(1a)P1+aP2;P = Lerp(P_1,P_2,a)\\ P = (1-a)*P_1 + a*P_2;

선이 집합이기 때문에 선의 원소는 점이죠. 그리고 이때 a에 대한 범위를 통해 대응되는 집합을 구할 수 있습니다. 이 범위에 따라 선을 부르는 명칭이 달라집니다.

보통 실수의 범위는 -\infin 부터 \infin 까지 입니다. 이제 나눠봅시다.

  • <a<-\infin < a < \infin 의 경우는 직선(Line)입니다. 수직선이라고 자주 말하기도 하는 그 직선입니다.
  • 0a<0 \leq a < \infin 의 경우는 반직선(Ray) 입니다. 어느 지점부터 끝까지 뻗어나가는 선이죠,
  • 0a10 \leq a \leq 1 인 경우는 선분(segment) 입니다. 정의를 보면 더 간단하죠. 점 A,BA,B 를 양끝으로 하는 선분을 선분 ABAB 라 정의하기 때문입니다. 그리고 이 점 A,BA,BP1,P2P_1,P_2 라고 생각하였을때, a=0,a=1a=0,a=1 의 경우를 생각하면 생성된 점의 위치는 P1,P2P_1,P_2 이므로 양 끝 점이기 때문입니다.

반직선의 활용예시

선분은 양 끝점이 존재하므로 길이를 잴 수 있습니다.

직선은 양 끝점이 존재하지 않고 무한하므로 개념적으로 사용할 수 있습니다.

그러면 반직선(Ray)은 뭘까요?

아까 설명하였을 때, 반직선은 한쪽은 닫혀있고, 한쪽은 무한대라고 하였습니다.

마치 단어 그대로 레이저포인터와 같은 느낌이죠.

레이저포인터의 개념을 생각해 봅시다.

어떤 지점에서 처음 빛이 닿는 무언가를 가르키는데 사용합니다.

이걸 물리엔진 관점에서 생각한다면 충돌검출이라고 볼 수 있습니다.

유니티에서 사용하는 Raycast가 그 예라고 생각할 수 있습니다.

또한 그래픽에서 높은 품질의 빛 표현을 위한 레이트레이싱 및 포톤매핑 같은 경우에도 반직선(Ray)을 사용합니다. 어디서 충돌해서 어디서 반사해서 어떤색상이 맺히는지, 얼마나 충돌해서 차폐가 어느정도 나타나는지, 굴절을 계산해서 커스틱을 만들때 등이 있겠네요.

게임엔진에서 3차원을 다룰 때 아핀공간임에도 Vector3을 사용하는 이유

개인적으로 아핀공간은 원점이 없다고 생각합니다.

밀기 연산을 통해 생성된 부분공간에서의 원점은 가짜원점이라고 생각합니다.

부분공간에서 더 큰 차원의 원점을 생각하려하면 당연한 것이겠죠.

원자적으로 생각하더라도 마지막 차원의 성분이 1이 되어야하는데, 어떻게 원점일까요?

그래서 아핀공간에서 원점이 존재한다면, 실제 존재하는 원점이 아닌 개념적으로 사람들이 약속한 기준점이라고 생각합니다.

즉, 원점이라는 기준점이 존재하고, 그 기준점의 성분이 가지는 각 값이 마지막 차원을 제외하고 모두 0이라고 약속한다면 이제 계산하기 쉬워집니다.

점에서 원점이라고 정의한 기준점(이하 원점)을 뺀다면 (원점의 역원을 더한다면) 변위벡터가 나타나게 되고, 원점에 변위벡터를 더하게 된다면 해당 변위만큼 이동한 점이 생성되기 때문입니다.

정리하자면

  • 원점 + 변위벡터 = 해당변위만큼 이동한 점
  • 특정 점 - 원점 = 원점에서 특정 점까지의 변위벡터

을 통해 자유롭게 두 개념의 변환이 가능하기 때문입니다.

데이터 흐름 관점에서의 게임로직과 렌더링 로직에서 다루는 데이터 구분

게임로직에서의 기본적인 이동은 행렬을 사용하지 않는 트랜스폼 조작입니다.

트랜스폼 : 이동(Vector3), 회전(Quaternion, 표기는 Euler각인 Vector3), 스케일(Vector3)

하지만 그렇다고 행렬을 사용하지 않는다고 볼 수는 없을 수 있습니다.

예를 들어 TransformPoint/InverseTransformPoint 등을 통한

로컬 \mapsto 월드 혹은 월드 \mapsto 로컬 은 행렬을 사용합니다.

이를 확인하기 위해 유니티를 까봅시다.

https://github.com/Unity-Technologies/UnityCsReference

그리고

아쉽게도 일부항목은 extern 처리 되어있어 네이티브 파일을 확인해야 알 것 같습니다.

그래서 대신 비슷한 작업을 할 것 이라 예상하는 함수를 가져왔습니다.

아마 조인트의 경우 transform에서 행렬을 가져오는게 아니라 앵커기준으로 맞춰야 하기 때문에 추가적으로 구현한 것 같은데, 덕분에 이 코드를 가져올 수 있었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//in Transform.bindings.cs
public extern Vector3 TransformPoint(Vector3 position);

// in Joint2DEditor.cs
static Matrix4x4 GetAnchorSpaceMatrix(Transform transform)
{
    // Anchor space transformation matrix
    return Matrix4x4.TRS(
				transform.position, 
				Quaternion.Euler(0, 0, transform.rotation.eulerAngles.z), 
				transform.lossyScale);
}

protected static Vector3 TransformPoint(Transform transform, Vector3 position)
{
    // Local to World
    return GetAnchorSpaceMatrix(transform).MultiplyPoint(position);
}

//in Matrix4x4.cs
public Vector3 MultiplyPoint(Vector3 point)
{
    Vector3 res;
    float w;
    res.x = this.m00 * point.x + this.m01 * point.y + this.m02 * point.z + this.m03;
    res.y = this.m10 * point.x + this.m11 * point.y + this.m12 * point.z + this.m13;
    res.z = this.m20 * point.x + this.m21 * point.y + this.m22 * point.z + this.m23;
    w = this.m30 * point.x + this.m31 * point.y + this.m32 * point.z + this.m33;

    w = 1F / w;
    res.x *= w;
    res.y *= w;
    res.z *= w;
    return res;
}

그리고 이제 회전,이동을 생각합시다.

회전을 생각할 때, 저희는 회전행렬을 통한 변환을 생각하였습니다.

하지만 실제 유니티에서는 어떻게 동작할까요?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
//in Quaternion.cs
// Rotates the point /point/ with /rotation/.
public static Vector3 operator*(Quaternion rotation, Vector3 point)
{
    float x = rotation.x * 2F;
    float y = rotation.y * 2F;
    float z = rotation.z * 2F;
    float xx = rotation.x * x;
    float yy = rotation.y * y;
    float zz = rotation.z * z;
    float xy = rotation.x * y;
    float xz = rotation.x * z;
    float yz = rotation.y * z;
    float wx = rotation.w * x;
    float wy = rotation.w * y;
    float wz = rotation.w * z;

    Vector3 res;
    res.x = (1F - (yy + zz)) * point.x + (xy - wz) * point.y + (xz + wy) * point.z;
    res.y = (xy + wz) * point.x + (1F - (xx + zz)) * point.y + (yz - wx) * point.z;
    res.z = (xz - wy) * point.x + (yz + wx) * point.y + (1F - (xx + yy)) * point.z;
    return res;
}

// Combines rotations /lhs/ and /rhs/.
public static Quaternion operator*(Quaternion lhs, Quaternion rhs)
{
    return new Quaternion(
        lhs.w * rhs.x + lhs.x * rhs.w + lhs.y * rhs.z - lhs.z * rhs.y,
        lhs.w * rhs.y + lhs.y * rhs.w + lhs.z * rhs.x - lhs.x * rhs.z,
        lhs.w * rhs.z + lhs.z * rhs.w + lhs.x * rhs.y - lhs.y * rhs.x,
        lhs.w * rhs.w - lhs.x * rhs.x - lhs.y * rhs.y - lhs.z * rhs.z);
}

// Transforms a [[Vector4]] by a matrix.
public static Vector4 operator*(Matrix4x4 lhs, Vector4 vector)
{
    Vector4 res;
    res.x = lhs.m00 * vector.x + lhs.m01 * vector.y + lhs.m02 * vector.z + lhs.m03 * vector.w;
    res.y = lhs.m10 * vector.x + lhs.m11 * vector.y + lhs.m12 * vector.z + lhs.m13 * vector.w;
    res.z = lhs.m20 * vector.x + lhs.m21 * vector.y + lhs.m22 * vector.z + lhs.m23 * vector.w;
    res.w = lhs.m30 * vector.x + lhs.m31 * vector.y + lhs.m32 * vector.z + lhs.m33 * vector.w;
    return res;
}

//in Transform.bindings.cs

// The rotation of the transform relative to the parent transform's rotation.
public extern Quaternion localRotation { get; set; }

// Applies a rotation of /eulerAngles.z/ degrees around the z axis, /eulerAngles.x/ degrees around the x axis, and /eulerAngles.y/ degrees around the y axis (in that order).
public void Rotate(Vector3 eulers, [UnityEngine.Internal.DefaultValue("Space.Self")] Space relativeTo)
{
    Quaternion eulerRot = Quaternion.Euler(eulers.x, eulers.y, eulers.z);
    if (relativeTo == Space.Self)
        localRotation = localRotation * eulerRot;
    else
    {
        rotation = rotation * (Quaternion.Inverse(rotation) * eulerRot * rotation);
    }
}

// Moves the transform in the direction and distance of /translation/.
public void Translate(Vector3 translation, Transform relativeTo)
{
    if (relativeTo)
        position += relativeTo.TransformDirection(translation);
    else
        position += translation;
}

쿼터니언 간의 곱을 하고 있습니다.

또한 Translate의 경우도 그저 값을 더해주고 있습니다.

어떻게보면 당연한 이야기입니다.

그리고 이제 렌더링입니다. 렌더링은 제가 그렇게 까보거나 할 자신도 없고 잘 몰라서 모르겠습니다.

렌더링은 메쉬 정보를 게임로직에 있는 트랜스폼의 행렬을 통한 변환한다고 생각합니다.

여기서 메쉬정보는 버텍스 위치, 노말의 방향 등이라고 생각합니다.

1
2
3
// in Transform.bindings.cs
// Matrix that transforms a point from local space into world space (RO).
public extern Matrix4x4 localToWorldMatrix { get; }

즉 메쉬정보는 현재 있는 트랜스폼의 로컬공간에 위치하기 때문에,

해당행렬을 가져와서 변환해주고, 뷰(View)행렬과 투영(Projection)행렬을 사용하여 또 변환하면

최종적으로 화면에 표시하기 위한 좌표계를 얻을 수 있다고 생각합니다.

This post is licensed under CC BY 4.0 by the author.
Contents