게임 수학 쿼터니언으로 회전을 구현해봤더니 달라진 것
캐릭터가 목표를 바라보게 만들었는데 갑자기 반대 방향으로 빙글 돌거나, 카메라를 위로 올리는 순간 회전축 하나가 사라진 듯 굳어 버린 적이 있나요? 숫자 세 개로 각도를 표현하는 오일러 각은 직관적이지만, 실제 게임 프로그래밍에서 회전을 이어 붙이고 보간할 때는 예상 밖의 문제가 자주 생깁니다.
저도 처음에는 쿼터니언의 x, y, z, w를 외우려 했습니다. 그러나 작은 회전 실험을 직접 구현해봤더니 핵심은 네 숫자의 공식보다 회전을 어떤 방식으로 저장하고 합성하는가에 있었습니다. 이 글은 복잡한 증명보다 캐릭터와 카메라를 움직여 보며 쿼터니언을 이해하고 싶은 초보 개발자를 위한 출발점입니다.
각도 세 개를 쓰다가 쿼터니언이 필요해진 순간
오일러 각은 왜 쉽고도 까다로울까
오일러 각은 보통 X축, Y축, Z축을 중심으로 얼마나 돌았는지를 세 개의 각도로 나타냅니다. 인스펙터에서 회전값을 직접 입력하기 좋고, ‘좌우로 30도 돌린다’처럼 사람이 읽기 쉽다는 장점이 있습니다. 단순한 문 회전이나 한 축만 사용하는 2D 오브젝트라면 오일러 각만으로도 충분합니다.
문제는 여러 축의 회전을 연속해서 적용할 때입니다. 회전은 이동과 달리 적용 순서에 따라 결과가 달라집니다. X축으로 돌린 다음 Y축으로 돌린 결과와 Y축 다음 X축으로 돌린 결과가 같지 않습니다. 여기에 특정 자세에서 두 회전축이 겹치는 짐벌 락까지 발생하면, 카메라가 부자연스럽게 뒤집히거나 조작축 하나를 잃은 것처럼 보일 수 있습니다.
쿼터니언은 회전을 네 성분으로 표현해 이런 합성과 보간을 안정적으로 다루는 도구입니다. 처음부터 모든 수학을 손으로 유도할 필요는 없습니다. 엔진이 제공하는 축-각 변환, 두 방향 사이의 회전, 구면 선형 보간 함수를 먼저 사용하고, 입력과 출력이 무엇인지 관찰하는 편이 훨씬 빠릅니다.
- 오일러 각이 편한 경우: 에디터에서 사람이 각도를 조절하거나 한 축 회전만 제한할 때
- 쿼터니언이 편한 경우: 여러 회전을 합성하고 목표 방향을 바라보며 자세를 부드럽게 보간할 때
- 둘을 함께 쓰는 경우: 사용자 입력은 각도로 받고 내부 계산과 저장은 쿼터니언으로 처리할 때
- 주의할 경우: 매 프레임 쿼터니언을 오일러 각으로 바꿨다가 다시 되돌리는 변환을 반복할 때
x, y, z, w를 각도로 읽지 않는 연습
쿼터니언 q=(x, y, z, w)에서 x, y, z는 각각 오일러 각의 회전량이 아닙니다. 단위 회전축 n과 회전각 θ를 이용하면 벡터 부분은 n·sin(θ/2), 스칼라 부분 w는 cos(θ/2)에 대응합니다. 그래서 인스펙터에 표시된 x가 0.5라고 해서 X축으로 0.5도나 50도 돌았다고 해석하면 안 됩니다.
또한 q와 -q는 같은 공간상의 회전을 나타냅니다. 실행 중 값의 부호가 갑자기 바뀌어도 오브젝트 자세가 동일할 수 있다는 뜻입니다. 숫자 자체의 연속성만 검사하면 정상 동작을 오류로 오해할 수 있으므로, 디버깅할 때는 전방 벡터와 위쪽 벡터가 실제로 어디를 향하는지 함께 그려 보는 것이 좋습니다.
입문 팁: 쿼터니언 네 성분을 사람이 읽는 각도로 해석하려 하지 마세요. ‘이 회전을 적용했을 때 로컬 축이 어디를 향하는가’를 화면의 디버그 선으로 확인하면 이해 속도가 훨씬 빨라집니다.
- 원점에 큐브 하나를 배치하고 현재 전방축을 파란 선으로 표시합니다.
- 월드 Y축을 기준으로 45도 회전하는 쿼터니언을 생성합니다.
- 현재 회전에 새 회전을 곱한 뒤 전방축의 변화를 관찰합니다.
- 곱셈 순서를 바꿔 결과가 어떻게 달라지는지 스크린샷으로 비교합니다.
- 마지막에만 오일러 각으로 변환해 사람이 읽을 수 있는 값과 실제 자세를 대조합니다.
캐릭터가 목표를 바라보게 만들며 회전 감각을 익혔다
방향 벡터에서 목표 회전까지 만드는 순서
가장 좋은 첫 실습은 캐릭터가 마우스 커서나 목표물을 바라보게 만드는 기능입니다. 먼저 목표 위치에서 캐릭터 위치를 빼서 방향 벡터를 구합니다. 바닥 위에서만 도는 캐릭터라면 방향 벡터의 높이 성분을 0으로 만들고 정규화한 다음, 이 방향과 기준 위쪽 축으로 목표 쿼터니언을 생성합니다.
방향 벡터의 길이가 거의 0일 때는 회전을 만들지 않아야 합니다. 캐릭터와 목표가 같은 위치라면 바라볼 방향이 정의되지 않기 때문입니다. 이 예외를 건너뛰지 않으면 NaN이 섞여 이후의 위치, 애니메이션, 물리 계산까지 연쇄적으로 망가질 수 있습니다. 작은 임계값을 두고 그보다 짧은 방향은 이전 회전을 유지하도록 처리하세요.
게임 기능은 기획 의도와 함께 검증해야 합니다. 예를 들어 즉시 목표를 바라보는 포탑과 무게감 있게 몸을 돌리는 보스 캐릭터는 같은 목표 회전을 계산하더라도 전혀 다른 속도 규칙이 필요합니다. 역할 정의가 낯설다면 게임 기획자의 업무 설명을 참고해, 회전 코드에도 플레이 감각을 수치로 합의하는 과정이 필요하다는 점을 함께 살펴볼 수 있습니다.
- 방향 계산: targetPosition - characterPosition으로 목표 방향을 구합니다.
- 평면 제한: 지상 캐릭터라면 Y 성분을 제거해 위아래로 기울지 않게 합니다.
- 영벡터 검사: 길이 제곱이 임계값보다 작으면 현재 자세를 유지합니다.
- 목표 회전 생성: 엔진의 LookRotation 또는 두 벡터 사이 회전 함수를 사용합니다.
- 속도 적용: 현재 회전에서 목표 회전으로 한 프레임에 이동할 수 있는 최대 각도를 제한합니다.
Lerp와 Slerp, RotateTowards를 직접 비교해보니
목표 회전으로 부드럽게 이동할 때 초보자가 가장 자주 만나는 함수는 Lerp, Slerp, RotateTowards입니다. Lerp는 성분을 선형 보간한 뒤 정규화하는 방식으로 구현되는 경우가 많아 비교적 단순하고, 작은 각도에서는 충분히 자연스럽습니다. Slerp는 4차원 단위구의 최단 경로를 따라 보간하므로 큰 회전에서도 각속도가 더 균일하게 느껴집니다.
하지만 매 프레임 Slerp(current, target, speed × deltaTime)를 호출하면 ‘초당 일정 각도’가 되지 않습니다. 남은 거리의 일정 비율만큼 움직이므로 목표에 가까워질수록 느려지는 감속 효과가 생깁니다. 일정한 회전 속도가 필요하다면 초당 각도와 deltaTime을 곱해 최대 이동 각도를 정하는 RotateTowards 방식이 의도에 더 가깝습니다.
어떤 방법이 무조건 우수한 것은 아닙니다. 부드럽게 안착하는 UI 오브젝트나 카메라에는 비율 보간이 어울리고, 초당 120도로 회전해야 하는 포탑이나 네트워크 예측에는 각속도 제한이 이해하기 쉽습니다. 성능을 논하기 전에는 목표 플랫폼에서 프로파일링해야 하며, 여러 개발 사례와 발표 흐름을 찾고 싶다면 GDC의 성격과 배경도 가볍게 확인할 만합니다.
| 방법 | 움직임의 특징 | 잘 맞는 상황 | 초보자 주의점 |
|---|---|---|---|
| Lerp | 가볍고 작은 각도에서 자연스러움 | 짧은 자세 전환, 단순 연출 | 회전값 정규화 여부 확인 |
| Slerp | 구면 최단 경로, 균일한 경로 | 큰 각도의 자세 보간 | t를 속도로 오해하지 않기 |
| RotateTowards | 프레임당 최대 각도 제한 | 포탑, 캐릭터 선회 속도 | 초당 각도에 deltaTime 적용 |
- 30도와 170도 회전을 각각 테스트해 작은 각도와 큰 각도의 차이를 확인합니다.
- 30fps와 120fps에서 같은 시간 후 자세가 일치하는지 비교합니다.
- 목표가 캐릭터 뒤를 지나갈 때 긴 방향으로 회전하지 않는지 관찰합니다.
- 시작과 도착 시점의 감속이 게임 기획 의도에 맞는지 체감으로 평가합니다.
실전 조언: 보간 함수부터 고르지 말고 ‘1초에 몇 도 움직여야 하는가’와 ‘목표 근처에서 감속해야 하는가’를 먼저 문장으로 적으세요. 그 문장이 함수 선택 기준이 됩니다.
10분짜리 회전 놀이터에서 네 가지 질문에 답해보자
구현 전에 많이 막히는 지점을 풀어보면
Q. 쿼터니언을 직접 구현해야 하나요?
첫 게임을 만드는 단계라면 엔진의 검증된 수학 라이브러리를 사용하는 편이 안전합니다. 대신 단위 쿼터니언 생성, 곱셈, 켤레, 벡터 회전 정도를 별도 연습 프로젝트에서 구현해보면 내부 원리를 이해하는 데 도움이 됩니다. 제품 코드에서는 정확성, SIMD 최적화, 플랫폼별 부동소수점 차이까지 고려해야 하므로 학습용 코드와 배포용 코드를 구분하세요.
Q. 회전 곱셈 순서는 어떻게 외우나요?
엔진의 좌표계와 연산 규칙에 따라 표기 해석이 달라질 수 있어 문서와 간단한 축 실험으로 확인하는 것이 가장 정확합니다. ‘월드 축 회전을 먼저 적용하는지, 현재 로컬 축 회전을 추가하는지’를 구체적인 문장으로 적고, 서로 다른 색의 축 선을 그리세요. 순서를 암기하는 것보다 입력 회전 A와 B를 90도로 고정해 결과를 관찰하는 습관이 오래갑니다.
Q. 값이 조금씩 틀어지는 이유는 무엇인가요?
부동소수점 연산을 반복하면 단위 길이에서 조금씩 벗어날 수 있습니다. 라이브러리가 자동 정규화하는지 확인하고, 직접 구현했다면 적절한 시점에 정규화하세요. 다만 매 연산마다 무조건 정규화하면 불필요한 비용이 생길 수 있으므로 테스트와 측정을 통해 주기를 정하는 편이 좋습니다.
- 카메라가 뒤집힘: 전방 방향과 위쪽 기준 벡터가 거의 나란한지 검사합니다.
- 캐릭터가 떨림: 이동 시스템과 회전 시스템이 서로 다른 목표 자세를 덮어쓰는지 확인합니다.
- 반대쪽으로 회전함: 두 쿼터니언의 내적과 최단 경로 처리 여부를 살펴봅니다.
- 프레임률마다 속도가 다름: deltaTime 적용 위치와 고정 업데이트 사용 여부를 점검합니다.
- 리플레이 결과가 어긋남: 입력, 시간 간격, 부동소수점 결정성까지 함께 기록합니다.
작은 실험 프로젝트를 오늘 바로 구성하는 법
Q. 학습용 프로젝트에는 무엇이 필요할까요?
큐브 두 개, 이동 가능한 목표점 하나, 현재 축을 표시할 세 개의 디버그 선이면 충분합니다. 첫 번째 큐브는 즉시 목표를 바라보게 하고, 두 번째 큐브는 초당 90도 제한을 적용하세요. 화면 한쪽에는 현재 회전, 목표와의 남은 각도, 프레임률을 표시하면 숫자와 움직임의 관계가 보입니다.
실험 항목을 한꺼번에 늘리면 무엇이 결과를 바꿨는지 알기 어렵습니다. 작은 개발 프로젝트도 기능별 시간과 실패 비용을 나눠 생각하면 학습 범위를 통제하기 쉬우며, 이런 자원 배분의 기본 관점은 계획예산 제도의 개념처럼 목표와 투입을 연결하는 방식에서도 힌트를 얻을 수 있습니다. 무료 엔진과 기본 디버그 도구만으로 충분하므로 처음부터 유료 플러그인을 구입할 필요는 없습니다.
마지막으로 성공 조건을 눈에 보이게 만드세요. 목표가 정면, 측면, 후면, 머리 위에 있을 때 자세를 각각 기록하고, 프레임률을 바꿔도 같은 시간에 비슷한 방향에 도착하는지 확인합니다. 실패한 장면은 회전값만 복사하지 말고 목표 방향과 각 축을 함께 캡처해야 원인을 다시 찾을 수 있습니다.
- 빈 장면에 큐브 두 개와 드래그 가능한 목표점을 놓습니다.
- 첫 큐브에는 즉시 바라보기, 둘째 큐브에는 초당 90도 회전을 적용합니다.
- 월드 전방·위쪽 축과 각 큐브의 로컬 축을 서로 다른 색으로 그립니다.
- 목표를 큐브 뒤쪽과 바로 위쪽으로 옮겨 특이 자세를 시험합니다.
- 30fps와 120fps로 제한한 뒤 1초 후 남은 각도를 기록합니다.
- Slerp 방식으로 바꾸고 목표 근처에서 속도가 줄어드는지 비교합니다.
지금 당장 할 행동은 간단합니다. 새 장면에 큐브 하나를 놓고 Y축 90도 쿼터니언을 현재 회전의 앞과 뒤에 각각 곱한 두 결과를 화면에 나란히 배치해보세요. 두 큐브의 로컬 전방축을 선으로 그리는 순간, 쿼터니언 곱셈 순서가 공식이 아니라 실제 게임 오브젝트의 움직임으로 기억되기 시작합니다.

- 다음글게임 네트워크, 빠른 서버보다 결정론이 먼저다 26.08.16
등록된 댓글이 없습니다.
