Typing diary

쿼터니언(Quaternion) 회전 본문

그래픽스, 게임 수학

쿼터니언(Quaternion) 회전

Jcon 2026. 3. 29. 17:38

 

오일러 각 — 계산 과정

3D 회전을 표현하는 가장 직관적인 방법은 오일러 각(Euler Angles)이다. X축으로 30°, Y축으로 45° 같은 식으로 회전을 표현한다.

오일러 각은 세 축(X, Y, Z)에 대한 회전 행렬을 각각 만들고, 순서대로 곱해서 최종 회전 행렬을 구한다.

각 축의 회전 행렬

Rx(α) =  [ 1    0       0   ]
          [ 0   cosα  -sinα  ]
          [ 0   sinα   cosα  ]

Ry(β) =  [ cosβ   0   sinβ  ]
          [  0     1    0    ]
          [-sinβ   0   cosβ  ]

Rz(γ) =  [ cosγ  -sinγ  0   ]
          [ sinγ   cosγ  0   ]
          [  0      0    1   ]

합성 (X→Y→Z 순서 적용)

R_final = Rz(γ) × Ry(β) × Rx(α)

점 p에 적용하려면:

p' = R_final × p

전체 연산 비용은 cos/sin 6회, 3×3 행렬 곱셈 2번이다.

순서가 결과를 바꾼다

행렬 곱셈은 교환법칙이 성립하지 않기 때문에, 적용 순서가 다르면 결과도 달라진다.

Rz × Ry × Rx  ≠  Rx × Ry × Rz

Inspector에서 X, Y, Z 값이 같아도 엔진마다 적용 순서가 다르면 자세가 다르게 나오는 이유가 여기 있다.


오일러 각의 한계 — 짐벌락

오일러 각을 쓰다 보면 특정 각도에서 문제가 생긴다.

Y = 90°일 때 Ry 행렬은 다음과 같다.

Ry(90°) = [  0   0   1  ]
           [  0   1   0  ]
           [ -1   0   0  ]

이 행렬이 x 성분과 z 성분을 교환해버린다. 이 상태에서 Rx와 Rz를 합성하면 두 행렬이 사실상 같은 축에 작용하게 된다. X 슬라이더와 Z 슬라이더가 따로 존재하지만, 실제로 제어할 수 있는 방향은 하나뿐이다. 자유도 3이 자유도 2로 줄어든 상태다. 이것이 짐벌락(Gimbal Lock) 이다.

오일러 각의 또 다른 문제는 보간이다. A 회전에서 B 회전으로 자연스럽게 전환할 때, X, Y, Z를 각각 선형 보간하면 예상과 다른 경로로 돌아가는 경우가 생긴다.


해결의 실마리 — 복소수와 2D 회전

쿼터니언을 이해하려면 먼저 복소수가 2D 회전을 어떻게 표현하는지 알아야 한다.

복소수 a + bi는 2D 평면의 점이다. 그리고 복소수끼리 곱하면 회전이 일어난다.

cos θ + sin θ · i

이 형태의 복소수를 다른 복소수에 곱하면 θ만큼 회전한 결과가 나온다.

단순히 x, y 벡터로 회전을 표현해도 결과는 같다. 그런데 회전을 합성할 때 차이가 드러난다. 벡터(x, y) 방식으로 두 회전을 합치려면 행렬 곱셈이 별도로 필요하다. 복소수 방식은 두 복소수를 곱하는 것만으로 회전이 합성된다.

q_A × q_B = 자동으로 A+B 회전

2D에서 복소수 곱셈은 2×2 회전 행렬 곱셈과 수학적으로 동형(isomorphic)이다. 표기법만 다르고 계산 결과는 동일하다. 복소수는 회전 자체가 숫자여서, 합성과 적용이 같은 연산으로 표현된다는 점이 핵심이다.

쿼터니언은 이 구조를 3D로 확장한 것이다.


쿼터니언 — 구조

복소수가 a + bi (실수 1개 + 허수 1개) 인 것처럼, 쿼터니언은 실수 1개 + 허수 3개로 구성된다.

q = w + xi + yj + zk

w → 실수부 (스칼라)
x → X축 방향 허수부
y → Y축 방향 허수부
z → Z축 방향 허수부

허수 단위가 3개인 이유는 3D 공간의 세 축을 각각 독립적으로 표현해야 하기 때문이다.

회전 쿼터니언은 반드시 단위 쿼터니언 조건을 만족해야 한다.

|q| = √(w² + x² + y² + z²) = 1

이 조건이 깨지면 회전이 아니라 크기 변환이 섞인다.


허수 단위 규칙 — 약속인가, 수학적 근거인가

i² = j² = k² = ijk = -1

ij = k,   ji = -k
jk = i,   kj = -i
ki = j,   ik = -j

이 규칙은 약속이 아니다. 아래 세 조건을 동시에 만족하려면 저 규칙밖에 없다는 게 수학적으로 유도된다.

1. i² = j² = k² = -1
2. |q₁ × q₂| = |q₁| × |q₂|  (단위 쿼터니언 곱은 단위 쿼터니언)
3. i, j, k는 서로 독립적

유일하게 선택 가능한 부분은 ij = +k로 할지 ij = -k로 할지다. 이것이 오른손 좌표계냐 왼손 좌표계냐의 차이다. 하나를 정하면 나머지는 전부 자동으로 결정된다.

rotA * rotB ≠ rotB * rotA인 이유도 여기 있다. 허수 단위 규칙 자체가 비교환성을 가지기 때문이다. 3D 회전도 순서가 결과를 바꾸므로 이 구조가 딱 맞아떨어진다.


쿼터니언 곱셈 공식

두 쿼터니언을 곱할 때 i² = -1, ij = k, ji = -k 규칙을 전개하면 아래 공식이 나온다.

q₁ = [w₁, x₁, y₁, z₁]
q₂ = [w₂, x₂, y₂, z₂]

결과 w = w₁w₂ - x₁x₂ - y₁y₂ - z₁z₂
결과 x = w₁x₂ + x₁w₂ + y₁z₂ - z₁y₂
결과 y = w₁y₂ - x₁z₂ + y₁w₂ + z₁x₂
결과 z = w₁z₂ + x₁y₂ - y₁x₂ + z₁w₂

두 단위 쿼터니언을 곱한 결과도 단위 쿼터니언이다.


회전 쿼터니언 만들기

회전을 쿼터니언으로 표현할 때는 축 벡터 n과 회전각 θ를 다음과 같이 변환한다.

w = cos(θ/2)
x = nx · sin(θ/2)
y = ny · sin(θ/2)
z = nz · sin(θ/2)

θ/2를 저장하는 이유는 회전 적용 시 q가 두 번 곱해지기 때문이다. 아래 샌드위치 곱에서 각도가 2배로 적용되므로, 미리 절반으로 나눠 저장한다.

특수한 경우:

θ = 0°   → w = 1, xyz = 0  (회전 없음)
θ = 180° → w = 0, xyz = 축 방향
θ = 360° → w = -1, xyz = 0  (한 바퀴, 회전 없음과 동일)

회전 적용 — 샌드위치 곱

3D 점 p를 쿼터니언 q로 회전시키는 공식은 다음과 같다.

r = q × p × q⁻¹

전체 과정

STEP 1: 점 (x, y, z) → 순수 쿼터니언  p = (0, x, y, z)
STEP 2: q × p 계산   → w ≠ 0, 4D 중간 상태
STEP 3: (q×p) × q⁻¹  → w ≈ 0, xyz = 회전된 3D 점

왜 양쪽에서 곱해야 하는가

3D 점은 4D 공간의 "w=0 바닥"에 살고 있다. q를 한쪽에서만 곱하면 점이 w=0 바닥에서 들려 4D 공중으로 뜬다. xyz 값도 "반쪽만 밀린" 중간 상태다.

q⁻¹를 오른쪽에서 곱하면 나머지 절반을 밀면서 동시에 w=0 바닥으로 다시 착지시킨다. 결과의 w가 다시 0이 되고 xyz만 남아 올바른 3D 회전 결과가 된다.

q 하나가 θ/2씩 두 번 나눠 밀기 때문에 합산 각도가 θ가 된다. w를 계산 끝에 강제로 0으로 바꾸는 것은 의미가 없다. q⁻¹가 하는 일은 w를 고치는 게 아니라 xyz 값 자체를 올바른 위치로 교정하는 것이다.


역원(q⁻¹) 구하는 법

일반적인 경우

q*   = w - xi - yj - zk   (켤레: xyz 부호만 뒤집기)
|q|² = w² + x² + y² + z²

q⁻¹ = q* / |q|²

q × q*를 전개하면 x, y, z 항은 ij = k, ji = -k 규칙으로 서로 상쇄되어 0이 되고, w 항만 |q|²으로 남는다. 따라서 q × (q*/|q|²) = 1이 성립한다.

회전 쿼터니언의 경우

회전 쿼터니언은 항상 단위 쿼터니언(|q| = 1)이므로:

q⁻¹ = q*   (켤레가 곧 역원)

켤레는 xyz 부호만 뒤집으면 끝이다. q가 +θ 회전이면 q*는 -θ 회전(되돌리기)이므로, 곱하면 회전 없음 = 항등원이 된다.


q × q⁻¹ = 1 에서 1의 의미

여기서 1은 숫자 1이 아니라 항등 쿼터니언(identity quaternion) 이다.

항등 쿼터니언 = (w=1, x=0, y=0, z=0)

회전 공식에 대입하면 cos(θ/2) = 1 → θ = 0°, 즉 회전이 없는 상태다. 행렬에서의 단위행렬 I와 같은 역할이다.

Unity에서는 Quaternion.identity가 이에 해당한다.

transform.rotation = Quaternion.identity; // 회전 없음, (0°, 0°, 0°)

Unity에서 q⁻¹을 직접 쓰지 않아도 되는 이유

Unity의 * 연산자는 오버로딩되어 있다. Quaternion * Vector3는 내부적으로 q × p × q⁻¹을 수행하고 결과 Vector3만 반환한다.

Vector3 rotated = q * someVector;
// 내부: q × (0, x, y, z) × q⁻¹ 계산 후 xyz만 반환

transform.rotation = q도 마찬가지로 모든 정점에 샌드위치 곱을 적용한다.

Quaternion.Inverse(q)가 직접 필요한 경우는 회전을 되돌릴 때다.

// 월드 공간 방향을 로컬 공간으로 변환
Vector3 local = Quaternion.Inverse(transform.rotation) * worldDirection;

정리

오일러 각 쿼터니언

저장 각도 3개 w, x, y, z (4개)
cos/sin 연산 6회 2회
짐벌락 있음 없음
보간 부자연스러움 Slerp (최적 경로)
오차 보정 행렬 재정규화 (복잡) normalize() 한 줄

쿼터니언이 게임 엔진의 표준이 된 이유는 세 가지다. 저장 크기(4개 숫자), 보간(Slerp — 4D 구 위의 최단 호), 오차 보정(크기=1 조건 하나만 유지).

 

'그래픽스, 게임 수학' 카테고리의 다른 글

텍스처 압축 포맷  (0) 2025.07.23
Verlet알고리즘으로 PhysicsBone 구현하기!  (1) 2025.07.09
드로우 콜(Draw Call)  (1) 2025.06.04
Raytracing  (0) 2022.08.21
Backface Culling  (0) 2022.08.21