2015. 3. 9.

[DirectX9]디바이스 생성

 DirectX9를 이용하여 3D 그래픽스를 공부하기 위한 첫 걸음은 D3D를 초기화하고, D3D 디바이스를 초기화하는 작업이다. 늘 그렇지만 새로운 환경에서 작업하기 위한 환경 설정이 필요하다. 환경 설정에 대한 좋은 포스팅은 매우 많다. 그러므로 링크를 걸어두고 넘어가고자 한다. Visual Studio에서 DirectX9 환경 설정하는 방법을 참고하도록 하자. 따라하면 문제없이 환경 설정을 마무리 할 수 있다. 환경 설정을 완료하였다면 이제 본격적으로 D3D 디바이스를 생성 및 초기화하러 가보자.

전체적인 과정
  1. 윈도우 클래스 생성 및 초기화, 등록
  2. 윈도우 생성
  3. Direct3D 초기화
  4. 윈도우 출력
  5. 메세지 루프
  6. 등록한 윈도우 클래스의 리소스 반환
 위의 과정을 따라가면서 호출되는 함수를 살펴보면 Direct3D 디바이스를 생성하는 과정을 샆펴볼 수 있다. 전체 과정을 코드로 제시하겠다.
 프로그램의 시작점인 WinMain() 함수를 보자. 윈도우를 생성하기 위해 필요한 윈도우 클래스를 선언하고 초기화한다. 그리고 이를 등록한다. 그리고 CreateWindow() 함수로 윈도우를 생성하고, 핸들을 받아왔다.

 다음은 Direct3D를 초기화하는 과정이 이어진다. InitD3D() 함수는 우리가 정의한 함수이다. 이 함수의 내부를 살펴보자. 먼저 D3D 객체를 생성한다. 그리고 D3D 디바이스를 생성한다. 이 순서는 반드시 D3D -> D3D 디바이스 순서로 이루어져야 한다. CreateDevice() 함수에 다양한 인자들이 전달되는데, 특히 D3DDEVTYPE_HAL은 하드웨어 가속을 지원하도록 설정하는 인자이다. 주목할 필요가 있다.

 D3D를 생성하고, D3D 디바이스를 성공적으로 생성한 후, 앞에서 생성한 윈도우를 통해 출력하면 된다. ShowWIndow(), UpdateWindow() 함수를 통해서 윈도우를 출력할 수 있다.

 다음은 메세지 루프 부분이다. 우리가 WinMain() 함수를 통해 프로그램을 실행시키고, 이 윈도우는 메세지 루프를 돌면서 이벤트 메세지를 기다린다. 만약 메세지가 전달되면, 이 메세지를 해석(TranslateMessage() 함수)하여, 메세지를 처리하는 함수(MsgProc() 함수)로 전달(DispatchMessage() 함수)한다. 뜬금없이 MsgProc() 함수가 등장하였다. WinMain() 함수 내부에서 MsgProc가 사용된 부분은 윈도우 클래스를 등록하는 부분이 유일한다. 윈도우 클래스를 생성 및 초기화하는 과정에서 우리는 메세지를 처리할 함수를 설정한다. 그리고 윈도우가 생성되고 나서 이벤트 메세지가 발생하면 DispatchMessage() 함수를 통해 메세지는 자동으로 미리 설정해둔 MsgProc() 함수에 의해 처리된다.

 MsgProc() 함수 내부는 크게 두 부분으로 이루어져 있다. WM_DESTROY는 윈도우가 파괴될 때 전달되는 메세지이다. 만약 이 프로그램을 실행시키고 윈도우를 강제로 닫으면 이 메세지가 전달될 것이다. 이 부분에 브레이크 포인트를 걸고 한 번 테스트 해보길 바란다. WM_PAINT는 UpdateWindow(), RedrawWIndow() 등의 함수에 의해 전달된다. 우리 윈도우 출력 부분에서 UpdateWindow() 함수를 호출하고 있다. 그래서 자동으로 WM_PAINT 메세지가 MsgProc() 함수로 전달된다. 그리고 GetMessage(), PeekMessage() 함수를 통해 메세지 큐에서 WM_PAINT 메세지가 있다면 DispatchMessage() 함수를 통해 전달될 수 있다. WM_PAINT 메세지가 전달되면 우리가 정의한 Render() 함수가 호출된다.

 Render() 함수 내부는 D3D 디바이스를 파란색으로 초기화하고, 장면을 그리도록 되어 있다. 그런데 장면을 그리는 부분에 아무런 명령이 없으므로 파란색으로 초기화된 바탕만 확인할 수 있을 것이다. 그런데 '후면 버퍼'라는 용어가 등장한다. 후면 버퍼는 흔히 Double Buffering(이중 버퍼)라고 불리는 개념에서 나온 것이다.


Double Buffering



 우리가 어떤 장면을 그리는 프로그램을 실행시키면 모니터라는 장비에 뭔가가 그려질 것이다. 모니터에 그러지는 장면은 흔히 Rendering Pipeline이라는 과정을 통해서 연산된 결과이다. 이 결과는 모니터의 해상도에 맞게 만들어진 버퍼에 쓰여지고, 모니터에 그려지는 장면은 바로 버퍼에 쓰여진 정보를 읽어서 그려진다. 그렇다면 모니터가 이 버퍼로부터 정보를 읽어서 모니터에 그리는 시간과 Rendering Pipeline에 통과한 결과가 버퍼에 쓰여지는 시간 간의 차이가 생기면 어떤 일이 발생하겠는가? 만약 Rendering Pipeline을 통과하여 버퍼에 정보가 쓰여지는 시간이 모니터에 그려지는 시간에 비해서 2배 느린 경우를 생각해보자. 버퍼에 정보가 다 채워지고, 모니터에 장면이 그려졌다. 그 다음 장면을 모니터에 그려야한다. 하지만 아직 그려야할 정보는 반만 버퍼에 저장되어 있다. 연산 결과를 반 정도 밖에 버퍼에 채우지 못하였다. 이 상황은 이전 장면의 정보와 다음 그려질 장면의 정보가 반반씩 섞여 있는 상황이다. 만약 이 장면을 모니터에 그리면 어떻게 되겠는가? 사용자에게 보여주면 안되는 장면을 보여주게 된다. 실제로 이런 경우 Flickering(깜빡임)이 발생하여 사용자를 불편하게 만든다.

 이 문제를 해결하기 위해 두 개의 버퍼를 사용한다. 반만 채워지는 경우를 방지하고자 전면 버퍼의 정보를 읽어 모니터에 그리고, 다음 장면을 후면 버퍼에 정보를 채운다. 후면 버퍼에 완전한 장면 결과가 채워지면 후면 버퍼에 있는 내용을 모니터에 그린다. 위 그림에서 Surface1, Surface2에 해당하는 것이 바로 전면, 후면 버퍼들이다. 사실 전면 버퍼의 내용이 모니터에 그려지는 동안 다음 장면은 후면 버퍼에 그려지고, 후면 버퍼에 정보가 다 채워지면 후면 버퍼가 전면 버퍼가 되어 버퍼의 정보를 읽어 모니터에 그리게 된다. 위 그림에서 Application이 가리키고 있는 Surface가 바로 전면 버퍼이다.

 다시 코드로 돌아가자. 후면 버퍼를 파란색으로 채우고, 후면 버퍼를 전면 버퍼로 바꿔주지 않으면 어떤 일이 발생하겠는가? 전면 버퍼에 있는 내용만 그려지고, 다음 그려질 내용이 모니터에 그려지지 않는다. 결국 전면 버퍼에 있는 내용을 한 번 그리고 나서 아무것도 그러지지 않는다. 이점을 잊지않도록 하자. 실제로 Present() 함수를 사용하지 않으면 아무것도 그려지지 않는다. 버퍼를 바꾸지 않으므로 더 그릴 것이 없기 때문이다.


그 밖에 과정들

 메세지 루프를 빠져나오는 경우는 WM_DESTROY 메세지가 전달된 경우이다. Cleanup() 함수에서 지정한 순서대로 리소스를 반환하고, PostQuitMessage() 함수를 호출하여 시스템에게 쓰레드를 하라는 요청이 발생하였다는 것을 알려준다.


정리하면

  • 윈도우를 생성하고, D3D와 D3D 디바이스를 생성하여 장면을 그린다.
  • 윈도우를 생성할 때 등록한 메세지 처리 함수를 통해 메세지 큐에 담긴 메세지가 전달되어 처리된다.
  • Flickering을 방지하기 위해서 Double Buffering을 사용하고, 이를 사용하기 위해서 Present() 함수를 호출한다.

[DirectX9]높이맵

 높이맵은 외부 지형을 처리하는 기법 중 하나이다. 전에 정점 버퍼, 인덱스 버퍼, 텍스처를 활용하여 가장 기본적인 지형을 생성하였다. 이 지형은 아무런 굴곡이 없는 편평한 지형이었다. 실제 외부 지형은 산과 같은 높이 굴곡이 있다. 이런 높이를 표현하기 위해서는 정점의 y 좌표값을 굴곡을 형성할 수 있게 설정해주어야 한다. 높이맵은 이런 y 값을 설정하는 방법 중 하나이다.


높이맵

 높이맵은 지형의 높이값을 0 ~ 255 사이의 명암값으로 나타낸 텍스처 파일이다.



 위 이미지가 바로 높이맵으로 사용되는 텍스처 파일이다. 아래는 지면을 입힐 텍스처이다. 0 ~ 255 사이의 명암값을 나타내는 파일이며, 255에 가까울수록 지형은 높아진다. 반대로 0에 가까울수록 지형은 낮다. 우리는 이 텍스처 파일로부터 지형의 높이 정보를 읽어와서 편평한 지형에 굴곡을 나타내려고 한다. 즉 정점의 x, z 값에 맵핑되는 텍스처의 명암값을 읽어서 정점의 y 값으로 설정해야 한다. 2차원 이미지의 밝기값이 3차원 공간에서 높이값으로 사용된다. 높이맵은 지형의 높이값을 저장하는 자료구조와도 같다.



 위 이미지는 높이맵과 이를 지형에 적용한 결과를 보여준다. 높이맵에 저장된 명암값을 읽어와서 지형의 높이값으로 변환하면 복잡한 지형을 간단하게 나타낼 수 있다. 그러면 이제 높이맵을 사용하기 위해서 어떤 과정을 거쳐야 하는지 알아보자.

  1. 높이맵으로 사용할 텍스처를 생성한다.
  2. 정점 버퍼에 정점을 저장할 때, 정점의 y 값을 높이맵의 명암값으로부터 읽어와서 저장한다.
 위 세 과정을 코드와 함께 살펴보도록 하자.


높이맵으로 사용할 텍스처를 생성하자

 먼저 높이맵으로 사용할 텍스처를 선언하고, 텍스처를 생성하자.
 2개의 텍스처를 생성하고 있다. 하나는 지면에 입힐 텍스처 파일이고, 하나는 높이맵으로 사용할 텍스처 파일이다. 그런데 높이맵을 생성하는 함수는 D3DXCreateTextureFromFileEx() 함수이다. 이 함수로 텍스처 파일을 생성하면 D3DFORMAT, D3DPOOL을 설정할 수 있다.


정점의 y 값을 높이맵의 명암값으로부터 읽어서 설정하자

 높이맵 텍스처로부터 명암값을 읽어오기 위해서는 2D 텍스처로부터 데이터를 읽어와야 한다. 이 때 사용되는 구조체가 바로 D3DSURFACE_DESC와 D3DLOCKED_RECT이다.
 D3DSURFACE_DESC 구조체로부터 텍스처의 가로, 세로 크기를 얻어 올 수 있다. 우리는 텍스처의 크기에 맞게 지형을 생성하고자 한다. 1픽셀 당 하나의 정점을 맵핑하고, 그 정점의 y 값을 텍스처의 명암값으로부터 읽어온다. 먼저 텍스처의 크기로부터 정점의 갯수가 정해지기 때문에 D3DSURFACE_DESC 구조체에서 가로, 세로 크기를 얻어와야 한다. 그리고 나서 가로 * 세로 갯수만큼 정점 버퍼를 생성해야 한다. 다음 코드는 이를 구현한 내용이다.
 정점 버퍼를 생성했다면 다음은 정점 버퍼에 정점을 저장할 차례이다. 이 때 정점을 저장하면서 높이맵 텍스처로부터 읽은 명암값으로부터 값을 읽어 정점의 y 값으로 설정한다. 높이맵 텍스처의 명암값을 읽어오기 위해서는 정점 버퍼에서 Lock() 함수를 호출하듯이, 텍스처에서 LockRect() 함수를 호출하여 메모리 주소값을 얻어와야 한다. 이 때 주소값을 저장하는 구조체가 바로 D3DLOCKED_RECT이다. 주소값만 저장할 용도라면 포인터로 선언하면 되는데 굳이 구조체로 만든 이유가 있다. 그래픽 하드웨어나 파일에 따라서 한 행의 크기가 다를 수 있기 때문에 Pitch 값을 통해 정확하게 읽어오기 위해서라고 한다.
 이 구조체에 주소값과 Pitch값을 저장한다.
 그 다음은 정점의 좌표값을 설정해야 한다. 특히 y 값은 텍스처의 명암값으로부터 읽어와야 한다. 텍스처의 명암값을 읽어오기 위해서는 LockRect() 함수로 텍스처의 색상값에 접근할 수 있는 주소값을 얻어와서 D3DLOCKED_RECT의 pBits 변수에 저장된다. 그리고 텍스처의 한 행의 바이트 값은 Pithch에 저장된다. 이 Pitch 값을 4로 나누면 한 행에 저장된 픽셀의 갯수를 알 수 있다. 결국 열의 값을 나타내는 오프셋(Offset)이 된다. 결국 pBits의 주소값을 기본으로 포인터 연산을 수행하여 한 픽셀 단위로 명암값을 읽어올 수 있다.
 이제 정점 버퍼에 좌표값도 모두 설정하였다. 이제 정점 버퍼에 보관된 정점을 읽어서 물체를 그리면 완료이다.


전체코드


실행결과


정리하면

  • 정점의 y 값을 텍스처의 명암값 형태로 저장한 것이 바로 높이맵 텍스처이다.
  • D3DSURFACE_DESC와 D3DLOCKED_RECT를 통해 텍스처의 크기 정보와 텍스처 픽셀의 명암값을 저장하여, 이를 정점 버퍼를 생성하고 정점의 값을 설정하는데 사용한다.

2015. 3. 8.

[DirectX9]지형 생성

 정점 버퍼인덱스 버퍼를 활용하여 지형을 생성해보자. 먼저 가장 기본적인 평평한 지형을 만들고, 그 다음 그 지형을 활용하여 굴곡이 있는 지형을 생성해보도록 하겠다. 이번 포스트에는 DirectX9에 대한 새로운 지식이 필요하다기 보다는 정점 버퍼와 인덱스 버퍼를 맵핑하는 알고리즘이 추가된 정도이다. 이 알고리즘 부분만 이해한다면 나머지 부분은 이미 여러 번 코드이므로 쉬울 것이다.


전체적인 과정
  1. 평면을 생성하는 정점을 선언하고, 정점 버퍼에 저장한다.
  2. 정점 버퍼에 저장된 정점을 활용하는 인덱스 버퍼를 만들어 평면을 생성하는 정보를 저장한다.
  3. 생성된 평면에 텍스처를 입힌다.
 크게 이 세 과정을 거쳐서 간단한 지형을 생성하고자 한다. 정점 버퍼, 인덱스 버퍼, 텍스처에 대한 간단한 지식이 한 번에 사용되는 예제가 될 것이다.


사용될 정점을 저장하는 정점 버퍼

가로 길이가 100이고, 세로 길이가 100인 평면 지형을 만들기 위해서는 몇 개의 정점이 있으면 되겠는가? 최소로 생각하면 4개만 있어도 충분하다. 평면 지형은 하나의 사각형으로 표현할 수도 있기 때문이다. 하지만 우리는 나중에 평면 지형을 활용하여 지형의 높이 굴곡도 표현하고자 하기 때문에 지금은 길이 1당 정점이 하나씩 필요하다고 생각하고 설명을 진행하도록 하겠다. 길이 1당 정점이 하나씩 필요하다면 100 * 100개의 정점이 필요하다.

 먼저 사용자 정의 정점을 선언하자. 
 사용자 정의 정점 구조체를 기반으로 100 * 100개의 정점을 위한 정점 버퍼를 먼저 생성해보자.
 가로 길이와 세로 길이 만큼 정점 버퍼의 크기를 설정하여 정점 버퍼를 생성하였다. 그리고 Lock() 함수를 통해 정점 정보를 저장할 주소값을 얻어왔다. 포인터 변수 pV에 그 주소값을 저장하였다. 정점의 위치, 수직 벡터, 텍스처 좌표를 설정한 후, 이 값을 정점 버퍼에 하나씩 저장하고 있다. 저장을 완료한 후에 Unlock() 함수를 호출하였다.

 우리는 현재 총 100 * 100개의 정점을 정점 버퍼에 저장하였다. 저장된 정점들과 인덱스 버퍼를 활용하여 지형을 생성하여야 한다. 그러기 위해서는 먼저 지형을 만들 수 있는 인덱스 정보들을 인덱스 버퍼에 넣어야 한다.


지형을 형성하는 인덱스 정보를 저장하는 인덱스 버퍼

 인덱스 버퍼의 장점에 대해서는 이전 포스트에서 언급한 적이 있다. 그리고자 하는 물체가 복잡하여 사용되는 정점의 수가 많을수록 정점 인덱스를 사용하는 효과는 더욱 커진다. 만약 인덱스 버퍼를 사용하지 않고, 가로 100, 세로 100의 평면을 삼각형으로 표현하기 위해서는 몇 개의 정점이 필요할까? 간단하게 계산할 수 있다. 총 100 * 100개의 작은 사각형이 그려져야하고, 이 사각형을 그리기 위한 2개의 작은 삼각형을 그려야 한다. 결국 100 * 100 * 2개의 삼각형을 그려야 한다. 정점 인덱스를 사용하지 않으면 좌표값이 동일한 정점의 중복을 허용해야 한다. 즉 하나의 삼각형을 그리는데 3개의 정점이 고정적으로 사용된다. 그래서 총 필요한 정점의 수는 100 * 100 * 2 * 3개가 된다. 이에 반해 인덱스 버퍼를 사용하면 필요한 정점의 수는 100 * 100으로 고정된다. 이 경우 인덱스 버퍼를 사용하면 정점 버퍼에 소비하는 메모리의 크기는 1/6로 줄일 수 있다. 여튼 좋으니깐 사용하도록 하자.

 인덱스 버퍼를 생성하는 코드를 보도록 하자.
 방금 전 평면 지형을 생성하기 위한 작은 사각형의 갯수에 대해 설명하였다. 그리고 그 사각형의 갯수의 두 배만큼의 삼각형이 필요하다. 그러므로 주석에서 설명하고 있는 것처럼 인덱스 버퍼의 크기는 코드에서처럼 설정된다.

 다음은 인덱스 버퍼에 작은 삼각형들을 생성하는 알고리즘이다. 코드를 보기 전에 정점 버퍼에 저장된 정점의 인덱스를 살펴보자. 아래 그림을 보자.


 우리가 정점 버퍼를 생성하면서 저장한 정점은 위의 그림처럼 저장되어 있다. 위의 숫자는 바로 정점의 인덱스를 의미한다. 위 그림은 가로 4, 세로 4인 경우 정점 버퍼에 저장된는 정점과 인덱스를 보여주고 있다. 가로 100, 세로 100의 경우는 아래와 같을 것이다.(화살표는 무시하도록 하자)


 첫 번째 행는 0 ~ 99, 두 번째 행은 100 ~ 199, 100번째 행은 99900 ~ 99999 인덱스가 부여될 것이다. 이 인덱스를 기반으로 평면을 형성하는 인덱스 버퍼를 만들어 보자.
 알고리즘에 따르면 첫 번째 삼각형은 [0, 1, 100], 두 번째 삼각형은 [100, 1, 101]로 인덱스 버퍼에 저장된다. 그리고 이 두 개의 삼각형으로 평면을 구성하는 작은 사각형을 표현할 수 있다. 이런 삼각형을 표현하는 MYINDEX 구조체가 100 * 100 * 2개만큼 인덱스 버퍼에 저장된다. 그리고 주목할만한 사실은 MYINDEX에 저장되는 인덱스가 반시계 방향(CCW)인지 시계 방향(CW)인지 살펴보아야 한다. 우리가 알고리즘을 통해 생성한 MYINDEX는 시계 방향으로 구성된다. SetRenderState() 함수를 통해 컬링 모드를 시계 방향으로 설정했다면 아무것도 그려지지 않게된다. 이 점을 유의하도록 해서 인덱스 버퍼와 컬링 모드를 설정하도록 하자.


생성한 지형에 텍스처를 입히자

 다음 이미지는 텍스처로 사용할 파일이다. 다운받아서 사용하도록 하자.

 텍스처를 초기화하였다.
 그리고 이 텍스처를 0번으로 설정하고, 확대 필터, 텍스처 좌표계를 설정하였다.


전체 코드

실행 결과


정리하면

  • 정점 버퍼, 인덱스 버퍼, 텍스처를 활용하여 지형을 생성할 수 있다.
  • 인덱스 버퍼를 사용하면 정점 버퍼에 저장하는 정점의 개수를 줄일 수 있다.

다음 주제: [DirectX9]높이맵

2015. 3. 7.

[DirectX9]인덱스 버퍼(Index Buffer)

 인덱스 버퍼는 정점 버퍼와 함께 사용하면 효과적이다. 이는 정점 버퍼에 대한 이해가 필요하다는 의미이다. 실제로 DirectX9에서 정점 버퍼를 사용하는 방법과 인덱스 버퍼를 사용하는 방법은 매우 유사하다. 정점 버퍼를 모르는 사람은 정점 버퍼의 내용을 살펴보고 난 후, 인덱스 버퍼를 공부하도록 하자.


인덱스 버퍼

 인덱스 버퍼는 정점을 저장하기 위한 정점 버퍼와 마찬가지로 정점의 인덱스를 보관하기 위한 전용 버퍼를 말한다. 인덱스 버퍼를 사용하면 다음과 같은 장점이 있다.

  • 정점을 여러 번 나열하는 것보다 메모리 소모량이 적다.
  • 자주 사용되는 정점을 캐시에 저장해서 성능 상 유리하다.
 글보다 다음 그림으로 먼저 이해해보자.

 왼쪽의 평행사변형은 인덱스를 사용하지 않은 경우이다. 평행사변형 즉, 사각형을 그리기 위해서는 삼각형 2개가 필요하다. [v0, v1, v2], [v3, v4, v5] 2개의 삼각형으로 사각형을 그렸다. 그런데 v1과 v4, v2와 v3는 동일한 좌표를 나타내는 정점이다. 동일한 좌표를 나타내는 정점이 중복된다는 문제가 발생한다. 이런 중복을 방지할 수 있는 방법이 바로 정점에 대한 인덱스를 부여하는 방법이다. 오른쪽의 경우를 살펴보자. 정점을 보관하기 위한 버퍼에 [v0, v1, v2, v3]가 저장되어 있다. 보관된 정점에 대해서 차례로 인덱스를 부여하면 [0-v0, 1-v1, 2-v2, 3-v3]가 될 것이다. 우리는 이제 정점의 인덱스를 활용하여 사각형을 그릴 것이다. 먼저 평행사변형의 [v0, v1, v2] 삼각형을 그리기 위해서 인덱스 [0, 1, 2]를 활용하면 된다. 다음 [v1, v2, v3] 삼각형을 그리기 위해서 인덱스 [2, 1, 3]를 활용하면 된다.

 인덱스가 없다면 왼쪽의 경우처럼 중복된 정점때문에 소모되는 메모리를 줄일 수 없다. 하지만 인덱스 버퍼를 활용하면 정점을 저장할 필요없이 인덱스만 보관하면 된다. 인덱스 버퍼를 사용하지 않으면 v1, v4는 동일한 위치를 나타내는 정점임에도 불구하고 정점 버퍼에 따로 저장되어야 한다. 하지만 인덱스 버퍼를 활용하면 중복된 정점을 보관할 필요없이 인덱스만 활용하면 된다. 그리고 평행사변형을 그리기 위해서 정점 버퍼에 4개의 정점을 캐시에 저장하는 것이 6개의 정점을 저정하는 것보다 캐시 힛(Cache Hit) 확률을 높힐 수 있다. 500개의 정점으로 구성된 물체가 있다고 가정하자. 이를 인덱스 버퍼를 사용하였더지 400개의 정점만 정점 버퍼에 보관하면 된고 가정하자. 만약 캐시에 100개의 정점 정보만 보관할 수 있다면 500개의 정점보다는 400개의 정점을 대상으로 연산하는 것이 유리하다. 특히 그래픽스에서 물체를 그릴 때에는 근처에 존재하는 정점이 다시 사용될 확률이 높다. 캐시 힛 확률을 높이는 것은 성능을 향상시키는데 도움이 된다.


인덱스 버퍼를 사용하여 육면체를 그려보자


 우리는 위와 같은 육면체를 그리고자 한다. 필요한 최소 정점은 8개이다. 하지만 인덱스를 사용하지 않으면 필요한 정점의 갯수는 증가할 것이다. 만약 인덱스 버퍼를 사용한다면 정점 8개와 이를 나타내는 8개의 인덱스로 우리가 구성하고자 하는 물체를 마음껏 표현할 수 있다. 그럼 먼저 8개의 정점을 부여해보자.


 아랫면을 구성하는 4개의 정점과 윗면을 구성하는 4개의 정점만 있으면 총 12개의 삼각형을 그릴 수 있고, 이를 통해 육면체를 완성할 수 있다. 그러면 이를 구현하기 위한 방법을 생각해보자. 확실한 것은 8개의 정점을 먼저 정점 버퍼에 보관해야 한다는 사실이다. 정점 버퍼에 정점이 보관되면 그 순서대로 인덱스가 부여된다. 그래서 따로 정점과 인덱스를 맵핑하는 과정은 고려하지 않아도 된다. 정점 버퍼에 정점들이 저장된 후, 인덱스를 나열하여 물체를 그리면 된다. 만약 위의 그림에서 육면체의 윗면을 그린다고 생각해보자. [4, 5, 7], [5, 7, 6]으로 인덱스를 나열하면 윗면이 완성된다. 이런 방식으로 6개 면을 구성하는 인덱스들의 모임을 나열하고, 이를 인덱스 버퍼에 저장하면 우리가 할 일은 끝난다.
 코드로 살펴보자. 먼저 8개의 정점을 정점 버퍼에 보관하는 코드이다.(참고로 위 그림의 인덱스와 코드의 인덱스는 다르다는 것을 참고하자)
 정점 버퍼를 이미 알고 있는 사람이라면 쉽게 이해할 수 있는 코드이다. 8개의 정점을 보관하기 위한 정점 버퍼를 만들고, Lock() 함수를 통해 정점을 저장할 수 있는 포인터값을 얻어, 정점을 저장한다. 그리고 Unlock() 함수를 호출하여 마무리하였다.

 다음은 정점 버퍼에 저장된 정점을 활용한 인덱스 버퍼를 설정하는 코드이다.
 코드를 보면 알겠지만 정점 버퍼를 사용하는 코드와 인덱스 버퍼를 사용하는 코드는 매우 유사하다. Lock(), Unlock() 함수를 사용하는 것은 동일하다. 정점 버퍼를 이해한 내용 그대로 인덱스 버퍼를 이해해도 무방하다.

 이제 우리는 육면체를 그릴 때, 인덱스 버퍼에 보관된 인덱스를 통해 정점 버퍼의 정점을 읽어오게 된다. 만약 인덱스 버퍼를 사용하지 않고, 정점 버퍼만 사용했다면 필요한 정점의 갯수는 증가했을 것이다.


전체 코드

실행 결과


정리하면

  • 인덱스 버퍼를 사용하면 정점 버퍼에 소모되는 메모리를 줄일 수 있다.
  • DirectX9에서 인덱스 버퍼는 정점 버퍼와 유사한 방법으로 활용할 수 있다.

다음 주제: [DirectX9]지형 생성

2015. 3. 5.

[DirectX9]멀티 텍스처(Mulit Texture)와 라이트 맵핑(Light Mapping)

 이 내용을 이해하기 위해서는 DirectX9에서의 텍스처를 어떻게 사용하는지에 대해서 알고 있어야 한다. 멀티 텍스처는 전과는 다르게 텍스처 이미지를 하나가 아닌 둘 이상 사용하는 경우를 말한다. 여러 개의 텍스처를 사용하는 것이 뭐가 중요하냐고 생각할 수도 있지만, DirectX9에서는 한 번에 결합할 수 있는 텍스처 스테이지의 갯수를 제한하기 때문에 주의해야 한다. 자신의 컴퓨터에서 이런 사항들을 확인할 수 있다. 윈도우 시작창에 "directx caps viewer"라고 입력해보자. 그러면 뷰어가 실행될 것이다. 아래의 스크린샷을 참고하여 해당 항목을 찾아보자.


  • MaxTextureBlendStages: 텍스처 스테이지 갯수이다.
  • MaxSimultaneousTextures: 출력순간에 결합할 수 있는 텍스처의 갯수이다. 한 번에 8장의 텍스처를 중첩해서 사용할 수 있다는 의미이다. 
 MatTextureBlendStages가 8이고, MaxSimultaneousTextures가 2인 경우를 생각해보자. 2개의 텍스처를 블랜딩(Blend) 처리하고 싶다면 아래 이미지처럼 수행해야 할 것이다.


  arg1, arg2 두 개의 인자로 전달받은 텍스처를 블랜딩하여 다음 스테이지로 그 결과물을 넘기게 된다. 우리는 2개의 텍스처를 블랜딩하기 때문에 더이상의 스테이지로 진행되지 않을 것이다. 만약 3개의 텍스처를 블랜딩한다면 2개의 텍스처를 블랜딩한 결과와 나머지 하나의 텍스처를 블랜딩하기 위해서 하나의 스테이지를 더 통과해야 할 것이다. 내 컴퓨터의 경우 8개의 텍스처를 동시에 입력받아 처리할 수 있으므로 1개의 스테이지에서 3개의 텍스처를 동시에 블랜딩할 수 있을 것이다.

 서론이 길어졌다. 이제 본격적으로 멀티 텍스처와 라이트 맵핑에 대해서 알아보자.


라이트 맵핑(Light Mapping)


 위의 그림은 가장 기본적인 라이트 맵핑을 보여주는 예이다. 벽 텍스처와 빛 텍스처를 합성하여 오른쪽과 같은 결과를 만들어 낼 수 있다. 보통 오른쪽과 같은 결과를 만들어 내는 가장 단순한 방법은 벽 텍스처와 빛 텍스처를 D3DTOP_MODULATE 연산을 통해 합성하면 된다. 이는 코드를 살펴볼 때 좀더 자세하게 알아보겠다.

 벽 텍스처는 벽을 나타내기 위한 텍스처로 이해하면 끝이다. 하지만 빛 텍스처는 흔히 Light Map이라고 불린다. Light Map은 광원이 비추는 영역을 실시간으로 연산하는 것이 아니라 미리 계산하여 2D 이미지로 저장한 결과이다. 위의 Light Map의 경우 가운데 지점을 중심으로 퍼져나가는 전구가 발하는 빛을 표현했다고 생각하자. 벽에 광원을 설치하여 이를 하나하나 연산한 결과로 표현해도 되지만, 만약 이런 빛이 100개가 있고, 이 빛들이 모두 위의 경우처럼 벽에 비춰지고 있다면 어떻게 할 것인가? DirectX9에서 제공하는 광원의 수는 제한적이다. 제한적이지 않다고 해도 100개의 광원을 연산하는 것은 성능의 큰 부담을 줄 수 있다. 이런 문제를 해결하기 위해 고안된 방법이 바로 라이트 맵핑(Light Mapping)이다.

 전구와 같은 점 광원(Point Light)는 위치만 정해지면, 그 위치에서 일정한 범위에 빛을 발할 것이다. 그 영역은 2D 이미지로 미리 표현할 수 있을 것이다. 위 그림의 Light Map이 바로 그 이지미이다. 우리는 이 이미지를 프로그램이 실행될 때 한 번 로딩하여 벽 텍스처와 합성하여 사용하면, 광원을 배치하는 것에 비해 성능 향상을 이룰 수 있다. 그리고 Light Map을 사용하면 DirectX9에서 제공하지 않는 광원의 형태로 활용할 수 있을 것이다. 


라이트 맵핑을 구현해보자

 우리의 목표는 다음 벽 텍스처와 Light Map을 합성하여 라이트 맵핑을 완성하는 것이다.

 
 두 텍스처를 합성한 결과는 쉽게 상상할 수 있을 것이다. 그럼 두 텍스처를 합성하기 위해 필요한 요소들을 생각해보자.
 앞에서 설명한 내용을 정리한다고 생각하고 다시 소개하였다.

 이제 벽을 만들어야 한다. 벽은 간단하게 사각형으로 만들자. 사각형은 삼각형 폴리곤 2개로 처리하도록 하자.
 정점 버퍼에 담은 정점을 DrawPrimitive() 함수를 통해 사각형으로 표현한다. 6개의 정점으로 2개의 삼각형을 만들고자 한다. 여기서는 단순히 DrawPrimitive() 함수의 인자로 D3DPT_TRIANGLELIST를 전달한다. 인덱스 버퍼를 사용하고, D3DPT_TRIANGLESTRIP을 사용하면 4개의 정점으로 벽을 표현할 수 있지만 이번은 단순하게 처리하였다.

 벽을 만들었으니 이번에는 벽에 벽 텍스처를 입혀보도록 하자.
 다음은 이번 포스트에 가장 중요한 사항이 라이트 맵핑 코드를 보자.
 Render() 함수가 길어졌다. 라이트 맵핑 기능 때문이다. 길어진 만큼 내용이 복잡해진 것은 아니다. 걱정하지 말고 차근차근 살펴보자. SetTextureStageState() 함수를 이해하는 것이 가장 중요하다.
 SetTextureStageState() 함수의 첫 번째 인자는 텍스처 스테이지를 의미한다. 두 번째 인자 D3DTSS_COLOROP는 색상에 대한 처리를 하겠다는 의미이고, 세 번째 인자 D3DTOP_SELECTARG1는 이 스테이지의 input 값을 다른 처리를 하지 않고 바로 output으로 전달하겠다는 의미이다. 결국 스테이지 0의 텍스처, 즉 벽 텍스처의 컬러값이 output으로 전달되었다. D3DTSS_ALPHAOP의 의미는 알파값을 의미한다. 컬러값과 마찬가지로 세 번째 인자를 D3DTOP_SELECTARG1로 전달하였으므로 벽 텍스처의 알파값이 그대로 output으로 전달될 것이다. 결과적으로 0번 텍스처 스테이지에서는 벽 텍스처의 컬러값과 알파값이 output으로 전달되었고, 이 값은 1번 텍스처 스테이지로 전달된다.
 다음은 1번 텍스처 스테이지 단계에 대한 처리이다. 먼저 1번 스테이지에서 처리될 텍스처에 대해서 살펴보자. 1번 텍스처 스테이지에는 라이트 맵핑할 빛의 텍스처가 이미 존재한다. 그리고 0번 텍스처에서 output으로 1번 스트에지로 전달된 컬러값과 알파값이 있다. 그래서 1번 텍스처 스테이지에서 D3DTSS_COLOROP는 0번 스테이지와 달리 D3DTOP_MODULATE로 처리한다. 알파값에 대한 연산도 마찬가지이다. 두 값을 모두 D3DTOP_MODULATE로 처리한다고 설정하였으므로, 어떤 값을 D3DTOP_MODULATE 처리할 것인지 정해야 한다. D3DTSS_COLORARG1, D3DTSS_COLORARG2를 통해 인자를 정할 수 있다. D3DTA_TEXTURE는 해당 스테이지의 텍스처를 의미하고, D3DTA_CURRENT는 이전 텍스처 스테이지에서 전달된 값을 의미한다. 결과적으로 1번 인자는 1번 스테이지의 텍스처의 컬러값, 2번 인자는 0번 스테이지에서 전달된 텍스처의 컬러값을 의미한다. 알파값에 대한 처리는 컬러값와 완전히 동일하다.
 1번 텍스처 스테이지의 결과가 2번 텍스처 스테이지로 전달되었다. 하지만 우리는 더이상의 텍스처 처리가 필요하지 않다. 그러므로 남은 텍스처 스테이지는 더이상 텍스처 처리를 하지 않겠다고 설정해주어야 한다.

 이제 라이트 맵핑을 구현한 전체 코드를 보도록 하자.
 실행 결과는 다음과 같다.

정리하면

  • 라이트 맵핑을 활용하여 광원을 대신하는 표현을 나타낼 수 있다. 광원을 배치하는 것에 비해 성능 향상을 기대할 수 있다.
  • 텍스처 스테이지 최대 갯수는 정해져있다. 그리고 한 번에 합성할 수 있는 텍스처의 수도 정해져있다.
  • SetTextureStageState() 함수의 두 번째 인자와 세 번째 인자의 조합으로 라이트 맵핑을 구현할 수 있다.

2015. 2. 26.

[DirectX9]텍스처(Texture)

 3D 그래픽스에서 사실성을 부여할 수 있는 대표적인 방법 중 하나가 텍스처이다. 텍스처는 메시와 같은 기하물체의 표면에 이미지을 씌우는 방법이다. 이미지를 씌우면 기하물체로 표현하기에는 부담스러운 표현을 비교적 간단하게 표현할 수 있다.


텍스처 맵핑(Texture Mapping)


 위의 그림에서 왼쪽의 3D 물체는 와이어프레임으로 출력되고 있다. 이 물체를 구성하는 폴리곤들의 표면에 가운데에 있는 2D 이미지를 씌운다고 생각하자. 그 결과는 오른쪽과 같이 지구처럼 나타날 것이다. 3D 구와 구에 덮어씌울 2D 이미지 한 장으로 지구를 만들었다. 이처럼 텍스처 맵핑은 단순하지만 효과적으로 사실성을 표현할 수 있는 기법 중 하나이다.

 DirectX9에서 텍스처 맵핑을 하는 과정은 다음과 같다.
  1. Direct3DTexture9 인터페이스 선언: 텍스처를 사용하기 위한 변수
  2. 텍스처 좌표를 갖는 정점 선언: 사용자 정의 정점 구조체에서 선언
  3. 텍스처 생성: 일반적으로 파일을 읽어서 생성
  4. 텍스처 스테이지 설정: 텍스처 스테이지는 텍스처의 좌표 생성 및 랩핑 모드 등 텍스처 제어에 관한 설정
  5. 그려질 텍스처를 지정: D3D 디바이스에 텍스처를 알린다.
  6. 메시 그리기
 각각의 과정에 대해서 좀더 살펴보면 정점 버퍼를 생성할 때도 인터페이스가 필요했듯이, 텍스처를 사용하기 위해서도 텍스처 인터페이스가 필요하다. 그리고 사용자 정의 정점 구조체의 정점이 텍스처 상의 uv 좌표계에서 어디에 맵핑될지도 설정하여야 한다. 사용자 정의 정점 구조체에 텍스처 좌표값을 위한 변수가 추가되었으므로 FVF 플래그도 변경해주어야 한다. 그 다음 텍스처를 생성한다. 텍스처는 D3D 디바이스를 통해 D3DXCreateTextureFromFile() 함수를 호출하여 생성할 수 있다. 텍스처를 생성한 후, 텍스처를 어떻게 제어할 것인지에 관한 정보를 텍스처 스테이지를 통해 설정한다. 텍스처와 정점의 색을 섞을지 말지 등을 결정한다. 그리고 생성한 텍스처에 인덱스를 부여하여 D3D가 텍스처를 관리할 수 있도록 한다. 

 지금까지 설명한 과정을 코드로 다시 살펴보자. 아래의 코드는 텍스처 맵핑을 하는 핵심적인 부분들을 발췌한 것이다. 완성된 코드가 아님을 알려둔다.

 아래의 이미지는 텍스처로 활용한 이미지이다. 텍스처를 생성할 때 파일명을 "sami.jpg"로 지정하였으므로 확인하도록 하자.



원통에 위의 이미지를 씌워보자
 #ifdef...#endif 선언이 자주 눈에 띌 것이다. 모르는 사람은 #ifdef...#endif에 대한 포스트을 확인하도록 하자. 어려운 것이 아니므로 금방 이해할 수 있을 것이다. SHOW_HOW_TO_USE_TCI가 선언되어 있는지 여부에 따라 컴파일이 달라진다. SHOW_HOW_TO_USE_TCI가 선언되지 않는 경우에는 알고리즘을 통해서 각각 정점에 해당하는 uv 좌표를 하나하나 설정해주고 있다. 위 이미지의 해상도는 100 * 100이다. uz 좌표계에서 (100, 0) 픽셀은 (1, 0)에 맵핑된다. (100, 100) 픽셀은 (1, 1)에 맵핑된다. 

 원기둥에 텍스처는 어떻게 맵핑되는지 살펴보자. 정점 버퍼 0번째 정점의 uv 좌표는 (0, 1)이다. 1번째 정점의 uv 좌표는 (0, 0)이다. 2번째 정점의 uv 좌표는 (1/49, 1), 3번째 정점의 uv 좌표는 (1/49, 0)이 된다. 결국 정점의 uv 좌표는 텍스처 이미지를 세로로 50등분한 수치가 정점에 맵핑된다. 

 참고로 텍스처로 사용할 파일은 main 함수가 실행되는 소스 파일과 같은 경우에 있거나, 그보다 한 단계 상위 폴더에 위치해야 한다. 이는 OS가 제공하는 현재 디렉토리(Current Directory) 개념과 관련이 있으므로 생소한 사람은 찾아보면 좋을 것 같다.


정리하면
  • 텍스처 이미지는 uv 좌표를 가진다.
  • 텍스처 맵핑을 하기 위해서 필요한 과정을 숙지해두자.