TL;DR —
lookAt함수의worldUphint는 “대충 비슷한 방향”을 주는 것이 아닙니다. roll이 없는 카메라 모델에서cross(forward, worldUp)을 정규화한 값은 수학적으로 정확한right벡터와 일치합니다. pitch가 외적 결과의 크기만 스케일링하고 방향은 바꾸지 않기 때문입니다.
Table of contents
Open Table of contents
들어가며
카메라를 만들 때 glm::lookAt(eye, target, worldUp) 같은 함수를 사용합니다. 세 번째 인자 worldUp에는 보통 을 넣는데, 이것이 카메라의 실제 up 벡터가 아니라는 점은 잘 알려져 있습니다. 카메라가 위를 보고 있으면 실제 up은 뒤쪽을 향하겠지만, 여전히 을 넣어도 됩니다. 그래서 이 인자를 up hint라고 부릅니다.
lookAt 내부에서는 이 hint로 right를 먼저 구하고, 그 right에서 실제 up을 다시 계산합니다. 그런데 hint 하나로 구한 right가 왜 정확한 값인지 — “대충 비슷한 방향”이 아니라 수학적으로 맞는 이유가 있는지 궁금했습니다. 이 글에서는 카메라 모델을 정의한 뒤, cross(forward, worldUp)을 정규화한 값이 해당 모델의 정확한 right 벡터와 일치한다는 것을 증명합니다.
이 글에서 다루는 내용:
- roll 없는 카메라 모델에서 “올바른
right”가 무엇인지 정의 cross(forward, worldUp)정규화 결과가 그 정의와 정확히 일치하는 이유를 수식으로 증명- roll이 들어가면 왜 hint만으로 부족한지 정리
- convention이 다른 자료를 참고할 때의 주의사항
사전 지식: 벡터 외적(
cross product), 회전행렬의 기본 개념을 전제로 합니다.
1. 문제 설정 — lookAt의 세 번째 인자는 무엇을 하는가
카메라를 만들 때 보통 이런 함수를 사용합니다:
glm::mat4 view = glm::lookAt(eye, target, worldUp);
세 번째 인자 worldUp은 을 넣는 것이 관례입니다. 이것은 카메라의 실제 up 벡터가 아닙니다. 카메라가 위를 보고 있으면 실제 up은 뒤쪽을 향할 텐데, 여전히 을 넣어도 됩니다.
그래서 이것을 up hint라고 부릅니다. lookAt 내부에서는 이 hint로 right를 구하고, 그 right에서 실제 up을 다시 계산합니다:
right = normalize(cross(forward, worldUp));
up = normalize(cross(right, forward));
질문은 이것입니다: hint 하나로 구한 right가 왜 정확한 값입니까? 대충 비슷한 값이 아니라 수학적으로 정확히 맞는 이유가 있는지 확인해 보겠습니다.
2. 카메라 모델 정의
증명에 앞서 카메라 모델을 고정합니다. 이 글에서 다루는 카메라는 다음과 같습니다:
- 좌표계: 오른손 좌표계(RH)
- 초기 basis:
forward_0 = (0, 0, -1),up_0 = (0, 1, 0),right_0 = (1, 0, 0) - yaw: 월드 축 기준 회전 (위에서 보면 반시계)
- pitch: 현재 카메라
right축 기준 회전 - roll: 없음
yaw와 pitch가 모두 0일 때 카메라는 방향을 바라봅니다.
중요한 점은, 이 값들이 “모든 카메라의 보편적 기본값”이라는 뜻이 아니라는 것입니다. 이 글에서는 yaw와 pitch를 해석할 기준 자세(reference basis) 를 이렇게 정하겠다는 뜻입니다. 뒤에서 나오는 forward, right_true, up 관련 식은 모두 이 기준 자세에서 출발한 같은 카메라 모델의 결과입니다.
3. right_true — 올바른 right는 무엇인가
이 카메라 모델에서 right에 영향을 줄 수 있는 회전은 세 가지입니다: yaw, pitch, roll. 이 중 어떤 것이 실제로 right를 바꾸는지 살펴보겠습니다.
- roll: 없습니다. 모델에서 제외했습니다.
- pitch:
right축 자체를 중심으로 도는 회전입니다. 회전축은 그 회전에 의해 움직이지 않습니다 — 문이 열릴 때 경첩은 제자리에 있는 것과 같습니다. 따라서pitch는right를 바꾸지 않습니다. - yaw: 월드 축 기준 회전입니다.
right는 XZ 평면 위에 있으므로yaw에 의해 방향이 바뀝니다.
결론: 올바른 right는 초기 right에 yaw 회전만 적용한 것입니다. 이것이 이 카메라 모델에서 “실제로 있어야 할 right” — 입니다. 즉 는 임의로 정한 정답이 아니라, 이 모델의 회전 규칙에서 직접 나온 정답입니다.
따라서:
축 기준 회전행렬 는:
초기 right 을 곱하면 첫 번째 열이 그대로 나옵니다:
검산해 보면:
- : — 초기
right그대로입니다 - : — 카메라가 를 바라볼 때
right는 입니다
카메라가 왼쪽(양의 yaw)으로 돌면, right도 같은 방향으로 따라 돕니다. 성분은 항상 0입니다 — yaw는 XZ 평면 위의 회전이라 right가 수평면을 벗어나지 않습니다.
이제 목표가 명확해졌습니다. lookAt이 내부에서 사용하는 cross(forward, worldUp)을 정규화한 값 — — 이 이 와 정확히 같다는 것을 보이면, worldUp hint로 구한 right는 “대충 비슷한 후보”가 아니라 **이 모델에서의 정답 right**라는 결론이 나옵니다.
4. right_hint — forward worldUp에서 복원한 right
이제 같은 카메라 모델에서 forward를 봅니다. 이것도 섹션 2에서 정한 기준 자세에서 yaw와 pitch를 적용해 얻은 벡터입니다. 즉, 아래 식의 forward와 섹션 3의 right_true는 서로 다른 출처의 값이 아니라 같은 모델에서 나온 두 결과입니다.
같은 카메라 모델에서 유도한 forward는:
여기에 을 외적하면:
외적을 전개하면:
세 성분 모두에 가 공통 계수로 붙어 있습니다.
5. 왜 같은가 — pitch는 방향을 바꾸지 않습니다
외적 결과를 다시 보겠습니다:
이 벡터에서 를 묶어내면:
를 빼면 남는 는 섹션 3에서 구한 와 정확히 같은 방향입니다. 즉 pitch 연산의 결과는 외적 벡터의 방향 자체에는 영향을 주지 않고, 만큼 크기를 스케일링한 것입니다.
왜 가 붙는지 살펴보겠습니다. forward에 pitch를 적용하면 벡터가 위/아래로 기울어지면서 XZ 수평 성분의 길이가 만큼 줄어듭니다. 는 사이의 값이므로 사실상 축소 스케일링이고, 그 축소가 외적 결과에 그대로 전달된 것입니다.
right는 방향 벡터입니다. 정규화하면 이 축소 스케일링이 사라져 순수하게 방향만 남습니다:
즉 cross(forward, worldUp)으로 얻은 값은 “hint에서 대충 만든 right”가 아니라, 이 카메라 모델에서 실제로 있어야 하는 right와 정확히 같은 방향입니다. 그래서 이 값을 정규화해서 카메라의 right로 사용해도 됩니다.
정리하면:
pitch는right축 자체를 중심으로 도는 회전이라right의 방향을 바꾸지 않습니다forward의 XZ 수평 성분이 만큼 축소되고, 외적 결과에 그 축소가 공통 계수로 전달됩니다right는 방향 벡터이므로 정규화하면 가 제거되어,yaw만 반영된 와 정확히 일치합니다
따라서 roll이 없고, worldUp이 으로 고정되어 있으면, cross(forward, worldUp)을 정규화한 것만으로 실제 right를 정확히 복원할 수 있습니다. 이것은 근사값이 아니라 수학적으로 정확한 값입니다.
여기서 는 편의상 붙인 이름이 아닙니다. roll이 없는 이 카메라 모델에서, 현재 yaw/pitch 상태의 카메라가 실제로 가져야 하는 올바른 right 방향입니다. 그리고 normalize(cross(forward, worldUp))이 그 와 정확히 같다는 것을 보였으므로, worldUp hint로 구한 right는 “대충 맞는 후보”가 아니라 **이 모델에서의 정답 right**입니다. 따라서 이 값을 그대로 카메라 basis의 right로 사용해도 됩니다.
단, 이 결론은
forward가worldUp과 평행하지 않은 구간, 즉 범위에서 성립합니다. 이면forward가 이 되어cross(forward, worldUp) = \mathbf{0}$이므로right`를 복원할 수 없습니다.
6. roll이 들어가면 — hint만으로는 부족합니다
roll이 없으면 같은 forward에 대해 카메라 자세가 하나로 정해집니다. “세계의 위쪽은 이고, 카메라는 그 기준을 유지한다”는 제약이 자유도를 잡아주기 때문입니다.
하지만 roll이 생기면 상황이 달라집니다. 같은 forward를 보더라도:
- 화면이 똑바른 자세
- 화면이 30도 기울어진 자세
- 화면이 90도 돌아간 자세
가 모두 가능합니다.
roll은 forward축 주위의 비틀림 정보인데, worldUp hint 방식은 이 정보를 저장하지 않습니다. cross(forward, worldUp)은 항상 “세계의 위쪽 기준으로 수평인 right”를 만들어버리기 때문에, roll이 적용된 카메라의 기울어진 right를 복원할 수 없습니다.
| 조건 | right 복원 방법 |
|---|---|
| roll 없음 | cross(forward, worldUp) hint로 정확히 복원 가능 |
| roll 있음 | orientation 전체(회전행렬 또는 quaternion)를 직접 보존해야 함 |
7. 주의사항 — convention의 함정
카메라 구현 과정에서 가장 혼란스러웠던 것은 LearnOpenGL 등에서 흔히 보는 forward 공식이었습니다:
forward.x = cos(pitch) * cos(yaw)
forward.y = sin(pitch)
forward.z = cos(pitch) * sin(yaw)
이 식은 yaw = 0일 때 를 기준으로 하는 heading 식입니다. 이 글에서 사용하는 RH + forward convention과는 기준축과 양의 yaw 방향이 다릅니다.
이것을 그대로 가져오면 카메라가 엉뚱한 방향을 보거나, 부호를 하나하나 뒤집다가 더 꼬이게 됩니다. 해결은 외워서 쓰는 공식을 버리고, 카메라 모델을 먼저 정의한 뒤 그 정의에서 forward를 유도하는 것이었습니다:
- 초기 basis를 정합니다:
forward_0 = (0, 0, -1),right_0 = (1, 0, 0) yaw/pitch회전 규칙을 정의합니다- 그 정의에서
forward/right/up을 유도합니다
이렇게 하면 convention이 다른 자료를 참고하더라도 기준이 흔들리지 않습니다.
정리하며
lookAt의worldUphint는 “대충 비슷한 방향”이 아니라, roll 없는 카메라 모델에서 수학적으로 정확한right를 복원합니다pitch는right축 중심 회전이므로right의 방향을 바꾸지 않고, 외적 결과에 스케일링만 추가합니다. 정규화하면 이 스케일링이 제거됩니다- 는 근사가 아니라 정확한 등식입니다 ( 범위에서)
- roll이 들어가면
worldUphint만으로는right를 복원할 수 없으며, 회전행렬이나quaternion으로 orientation 전체를 보존해야 합니다 - convention이 다른 자료의 공식을 가져오면 혼란이 생깁니다. 카메라 모델을 먼저 정의하고, 그 정의에서 유도하는 것이 안전합니다
참고 자료
이 게시물은 학습한 내용을 바탕으로 초안을 작성한 뒤, LLM의 도움을 받아 내용을 검수하고 다듬어 완성되었습니다.