본문 바로가기

개발/개발 일기

2021.7.25 개발 일기 - 코넬 박스 구현과 뻘짓

1주일 전에 레이트레이서에 쓰레드 풀을 적용했다. 그 후, 여러가지 샘플링 기법을 적용해서 렌더러 부분을 최적화 시킬 예정이었다. 그런 과정에서 우선 기존의 렌더러에 존재했던 문제를 해결했어야 했다.

 

패스트레이서를 구현하고, 사실 멀티프로세서를 코드를 빠르게 구현해보고 싶어서, 렌더러의 세세한 부분을 작업을 하지 않았다. 

 

특히 적분기의 기능을 구현할 때, pdf를 전혀 고려하지 않은 코드를 작성했다. 그러다보니, 샘플링 기법에 따라서 씬의 밝기가 바뀌는 현상을 겪었다. 처음 이 문제를 보고, pdf(probability density function)를 전혀 고려하지 않아서 그렇게 되었다는 것은 알았지만, 이 부분을 작업하기가 너무 귀찮아서 한동안 Python의 matplotlib를 공부하기 시작했다.

 

사실 평소에 Python을 쓸 일도 없고, 많이 써보지는 않았지만 여러 샘플링 기법을 적용하고 공부하기 위해서, 눈으로 직접 어떻게 샘플링이 되는지 봐야지 확실히 이해할 수 있고, 또한 블로그에 올릴 때 도움이 될 것 같아서 공부를 시작했다. 

 

Python의 문법을 수박 겉핥기 수준으로만 보고 바로 시작한거라서, 배열의 슬라이스를 처리하는 코드가 헷갈릴 때가 많다. Python을 평소에 자주 사용하던 사람들이라면 매우 익숙하겠지만, 평소 c++, c#만 쓰던 나에게는 매우 생소한 코드였다.

 

아무튼, matplotlib의 기본적인 사용법을 익히고 나서는 다시 레이트레이서의 렌더러에 pdf 코드를 추가하기 시작했다. 작업을 하면서 가장 참고하기 좋은 코드는 어렵기는 해도, pbrt 코드가 최고인 것 같다. 물론 pbrt 코드는 이미 완성된 코드이고, 최적화를 위한 여러 전략들이 들어가서 한번에 이해하기는 매우 어렵지만, 천천히 보다보면 어떻게는 대강 이해가 된다.

 

이전 내 코드는 bxdf의 값을 구하기 위해서 따로 클래스를 생성하지 않고, 함수를 통해서 간단하게 처리했다. 하지만, pdf를 각 bxdf에 매칭되는 함수로 만들기에는 코드의 관리가 너무 어려울 것이라고 판단해, pbrt의 방식대로 bxdf 클래스를 생성하고, 여러 bxdf를 포함하는 bsdf 클래스를 추가했다(물론 아직까지 여러 bxdf를 사용하는 머티리얼은 없다...).

 

처음 pbrt의 머티리얼 클래스에 ComputeScattertingFunction이라는 함수만 정의되어있고, 이 함수에서 bxdf를 생성하는 코드를 보고 왜 이렇게 비효율적으로 작성을 하지? 라는 생각이 들었지만, 직접 이 부분을 작업하다 보니 효율성을 위해서 이렇게 작업을 했다는 것을 알게 되었다.

 

지금 현재 나의 씬은 매우 간단한 씬이기에, 각 머티리얼마다 bxdf를 저장해도 메모리 소모량은 크지 않다. 하지만 대규모의 씬을 렌더링하고 많은 머티리얼을 보관할 때 각 머티리얼마다 bxdf를 따로 저장한다면, 화면에 렌더링되지 않는 물체의 경우에는 렌더링도 되지 않으면서, 메모리를 많이 잡아 먹을 것이다.

 

그렇기 때문에, pbrt 코드에서는 광선이 특정 물체와 충돌할 때 bxdf를 생성해준다. 생성할 때의 메모리 할당 속도 문제는 커스텀 메모리 할당을 통해서 해결한다. 나도 이 방법을 적용할까 하다가, 그냥 단순하게 머티리얼을 처음에 생성할 때 필요한 bxdf를 직접 생성하는 방식으로 작업을 했다.

 

이렇게 bxdf와 bsdf 작업을 완료하고, 렌더러에 pdf의 개념을 적용했다. 

 

적용한 결과는 아래와 같다.

 

사진 1) pdf 적용 전 cosine 샘플링
사진 2) pdf 적용 후 cosine 샘플링

 

사진 1을 보면 반구에 꼭대기 부분과 빛이 투과된 부분의 색상이 비정상적으로 밝은 것이 보일 것이다. Uniform 샘플링이 아닌, Cosine 샘플링으로 샘플링을 하고, pdf를 적용하지 않아서 생긴 참극이다. 아래의 사진은 Cosine 샘플링의 pdf인 \(\frac{ 1 }{ 2\pi }\)을 적용한 결과이다. 반구의 머리 부분과 투과된 부분이 비정상적으로 밝지 않고 제대로 샘플링 된 모습을 볼 수 있다.

 

pdf를 적용하지 않은 것은 귀찮아서 하지 않은 것이지만, pdf를 적용하지 않을 것이면 Importance 샘플링을 하지 말았어야 하는데, 바보같이 Consine 샘플링을 하고 있었다. 

 

아무튼 이렇게 렌더러에 pdf의 개념을 추가했다. 정확히 말하면, pdf의 개념을 추가했다기 보다는 Importance 샘플링을 추가했다고 표현하는 것이 맞을 것 같다.

 

그 후 여러, 샘플링 방법을 적용해서 렌더러를 개선하려고 했는데, 현재 있는 단순한 투과와 반구만 있는 씬으로는 렌더러가 개선되는 점을 보여주기 힘들 것 같아서 많이들 쓰는 코넬 박스 씬을 제작하기로 했다.

 

현재 레이트레이서에 있는 모양은 구밖에 없었기 때문에 간단하게 사각형 모양을 추가했다. 그런데 사각형 모양을 추가했는데, 원하는데로 코드가 동작하지 않아서, 한참을 뻘짓을 했던 것 같았다.

 

이 과정에서 본의아니게 작성한 여러 코드에서 버그를 발견했는데, 그 중 하나는 Rotate 코드였다. 회전 행렬을 구하기 위해서 \(sin\theta\), \(cos\theta\)의 값을 구해야하는데, 변수의 이름만 \(sin\theta\)로 해놓고, \(cos\theta\)를 계산하고 있었다. 이 코드가 지금까지 발견되지 않은 이유는 구만 있는 씬이어서 회전이 필요없었기 때문이었다.

 

사실 이전에 카메라를 회전하면서, 회전이 이상하게 되는 것 같다고 생각을 하긴 했지만, 그 당시에는 렌더러가 제대로 구현되기 전이어서 렌더러의 문제라고 치부하고 넘어갔다.

 

아무튼 이렇게 여러 버그들을 수정하면서 작업을 했는데도, 결과에는 아무런 변화가 없었다. 이것 때문에 한 이틀정도를 날렸는데 결국 원인이었던 버그는 광선을 오브젝트 공간으로 변환을 하고 나서, 충돌점을 계산할 때 오브젝트 공간에 있는 광선으로 계산을 하지 않고, 월드 공간에 있는 광선으로 계산을 했다. 발견하고 너무 충격이었다.... 이런 코드를 내가 작성했다니 하면서 자책도 했다.  또 발견하기도 어려웠던게 대충 코드를 보다보면, 맞는 코드처럼 보이기에 몇번을 넘어갔었다.

 

우여곡절 끝에 코넬 박스 씬을 완성했다. 

 

사진 3) 코넬 박스 씬

 

아직은 렌더러의 완성도가 매우 떨어지지만, 컬러 브리딩도 잘 들어가있고 점차 여러 샘플링 방식과 렌더러를 개선할 수 있는 방법을 추가해보면서 개선해야겠다. 

 

여러 샘플링을 적용하면서 샘플링 방식에 대한 자료도 블로그에 업로드 할 예정이다. 사실 자료들을 올려도 읽는 사람은 없지만, 만약 나와 비슷한 고민을 하던 사람이 우연히 이 블로그를 보고 도움이 되면 너무 기쁠 것 같다. 

 

샘플링 자료를 업로드하기 이전에 Rust를 이용한 DirectX12 개발 관련 자료를 다 업로드해야 하는데, Rust로 DirectX12를 사용하는 것이 그렇게 효율적인 것 같지 않다고 판단한 이후로 급격하게 의욕이 떨어졌는데, 아무튼 시작을 했으면 마무리를 짓는 것이 맞기 때문에, 샘플링 자료를 올리기 전에 Rust를 이용한 DirectX12 개발 관련 자료를 마저 업로드해야겠다.