2015. 2. 24.

[DirectX9]정점 버퍼(Vertex Buffer)

 3D 그래픽스에서 기하물체는 정점(Vertex)로 표현된다. 정점은 기본적으로 x, y, z 좌표 값을 가지고, 색상값과 같은 추가적인 정보도 가질 수 있다. 이런 정점을 연결하면 선분이 되기도 하고, 삼각형과 같은 기하물체를 이루기도 한다. DirectX에서는 정점을 좀더 효율적으로 관리하고 사용하기 위해서 정점 버퍼(Vertex Buffer) 기능을 제공한다. 이 정점 버퍼는 무엇이고, 정점 버퍼를 사용하는 방법은 어떻게 되는지 알아보자.


FVF(Flexible Vertex Format)

 FVF는 사용자가 직접 정점의 구조를 정의해서 사용할 수 있도록 DirectX에서 제공하는 기능이다. 사용자가 필요로 하는 정점 포맷을 선택하여 조합할 수 있다.


 위의 그림은 FVF를 구성하는 요소를 보여주고 있다. x, y, z는 정점의 좌표값, w는 동차 좌표계를 표시할 때 사용하는 w값이다. 다음은 확산광, 반사광, 텍스처 좌표가 뒤를 따른다. FVF는 이런 값들의 조합으로 이루어진다. 그러면 실제로 코드에서 어떻게 사용하는지를 살펴보자.
 먼저 사용자 정의 정점 구조체를 선언하였다. 구조체 내부에는 x, y, z, w 값과 함께 정점의 색상값도 선언해두었다. 우리가 사용할 정점은 이 정보만 가지는 정점이다. 반사광, 텍스처 좌표값 등은 우리가 정의한 정점에서는 사용되지 않는다. 이제 정의한 정점 구조체에 대한 플래그 선언을 해주자. 플래그 선언은 D3D 디바이스가 사용자 정의 정점 구조체에 대한 정보를 파악하기 위해 필요하다. 다시 말해 플래그 선언을 통해 사용자는 D3D 디바이스에게 사용자 정의 정점 구조체가 어떤 정보들을 담고 있는지 알려준다. 다음에 또 설명하겠지만 이 플래그 선언은 정점 버퍼를 생성할 때도 사용된다.

 그리고 사용자 정의 정점 구조체를 정의할 때는 그 순서에 주의해야 한다. FVF 플래그를 설정하는 D3DFVF_XYZRHW는 이미 그 순서가 정해져 있다. 반드시 x, y, z, rhw 순서로 이루어져야 한다. 하지만 우리가 다음과 같이 사용자 정의 정점 구조체를 선언하면 어떤 결과가 나올지 장담할 수 없게 된다.
 사용자의 의도와는 달리 rhw값은 x값으로, x값은 y값으로, y값은 z값으로, z값은 rhw값으로 정의되고 있다.

 이제 사용자 정의 정점 구조체, FVF 플래그 설정을 마쳤다. 사용자가 어떤 정점을 사용할지 설정을 끝낸 것이다. 이제 이 정점을 저장할 버퍼를 만들어 보자. 우리가 만든 임의의 정점 구조체에 대한 정보는 FVF 플래그 설정을 통해서 전달하면 되므로 어려울 것 없다.


정점 버퍼

 정점을 모아두는 일종의 메모리이다. 그런데 이 메모리가 단순한 배열이나 new, malloc() 등에 의한 메모리와 구별되는 것은 정점 처리만을 위해 만들어진 특수한 메모리이기 때문에 효율적이라는 점이다. 정점 버퍼는 크게 두 가지 메모리 영역을 사용한다고 한다. 하나는 비디오 메모리이고, 하나는 시스템 메모리이다.

 비디오 메모리에 생성된 정점 버퍼는 GPU의 하드웨어 가속을 사용할 수 있다는 장점이 있지만, 시스템 메모리에 비해 제한된 메모리 용량을 사용할 수 밖에 없다. 그나마 제한된 메모리 용량마저도 텍스처와 함께 사용하기 때문에 언제나 넉넉하게 사용할 수 있는 것이 아니다. 반면에 시스템 메모리에 생성된 정점 버퍼는 하드웨어 가속을 사용할 수 없다는 단점이 있지만, 비디오 메모리에 비해 넉넉한 용량을 사용할 수 있다. 이제 정점 버퍼를 만드는 함수를 살펴보도록 하자.
 우리가 CreateVertexBuffer() 함수를 통해 생성한 것은 정점 버퍼의 인터페이스이다. 정점 버퍼에 직접 접근할 수 있는 주소값을 얻은 것이 아니다. 정점 버퍼에 정점을 입력하기 위해서는 정점 버퍼의 주소값이 필요하다. 이를 위해서 사용하는 함수가 바로 Lock() 함수이다.
 세 번째 out parameter로 주소값을 얻어와서 그 주소값에 정점을 복사해서 넣으면 결과적으로 정점 버퍼에 정점을 보관할 수 있게 된다. Lock() 함수를 사용한 정점 버퍼는 반드시 Unlock()을 호출해야 한다는 점을 잊지말자. 결국 Lock() 함수와 Unlock() 함수는 항상 세트로 사용되어야 한다고 생각하자.

 지금까지 사용자 정의 정점 구조체를 선언하고, FVF 플래그를 설정하고, 정점 버퍼를 생성하는 방법에 대해서 알아보았다. 여기에 해당하는 내용을 코드로 살펴보자.
 정점 버퍼를 위한 변수 g_pVB가 선언되었다. 그리고 사용자 정의 정점 구조체 CUSTOMVERTEX를 선언하였고, FVF 플래그도 설정하였다. 그리고 D3D 디바이스를 통해 정점 버퍼를 생성하였고, 그 인터페이스를 g_pVB에 저장하였다. 생성한 정점 버퍼에 Lock() 함수를 사용하여 정점 정보를 입력하였고, Unlock() 함수를 호출하였다.

 정점 버퍼를 비디오 메모리에 생성하고, Lock() 함수를 호출하여 얻은 메모리 주소을 통해 비디오 메모리를 일반 메모리처럼 사용할 수 있다. 하지만 일반 메모리와 달리 비디오 메모리에 접근하는 것은 PCI-Express 등의 슬롯을 통해서 접근하기 때문에 일반 메모리에 접근하는 것과 달리 매우 긴 접근 시간이 필요하다. 만약 매 프레임마다 Lock() 함수를 호출하여 비디오 메모리 주소를 얻어와서 비디오 메모리에 접근한다면 병목이 발생하여 성능이 저하될 것이다.

 정점 버퍼를 위한 리소스가 생성되었으므로 이 리소스를 반환하는 코드도 추가하여야 한다. Cleanup() 함수에 여기에 해당하는 내용이 구현되어 있다.


정점 버퍼를 사용하여 그리기

 정점 버퍼를 사용하여 기하물체를 그리기 위해서는 다음의 과정을 거쳐야 한다.

  1. SetStreamSource() 함수를 호출하여 정점 버퍼와 디바이스의 데이터 스트림과 연결시킨다.
  2. SetFVF() 함수를 호출하여 디바이스의 정점 포맷을 지정한다.
  3. DrawPrimitive() 함수를 후출하여 정점 버퍼의 정점을 활용하여 기하물체를 그린다.
 이 과정을 보여주는 코드를 살펴보자.

 실제로 기하물체를 그리는 기능을 하는 함수는 DrawPrimitive() 함수이다.



정점 버퍼를 사용하여 삼각형 그리기



실행결과

 위와 같이 삼각형 내부의 색이 그라데이션 처리가 된 것처럼 채워진 이유는 DirectX9 내부적으로 폴리곤 내부를 칠하는 작업인 쉐이딩(shading)을 할 때, 정점의 색을 보간(interpolation)하기 때문이다.

정리하면

  • 사용자 정의 정점 구조체를 설정하고, FVF 플래그를 설정할 수 있다.
  • 정점 버퍼를 활용하면 하드웨어 가속을 받을 수 있다.
  • Lock() 함수를 통해 정점 버퍼가 생성된 비디오 메모리 주소를 얻어올 수 있다.
  • 비디오 메모리에 접근하기 위해서는 매우 긴 시간이 필요하므로 주의할 필요가 있다.

댓글 1개: