게임 프로그래밍용 수학 라이브러리 직접 제작 경험

profile_image
작성자 도구개발자 시온
댓글 0건 조회 5회

캐릭터가 벽을 통과하는 문제를 고쳤더니 카메라가 떨리고, 행렬 연산을 최적화했더니 에디터의 좌표축이 뒤집혔습니다. 게임 프로젝트에서 수학 코드는 짧아 보여도 파급 범위가 넓습니다. 저는 벡터와 행렬, 쿼터니언을 직접 구현한 수학 라이브러리를 실제 프로토타입과 소형 엔진에 적용하면서 직접 제작의 장점과 예상보다 비싼 유지 비용을 함께 경험했습니다.

직접 만들기로 결정한 순간의 판단

기존 라이브러리가 부족해서 시작한 것은 아니었다

처음에는 외부 라이브러리가 느리다고 생각했습니다. 하지만 프로파일링 결과 병목은 연산 속도보다 자료형 변환과 좌표계 혼용에서 더 자주 발생했습니다. 렌더러는 오른손 좌표계를 가정하고 물리 모듈은 다른 축을 위쪽으로 사용했으며, 게임플레이 코드에서는 각도를 도와 라디안으로 번갈아 전달하고 있었습니다. 직접 만든 이유는 성능보다 프로젝트 전체의 수학 규칙을 한곳에 고정하기 위해서였습니다.

첫 구현 범위는 Vector2, Vector3, Vector4, Matrix4, Quaternion으로 제한했습니다. 충돌 도형과 곡선, 난수, 노이즈까지 한 번에 넣으려 하자 테스트할 조합이 급격히 늘어났기 때문입니다. 개발자의 개인 프로젝트라 해도 시간은 예산입니다. 계획예산 제도의 개념처럼 목표와 투입 자원을 연결해 생각하니, 필요한 기능과 갖고 싶은 기능을 구분하기가 쉬웠습니다.

  • 도입 목적: 좌표계와 단위 규칙의 통일
  • 초기 범위: 게임 루프에서 매 프레임 사용하는 기본 자료형
  • 보류 항목: 복소수, 범용 선형대수 분해, 고차원 벡터
  • 중단 기준: 테스트 없이 기능 개수만 늘어나는 시점

벡터 API를 사용하며 체감한 장단점

짧은 코드보다 오해하기 어려운 이름이 유용했다

직접 사용해 보니 가장 만족스러운 부분은 함수 이름을 게임 개발 문맥에 맞출 수 있다는 점이었습니다. normalize 하나만 제공하지 않고 normalized와 normalizeInPlace를 분리했으며, length와 lengthSquared도 명확히 노출했습니다. 호출자가 원본이 바뀌는지 추측하지 않아도 되니 리뷰 시간이 짧아졌고, 불필요한 제곱근을 피해야 하는 거리 비교도 자연스러워졌습니다.

반면 연산자 오버로딩은 예상보다 논쟁이 많았습니다. 벡터끼리 곱할 때 성분별 곱인지 내적인지 코드만 보고 판단하기 어려웠고, 스칼라와 벡터의 곱셈 순서까지 지원하면 구현과 테스트가 늘었습니다. 결국 곱셈 연산자는 스칼라 배율에만 사용하고 내적은 dot, 외적은 cross라는 이름을 강제했습니다. 한 줄을 짧게 만드는 편의보다 의미가 한 번에 읽히는 API가 오래 살아남았습니다.

  1. 반환형이 새 값인지 참조인지 이름과 문서에서 밝힙니다.
  2. 거리 비교에는 distanceSquared를 우선 사용합니다.
  3. 영벡터 정규화의 결과와 오류 처리 방식을 고정합니다.
  4. 근사 비교는 연산자 대신 nearlyEqual로 분리합니다.
  5. 암시적 형 변환은 디버깅 비용이 커서 최소화합니다.
사용 팁: 수학 API를 설계할 때는 가장 영리한 표현보다 팀원이 디버거에서 즉시 이해할 수 있는 표현을 선택하는 편이 좋았습니다.

행렬과 쿼터니언에서 실제로 겪은 실패

공식보다 저장 방식과 곱셈 순서가 문제였다

가장 오래 붙잡힌 버그는 행렬 공식이 틀려서가 아니었습니다. CPU 코드는 행 우선으로 읽고 셰이더는 열 우선처럼 사용했는데, 업로드 단계에서 전치가 한 번 더 적용되고 있었습니다. 단위 행렬이나 균일 크기 변환에서는 정상처럼 보여 발견이 늦었습니다. 비균일 크기 조절과 회전을 함께 적용한 테스트 장면에서야 노멀 방향이 틀어진 이유를 찾았습니다.

쿼터니언도 비슷했습니다. fromEuler 함수가 받는 회전 순서를 문서화하지 않아 카메라와 애니메이션이 서로 다른 결과를 냈습니다. 저는 이후 함수 이름에 축 순서를 드러내고, 게임플레이 계층에서는 가능한 한 lookRotation이나 axisAngle처럼 의도가 분명한 생성 함수를 사용했습니다. 보간에서는 두 쿼터니언의 내적이 음수일 때 한쪽 부호를 뒤집지 않으면 긴 회전 경로를 택할 수 있다는 점도 실제 캐릭터 회전에서 확인했습니다.

  • 행렬 테스트: 이동·회전·비균일 크기를 동시에 적용
  • 역행렬 테스트: 원래 행렬과 곱해 단위 행렬에 가까운지 확인
  • 쿼터니언 테스트: 같은 방향을 나타내는 q와 -q를 함께 검사
  • 변환 순서: 월드·뷰·투영 행렬의 곱셈 규칙을 코드 옆에 기록
  • 셰이더 경계: 업로드 직전 값과 GPU에서 해석되는 배열을 비교

테스트 장면이 수학 공식보다 효과적이었던 이유

숫자가 맞아도 화면에서는 틀릴 수 있다

초기에는 단위 테스트만 통과하면 충분하다고 봤습니다. dot product와 cross product, 역행렬 결과를 알려진 값과 비교했지만 실제 게임에서는 계속 문제가 나왔습니다. 원인은 수학 함수 하나가 아니라 여러 모듈이 연결되는 경계에 있었습니다. 그래서 원점, 축, 카메라 절두체, 바운딩 박스, 표면 노멀을 한 화면에 표시하는 작은 테스트 장면을 만들었습니다.

이 장면은 화려하지 않지만 수학 라이브러리의 사용 후기를 바꾼 도구였습니다. 빨간색·초록색·파란색 축이 항상 같은 방향을 가리키는지, 카메라가 물체 뒤로 갔을 때 컬링 결과가 맞는지, 부모 오브젝트에 음수 스케일을 주면 자식의 방향이 어떻게 변하는지 바로 확인할 수 있었습니다. 게임 기술 발표가 집중되는 GDC의 의미와 성격을 살펴보며 발표 자료의 데모 장면을 참고한 것도, 재현 가능한 시각 테스트를 구성하는 데 도움이 됐습니다.

  1. 원점에 축과 격자를 표시하고 좌표계 방향을 확인합니다.
  2. 비대칭 물체를 배치해 좌우 반전과 회전 오류를 드러냅니다.
  3. 부모·자식 변환을 세 단계 이상 연결합니다.
  4. 카메라 시야각과 종횡비를 극단적으로 바꿉니다.
  5. 실패 장면은 스크린샷과 입력값을 함께 회귀 테스트로 남깁니다.
수치 테스트는 계산이 맞는지 알려주고, 시각 테스트는 그 계산을 게임이 같은 뜻으로 사용하고 있는지 알려줬습니다.

최적화 전후에 달라진 실제 사용감

SIMD보다 임시 객체 제거가 먼저였다

수학 라이브러리를 만들면 곧바로 SIMD 최적화를 떠올리기 쉽습니다. 저도 벡터 자료형의 정렬과 명령어 활용부터 손댔지만, 첫 프로파일링에서 더 큰 비용은 임시 객체와 반복적인 행렬 생성이었습니다. 매 프레임 모든 오브젝트가 월드 행렬을 다시 만들고 있었고, 카메라의 뷰·투영 행렬도 값이 바뀌지 않았는데 계속 계산됐습니다. dirty flag와 캐시를 적용하자 복잡한 저수준 최적화 없이도 눈에 띄는 개선이 나왔습니다.

SIMD는 대량 데이터를 같은 방식으로 처리할 때 효과가 컸습니다. 반면 게임플레이 코드에서 벡터 몇 개를 산발적으로 계산하는 구간은 체감 차이가 작았습니다. 자료 정렬 조건이 까다로워지고 디버거 표시가 불편해지는 단점도 있었습니다. 제 경험에서는 일반 구현을 기준점으로 유지하고, 입자나 스키닝 보조 계산처럼 반복량이 확인된 경로만 별도 최적화하는 방식이 가장 관리하기 편했습니다.

  • 효과가 컸던 작업: 변환 캐시, 불필요한 정규화 제거, 제곱 거리 비교
  • 조건부 효과: 구조체 배열을 연속 데이터로 바꾸는 작업
  • 효과가 작았던 작업: 호출 횟수가 적은 함수의 강제 인라인
  • 주의할 점: 정렬 크기 변경에 따른 직렬화 호환성
  • 측정 방식: 디버그 빌드가 아닌 실제 배포 설정에서 반복 측정

팀 프로젝트에 넣으며 바꾼 운영 방식

수학 규칙도 협업 문서가 필요했다

혼자 쓸 때는 머릿속 규칙으로 충분했지만 다른 개발자가 참여하자 질문이 쏟아졌습니다. 전방 축은 어디인지, 각도 입력은 라디안인지, 행렬 곱은 어느 쪽부터 적용되는지, nearlyEqual의 허용 오차는 왜 그 값인지가 대표적이었습니다. 이를 함수 주석에 흩어 놓으니 찾기 어려워 한 페이지짜리 수학 규약과 실행 가능한 예제 코드를 함께 만들었습니다.

특히 기획 도구와 엔진의 경계가 중요했습니다. 회전값을 도 단위로 입력하는 레벨 편집기와 라디안을 사용하는 런타임 사이에서 변환 위치를 하나로 제한해야 했습니다. 기획자의 역할에 관한 설명처럼 게임 제작은 여러 직군의 의도를 연결하는 과정입니다. 수학 라이브러리도 프로그래머만의 내부 도구로 보지 않고, 편집기에서 어떤 단위와 용어가 보이는지까지 설계해야 실수가 줄었습니다.

  1. 좌표계, 전방 축, 위쪽 축을 프로젝트 첫 문서에 고정합니다.
  2. 에디터 표시 단위와 런타임 저장 단위를 구분합니다.
  3. 공개 API 변경에는 이전 호출부를 찾을 수 있는 마이그레이션 메모를 남깁니다.
  4. 버그 수정 때 재현 입력과 회귀 테스트를 함께 추가합니다.
  5. 성능 최적화 코드는 일반 구현과 결과를 교차 검증합니다.

외부 라이브러리 대신 계속 직접 써도 될까

자주 받은 질문에 대한 현실적인 답

제가 가장 자주 받은 질문은 “직접 만든 수학 라이브러리를 실제 게임 끝까지 사용해도 되느냐”였습니다. 답은 프로젝트의 목표와 인력에 따라 달랐습니다. 학습용 엔진이나 좌표 규칙을 세밀하게 통제해야 하는 기술 데모에서는 직접 구현이 큰 도움이 됐습니다. 연산 하나가 렌더링과 물리, 애니메이션으로 어떻게 이어지는지 이해할 수 있고, 문제 발생 시 내부를 끝까지 추적할 수 있기 때문입니다.

하지만 출시 일정이 중요한 상용 프로젝트라면 검증된 라이브러리를 채택하고 얇은 래퍼로 프로젝트 규칙을 고정하는 편이 안전할 때가 많습니다. 역행렬의 특이 조건, 부동소수점 오차, 플랫폼별 SIMD, 정렬과 ABI 같은 문제를 모두 책임지는 비용이 큽니다. 직접 제작을 택하더라도 범용 수학 패키지를 목표로 하기보다 게임에 실제로 필요한 작은 코어를 만들고 외부 구현과 결과를 대조하는 방식을 권합니다.

판단이 어렵다면 일주일 동안 핵심 자료형과 테스트 장면만 만들어 실제 기능 하나에 연결해 보세요. 그 기간 안에 팀의 좌표계 혼란이 줄고 디버깅 경로가 명확해진다면 유지할 가치가 있습니다. 반대로 기본 함수 추가 요청이 계속 쌓이고 게임 기능 개발이 멈춘다면, 그 경험을 버리는 것이 아니라 API 래퍼와 테스트 자산으로 전환하면 됩니다. 제가 직접 사용하며 얻은 가장 실용적인 기준은 코드를 소유하는 만족감이 아니라, 게임의 불확실성을 실제로 줄였는가였습니다.

  • 직접 제작이 어울리는 경우: 엔진 학습, 연구형 프로토타입, 특수 좌표 체계
  • 외부 도입이 유리한 경우: 촉박한 출시 일정, 다중 플랫폼, 소규모 유지 인력
  • 절충안: 검증된 구현 위에 단위와 좌표 규칙을 강제하는 래퍼 작성
  • 최종 판단: 라이브러리 개발 시간이 게임 기능의 진행 속도를 계속 늦추는지 측정

게임 프로그래밍용 수학 라이브러리 직접 제작 경험

댓글목록

등록된 댓글이 없습니다.