피킹( Picking ) 은 마우스 등으로 화면에 특정 위치를 지정 하면 그 위치에 있는
객체를 선택할수 있게끔 하는 기법이다.
예를 들면 전략 시뮬레이션에서 마우스를 이용해 유닛을 클릭하면 유닛이
활성화 되는 방식이 아마도 이 기법을 이용할것으로 예상된다.
2D 게임이야 상당히 간단하다. 마우스로 클릭한 지점, 즉 픽킹한 지점이 화면 좌표계이고
게임상의 객체( 선택이 될 ) 또한 이 화면 좌표계를 사용하므로 피킹한 지점이 캐릭터의
경계 범위( 보통 사각형을 사용 )안에 들어 있다면 선택됐다고 판별하면 되기 때문이다.
사실상 피킹은 또 다른 의미의 충돌 처리라고도 볼수 있다.
보통은 객체와 객체에 대해 충돌 처리가 진행돼지만 피킹의 경우 피킹한 지점( 점 또는 사각형 영역 )과 선택할 객체와의 충돌처리를 한다.
위에서 언급했다시피 2D에서의 피킹, 즉 선택한 지점과 선택될 객체와의 충돌 처리가 간단하지만
3D 에서 그리 간단하진 않다.
- 뭐 그렇다고 그렇게 복잡하지도 않으니 쫄지 말길 바란다.
3D 가 2D 보다 복잡하게되는 이유는 피킹하는 방법에서 과정이 하나 더 추가되기 때문이다.
그건 바로 좌표계의 일치화 이다.
마우스를 이용해 화면의 특정위치를 클릭하면 거기서 얻을수 있는 좌표는
화면( 윈도우 )좌표 기준의 위치이다.
근데 문제는 3D 객체의 기준 좌표는 보통 월드 좌표이다.
간단히 말해 두 좌표계가 같지 않다면 충돌처리 자체를 할수 없다는 것이다.
그럼 할일은 정해진 셈이다. 훗...
둘 중 하나를 한쪽의 기준 좌표계로 변환해주고 동일한 좌표계 상에서 충돌처리를 하면 된다.
개인적으로는 월드 좌표계 기준으로 정의된 3D 객체를 뷰포트( 윈도우 ) 기준의 좌표계로
변환하는 방법을 선택했다.
즉, 화면 좌표계 기준으로 피킹한 지점( 점 )과 3D 메쉬( 화면 좌표계 기준으로 변환된) 간에
충돌을 채크 할것이다.
그럼 피킹 하는 전체 과정을 몇가지 단계로 요약해 보겄다.
1. 로컬(모델) 좌표계 상에서 정의된 3D 객체의 정점들을 화면 좌표계 기준으로 변환한다.
2. 마우스의 위치를 얻는다.( 물론 화면 좌표계 기준으로 )
3. 변환된 3D 객체의 화면 좌표계 상의 범위( 다각형들 )와 마우스 의 위치( 점 )를 비교해
점이 다각형 안에 포함됐는지를 판별 한다.
이제부터 각 단계 별에 대해 친절한 설명 들어간다.
1. 3D 객체의 정점들을 화면 좌표계 기준으로 변환
이젠 지겨울때도된 렌더링 파이프라인( Rendering Pipe Line )얘기를 또 끄집어내야겄다.
혹시나 렌더링 파이프 라인에 대한 이해가 부족한 인간들은 이점을 명심하길 바란다.
렌더링 파이프라인만 완벽히 이해해도 3D Programming의 반은 먹고 들어간다.
P' = P * W * V * Proj
위의 식이 바로 로컬 좌표계 정점( P )가 투영 좌표계 기준( P' )되기까지의
아름다운 여정인 렌더링 파이프라인 과정을 나타내는 것이다.
로컬 좌표계 기준으로 정의된 정점 P는 World, View, Projection 등의 여러 명의 손길을 거쳐
궁극적으로는 투영 좌표계 기준 정점 P'로 새롭게 거듭나는 것이다. 그냥 각각 곱하면 된다. 쩝.
- 정점이 화면에 나타나기 위해서는 하나의 과정을 더 거쳐야한다. 그건 나중에 설명 하겠다.
그럼 로컬 좌표계 기준으로 정의된 3D 객체를 화면 좌표계 기준으로 바꾼다고 할때
3D 객체의 각 정점들을 모두 P 에다 넣으면 궁극적으론 화면 좌표계 기준으로 변환된 위치값을
알수 있을것이다.
사실, 어차피 저 과정은 3D 객체( 로컬 좌표계 기준 )를 화면에 렌더링하기 위해 반드시
행해져야 돼는 일이다. Direct3D 에서는 다음과 같이 각각의 파이프라인 변환들을
다음과 같이 지정한다.
...
g_pd3dDevice->SetTransform( D3DTS_VIEW, matView );
...
위와 같은 방식으로 나머지 변환들에 대해서도 적절한 변환 행렬을 지정한 뒤에
DrawPrimitive...() 함수를 호출하면 자동적으로 정점들은 변환이 돼 화면에 출력돼게 된다.
단, 문제는 각 과정에서의 변환된 결과 값들을 우리는 알수 있는 방법이 없다.
다시 말해 우리는 변환된 정점의 위치 값을 알고 싶단 말이다!!!
왜냐면 마우스로 피킹된 지점이 3D객체에 포함됐는지 검사( 충돌 처리 )해야 하므로.
뭐 본인의 경우 특별한 방법은 모르겠고 해서 직접 행렬을 곱해버렸다.
MATRIXA16 matResult; // 결과 행렬
matResult = g_matWorld * g_matView * g_matProj; // 곱하는 순서대로 월드, 뷰, 투영 변환
평소에 이쁜짓(...)을 많이 하는 MS 가 행렬( MATRIXA16 )에게 연산자를
다중 정의( Operator OverLoading )해준 덕분에 구지 별도의 함수를 사용하지 않고
직접 곱할수 있다.
*한가지 주위 할점은 반드시 위처럼 곱해진 순서를 지켜야한다는 것이다.
자, 이제 변환 행렬은 완성 됐고 이제 남은건 로컬 좌표계 정점 P를 방금
구한 변환 행렬로 변환하는것이다.
마우스 스크룰이 귀찮은 인간들을 위해 아래에 한번 더 붙여넣었다.
P' = P * W * V * Proj
-> P' = P * matResult
안타깝게도 벡터( 정점 )과 행렬의 곱셈은 정의가 안된 관계로 바로 곱셈 연산자를 사용해 곱하면
안되고 벡터와 행렬을 곱해주는 전용 함수를 사용해야 한다.
D3DXVec3TransformCoord( &vertex, &vertex, &matResult );
그냥 인자로 주어진 벡터와 행렬을 곱해 결과( 벡터 )를 레퍼런스로 넘겨 준다.
드디어 변환된 정점( P' )의 위치값을 구했다.
그럼 이제 피킹 지점( 마우스 포인트 )과 비교할수 있는 준비가 된겄인가?
아까 잠깐 언급한대로 하나의 과정이 더 남았다.
바로 화면 좌표계 기준으로 정점을 한번 더 변환하는 것인데 왠일인지 렌더링
파이프 라인 과정에서는 빠져 있으므로 직접 이 과정을 구현해야 한다.
그러나 그 과정은 상당히 쉬우니 긴장하지 말길 바란다.
위에서 얻은 P'는 투영 좌표계로 기준으로 변환된 정점이다.
투영 변환은 직교 투영이던, 원근 투영이던간에 무조건 카메라 절두체를 ( -1,-1,-1 ) 과 ( 1, 1, 1 )
범위의 정 육면체로 만들어 버리는 과정이다. 절두체가 -1.0f 과 1.0f 범위를 갖게 돼니
그것에( 절두체 ) 포함된 모든 정점 또한 투영 변환뒤에는 당연히 -1.0f 와 1.0f 의 범위를 갖게 된다.
뭐 유도 할것도 없이 바로 공식 들어간다.
스크린 위치 좌표 = (( - 1.0f - 투영 좌표 ) / 2.0f ) * 윈도우의 길이
가령 윈도우 상의 X 좌표를 알고 싶다면 다음과 같이 구하면 된다.
ScreenPos.x = ( -1.0f - vertex.x ) / ( - 2.0f ) * WindowWidth;
하여간 이렇게 해서 피킹 작업의 1 번 과정은 끝났다.
2. 마우스의 위치
다음으로 구할것은 마우스 포인트 위치를 화면 좌표계 기준으로 얻어오는 것이다.
사실 별로 할건 없고 API 함수를 통해 바로 받아올수 있다.
//---------------------------------------------------------------
// * 전역( 전체 화면 ) 좌표를 윈도우 기준의 좌표로 변환한다
//---------------------------------------------------------------
POINT pt;
RECT Rect;
GetCursorPos( &pt ); // 마우스 커서의 화면 좌표를 얻는다.
GetWindowRect( g_hwnd,&Rect); // 윈도우의 사이즈 및 위치를 얻어온다.
float DX = pt.x - Rect.left; // 전체 화면 기준에서 윈도우 기준으로 바꾼다.
float DY = pt.y - Rect.top;
pt.x = DX;
pt.y = DY;
3. 점이 다각형 안에 포함됐는지를 판별
피킹 작업의 하이라이트 단계이다.
1 번 과정에서 로컬 좌표계로 정의된 3D 객체의 정점들을 화면 좌표계 기준( 2D )으로 바꿨으며
2 번 과정에서 마우스로 선택한 지점을 화면 좌표계 기준으로 얻어왔다.
이제 남은 일은 2 번에서 얻은 점( 마우스 포인트 )이 1 번에서 얻은 영역에 포함돼는
지를 판별하기만 하면 된다.
위 문제를 좀더 일반화 시키면 결국 점이 다각형에 포함됐는지 여부를 검사하는 것이다.
이것에 대한 해결책을 알고 싶다면 아래 링크를 따라가 보도록.
http://blog.naver.com/kzh8055/140053121675
ps) 위의 방법을 써도 되지만 교수님이 가르쳐주신 백터와의 내적을 사용해서도 구할 수 있다.
출 처 : http://blog.naver.com/kzh8055?Redirect=Log&logNo=140053041553
|
|
함수 또는 클래스 | |
|
스택 영역(Stack) |
지역변수, 매개변수와 같이 쓰고 지우는 일이 빈번한 데이터는 스택영역을 사용한다. 스레드 당 1개씩 생성되며, 기본 크기는 1MB이다. 용량이 작아서 이 용량을 초과할 경우 Stack Overflow라는 에러 메시지가 발생한다. 링킹시 옵션으로 “/STACK:reserve [.commit]” 형태로 지정을 하면, 그 내용이 실행파일(EXE) 초반부에 기록되고, 프로그램이 실행될 때 운영체제가 이를 참조하여 스택이 사용할 메모리 영역을 할당한다. | |
|
데이터영역(Data) |
Static 변수 |
코드 내에서 static 키워드로 생성된 데이터로써, 프로그램 생성시 할당된다. |
|
전역변수(Global) |
함수 블록 내에 포함되지 않은 변수로서, 프로그램 생성시 할당된다. | |
|
동적할당(Heap) |
힙 메모리 함수에 의해 생성되는 데이터로 개발자의 필요에 따라 할당, 해제 할 수 있다. 프로그램이 실행되면 기본적으로 1MB의 크기의 힙 메모리 영역을 할당한다. | |
|
코드 영역(Code) |
함수 코드가 이 영역에 저장되며, 함수 코드는 프로그램이 실행될 때 변경되면 안 되므로 읽기 전용이다. | |
|
등급 |
내용 |
|
Committed |
물리적 메모리에 맵핑된 상태의 메모리 영역이다. Commit 상태의 메모리 영역은 읽거나 쓸 수 있다. VirtualAlloc()을 통하여 Commit 상태로 변경할 수 있으며, Commit 상태를 해제하여 Reserved 또는 Commit 상태로 바꿀 수 있다. WIN16 API LocalAlloc()을 이용하여 Commit 상태로 변경할 수도 있다. |
|
Reserved |
특정 크기가 메모리 영역의 사용을 예약해 놓은 상태이다. 즉, 현재는 사용하지 않지만 앞으로 필요하게 될 부분이므로 다른 함수에 의해서 메모리가 할당될 때, 이 부분은 사용하지 말라는 뜻이다. 이 상태에서는 읽거나 쓸 수 없다. 왜냐하면, 물리적 메모리와 맵핑되지 않은 상태이기 때문이다. 사용하기 위해서는 commit 상태로 되어야 한다. 이때 사용되는 함수가 VirtualAlloc() 이다. Reserved 상태를 해제하려면, VirtualFree() 를 사용한다. Reserved 영역을 해제하면 Free 상태가 되고, 프로그램의 다른 부분에 의해 자유롭게 사용이 가능하게 된다. |
|
Free |
최초 가상 메모리가 생성될 때, 모든 가상 메모리 영역은 Free 상태에 놓이게 되는데, 읽거나 쓸 수 없는 빈 영역이라고 생각하면 된다. 이 영역을 Reserved 또는 Commit 상태로 변경할 수 있다. 물론 물리적 메모리와는 맵핑 되지 않은 상태이다. |
|
등급 |
내용 |
|
PAGE_NOACCESS |
접근이 금지된 상태 |
|
PAGE_READONLY |
읽기 전용 상태. 중요한 데이터의 경우 덮어쓰기 등의 경우로 데이터를 잃어버리는 경우를 막기 위하여 사용된다. |
|
PAGE_READWRITE |
읽거나 쓸 수 있는 상태. 가장 일반적인 형태로 Commit된 메모리 페이지에 대하여 모든 권한을 부여한다. |
|
0x00000000
0x00000FFF |
NULL 값 할당 영역(4KB) |
Private
2GB |
|
0x00001000
0x003FFFFF |
도스 및 16비트 어플리케이션 영역(4KB) | |
|
0x00400000
0x7FFFFFFF |
프로세스 영역(사용자 영역)
User-Mode(1.99GB) | |
|
0x80000000
0xC0000000 |
공유 메모리 영역(메모리 맵 파일 영역)
Shared Memory-Mapped File(1GB) |
Shared
2GB |
|
0xFFFFFFFF |
커널 영역
Kernel-Mode(1GB) |
|
0x00000000
0x0000FFFF
0x00001000 |
NULL 값 할당 영역(4KB) |
Private
2GB |
|
0xBFFEFFFF
0xBFFF0000
0xBFFFFFFF
0xC0000000 |
프로세스 영역(사용자 영역)
User-Mode(1.99GB) | |
|
|
Off-Limit 영역(64KB) | |
|
0xFFFFFFFF |
커널 영역
Kernel-Mode(2GB) |
Shared
2GB |
|
WIN32 Application (32비트 윈도우 프로그램) |
| ||||
|
| |||||
|
|
Local, Global Memory API |
CRT Memory Function |
|
WIN32 SubSystem | |
|
|
Heap Memory API |
WIN32 Mapped File API | |||
|
Virtual Memory API | |||||
|
Windows Virtual Memory Manager |
Kernel | ||||
|
구분 |
내용 |
|
Virtual Memory |
대용량 객체 또는 구조체 배열 관리에 용이 |
|
Heap Memory |
작은 용량의 많은 데이터 관리에 용이 |
|
Memory Mapped File |
파일과 같은 대용량 스트림(stream) 데이터, 시스템 내 프로세스간의 공유 데이터 관리에 용이 |
|
|
내용 |
|
IsBadCodePtr |
현재의 프로세스가 해당 주소가 가리키는 영역을 읽을 수 있는지의 여부를 확인한다. |
|
IsBadReadPtr |
현재의 프로세스가 해당 주소가 가리키는 영역을 읽을 수 있는지의 여부를 확인한다. |
|
IsBadStringPtr |
현재의 프로세스가 해당 주소가 가리키는 영역을 문자열로 읽을 수 있는지의 여부 확인 하며, 시작 포인터로부터 NULL이 나올 때까지 유효성을 검사한다. |
|
IsBadWritePtr |
현재의 프로세스가 해당 주소가 가리키는 영역에 기록할 수 있는지의 여부를 확인한다. |
저는 쿼터니언이라는 용어가 마치 비밀스러운 어둠의 힘을 가지고 있는 암흑물체에 관한 양자론 용어처럼 느껴집니다. 여러분도 역시 이 어둠의 힘에 매료되었다면 이 글이 도움이 될 것입니다(그렇게 되기를 바랍니다). 이 글은 쿼터니언을 이용해서 회전을 하는 방법을 보여주어 쿼터니언을 좀더 쉽게 이해하는데 도움을 줄 것입니다. 만약 여러분이 글에서 잘못된 내용을 발견하시면 robin@cyberversion.com로 메일을 보내주세요. 또, 이 기사를 여러분의 사이트에 실으려고 하신다면 저에게 메일을 보내주세요. 저는 저의 글이 어디에 퍼져있는지 알고 싶답니다.
이 질문에 대답하기 위해서 우선 방향을 나타내는 일반적인 방법에 대해 논의해 보도록 합시다.
이것이 현재까지 알려진 방법중 방향을 나타내는 가장 간단한 방법입니다. 오일러 표현은 각각의 축마다 축주위의 회전량을 가지고 있습니다. 따라서, 다음과 같은 0도에서 360도(혹은 0~2π)의 범위에서 변하는 3개의 값을 가집니다.
x, y, z <-- 전역 좌표축 주위의 회전 각도이 값은 각각 롤, 피치, 요(혹은 피치, 롤, 요, 등 등)를 표현합니다. 방향은 3개의 각도로부터 생성되는 3개의 회전 행렬을 지정한 순서대로 곱해서 구할수 있습니다.
Note: 회전은 전역 좌표계의 좌표축을 기준으로 회전합니다. 즉 최초의 회전이 그 후의 2번째 3번째의 회전축에 영향을 주지 않습니다. 이 때문에 짐벌락(gimbal lock) 문제가 발생하게 됩니다. 짐벌락에 대해서는 조금 후에 자세히 설명하겠습니다.
이 방법은 짐벌락을 피할수 있으므로 오일러각 방법보다 좀더 낫습니다. 회전축과 각도에 의한 표현은 임의의 회전축을 나타내는 단위 벡터와 단위 벡터 주위의 회전을 나타내는(0~360) 값으로 구성됩니다.
x, y, z <-- 임의의 축을 나타내는 단위 벡터 angle <-- 바로 윗줄에서 정의한 축 주위로 회전 각도왜 이 방법이 나쁠까요?
오일러 표현에 있어서 회전은 전역 좌표계에서 일어나기 때문에 한 축의 회전이 다른 축의 회전과 겹치는(override) 문제가 발생합니다. 따라서 회전을 할 수 있는 축 하나를 잃게 됩니다. 이것을 짐벌락이라고 합니다.
예를 들어, X축과 평행한 어떤 벡터를 Y축주위로 회전해서 그 벡터가 Z축과 평행하게 되었다고 하면 이 때, Z축주위로 아무리 회전시켜도, 벡터의 방향은 전혀 변하지 않게 됩니다.
나중에 여러분에게 짐벌락의 예와 쿼터니언을 사용해서 짐벌락을 해결하는 방법을 보여드리겠습니다.
회전축 표현이 짐벌락 문제로 부터 자유롭긴 하지만 두개의 회전을 보간하는 경우, 또 다른 문제가 발생합니다. 보간계산을 마친 회전들이 부드럽지 못하여 회전 애니메이션이 삑사리가 날 수도 있습니다. 오일러 표현도 마찬가지로 이 문제를 가지고 있습니다.
시작하기에 앞서, 몇가지 가정을 세우겠습니다. 저는 수학적인 이해가 중요시되는 글에서 수학적인 내용들을 대충 생략해버리는 글들에 진절머리가 납니다. 이런 글들은 독자들을 혼란에 빠뜨리기 쉽상입니다.
좌표계 - 이 글은 OpenGL과 같은 오른손 좌표계를 사용합니다. 만약 여러분이 Direct3D 같은 왼손 좌표계를 사용하고 계시다면 행렬들을 전치(transpose)하셔야 합니다. 이미 Direct3D의 샘플들은 쿼터니언 라이브리러를 가지고 있다는 사실에 유념해주시기 바랍니다. 그럼에도 불구하고 저는 여러분이 그 라이브러리를 사용하시기 전에 그 내부가 어떻게 구성되는지를 한번 짚고 넘어갔으면 하는 바램입니다.
회전순서 - 오일러 표현에서 회전 순서는 X->Y->Z의 순입니다. 행렬 형태로 아래와 같이 표현합니다.
RotX * RotY * RotZ <-- 매우 중요행렬 - 행렬은 OpenGL 처럼 열우선 방식으로 합니다.
예 [ 0 4 8 12 1 5 9 13 2 6 10 14 3 7 11 15 ]
벡터와 점 - 변환을 위해 벡터와 점은 4x1의 행렬로 표현합니다. 다음과 같은 모습입니다.
저는 특히 Direct3D보다 OpenGL을 선호하지는 않습니다. 그저 제가 OpenGL을 먼저 배웠고, 쿼터니언도 OpenGL을 통해 익혔을 뿐입니다.
Note: 만약 X->Y->Z 순서가 아닌 다른 회전 순서를 지정하면 몇몇 쿼터니언의 함수들을 다시 구현해야 합니다. 특히 오일러 표현을 다루는 함수들이 그렇습니다.
복소수는 i라는 기호를 사용하여 정의하는 허수(가상의 수)입니다. i는 i * i = -1 라는 성질을 가지고 있습니다.
쿼터니언은 복소수의 확장입니다. i만 사용하는 것이 아니라 제곱근이 ―1 이 되는 3개의 허수를 가집니다. 이 3개의 수는 보통 i, j, k로 표기합니다. 다시 말해 이것은 다음과 같은 성질을 가집니다.
j * j = -1 k * k = -1따라서 쿼터니언을 아래와 같이 표현할 수 있습니다.
q = w + xi + yj + zk여기서 w는 실수, x, y, z는 복소수입니다.
흔히 사용하는 또 다른 표현은 아래처럼 벡터형태로 표현할수 있습니다.
q = [ w, v ]여기서 v = (x, y, z)는 "벡터"라고 말하며 w는 "스칼라"입니다.
v를 벡터라고 부르지만 이것은 일반적인 3차원 벡터가 아닌 4 차원 공간상에 벡터를 표현한 것으로 직관적으로 시각화할 수 없습니다.
벡터와 다르게 2개의 항등 쿼터니언이 있습니다.
곱 항등 쿼터니언은 아래 처럼 표현합니다.
q = [1, (0, 0, 0)]그래서 이 곱 항등 쿼터니언과 곱해진 어떤 쿼터니언도 변하지 않습니다.
가산 항등 쿼터니언은 (저희는 사용하지 않습니다)은 아래처럼 표현합니다.
q = [0, (0, 0, 0)]우선 저는 쿼터니언이 벡터가 아니라는 사실을 말씀드리고 싶습니다. 따라서 여기서 벡터 수학을 사용하지 말아주십시요.
이 부분은 매우 수학적인 내용을 다룰 것입니다. 참을성을 가지고 제 설명을 읽어주세요.
우선 쿼터니언의 크기를 정의합니다.
|| q || = Norm(q) = sqrt(w2 + x2 + y2 + z2)단위 쿼터니언은 아래와 같은 속성을 가지고 있습니다.
w2 + x2 + y2 + z2 = 1그 때문에 쿼터니언의 정규화는 아래와 같이 구합니다
q = q / || q || = q / sqrt(w2 + x2 + y2 + z2)이 단위 쿼터니언은 특별합니다. 왜냐하면 단위 쿼터니언은 3D 공간에서 방향을 표현할 수 있기 때문입니다. 따라서 앞에서 논의된 두가지 방법 대신에 단위 쿼터니언을 통해 방향을 표현할 수 있습니다. 단위 쿼터니언으로 방향을 표현하려면 단위 쿼터니언을 다른 표현(예: 행렬)으로 변환하거나 반대로 변환하는 방법이 필요합니다. 이것에 대해서는 곧 설명드리겠습니다.
단위 쿼터니언은 (x, y, z) 요소를 임의의 축, w요소를 회전 각도로 하는 4차원 공간상의 회전으로서 시각화할 수 있습니다. 모든 단위 쿼터니언들은 4D 공간에서 단위 길이를 가지는 구를 형성하게 됩니다. 다시한번 이게 무슨 소리지 하고 여러분은 직관적으로 이해가 안되실것입니다. 하지만 제가 정말 말하고 싶은 것은 쿼터니언의 스칼라 요소(w)의 부호를 반전시키는 것만으로 180도 회전한 쿼터니언을 얻을 수 있다는 것입니다.
Note: 단위 쿼터니언만을 방향표현에 사용할 수 있습니다. 이 뒤에 논할 모든 내용은 모두 단위 쿼터니언을 사용한다고 가정합니다.
효과적으로 쿼터니언을 사용하려면 결국 쿼터니언을 다른 표현으로 변환해야 할 필요가 있을 것입니다. 키 눌림을 쿼터니언으로 해석할 수는 없지 않습니까? 할수 있으신 분 계신가요? 글쎄여. 아직까지는 없는 듯 하죠?
OpenGL와 DirectX는 행렬로 회전을 표현하기 때문에 쿼터니언->행렬 변환이 아마 가장 중요한 변환 함수일 것입니다. 왜냐하면 동차 행렬이 3D의 기본 표현이기 떄문입니다.
쿼터니언 회전과 동등한 회전 행렬은 다음과 같습니다.
행렬 = [ w2 + x2 - y2 - z2 2xy - 2wz 2xz + 2wy 0
2xy + 2wz w2 - x2 + y2 - z2 2yz - 2wx 0
2xz - 2wy 2yz + 2wx w2 - x2 - y2 + z2 0
0 0 0 w2 - x2 - y2 + z2]
w2 + x2 + y2 + z2 = 1이 되는 단위 쿼터니언의 속성을 사용하면 위 식을 다음과 같이 간단하게 만들 수 있습니다.
행렬 = [ 1 - 2y2 - 2z2 2xy - 2wz 2xz + 2wy 0
2xy + 2wz 1 - 2x2 - 2z2 2yz - 2wx 0
2xz - 2wy 2yz + 2wx 1 - 2x2 - 2y2 0
0 0 0 1 ]
한 쿼터니언을 삼차원 공간에서의 임의축 주위의 회전축과 각도에 의한 표현으로 변환하는 방법은 다음과 같습니다.
회전축이 (ax, ay, az)이고 각도가 theta (라디안)이면 각도는 angle= 2 * acos(w)가 됩니다. 그 때 ax= x / scale ay= y / scale az= z / scale 이 됩니다. scale은 scale = sqrt (x2 + y2 + z2)입니다..제가 알고 있는 다른 방법은 scale = sin(acos(w))을 사용하는 것입니다. 저 스스로 수학적으로 동치관계를 증명하려고 하지는 않았지만 두 방법 다 결과는 같을 것이라 생각합니다.
어쨌든, scale이 0이라면 회전이 없다는 뜻입니다. 그리고 여러분이 특별한 조치를 취하지 않는다면 회전축이 무한대가 됩니다. 따라서 scale이 0인 경우에는 언제나 회전각이 0인 임의의 단위벡터를 그 축에 설정하시면 됩니다.
제가 설명하려고 하는 것들에 대해서 뭐가 뭔지 모르겠다 하시는 독자분들을 위해 이제부터 간단한 예를 보여드리겠습니다.
우선 카메라 방향을 오일러 각으로 표현한다고 해봅시다. 그러면 렌더링 루프에서 다음과 같은 식을 이용하여 카메라를 위치시킵니다.
RotateX * RotateY * RotateZ * Translate이 때 각각의 요소는 4x4 행렬입니다.
이제 단위 쿼터니언을 사용해서 카메라의 방향을 표현하려면 먼저 쿼터니언을 행렬로 변환해야 합니다. 그러면
(쿼터니언으로부터 변환한 회전 행렬) Rotate * Translate같은 것이 생기게 됩니다
OpenGL에 특화한 예는 다음과 같게 될것입니다.
| 오일러 | 쿼터니언 |
|
glRotatef( angleX, 1, 0, 0) |
// 오일러를 쿼터니언으로 변환 |
위의 표현은 모두 같습니다. 제가 말할려고 하는 것은 방향에 쿼터니언을 사용하는 것은 오일러나 회전축과 각도에 의한 표현과 완전히 같고, 앞서 언급한 변환 함수를 통해 상호교환이 가능하다라는 것입니다.
다만, 위의 쿼터니언에 의한 표현에는 오일러 표현법과 마찬가지로 짐벌락의 위험성이 있습니다.
역자주 – "왜 짐벌락 위험성이 있지"라는 답을 글내용상 알수가 없습니다.
물론, 아직은 회전을 쿼터니언으로 만드는 방법을 모르시겠지만 그것은 아래 부분에서 설명하겠습니다.
Note: Direct3D나 OpenGL를 사용하시는 독자분들은 API가 행렬 연결을 처리해주기 때문에 직접 행렬을 취급하는 일은 없겠지만, 이것에 대해서 알아둘 필요가 있습니다.
단위쿼터니언이 3D공간에서의 한 방향을 표현하기 때문에 2개의 단위 쿼터니언간의 곱은 2개의 단위 쿼터니언의 회전을 결합한 회전을 나타내는 단위 쿼터니언이 됩니다. 놀라운 일이지만, 사실입니다.
다음과 같은 2개의 쿼터니안이 있다고 가정해봅시다.
Q1=(w1, x1, y1, z1); Q2=(w2, x2, y2, z2);이 2개의 단위 쿼터니언을 결합한 회전은 아래와 같이 구해집니다
Q1 * Q2 =( w1.w2 - v1.v2, w1.v2 + w2.v1 + v1*v2)이 때 ,
v1 = (x1, y1, z1) v2 = (x2, y2, z2)이 됩니다.
여기서 .와 *은 내적과 외적을 나타냅니다.
하지만 위 식을 아래와 같이 최적화할 수 있습니다.
w = w1w2 - x1x2 - y1y2 - z1z2 x = w1x2 + x1w2 + y1z2 - z1y2 y = w1y2 + y1w2 + z1x2 - x1z2 z = w1z2 + z1w2 + x1y2 - y1x2물론, 여타 쿼터니언들과 마찬가지로 결과 단위 쿼터니언을 다른 회전 표현으로 변환하는 것도 가능합니다. 이런 점이 바로 쿼터니언의 진정한 미학입니다. 2개의 단위 쿼터니언은 구 상에 위치하기 때문에 4차원 공간에서 이들을 곱하는 방법은 짐벌락의 문제를 해결합니다.
여기서 곱하는 순서가 중요합니다. 쿼터니언의 곱은 교환칙이 성립되지 않습니다. 즉 이것은 아래의 식을 의미합니다.
q1 * q2 ≠ q2 * q1Note: 2개의 쿼터니언은 동일한 좌표계 축을 참조해야 합니다. 저는 서로 다른 좌표계에서 2개의 쿼터니언을 합성하는 실수를 범한 적이 있고, 이 때 그 결과 쿼터니언이 특정 각도에서만 실패하는 이유를 알아내기 위해 많은 시간을 고민했습니다.
이제 다른 표현법을 쿼터니언으로 변환하는 방법을 배워봅시다. 저는 샘플 프로그램에 있는 모든 변환들을 사용하지는 않지만 역운동학 등의 보다 진보된 용도로 쿼터니언 방향을 사용하려고 할 때 이것들이 필요할 것입니다.
3D공간에서의 임의의 회전축을 도는 회전은 아래처럼 쿼터니언으로 변환됩니다.
회전축이 (ax, ay, az) 이고 (반드시 단위 벡터여야 함) 회전 각도를 theta (라디안)이면 w = cos(theta/2) x = ax * sin(theta/2) y = ay * sin(theta/2) z = az * sin(theta/2)이 됩니다.우선 회전축이 정규화되어 있어야 합니다. 만약 정규화된 회전축 벡터의 길이가 0이라면 회전이 없는 것을 의미하므로 쿼터니언은 단위(identity) 쿼터니언으로 설정되어야 합니다.
오일러로부터 쿼터니언으로의 변환은 회전순서가 올바르지 않으면 안 되기 때문에 조금더 힘듭니다. 임의축으로부터 3개의 좌표축을 분해하면 오일러 각을 3개의 독립적인 쿼터니언으로 변환해서 이 3개의 쿼터니언을 곱하면 최종 쿼터니언을 얻을 수 있습니다.
따라서 오일러 각 (a, b, c)으로 세개의 독립적인 쿼터니언을 만들수 있으며
Qx = [ cos(a/2), (sin(a/2), 0, 0)] Qy = [ cos(b/2), (0, sin(b/2), 0)] Qz = [ cos(c/2), (0, 0, sin(c/2))]최종 쿼터니언은 Qx * Qy * Qz로 구할수 있습니다.
역자주 –이것은 오일러 회전 순서가 x->y->z의 순서인 경우에만 해당됩니다. 다른 순서로 된다면 Qx * Qy * Qz이 다르게 표현됩니다드디어 "어떻게 쿼터니언이 짐벌락을 피할수 있지?"에 대해서 모두가 기다렸던 궁금증에 대답할 때가 왔습니다.
기본적인 아이디어는
우선, 저는 샘플 코드에 대해서는 어떠한 책임도 지지 않겠습니다. 이 코드는 난잡스러고 제대로 구성되어 있지도 않습니다. 이것은 쿼터니언 테스트할 때 제 프로그램에서 사용했던 쿼터니언 코드를 간략히 줄여놓은 버전일 뿐입니다. 따라서 코드에 대해서 크게 신경을 쓰지 않았다는 점만 기억해 주셨으면 좋겠습니다(돈받고 파는 코드가 아니니까요 ^^)
저는 2개의 실행 가능한 샘플을 준비했습니다. 첫번째 프로그램인 CameraEuler.exe (http://www.gamedev.net/reference/programming/features/qpowers/CameraEuler.zip)는 오일러 각을 사용해서 카메라를 구현한 예입니다.
여러분이 주의깊게 봐야 할 부분은 main.cpp의 Main_Lopp 함수입니다.
여러분이 while 문에서 눈여겨 봐야할 곳은 다음과 같습니다.
위/아래 화살표 키는 X축의 회전, 왼쪽/오른쪽 화살표 키는 Y축회전, Insert/PageUp 키는 Z축회전을 담당합니다.
이 프로그램은, 짐벌락을 일으킵니다. 여러분이 짐벌락 현상을 보고 싶으면, 요를 90도로 취하고 X축이나 Z축회전을 시도해 보십시오. 그리고 무슨일이 일어나는지 살펴보세요.
지금부터는 쿼터니언으로 짐벌락 문제를 해결한 프로그램을 살펴봅시다. 이 프로그램은 CameraQuat.exe (http://www.gamedev.net/reference/programming/features/qpowers/CameraQuat.zip)이며 앞의 프로그램을 조금 변경한 프로그램입니다.
여러분이 while문에서 눈여겨봐야하는 곳은 다음과 같습니다.
키가 눌러졌을 때, 저는 키 입력에 대응하는 고유의 축에서의 작은 회전을 나타내는 임시 쿼터니언을 생성했습니다. 그리고 임시 쿼터니언과 카메라 쿼터니언을 곱했습니다. 이 4차원 공간에서의 회전을 합성한 것이 짐벌락을 피하게 해줍니다. 여러분이 직접 이것을 해보고 자신의 눈으로 확인해주세요.
카메라 쿼터니언은 최종 변환 행렬로 합성할 수 있도록 행렬이나 그에 상응하는 형태로 변환되야 합니다. 여러분이 쿼터니언을 사용할 때, 3차원 공간과 4차원 공간은 섞일 수가 없기 때문에 항상 이 작업을 해주셔야 합니다. OpenGL의 경우에 저는 그냥 쿼터니언으로부터 회전축을 변경하기만 했고 나머지는 API에게 위임했습니다.
제가 두번째 프로그램에서 오일러 각을 회전에 사용하지 않았지만 저는 여러분이 첫번째 프로그램에서의 오일러 회전에 대해서 볼 수 있게 하기 위해서 오일러 회전 부분을 남겨 두었습니다. 오일러 각은 하나 이상의 축으로 회전시키면 올바르게 되지 않을 것입니다. 왜냐하면 쿼터니언이 카메라 쿼터니언으로부터 오일러 각을 얻는 대신 키 입력을 통해서 계산하기 때문입니다. 두번째 프로그램은 프로그램을 시작했을 때 그냥 여러분이 요 값을 90도로 회전하려는 경우에 짐벌락이 더 이상 일어나지 않는다는 것을 보기 위해서 만든 참고자료일뿐입니다.
Note: 저는 여러분에게 저의 수학 라이브러리를 사용하라고 추천하고 싶지 않습니다. 여러분이 쿼터니언을 이해하고 나서 스스로 짜 보십시오. 참고로 저는 이 코드를 전부 버리고 다시 만들 예정입니다. 이 코드들은 저에게 있어서 너무나 지저분하고 난잡한 코드입니다.
여러분이 이미 눈치채셔겠지만 저는 쿼터니언을 오일러 각으로 변환하는 방법에 대해서는 설명하지 않았습니다. 그 이유는 아직까지도 완벽하게 동작하는 변환을 제가 알아내지 못했기 때문입니다. 제가 알고 있는 유일한 방법은 쿼터니언으로부터 행렬을 얻고, 그 행렬로부터 오일러 각을 추출하는 것입니다. 그러나, 오일러에서 행렬 변환은 다대일 관계(sin과 cos으로 인해)이기 때문에 저는 atan2를 사용해서 그 역을 구하는 방법을 알지 못합니다. 만약 정확하게 행렬로부터 오일러 각을 추출하는 방법을 알고 계시다면 저에게 알려주세요.
제가 보여드리지 않은 다른 내용은 행렬을 쿼터니언으로 변환하는 방법입니다. 여러분이 오일러 표현과 회전축과 각도에 의한 표현을 쿼터니언으로 변환할 때 행렬을 거치지 않고도 직접변환이 가능하므로 굳이 행렬을 쿼터니언으로 변환할 필요가 없기 때문입니다.
여러분이 쿼터니언을 완전히 이해했다고 생각하신다면 다시 한번 생각해 보십시오. 아직도 쿼터니언에 대해서 더 배워야 할 게 남아 있습니다. 제가 전에 회전축과 각도(Axis Angle)에 의한 표현이 왜 나쁠까라고 말했던 것을 기억하시나요? '보간'이라는 단어가 갑자기 떠오르지 않나요?
저는 쿼터니언을 사용한 보간에 대해서 설명한 시간을 갖지 못했습니다. 이 글은 제가 예상한 것보다 시간이 오래 걸렸습니다. 여기서 SLERP(구면 선형 보간)에 대한 기본적인 아이디어를 드리겠습니다. 이것은 기본적으로 2개의 쿼터니언 방향 사이에 일련의 쿼터니언들을 생성합니다. 일련의 쿼터니언들은 처음과 마지막 쿼터니언사이에서 부드러운 모션(움직임)의 결과를 제공합니다. 오일러와 회전축 표현으로는 일관성있게 이러한 부드러운 움직임을 만들 수 없습니다.
저는 이 기사가 쿼터니언 이론 뒤에 숨겨진 알수 없는 미스테리들을 속시원하게 없애주었길 바랍니다. 다시한번 마지막으로 여러분에게 당부하고 싶은 말은 서로 다른 좌표계에서 2개의 쿼터니언을 곱하지 말아주십시오. 이렇게 하시면 여러분은 고통의 시간을 맛보시게 될것입니다.
그러면 새로 찾아낸 쿼터니언의 능력에 여러분이 기뻐하시길 바라면서 저는 이제 그만 여러분과 헤어질까 합니다. 몸조심하시구여. 그리구 다시 만나뵙기를 바라면서...