쿼터니언을 몰라도 쓰는 법
Esc

    쿼터니언을 몰라도 쓰는 법

    –Unity기초

    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(일정한 각속도로)로 짧은 길을 따라 잇습니다