원문 : http://www.gamasutra.com/features/19980703/quaternions_01.htm
작년은 하드웨어 가속의 시대로서의 역사에 남겨졌다고 할 수 있다. 폴리곤을 래스터라이즈화하고 텍스처 매핑을 하는 많은 작업들이 전용 하드웨어에 넘겨졌다. 결과적으로 우리 게임 개발자들은 이제 물리적 시뮬레이션 및 다른 기능들을 위해 많은 CPU 사이클을 절약할 수 있게 되었다. 쿼터니온 덕택에 그러한 부가적인 사이클들이 회전 및 애니메이션을 부드럽게 하는 것과 같은 작업을 위해 적용될 수 있게 되었다.
많은 게임 프로그래머들은 이미 쿼터니온의 대단한 세계에 대해서 발견했으며, 그것들을 광범위하게 사용하기 시작했다. TOMB RADIDER 타이틀을 포함한 일부 삼인칭 게임들은 그것의 카메라 움직임을 애니메이션하기 위해서 쿼터니온 회전을 사용한다. 모든 삼인칭 게임은 플레이어의 캐릭터의 측면이나 뒤에서 따라가는 가상 카메라를 가지고 있다. 이 카메라는 캐릭터와는 다른 동작을 통해(즉 다른 길이의 호(arc)를 통해서) 움직이기 때문에, 카메라 동작은 (플레이어의) 행동을 따라가기에는 부자연스럽고 플레이어에 비해 너무 "변덕"스러웠다. 이것이 쿼터니온이 해결사로 나서게 된 하나의 영역이다.
오브젝트의 방향을 표현하기 위한 많은 방식이 존재한다. 대부분의 프로그래머들은 3x3 회전 행렬이나 세 개의 오일러(Euler) 각을 사용해 이 정보를 저장한다. 이러한 해결책들은 오브젝트의 두 방향 사이를 부드럽게 보간하려고 하기 전에는 잘 동작한다. 사용자가 제어하지 않고 단순히 공간을 자유롭게 회전하는 오브젝트를 상상해 보자(예를 들어 회전문). 만약 당신이 문의 방향을 회전 행렬이나 오일러 각으로 저장하기로 한다면, 당신은 회전 행렬의 값들을 부드럽게 보간하는 것은 계산적으로 비용이 많이 들고 쿼터니온 보간보다는 플레이어의 시점에 대해서 정확하고 부드럽게 보이지 않는다는 것을 알게 될 것이다.
이 문제를 행렬이나 오일러 각을 사용해 해결하고자 한다면, 애니메이터는 단순히 기정의된(keyed) 방향의 숫자를 증가시키기만 해야 할 것이다. 그러나 아무도 얼마나 많은 방향이 충분한 것인지 알 수는 없다. 왜냐하면 게임은 서로 다른 컴퓨터 상에서 서로 다른 프레임율로 작동하며, 회전의 부드러움에 영향을 줄 수 있기 때문이다. 이때가 쿼터니온을 사용할 적절한 시점이며, 이 기법은 우리의 회전문과 같은 오브젝트의 단순한 회전을 표현하기 위해서 단지 두/세개의 방향만을 요구한다. 또한 당신은 개별적인 프레임율에 따라서 보간되는 위치의 개수를 동적으로 조정할 수도 있다.
회전은 단지 x, y, z 좌표축을 중심으로 한 세 개의 자유각(degrees of freedom, DOF)만을 포함한다. 그러나 9 DOF(3x3 행렬이라 가정)는 회전에 제약을 가할 것을 요구한다 - 우리가 필요로하는 것보다 더 많이. 또한 행렬은 "미끄러지는(drifting)" 경향이 있다. 이것은 6개의 제약(constraint) 중 하나가 위반되고 행렬이 상대적인 축을 중심으로 한 회전을 설명할 때 이러한 상황이 발생한다.
이 문제와 싸우는 것은 행렬 직교화(orthonormalized)를 유지할 것을 요구한다 - 그것은 제약에 순종하는 것을 보증한다. 그러나 이렇게 하는 것은 계산적인 낭비이다. 행렬 미끄러짐을 해결하기 위한 일반적인 방법은 상대적인 기반(basis)를 직교 기반으로 변환하기 위한 Gram-Schmidt 알고리즘에 의존한다. Gram-Schmidt 알고리즘이나 행렬 미끄러짐을 해결하기 위해서 정확한 행렬을 계산하는 것은 많은 CPU 사이클을 소비하며, 부동 소수점 수학을 사용하고 있음에도 불구하고 매우 자주 수행되어야만 한다.
회전 행렬의 또 다른 단점은 그것들이 두 개의 방향 사이의 회전을 보간하기에는 사용하기 너무 어렵다는 것이다. 또한 결과 보간은 가시적으로 매우 변덕스러우며, 이것은 게임에 더 이상 적절치 않음을 의미한다. 오일러 각 표현은 매우 효율적이다. 왜냐하면 그것은 단지 세 개의 변수만을 사용해 세 개의 DOF 를 표현하기 때문이다. 또한 오일러 각은 모든 제약에 복종할 필요가 없으며, 결국 미끌어지는 경향도 없고 재조정될 필요도 없다.
예를 들어 비행 시뮬레이션에 의해서 연속되는 회전이 수행되었다고 상상해 보자. 당신은 첫 번째 회전을 x 축 중심의 Q1 이라고 지정하고, 두 번째 회전을 y 축 중심의 90 도 라고 지정하고, 세 번째 회전을 z 축 중심의 Q3 라고 지정했다고 하자. 만약 지정된 회전이 성공한다면, 당신은 z 축 중심의 Q3 회전이 초기 x 축 중심의 회전과 같은 효과를 가지고 있음을 발견하게 될 것이다. y 축 회전은 x 축과 z 축을 정렬되도록 만들어 버렸으며, 당신은 DOF 를 잃어버리게 되었다. 왜냐하면 한 축을 중심으로 하는 회전이 다른 축을 중심으로 하는 반대 회전과 같기 때문이다. Gimbal lock 문제에 대한 세부적인 논의를 알기 위해서는 Advanced Animation and Rendering Techniques : Theory and Practice by Alan and Mark Watt(Addison Wesley, 1992) 를 읽어볼 것을 강력히 추천한다. 축과 각을 사용하는 표현은 회전 표현의 또 다른 방식이다. 당신은 Figure 2 에서 보이는 것 처럼 상대적 축과 각을 지정한다(만약 반시계방향이라면 양의 회전임). 이것은 회전을 표현하기 위한 효율적인 방식이지만, 그것은 오일러 각 표현을 설명했던 것과 (Gimbal lock 문제를 제외하고는) 같은 문제를 내포한다 18세기에 W.R.Hamilton 은 복잡한 숫자에 대한 4차원 확장으로써 쿼터니온을 고안했다. 이후에 쿼터니온이 회전과 방향을 3차원에서 표현할 수도 있음이 증명되었다. 쿼터니온을 표현하기 위해서 사용할 수 있는 몇 가지 공식이 있다. 두 개의 가장 유명한 공식은 complex number 공식(Eq. 1) 과 4D vector 공식(Eq. 2)이다.
w + xi + yj + zk (i2 = j2 = k2 = -1 이며 ij = k = -ji 이고, w, x, y, z 는 실제 값이다.) 나는 두 번째 공식을 이 기사 전반에서 사용할 것이다. 쿼터니온이 표현되는 방식에 대해서 알게 되었으니, 그것들을 사용하는 기본적인 연산을 배워보자.
모든 다른 연산들은 이들 기본적인 것들로부터 쉽게 이끌어내질 수 있으며, 그것들은 여기에 동봉된 라이브러리에서 세부적으로 설명해 놓았다. 나는 단지 단위 쿼터니온만을 다룰 것이다. 각 쿼터니온은 4D 공간으로 이동될 수 있으며(왜냐하면 각 쿼터니온은 네 개의 부분으로 구성되기 때문이다) 그 공간은 쿼터니온 공간이라고 불린다. 단위 쿼터니온은 그것들의 크기(magnitude)각 1이고 그것은 쿼터니온 공간의 하위 공간인 S3를 형성한다. 이 하위공간은 4D 구체로서 표현될 수 있다. (그것들은 하나의 단위 법선을 가진다) 이것은 당신이 수행해야만 하는 필수 연산들의 개수를 감소시켜 준다.
오늘날 대부분 지원하는 Direct3D Immediate 모드(retained 모드는 쿼터니온 회전의 제한된 집합을 가진다) 와 OpenGL 의 API는 쿼터니온을 직접적으로 지원하지 않는다. 결과적으로 당신은 이 정보를 당신이 선호하는 API 에 넘기기 위해서 쿼터니온 방향을 변환할 필요가 있다. OpenGL 과 Direct3D 모두 당신에게 회전을 행렬로 지정하는 방법을 제공하며, 쿼터니온에서 행렬로 변환하는 루틴이 유용하다. 또한 당신이 회전을 일련의 쿼터니온으로서 저장하지 않는 (NewTek 의 LightWave 와 같은) 그래픽 패키지로부터 씬 정보를 import 하기를 원한다면, 당신은 쿼터니온 공간으로부터 혹은 공간으로 변환하는 방법을 필요로 하게 될 것이다.
쿼터니온을 사용해서 회전을 직접 지정하는 것은 어렵다. 당신의 캐릭터나 오브젝트의 방향을 오일러 각으로 젖아하고 그것을 보간 전에 쿼터니온으로 변환하는 것이 최선이다. 사용자의 입력을 받은 이후에 쿼터니온을 직접 재계산하는 것보다는 오일러 각을 사용해 회전을 각으로 증가시키는 것이 더 쉽다(즉 roll = roll + 1).
쿼너티온, 회전 행렬, 오일러 각 사이의 변환은 매우 자주 수행되기 때문에, 변환 과정을 최적화하는 것이 매우 중요하다. 단위 쿼터니온과 행렬 사이의 (9개의 곱셈만을 포함하는) 매우 빠른 변환은 Listing 2 에 나와 있다. 그 코드는 행렬이 오른손 좌표계에 있으며 행렬 회전이 열우선 순으로 표현된다고 가정한다는 데 주의하라(예를 들어 OpenGL 호환). MatToQuat(float m[4][4], QUAT * quat)
만약 당신이 단위 쿼터니온을 다루고 있지 않다면, 부가적인 곱셈 및 나눗셈이 요구된다. 오일러 각을 쿼터니온으로 변환하는 것은 Listing 3 에 나와 있다. 게임 프로그래머에 있어서 쿼터니온의 가장 강력한 장점 중 하나는 두 개의 쿼터니온 방향 사이의 보간이 매우 쉬우며 부드러운 애니메이션을 생성할 수 있다는 것이다. 이게 왜 그런지 설명하기 위해서 구체 회전을 사용한 예제를 살펴 보자. 구체 회전 보간은 4 차원에서 최단 경로(arc)인 단위 쿼터니온 구체를 따른다. 4D 구체는 상상하기가 어렵다. 나는 3D 구체(Figure 3)를 사용해 쿼터니온 회전과 보간을 가시화하도록 하겠다.
쿼터니온은 회전 혼합시에 요구되는 계산을 단순화한다. 예를 들어 당신이 행렬로서 표현되는 두 개 이상의 방향을 가지고 있다면, 두 개의 중간 회전을 곱함으로써 그것들을 쉽게 결합할 수 있다.
QuatToMatrix(QUAT * quat, float m[4][4]){
}
이 결합(composition)은 27 개의 곱셈과 18 개의 덧셈을 포함하며, 3x3 행렬로 간주한다. 다시 말해 쿼터니온 결합은 다음과 같이 표현될 수 있다.
이제 효율적인 곱하기 루틴을 가지게 되었다. 가장 짧은 호를 따라서 두 쿼터니온 회전을 보간하는 방법에 대해서 살펴 보자. 구형 선형 보간(Spherical Linear intERPolation(SLERP)) 가 이를 수행하며 다음과 같이 작성될 수 있다.
여기에서 pq = cos(q) 와 인자 t 는 0 부터 1 사이의 값이다. 이 공식의 구현은 Listing 5 에 제출되어 있다. 만약 두 개의 방향이 너무 가깝다면, 당신은 0 으로 나누는 것을 막기 위해서 선형 보간을 사용할 수 있다.
Listing 3: 오일러각을 쿼터니온으로 변환. EulerToQuat(float roll, float pitch, float yaw, QUAT * quat) { float cr, cp, cy, sr, sp, sy, cpcy, spsy; // calculate trig identities cr = cos(roll/2); cp = cos(pitch/2); cy = cos(yaw/2); sr = sin(roll/2); sp = sin(pitch/2); sy = sin(yaw/2); cpcy = cp * cy; spsy = sp * sy; quat->w = cr * cpcy + sr * spsy; quat->x = sr * cpcy - cr * spsy; quat->y = cr * sp * cy + sr * cp * sy; quat->z = cr * cp * sy - sr * sp * cy; }
기본 SLERP 회전 알고리즘은 Listing 6 에 나와 있다. 당신의 회전 표현이 상대적인 회전이 아니라 절대적인 회전이어야만 함에 주의하라. 상대적 회전은 이전의 (중간) 방향으로부터의 회전이라고 생각할 수 있으며, 절대적 회전은 초기 방향으로부터의 회전이라고 생각할 수 있다. 이것은 당신이 Figure 3 의 q2 쿼터니온방향을 상대 회전으로 생각하면 명확해 진다. 왜냐하면 그것은 q1 방향에 대해 상대적으로 움직였기 때문이다. 주어진 쿼터니온의 절대 회전을 획득하기 위해서는 단지 현재 상대 회전에 이전 상대 회전을 곱하기만 하면 된다. 오브젝트의 초기 방향은 단위 곱 [1, (0, 0, 0)]으로서 표현될 수 있다. 이것은 첫 번째 방향이 항상 절대적인 것임을 의미한다. 왜냐하면
QuatMul(QUAT *q1, QUAT *q2, QUAT *res) { float A, B, C, D, E, F, G, H; A = (q1->w + q1->x)*(q2->w + q2->x); B = (q1->z - q1->y)*(q2->y - q2->z); C = (q1->w - q1->x)*(q2->y + q2->z); D = (q1->y + q1->z)*(q2->w - q2->x); E = (q1->x + q1->z)*(q2->x + q2->y); F = (q1->x - q1->z)*(q2->x - q2->y); G = (q1->w + q1->y)*(q2->w - q2->z); H = (q1->w - q1->y)*(q2->w + q2->z);
res->w = B + (-E - F + G + H) /2; res->x = A - (E + F + G + H)/2; res->y = C + (E - F + G - H)/2; res->z = D + (E - F - G + H)/2; }
이전에 언급했듯이 쿼터니온의 실제 용도는 삼인칭 게임에서의 카메라 회전을 포함한다. 나는 TOMB RAIDER 에서의 카메라 구현을 본 이후로 나는 비슷한 것을 구현하기를 원했다. 자 삼인칭 카메라를 구현해 보자(Figure 4). 먼저 항상 캐릭터의 머리 위쪽에 위치하고, 캐릭터의 머리 약간 위쪽을 바라보는 카메라를 생성하자. 또한 그 카메라는 메인 캐릭터의 뒤에서 d 단위만큼 떨어져 있다. 또한 x 축 중심으로 회전함으로써 roll (Figure 4 의 각 q)을 변경하도록 구현할 수도 있다.
당신은 카메라의 회전 중심(pivot point) 을 그것이 따라 움직이는 오브젝트의 중심으로 설정할 수 있다. 이것은 당신이 캐릭터가 게임 월드 안에서 이동할 때 이미 만들어낸 계산을 이용할 수 있도록 해 준다.
1인칭 액션 게임에서는 쿼터니온 보간을 이용하지 말라고 하고 싶다. 왜냐하면 이들 게임은 일반적으로 플레이어의 행동에 대한 즉각적인 반응을 요구하며, SLERP 는 시간을 소비하기 때문이다.
QuatSlerp(QUAT * from, QUAT * to, float t, QUAT * res) { float to1[4]; double omega, cosom, sinom, scale0, scale1; // calc cosine cosom = from->x * to->x + from->y * to->y + from->z * to->z + from->w * to->w; // adjust signs (if necessary) if ( cosom <0.0 ) { cosom = -cosom; to1[0] = - to->x; to1[1] = - to->y; to1[2] = - to->z; to1[3] = - to->w; } else { to1[0] = to->x; to1[1] = to->y; to1[2] = to->z; to1[3] = to->w; } // calculate coefficients if ( (1.0 - cosom) > DELTA ) { // standard case (slerp) omega = acos(cosom); sinom = sin(omega); scale0 = sin((1.0 - t) * omega ) / sinom; scale1 = sin(t * omega) / sinom; } else { // "from" and "to" quaternions are very close // ... so we can do a linear interpolation scale0 = 1.0 - t; scale1 = t; } // calculate final values res->x = scale0 * from->x + scale1 * to1[0]; res->y = scale0 * from->y + scale1 * to1[1]; res->z = scale0 * from->z + scale1 * to1[2]; res->w = scale0 * from->w + scale1 * to1[3]; }
작년에 Chris Hecker 의 물리 관련 칼럼을 읽은 후에, 나는 내가 작업하고 있던 게임 엔진에 각속도(angular velocity)를 추가하고 싶어졌다. Chris 는 주로 행렬 수학에 대해 다뤘는데, 나는 쿼터니온 에서 행렬로 행렬에서 쿼터니온으로 변환하는 것을 줄이고자 했기 때문에(우리 게임엔진은 쿼터니온 수학에 기반하고 있다), 나는 약간을 연구를 통해 쿼터니온 방향을 위한 (벡터로 표현된) 각속도를 추가하는게 쉽다는 것을 발견했다. 그 해결책(Eq. 11)은 다음과 같은 미분방정식으로 표현될 수 있다.
여기에서 quat(angluar) 는 0 스칼라 부분을 가지고 있는 (즉 w = 0) 쿼터니온이며 벡터 부분은 각속도 벡터와 같다. Q 는 원래 쿼터니온 방향이다.
위의 쿼터니온 (Q + dQ / dt) 를 통합하기 위해서, 나는 Runge-Kutta order four method을 사용할 것을 추천한다. 만약 당신이 행렬을 사용하고 있다면, Runge-Kutta order five method을 사용해 더 좋은 결과를 산출할 수 있을 것이다. (Runge-Kutta method 는 특별한 공식을 통합하는 방식이다. 이 기법에 대한 자세한 설명은 Numerical Recipes in C 와 같은 모든 기초 수치 알고리즘 책에서 찾아볼 수 있다. 그것은 수치적, 미분 방정식을 주제로한 세부적인 섹션을 포함하고 있다.) 각속도 통합의 완벽한 유도(derivation)을 원한다면 Dave Baraff 의 SIGGRAPH 튜토리얼을 찾아보기 바란다. 쿼터니온은 회전을 저장하고 수행하기 위한 매우 효율적이며 극단적으로 유용한 기법이다. 그리고 그것들은 다른 메서드들에 비해서 많은 이점을 제공한다. 불운하게도 그것들은 가시화될 수 없으며, 매우 직관적이지 못하다. 그러나 만약 당신이 내부적으로는 쿼터니온을 사용해 회전을 표현하고, 직접 표현으로서 약간의 다른 메서드들(예를 들어 각-축 또는 오일러각) 사용한다면, 당신은 그것들을 가시화할 필요가 없어질 것이다. [출처] 쿼터니온을 사용해 오브젝트 회전시키기|작성자 라이푸 |
'수학 (Mathematics) > 3D수학' 카테고리의 다른 글
투영행렬로 변환 한 후 다시 카메라 공간의 위치,벡터(norm) 을 구하는 방법과 d3dx9math.h (0) | 2013.07.09 |
---|---|
점,벡터 구분의 미묘함 (0) | 2013.05.08 |
Catmull-Rom 스플라인 곡선 D3DXVec3CatmullRom (0) | 2013.03.21 |
Fast Overlap Test for OBBs (0) | 2013.03.20 |
Object Oriented Bounding Box 를 이용한 Collision Detection (0) | 2013.03.18 |