쿼터니언을 몰라도 쓰는 법
Inspector 의 Rotation 에는 (0, 90, 0) 처럼 각도 셋이 보이는데, 코드에서 transform.rotation.y 를 읽으면 0.71 같은 숫자가 나옵니다.
transform.rotation 은 각도 셋이 아니라 쿼터니언이라는 네 숫자 (x, y, z, w) 이고, 회전 하나를 “어느 축으로, 몇 도” 로 나타냅니다.
네 숫자의 뜻을 몰라도 됩니다. 만들고, 곱하고, 벡터에 곱하는 세 가지만 알면 포탑 조준, 부채꼴 사격, 자기 기준 회전이 대부분 풀립니다.
쿼터니언은
Quaternion.Euler·AngleAxis·LookRotation같은 함수로 만듭니다. 네 숫자를 직접 넣지 않습니다.q * v는 벡터 v 를 q 만큼 돌린 벡터입니다.a * b는 두 회전을 합친 것이고, 순서를 바꾸면 결과가 다릅니다.회전 * q는 자기 축 기준,q * 회전은 월드 축 기준입니다.
쿼터니언은 축과 각도다
어떤 자세(물체가 어느 쪽을 보고 어느 쪽이 위인지)든, 처음 자세에서 축 하나를 잡고 그 축으로 한 번 돌리면 만들 수 있습니다. 여기서 축은 팽이의 심처럼 돌 때 움직이지 않는 막대입니다. 의자에 앉아 오른쪽으로 90° 돌아앉은 자세는 “위아래 축(y)으로 90°” 한 번이고, 비스듬히 기울어진 자세도 비스듬한 축 하나로 한 번 돌린 것입니다.
쿼터니언은 그 축과 각도를 네 숫자에 담습니다. 이름도 넷(quater)에서 왔습니다. 이때 각도를 그대로 넣지 않고 각도의 절반(반각) 을 씁니다. 90° 면 45° 입니다.
- x, y, z 자리: 축 방향에 sin(반각)을 곱한 값
- w 자리: cos(반각)
sin 과 cos 는 길이 1 인 막대를 그 각도로 세웠을 때의 높이와 바닥 길이입니다. 45° 면 둘 다 0.71 입니다. 그래서 y 축 (0, 1, 0) 으로 90° 돈 회전은 x 0, y 1 × 0.71, z 0, w 0.71 로 (0, 0.71, 0, 0.71) 이 됩니다.
| 회전 | 축 | 각도 | (x, y, z, w) |
|---|---|---|---|
| 안 돌림 | 아무거나 | 0° | (0, 0, 0, 1) |
| 좌우로 90° | y (0, 1, 0) | 90° | (0, 0.71, 0, 0.71) |
| 좌우로 180° | y (0, 1, 0) | 180° | (0, 1, 0, 0) |
| 앞으로 90° 숙이기 | x (1, 0, 0) | 90° | (0.71, 0, 0, 0.71) |
rotation.y 가 0.71 이었던 이유가 이 표에 있습니다. y 축으로 90° 돈 회전의 y 자리는 sin 45° = 0.71 입니다.
각도가 아니라 반각의 sin 을 축에 곱한 값이라서, 이 숫자를 각도처럼 읽거나 고치면 안 됩니다.
왜 절반인지는 네 숫자끼리 곱하는 규칙이 그렇게 맞춰져 있어서인데, 쓰는 사람이 이 계산을 할 일은 없습니다. 이 절에서 가져갈 것은 이 숫자는 각도가 아니다 하나입니다.
Inspector 의 Rotation 칸처럼 회전을 “z 로 몇 도, x 로 몇 도, y 로 몇 도” 세 숫자로 적는 방법을 오일러 각이라 합니다. 오일러 각은 세 축으로 차례로 돌기 때문에, 가운데 x 를 90° 숙이면 첫 번째 z 회전과 마지막 y 회전이 같은 쪽으로 돌아 회전 방향 하나를 잃습니다. 이것이 짐벌 락입니다. 쿼터니언은 축 하나로 한 번에 도는 회전 하나라서, 서로 겹쳐질 다른 축이 없습니다. 두 회전을 이어도 곱한 결과가 다시 축 하나 각도 하나인 회전 하나가 됩니다.
| 오일러 각 | 쿼터니언 | |
|---|---|---|
| 숫자 | 셋. 도 단위라 사람이 읽기 쉽습니다 | 넷. 사람이 읽고 고치기 어렵습니다 |
| 짐벌 락 | x 가 ±90 일 때 생깁니다 | 없습니다 |
| 같은 자세를 쓰는 숫자 | 끝없이 많습니다 | 둘뿐입니다 (q 와 −q, 아래 「하지 말 것」) |
| 두 자세 사이 잇기 | 숫자로 섞으면 먼 길로 돌 수 있습니다 | Slerp 가 짧은 길로 잇습니다 |
| 한 바퀴 넘게 감기 | 720° 처럼 적을 수 있습니다 | 자세만 담아서 두 바퀴와 0 바퀴가 같습니다 |
그래서 유니티는 회전을 쿼터니언으로 저장하고, Inspector 에서만 오일러 각으로 보여 줍니다.
네 숫자는 늘 x² + y² + z² + w² = 1 을 지킵니다. new Quaternion(0, 90, 0, 0) 처럼 각도를 넣으면 이 약속이 깨진 엉뚱한 값이 되고, y 로 90° 도는 회전도 아닙니다.
만들 때는 함수를 쓴다
| 함수 | 만드는 회전 | 예 |
|---|---|---|
Quaternion.identity |
안 돌림 | 처음 자세 |
Quaternion.Euler(x, y, z) |
오일러 각(도) | Quaternion.Euler(0, 90, 0) |
Quaternion.AngleAxis(각도, 축) |
축으로 각도만큼 | Quaternion.AngleAxis(30, Vector3.up) |
Quaternion.LookRotation(앞, 위) |
앞이 그 방향을 보게 | 적을 바라보는 포탑 |
Quaternion.FromToRotation(a, b) |
a 방향을 b 방향으로 돌리는 회전 | 발을 경사면 법선(면에 수직으로 선 방향)에 맞추기 |
포탑이 적을 바라보게 하는 코드입니다.
Vector3 toEnemy = enemy.position - turret.position; // 도착 − 출발
toEnemy.y = 0f; // 위아래로는 숙이지 않는다
// 앞이 toEnemy 를 보고, 위가 월드 위를 보는 회전
turret.rotation = Quaternion.LookRotation(toEnemy, Vector3.up);
LookRotation 은 방향의 길이를 따지지 않으니 정규화하지 않아도 됩니다. 다만 방향이 (0, 0, 0) 이면 바라볼 곳이 없어 회전을 만들 수 없으니, 적과 겹쳤을 때는 부르지 않습니다.
q * v 는 벡터를 돌린다
쿼터니언에 벡터를 곱하면 그 벡터를 그만큼 돌린 벡터가 나옵니다. 순서는 늘 회전 * 벡터 이고, 벡터 * 회전 은 컴파일되지 않습니다.
| 코드 | 결과 |
|---|---|
Quaternion.Euler(0, 90, 0) * Vector3.forward |
(1, 0, 0). 앞을 오른쪽으로 90° |
Quaternion.Euler(0, 180, 0) * Vector3.forward |
(0, 0, −1). 뒤 |
transform.rotation * Vector3.forward |
transform.forward 와 같음 |
transform.forward 는 원래 이 곱으로 만든 값입니다. 오브젝트의 회전으로 월드 앞 (0, 0, 1) 을 돌린 것이 오브젝트의 앞입니다.
총알을 부채꼴로 퍼뜨리는 것도 이 곱 한 번입니다.
// 정면을 기준으로 -30, -15, 0, 15, 30 도
for (int i = -2; i <= 2; i++)
{
Quaternion spread = Quaternion.AngleAxis(i * 15f, Vector3.up);
Vector3 dir = spread * transform.forward;
Fire(dir);
}
합치기는 곱이고, 순서가 중요하다
두 회전을 이어서 하고 싶으면 쿼터니언끼리 곱합니다. a * b 를 벡터에 곱하면 b 를 먼저, a 를 나중에 한 것과 같습니다.
그리고 회전은 순서를 바꾸면 결과가 다릅니다. 아래는 y 로 90° 와 x 로 90° 를 순서만 바꾼 두 경우입니다. 둘 다 월드 축, 곧 장면 전체에 고정된 x(오른쪽) · y(위) · z(앞)를 기준으로 돕니다.
파란 화살표가 오브젝트의 앞, 청록 화살표가 오브젝트의 위입니다.
코드를 보면 먼저 한 회전이 오른쪽에 있습니다. Qx * Qy 가 “y 먼저, x 나중” 입니다.
(Qx * Qy) * v 는 Qx * (Qy * v) 와 같기 때문입니다. 괄호 안의 Qy 가 먼저 v 를 돌리고, 그 결과를 Qx 가 다시 돌립니다. 벡터와 가까운 쪽부터 적용되는 셈입니다.
이 순서가 실제로 쓰이는 곳은 지금 회전에 조금 더 돌릴 때입니다. 이때 월드 축과 짝이 되는 말이 자기 축입니다. 물체에 붙어 함께 도는 축, 곧 물체의 오른쪽 · 위 · 앞입니다. 오른쪽으로 돌아선 오브젝트의 앞은 월드 x 쪽을 가리킵니다.
| 코드 | 어느 축 기준으로 더 도나 |
|---|---|
transform.rotation = transform.rotation * q |
자기 축. 내 오른쪽 축으로 숙이기, 내 위 축으로 돌기 |
transform.rotation = q * transform.rotation |
월드 축. 내가 어떻게 기울었든 월드 위 축으로 돌기 |
왜 오른쪽에 붙이면 자기 축이 되는지는 바로 위의 규칙으로 보입니다.
transform.rotation * q 는 q 가 먼저 돕니다. q 는 아직 처음 자세, 곧 오브젝트의 축이 월드 축과 똑같을 때 돕니다.
그다음 지금 회전이 오브젝트를 현재 자세로 옮기면서 q 가 돌던 축도 함께 데려갑니다. 그래서 결과적으로 오브젝트 자기 축으로 돈 것이 됩니다.
| 단계 | y 로 90° 돌아 오른쪽을 보는 오브젝트에 rotation * Euler(30, 0, 0) |
|---|---|
| 1. q 먼저 | 처음 자세에서 월드 x 로 30° 숙임 |
| 2. 지금 회전 | 숙인 채로 y 로 90° 돌아 오른쪽을 봄 |
| 결과 | 오브젝트의 오른쪽 축(월드 −z)으로 30° 숙인 자세 |
반대로 q * transform.rotation 은 지금 자세가 다 된 뒤에 q 를 하므로 월드 축 그대로 돕니다.
비행기가 자기 날개 축으로 기수를 들게 하려면 앞의 것, 기울어진 오브젝트를 세상 위쪽을 기준으로 빙 돌리려면 뒤의 것입니다.
유니티 문서는 a * b 를 “a 를 먼저 하고, a 가 만든 자기 축을 기준으로 b 를 한다” 고 설명합니다. 같은 결과를 자기 축 쪽에서 읽은 말이라, 위의 표와 어긋나지 않습니다.
transform.Rotate(x, y, z) 는 기본이 자기 축이라 앞의 것과 같고, Space.World 를 넘기면 뒤의 것과 같습니다.
두 회전 사이를 잇는다
포탑이 적을 향해 천천히 돌게 하려면 지금 회전에서 목표 회전으로 조금씩 가야 합니다.
여기서 쓰는 말이 t 비율입니다. a 에서 b 까지 가는 길 중 얼마나 왔는지를 0 ~ 1 로 적은 것입니다. 0 이면 a, 1 이면 b, 0.5 면 한가운데입니다.
숫자 0 에서 10 까지면 t 0.5 는 5, t 0.25 는 2.5 입니다. 이렇게 섞는 함수가 Mathf.Lerp(a, b, t) 입니다.
회전을 각도 숫자로 섞으면 문제가 생깁니다. y 각도 350° 에서 10° 로 돌고 싶을 때, 숫자로는 340 이나 차이 나지만 실제로는 360 을 넘어가는 쪽으로 20° 만 돌면 됩니다. 아래 그림은 위에서 내려다본 모습입니다.
쿼터니언 함수는 숫자를 섞지 않고 회전 전체를 한 축으로 도는 짧은 길로 잇기 때문에 이런 일이 없습니다.
| 함수 | 하는 일 | 쓰는 곳 |
|---|---|---|
Quaternion.Slerp(a, b, t) |
a 와 b 사이를 t 비율(0 ~ 1)만큼. 가장 짧은 길로 | 시작과 끝, 걸리는 시간이 정해진 연출 |
Quaternion.RotateTowards(a, b, 최대 각도) |
a 에서 b 쪽으로 최대 각도만큼만 | 일정한 속도로 따라 돌기 |
Quaternion.Angle(a, b) |
두 회전 사이의 각도(도) | 다 돌았는지 확인 |
Quaternion target = Quaternion.LookRotation(toEnemy, Vector3.up);
// 1초에 최대 90도씩 목표 쪽으로
float maxStep = 90f * Time.deltaTime;
turret.rotation = Quaternion.RotateTowards(turret.rotation, target, maxStep);
bool aimed = Quaternion.Angle(turret.rotation, target) < 1f;
Time.deltaTime 은 화면에 그림 한 장(프레임)을 그리는 데 걸린 초입니다. 90 에 곱하면 “1초에 90도” 가 됩니다.
Slerp(현재, 목표, 0.1f) 를 매 프레임 부르는 코드는 매 프레임 남은 각도의 10% 씩 가서, 1초에 그림을 많이 그리는 기계(fps 가 높은 기계)일수록 빨리 돕니다. 일정한 빠르기가 필요하면 RotateTowards 를 씁니다.
하지 말 것
- x, y, z, w 를 직접 넣거나 고치지 않습니다. 각도가 아니라 반각의 sin 과 cos 라서, 고치면 의도와 다른 회전이 되거나 길이 1 약속이 깨집니다
eulerAngles를 읽어서 고쳐 다시 넣지 않습니다. 같은 자세를 쓰는 오일러 각 조합이 여럿이라 읽을 때마다 다른 조합이 나올 수 있습니다. 각도를 직접 들고 있다가Quaternion.Euler로 새로 만듭니다- 회전이 같은지 비교할 때는
Quaternion.Angle(a, b) < 작은 값으로 봅니다. 쿼터니언은 q 와 −q 가 같은 회전이라 네 숫자가 달라도 같은 자세일 수 있습니다. y 로 90° 는 (0, 0.71, 0, 0.71) 인데, 360° 를 더 돈 450° 는 반각이 225° 라 (0, −0.71, 0, −0.71) 입니다. 자세는 같고 부호만 모두 뒤집혔습니다 벡터 * 회전은 없습니다. 늘회전 * 벡터입니다
정리
- 쿼터니언은 축 하나와 그 축으로 돈 각도를 (x, y, z, w) 에 담은 회전입니다. 숫자는 반각의 sin 과 cos 라 각도로 읽지 않습니다
- 만들 때는
Euler,AngleAxis,LookRotation,FromToRotation을 씁니다 q * v는 벡터를 돌립니다.transform.forward가rotation * Vector3.forward입니다a * b는 b 먼저, a 나중입니다. 순서를 바꾸면 다른 자세가 됩니다회전 * q는 자기 축 기준,q * 회전은 월드 축 기준으로 더 돌립니다- 두 회전 사이는
Slerp(비율로)와RotateTowards(일정한 각속도로)로 짧은 길을 따라 잇습니다