게임 수학 면접 질문 모범답변 (게임 클라이언트 프로그래머용)
1. 벡터
1. 스칼라와 벡터의 차이는 무엇인가요?
스칼라는 크기만 가지는 값(속력, 질량, 체력)이고, 벡터는 크기와 방향을 함께 가지는 값(속도, 힘, 이동 방향)입니다. 게임에서 위치, 이동, 시선 방향, 법선 등 공간에 관한 거의 모든 것이 벡터로 표현됩니다. 예를 들어 “초속 5m”는 스칼라지만 “앞쪽으로 초속 5m”는 벡터이고, 이동 코드로 쓰면 velocity = direction * speed처럼 방향(정규화된 벡터)과 크기(스칼라)의 곱으로 분해됩니다.
2. 벡터의 크기를 구하는 방법은 무엇인가요?
각 성분의 제곱 합에 루트를 씌웁니다. 3차원이면 |v| = √(x² + y² + z²)로, 피타고라스 정리의 확장입니다. 실무에서 중요한 최적화 포인트는, 거리 비교만 할 때는 제곱근을 생략하고 크기의 제곱끼리 비교하는 것입니다. sqrt는 상대적으로 비싼 연산이고, |a| < |b| ⟺ |a|² < |b|²이므로 사거리 판정 같은 코드는 distSq < rangeSq로 작성하는 것이 관례입니다.
3. 벡터를 정규화한다는 것은 무엇인가요?
방향은 유지하면서 크기를 1로 만드는 것입니다. 벡터를 자신의 크기로 나누면 되고(v / |v|), 결과를 단위 벡터라 부릅니다. 정규화가 필요한 이유는 “순수한 방향”이 필요한 연산이 많기 때문입니다. 내적으로 각도를 구할 때, 이동 방향에 속도를 곱할 때, 법선을 다룰 때 크기가 1이 아니면 결과가 왜곡됩니다. 예를 들어 대각선 이동 시 (1, 0, 1)을 정규화하지 않고 쓰면 크기가 √2라서 대각선만 41% 빨라지는 고전적인 버그가 생깁니다.
4. 영벡터를 정규화하면 어떤 문제가 발생하나요?
크기가 0이므로 0으로 나누기가 발생해, 결과가 NaN 또는 무한대가 됩니다. 문제는 NaN이 연산을 타고 전파된다는 점입니다. NaN이 위치에 한 번 섞이면 캐릭터가 사라지고, 행렬까지 오염되면 화면 전체가 깨지는데, 원인 지점과 증상 지점이 멀어 디버깅이 매우 어렵습니다. 그래서 정규화 전에 크기(또는 크기 제곱)가 epsilon 이하인지 검사하고, 영벡터면 기본 방향을 반환하거나 이전 프레임 방향을 유지하는 방어 코드가 필수입니다. 입력이 없을 때 이동 방향이 (0,0,0)이 되는 경우가 대표적인 발생 지점입니다.
5. 내적을 설명해주세요.
두 벡터를 스칼라 하나로 축약하는 연산으로, a·b = axbx + ayby + azbz이며, 기하학적으로는 a·b = |a||b|cosθ입니다. 직관적으로는 “두 벡터가 얼마나 같은 방향을 향하는가”를 수치화한 것입니다. 같은 방향이면 양수(cos0°=1), 수직이면 0(cos90°=0), 반대 방향이면 음수(cos180°=-1)입니다. 곱셈과 덧셈만으로 계산되는 매우 싼 연산이면서 각도 정보를 담고 있어서, 게임 수학에서 가장 많이 쓰이는 연산입니다.
6. 내적을 게임에서 어디에 사용하나요?
대표적으로 ① 시야각/배후 판정: 캐릭터의 앞 방향과 대상 방향의 내적 부호·크기로 앞뒤와 시야 안팎을 판정합니다. ② 조명 계산: 램버트 조명이 N·L(법선과 광원 방향의 내적) 그 자체입니다. ③ 벡터 투영: 이동 벡터를 경사면 방향으로 분해할 때 사용합니다. ④ 백페이스 컬링: 면 법선과 시선의 내적 부호로 뒷면을 걸러냅니다. ⑤ 거리 계산: 자기 자신과의 내적이 크기의 제곱입니다. 제 엔진에서는 Hi-Z 컬링 전 단계의 시야 판정과, 디퍼드 렌더링의 라이팅 패스에서 내적이 핵심이었습니다.
7. 두 벡터 사이의 각도를 어떻게 구하나요?
내적 공식을 뒤집으면 됩니다. cosθ = (a·b) / (|a||b|)이므로, θ = acos(a·b / (|a||b|))입니다. 두 벡터가 이미 정규화되어 있으면 θ = acos(a·b)로 간단해집니다. 실무 주의점 두 가지가 있습니다. 첫째, 부동소수점 오차로 내적 값이 1.0001처럼 범위를 살짝 벗어나면 acos이 NaN을 반환하므로, clamp(-1, 1)을 먼저 해야 합니다. 둘째, 많은 경우 실제 각도는 필요 없고 cos 값끼리 비교하면 충분합니다. acos은 비싸므로 “60도 이내인가”는 dot >= cos(60°)로 판정하는 것이 정석입니다.
8. 내적으로 앞뒤 판정을 어떻게 하나요?
내 앞 방향 벡터(forward)와, 나에게서 대상으로 향하는 벡터(toTarget = targetPos - myPos)의 내적 부호를 봅니다. 양수면 대상이 내 앞쪽(사잇각 90도 미만), 0이면 정확히 옆, 음수면 뒤쪽입니다. cosθ의 부호가 90도를 경계로 바뀌는 성질을 그대로 쓰는 것이라, 정규화 없이도 부호만으로 판정 가능해 비용이 거의 없습니다. 백어택 판정을 할 때는 반대로 대상의 forward와 toTarget(대상→나)의 내적이 음수인지, 즉 내가 대상의 뒤쪽 반구에 있는지를 봅니다.
9. 내적으로 시야각 판정을 어떻게 하나요?
앞뒤 판정을 임계값 비교로 확장한 것입니다. 시야각이 120도라면 절반인 60도가 시선 기준 허용 각도이므로, forward와 정규화된 toTarget의 내적이 cos(60°) = 0.5 이상이면 시야 안입니다. 즉 dot(forward, normalize(toTarget)) >= cos(halfFOV) 한 줄로 판정됩니다. cos 임계값은 시야각이 바뀌지 않는 한 상수이므로 미리 계산해두면 acos 없이 내적 한 번으로 끝납니다. 실제 AI 시야 시스템에서는 이 각도 판정에 거리 제한과 레이캐스트(장애물 차폐 확인)를 더해 완성합니다.
10. 외적을 설명해주세요.
두 3차원 벡터에서 둘 모두에 수직인 새로운 벡터를 만드는 연산입니다. a×b의 크기는 |a||b|sinθ로 두 벡터가 만드는 평행사변형의 넓이이고, 방향은 좌표계 규약(왼손/오른손)에 따라 정해지는 수직 방향입니다. 내적이 “얼마나 같은 방향인가”라면 외적은 “두 벡터가 만드는 평면의 수직축이 어디인가”를 알려줍니다. 순서를 바꾸면 방향이 반대가 되는(a×b = -(b×a)) 비교환 연산이라는 점이 중요합니다.
11. 외적의 결과가 벡터인 이유는 무엇인가요?
외적이 답하려는 질문 자체가 방향을 요구하기 때문입니다. 두 벡터가 만드는 평면의 수직축은 “어느 쪽으로 수직인가”까지 정해져야 유일하게 결정되는데, 크기(넓이)만으로는 이를 표현할 수 없습니다. 그래서 결과는 크기(평행사변형 넓이)와 방향(평면의 법선)을 함께 가지는 벡터가 됩니다. 이 성질 덕분에 삼각형의 두 변을 외적해 면 법선을 얻고, forward와 up을 외적해 right 축을 얻는 등, “기존 축들로부터 새 축을 만들어내는” 용도로 쓸 수 있습니다.
12. 외적으로 좌우 판정을 어떻게 하나요?
내 forward와 toTarget을 외적한 결과가 위를 향하는지 아래를 향하는지를 봅니다. 구현으로는 cross(forward, toTarget)의 y 성분(또는 up 벡터와의 내적) 부호를 확인하면 됩니다. 왼손 좌표계(DirectX) 기준으로 결과의 y가 양수면 대상이 오른쪽, 음수면 왼쪽입니다. 내적의 앞뒤 판정과 조합하면 대상이 전후좌우 어느 사분면에 있는지 알 수 있어서, 피격 방향 UI 표시, AI의 회전 방향 결정(어느 쪽으로 도는 게 빠른가), 방향별 피격 애니메이션 선택에 사용합니다.
13. 왼손 좌표계와 오른손 좌표계에서 외적 방향은 어떻게 달라지나요?
외적의 성분 계산식 자체는 동일하지만, 그 결과 벡터가 가리키는 방향의 기하학적 해석이 좌표계에 따라 반대가 됩니다. 오른손 좌표계에서는 오른손 법칙(a에서 b로 감아쥘 때 엄지 방향), 왼손 좌표계에서는 왼손 법칙을 따릅니다. 실무에서 이것이 문제가 되는 지점은 툴 간 데이터 이동입니다. DirectX는 왼손(z가 화면 안쪽), OpenGL 계열은 오른손 관례라, 좌표계가 다른 툴에서 익스포트한 모델은 법선이나 와인딩 순서가 뒤집혀 백페이스 컬링이 반대로 되는 문제가 생깁니다. 저도 모델 로더를 만들면서 z축 반전과 인덱스 와인딩 순서 뒤집기를 처리한 경험이 있습니다.
14. 벡터 투영이란 무엇인가요?
한 벡터를 다른 벡터 방향으로 드리운 그림자 성분을 구하는 것입니다. a를 b에 투영하면, b 방향 단위 벡터를 b̂라 할 때 proj = (a·b̂)b̂입니다. 내적 a·b̂가 그림자의 길이(부호 포함)이고, 거기에 방향 b̂를 다시 곱해 벡터로 만든 것입니다. 게임에서는 벡터를 특정 축 성분과 나머지 성분으로 분해할 때 씁니다. 예를 들어 캐릭터 속도를 경사면의 법선 방향과 표면 방향으로 분해해 표면 성분만 남기면 경사면을 따라 미끄러지는 이동이 됩니다.
15. 평면 위로 벡터를 투영하려면 어떻게 하나요?
원본 벡터에서 평면 법선 방향 성분을 빼면 됩니다. 법선을 n(단위 벡터)이라 할 때, projPlane = v - (v·n)n입니다. 벡터 = 법선 성분 + 평면 성분으로 분해되므로, 법선 성분을 제거하면 평면 성분만 남는 원리입니다. 실전 활용의 대표가 벽 슬라이딩입니다. 캐릭터가 벽에 비스듬히 부딪혔을 때 이동 벡터를 벽 평면에 투영하면, 벽을 뚫지도 완전히 멈추지도 않고 벽면을 따라 자연스럽게 미끄러지는 이동이 됩니다. 경사면 이동도 같은 수식입니다.
16. 반사 벡터는 어떻게 구하나요?
입사 벡터 v와 표면 법선 n(단위 벡터)에 대해 r = v - 2(v·n)n입니다. 유도는 투영의 응용입니다. v를 법선 성분 (v·n)n과 표면 성분으로 나누면, 반사는 표면 성분은 그대로 두고 법선 성분만 부호를 뒤집는 것이므로, 법선 성분을 두 번 빼는 것과 같습니다. 총알 도탄, 공 튕기기 같은 물리 반사와, 스페큘러 조명·환경 매핑에서 반사 시선을 구할 때 사용합니다. HLSL에는 reflect() 내장 함수가 있지만, 유도 과정을 그림으로 설명할 수 있어야 하는 단골 화이트보드 문제입니다.
2. 행렬·변환
17. 행렬을 사용하는 이유는 무엇인가요?
여러 변환을 하나의 자료구조로 통일해 표현하고, 곱셈으로 합성할 수 있기 때문입니다. 이동·회전·크기 변환을 각각 다른 방식으로 다루면 코드가 복잡해지지만, 4×4 행렬로 통일하면 어떤 변환이든 “행렬 곱 한 번”이 됩니다. 특히 합성이 강력합니다. S×R×T를 미리 곱해 월드 행렬 하나로 만들어두면, 정점이 수만 개라도 정점당 행렬 곱 한 번으로 끝납니다. GPU가 행렬 연산에 최적화되어 있고, 셰이더 파이프라인 전체(월드→뷰→투영)가 행렬 곱의 연쇄로 설계되어 있다는 점도 이유입니다.
18. 단위 행렬이란 무엇인가요?
대각 성분이 1이고 나머지가 0인 행렬로, 곱해도 아무 변화를 일으키지 않는 항등원입니다(IM = MI = M). 숫자의 1에 해당합니다. 변환 관점에서는 “이동 0, 회전 0, 스케일 1”인 무변환 상태를 의미하므로, 트랜스폼의 초기값으로 쓰이고, 역행렬의 정의(M×M⁻¹ = I)에서 기준이 됩니다. 행렬을 초기화하지 않아 쓰레기 값이 들어간 채 렌더링이 깨지는 버그를 겪어보면, 단위 행렬 초기화의 의미를 체감하게 됩니다.
19. 전치 행렬이란 무엇인가요?
행과 열을 뒤바꾼 행렬입니다(Mᵀ의 i행 j열 = M의 j행 i열). 게임에서 실질적으로 중요한 두 지점이 있습니다. 첫째, 회전 행렬 같은 직교 행렬은 역행렬이 전치와 같아서(R⁻¹ = Rᵀ), 비싼 역행렬 계산을 전치로 대체할 수 있습니다. 둘째, CPU 코드와 셰이더의 행렬 규약(행 우선/열 우선) 차이를 맞출 때 전치가 쓰입니다. DirectXMath는 행 우선, HLSL 기본은 열 우선이라 상수 버퍼에 행렬을 올릴 때 전치해서 보내는 것이 관례이고, 이걸 빼먹으면 화면이 완전히 뒤틀리는 버그의 단골 원인입니다. 법선 변환에 역전치 행렬을 쓰는 것도 관련 주제입니다.
20. 역행렬이란 무엇인가요?
곱했을 때 단위 행렬이 되는 행렬(M×M⁻¹ = I), 즉 변환을 되돌리는 행렬입니다. 이동 (5,0,0)의 역은 이동 (-5,0,0), 2배 스케일의 역은 0.5배 스케일인 것처럼, 역행렬을 곱하면 원래 공간으로 돌아갑니다. 게임에서의 대표 용도는 ① 뷰 행렬(카메라 월드 행렬의 역), ② 월드→로컬 변환(어떤 점이 특정 오브젝트 기준으로 어디인지), ③ 피킹(화면 좌표를 투영·뷰 역행렬로 되짚어 월드 레이 생성)입니다.
21. 역행렬이 존재하지 않는 경우는 언제인가요?
행렬식(determinant)이 0일 때입니다. 기하학적 의미는 변환이 공간을 붕괴시켜(3D→평면·선·점) 정보가 소실됐다는 뜻입니다. 여러 점이 같은 점으로 이동해버리면 어디서 왔는지 되돌릴 수 없으므로 역이 없습니다. 게임에서 실제로 만나는 경우는 스케일에 0이 들어갈 때입니다. 오브젝트를 안 보이게 하려고 스케일을 0으로 두는 꼼수를 쓰면, 그 월드 행렬의 역이 필요한 순간(법선 변환, 로컬 변환) NaN이 터집니다. 그래서 스케일 0 대신 가시성 플래그를 쓰는 것이 안전합니다.
22. 행렬 곱셈의 순서가 중요한 이유는 무엇인가요?
행렬 곱은 교환법칙이 성립하지 않기 때문입니다(AB ≠ BA). 기하학적으로 자명한데, “이동 후 회전”과 “회전 후 이동”은 완전히 다른 결과입니다. 원점에서 회전 후 이동하면 제자리에서 방향만 바꾼 채 이동하지만, 이동 후 회전하면 원점을 중심으로 공전하게 됩니다. 그래서 표준 순서인 S→R→T(크기→회전→이동)를 지키는데, 이는 크기와 회전을 원점 기준으로 적용한 뒤 최종 위치로 보내기 위함입니다. 행 벡터 규약(DirectXMath)에서는 v × S × R × T로 왼쪽부터 적용되고, 열 벡터 규약에서는 반대로 T × R × S × v가 됩니다.
23. 이동, 회전, 크기 변환 행렬을 설명해주세요.
크기 행렬은 대각 성분에 배율(sx, sy, sz)을 놓은 행렬로, 각 축 성분을 그대로 배수합니다. 회전 행렬은 각 축 회전에 대해 sin, cos으로 구성된 직교 행렬이며, 예를 들어 z축 회전은 xy 평면에서 (cosθ, -sinθ / sinθ, cosθ) 형태로 좌표를 돌립니다. 새 기저축들을 행(또는 열)에 배치한 것으로 해석할 수 있습니다. 이동 행렬은 선형 변환이 아니라 3×3으로는 표현이 불가능하고, 4×4 행렬의 마지막 행(행 벡터 규약 기준)에 (tx, ty, tz)를 놓아 동차 좌표의 w=1과 곱해지며 더해지는 방식으로 구현됩니다.
24. 이동 변환을 4×4 행렬로 표현하는 이유는 무엇인가요?
이동은 선형 변환이 아니기 때문입니다. 선형 변환은 원점을 반드시 원점으로 보내는데(M×0 = 0), 이동은 원점도 옮기므로 3×3 행렬 곱(성분들의 배수 합)으로는 상수를 더하는 연산을 만들 수 없습니다. 그래서 차원을 하나 늘려 동차 좌표 (x, y, z, 1)로 확장하면, 4번째 성분 1이 행렬의 이동 성분과 곱해지면서 “덧셈이 곱셈 안에” 들어옵니다. 이 덕분에 이동·회전·크기를 전부 4×4 곱 하나로 통일할 수 있고, 변환 합성과 GPU 파이프라인 설계가 일관해집니다.
25. 동차 좌표계란 무엇인가요?
n차원 좌표에 성분 w를 하나 추가해 (x, y, z, w)로 표현하는 좌표계입니다. 실제 3D 위치는 (x/w, y/w, z/w)에 대응합니다. 도입 목적은 두 가지입니다. 첫째, 이동을 행렬 곱에 포함시켜 모든 아핀 변환을 4×4로 통일하는 것입니다. 둘째, 원근 투영을 가능하게 하는 것입니다. 투영 행렬은 z 값을 w에 복사해두고, 이후 원근 나눗셈(w로 나누기)에서 멀리 있는 것이 작아지는 효과가 만들어집니다. 즉 동차 좌표는 “이동의 행렬화”와 “원근의 수학적 표현”이라는 두 문제를 한 번에 푸는 장치입니다.
26. 점과 방향 벡터의 w값 차이는 무엇인가요?
점은 w=1, 방향 벡터는 w=0으로 둡니다. 차이는 이동 변환의 적용 여부입니다. 변환 행렬을 곱할 때 이동 성분은 w와 곱해져 더해지므로, w=1인 점은 이동이 적용되고 w=0인 방향은 이동이 무시되어 회전·크기만 적용됩니다. 이것이 의미상 정확합니다. “북쪽”이라는 방향은 물체가 어디로 이동하든 변하지 않아야 하기 때문입니다. 법선이나 시선 방향을 변환할 때 실수로 w=1로 곱하면 방향이 위치처럼 밀려버리는 버그가 생기며, DirectXMath에서 XMVector3TransformCoord(점)와 XMVector3TransformNormal(방향)이 구분되어 있는 이유가 이것입니다.
27. 로컬, 월드, 뷰, 투영 공간을 설명해주세요.
정점이 화면에 그려지기까지 거치는 좌표 공간들입니다. 로컬(모델) 공간은 모델링 툴에서 만들어진, 오브젝트 자신의 원점 기준 좌표입니다. 월드 공간은 월드 행렬을 곱해 씬 전체의 공통 기준으로 배치된 좌표입니다. 뷰(카메라) 공간은 뷰 행렬을 곱해 카메라가 원점, 카메라가 보는 방향이 축이 되도록 재해석한 좌표입니다. 투영(클립) 공간은 투영 행렬을 곱해 시야 절두체를 정규화된 상자로 변형한 좌표로, 이후 원근 나눗셈과 클리핑, 뷰포트 변환을 거쳐 화면 픽셀 좌표가 됩니다. 정점 셰이더의 본질이 바로 이 로컬→클립 변환(WVP 곱)입니다.
28. 월드 행렬의 역할은 무엇인가요?
오브젝트를 로컬 공간에서 월드 공간으로 배치하는 행렬로, 그 오브젝트의 크기·회전·위치(SRT)를 담고 있습니다. 같은 모델 데이터(로컬 정점) 하나를 서로 다른 월드 행렬로 여러 번 그리면 여러 위치에 배치되는 것이므로, 월드 행렬이 곧 “인스턴스의 트랜스폼”입니다. 계층 구조에서는 부모의 월드 행렬에 자식의 로컬 행렬을 곱해 자식의 월드 행렬을 만들며(무기가 손을 따라다니는 원리), 씬 그래프 순회가 곧 월드 행렬의 연쇄 곱입니다.
29. 뷰 행렬의 역할은 무엇인가요?
월드 공간의 모든 것을 카메라 기준 좌표로 변환하는 행렬입니다. 변환 후에는 카메라가 원점에 있고 카메라의 시선 방향이 +z(DirectX 왼손 기준)가 되도록 세상 전체가 재배치됩니다. 개념적으로 “카메라를 움직이는 것”과 “세상을 반대로 움직이는 것”은 동일하며, 뷰 행렬은 후자를 수행합니다. 이렇게 기준을 통일해두면 이후의 투영, 클리핑, 조명 계산(뷰 공간 기준일 경우)이 카메라 위치와 무관한 고정된 수식으로 처리될 수 있습니다. 보통 카메라의 위치(eye), 바라볼 지점(at), 위쪽(up)으로 LookAt 행렬을 만들어 사용합니다.
30. 뷰 행렬이 카메라 월드 행렬의 역행렬인 이유는 무엇인가요?
두 행렬이 정확히 반대 방향의 변환이기 때문입니다. 카메라의 월드 행렬은 “카메라 로컬 공간 → 월드 공간”으로 카메라를 세상에 배치하는 변환이고, 뷰 행렬은 “월드 공간 → 카메라 로컬 공간”으로 세상을 카메라 기준으로 가져오는 변환입니다. 방향이 정반대인 변환 쌍이므로 서로 역행렬 관계입니다. 직관적으로도, 카메라가 오른쪽으로 5만큼 이동하면 화면 속 세상은 왼쪽으로 5만큼 이동해 보이는 것과 같습니다. 실무적으로는 카메라 트랜스폼(회전 R, 이동 T)에 대해 뷰 = (RT)⁻¹ = T⁻¹R⁻¹ = T(-pos) × Rᵀ로, 회전의 직교성 덕분에 전치로 싸게 계산할 수 있습니다.
31. 투영 행렬의 역할은 무엇인가요?
3D 뷰 공간을 화면에 투영 가능한 정규화된 클립 공간으로 변환하는 행렬입니다. 카메라가 보는 영역인 절두체(frustum)를, 클리핑하기 쉬운 표준 상자(NDC, DirectX 기준 x,y는 -11, z는 01)로 눌러 담는 변환이라고 할 수 있습니다. 원근 투영의 경우 z 값을 w 성분에 복사해두는 것이 핵심 트릭으로, 파이프라인의 원근 나눗셈 단계에서 모든 성분이 w(=뷰 공간 깊이)로 나눠지며 멀리 있는 것이 작게 보이는 원근감이 만들어집니다. FOV, 종횡비, near/far 값이 이 행렬의 성분을 결정합니다.
32. 직교 투영과 원근 투영의 차이는 무엇인가요?
원근 투영은 거리에 따라 크기가 줄어드는 투영으로, 시야 영역이 절두체(피라미드를 자른 모양)이며 w 나눗셈으로 원근감을 만듭니다. 사람 눈과 같은 방식이라 3D 게임 월드에 사용합니다. 직교 투영은 거리와 무관하게 크기가 유지되는 평행 투영으로, 시야 영역이 직육면체이고 w가 항상 1이라 원근 나눗셈의 효과가 없습니다. UI/HUD 렌더링, 2D 게임, 그리고 디렉셔널 라이트의 섀도우 맵(태양광은 평행광이므로)에 사용합니다. 제 엔진에서도 씬은 원근, UI 패스와 섀도우 맵은 직교 투영으로 분리해 사용했습니다.
33. FOV와 종횡비가 투영에 어떤 영향을 주나요?
FOV(시야각)는 절두체가 벌어지는 각도로, 클수록 넓은 영역이 화면에 담기는 대신 가장자리 왜곡이 커지고(광각), 작을수록 좁은 영역이 확대됩니다(망원). FPS에서 저격 조준경 줌이 FOV를 줄이는 방식으로 구현되는 것이 대표 사례입니다. 종횡비(aspect ratio)는 화면 가로/세로 비율로, 투영 행렬의 x 스케일에 반영되어 화면 비율이 달라져도 오브젝트가 늘어나 보이지 않게 보정합니다. 해상도 변경 시 투영 행렬을 갱신하지 않으면 화면이 옆으로 퍼져 보이는 것이 이 값을 빼먹은 증상입니다. 참고로 FPS에서 와이드 모니터의 수평 FOV 처리는 시야 이점과 직결되는 민감한 설계 사항입니다.
34. Near Plane과 Far Plane은 왜 필요한가요?
깊이를 유한한 범위로 한정해 깊이 버퍼에 매핑하기 위해서입니다. 깊이 버퍼는 유한 정밀도(보통 2432비트)라 0무한대를 담을 수 없으므로, nearfar 구간을 01로 정규화해 기록합니다. near는 0이 될 수 없는데, 원근 나눗셈에서 z=0 근처가 특이점이 되고 깊이 정밀도 배분이 무너지기 때문입니다. 실무 핵심은 깊이 정밀도가 near 쪽에 극도로 몰려 있다는 것입니다. 원근 매핑이 1/z 형태라 near를 조금만 줄여도(예: 1.0→0.01) 먼 거리 정밀도가 급감해 z-fighting이 발생합니다. 그래서 “near는 최대한 밀고, far는 최대한 당겨라”가 원칙이고, 대규모 씬에서는 Reversed-Z 같은 기법으로 정밀도 분포를 개선합니다. Hi-Z 컬링을 구현하면서 깊이 버퍼의 비선형성을 직접 다뤄본 경험이 있어, 이 주제는 실감하며 이해하고 있습니다.
35. 행 벡터와 열 벡터 방식의 차이는 무엇인가요?
벡터를 행렬의 어느 쪽에 곱하느냐의 규약 차이입니다. 행 벡터 방식은 v’ = v × M으로 벡터가 왼쪽에 오고, 변환 합성이 읽는 순서대로(v × S × R × T) 왼쪽에서 오른쪽으로 적용됩니다. 열 벡터 방식은 v’ = M × v로 벡터가 오른쪽에 오고, 합성은 오른쪽부터(T × R × S × v) 적용됩니다. 같은 변환이라도 두 방식의 행렬은 서로 전치 관계입니다. 수학적으로 어느 쪽이 옳은 게 아니라 규약의 문제지만, DirectX/DirectXMath는 행 벡터, OpenGL/GLM은 열 벡터 전통이라, 이를 혼동하면 곱셈 순서와 메모리 레이아웃이 전부 어긋나므로 코드베이스 전체에서 하나로 통일하는 것이 중요합니다.
36. DirectXMath의 행렬 규약을 설명해주세요.
DirectXMath는 행 우선(row-major) 메모리 배치에 행 벡터 규약을 사용합니다. 즉 v’ = v × M이고, 합성은 XMMatrixMultiply(S, R) 순서처럼 적용 순서대로 곱합니다. 이동 성분은 행렬의 4번째 행(r[3])에 위치합니다. 실무에서 가장 중요한 포인트는 HLSL과의 규약 차이입니다. HLSL 상수 버퍼의 행렬은 기본이 열 우선(column-major) 해석이라, CPU에서 만든 행렬을 그대로 올리면 어긋납니다. 그래서 상수 버퍼에 넣기 전에 XMMatrixTranspose로 전치해서 보내고 셰이더에서 mul(v, M)을 쓰거나, 셰이더 쪽에 row_major 지정자를 쓰는 방법 중 하나로 통일합니다. 저는 자체 엔진에서 전치 후 업로드 방식으로 통일했고, Shader Reflection 기반 파라미터 시스템에서 행렬 타입은 자동으로 전치되도록 처리했습니다.
3. 회전
37. 오일러 각의 장단점은 무엇인가요?
세 축의 회전 각도(pitch, yaw, roll)로 회전을 표현하는 방식입니다. 장점은 직관성입니다. 사람이 읽고 쓰기 쉬워 에디터 노출, 디자이너 조정, 간단한 카메라(yaw/pitch만 쓰는 FPS 카메라)에 적합하고, 메모리도 float 3개로 작습니다. 단점은 ① 짐벌락으로 특정 자세에서 자유도를 잃고, ② 회전 적용 순서(XYZ, YXZ 등)에 따라 결과가 달라 규약 혼란이 있으며, ③ 두 자세 사이의 보간이 자연스럽지 않고(각도를 따로 보간하면 이상한 경로로 회전), ④ 같은 자세를 여러 조합으로 표현할 수 있어 모호합니다. 그래서 내부 연산·보간은 쿼터니언으로 하고, 오일러는 입출력 인터페이스로만 쓰는 것이 일반적입니다.
38. 짐벌락이란 무엇인가요?
오일러 각에서 두 회전축이 겹쳐 자유도 하나를 잃는 현상입니다. 오일러 회전은 축별 회전의 순차 적용인데, 중간 축이 ±90도가 되면 첫 번째 축과 세 번째 축이 같은 평면의 회전이 되어버립니다. 예를 들어 pitch가 90도(정확히 위를 봄)일 때 yaw와 roll이 같은 회전이 되어, 어떤 조합으로도 특정 방향 회전을 만들 수 없게 됩니다. 증상으로는 카메라나 본이 특정 자세 근처에서 홱 뒤집히거나 이상한 경로로 회전하는 것으로 나타납니다. 원인이 “순차 적용”이라는 구조 자체에 있으므로 오일러 안에서는 근본 해결이 안 되고, 회전을 하나의 축·각으로 통합 표현하는 쿼터니언으로 해결합니다.
39. 쿼터니언을 사용하는 이유는 무엇인가요?
세 가지가 핵심입니다. ① 짐벌락이 없습니다. 회전을 축별 순차 적용이 아니라 단일 회전(축+각)으로 표현하기 때문입니다. ② 보간이 우수합니다. SLERP로 두 자세 사이를 최단 경로·등속으로 부드럽게 보간할 수 있어 애니메이션 블렌딩에 필수적입니다. ③ 효율적입니다. float 4개로 행렬(16개)보다 작고, 회전 합성(쿼터니언 곱)이 행렬 곱보다 싸며, 정규화로 수치 오차 복구도 간단합니다. 그래서 캐릭터 애니메이션의 본 회전, 회전 상태의 저장·전송·보간은 쿼터니언으로 하고, 최종 렌더링 직전에만 행렬로 변환하는 파이프라인이 표준입니다.
40. 쿼터니언은 어떻게 회전을 표현하나요?
회전축 n과 회전각 θ를 4개의 수로 담습니다. q = (x, y, z, w) = (n·sin(θ/2), cos(θ/2))로, 벡터부가 축 방향에 sin(θ/2)를 곱한 것, 스칼라부 w가 cos(θ/2)입니다. 회전 적용은 v’ = q·v·q⁻¹ 형태의 켤레 곱으로 이루어지고, 회전의 합성은 쿼터니언 곱(q2·q1 = q1 먼저, q2 나중)입니다. 절반각이 들어가는 이유까지 설명하면 좋은데, 켤레 곱에서 q가 두 번 곱해지며 회전이 두 번 적용되는 구조라 절반씩 나눠 담는 것입니다. 이로 인해 q와 -q가 같은 회전을 나타내는 이중 표현(double cover) 성질이 생기고, 이것이 SLERP에서 최단 경로 처리와 연결됩니다.
41. 쿼터니언의 정규화가 필요한 이유는 무엇인가요?
회전을 나타내는 쿼터니언은 크기가 1인 단위 쿼터니언이어야 하는데, 부동소수점 연산 오차가 누적되면 크기가 1에서 벗어나기 때문입니다. 크기가 1이 아닌 쿼터니언으로 회전을 적용하면 순수 회전이 아니라 스케일이 섞인 변환이 되어, 모델이 미세하게 커지거나 찌그러지고 회전 결과도 왜곡됩니다. 매 프레임 회전을 누적하는 코드(카메라, 본 애니메이션)에서는 주기적으로, 또는 곱셈 후마다 정규화해 오차를 리셋하는 것이 안전합니다. 벡터 정규화와 마찬가지로 크기로 나누기만 하면 되므로 복구 비용이 싸다는 것도 쿼터니언의 장점입니다.
42. SLERP와 LERP의 차이는 무엇인가요?
LERP는 두 값을 직선으로 보간하고, SLERP(구면 선형 보간)는 단위 구면 위의 호를 따라 보간합니다. 쿼터니언에 LERP를 쓰면 두 가지 문제가 있습니다. 결과가 단위 구면을 벗어나므로 재정규화가 필요하고(NLERP), 정규화해도 각속도가 일정하지 않아 회전이 중간에서 빨라졌다 느려집니다. SLERP는 sin 함수로 가중치를 계산해 등속 회전을 보장하는 대신 비용이 더 큽니다. 실무 선택 기준은, 두 자세의 각도 차가 작으면(애니메이션 프레임 간 보간) NLERP로도 오차가 미미해 성능상 NLERP를 쓰고, 각도 차가 크거나 등속성이 중요하면 SLERP를 씁니다. 또한 보간 전에 두 쿼터니언의 내적이 음수면 한쪽 부호를 뒤집어 최단 경로로 돌게 하는 처리가 필요합니다.
43. 회전 행렬과 쿼터니언의 장단점은 무엇인가요?
회전 행렬은 GPU 파이프라인이 요구하는 최종 형태이고, 점 변환이 곱 한 번으로 즉시 되며, 이동·크기와의 합성이 자연스럽다는 장점이 있습니다. 단점은 메모리가 크고(9~16 float), 오차 누적 시 직교성이 깨지는데 복구(직교화)가 비싸며, 보간이 사실상 불가능하다는 것입니다. 쿼터니언은 작고(4 float), 합성이 싸고, SLERP 보간이 되고, 정규화로 오차 복구가 간단하다는 장점이 있지만, 점을 직접 변환하기엔 켤레 곱이 필요하고 이동·크기를 담을 수 없습니다. 그래서 역할을 나눕니다. 회전의 저장·보간·네트워크 전송은 쿼터니언, 정점 변환 직전의 최종 조립(SRT 월드 행렬)은 행렬로 하는 하이브리드가 표준 설계입니다.
44. 특정 방향을 바라보도록 회전시키려면 어떻게 하나요?
LookAt 방식으로 직교 기저를 직접 구성하는 것이 기본입니다. forward = normalize(target - position)을 구하고, right = normalize(cross(worldUp, forward)), up = cross(forward, right)로 서로 수직인 세 축을 만든 뒤, 이 축들을 행렬의 기저로 배치하면 회전 행렬이 완성됩니다. 주의할 예외가 forward가 worldUp과 평행할 때(정확히 위/아래를 볼 때)로, 외적이 영벡터가 되므로 대체 up 벡터를 쓰는 방어가 필요합니다. 즉시 바라보는 게 아니라 부드럽게 돌아보게 하려면, 현재 회전과 목표 회전(LookAt 결과)을 쿼터니언으로 변환해 SLERP로 매 프레임 보간합니다. AI가 타겟을 향해 자연스럽게 몸을 트는 연출이 이 조합입니다.
4. 보간·부동소수점
45. 선형 보간 LERP를 설명해주세요.
두 값 a, b 사이를 비율 t로 섞는 연산입니다. lerp(a, b, t) = a + (b - a) × t = (1-t)a + tb로, t=0이면 a, t=1이면 b, t=0.5면 정확히 중간입니다. 스칼라뿐 아니라 벡터, 색상 등 성분별로 적용 가능한 모든 값에 쓸 수 있습니다. 게임에서는 위치 이동 보간, 카메라 추적, UI 페이드, 색상 전환, 애니메이션 블렌딩 가중치 등 “A에서 B로 부드럽게”가 필요한 거의 모든 곳에 쓰이는 가장 기본적인 도구입니다. 네트워크 위치 동기화의 스냅샷 보간도 본질은 두 스냅샷 사이의 LERP입니다.
46. 보간 계수 t가 0~1 범위를 벗어나면 어떻게 되나요?
보간(interpolation)이 아니라 외삽(extrapolation)이 됩니다. t > 1이면 b를 지나쳐 그 너머로, t < 0이면 a의 반대편으로 나아갑니다. 수식 자체는 문제없이 동작하므로, 의도적으로 쓰면 “추세 연장”(네트워크 지연 시 마지막 속도로 위치 추정)이 되지만, 의도치 않게 발생하면 버그입니다. 대표 사례가 프레임 스파이크로 deltaTime이 커져 t = speed × dt가 1을 넘는 경우로, 목표를 지나쳐 진동하거나 튕겨 나갑니다. 그래서 보간 용도라면 t를 saturate/clamp(0, 1)로 잘라주는 방어가 관례이고, 라이브러리에 따라 clamped와 unclamped 버전이 나뉘어 있는 이유입니다.
47. Ease-in, Ease-out은 무엇인가요?
보간의 속도 곡선을 조절하는 이징 기법입니다. LERP는 t가 시간에 비례해 등속으로 움직여 기계적인 느낌을 주는데, t를 곡선 함수에 통과시켜 가속감을 부여합니다. Ease-in은 천천히 시작해 점점 빨라지는 것(t² 등), Ease-out은 빠르게 시작해 천천히 멈추는 것(1-(1-t)² 등), Ease-in-out은 양끝이 부드러운 S자 곡선(smoothstep = 3t²-2t³ 등)입니다. 핵심 구현 아이디어는 lerp(a, b, ease(t))처럼 보간 공식은 그대로 두고 t만 변형한다는 것입니다. UI 팝업, 카메라 연출, 대시의 감속 등 게임의 “손맛”을 만드는 요소이고, 저는 시퀀스 연출 시스템에서 컷 전환과 카메라 이동에 이징 커브를 적용해 사용했습니다.
48. 프레임 독립적인 이동은 어떻게 구현하나요?
이동량을 “프레임당”이 아니라 “시간당”으로 정의하고, 매 프레임 실제 경과 시간(deltaTime)을 곱합니다. position += velocity * deltaTime이 기본형입니다. 프레임당 고정량으로 움직이면 60fps 유저가 30fps 유저보다 2배 빠르게 움직이는, 게임 규칙 자체가 프레임레이트에 종속되는 문제가 생깁니다. 주의할 심화 지점이 있습니다. 지수적 감쇠 형태의 보간(pos = lerp(pos, target, 0.1) 같은 코드)은 dt를 단순 곱해도 프레임 독립이 되지 않아 지수 보정이 필요하고, 물리 시뮬레이션은 가변 dt에서 결과가 달라지므로 고정 타임스텝(fixed timestep)으로 로직을 돌리고 렌더링만 보간하는 구조를 씁니다.
49. position += speed * deltaTime의 의미는 무엇인가요?
“거리 = 속력 × 시간”을 매 프레임 적분하는 코드입니다. speed가 초당 이동량(m/s), deltaTime이 이번 프레임의 경과 시간(초)이므로, 곱하면 이번 프레임 동안 이동해야 할 거리가 되고, 이를 누적하면 프레임레이트와 무관하게 1초 뒤 정확히 speed만큼 이동해 있게 됩니다. 수학적으로는 속도 함수의 수치 적분(오일러 적분)의 한 스텝입니다. 그래서 등속 이동에는 정확하지만, 가속도가 있는 물리에서는 dt가 클수록 오차가 커지는 근사라는 한계도 함께 알아두면 좋습니다. 이것이 물리 엔진이 고정 타임스텝이나 더 정밀한 적분기(세미-임플리시트 오일러, RK4)를 쓰는 이유로 이어집니다.
50. 부동소수점 오차가 발생하는 이유는 무엇인가요?
float가 실수를 유한한 비트(부호 1 + 지수 8 + 가수 23)로 근사 표현하기 때문입니다. 두 종류의 오차가 있습니다. 첫째, 표현 오차입니다. 0.1 같은 값은 이진법으로 무한소수라 정확히 저장할 수 없고 가장 가까운 표현 가능한 값으로 반올림됩니다. 둘째, 연산 오차입니다. 연산 결과가 표현 가능한 값이 아니면 매번 반올림되고, 이것이 누적됩니다. 또한 float는 값이 커질수록 표현 간격이 벌어지는 구조라(정밀도가 상대적), 큰 좌표에서는 작은 이동량이 뭉개지는 문제도 생깁니다. 요약하면 “float는 근사값이며, 모든 연산이 반올림을 동반한다”가 전제이고, 게임 코드는 이 전제 위에서 작성되어야 합니다.
51. 실수를 ==로 비교하면 안 되는 이유는 무엇인가요?
수학적으로 같아야 할 값이 계산 경로에 따라 비트 수준에서 다를 수 있기 때문입니다. 예를 들어 0.1을 10번 더한 값과 1.0은 == 비교에서 false가 나옵니다. 각 덧셈마다 반올림 오차가 누적됐기 때문입니다. 연산 순서, 컴파일러 최적화, 중간 정밀도에 따라서도 결과 비트가 달라질 수 있습니다. 실전 증상으로는 “타이머가 정확히 0이 되면”이나 “위치가 목표와 같으면” 같은 조건이 영원히 참이 되지 않아 로직이 멈추는 버그로 나타납니다. 그래서 실수 비교는 “같은가”가 아니라 “충분히 가까운가”(epsilon 비교)로 해야 하고, 도달 판정은 등호 대신 부등호(거리 < 임계값)로 설계하는 것이 원칙입니다.
52. epsilon 비교는 어떻게 하나요?
기본형은 |a - b| < ε (절대 오차 비교)입니다. 다만 이 방식은 값의 크기에 따라 한계가 있습니다. float는 값이 클수록 표현 간격이 커지므로, 10000 근처에서는 ε=0.0001이 표현 간격보다 작아 비교가 무의미해지고, 아주 작은 값 근처에서는 반대로 너무 관대해집니다. 그래서 값이 큰 경우엔 상대 오차 비교 |a - b| < ε × max(|a|, |b|)를 쓰고, 실무에서는 둘을 결합해 “절대 오차(0 근처 대응) 또는 상대 오차(큰 값 대응) 중 하나라도 만족하면 같다”로 처리합니다. 핵심은 ε이 만능 상수가 아니라 값의 스케일과 용도(위치 판정 vs 정규화 검사)에 맞게 선택해야 하는 값이라는 것입니다.
53. 큰 수와 작은 수를 더할 때 정밀도 문제가 발생하는 이유는 무엇인가요?
float 덧셈은 지수를 큰 쪽에 맞춰 정렬한 뒤 가수를 더하는데, 두 수의 크기 차이가 가수의 표현 범위(float 약 7자리)를 넘으면 작은 수의 비트가 정렬 과정에서 밀려나 사라지기 때문입니다. 극단적으로 1,000,000.0f + 0.0001f는 그냥 1,000,000.0f입니다. 게임에서의 실제 증상이 명확합니다. 원점에서 수십 km 떨어진 오픈월드 좌표에서는 float 간격이 밀리미터~센티미터 단위로 벌어져, 미세한 이동이 씹히고 물체가 떨리는(지터) 현상이 생깁니다. 누적 게임 시간이 커진 뒤 deltaTime을 더하면 시간이 안 흐르는 버그도 같은 원인입니다. 대응책으로 대규모 월드는 원점 이동(origin shifting)이나 청크 로컬 좌표를 쓰고, 시간 누적은 double을 쓰는 것이 관례입니다.
54. 고정소수점은 언제 사용하나요?
정수에 스케일(예: 1/65536)을 약속해 실수를 표현하는 방식으로, 핵심 성질은 연산이 정수 연산이라 어떤 하드웨어·컴파일러에서도 결과가 비트 단위로 동일하다는 결정론입니다. 그래서 ① 결정론이 필수인 곳, 즉 락스텝 동기화 RTS나 리플레이 시스템, 정확한 판정 재현이 필요한 격투 게임의 시뮬레이션 로직에 쓰고, ② 값의 범위가 한정적이고 균일한 정밀도가 필요한 곳(화폐 계산, 네트워크로 보내는 좌표의 양자화)에도 적합합니다. 단점은 표현 범위가 좁아 오버플로에 취약하고, 곱셈·나눗셈 시 스케일 관리가 번거로우며, sqrt·삼각함수 같은 수학 함수를 직접 구현해야 한다는 것입니다. 그래서 “시뮬레이션은 고정소수점, 렌더링·연출은 float”로 나누는 하이브리드가 일반적입니다.
55. 네트워크 게임에서 부동소수점 결정성이 문제가 되는 이유는 무엇인가요?
같은 입력으로 같은 계산을 해도, 머신·컴파일러·최적화 옵션에 따라 float 결과의 비트가 달라질 수 있기 때문입니다. 원인은 x87과 SSE의 중간 정밀도 차이, FMA(fused multiply-add) 사용 여부, 컴파일러의 연산 재배치(fast-math), 라이브러리별 삼각함수 구현 차이 등 다양합니다. 이것이 치명적인 구조가 결정론적 락스텝 동기화입니다. 모든 클라이언트가 입력만 공유하고 시뮬레이션을 각자 돌리는 방식인데, 어느 한 값이 1비트라도 어긋나면 이후 시뮬레이션이 나비효과로 완전히 갈라져 비동기(desync)가 발생합니다. 대응책은 ① 시뮬레이션을 고정소수점으로 작성, ② 컴파일 옵션과 수학 라이브러리를 엄격히 통제, ③ 또는 애초에 락스텝 대신 서버 권위 + 상태 동기화 구조를 택해 서버 계산만이 정답이 되게 하는 것입니다. FPS가 대부분 서버 권위인 이유 중 하나이기도 합니다.