| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- c#
- 게임 수학
- Unity #Android #Build
- 알고리즘
- graphics
- 쿼터니언
- 뉴럴네트워크
- mnist
- Game Development
- gamedev
- C
- 유니티
- 게임 물리
- Unity
- Programming
- game dev
- C++
- 다익스트라
- 딥러닝
- 게임 개발
- unity #graphics
- quaternion
- untiy
- rendering
- Addressables
- Serialize
- Today
- Total
Typing diary
쿼터니언(Quaternion) 회전 본문
오일러 각 — 계산 과정
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 |