Home 12. 뷰 좌표계와 트랜스폼
Post
Cancel

12. 뷰 좌표계와 트랜스폼

View Space, Transform

뷰 좌표계

월드공간에 배치된 물체를 카메라 중심으로 변환하는 방법


카메라 중심이라..

어짜피 2차원 평면이라면 원점 및 축의 방향을 조정하는것과 비슷하겠죠?

중학생때 2차함수의 평행이동을 생각하면 간단합니다.

함수가 가만히 있다고 가정하고 전체적인 시야가 이동한 것과 같은 모습이죠?

예. 물체가 가만히 있어도 카메라가 오른쪽으로 이동했다면, 물체전체를 왼쪽으로 이동한 것 과 같습니다.

비슷한 개념으로 상대성이론을 생각해보시면 편합니다.

관찰자가 내부에 있다면 관찰자가 이동하는지, 세상이 이동하는지에 대해서는 알 수 없습니다.

무중력 공간에서 점점 가속하는 물체 A 내부에 있는 인간은 중력이 작용하는건지 가속력이 작용하는건지 알 수 없게 되어 버리는 것입니다.

그러므로 뷰좌표계는 모든 연산이 반대로 처리(역행렬을 통한 변환)되었다고 보면 될 것 같습니다.

위는 아래이며, 아래는 위고, 시계방향 15도는 반시계방향 15도입니다.

그러면 이동부터 생각해 보겠습니다.

위치이동에 대한 역은 이동 역행렬로 생각할 수 있고,

항상 계산하던대로 계산한다면 이동행렬의 곱으로 표현하여 다음과 같습니다.

Tcamera1TobjectT_{camera}^{-1} \cdot T_{object}

그리고 회전 및 스케일 역시 동일하게 처리할 수 있겠죠.

만약 회전각에 -를 넣게 된다면,

이는 반대 방향으로 회전하는 것이며,

역행렬과 같고 전치행렬과 같습니다.

다시 다른 과거로 돌아가, 회전행렬의 두 각의 합의 회전은 두 각 a,ba,b 가 있다 할 때,

aa 만큼 회전한 행렬과 bb 만큼 회전한 행렬을 곱한 것과 같습니다.

삼각형의 합의 공식으로 정리하여 나타낸다면, ( Ra+b=RaRbR_{a+b}=R_a \cdot R_b ) 이므로,

따라서 다음과 같이 볼 수 있습니다.

Rcamera1RobjectR_{camera}^{-1} \cdot R_{object}

카메라 트랜스폼으로부터 뷰행렬을 구하는 과정


아까의 개념에서 뷰행렬을 개념적으로 곱해봅시다.

버텍스의 로컬을 vlocalv_{local} , 뷰 행렬까지 좌표가 변환된 버텍스를 vviewv_{view} 라고 정의할 때,

다음과 같이 볼 수 있습니다.

vview=VMvlocalvview=VTRSvlocalvview=Rcamera1Tcamera1TRSvlocalvview=(TcameraRcamera)1TRSvlocalv_{view} = V \cdot M \cdot v_{local}\\ v_{view} = V \cdot TRS \cdot v_{local}\\ \quad\\ v_{view} = R^{-1}_{camera}T^{-1}_{camera} \cdot TRS \cdot v_{local}\\ v_{view} = (T_{camera}R_{camera})^{-1} \cdot TRS \cdot v_{local}\\

그러면 조금 더 개념적이 아닌, 수식적으로 접근해 봅시다.

2차원 공간에서 이동의 역행렬은 다음과 같습니다.

T1=[10tx01ty001]T^{-1}=\begin{bmatrix} 1 & 0 & -t_x \\ 0 & 1 & -t_y \\ 0 & 0 & 1 \end{bmatrix}\\

회전의 역행렬은 회전의 전치행렬이며, 다음과 같죠

R1=RT=[cosθsinθ0sinθcosθ0001]R^{-1}=R^{T}= \begin{bmatrix} cos\theta & sin\theta & 0 \\ -sin\theta & cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix}\\

최종적으로 행렬곱 연산을 해본다면 다음과 같습니다.

V=R1T1=[cosθsinθ(txcosθ+tysinθ)sinθcosθ(txsinθ+tycosθ)001]V=R^{-1}T^{-1}= \begin{bmatrix} cos\theta & sin\theta & -(t_xcos\theta+t_ysin\theta) \\ -sin\theta & cos\theta & -(-t_xsin\theta +t_ycos\theta) \\ 0 & 0 & 1 \end{bmatrix}\\

그리고 저는 여기서 조금 다른 생각을 하였습니다.

카메라 행렬은 단순히 다른 기저를 갖는 것은 아닐까?


만약 카메라는 반대 방향의 기저를 갖고 있다고 생각해도 되는걸까?

저희가 사용하는 기저는 e1=(1,0),e2=(0,1)e_1 = (1,0) ,\quad e_2 = (0,1) 입니다.

그리고 보통 이런식으로 사용하죠.

(x,y)=xe1+ye2v=x(cosθ,sinθ)+y(sinθ,cosθ)v=(xcosθysinθ,xsinθ+ycosθ)\\ \\ (x,y)=x\cdot e_1+y\cdot e_2 \\ v'=x\cdot (cos\theta, sin\theta)+y\cdot (-sin\theta, cos\theta) \\ v'=(x\cdot cos\theta-y\cdot sin\theta, x\cdot sin\theta+y\cdot cos\theta)

표현하기 쉽게 두 기저벡터를 i^,j^\hat{i}, \hat{j} 라고 얘기하겠습니다.

R=[cosθsinθsinθcosθ]B=[i^xj^xi^yj^y]\bold R = \begin{bmatrix} cos\theta &-sin\theta \\ sin\theta &cos\theta \end{bmatrix} \\ \bold B = \begin{bmatrix} \hat{i}_x&\hat{j}_x \\ \hat{i}_y &\hat{j}_y \end{bmatrix}

보고있으면 이것또한 하나의 선형변환을 할 수 있는 행렬이 아닐까요?

5주차에 설명했던 선형변환을 다시 생각해봅시다.

Av=[abcd][xy]=[ax+bycx+dy]=u\bold A\vec v =\begin{bmatrix} a & b\\ c & d \end{bmatrix} \begin{bmatrix} x\\ y \end{bmatrix} = \begin{bmatrix} ax+by\\ cx+dy \end{bmatrix} = \bold u

그리고 카메라용 기저벡터. 원 기저벡터의 역을 생각해 봅니다. (-1,0)과 (0,-1)

이건 이렇게 변환된다고 볼 수 있습니다.

Bv=[1001][xy]=[x00y]=u\bold B\vec v =\begin{bmatrix} -1 & 0 \\ 0 & -1 \\ \end{bmatrix} \begin{bmatrix} x\\ y \end{bmatrix} = \begin{bmatrix} -x& 0\\ 0 &-y \end{bmatrix} = \bold u

개념적으로 스케일을 모두 뒤집는다면 공간이 뒤집힐테니, 얼추 맞다고 생각할 수 있습니다.

회전행렬과 곱해봅시다

BR=[1001][cosθsinθsinθcosθ]=[cosθsinθsinθcosθ]=[cosθsinθsinθcosθ]\bold {BR} =\begin{bmatrix} -1 & 0 \\ 0 & -1 \\ \end{bmatrix} \begin{bmatrix} cos\theta &-sin\theta \\ sin\theta &cos\theta \end{bmatrix} = \begin{bmatrix} -cos\theta &sin\theta \\ -sin\theta &-cos\theta \end{bmatrix} = \begin{bmatrix} cos\theta &sin\theta \\ -sin\theta &cos\theta \end{bmatrix}

이동행렬과 곱해볼까요?

BR=[100010001][10tx01ty001]=[10tx01ty001]\bold {BR} =\begin{bmatrix} -1 & 0 & 0 \\ 0 & -1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} -1 & 0 & -t_x \\ 0 & -1 & -t_y \\ 0 & 0 & 1 \end{bmatrix}

다 곱해볼까요?

BRT=[100010001][cosθsinθ0sinθcosθ0001][10tx01ty001]=[100010001][cosθsinθ(txcosθtysinθ)sinθcosθ(txsinθ+tycosθ)001]=[cosθsinθ(txcosθtysinθ)sinθcosθ(txsinθ+tycosθ)001]=[cosθsinθ(txcosθtysinθ)sinθcosθ(txsinθ+tycosθ)001]\bold {BRT} =\begin{bmatrix} -1 & 0 & 0 \\ 0 & -1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} cos\theta & -sin\theta & 0 \\ sin\theta & cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix}\\ = \begin{bmatrix} -1 & 0 & 0 \\ 0 & -1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} cos\theta & -sin\theta & (t_xcos\theta-t_ysin\theta) \\ sin\theta & cos\theta & (t_xsin\theta +t_ycos\theta) \\ 0 & 0 & 1 \end{bmatrix}\\ = \begin{bmatrix} -cos\theta & sin\theta & -(t_xcos\theta-t_ysin\theta) \\ -sin\theta & -cos\theta & -(t_xsin\theta +t_ycos\theta) \\ 0 & 0 & 1 \end{bmatrix}\\ = \begin{bmatrix} cos\theta & sin\theta & -(t_xcos\theta-t_ysin\theta) \\ -sin\theta & cos\theta & -(t_xsin\theta +t_ycos\theta) \\ 0 & 0 & 1 \end{bmatrix}\\

음? 생각했던것과 조금 다릅니다. 이동한 좌표가 회전을 반대로 먹었습니다.

기저 변환을 먼저 적용해야하는걸까요?

어느쪽으로 이동하거나 회전하는것은 그 크기를 나타내는 것이니 기저행렬을 먼저 곱해야겠군요!

RTB=[cosθsinθ0sinθcosθ0001][10tx01ty001][100010001]=[cosθsinθ(txcosθtysinθ)sinθcosθ(txsinθ+tycosθ)001][100010001]=[cosθsinθ(txcosθtysinθ)sinθcosθ(txsinθ+tycosθ)001]=[cosθsinθ(txcosθ+tysinθ)sinθcosθ(txsinθ+tycosθ)001]\bold {RTB} = \begin{bmatrix} cos\theta & -sin\theta & 0 \\ sin\theta & cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} -1 & 0 & 0 \\ 0 & -1 & 0 \\ 0 & 0 & 1 \end{bmatrix}\\ = \begin{bmatrix} cos\theta & -sin\theta & (t_xcos\theta-t_ysin\theta) \\ sin\theta & cos\theta & (t_xsin\theta +t_ycos\theta) \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} -1 & 0 & 0 \\ 0 & -1 & 0 \\ 0 & 0 & 1 \end{bmatrix}\\ = \begin{bmatrix} -cos\theta & sin\theta & (t_xcos\theta-t_ysin\theta) \\ -sin\theta & -cos\theta & (t_xsin\theta +t_ycos\theta) \\ 0 & 0 & 1 \end{bmatrix}\\ = \begin{bmatrix} cos\theta & sin\theta & -(t_xcos\theta+t_ysin\theta) \\ -sin\theta & cos\theta & -(-t_xsin\theta +t_ycos\theta) \\ 0 & 0 & 1 \end{bmatrix}\\

아무튼 값 자체는 연산에 실수가 없었다면 동일하게 나온 것 같습니다.

과연 카메라 행렬은 단지 다른 기저를 가진 행렬이라고 생각할 수 있을까요?

누군가 아시는분이 있다면 도움을 주셨으면 좋겠습니다.

카메라중심으로 변환할때 이동을 먼저 적용하고 회전을 적용하는 이유에 대해 정리


수학적으로 생각한다면,

vview=Rcamera1Tcamera1TRSvlocalvview=(TcameraRcamera)1TRSvlocalv_{view} = R^{-1}_{camera}T^{-1}_{camera} \cdot TRS \cdot v_{local}\\ v_{view} = (T_{camera}R_{camera})^{-1} \cdot TRS \cdot v_{local}\\

이므로, 일반적인 행렬의 적용순서인 TRSvTRSv 와 같이

회전을 먼저 적용한 후 이동을 적용한 것의 역행렬이라고 생각해도 괜찮습니다.

이를 트랜스폼을 고려한다면, 영향을 미치지 않는 S를 제외한 트랜스폼의 역행렬이라고 볼 수 있습니다.

감각적으로 생각한다면

회전을 먼저 적용하고 이동을 할 경우,

물체를 이동을 먼저하고 회전을 적용하는것 처럼 어색한 위치에 위치하게 됩니다.

트랜스폼 계층구조


엔진 및 기타 툴을 사용하다 보면 계층(hierarchy)를 꽤나 자주보게 됩니다.

동작구조는 쉽게 설명하자면 피벗(앵커)과 스케일링이라고 볼 수 있습니다.

각 객체는 하나 혹은 0개의 부모를 갖으며, 부모의 변화에 대해 영향을 받게 됩니다.

유니티 2018부터 나온 constraint를 살펴보면 해당 상태을 가장 유사하게 따라한 것이

Parent Constrain인데, 부모제약이라고 읽듯 부모의 변화에 대해 영향을 받습니다.

로컬과 월드. 두개를 계속 사용할때마다 변환하는 것은 비효율적이기 때문에

로걸 트랜스폼과 월드 트랜스폼 두개를 하나의 오브젝트가 소유하고, 같이 갱신하며 동기화합니다.

이때의 시나리오를 한 번 살펴봅시다.

월드 트랜스폼이 변경되는 경우

  • 부모의 월드 트랜스폼이 변경되는 경우
    • 자식의 로컬 트랜스폼은 변할 필요가 없습니다. 하지만 자식의 월드 트랜스폼은 변화와 동일하게 바뀝니다.
  • 자기자신의 로컬트랜스폼이 변화할 경우
    • 로컬좌표가 수정되었으므로, 월드좌표역시 수정되어야겠죠.

이를 일반화 할 경우, 어떤 객체의 월드트랜스폼이 변경될 경우, 모든 자식 및 말단 자식까지 월드트랜스폼 변화가 발생합니다

로컬 트랜스폼이 변경되는 경우

  • 단순하게 자식의 트랜스폼이 변할 경우
    • 이것또한 월드트랜스폼역시 같이 변경되므로, 변경된 자식트랜스폼 하위의 모든 자식 및 말단자식까지 월드트랜스폼 변화가 발생합니다.
  • 부모가 변경되는 경우
    • 이 경우, 월드트랜스폼은 바뀌지 않지만 로컬트랜스폼은 새로운 부모에 대해 다시 비교하여 새로 산출하여 저장합니다.

정리하면 어떤 물체의 월드 및 로컬 좌표계값이 바뀌면 하위객체의 모든 월드트랜스폼이 바뀌며,

부모가 변경되는 경우에만 변경된 부모를 가지는 하나의 객체의 로컬트랜스폼만 재산출합니다.

좌표계변환 함수 구현


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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#pragma once

namespace CK
{

	struct Transform2D
	{
	public:
		FORCEINLINE constexpr Transform2D() = default;
		FORCEINLINE constexpr Transform2D(const Vector2& InPosition) : Position(InPosition) { }
		FORCEINLINE constexpr Transform2D(const Vector2& InPosition, float InRotation) : Position(InPosition), Rotation(InRotation) { }
		FORCEINLINE constexpr Transform2D(const Vector2& InPosition, float InRotation, const Vector2& InScale) : Position(InPosition), Rotation(InRotation), Scale(InScale) { }

	public: // 트랜스폼 설정함수
		constexpr void SetPosition(const Vector2& InPosition) { Position = InPosition; }
		constexpr void AddPosition(const Vector2& InDeltaPosition) { Position += InDeltaPosition; }
		void AddRotation(float InDegree) { Rotation += InDegree; Update(); }

		void SetRotation(float InDegree) { Rotation = InDegree; Update(); }
		constexpr void SetScale(const Vector2& InScale) { Scale = InScale; }

		FORCEINLINE constexpr Vector2 GetXAxis() const { return Right; }
		FORCEINLINE constexpr Vector2 GetYAxis() const { return Up; }
		constexpr Matrix3x3 GetMatrix() const;

		FORCEINLINE constexpr Vector2 GetPosition() const { return Position; }
		FORCEINLINE constexpr float GetRotation() const { return Rotation; }
		FORCEINLINE constexpr Vector2 GetScale() const { return Scale; }

		// 트랜스폼 변환
		FORCEINLINE Transform2D Inverse() const;
		FORCEINLINE Transform2D LocalToWorld(const Transform2D& InParentWorldTransform) const;
		FORCEINLINE Transform2D WorldToLocal(const Transform2D& InParentWorldTransform) const;

		void Update()
		{
			ClampAngle();
			float sin = 0.f, cos = 0.f;
			Math::GetSinCos(sin, cos, Rotation);
			Right = Vector2(cos, sin);
			Up = Vector2(-sin, cos);
		}

	private:
		void ClampAngle()
		{
			Rotation = Math::FMod(Rotation, 360.f);
			if (Rotation < 0.f)
			{
				Rotation += 360.f;
			}
		}

	private: // 트랜스폼에 관련된 변수
		Vector2 Position;
		float Rotation = 0.f;
		Vector2 Scale = Vector2::One;

		Vector2 Up = Vector2::UnitY;
		Vector2 Right = Vector2::UnitX;
	};

	FORCEINLINE constexpr Matrix3x3 Transform2D::GetMatrix() const
	{
		return Matrix3x3(
			Vector3(GetXAxis() * Scale.X, false),
			Vector3(GetYAxis() * Scale.Y, false),
			Vector3(Position, true)
		);
	}

	FORCEINLINE Transform2D Transform2D::Inverse() const
	{
		// 로컬 정보만 남기기 위한 트랜스폼 ( 역행렬 )
		Vector2 reciprocalScale = Vector2::Zero;
		if (!Math::EqualsInTolerance(Scale.X, 0.f)) reciprocalScale.X = 1.f / Scale.X;
		if (!Math::EqualsInTolerance(Scale.Y, 0.f)) reciprocalScale.Y = 1.f / Scale.Y;

		// 값을 변경할 때는 SetRotation, SetScale, SetPosition 함수를 사용하시오.
		Transform2D result;
		result.SetScale(reciprocalScale);
		result.SetRotation(-Rotation);

		auto scaledPos = reciprocalScale * -Position;
		Matrix2x2 basisMatrix = Matrix2x2(result.GetXAxis(), result.GetYAxis());

		result.SetPosition(basisMatrix * scaledPos);
		return result;
	}

	FORCEINLINE Transform2D Transform2D::LocalToWorld(const Transform2D& InParentWorldTransform) const
	{
		// 현재 트랜스폼 정보가 로컬인 경우
		// 값을 변경할 때는 SetRotation, SetScale, SetPosition 함수를 사용하시오.
		Transform2D result;

		result.SetScale(InParentWorldTransform.Scale * Scale);
		result.SetRotation(InParentWorldTransform.Rotation + Rotation);

		auto scaledPos = InParentWorldTransform.Scale * Position;
		Matrix2x2 basisMatrix = Matrix2x2(InParentWorldTransform.GetXAxis(), InParentWorldTransform.GetYAxis());

		result.SetPosition(InParentWorldTransform.Position + basisMatrix * scaledPos);
		return result;
	}

	FORCEINLINE Transform2D Transform2D::WorldToLocal(const Transform2D& InParentWorldTransform) const
	{
		Transform2D invParent = InParentWorldTransform.Inverse();

		// 현재 트랜스폼 정보가 월드인 경우
		// 값을 변경할 때는 SetRotation, SetScale, SetPosition 함수를 사용하시오.
		Transform2D result;

		auto scaledPos = invParent.GetScale() * Position;
		Matrix2x2 basisMatrix = Matrix2x2(invParent.GetXAxis(), invParent.GetYAxis());

		result.SetScale(Scale * invParent.Scale);
		result.SetRotation(Rotation + invParent.Rotation);
		result.SetPosition(invParent.Position + basisMatrix * scaledPos);
		return result;
	}

}

길게보기 힘드니 잘라서 보겠습니다.

대략적인 구조는 같습니다.

어짜피 쉽게 구할 수 있는 (다른 값에 덜 의존적인) 회전과 크기는 바로바로 넣은 후,

약간의 생각을 해야하는 좌표계산만 처리합니다.

좌표는 크기와 방향에 모두 영향을 받습니다.

따라서 미리 크기를 곱한 다음, 회전을 곱해야 하는데

자연스럽게 Rotation * Position 을 하였으나… 동작하지 않았습니다.

엔진(쿼터니언과 벡터의 곱)에 익숙해져버린 저는 습관을 이기지 못한 것이였습니다.

그래서 이해 및 리딩을 하기 편하게 matrix를 선언하여 사용하였습니다.

어짜피 내부연산은 내적이기때문에, 실제 연산을 줄이고 최적화를 진행해야한다면,

행렬로 처리하였던 각 값을 벡터로 분리하여 다시 내적해주면 될 것 입니다.

다음과 같이요.

1
2
3
...SetPosition(V2(
	V2(...GetXAxis().x,GetYAxis().x).Dot(scaledPos),
	V2(...GetXAxis().y,GetYAxis().y).Dot(scaledPos)));

아래는 짜잘한 코드를 제거하고 대략적인 흐름만 볼 수 있는 코드를 작성하여 보았습니다.

Inverse

1
2
3
4
5
6
7
8
9
Transform2D result;
auto scale = Vector2(1.f / Scale.X, 1.f / Scale.Y);
result.SetScale(scale );
result.SetRotation(-Rotation);

auto scaledPos = scale * -Position;
Matrix2x2 basisMatrix = Matrix2x2(result.GetXAxis(), result.GetYAxis());

result.SetPosition(basisMatrix * scaledPos);

LocalToWorld

Local To World.

자기자신의 위치와, 부모의 위치를 더한다는 생각에서 시작하였습니다.

즉, 단순 합과 단순 곱이며, 회전 및 스케일링 된 좌표에 대해서만 다시한번 처리해줍니다.

1
2
3
4
5
6
7
8
9
Transform2D result;

result.SetScale(InParentWorldTransform.Scale * Scale); 
result.SetRotation(InParentWorldTransform.Rotation + Rotation);

auto scaledPos = InParentWorldTransform.Scale * Position;
Matrix2x2 basisMatrix = Matrix2x2(InParentWorldTransform.GetXAxis(), InParentWorldTransform.GetYAxis());

result.SetPosition(InParentWorldTransform.Position + basisMatrix * scaledPos);

WorldToLocal

사실 여기가 살짝 어렵습니다.

처음 구현에서 Inverse를 잘못 구현해서,

뭔가 잘못되었기 시작했기 때문입니다.

자기자신의 위치에서 부모의 위치를 뺀다는 생각에서 나아가서 진행하였습니다.

생각한 흔적 일부

생각한 흔적 일부

1
2
3
4
5
6
7
8
9
Transform2D result;
Transform2D invParent = InParentWorldTransform.Inverse();
auto scaledPos = invParent.GetScale() * Position;
Matrix2x2 basisMatrix = Matrix2x2(invParent.GetXAxis(), invParent.GetYAxis());

result.SetScale(Scale * invParent.Scale);
result.SetRotation(Rotation + invParent.Rotation);
result.SetPosition(invParent.Position + basisMatrix * scaledPos);
return result;
This post is licensed under CC BY 4.0 by the author.
Contents