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() 함수를 호출한다.

댓글 없음:

댓글 쓰기