일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 평면에 물체 던지기
- AR 게임
- AR 던지기 게임
- 레이 트레이싱
- 렌더링
- AR Foundation Throwing
- Ray Tracing in One Weekend
- ray tracing
- ar game
- AR Throwing Game
- Today
- Total
요리조리
Bounding Volume Hierarchy (BVH)로 렌더링 가속화하기 본문
레이 트레이싱에서 이미지의 픽셀 색깔을 결정할 때는, 픽셀마다 레이(ray)를 쏘아 3차원 공간 상에서 해당 레이가 어떻게 물체들과 부딪혀 이동하는지 경로를 탐색한다. 이때, 레이는 3차원 공간 상에서 특정한 위치(origin)에서 출발하여 특정 방향(direction)으로 이동하는 반직선의 벡터이다. (말 그대로 현실의 광선 그 자체를 컴퓨터 상에서 구현한 것..!) 따라서 레이가 3차원 공간 상의 어떤 물체와 부딪히는지 확인하기 위해서 공간에 있는 모든 물체들과의 충돌 테스트(intersection test)를 수행해야 한다. 즉, 물체 객체들을 담은 배열이 있을 때, 그 배열의 요소(element)를 하나씩 방문해 레이와 해당 요소가 부딫치는지 아닌지를 확인하고, 부딪친다면 어느 시점(time)에서 부딪히는지 확인 후, 가장 가까이에서 부딪힌 물체를 최종적으로 기록해야 한다. 따라서 하나의 레이마다 모든 물체에 대해 충돌 테스트 연산을 수행해야 하므로, 공간 상에 물체의 개수에 비례하게 연산량이 늘어나게 되는 것이다. (물체 하나를 하나의 수식으로 표현할 수 있는 경우 레이와 물체 하나와의 충돌 테스트를 수행할 수 있다. 하나의 수식으로 표현할 수 없는, 여러 개의 삼각형들로 이루어진 복잡한 물체의 경우, 삼각형 마다 레이와의 충돌 테스트를 수행해야 한다. 즉, 삼각형 하나가 한 물체로 여겨지는 것!)
레이 트레이싱은 대게 사실적이고 섬세한 이미지를 만들어내기 위해 사용된다. 즉, 레이 트레이싱에 사용되는 3차원 공간은 아주 복잡한 경우가 많다는 의미이다...! 따라서 이미지를 렌더링하는 시간이 오래 걸리기 때문에 이를 개선하기 위해 가속 자료구조(acceleration data structure)들을 사용하기도 한다. 레이와 모든 물체들과의 충돌 테스트를 바로 수행하지 않고 물체들을 감싸는 bounding volume을 만들어 레이와 해당 볼륨과 충돌하지 않는 경우는 모두 제하는 방식이다. 이 방식을 사용하면 레이가 접근하지 않는 특정 영역에 있는 물체들은 아예 충돌 테스트를 수행하지 않아도 되므로 연산량이 대폭 줄어든다. 이러한 가속 자료구조의 대표가 바로 Bounding Volume Hierarchy (BVH)이다. BVH는 트리형의 자료구조로, 모든 물체를 감싸는 볼륨이 루트 노드(root node)가 되고, 해당 볼륨을 점점 쪼개어 나가면서 자식 노드(child node)를 생성하는 방식으로 만들어진다.
삼각형 데이터에는 삼각형을 구성하는 정점들의 인덱스만 저장하고, 실제 정점들의 위치와 법선 벡터의 데이터는 한 번만 저장하게 하는 방식: BFP로 이를 표현하게 되면 데이터의 일관성이 유지될 수 있지 않을까? 예를 들면, 삼각형마다 실제 정점 데이터를 저장하는 경우 발생할 수 있는 문제를 생각해 보자. 두 개의 삼각형이 공유하는 정점 2개가 있다고 할 때, 두 정점을 별도의 common exponent를 가진 BFP로 각각 표현하게 되면, 실제로는 같은 점인데 약간의 차이가 있는 값을 가지게 된다. 따라서 물체에 작은 구멍들이 생기는 문제가 발생하지 않을까?! 흠흠....
BVH Construction
Problem 1
문제상황
두 개의 헤더파일이 있다고 할 때, 서로가 서로를 include하는 경우, 링킹 과정에서 에러가 발생한다. 따라서 bvh.h에 작성된, BVH를 만들고 탐색하는 함수들의 매개변수로 메시(mesh)의 주소를 전달하는 방식이 불가능해진다 (기존의 교재에서 작성한 헤더파일들을 그대로 유지하는 것이 불가능해진다는 의미이다). 그렇다면 왜 굳이 메시를 매개변수로 전달해줘야 할까? 그 이유는 바로, 삼각형 클래스(혹은 구조체)가 실질적인 데이터(vertex, normal값)을 저장하는 것이 아니라, 그것이 저장된 위치(배열의 인덱스)를 저장하고 있기 때문이다. 왜 이런 방식을 사용할까? vertex, normal 배열을 하나씩 만들어 해당 값들을 중복 없이 한 번만 저장해야 메모리 사용량을 줄일 수 있기 때문이다.
해결방법
BVH를 만드는 방식을 크게 두 가지로 생각해 보았다.
1) 삼각형이 실질적인 데이터를 저장하게끔 하여 mesh를 전달하지 않고 BVH를 construct, search하는 방식 (모든 mesh에 대한 하나의 BVH를 만드는 것이 가능해짐)
2) Mesh가 BVH를 멤버로 갖게끔 하는 방식 (mesh마다 각각 하나의 BVH를 가져야 함)
: 메시 안에 BVH를 만들어야 하므로, 기존처럼 bvh.h 헤더파일에 BvhNode 클래스를 정의해서 mesh.h에 include하는 방식은 불가능하다. (bvh.h도 mesh.h를 include해야하고, mesh 내에서도 bvh.h를 include해야하기 때문에 링크가 이상해지기 때문이다.)
=> 방법 1)에서처럼, 삼각형마다 실질적인 값을 저장하게 되면, 메시를 구성하는 삼각형의 개수가 늘어날수록 중복된 데이터가 많아진다. 또한 우리가 연구하고 있는 low precision을 적용하게 되면, 동일한 vertex, normal 값이 여러 곳에 중복적으로 저장되어 있기 때문에 데이터가 일관되지 않게 변경될 수 있다. 즉, 원래는 붙어 있던 삼각형들(동일한 vertex를 공유하던 삼각형들) 사이에 약간의 틈(hole)이 생길 수 있다는 의미이다. 이는 렌더링 결과에도 영향을 미치므로 우리가 진행하는 프로젝트에서는 더더욱 문제가 있는 방식이다.
Problem 2
변경사항
BVH 노드들을 만들 때 부모 노드를 2개의 자식 노드들로 쪼개는 방식
AABB를 어느 축에서 비교할 지 그 기준축을 정한 후, 기존에는 AABB의 minimum point의 해당 축의 성분 값을 비교하였다. 하지만 그렇게 될 경우 박스의 크기는 고려하지 않고 단순히 최소 지점만을 고려하게 되므로 quality가 낮은 BVH가 만들어질 가능성이 높다. 따라서 박스의 위치와 크기까지 모두 반영할 수 있게끔 박스의 중심을 기준으로 비교한다.
Problem 3
문제상황
AABB의 minimum point, maximum point를 구할 때:
minimum point의 성분들(x_min, y_min, z_min)은 float 데이터 타입으로 표현할 수 있는 최댓값으로, maximum point의 성분들(x_max, y_max, z_max)은 그의 최솟값으로 설정해야 한다. 나는 그 반대로 설정하는 실수를 범했기 때문에 아래 디버깅 내용과 같이 박스가 잘못 계산되는 문제가 발생했다.
- AABB: (3.40282e+38 3.40282e+38 3.40282e+38) ~ (-3.40282e+38 -3.40282e+38 -3.40282e+38)
원인
Problem 4
문제상황
BVH 노드를 만드는 방식에서 문제가 있었다. (아, 그래서 GPU의 BVH를 만들 떄도 문제가 있었던 거구나...!) 문제가 있는 곳은 바로바로, 인덱스 계산 부분이다. 기존에는 노드가 감싸는 삼각형의 개수가 2개 이상이면 자식 노드를 만들었다. 따라서, 트리가 이진 트리가 아니라 최소 2개의 자식 노드를 갖는 애매한 이진 트리가 되는 것이었다. (듬성듬성한 트리라고 생각하면 되겠다..!) 즉, 현재 노드의 두 자식 노드들의 인덱스가 항상 2n+1(왼쪽 자식), 2n+2(오른쪽 자식)이 되지 않는 경우가 발생할 수 있다. 이 문제를 해결하는 방법은 두 가지가 존재한다.
해결방법
1)
현재 노드가 포함하는 삼각형의 개수가 1이더라도 자식 노드 2개를 항상 만드는 방법이다 (RTTNW 교재의 방법과 동일하다). 즉, 완전한 이진트리를 만들어서 항상 한 노드의 자식 노드들의 인덱스가 2n+1, 2n+2이 되게 하는 방식이다. 이 방식은 교재의 내용을 그대로 따라하면 돼서 수월하지만 쓸데없이 메모리를 낭비한다는 단점이 있다. (교재의 경우, sphere 클래스는 따로 그의 bounding box(AABB)를 저장하지 않고 그때그때 on the fly로 AABB를 구하기 때문에 메모리 낭비가 별로 없다. 하지만 우리는 삼각형 메시를 렌더링하는 코드를 작성하기 위해 각 삼각형들이 자신을 감싸는 AABB를 사전에 구하여 저장하게 된다. 즉, 삼각형 하나만을 감싸는 leaf 노드를 생성할 필요가 없다. 어차피 삼각형 안에 AABB가 저장되어 있기 때문에 노드를 만들지 않고 그냥 해당 삼각형 데이터로 접근해서 가져오면 되기 때문이다.)
2)
노드마다 왼쪽 자식 노드의 인덱스와 오른쪽 자식 노드의 인덱스를 저장하는 방식이다. 완전한 이진 트리가 아니기 때문에 이진 트리의 인덱스 계산법을 사용할 수 없고, 따라서 자식 노드들의 인덱스를 저장하는 메모리가 추가적으로 필요하다는 단점이 있다.
3)
현재 노드가 감싸는 삼각형 개수가 3개 이상일 때만 자식 노드를 만드는 방법이다. 2개인 경우, 자식 leaf 노드는 따로 만들지 않고 암묵적으로(implicitly) 삼각형 하나가 왼쪽 leaf node, 다른 하나가 오른쪽 leaf node라 여기는 방법이다. 메모리를 가장 아낄 수 있고 이진 트리의 인덱스 계산법을 사용할 수도 있어서 가장 효율적이다.
=> 다시 보니 이 방법도 인덱스 계산이 불가능하다! 삼각형 개수가 10개인 경우를 생각해 보자. 루트 노드의 자식 노드는 삼각형을 5개씩 나누어 갖는다. 즉, 두 자식 노드가 모두 2의 제곱수가 아닌 개수의 삼각형을 포함하므로... 마찬가지로 듬성듬성한 트리가 만들어진다.
결국, 해결법 1이 가장 최선의 선택이 아닐까... 모르겠다. 2+3을 하게 되면, 모든 노드들이 int 2개씩을 추가로 갖게 되면서 1보다는 노드 개수가 줄어들게 된다. 1을 하게 되면, 완전한 이진 트리를 만들기 위해 leaf node의 개수가 늘어나는 대신에 모든 노드들이 자식 노드들의 인덱스를 저장하지 않아도 되므로 int 2개씩을 절약할 수 있게 된다.
=> 음... 근데 다시 보니까 노드 개수가 너무 많이 늘어나는 것 같기도 하다...흠... 2는 거듭 제곱할수록 그 수들끼리의 간격이 급격하게 늘어난다 (e.g. 1, 2, 4, 8, 16, 32, ... 를 보면, 각 수 간의 간격은 1, 2, 4, 8, 16, ...로 거듭해서 늘어난다). 즉, 이진 트리를 구성하는 노드의 개수는, 삼각형의 개수가 늘어나면 늘어날수록 쓸데없는 노드의 개수가 급격하게 늘어나게 된다. 그래서 그냥 해결법 2+3이 낫지 않을까 싶다...
=> 해결법 2+3의 경우, 자식 노드의 인덱스를 저장하는 대신, 기존에 저장하던 f_first, f_last(포함하는 삼각형들의 시작 인덱스, 끝 인덱스)를 저장할 필요가 없다. BVH의 construction 과정에서는 f_first, f_last, f_mid를 계산해서 임시적으로 저장하되 노드에는 영구적으로 저장하지 않으면 된다. 자세한 방법은 더 생각해봐야 할 것 같다. 계층이 깊어질수록 저장해야 할 값들이 많아지기 떄문에 단순히 3개의 변수만으로는 안된다. 스택을 쓰든가 패턴을 분석하든가 해야될 것 같다. 그래도 이 방법이 낫다고 여기는 이유는, 바로 construction에서 잠시 더 많은 메모리를 사용하더라도 BVH의 크기 자체를 줄이는 게 더 성능 개선을 할 수 있는 방법인 것 같아서이다 (맞겠지?)
(일단은 기존의 f_first, f_last도 사용하고 추가적으로 left, right도 사용하는 방법을 구현해보자...!)
=> 우선은 2+3이 아니라 2로 해보자! (즉, 노드가 감싸는 삼각형의 개수가 2개 이상인 경우에는 자식 노드를 생성하는 방식)
struct BvhNode
{
int idx;
int f_first; // index of first face(triangle)
int f_last; // index of last face(triangle)
int left; // left child index
int right; // right child index
Aabb box;
느낀점
index out of range를 방지하는 조건문이 정말 중요하구나..! 이게 디버깅하기도 수월하다~ 어디에서 버그가 생겼는지 확인하기에 좋다.
결과
FACES # = 6
NODES # = 11
Node 0
- Faces: 0~5
- AABB: (-1 0 -1) ~ (1 1 1)
Node 1
- Faces: 0~2
- AABB: (-1 0 -1) ~ (1 1 1)
Node 2
- Faces: 3~5
- AABB: (-1 0 -1) ~ (1 1 1)
Node 3
- Faces: 0~1
- AABB: (-1 0 -1) ~ (1 1 1)
Node 4
- Faces: 2~2
- AABB: (-1 0 0) ~ (1 1 1)
Node 5
- Faces: 3~4
- AABB: (-1 0 -1) ~ (1 0 1)
Node 6
- Faces: 5~5
- AABB: (0 0 -1) ~ (1 1 1)
Node 7
- Faces: 0~0
- AABB: (-1 0 -1) ~ (0 1 1)
Node 8
- Faces: 1~1
- AABB: (-1 0 -1) ~ (1 1 0)
Node 9
- Faces: 3~3
- AABB: (-1 0 -1) ~ (1 0 1)
Node 10
- Faces: 4~4
- AABB: (-1 0 -1) ~ (1 0 1)
================================== BVH CONSTURCTION COMPLETED ==================================
------------------------------ Node 0 INFO (obj # = 6) ------------------------------
- Faces: 0~5
- AABB: -1 0 -1 ~ 1 1 1
------------------------------ Node 1 INFO (obj # = 3) ------------------------------
- Faces: 0~2
- AABB: -1 0 -1 ~ 1 1 1
------------------------------ Node 2 INFO (obj # = 3) ------------------------------
- Faces: 3~5
- AABB: -1 0 -1 ~ 1 1 1
------------------------------ Node 3 INFO (obj # = 2) ------------------------------
- Faces: 0~1
- AABB: -1 0 -1 ~ 1 1 1
------------------------------ Node 4 INFO (obj # = 1) ------------------------------
- Faces: 2~2
- AABB: -1 0 0 ~ 1 1 1
- Face[2] Vs: 1 4 5
------------------------------ Node 5 INFO (obj # = 2) ------------------------------
- Faces: 3~4
- AABB: -1 0 -1 ~ 1 0 1
------------------------------ Node 6 INFO (obj # = 1) ------------------------------
- Faces: 5~5
- AABB: 0 0 -1 ~ 1 1 1
- Face[5] Vs: 1 3 4
------------------------------ Node 7 INFO (obj # = 1) ------------------------------
- Faces: 0~0
- AABB: -1 0 -1 ~ 0 1 1
- Face[0] Vs: 1 5 2
------------------------------ Node 8 INFO (obj # = 1) ------------------------------
- Faces: 1~1
- AABB: -1 0 -1 ~ 1 1 0
- Face[1] Vs: 1 2 3
------------------------------ Node 9 INFO (obj # = 1) ------------------------------
- Faces: 3~3
- AABB: -1 0 -1 ~ 1 0 1
- Face[3] Vs: 5 4 3
------------------------------ Node 10 INFO (obj # = 1) ------------------------------
- Faces: 4~4
- AABB: -1 0 -1 ~ 1 0 1
- Face[4] Vs: 5 3 2
BVH 탐색(Traversal)
Main Idea 1
std::vector를 사용해서 스택을 만들어 보자. 스택을 이용해서 BVH를 depth-first order로 탐색하면 된다. Pseudo code는 아래와 같다.
Main Idea 2
[intersection test]
1) 조건문을 많이 쓰기
논리적으로 이해가 잘 된다. 어떤 경우에 어떤 동작을 하는지가 명확해서 변수들의 용도 등을 파악하기가 더 수월하다.
// Do the ray-triangle intersection test
if(cur_f->hit(r, (is_hit_first)?rec.time:t_min, rec)) { // If the current object is hit
if(is_hit_first != true) is_hit_first = true; // if the current one is the first intersected object
}
2) 조건문을 제거하고 최대한 항상 동일한 동작을 하게 하기 (branch condition 줄이기 => GPU에 적합?)
항상 비슷한 동작을 하게 하기 위해서 불필요하게 명령어(command)를 수행하는 경우가 있다.
// Do the ray-triangle intersection test
if(cur_f->hit(r, t_min, rec)) { // If the current object is hit
t_min = rec.time; // Always update the current closest time
is_hit = true;
}
Main Idea 3
스택 사용해서 탐색하는 방법
1) 현재 방문한 노드의 자식 노드 중 다음 번에 방문할 노드(보통 왼쪽 자식 노드)를 현재 iteration에서 미리 결정하고, 이를 제외한 남은 노드(보통 오른쪽 자식 노드)만 스택에 넣는 방법
: 바로 다음 차례에 방문할 노드를 스택에 넣고 또 바로 빼는 연산을 줄일 수 있고, 스택에 할당되는 메모리 사이즈도 조금 줄일 수 있다.
if(hit_l) // If the left child is hit,
{
cur_node = child_l; // Visit the left child in the next iteration.
if(hit_r) {
stack[top++] = child_r; // Push the right child node if it is hit.
}
}
else if(hit_r) // If only the right child is hit,
{
cur_node = child_r; // Visit the right child in the next iteration.
}
else // If both are not hit,
{
if(top == 0) break; // If stack is empty, end the traversal.
else cur_node = stack[--top]; // Otherwise, pop one node.
}
2) 현재 방문한 노드의 자식 노드들 두 개를 모두 스택에 넣고, 다음 차례에 스택에서 노드(보통 왼쪽 자식 노드)를 하나 빼서 방문하는 방법.
: 코드의 사이즈를 줄일 수 있고, 코드가 간단해서 이해하기 더 쉽다.
=> BvhNode를 넣다 뺏다 하는 스택이 아닌, BvhNode의 인덱스만 사용하는 스택을 사용하면 좋을 것 같다. 스택의 데이터타입을 int로 설정한다면 1) 못지않게 효율적일지도...?
<변경 전>
vector<BvhNode> stack; // stack (storing node itself)
...
// 2) intermediate node
else if (f_num > 1) {
BvhNode* child_l = &m_bvh[cur_node->left]; // left child
BvhNode* child_r = &m_bvh[cur_node->right]; // right child
// Do the intersection test of the child nodes
int hit_l = child_l->hit(r, t_min, rec);
int hit_r = child_r->hit(r, t_min, rec);
if(hit_l) stack.push_back(child_l);
if(hit_r) stack.push_back(child_r);
}
<변경 후>
vector<int> stack; // stack (storing node index)
...
// 2) intermediate node
else if (f_num > 1) {
BvhNode* child_l = &m_bvh[cur_node->left]; // left child
BvhNode* child_r = &m_bvh[cur_node->right]; // right child
// Do the intersection test of the child nodes
int hit_l = child_l->hit(r, t_min, rec);
int hit_r = child_r->hit(r, t_min, rec);
if(hit_l) stack.push_back(cur_node->left); // (or child_l->idx)
if(hit_r) stack.push_back(cur_node->right); // (or child_r->idx)
}
Problem 1
* 해결 못 한 문제 *
1. BVH Construction에서 현재 노드의 왼쪽, 오른쪽 자식 노드의 인덱스 값을 설정하는 부분에서 문제가 있다. 이상하게 루트 노드만 자꾸 오른쪽 자식 인덱스가 2가 아닌 -1로 저장이 되어 있는 것을 발견할 수 있었다.
FACES # = 6
NODES # = 11
Node 0
- Faces: 0~5
- AABB: (-1 0 -1) ~ (1 1 1)
LLLEFT 1
RRIGHT 2
RRIGHT -1
Node 1
- Faces: 0~2
- AABB: (-1 0 -1) ~ (1 1 1)
LLLEFT 3
RRIGHT 4
RRIGHT 4
Node 2
- Faces: 3~5
- AABB: (-1 0 -1) ~ (1 1 1)
LLLEFT 5
RRIGHT 6
RRIGHT 6
Node 3
- Faces: 0~1
- AABB: (-1 0 -1) ~ (1 1 1)
LLLEFT 7
RRIGHT 8
RRIGHT 8
Node 4
- Faces: 2~2
- AABB: (-1 0 0) ~ (1 1 1)
Node 5
- Faces: 3~4
- AABB: (-1 0 -1) ~ (1 0 1)
LLLEFT 9
RRIGHT 10
RRIGHT 10
Node 6
- Faces: 5~5
- AABB: (0 0 -1) ~ (1 1 1)
Node 7
- Faces: 0~0
- AABB: (-1 0 -1) ~ (0 1 1)
Node 8
- Faces: 1~1
- AABB: (-1 0 -1) ~ (1 1 0)
Node 9
- Faces: 3~3
- AABB: (-1 0 -1) ~ (1 0 1)
Node 10
- Faces: 4~4
- AABB: (-1 0 -1) ~ (1 0 1)
일단은 임시 해결 방법을 찾았다. cur_node 포인터로 right(오른쪽 자식 노드의 인덱스)값을 설정하지 않고, 직접 bvh 벡터에 접근해서 값을 설정하는 방법이다. 뭔가 포인터에서 문제가 있는 것 같은데... 왜 루트 노드에서만 이런 문제가 발생하는 건지는 미스테리다... 정말...
if(r_idx > 0 && r_idx < node_num)
{
// 왜 루트 노드만 오른쪽 자식 노드 인덱스가 자꾸 -1이 되는 걸까...
//cur_node->right = r_idx; // Update current node's right child index
bvh[idx].right = r_idx;
문제
BVH Traversal은 제대로 되고 있는 것 같은데..., 삼각형과의 충돌이 제대로 연산이 되지 않는 것 같다. 그래서 BVH 없는 ray-triangle intersection test 결과만을 BVH 있는 버전과 없는 버전을 비교해야겠다. ray 시작점과 방향, 삼각형의 세 꼭짓점 위치이 동일하게 매개변수로 전달되는지 디버깅해보자!
=> BVH를 출력해서 제대로 construct가 되었는지 확인해 보았다. 문제가 없었다... 흠 왜 그럴까...
이유: 현재 픽셀의 색깔을 결정하는 computeRayWithBVh 함수는 재귀적으로 자기 자신을 호출한다. 이때, 기존의 코드를 수정하지 않는 바람에 computeRayWithBVh가 computeRay를 재귀적으로 호출하게 되어있었다. 따라서 이미지가 이상하게 출력되었다.
'연구실 인턴 > Ray Tracing' 카테고리의 다른 글
폴리곤 메시 렌더링하기 (Rendering Polygon Meshes)_2 (0) | 2023.02.01 |
---|---|
폴리곤 메시 렌더링하기 (Rendering Polygon Meshes)_1 (0) | 2023.01.26 |