0. 서론

이번 주엔 다음 기능들을 구현했다.

  • 깊이 검사(z-Buffering)
  • RenderObject 구조 변경(Buffer와 Context 분리)
  • 새로운 Scan Conversion(삼각형 그리기)
  • 성능 개선 확인을 위한 Frame 체크 GDI

1. z-Buffering

3D 공간에서 모든 3D 오브젝트는 전면(Front-face)과 후면(Back-face)을 가지게 된다. 카메라의 시점에서 특정 오브젝트의 후면은 전면에 의해 가려져야 하는데, 이를 가능케 하는 방법은 두 가지가 있다.

  • Back-face Culling

    face normal과 camera front가 이루는 각도를 측정해 카메라와 마주보지 않는 폴리곤들을 렌더오브젝트에서 제외함으로써 렌더링 횟수를 줄이는 방식

  • z-Buffering

    깊이 검사를 통해 특정 픽셀을 설정할 때 해당 픽셀에 깊이가 얕은(카메라에 더 가까운) 정보가 존재하면 픽셀 설정을 건너뛰는 방식


엄밀히 Culling과 z-buffering은 용도가 다른데, Culling은 카메라 앞에 삼각형이 단 하나 존재하더라도 해당 삼각형이 카메라를 등지고 있다면 그려내지 않지만 z-buffering은 삼각형이 카메라를 등지고 있더라도 해당 삼각형과 카메라 사이에 다른 물체가 없다면 해당 삼각형을 그려낸다.


z-Buffering은 삼각형을 그려내는 과정에서 해당 픽셀에 대응되는 z-Buffer의 값과 픽셀의 z값을 비교해 픽셀의 z값이 더 작다면(0에 가까울수록 카메라와 가깝고 1에 가까울수록 카메라와 멀리 위치한다) 해당 픽셀을 설정하고 z-Buffer의 값을 비교했던 z값으로 갱신한다.

z-Buffer를 구현한 결과가 궁금하면 링크에서 확인할 수 있다.

2. RenderObject 와 Buffer 분리

지난 주 까지는 각각의 RenderObject(Context)가 자신의 Buffer를 가지고 있어야 했다.

이 방식은 동일한 물체가 여러 개 화면에 그려져야 할 경우 불필요한 메모리 낭비가 존재한다는 점인데, 예를 들어 색만 다른 큐브를 화면에 100개 그린다고 가정하면 당장 Vertex만 고려하더라도 각각의 큐브가 큐브를 구성하는 8개의 정점이 포함된 Vertex Buffer를 가져야 하므로 800개의 정점이 메모리에 필요하다.


그러나 큐브를 구성하기 위한 Vertex Buffer 하나만을 Core에서 관리하고 각 RenderObject들이 Cube 형태 구성을 위한 Vertex Buffer를 포인터로 참조만 한다면 메모리에 단 8개의 정점만 가지고 있으면 된다.

따라서 정점, 텍스처, 노말, 인덱스 등의 여러 오브젝트가 공통으로 사용할 수 있는 버퍼들을 따로 떼어내 독립적인 객체로 구성한 후, 이들을 관리하는 ObjectManager클래스를 구현해 리스트로 관리하게 하였고, 각각의 RenderObject들은 포인터로 ObjectManager에게 해당 정보를 참조할 수 있는 주소를 얻어 사용하게 하였다.

3. 삼각형 렌더링 성능 개선(Scan Conversion)

이전 포스트에서 삼각형의 내/외부 판별을 이용해 삼각형을 그려내는 방법에 대해 포스팅한 적이 있는데, 이 방식의 가장 큰 단점은 실시간 렌더링을 하기에는 연산량이 너무 많다는 것이다.

첫 번째로 삼각형을 그리기 위해 실제 삼각형 면적보다 두 배나 면적이 큰 Render Box를 구해 해당 박스 안의 모든 픽셀을 대상으로 삼각형 내부 판별을 하게 되는데, 연산 방식을 수정하지 않는다 하더라도 실제 그려질 삼각형만 이상적으로 따내 그리는 것보다 2배나 많은 연산량을 가지게 된다.

또, Visual Studio Profiler를 이용해 퍼포먼스 체크를 한 결과 내부 판별을 위한 외적 연산과 이후 무게중심 좌표의 범위 체크를 위한 조건문에 연산 부하가 큰 걸 확인하였기에 이러한 조건이 없는 새로운 삼각형 드로잉 방식을 구현해야 했다.

다시 정리하면, 새로운 Scan Conversion 기법은 다음을 만족해야 했다.

  • 그려낼 삼각형의 면적보다 더 큰 면적을 이용하는 연산 낭비가 없는 것이 바람직함
  • 비교 연산을 최소화해야 함


2차원 그래픽스 이론 중 직선을 그리기 위한 알고리즘에 DDA 알고리즘이 존재한다.

시작점과 끝점의 변화율($\frac{\Delta y}{\Delta x}$)을 구해낸 후 x를 1씩 증가시켜가면서 y에 $\frac{\Delta y}{\Delta x}$를 더해 직선을 구성하는 픽셀을 정하는 방식인데 이 과정에서 처음 변화율만 구해진다면 이후 다른 연산 필요 없이 오직 덧셈 반복문만을 이용하면 된다.


이 알고리즘을 기반으로 새로운 삼각형 그리기 함수를 구현했으며, 다음과 같은 알고리즘으로 구현했다.

  1. 삼각형의 세 점을 y좌표 기준으로 오름차순한다(Viewport의 원점이 좌측상단에 존재하므로)

  2. 정렬된 세 점 중 첫 번째 점(가장 y값이 작은 점)과 세 번째 점(가장 y값이 큰 점)의 $\frac{\Delta x}{\Delta y}$를 구한다.

  3. 임의의 한 점을 추가로 생성하는데, 이때 이 점은 정렬된 세 점 중 두 번째 점과 같은 y좌표를 부여하며, 해당 y좌표에 대한 x좌표는 앞서 구한 $\frac{\Delta x}{\Delta y}$로 지정한다.

  4. 이제 주어진 점이 4개가 되었으며, 4개의 점 중 두 번째 점과 세 번째 점을 x좌표 기준으로 오름차순 정렬한다.

  5. 첫 번째 점과 두 번째 점의 변화율 dl($dl = \frac{\Delta x}{\Delta y}$)와 첫 번째 점과 세 번째 점의 dr($dr = \frac{\Delta x}{\Delta y}$)를 구한다(첫 번째 점과 세 번째 점의 변화율은 사실 앞서 구했기 때문에 다시 구할 필요는 없다.

  6. 첫 번째 점의 y좌표부터 시작해 y좌표를 1씩 증가시켜 가며 두 번째 점 혹은 세 번째 점까지 y좌표가 도달할 때 까지 x축에 평행하게 삼각형을 칠한다. 이때 시작 x지점과 끝 x지점은 첫 번째 점의 x좌표로 초기화 한 상태에서 y가 1 증가할 때마다 각각에 dl, dr을 더해 구한다.

이 때 픽셀 드로잉을 x축에 평행하게 하므로 위 그림처럼 $V_1, V_2$와 $V_3$의 $y$좌표가 다른 경우 $y$가 $V_1, V_2$보다 큰 지점($V_1, V_2$와 $V_3$ 사이 영역)을 그릴 수 없다. 따라서 삼각형을 두 부분으로 분할해서 각각 위 과정을 진행해 주어야 한다.

또, 이외에도 $V_0$과 $V_1, V_2$의 $y$좌표가 매우 작은 차이가 있을 경우, 변화율이 매우 커질 수 있어 이에 대한 제한 조건도 걸어야 한다.

이렇게 구현한 새로운 방식은 이전 방식보다 동일 환경(Cube 128개 렌더링)에서 대략 5배나 더 높은 프레임을 뽑아주어서 매우 성공적인 개선이 이루어졌다. 영상은 링크에서 확인 가능하다.

4. GDI를 이용한 Frame 표시

위의 새로운 삼각형 그리기 함수를 구현하면서 어느 정도의 성능 향상이 있었는지를 확인하기 위해, 그리고 이후에도 렌더링 엔진이 어느 정도의 프레임으로 화면을 그려내는지 확인할 필요가 있어 Frame을 표시하는 GDI 코드를 추가하였다. WinAPI를 다뤄 보았다면 매우 쉽게 구현할 수 있는 부분이라 별도의 설명은 달지 않겠다.

5. 결론

하나씩 착착 진행되니 매우 뿌듯하다. 다만 4월 부터는 단체 프로젝트가 하나 진행될 예정이라 이 프로젝트에만 온전히 시간을 쏟지 못할 것 같다. 3월 동안 최대한 구현하고 싶은 것들을 전부 구현하고, 나머지는 틈틈이 시간이 될 때마다 진행해 보려 한다.