1. Ray Tracing
레이트레이싱은 3D 컴퓨터 그래픽에서 빛과 물체의 물리적 상호작용 방식을 수학적으로 모델링하여 디지털 이미지를 얻어내는 기법을 말합니다. 간단히 말해 현실세계에서 빛이 광원에서 출발하여 물체에서 반사되고 우리 눈에 들어오는 모든 과정을 컴퓨터 상에서 모방한 겁니다. 다만 살짝 다른 점은 현실세계에선 광원에서 출발한 빛이 눈에 도착한다면, 레이트레이싱 기법에선 눈에서 출발한 빛이 광원에 도착한다고 가정한다는 것입니다. 광원에서 나온 수많은 빛 중 극소수만이 눈에 보이기 때문에 컴퓨터에서 계산량을 줄이기 위해 역으로 빛을 추적합니다.
레이트레이싱은 일반적인 3D 게임 등에 쓰이는 레스터라이징 기법과는 다르게 계산량이 상당하지만 현실적인 이미지를 얻을 수 있습니다. 따라서 일반적으로 실시간으로 이미지를 얻어내지 않아도 되는 CGI, VFX 등 분야에 쓰입니다. 하지만 GPU를 통해 레이트레이싱을 가속화하기 위한 하드웨어가 점차 보급됨에 따라서 실시간 레이트레이싱도 가능해지고 있습니다.
레이트레이싱은 앞서 말했듯이 물리적인 빛의 상호작용을 모방하기 때문에 다양한 빛의 효과를 그대로 구현하기 용이합니다. 간단하게는 그림자, 굴절, 반사부터 복잡하게는 스넬 법칙, 모션 블러, 아웃 포커싱 등도 구현할 수 있습니다.
레이트레이싱 기법을 구현하기 위한 방법에도 여러가지가 있습니다. ray casting, ray marching, path tracing 등이 있는데 이번에 만든 프로그램에선 ray marching과 path tracing을 사용했습니다.
2. Optix SDK
Optix SDK는 NVIDIA GPU에서 사용가능한 레이트레이싱 API입니다. 이 API를 사용하면 레이트레이싱에 사용되는 많은 부분에서 하드웨어 가속기능을 사용할 수 있습니다. 대표적으로 삼각형과 광선의 intersection 계산, 효율적인 광 추적을 위한 Acceleration Structure 빌드 등이 있습니다. 또 CUDA와 함께 사용하기 때문에 GPU의 강력한 병렬처리 기능도 편하게 활용할 수 있습니다.
아래는 Optix SDK가 GPU에서 사용하는 프로그램들 입니다. 여기서 프로그램은 간단히 OpenGL이나 Direct X 등의 쉐이더에 해당한다고 생각하면 됩니다. 다만 쉐이더는 glsl, hlsl등의 언어로 작성되는 반면에 Optix SDK의 프로그램은 CUDA를 통해 C나 C++로 작성할 수 있습니다.

그림의 다이어그램을 대략적으로 설명드리겠습니다. 우선 초록색은 사용자가 임의로 수정할 수 없는 프로그램으로 Optix SDK에서 제공됩니다. 화살표는 각 프로그램의 호출관계를 나타냅니다. 아래는 각 프로그램에 대한 설명입니다.
- Ray generation: 가장 먼저 실행되는 프로그램 입니다. 픽셀을 샘플링 하거나 프레임 버퍼에 만들어진 이미지를 쓰는 등의 과정은 이 프로그램에서 구현됩니다.
- Scene traversal: launch 준비 단계에서 미리 빌드해놓은 Acceleration Structure를 통해 물체를 추적합니다.
- Intersection: 프로그램은 광선이 물체에 교차하는지 검사합니다. 검사를 통해 교차하는 정확한 위치, normal, uv coordinate 등을 반환할 수 있습니다. 반만 초록색인 이유는 삼각형, 구, 곡선과 같은 프리미티브는 하드웨어 가속을 지원해서 작성하지 않아도 되기 때문입니다.
- Any-hit: 추적과정 중에 광선이 교차하는 모든 후보 물체에서 실행됩니다. 즉 광선이 직진하면서 꿰뚫는 모든 물체에 대해서 실행된다고 보면 됩니다.
- Closest-hit: 광선이 진행중에 가장 먼저 교차하는 물체에 대해서 실행됩니다. 즉 광선이 직진하면서 가장 먼저 만나는 물체에 대해서 실행됩니다.
- Miss: 진행중인 광선과 어떠한 물체도 교차하지 않을 때 실행됩니다. 배경과 관련된 부분을 다룰 때 사용됩니다. 여기서 배경에 파노라마 사진을 입힌다거나 하늘 배경 등을 만들 수 있습니다.
- 기타: Continuation callable과 Direct callable은 범용으로 사용되는 프로그램입니다. 다만 Direct callable은 다른 프로그램을 호출하진 못합니다.
Optix-SDK에선 위에서 설명한 프로그램말고도 다양한 요소들이 있습니다. 자세한 내용은 생략하고 Optix SDK를 사용하는 애플리케이션의 대략적인 흐름을 아주 간단하게 살펴보겠습니다.
- Optix 콘텍스트 생성: Optix API를 드라이버에서 가져오고 콘텍스트를 만들어 초기화합니다.
- Acelleration Structure 빌드: 광선과 물체의 교차 테스트를 가속화 하기 위한 자료구조를 빌드합니다.
- 모듈 로드: nvcc를 통해 optix-ir로 컴파일 해놓은 바이너리 파일을 OptixModule로 가져옵니다.
- 프로그램 그룹 생성: 가져온 모듈에서 필요에 따라 프로그램 그룹을 만듭니다.
- 파이프라인 생성: 만들어진 프로그램 그룹으로 파이프라인을 만듭니다.
- 쉐이더 바인딩 테이블(SBT) 생성: 물체와 프로그램을 바인딩하기 위한 테이블을 만듭니다.
- optix 런치: 본격적으로 프레임을 렌더링합니다.
3. Mandelbrot set
제 블로그의 단골 주제인 망델브로 집합은 비교적 간단한 점화식으로 정의되는 2차원 프랙탈 집합입니다. 망델브로 집합을 나타내기 위해선 원래 점화식을 무한하게 계산해야 하지만, 컴퓨터상에서 나타내기 위해 점화식을 주어진 반복 횟수에 맞춰서 계산합니다.
아래는 실제로 CUDA를 통해 GPU상에서 돌아가는 망델브로 집합 계산 코드입니다. float형 데이터를 복소수 계산에 맞춰 인라인으로 집어넣었습니다. 좀 더 간단하게 만들기 위해 <complex>의 std::complex<float>를 사용해도 되긴 합니다.
static __forceinline__ __device__ bool mandelbrot(float c_real, float c_imag, uint32_t iter)
{
float z_real = 0.f;
float z_imag = 0.f;
float q = (c_real - 0.25f) * (c_real - 0.25f) + c_imag * c_imag;
if (q * (q + c_real - 0.25f) <= 0.25f * c_imag * c_imag) return true;
if ((c_real + 1.f) * (c_real + 1.f) + c_imag * c_imag <= 0.0625f) return true;
while (iter --> 0) {
float temp_real = z_real * z_real - z_imag * z_imag + c_real;
float temp_imag = 2.f * z_real * z_imag + c_imag;
z_real = temp_real;
z_imag = temp_imag;
if (z_real * z_real + z_imag * z_imag > (float)4.f) return false;
}
return true;
}
C++7~9번 줄에 해당하는 코드는 iteration을 반복하기전에 미리 큰 부분을 필터링하기 위한 코드입니다. 해당 코드가 필터링 하는 영역은 아래와 같습니다.


4. Zulia set
쥘리아 집합도 마찬가지로 2차원 프랙탈 집합이고 점화식도 망델브로 집합과 동일합니다. 다만 다른 점은 망델브로 집합은 C에 대한 집합이지만, 쥘리아 집합은 Z0에 대한 집합입니다. 따라서 C의 값에 따라 다양한 모양을 얻을 수 있습니다.

최신 댓글