Lightfold 프로젝트를 시작한다. Devlog를 써가면서 좀 더 제대로 정리하는 게 목표다. 일단 원대한 계획은 아주 general한 상황에 대응할 수 있는 PBR 엔진을 만드는 것인데, 성능보다는 확장성에 초점을 뒀다고 보면 될 것 같다. 그래도 성능을 완전히 등한시할 건 아니고, 할 수 있는 데까지는 최적화해볼 생각이다. 이전에는 PBRT의 소스코드를 가져와서 이리저리 뜯어보고 수정해보았는데, 이번에는 아예 처음부터 새로 쓰려고 한다. 그래도 내가 PBRT를 보고 공부한터라 PBRT의 플로우를 많이 따라가게 되기는 할 듯. Wave optics까지는 아직 계획이 없는데, 혹시 나중에 추가할 수도… 혹시 전체 코드를 보고 싶다면 GitHub 에서 확인할 수 있다.
이러니저러니해도 렌더링 엔진이 하는 일은 이미지를 만들어내는 것이다. 그러니까 이미지를 파일로 쓰는 것부터 시작하자. 나중에 이미지를 읽는 기능도 필요하겠지만, 지금은 쓰기만으로 충분하다. 나는 TinyEXR이라는 라이브러리를 사용했다. 공식적인 OpenEXR C++ 라이브러리를 사용할 수도 있겠지만, 무겁기도 하고 CMake 빌드 과정에서 (아마 내 잘못인) 문제가 자꾸 생겨서 그냥 TinyEXR을 사용했다. core/image.h의 WriteEXR 함수로 쉽게 이미지를 쓸 수 있다.
int WriteEXR(std::unique_ptr<RGB[]> rgb, int width, int height, const char* outfilename);
그러면 WriteEXR 함수에 전달될 rgb 배열은 어디서 만들어질까? 바로 카메라의 필름이다. 그런데 필름에서 바로 RGB 신호가 나오지는 않고, 필름에 도달한 빛이 센서에서 RGB 신호로 변환될 것이다. 한 지점에 도달한 빛을 나타내는 클래스 Spectrum을 만들자 (core/spectrum.h). Spectrum의 세부 구현은 렌더하고자 하는 Scene에 따라 달라져야 한다. 지금 생각나는 Spectrum의 구현으로는 간단한 것부터 복잡한 것까지 greyscale<RGB<spectral<spectral with polarization 정도가 있다. 이 구현들 중 무엇을 적용할 것인지는 컴파일 단계에서부터 결정되는 것이 자연스럽다. 일단 임시로 그 세팅을 core/settings.h에 만들어두었다.
#define SPECTRUM GREYSCALE // GREYSCALE, RGBSPEC, SPECTRAL, POLAR #define GREYSCALE 0 #define RGBSPEC 1 #define SPECTRAL 2 #define POLAR 3
일단 가장 간단한 greyscale에 대한 구현만 해두었다. Spectrum 클래스가 해야 할 가장 중요한 일은 데이터를 RGB로 변환해주는 것이다. 이건 getRGB 함수에서 처리한다.
class Spectrum { public: RGB getRGB() const; private: #if SPECTRUM == GREYSCALE Float val; #endif };
이제 Film 클래스를 만들자(core/film.h). 필름이 해야 할 일은 무엇인가? 픽셀을 선택하면 그 픽셀에 들어온 빛을 RGB로 출력하는 것이다. 그런데 지금은 레이트레이싱 구현이 하나도 되어 있지 않기 때문에 픽셀값을 출력할 마땅한 방법이 없다. 그러니 일단 함수 getPixel만 만들어놓고 구현은 나중으로 미룬다.
class Film { public: RGB getPixel() const; };
CG에서는 수학, 특히 기하학 연산을 많이 사용한다. 사용할 함수와 클래스를 util/math.h(일반 수학연산), util/vector.h(벡터, 행렬 연산)와 util/sample.h(난수 연산)에 모아둘 것이다.
벡터와 관련해서는 조금 수학적으로 미묘한 구석이 많다. 그냥 숫자 몇개를 모아두는 게 벡터 연산의 전부가 아니다. 좌표변환이라는 측면에서 벡터는 몇 종류로 나누어진다.
- 위치벡터. 간단히 말해 물체의 위치를 나타낸다. 사실은 어떻게 봐도 벡터가 아니다(위치끼리 더하거나 위치를 2배한다는 것이 말이 되지 않는다). 좌표계의 평행이동과 회전에 모두 반대로 변하는데, 비유클리드 공간까지 생각하면 전역적인 변환(global coordinate transformation)을 고려해야 한다.
- 변위벡터. 두 위치 사이의 변위를 나타낸다. 적어도 유클리드 공간에서는 접선벡터처럼 볼 수 있다.
- 접선벡터. 국소적인 벡터로, 쉽게 말해 어떤 선의 방향을 나타내는 벡터이다. 국소적인 좌표 변환 텐서와 반변(contravariant)하는 벡터이다.
- 법선벡터. 국소적인 벡터로, 어떤 평면의 방향을 나타내는 벡터이다. 더 정확히는 듀얼 벡터(dual vector, covector)이며 국소적인 좌표 변환 텐서와 공변(covariant)하는 벡터이다.
PBRT에서는 1, 3, 4를 각각 Point, Vector, Normal로 지칭하고 있으며 2와 3을 사실상 같은 것으로 취급하고 있다. 이 방법의 장점은 Point + Vector = Point, Point – Point = Vector와 같이 연산식을 쉽게 적을 수 있다는 것이다. 그러나 아주 일반적인 비유클리드 공간에서 그런 방법은 통하지 않으므로, 우리는 다른 정의를 생각해야 한다.
Point + Vector = Point 형태의 식은 transport 함수를 정의하여 처리한다. Transport 함수는 point, vector와 multiplier를 받아 point를 vector의 방향으로 multiplier만큼 이동시킨 위치를 출력한다. 유클리드 공간에서는 point + multiplier * vector와 같다.
Point – Point = Vector 형태의 식은 Point 2개를 받아 그 사이를 잇는 측지선(geodesic)의 방향(Vector)을 출력하는 함수 fromTo를 통해 정의된다. 점 2개를 잇는 측지선이 여러개라면 가장 짧은 것을 출력하는 게 바람직하며, 거리가 같을 경우 그 중 아무거나 출력할 것이다.
일단 상황을 간단히 하기 위해 유클리드 기하에서 이상의 연산들을 정의한 뒤 setting에 비유클리드 기하를 사용할 것인지 여부를 설정하는 항목을 추가한다.
class Point; class Vector; Point transport(Point p, Vector v, Float multiplier = 1); Vector fromTo(Point p, Point q);
위의 모든 내용은 3차원에 한정되며, 2차원 벡터는 주로 이미지 픽셀을 나타내는 데만 사용될 예정이므로 Point와 Vector 2가지로 평범하게 정의된다.