티스토리 뷰
1. 서론
아래의 영상과 같이 3D 오브젝트를 단순히 회전시키는 프로그램을 제작하는 데에도 예상 외로 심오한 수학이론이 필요합니다. 레온하르트 오일러(1707~1783)가 제안한 오일러 각 이론을 이용하거나 윌리엄 로언 해밀턴(1805~1865)이 제시한 쿼터니안(Quaternion)이란 숫자체계 체계를 도입하여야 3D 오브젝트의 회전을 구현 할 수 있습니다. 위 두 방법론은 각각 장단점과 특징을 갖습니다.
우선 오일러 각을 이용한 방법은 매우 직관적이어서 처음 접하는 사람들이 이해하기 쉽습니다. 그러나 짐벌락이라 불리는 무시무시한 함정에 빠질 수가 있습니다. 오일러 각과 짐벌락에 관한 자세한 이야기는 다음 자료에서 다루고 이번 자료에선 아주 간략히 언급해보겠습니다. 오일러각도를 이용한 회전은 기본적으로 xyz 좌표계에 갇혀 있는 느낌이듭니다. 일반적으로 회전이란 하나의 회전 축과, 회전 각도를 통해 정의 되는데, 오일러 각도는 하나의 회전을 정의하기 위해 기준 축(기준 xyz 축 또는 회전한 후의 xyz 축)을 기준으로 3가지의 회전 각도와 회전축을 상정합니다. 이 상정된 세 번의 연속된 회전으로 하나의 회전을 정의하는 것입니다. 이러한 방법은 x, y, z축을 기준으로 한 회전에 익숙한 일반적인 사람들에게 쉬운 이해를 가져다주지만 하나의 회전을 세 개의 회전으로 억지로 나눔으로 인해 짐벌락과 같은 버그나, 최단거리가 아닌 길로 회전하게 되는 문제를 발생시키는 것으로 보입니다. 또한 이러한 방법은 머리로는 이해가 쉬워도 코드로 구현하는데 훨씬 복잡합니다. 우선 세 개의 각도를 찾는 것 부터 꽤나 복잡한 문제로 보입니다.
쿼터니안을 이용한 회전은 장단점이 딱 반대라고 할 수 있습니다. 하나의 회전을 구현하는데 억지로 3개의 회전으로 나누는 과정 없이 하나의 축, 하나의 회전 각도만을 사용합니다. 이로인해 짐벌락이 전혀 발생하지 않는 깔끔한 회전을 구현할 수 있지만, 쿼터니안이란 수학이론 자체의 이해가 다소 어렵습니다. 저는 쿼터니안을 이용한 회전을 처음 접할 때, 회전은 3차원 현상인데 왜 4차원의 숫자가 필요한지도 상당히 의심스러웠고 인터넷에서 찾을 수 있는 다른 많은 자료들이 상당히 부실하다고 느꼈습니다. 대다수의 자료들이 아래의 쿼터니안을 정의하는 복잡한 사차원 행렬과 사차원의 벡터를 딱 가져다 놓고, '이것이 쿼터니안이다. 자세한 이유는 이해가 어렵지만 이러한 연산을 하면 회전이 된다'라는 논조였습니다.
물론 이해 없이 구현하는 것에만 목적이 있다면 이 정도 설명도 충분하지만, 명확한 원리의 이해를 중시하는 이공학도들의 입장에선 찝찝한 설명들이 아닐 수 없습니다. 그래서 여러 가지 설명들을 더 찾아봤는데 꽤 훌륭하다 느껴지는 설명도 있었습니다. 아래 링크는 4차원을 설명하기위해 그래픽을 도입하여 쿼터니안의 실수부의 의미에 관해 설명하는 자료인데 저는 정말 흥미롭게 보았으며 쿼터니안에 관한 이해도를 넓힐 수 있었습니다.
(https://www.youtube.com/watch?v=d4EgbgTm0Bg&t=280s)
4차원을 통한 설명은 우아해 보이긴하나 머릿 속에서 의심없이 완벽히 정립되는 느낌을 받을 수가 없었습니다. 무엇보다 3D 회전을 설명하는데 3D만으로 좀 더 직관적으로 설명할 수 없을까에 관해 의심을 가졌으며 이것이 이 글을 쓰게 된 동기가 되었습니다. 이 번 글에선 저 복잡한 쿼터니안 식이 왜 3D 회전인가에 관해 4차원 없이 아주 쉬운 수학이론으로 설명을 전개할 것이며 프로그램 카테고리에 그것에 관한 간단한 실습 프로그램과 설명자료를 제시할 것입니다.
2. 쿼터니안의 정의
쿼터니안이 왜 회전인가를 얘기하기 전에 먼저 쿼터니안이란 수체계에 대해 정리하겠습니다. 사실 제가 복소수 이론이나 복소 해석학에 관한 지식이 부족하여, 쿼터니안에 관한 글을 이렇게 자신있게 적어도 되는지 의심스럽지만, 3D 회전을 설명하는데 필요한 수준까지 설명해보도록 하겠습니다. 댓글을 통해 제가 잘못 생각하는 부분을 지적해주시고 활발하게 지식을 교류하고 싶습니다.
쿼터니안이란 하나의 실수부와 3개의 허수부(또는 하나의 3차원 벡터부)로 정의 됩니다.
q=s(실수부)+xi+yj+zk(허수부)
q0=s0+ x0i+y0j+z0k=s0+ v0
q1=s1+ x1i+y1j+z1k=s1+ v1
q0×q1=(s0+ x0i+y0j+z0k)×(s1+ x1i+y1j+z1k)
= {s0s1-(x0x1+ y0y1 + z0z1)}+{s0(x1i + y1j+z1k)+s1(x0i+y0j+z0k)+(y0z1-y1z0)i+(z0x1-z1x0)j+(x0y1-x1y0)k}
q0×q1= (s0s1-v0∙v1 )+ (s0v1+s1v0+v0×v1)
본 자료에선 마지막 수식인 두 쿼터니안 곱을 스칼라와 벡터의 외적 내적의 조합으로 나타낸 식이 가장 중요합니다. 앞으로 ‘쿼터니안으로 어떻게 회전연산을 하는가?’의 내용을 전개함에 있어서 이 수식만 기억하고 있으면 됩니다.
3. 쿼터니안이 왜 회전인가?
일반적으로 쿼터니안을 이용한 회전 연산은 아래와 같은 수식으로 계산됩니다.
q =cos(θ/2) - sin(θ/2)n ̂
q*=cos(θ/2) + sin (θ/2)n ̂
p=0+ x0i+y0j+z0k
p'=q*pq
위 수식에서 n ̂은 회전 축을의 의미하는 단위벡터이며, θ는 회전각도를 의미합니다. p는 회전 전의 점의 위치를 의미하며 위치는 (x0, y0, z0) 이고, 앞의 스칼라 값은 0입니다. (점의 위치는 삼차원이므로 사차원을 만드는 스칼라부는 0이다.) p'는 회전 후 의 점의 위치를 나타냅니다. 많은 자료에서 보여주는 쿼터니안 회전연산을 설명할 때 제시한 4차원 행렬은 위의 연산을 행렬 연산으로 나타낸 것일 뿐 완전히 등가입니다. 다음 절에서 왜 등가인지도 유도해보겠습니다. 그렇다면, 왜 저 연산이 축 n ̂을 기준으로 θ만큼 회전시키는 연산인 것일까요? 회전 전 쿼터니안에 회전 쿼터니안을 앞뒤로 곱하는 것이나 θ가 아닌 θ/2를 사용하는 것도 굉장히 기괴해 보입니다. 그럼 지금부터 어떠한 원리가 숨어있는지 파헤쳐 보도록 하겠습니다.
먼저 P의 벡터성분을 회전 축에 수직한 성분과 수평한 성분으로 분리하겠습니다. 분리하지 않고도 증명할 수 있지만 분리할 경우 이해가 훨씬 쉽다고 생각하므로 이 자료에선 분리한 후 전개하겠습니다.
p=portho + pparal=(0+vortho )+ (0+vparal)
먼저 portho에 위의 연산에 적용한 후 분석하겠습니다. 임의의 두 쿼터니안의 곱은 위에서 언급했듯이 아래의 식과 같이 표현 됩니다.
q0×q1= (s0s1-v0∙v1)+(s0v1+s1v0+v0×v1)
이를 이용하여 portho x q를 전개하겠습니다.
portho q=(0×cosθ/2 - vortho∙sinθ/2n ̂ ) + (0×-sin θ/2n ̂+cosθ/2×vortho+vortho× -sin θ/2n ̂ )
vortho와 n ̂ 은 직교하므로 vortho∙-sin θ/2n ̂ 은 0이 됩니다. 그러므로 portho q의 스칼라 파트는 0이 됩니다. portho q를 다시 정리하면
portho q = 0 + (cosθ/2vortho+ vortho×-sin θ/2n ̂)
그러면 portho q 의 벡터파트의 의미를 기하학적으로 분석해보겠습니다. 우선, portho를 θ/2 만큼 회전한 쿼터니안을 p'ortho 라 하겠습니다. cosθ/2 ×vortho 는 벡터인 vortho에 cosθ/2 만큼 곱해져 있으므로 p'ortho의 벡터성분에서 portho의 벡터성분으로 내린 정사영의 크기를 갖고 방향은 portho의 벡터성분과 같은 방향입니다. 즉 옆의 그림의 초록색벡터를 의미합니다. vortho × (-sin θ/2)n ̂은 두 벡터의 외적이므로 이 벡터의 방향은 두벡터에 모두 수직이며(오른손의 법칙을 따름), 크기는 vortho의 크기에 sin θ/2를 곱한것과 같게됩니다.(n ̂은 단위벡터이므로) 즉 옆 그림의 파란색 벡터를 의미한게 됩니다.
그러므로 두 벡터cosθ/2×vortho와 vortho × (-sin θ/2)n ̂의 합인 portho q는 portho를 θ/2 만큼 회전한 벡터를 의미합니다. 즉 스칼라 값이 영인 쿼터니안에 그 쿼터니안의 벡터 성분과 수직인 n ̂을 갖는 쿼터니안 q =cosθ/2 - sin θ/2n ̂ 를 곱한다는 것은 원래 쿼터니안의 벡터 값을 θ/2만큼 회전시킨다는 의미를 갖게 됩니다.
그렇다면 켤레 쿼터니안인 q*를 앞에다 곱하는 것은 어떤 의미일까요? 켤레 쿼터니안 q* = cosθ/2 + sin θ/2n ̂ 은 q* = cos(-θ/2) - sin(-θ/2)n ̂ 로 볼 수 있습니다. 즉 -θ/2 만큼 회전 시키는 쿼터니안으로 생각할 수 있습니다. 그런데 앞에다 곱하게 될 경우 벡터의 외적 연산 시 방향이 반대가 됩니다. 그러므로 앞에 곱한다는 것은 반대방향 회전 연산자를 뜻하게 됩니다. 반대방향으로 -θ/2 회전한 다는 것은 원래 방향으로 θ/2회전한다는 의미하므로, p'=q^ porthoq 을 한다는 것은 p의 벡터성분을 n ̂을 축으로 온전히θ만큼 회전시키는 것을 의미게 됩니다. 이 것이 가장 핵심적인 개념입니다.
그렇다면 한번에 θ를 만큼 회전하면 될 것인데 무엇 때문에 p'=q* pq 와 같이 θ/2의 회전을 의미하는 쿼터니안을 앞뒤로 곱하는 복잡한 연산을 하는 걸까요? 그 이유는 회전 축에 수평한 성분 쿼터니안인 pparal 때문입니다. 수평한 성분인 pparal 경우 q 를 곱하였을 때 스칼라 값이 사라지지 않아서 삼차원의 점을 의미하지 않게 됩니다. 그럼 수평 성분에 관한 수식적 분석도 하도록 해보겠습니다.
pparal q=(0×cosθ/2 - vparal∙(-sin θ/2)n ̂ )+ (0×-sin θ/2n ̂+cosθ/2×vparal+vparal × (-sin θ/2)n ̂ )
vparal과 n ̂ 은 평행하므로 내적하면 vparal 크기 값이나옵니다.( vparal∙n ̂= |vparal ||n ̂ |cos(0) = |vparal| ) 또한, vparal × (-sin θ/2)n ̂ 은, vparal과 n ̂ 은 평행하여 외적값은 0이므로, 0값을 갖습니다.
pparal q=( sin θ/2 |vparal|) + (cos θ/2 vparal)
즉 수평성분의 쿼터니안에 q를 곱하면 스칼라 값이 sin θ/2 |vparal| 이고 벡터값의 길이가cos θ/2배로 줄게 됩니다. 그러면 이제 순서에 맞게 켤레 쿼터니안인 q*=cos θ/2 + sin θ/2 n ̂ 을 앞쪽에 곱해보겠습니다.
q* pparal q=(cos(θ/2) sin θ/2 |vparal|-cos θ/2 vparal∙(sin θ/2)n ̂ )
+(sin θ/2 sin θ/2 |vparal|n ̂+cos θ/2 cos θ/2 vparal- sin θ/2 n ̂ × (cos θ/2) vparal )
vparal와 n ̂ 의 내적값은 |vparal| 이므로 스칼라 파트는 0이 됩니다. 벡터파트의 경우vparal와 n ̂의 방향이 같으므로 |vparal |n ̂ 는 vparal 이 됩니다. 수식을 정리하면,
q* pparal q= +(cos θ/2 cos θ/2+sin θ/2 sin θ/2) vparal - sin θ/2 n ̂ × cos θ/2 vparal
cos θ/2 cos θ/2+sin θ/2 sin θ/2은 1이며 q* pparal q의 벡터성분은 n ̂와 vparal 이 평행하므로 외적값은 0이 됩니다.
q* pparal q=0+vparal=pparal
즉 q* pparal q 은 연산을 하기 전인pparal 와 같습니다. 결론적으로,
q* pq=q* (portho+pparal )q = q* portho q+ q* pparal q = q* portho q+ q* pparal q=p'ortho+pparal
pparal 은 p'paral 와 같으므로
q* pq = p'ortho+p'paral= p'
이러한 원리로 q* pq가 q의 벡터 성분을 축으로 하여 θ만큼 회전하는 회전연산인 것입니다. 여기까지 3차원 컨셉만을 이용하여 쿼터니안 연산 q* pq 이 왜 회전연산인지 분석해보았습니다.
4. 쿼터니안 프로그래밍
쿼터니안으로 삼차원회전을 구현한 프로그램은 필자의 GitHub주소를 통하여 공유하겠습니다. 프로그램의 설명은 프로그램 카테고리의 게시물에서 상세히 다루도록 하겠습니다. 여기서는 핵심 개념만 언급하겠습니다.
쿼터니안 연산은 쿼터니안의 곱셈 규칙만 잘 유의한다면 행렬곱으로 표현가능합니다.
q0×q1=(s0+ x0i+y0j+z0k)×(s1+ x1i+y1j+z1k)
= {s0s1-(x0x1+y0y1+z0z1 )} + {s0 (x1i+y1j+z1k)+s1(x0i+y0j+z0k)+(y0z1-y1z0)i + (z0x1-z1x0)j+(x0y1-x1y0 )k}
위 수식을 행렬곱으로 표현하면 아래와 같습니다.
즉 쿼터니안 q1을 뒤에다 곱하는 것은
이 행렬을 쿼터니안을 나타내는 벡터에 곱하는 것과 같습니다. 또한 q1을 앞에다 곱하는 것은 외적연산으로 부호가 바뀌는 부분만 유의하면 아래 행렬을 곱하는 것과 같다는 것을 알 수 있습니다.
그러므로 q* pq 연산은 p 쿼터니안에 두행렬을 연달아 곱하는 것과 같습니다.
이 두행렬을 곱하면 서론에서 언급한 복잡해보이는 행렬이 나오게되는 것입니다.
프로그램에선 키보드 콜백을 통해 회전각도 θ와 회전 축 n의 값을 입력 받습니다. 이 값을 통해 위의 회전행렬 매트릭스를 구성하고 이를 그래픽카드로 전달합니다. 프로그램 시작 부분에서 3D 오브젝트의 좌표값들은 먼저 그래픽카드에 전달되어있는데, 전달되어 있는 좌표값으로 이루어진 쿼터니안에 회전행렬을 곱하여 나온 값을 랜더링하게되는 구조입니다. 코드의 상세 설명은 위에서 언급한데로 다음 자료에서 설명드리도록하겠습니다. 이상입니다.
'수학&물리' 카테고리의 다른 글
열역학자료 2: 입자의 속도 분포에 관하여 (0) | 2019.12.06 |
---|---|
열역학자료 1: 온도의 정의에 관하여 (2) | 2019.10.26 |
쿼터니안과 삼차원 회전 2 (0) | 2019.09.23 |
벡터의 곱셈에 관한 고찰 (1) | 2019.09.23 |
OpenGL 및 Assimp를 활용한 스켈레톤 애니메이션 프로그래밍 (2/3) (2) | 2019.08.29 |
- Total
- Today
- Yesterday
- RL
- SRP
- 강화학습
- batching
- static batching
- 참조 형식
- value type
- AABB
- Mesh
- MeshProcessing
- 루빅스큐브
- Scriptable Render Pipeline
- RubiksCube
- normalized device coordinate
- perspective projection
- Unreal
- opengl
- NDC
- dynamic batching
- reference type
- transform
- VTK
- Unity
- 유니티
- collision detection
- Mesh Processing
- CollisionDetection
- C#
- 값 형식
- Transformation
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |