본문 바로가기
게임 공부/DirectX

DirectDraw 학습 (1) - Initialization

by woohyeon 2020. 11. 15.
반응형

학습 목적의 글입니다.

자료: www.youtube.com/watch?v=J0MNKUYw1zY

영천님께서 Direct3D 학습 전에 DirectDraw 좀 익히고 시작하면 도움이 된다고 하니.. 특히 비트맵과 관련하여 세부적인 내용들을 경험해 볼 수 있다고 하시니 믿고 공부!


학습 전에 컴퓨터 그래픽스에 대해 공부를 하고 오면 더욱 이해가 잘 될 것이다.

우선 다음은 DirectDraw 객체를 사용하기 위해 필요한 기본적인 변수들이다. 초기화 과정에서 아래 변수들을 초기화할 것이다.

LPDIRECTDRAW         pDD;
LPDIRECTDRAW7        pDD7;
LPDIRECTDRAWSURFACE7 pDDPrimary;
LPDIRECTDRAWSURFACE7 pDDBack;
LPDIRECTDRAWCLIPPER  pClipper;

DirectDraw를 사용하려면 가장 기본이 되는 DirectDraw에 대한 인스턴스를 생성해야 한다. DirectDraw는 Direct7 까지 존재하기에, 사용 가능한 버전 중 최신인 7 버전의 타입을 사용한다. 7 버전에 맞는 타입의 변수가 pDD7 이다. 따라서 인스턴스는 pDD7에 저장이 될 것이다.

그런데 그전에 선언된 pDD7과 비슷한 타입의 변수 pDD가 보인다. 유추해 보았을 때 타입 자체는 동일한데 숫자만 없으니 초기 버전일 것이라 예측할 수 있다. 최신 버전 타입의 객체를 담을 변수가 선언되었음에도 불구하고 초기 버전 객체가 필요한 이유는 DirectDraw의 구조 때문이다. 그리고 DirectDraw 뿐만 아니라 DirectX와 관련된 모든 객체들이 그렇다. 이는 COM과 관련이 있는데 COM에 대한 정확한 이해는 나도 아직 못했으므로.. 자세한 내용은 여기를 참조하고, 여기선 간단한 설명만 한다.  

COM은 MS가 만든 일종의 메커니즘인데, DirectDraw의 객체들에 이 메커니즘이 적용되어 모두 공통적인 인터페이스를 가지고 있다. 그 중에서도 모든 객체는 IUnknown 이라는 인터페이스를 상속받는데 이 안에 QueryInterface, AddRef, Release 라는 세 함수를 품고 있다. 모든 COM 객체엔 레퍼런스 카운팅이 적용된다. Release는 레퍼런스 카운트를 1 감소 시키는데, 해당 객체를 더 이상 사용하지 않을 때 사용한다. 나머지는 QueryInterface만 기억하면 된다. 

QueryInterface는 id와 output 파라미터를 받는다. id에 해당하는 객체에 대한 포인터를 얻을 수 있다면 output 파라미터에 저장하고, 그렇지 않다면 NULL을 반환한다. 즉 인자로 버전 7에 대한 ID를 넘기면, 버전 7 객체에 대한 포인터를 얻을 수 있는지 '질의' 한다. 이를 위해 가장 초기 버전인 pDD를 통해 QueryInterface를 호출한다. 즉 초기 버전 객체를 이용하여 실질적으로 사용할 필요한 버전의 포인터를 얻는 것이다. (참고로 AddRef는 자동으로 QueryInterface 성공 시 호출된다.) 

pDDPrimarypDDBack은 쉽게 말해 전면 버퍼(Front buffer)와 후면 버퍼(Back buffer)이다.(살짝 다르지만) 나는 Direct3D를 아주 쬐~금 배워서 대략 무엇인지 알고 있다. 모르는 분들을 위해 간단하게 설명을 해보면, 일단 우리가 보는 2D 화면에 그림 등을 출력하려면 출력할 데이터를 저장한 버퍼가 필요하다. 즉 화면에 보여줄 장면(Scene)들을 버퍼에 그린다고 표현한다. 그런데 하나의 버퍼만 사용하면 다음 장면을 그릴 때 약간의 문제가 생긴다. 버퍼를 화면에 출력하는 동안은 그 버퍼를 수정하지 못한다. 즉 다음에 출력할 장면을 미리 그려놓을 수가 없다. 따라서 모두 출력 후 버퍼를 clear 했다가 다음 장면을 그려야 하는데 이 잠깐의 공백기 때문에 화면이 깜빡이는 것처럼 보인다. 

그래서 화면에 출력하기 위한 전면 버퍼와 출력하는 동안 다음 장면의 데이터를 미리 그려 놓을 후면 버퍼, 총 2개의 버퍼를 사용하기로 한다. 출력이 끝나면 후면 버퍼의 데이터를 한번에 전면 버퍼로 복사한다. 이렇게 하면 깜빡임이 없어진다. 이러한 기법을 더블버퍼링(double buffering)이라 하는데 아마 모든 그래픽 라이브러리에서 이 방식을 사용하는 것으로 알고 있다. 용어는 다를 수 있다. 예를 들면 스왑체인(swap chain) 등.. 내용도 조금씩은 다르지만 어쨌든 기본 원리는 같다. 이를 위한 타입이 LPDIRECTDRAWSURFACE7 이며 전면, 후면을 위한 2개의 변수가 선언되어 있다. 전면 버퍼는 우리 눈에 보이는 화면 그 자체라고 생각해도 된다.

http://www.comscigate.com/JDJ/archives/0803/birken/index.html

 

마지막으로 pClipper는 클리핑을 수행하는 객체다. 클리핑이란 컴퓨터 그래픽스에서 보여줄 필요가 없는 부분을 제외시키는 행위를 말한다. 좀 더 자세한 내용은 표면에 대해 더 설명 후 보충한다.

 

지금까지의 내용을 정리해보면, 우선 초기 버전의 DirectDraw 인스턴스를 생성한다. 그리고 이를 통해 DirectDraw7 버전의 인스턴스를 얻는다. 그리고 전면 버퍼와 후면 버퍼를 생성하고 클리퍼를 생성. 이 정도이다. 이를 예외 처리 없이 간단하게 코드로 표현해보면 다음과 같다.

LPDIRECTDRAW            pDD;		 		 
LPDIRECTDRAW7           pDD7;		 
LPDIRECTDRAWSURFACE7    pDDPrimary;	 
LPDIRECTDRAWSURFACE7    pDDBack;	 
LPDIRECTDRAWCLIPPER     pClipper;

// 가장 초기 버전의 DirectDraw 인스턴스를 얻습니다.
DirectDrawCreate(nullptr, &pDD, nullptr);

// pDD를 통해 7 버전의 DirectDraw 인스턴스를 얻습니다.
pDD->QueryInterface(IID_IDirectDraw7, (LPVOID*)&pDD7);

// Surface를 생성하기 위해 필요한 사전작업
// 보통 윈도우 모드 또는 풀스크린 모드로 설정
// 해당 모드는 윈도우 모드를 의미
pDD7->SetCooperativelLevel(hWnd, DDSCL_NORMAL);

// 전면 버퍼를 생성합니다.
pDD7->CreateSurface(&ddsd, &pDDPrimary, nullptr);

// 후면 버퍼를 생성합니다.
pDD7->CreateSurface(&ddsd, &pDDBack, nullptr);

// Clipper를 생성하고 전면 버퍼에 적용합니다. 
pDD7->CreateClipper(0, &pClipper, nullptr);
pClipper->SetHWnd(0, hWnd);
pDDPrimary->SetClipper(pClipper);

 

DirectDrawCreate: DirectDraw 인스턴스를 생성하여 두 번째 인자에 저장한다. 그러나 이 함수를 통해 생성된 인스턴스가 7 버전이라면, 7 버전의 모든 기능을 사용할 순 없다. 즉 해당 함수로 7 버전의 인스턴스를 생성하게 되면 그 인스턴스는 불완전한 인스턴스가 된다. 이를 위해 2가지 방법이 존재한다. 첫번 째는, 초기 버전을 생성 후 QueryInterface를 통해 원하는 버전의 인스턴스를 얻을 수 있다. 두 번째로, DirectDrawCreateEx 함수를 사용하면 7 버전의 인스턴스 얻을 수 있다. 굳이 Ex 함수를 사용하지 않고, 초기 버전을 통해 7 버전을 얻은 이유는 아마 COM 객체가 이런식으로 동작할 수 있다는 것을 알려주기 위한 의도가 아닌가 싶다.

QueryInterface: 첫 번째 인자에 전달된 ID 버전에 대한 인스턴스를 요청한다. 만약 사용 가능하다면 두 번째 인자에 저장되며, 그렇지 않을 경우 NULL이 저장된다. 위에서 말했듯이 DirectDrawCreateEx 사용 시 이 함수를 호출할 필요는 없다.

SetCooperativeLevel: 협력 레벨을 설정한다. 협력 레벨이 정확히 무엇인지는 와닿지 않는다. 대충 가장 top-level의 윈도우, 즉 메인 윈도우의 값을 설정하는 것으로 보인다. 반드시 메인 윈도우를 생성한 스레드와 동일한 스레드가 호출해야 하며, 첫 번째 인자인 윈도우 핸들은 반드시 메인 윈도우의 핸들이여야 한다. 두 번째 인자로 플래그를 받으며 OR(|)을 통해 여러 플래그를 같이 사용할 수 있다. 반드시 DDSCL_EXCLUSIVE 또는 DDSCL_NORMAL 중 1개는 설정되어야 한다고 한다. 두 플래그를 함께 사용할 수 없다. DDSCL_NORMAL이 전형적인 윈도우즈 애플리케이션을 의미하는 플래그라 한다. 이것이 창모드를 의미하는 것인지는 확실치 않다. 하지만 풀스크린의 경우 DDSCL_EXCLUSIVE을 사용해야 하는 것은 확실하다. DDSCL_EXCLUSIVE 플래그를 사용할 경우 반드시 DDSCL_FULLSCREEN 플래그와 함께 사용하여야 한다. 더 자세한 내용은 여기를 참고.

CreateSurface: Surface를 생성한다. 첫 번째 인자로 DDSURFACEDESC2 타입에 대한 포인터를 받는다. 두 번째 타입은 surface 인스턴스를 저장할 변수이며, 세 번째 인자는 사용되지 않으므로 반드시 NULL을 대입한다. DDSURFACEDESC2 타입은 생성할 surface에 대한 설정 옵션들을 가진 구조체이다. 이 구조체를 통해 surface의 전면 버퍼, 후면 버퍼와 같은 용도를 플래그를 통해 설정해주어야 한다. 그중 눈에 띄는건 DDSCAPS_BACKBUFFER, DDSCAPS_FRONTBUFFER, DDSCAPS_PRIMARYSURFACE가 있었다. 단순히 전면 버퍼를 부르는 이름만 다른 줄 알았는데 플래그 자체가 다른걸 보니 다른 의미인지 혼란이 왔다. 한 한시간동안 조사를 해봤는데 깔끔한 결론을 얻을 순 없었고, 알아낸 건 다음과 같다. 

우선 풀스크린 모드와 윈도우 모드가 더블 버퍼링을 사용하는 방법이 살짝 다르다. 풀스크린 모드에선 Flip 이란 것을 이용하며, 윈도우 모드에선 Blit(Bit blit, blt 등 모두 동일)을 이용한다. 두 모드는 모두 두 개의 버퍼를 이용하는 것은 맞다. 다만 Flip은 단순히 포인터swap하는 방식으로 버퍼를 교체하고, 윈도우 모드는 버퍼의 내용을 copy 하는 식(copy를 blit라고 함)이다. 아마 풀스크린의 경우 화면 전체를 독점하기 때문에 단순한 포인터 교환을 허용하는 것이 아닌가 싶다. 그렇다고 풀스크린 모드에서 blit를 사용하지 않는 것은 아니다. 몇몇 이미지는 오프스크린에 그린 뒤 보조 표면에 blit 후 flip하기도 한다.  

Flip을 사용할 경우 주 표면과 보조 표면은 각각 DDSCAPS_PRIMARYSURFACEDDSCAPS_BACKBUFFER 플래그를 사용한다. 반면에 copy로 할 경우 각각 DDSCAPS_PRIMARYSURFACEDDSCAPS_OFFSCREENPLAIN이라는 옵션을 사용한다. 그리고 copy 방식에선 보조 표면이란 용어를 사용하지 않는 것 같다. 혼용 하는 것 같다. 하지만 혼용한다면
오프스크린이라는 것을 알릴 필요성이 있어 보인다. 오프스크린의 사전적 의미는 보이지 않는 화면이다.
MSDN에 오프스크린 플래그는 다음과 같이 설명되어 있다. 

This surface is any offscreen surface that is not an overlay, texture, z-buffer, front-buffer, back-buffer, or alpha surface. It is used to identify plain surfaces.

윈도우 모드에서 보조 표면이란 용어를 사용하더라도 오프스크린이라는 것만 기억하자. 그리고 한번 생각해봐도 이해가 되긴한다. Flip일 경우 말은 주 표면, 보조 표면이지만 포인터를 교체하는 것이기 때문에 결국은 표면 자체를 바꾸는 것이다. 즉 보조 표면이 주 표면이 될 수 있다. 그래서 Flip의 보조 표면에 오프스크린이란 용어는 적절치 않다. 반면에 Blit를 사용할 경우 보조 표면은 화면에 나오지 않고, 오로지 copy를 통해 내용만 복사가 된다. 즉 표면의 역할이 고정되어 있다. 그래서 윈도우 모드의 보조 표면에만 오프스크린 플래그를 사용하는 것이 아닌가 싶다. 

+ 추가 내용)
표면에 대해 조금 더 설명해 보자면, 아까 전면 버퍼, 즉 주 표면은 우리가 보는 화면 그 자체로 생각해도 된다 했다. 그 이유는 주 표면은 모니터 해상도와 동일한 스크린을 의미하고 비디오 메모리에 존재하는 표면이기 때문이다. 즉 주 표면은 1개만 존재하는 특수한 화면이며, Flip용 보조 표면(백 버퍼)은 여러개 존재할 수 있다. 그 밖의 오프스크린이라는 표면은 크기가 해상도와 동일할 필요가 없다. 따라서 주 표면은 풀스크린이며, 윈도우 모드에서도 주 표면은 풀스크린이다. 즉 윈도우 모드에서 주 표면의 (0, 0) 좌표는 윈도우 내부의 (0, 0)이 아닌 풀스크린 기준의 (0, 0)을 의미한다. 이를 윈도우 크기에 맞게 나타내기 위해서 Clipper를 메인 윈도우의 핸들(hWnd)과 주 표면에 등록해서 사용해야 하는 것이다.  

DDSCAPS_FRONTBUFFER 플래그는 어디에 사용되는지 모르겠다.. 그리고 surface는 플래그를 통해 시스템 메모리(일반 RAM) 또는 비디오 메모리(VRAM)를 사용하도록 지정할 수 있다.

풀 스크린 모드에서 주 표면/보조 표면을 설정 및 생성하려면 다음과 같은 과정들이 필요하다.

///////////////////
// FullScreen Mode
///////////////////

// 풀스크린 모드로 설정
pDD7->SetCooperativeLevel(hWnd, DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN);

// 출력 모드 설정 (풀스크린 시 필요)
pDD7->SetDisplayMode(dwWidth, dwHeight, dwBPP, 0, 0);

// 주 표면의 상세 정보를 기술
// DDSCAPS 및 dwBackBufferCount 사용 위해 해당 플래그 set 필요
// 주 표면 용도로 사용, FLIP 사용, 2개 이상의 surface 사용
// 사용할 보조 표면(후면 버퍼)의 개수 설정
DDSURFACEDESC2 ddsd = {};
ddsd.dwSize = sizeof(DDSURFACEDESC2);
ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT; 
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX;
ddsd.dwBackBufferCount = 1;

// 주 표면 생성
pDD7->CreateSurface(&ddsd, &pDDPrimary, nullptr);

// 보조 표면을 위한 데이터 set
// BackBuffer 플래그 set
DDSCAPS2 ddscaps;
ZeroMemory(&ddscaps, sizeof(ddscaps)); 
ddscaps.dwCaps = DDSCAPS_BACKBUFFER;

// 주 표면과 연결된 보조 표면을 얻습니다.
pDDPrimary->GetAttachedSurface(&ddscaps, &pddBack);

보조 표면은 CreateSurface가 아닌 GetAttachedSurface 함수를 통해 얻는 것을 기억! 만약 생성할 surface가 시스템 메모리에 상주할 것인지 비디오 메모리에 상주할 것인지 지정하려면 dwCaps에 DDSCAPS_SYSTEMMEMORY 또는 DDSCAPS_VIDEOMEMORY 플래그를 추가하면 된다. COMPLEX 플래그는 주 표면에 연결된 surface가 1개 이상일 경우만 적용되는 것 같다. 즉 윈도우 모드에서는 주 표면과 연결된 surface가 아니므로 COMPLEX 플래그를 사용하면 안된다. 풀스크린 모드는 윈도우 모드와 다르게 DirectDraw의 지원을 통해 가능하다. 단순히 윈도우 모드와 크기만 다른 것이 아니기 때문에 SetDisplayMode 함수를 통해 출력 모드를 설정해 주어야 한다. bpp는 bits per pixel을 의미한다. 풀스크린 모드의 주 표면과 보조 표면은 기본적으로 해상도와 동일해야 하기 때문에 크기를 따로 설정하지 않는다.

 

윈도우 모드에서의 주 표면/보조 표면(offscreen) 생성이다.

///////////////////
// Window Mode
///////////////////

pDD7->SetCooperativeLevel(hWnd, DDSCL_NORMAL);

// 주 표면의 상세 정보를 기술
// DDSCAPS 사용 시 해당 플래그 set 필요
// 주 표면 용도로 사용
DDSURFACEDESC2 ddsd = {};
ddsd.dwSize = sizeof(DDSURFACEDESC2);
ddsd.dwFlags = DDSD_CAPS; 
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;

// 주 표면 생성
pDD7->CreateSurface(&ddsd, &pDDPrimary, nullptr);

// dwSize와 dwFlags는 동일하기 때문에 일부 값 그대로 사용
// dwWidth, dwHeight는 윈도우 작업 영역과 동일한 크기라고 가정
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
ddsd.dwWidth  = dwWidth;
ddsd.dwHeight = dwHeight;

// 보조 표면(Offscreen) 생성
pdd7->CreateSurface(&ddsd, &pDDBack, nullptr);

윈도우 모드에서의 offscreen은 CreateSurface 함수를 통해 생성한다. 윈도우 모드는 풀스크린을 사용하는 것이 아니기 때문에 보조 표면 역시 윈도우 크기와 동일한 크기를 설정해주어야 한다. (주 표면은 Clipper를 통해

 

참고 자료




댓글