밑바닥부터 구현한 레이 트레이서 제작기
I wrote a Ray Tracer from scratch... in a Year by Jacob Gordiak
1년 동안 맨바닥에서 구현한 레이 트레이서 제작기 전체 기록
이 문서는 개발자 Jacob Gordiak이 약 1년 동안 파이썬에서 시작하여 C++, OpenGL, 컴퓨트 셰이더를 거치며 실시간 레이 트레이서를 직접 구현해 나가는 기술적 여정과 그 과정에서 학습한 핵심 개념들을 상세히 기록하고 있습니다.
1. 레이 트레이싱의 기초와 원리
- 레이 트레이싱의 목표: 빛이 물체에서 반사되는 방식을 시뮬레이션하여 매우 현실적인 이미지를 얻는 것입니다.
- 물리적 실제와 최적화:
- 현실에서는 태양이나 전등 같은 광원이 빛을 방출하고, 물체에 반사된 극히 일부가 인간의 눈에 들어옵니다.
- 하지만 컴퓨터 시뮬레이션에서는 광원에서 나온 모든 빛을 추적하는 것은 비효율적입니다. 대부분의 빛이 눈에 닿지 않기 때문입니다.
- 역방향 추적: 눈(카메라)에서 광선을 쏘아 물체에 맞고 튕겨 나간 뒤 광원에 도달하는지 확인하는 방식이 훨씬 효율적입니다.
- 픽셀과 광선의 관계: 화면의 모든 픽셀은 각자의 광선을 가집니다. 광선이 빨간 큐브에 맞고 태양에 도달하면 해당 픽셀은 빨간색이 되고, 빛에 도달하지 못하면 검은색이 됩니다.
2. 카메라 시스템 구현
- 카메라의 정의: 3D 공간상의 한 점(위치)과 서로 수직인 세 개의 정규화된 벡터(전방, 우측, 상단 벡터)로 구성됩니다.
- 벡터의 기본:
- 벡터는 방향과 시작점을 가진 화살표이며, 3D 공간에서는 X, Y, Z 세 성분으로 표현됩니다.
- 정규화: 벡터의 방향은 유지하면서 길이를 1로 만드는 과정입니다. 피타고라스 정리를 이용해 크기를 구한 뒤 각 성분을 크기로 나눕니다.
- 카메라 회전:
- 사원수(Quaternion) 대신 삼각함수(사인, 코사인)를 이용한 회전 행렬 방식을 선택했습니다.
- 회전 순서: 전형적인 1인칭 카메라를 위해 먼저 Z축(피치)을 회전시킨 후 Y축(요)을 회전시킵니다.
- 이동 순서: 항상 원점(0, 0, 0)에서 회전을 먼저 수행한 후 목표 위치로 카메라를 이동시킵니다.
- 두 가지 카메라 모드:
- 일반 모드: 위를 보고 있어도 전진 키를 누르면 수평으로 이동합니다.
- 비행 시뮬레이터 모드: 바라보는 방향 그대로 이동하며, 롤(Roll) 회전이 가능해 배럴 롤 같은 동작이 가능합니다.
** 3. 광선 생성 및 초기 렌더링**
- 카메라 속성 설정:
- 화면비: 화면의 너비와 높이 비율(16:9, 4:3 등)입니다.
- 시야각(FOV) : 카메라와 화면의 양 끝단을 잇는 각도이며, 초점 거리를 결정합니다.
- 광선 계산: 카메라 위치와 각 픽셀의 3D 좌표를 이용해 방향을 구하고 정규화합니다. 이후 로컬 투 월드 행렬(모델 행렬)을 곱해 카메라의 회전과 위치에 맞게 광선들을 정렬합니다.
- 초기 성능 한계: 파이썬으로 구현했을 때 Full HD 해상도 한 프레임을 뽑는 데 약 18분이 소요되었습니다.
4. C++ 전환 및 GPU 가속(OpenGL)
- C++ 전환: 파이썬의 비효율성을 극복하기 위해 C++로 전환하여 초당 약 3프레임(Full HD 기준)까지 성능을 올렸습니다.
- GPU 활용의 필요성: 수백만 개의 픽셀에 대한 광선 계산은 각자 독립적이므로 병렬 처리에 최적화된 그래픽카드가 필요합니다.
- 셰이더(Shader) 도입 :
- 프래그먼트 셰이더: 화면의 개별 픽셀을 그리는 역할을 하며, 이를 통해 GPU에서 수많은 광선을 동시에 계산합니다.
- 성능 향상: GPU 전환 후 Full HD 화면에서 매우 매끄러운 속도를 구현했습니다.
5. 물체 렌더링 및 광학 시뮬레이션
- 구체(Sphere) 구현 : 구체는 중심점과 반지름으로 정의됩니다. 광선과 구체의 교차 여부를 확인하는 함수를 통해 히트 정보(거리, 충돌 지점, 법선 벡터)를 얻습니다.
- 다중 물체 처리: 장면 내 모든 구체를 루프 돌며 확인하되, 가장 가까운 충돌 지점의 정보를 유지하여 물체가 겹치는 현상을 해결합니다.
- 재질과 반사:
- 정반사: 물 표면처럼 매끄러운 곳에서 빛이 입사각과 동일한 각도로 일정하게 반사됩니다.
- 난반사: 미세한 요철이 있는 거친 표면에서 빛이 무작위 방향으로 흩어지는 현상입니다.
- 색상 원리: 물체가 특정 파장의 빛은 흡수하고 나머지는 반사하는 원리를 이용합니다. (예: 파란 물체는 빨강/초록 파장을 흡수)
- 레이 트레이싱 루프:
- 광선이 물체에 부딪힐 때마다 해당 물체의 색상을 광선 색상에 곱합니다.
- 물체의 방출 강도와 방출 색상이 설정되어 있으면 이를 광원으로 간주하고 밝기 점수를 누적합니다.
- 반사 횟수(바운스)를 늘릴수록 이미지가 사실적으로 변하지만 계산량도 늘어납니다.
- 프레임 누적(Accumulation) : 노이즈를 줄이기 위해 카메라가 정지해 있을 때 여러 프레임의 렌더링 결과를 합쳐 이미지를 부드럽게 만듭니다.
6. 사용자 인터페이스(UI) 구축
- ImGui 라이브러리: C++용 라이브러리를 사용해 실시간 제어판을 제작했습니다.
- 기능: 가변형 뷰포트, 카메라 위치/회전/속도 조절, FOV/화면비 변경, 광선 개수 및 반사 횟수 설정, 성능 측정기(ms 단위) 등을 포함합니다.
** 7. 임의의 형상과 삼각형 렌더링**
- 삼각형 도입: 대부분의 3D 그래픽은 삼각형으로 이루어집니다. 가장 빠른 알고리즘인 Moeller-Trumbore 교차 알고리즘을 적용했습니다.
- 컴퓨트 셰이더(Compute Shader) : 그래픽 파이프라인에 얽매이지 않고 GPU의 연산 능력을 자유롭게 사용하기 위해 프래그먼트 셰이더에서 컴퓨트 셰이더로 구조를 변경했습니다. 이는 코드의 단순화와 성능 향상을 가져왔습니다.
8. 공간 분할 및 가속 구조(BVH)
- 문제점: 수만 개의 삼각형이 있는 장면에서 매 프레임 모든 삼각형과 광선의 충돌을 검사하면 계산량이 수조 단위로 치솟아 렌더링이 불가능해집니다.
- BVH(Bounding Volume Hierarchy) :
- 물체들을 상자(AABB, 축 정렬 경계 상자) 안에 넣고 계층 구조로 관리합니다.
- 광선이 큰 상자에 맞지 않으면 그 안의 수천 개 삼각형 검사를 통째로 생략할 수 있습니다.
- BVH 구축 전략:
- 리프 노드: 실제 삼각형을 포함하는 마지막 노드입니다.
- 비-리프 노드: 하위 노드에 대한 참조만 가집니다.
- 중점 분할: 상자의 가장 긴 축을 기준으로 절반을 나누는 단순한 방식입니다.
- 스택 기반 탐색: 3D 공간에서 광선이 어떤 상자와 충돌하는지 확인하기 위해 '깊이 우선 탐색'과 스택 구조를 사용합니다.
9. 고급 분할 알고리즘 (SAH)
- 표면적 휴리스틱(Surface Area Heuristic) : 단순히 중점을 나누는 것보다 더 효율적인 BVH를 만드는 방법입니다.
- 원리: 각 분할 방식의 '계산 비용'을 수식으로 계산하여 가장 낮은 비용의 분할 지점을 선택합니다.
- 수식 요소: 하위 상자의 표면적 비율(확률), 포함된 개체 수, 탐색 비용 등.
- 최적화(버킷 분할) : 모든 가능한 분할 지점을 검사하면 구축 시간이 너무 오래 걸리므로, 상자를 16개의 버킷으로 나누어 검사하는 방식을 사용하여 구축 속도를 대폭 개선했습니다.
- 성능 결과: SAH를 적용했을 때 일반적인 중앙값 분할보다 약 15 FPS 정도의 성능 향상을 확인했습니다.
10. 시각화 및 최종 결과
- 히트맵 구현: 광선이 삼각형을 찾기까지 수행한 교차 테스트 횟수를 RGB 색상(가시광선 파장 기반)으로 변환하여 성능을 시각화했습니다. 파란색 영역은 연산량이 적은 곳, 붉은색 영역은 연산량이 많은 곳을 나타냅니다.
- 모델 테스트:
- 스탠퍼드 토끼(5,000개 삼각형)
- 스탠퍼드 용(20,000개 삼각형)
- 스폰자(Sponza) 장면: 매우 복잡한 장면으로, 현재 단계에서는 실시간 렌더링이 다소 어렵지만 최적화 가능성을 확인했습니다.