쿼터니온을 사용해 오브젝트 회전시키기
원문: http://www.gamasutra.com/features/19980703/quaternions_01.htm
작년은 하드웨어 가속의 시대로서의 역사에 남겨졌다고 할 수 있다. 폴리곤을 래스터라이즈화하고 텍스처 매핑을 하는 많은 작업들이 전용 하드웨어에 넘겨졌다. 결과적으로 우리 게임 개발자들은 이제 물리적 시뮬레이션 및 다른 기능들을 위해 많은 CPU 사이클을 절약할 수 있게 되었다. 쿼터니온 덕택에 그러한 부가적인 사이클들이 회전 및 애니메이션을 부드럽게 하는 것과 같은 작업을 위해 적용될 수 있게 되었다.
많은 게임 프로그래머들은 이미 쿼터니온의 대단한 세계에 대해서 발견했으며, 그것들을 광범위하게 사용하기 시작했다. TOMB RADIDER 타이틀을 포함한 일부 삼인칭 게임들은 그것의 카메라 움직임을 애니메이션하기 위해서 쿼터니온 회전을 사용한다. 모든 삼인칭 게임은 플레이어의 캐릭터의 측면이나 뒤에서 따라가는 가상 카메라를 가지고 있다. 이 카메라는 캐릭터와는 다른 동작을 통해(즉 다른 길이의 호(arc)를 통해서) 움직이기 때문에, 카메라 동작은 (플레이어의) 행동을 따라가기에는 부자연스럽고 플레이어에 비해 너무 "변덕"스러웠다. 이것이 쿼터니온이 해결사로 나서게 된 하나의 영역이다. Tomb Raider 타이틀은 카메라 움직임을 애니메이션하기 위해서 쿼터니온 회전을 사용한다.
쿼터니온의 또다른 일반적 용도는 군용/상업용 비행 시뮬레이터이다. 세 개의 각(roll, pitch, yaw)를 사용하여 비행기의 방향을 조작하는 대신에 상대적으로 x, y, z 축에 대한 회전을 표현하면, 단일 쿼터니온을 사용하는 데 있어 매우 간단해 진다.
올해에 나오는 많은 게임들이 실제적인 물리를 표현하게 될 것이며, 놀라운 게임 플레이와 집중을 가능하게 할 것이다. 만약 쿼터니온으로 방향을 저장한다면, 쿼터니온에 각의 속도(velocities)를 추가하는 것이 행렬에 추가하는 것보다 계산적으로 더 적은 비용을 소비하게 할 것이다.
오브젝트의 방향을 표현하기 위한 많은 방식이 존재한다. 대부분의 프로그래머들은 3x3 회전 행렬이나 세 개의 오일러(Euler) 각을 사용해 이 정보를 저장한다. 이러한 해결책들은 오브젝트의 두 방향 사이를 부드럽게 보간하려고 하기 전에는 잘 동작한다. 사용자가 제어하지 않고 단순히 공간을 자유롭게 회전하는 오브젝트를 상상해 보자(예를 들어 회전문). 만약 당신이 문의 방향을 회전 행렬이나 오일러 각으로 저장하기로 한다면, 당신은 회전 행렬의 값들을 부드럽게 보간하는 것은 계산적으로 비용이 많이 들고 쿼터니온 보간보다는 플레이어의 시점에 대해서 정확하고 부드럽게 보이지 않는다는 것을 알게 될 것이다.
이 문제를 행렬이나 오일러 각을 사용해 해결하고자 한다면, 애니메이터는 단순히 기정의된(keyed) 방향의 숫자를 증가시키기만 해야 할 것이다. 그러나 아무도 얼마나 많은 방향이 충분한 것인지 알 수는 없다. 왜냐하면 게임은 서로 다른 컴퓨터 상에서 서로 다른 프레임율로 작동하며, 회전의 부드러움에 영향을 줄 수 있기 때문이다. 이때가 쿼터니온을 사용할 적절한 시점이며, 이 기법은 우리의 회전문과 같은 오브젝트의 단순한 회전을 표현하기 위해서 단지 두/세개의 방향만을 요구한다. 또한 당신은 개별적인 프레임율에 따라서 보간되는 위치의 개수를 동적으로 조정할 수도 있다.
쿼터니온 이론과 응용프로그램에 대해서 살펴보기 전에 회전이 표현되는 방식에 대해서 살펴보도록 하자. 나는 회전 행렬, 오일러각, 축, 각 표현과 같은 기법에 대해서 다룰 것이며, 그것들의 단점 및 쿼터니온과의 관계에 대해서 설명할 것이다. 만약 이러한 기법들에 대해서 익숙하지 않다면 그래픽 책을 펴서 공부하기를 바란다.
지금까지 나는 4x4 나 3x3 행렬을 사용하는 회전에 대해서 이야기하지 않는 3D 그래픽 책을 본 적이 없다. 결국 나는 대부분의 게임 프로그래머들이 이 기법에 대해 매우 친숙하다고 가정할 것이고, 나는 그것의 단점에 대해서만 이야기하도록 하겠다. 또한 Game Developer 에 1997년 6월에 작성된 Chris Hecker 의 기사("Physics, Part 4 : The Third Dimension," 페이지 15-26)를 다시 읽어볼 것을 강력히 추천한다. 왜냐하면 그것은 3D 오브젝트의 방향설정(orenting)의 문제에 대해서 문제제기하기 때문이다.
회전은 단지 x, y, z 좌표축을 중심으로 한 세 개의 자유각(degrees of freedom, DOF)만을 포함한다. 그러나 9 DOF(3x3 행렬이라 가정)는 회전에 제약을 가할 것을 요구한다 - 우리가 필요로하는 것보다 더 많이. 또한 행렬은 "미끄러지는(drifting)" 경향이 있다. 이것은 6개의 제약(constraint) 중 하나가 위반되고 행렬이 상대적인 축을 중심으로 한 회전을 설명할 때 이러한 상황이 발생한다.
이 문제와 싸우는 것은 행렬 직교화(orthonormalized)를 유지할 것을 요구한다 - 그것은 제약에 순종하는 것을 보증한다. 그러나 이렇게 하는 것은 계산적인 낭비이다. 행렬 미끄러짐을 해결하기 위한 일반적인 방법은 상대적인 기반(basis)를 직교 기반으로 변환하기 위한 Gram-Schmidt 알고리즘에 의존한다. Gram-Schmidt 알고리즘이나 행렬 미끄러짐을 해결하기 위해서 정확한 행렬을 계산하는 것은 많은 CPU 사이클을 소비하며, 부동 소수점 수학을 사용하고 있음에도 불구하고 매우 자주 수행되어야만 한다.
회전 행렬의 또 다른 단점은 그것들이 두 개의 방향 사이의 회전을 보간하기에는 사용하기 너무 어렵다는 것이다. 또한 결과 보간은 가시적으로 매우 변덕스러우며, 이것은 게임에 더 이상 적절치 않음을 의미한다.
당신은 세 좌표축에 대한 회전을 표현하기 위해서 각을 사용할 수도 있다. 이것을 (q, c, f) 식으로 기록할 수 있다; "q 각만큼 z 축 중심으로 반시계 방향으로 회전함으로써 점을 변환한다, 그리고 나서 c 각만큼 y 축 중심으로 회전하고, f 각만큼 x 축 중심으로 회전한다"라는 단순한 상태이다. 여기에는 당신이 오일러 각을 사용하여 회전을 표현하기 위해서 사용할 수 있는 12개의 규약(convention)이 존재한다. 왜냐하면 당신은 회전(XYZ, XYX, XYY...)을 표현하기 위해서 각들의 조합을 사용할 수 있기 때문이다. 우리는 첫 번째 규약 (XYZ)를 제출할 모든 예제를 위한 규약으로 간주한다. 나는 양의(positive) 회전은 모두 반시계 방향이라고 간주할 것이다(Figure 1).
오일러 각 표현은 매우 효율적이다. 왜냐하면 그것은 단지 세 개의 변수만을 사용해 세 개의 DOF 를 표현하기 때문이다. 또한 오일러 각은 모든 제약에 복종할 필요가 없으며, 결국 미끌어지는 경향도 없고 재조정될 필요도 없다.
그러나 연속되는 회전 결합과 관련되어 오일러 각을 사용한 단일 회전을 표현하는 쉬운 방법은 없다. 더우기 두 방향 사이의 부드러운 보간은 수치적 통합(numerical integration)을 포함하는데, 이것은 계산적으로 낭비이다. 또한 오일러 각은 "Gimbal lock" 문제나 a loss of one degree of rotational freedom 이라는 문제를 안고 있다. Gimbal lock 90도로 연속되는 회전이 수행될 때 발생된다; 갑자기 회전이 발생하지 않게 된다. 축에 정렬되어버리기 때문이다.
Figure 1: 오일러 각 표현.
예를 들어 비행 시뮬레이션에 의해서 연속되는 회전이 수행되었다고 상상해 보자. 당신은 첫 번째 회전을 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 는 실제 값이다.)
(Eq. 1)
[w, v] (v = (x, y, z) 는 "벡터" 라 불리며 w 는 "스칼라"라고 불린다)
(Eq. 2)
나는 두 번째 공식을 이 기사 전반에서 사용할 것이다. 쿼터니온이 표현되는 방식에 대해서 알게 되었으니, 그것들을 사용하는 기본적인 연산을 배워보자.
만약 q 와 q' 가 쿼터니온에 의해 표현되는 두 개의 방향이라고 한다면, 당신은 이들 쿼터니온에 Table 1 에 있는 연산을 정의할 수 있다.
모든 다른 연산들은 이들 기본적인 것들로부터 쉽게 이끌어내질 수 있으며, 그것들은 여기에 동봉된 라이브러리에서 세부적으로 설명해 놓았다. 나는 단지 단위 쿼터니온만을 다룰 것이다. 각 쿼터니온은 4D 공간으로 이동될 수 있으며(왜냐하면 각 쿼터니온은 네 개의 부분으로 구성되기 때문이다) 그 공간은 쿼터니온 공간이라고 불린다. 단위 쿼터니온은 그것들의 크기(magnitude)각 1이고 그것은 쿼터니온 공간의 하위 공간인 S3를 형성한다. 이 하위공간은 4D 구체로서 표현될 수 있다. (그것들은 하나의 단위 법선을 가진다) 이것은 당신이 수행해야만 하는 필수 연산들의 개수를 감소시켜 준다.
단지 단위 쿼터니온들만이 회전을 표현하고, 내가 쿼터니온에 대해서 이야기할 때 다른 것이 아니라 단위 쿼터니온에 대해서 이야기하고 있는 것이라고 생각하는 것은 매우 중요하다.
당신은 다른 기법들이 회전을 표현하는 방식에 대해서 이미 살펴 보았기 대문에, 쿼터니온을 사용해 회전을 지정할 수 있는 방법에 대해서 살펴 보도록 하자. 단위 쿼터니온 q 에 의한 벡터 v 의 회전은 다음과 같이 표현될 수 있다는 것이 증명될 수 있다(그리고 증명은 그렇게 어렵지 않다).
v´ = q v q-1 (v = [0, v])
(Eq. 3)
결과는 회전된 벡터 v' 이며, w 를 위해서 항상 0 스칼라 값을 가지게 될 것이다(이전의 Eq. 2를 재호출). 그래서 당신은 계산을 생략할 수 있다.
Table 1. 쿼터니온을 사용한 기본 연산. |
Addition: q + q´ = [w + w´, v + v´]
|
오늘날 대부분 지원하는 Direct3D Immediate 모드(retained 모드는 쿼터니온 회전의 제한된 집합을 가진다) 와 OpenGL 의 API는 쿼터니온을 직접적으로 지원하지 않는다. 결과적으로 당신은 이 정보를 당신이 선호하는 API 에 넘기기 위해서 쿼터니온 방향을 변환할 필요가 있다. OpenGL 과 Direct3D 모두 당신에게 회전을 행렬로 지정하는 방법을 제공하며, 쿼터니온에서 행렬로 변환하는 루틴이 유용하다. 또한 당신이 회전을 일련의 쿼터니온으로서 저장하지 않는 (NewTek 의 LightWave 와 같은) 그래픽 패키지로부터 씬 정보를 import 하기를 원한다면, 당신은 쿼터니온 공간으로부터 혹은 공간으로 변환하는 방법을 필요로 하게 될 것이다.
각 과 축. 각과 축 공식으로부터 쿼터니온 공식으로 변환하는 것은 두 개의 삼각형(trigonometric) 연산과 함께 몇몇 곱셈과 나눗셈을 포함한다. 그것은 다음과 같이 표현될 수 있다.
q = [cos(Q/2), sin(Q /2)v] (Q 는 각이며 v는 축이다)
(Eq. 4)
오일러 각. 오일러 각을 쿼터니온으로 변환하는 것은 유사한 과정이다 - 당신은 단지 정확한 순서로 연산을 수행하는 데 주의하기만 하면 도니다. 예를 들어 비행 시뮬레이션에서 비행기가 yaw, pitch, roll 순으로 작업을 수행했다고 하자. 당신은 이 결합된 쿼터니온 회전을 다음과 같이 표현할 수 있다
q = qyaw qpitch qroll 에서:
qroll = [cos (y/2), (sin(y/2), 0, 0)]
qpitch = [cos (q/2), (0, sin(q/2), 0)]
qyaw = [cos(f /2), (0, 0, sin(f /2)]
(Eq. 5)
곱셈을 수행하는 순서는 중요하다. 쿼터니온 곱셈에는 교환법칙이 성립하지 않는다(벡터 내적을 포함하고 있기 때문이다). 다시 말해 당신이 오브젝트를 다양한 각에 대해서 회전하는 순서를 바꾸게 된다면, 그것은 다른 결과 방향을 생성할 수 있으며, 결국 순서가 매우 중요하다.
회전 행렬. 회전 행렬에서 쿼터니온 표현으로 변환하는 것은 일이 좀 더 많으며, 그것의 구현은 Listing 1 에 나와 있다.
단위 쿼터니온과 회전 행렬 사이의 변환은 다음과 같이 지정될 수 있다.
(Eq. 6)
쿼터니온을 사용해서 회전을 직접 지정하는 것은 어렵다. 당신의 캐릭터나 오브젝트의 방향을 오일러 각으로 젖아하고 그것을 보간 전에 쿼터니온으로 변환하는 것이 최선이다. 사용자의 입력을 받은 이후에 쿼터니온을 직접 재계산하는 것보다는 오일러 각을 사용해 회전을 각으로 증가시키는 것이 더 쉽다(즉 roll = roll + 1).
쿼너티온, 회전 행렬, 오일러 각 사이의 변환은 매우 자주 수행되기 때문에, 변환 과정을 최적화하는 것이 매우 중요하다. 단위 쿼터니온과 행렬 사이의 (9개의 곱셈만을 포함하는) 매우 빠른 변환은 Listing 2 에 나와 있다. 그 코드는 행렬이 오른손 좌표계에 있으며 행렬 회전이 열우선 순으로 표현된다고 가정한다는 데 주의하라(예를 들어 OpenGL 호환).
Listing 1: 행렬에서 쿼터니온으로 변환하는 코드.
MatToQuat(float m[4][4], QUAT * quat)
{
float tr, s, q[4];
int i, j, k;
int nxt[3] = {1, 2, 0};
tr = m[0][0] + m[1][1] + m[2][2];
// check the diagonal
if (tr > 0.0) {
s = sqrt (tr + 1.0);
quat->w = s / 2.0;
s = 0.5 / s;
quat->x = (m[1][2] - m[2][1]) * s;
quat->y = (m[2][0] - m[0][2]) * s;
quat->z = (m[0][1] - m[1][0]) * s;
} else {
// diagonal is negative
i = 0;
if (m[1][1] > m[0][0]) i = 1;
if (m[2][2] > m[i][i]) i = 2;
j = nxt[i];
k = nxt[j];
s = sqrt ((m[i][i] - (m[j][j] + m[k][k])) + 1.0);
q[i] = s * 0.5;
if (s != 0.0) s = 0.5 / s;
q[3] = (m[j][k] - m[k][j]) * s;
q[j] = (m[i][j] + m[j][i]) * s;
q[k] = (m[i][k] + m[k][i]) * s;
quat->x = q[0];
quat->y = q[1];
quat->z = q[2];
quat->w = q[3];
}
}
만약 당신이 단위 쿼터니온을 다루고 있지 않다면, 부가적인 곱셈 및 나눗셈이 요구된다. 오일러 각을 쿼터니온으로 변환하는 것은 Listing 3 에 나와 있다. 게임 프로그래머에 있어서 쿼터니온의 가장 강력한 장점 중 하나는 두 개의 쿼터니온 방향 사이의 보간이 매우 쉬우며 부드러운 애니메이션을 생성할 수 있다는 것이다. 이게 왜 그런지 설명하기 위해서 구체 회전을 사용한 예제를 살펴 보자. 구체 회전 보간은 4 차원에서 최단 경로(arc)인 단위 쿼터니온 구체를 따른다. 4D 구체는 상상하기가 어렵다. 나는 3D 구체(Figure 3)를 사용해 쿼터니온 회전과 보간을 가시화하도록 하겠다.
구체의 중심으로부터 애니메이션되고 있는 벡터의 초기 방향은 q1 에 의해서 표현되고 벡터의 최종 방향은 q3 에 의해서 표현된다고 가정하자. q1 과 q3 사이의 호(arc)는 보간이 따라가는 경로이다. Figure 3 은 우리가 중간 위치인 q2 를 가지고 있다면, q1 -> q2 -> q3 의 보간 경로가 q1 -> q3 보간의 경로와 반드시 같지는 않을 것이다. 초기 및 최종 방향은 같지만 경로는 같지 않다.
쿼터니온은 회전 혼합시에 요구되는 계산을 단순화한다. 예를 들어 당신이 행렬로서 표현되는 두 개 이상의 방향을 가지고 있다면, 두 개의 중간 회전을 곱함으로써 그것들을 쉽게 결합할 수 있다.
R = R2R1 (회전 R1 다음에 회전 R2 가 옴)
(Eq. 7)
Listing 2: 쿼터니온을 행렬로 변환. ([07.30.02] 역자 노트 : 다음 QuatToMatrix 함수는 초기에는 버그가 있었다 -- 열과 행의 순서가 바뀌어 있었다. 이것은 교정된 버전이다. 이것을 지적해 준 John Ratcliff 과 Eric Haines 에게 감사한다.)
QuatToMatrix(QUAT * quat, float m[4][4]){
float wx, wy, wz, xx, yy, yz, xy, xz, zz, x2, y2, z2;
// 계수 계산
x2 = quat->x + quat->x; y2 = quat->y + quat->y;
z2 = quat->z + quat->z;
xx = quat->x * x2; xy = quat->x * y2; xz = quat->x * z2;
yy = quat->y * y2; yz = quat->y * z2; zz = quat->z * z2;
wx = quat->w * x2; wy = quat->w * y2; wz = quat->w * z2;
m[0][0] = 1.0 - (yy + zz); m[1][0] = xy - wz;
m[2][0] = xz + wy; m[3][0] = 0.0;
m[0][1] = xy + wz; m[1][1] = 1.0 - (xx + zz);
m[2][1] = yz - wx; m[3][1] = 0.0;
m[0][2] = xz - wy; m[1][2] = yz + wx;
m[2][2] = 1.0 - (xx + yy); m[3][2] = 0.0;
m[0][3] = 0; m[1][3] = 0;
m[2][3] = 0; m[3][3] = 1;
}
이 결합(composition)은 27 개의 곱셈과 18 개의 덧셈을 포함하며, 3x3 행렬로 간주한다. 다시 말해 쿼터니온 결합은 다음과 같이 표현될 수 있다.
q = q2q1 (회전 q1 다음에 회전 q2 가 온다)
(Eq. 8)
이미 살펴 보았듯이 쿼너티온 기법은 행렬 결합과 유사하다. 그러나 쿼터니온 기법은 단지 8 개의 곱셈과 4 개의 나눗셈만을 요구한다(Listing 4). 그래서 쿼터니온을 결합하는 것은 계산적으로 행렬 결합보다 비용이 싸다. 이러한 비용을 아끼는 것은 계층적 오브젝트 표현과 역운동학에 있어서 특히 중요하다.
이제 효율적인 곱하기 루틴을 가지게 되었다. 가장 짧은 호를 따라서 두 쿼터니온 회전을 보간하는 방법에 대해서 살펴 보자. 구형 선형 보간(Spherical Linear intERPolation(SLERP)) 가 이를 수행하며 다음과 같이 작성될 수 있다.
(Eq. 9)
여기에서 pq = cos(q) 와 인자 t 는 0 부터 1 사이의 값이다. 이 공식의 구현은 Listing 5 에 제출되어 있다. 만약 두 개의 방향이 너무 가깝다면, 당신은 0 으로 나누는 것을 막기 위해서 선형 보간을 사용할 수 있다.
Figure 3. 쿼터니온 회전.
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)]으로서 표현될 수 있다. 이것은 첫 번째 방향이 항상 절대적인 것임을 의미한다. 왜냐하면
q = qidentity q
(Eq. 10)
이기 때문이다.
Listing 4: 효율적인 쿼터니온 곱.
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)을 변경하도록 구현할 수도 있다.
플레이어가 캐릭터의 방향을 바꾸면, 당신은 캐릭터를 즉시 회전시키고 SLERP 를 사용해 캐릭터의 뒤에서 카메라의 방향을 재설정한다(Figure 5). 이것은 부드러운 카메라 회전을 제공하고 플레이어로 하여금 게임이 그들의 입력에 즉각 반응하는 것처럼 느끼게 만드는 두 가지 이점을 가지고 있다.
Figure 4. Third-person camera.
Figure 5. Camera from top.
당신은 카메라의 회전 중심(pivot point) 을 그것이 따라 움직이는 오브젝트의 중심으로 설정할 수 있다. 이것은 당신이 캐릭터가 게임 월드 안에서 이동할 때 이미 만들어낸 계산을 이용할 수 있도록 해 준다.
1인칭 액션 게임에서는 쿼터니온 보간을 이용하지 말라고 하고 싶다. 왜냐하면 이들 게임은 일반적으로 플레이어의 행동에 대한 즉각적인 반응을 요구하며, SLERP 는 시간을 소비하기 때문이다.
그러나 우리는 그것을 특별한 씬에서 사용할 수 있다. 예를 들어 당신이 탱크 시뮬레이션을 만들고 있다고 하자. 모든 탱크는 scope나 targeting 메커니즘을 가지고 있으며, 당신은 그것을 가능한한 실제적으로 시뮬레이션하고 싶을 것이다. scoping 메커니즘과 탱크의 포신은 플레이어가 제어하는 모터 집합에 의해서 제어된다. scope 의 줌(zoom) 능력과 대상 오브젝트의 거리에 의존해, 모터의 작은 움직임이더라도 가시 각에 있어서는 큰 차이를 보일 수 있으며, 개별 프레임 사이에서 약간 끊기거나 큰 움직임을 보일 수 있다. 이러한 원하지 않는 효과를 제거하기 위해서는 줌과 오브젝트의 거리에 관련한 방향 보간을 수행할 수 있다. 몇 프레임 동안 두 위치 사이를 보간하는 것은 빠른 움직임을 완충하고 플레이어가 방향감각을 상실하지 않게 만들어 준다.
쿼터니온을 사용하는 또다른 유용한 응용프로그램은 이미 기록된 (그러나 렌더링되지 않은) 애니메이션이다. (최근의 많은 게임들처럼) 게임을 플레이해 보면서 카메라의 움직임을 기록하는 대신에 Softimage 3D 나 3D Studio MAX 와 같은 상업 패키지를 사용해 카메라의 움직임이나 회전을 미리 기록할 수 있다. 이것은 공간과 렌더링 시간을 절약해 준다. 그리고 나서 당신은 영화같은 씬을 위한 스크립트 호출이 있을 때마다 키프레임 카메라 모션을 재생하기만 하면 된다.
Listing 5: 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)은 다음과 같은 미분방정식으로 표현될 수 있다.
(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 튜토리얼을 찾아보기 바란다.
쿼터니온은 회전을 저장하고 수행하기 위한 매우 효율적이며 극단적으로 유용한 기법이다. 그리고 그것들은 다른 메서드들에 비해서 많은 이점을 제공한다. 불운하게도 그것들은 가시화될 수 없으며, 매우 직관적이지 못하다. 그러나 만약 당신이 내부적으로는 쿼터니온을 사용해 회전을 표현하고, 직접 표현으로서 약간의 다른 메서드들(예를 들어 각-축 또는 오일러각) 사용한다면, 당신은 그것들을 가시화할 필요가 없어질 것이다.
Nick Bobick Caged Entertainment Inc 의 게임 개발자이다. 그리고 그는 현재 멋진 3D game 작업을 하고 있다. 그의 메일은 nb@netcom.ca 이다. 저자는 그의 연구와 출판을 위해서 도움을 줌 Ken Shoemake 에게 감사의 인사를 보내길 원한다 그가 없었다면 이 기사는 불가능했을 것이다.
'Computer_Graphics' 카테고리의 다른 글
쿼터니언의 능력 (0) | 2007.03.22 |
---|---|
게임프로그래밍 :: 사원수 이해하기 (0) | 2007.03.22 |
XRef Object, XRef Scene (0) | 2007.03.14 |