본문 바로가기
Programming/etc

Initialize Direct3D 10

by 작은별하나 2011. 9. 29.
반응형

이 글은 Frank D. Luna가 쓴 "Introduction to 3D game programming with DirectX 10" 책을 기반으로 했습니다. 책의 내용을 그대로 옮기기 보다는 제 경험적인 것을 같이 적었습니다. DirectX 10 이상을 사용하실 분들에게 도움이 되었으면 합니다.

 

DirectX 10이 나온지도 꽤 되었고, 이제는 DirectX 11도 나왔는데, 정작 참고할 수 있는 도서는 별로 없더군요. 가능하면 DirectX 11을 해볼려고 했는데, 원서에도 DirectX 11 책은 몇 달전까지만 해도 나오지 않은 상태였습니다. 번역본이나 한글 서적으로는 DirectX 10조차도 못 구했고요. 아직까지 학교나 개발쪽에서는 DirectX 9이 주류인 것 같습니다만, 책을 읽다보니 DirectX 10 이상을 사용할 이유를 절실히 느꼈습니다.

 

DirectX 10은 이전버전에 비해서 보다 체계적이고, 쉐이더에 더 적합한 구조로 작성되었다는 느낌입니다. 특히 버퍼의 사용은 그동안 DirectX 9을 해보면서 답답했던 부분들을 해결하였더군요.

 

버퍼란 것은 결국 메모리 공간입니다. 이 메모리 공간에 3D 정점 정보를 담고 있으면 정점 버퍼(Vertex buffer)가 되는 것이고, 텍스처 정보를 담고 있으면 텍스처 버퍼(Texture buffer)가 되는 것 뿐입니다. DirectX 9에서는 이러한 버퍼에 유동성이 많지 않았죠. 물론 사용자 정의 정점(FVF)을 통해서 정점 버퍼에 유동성을 주었지만, 그 이상은 기대하기 힘들었죠.

 

일단 각설하고, 프로그램을 쫓아가면서 설명을 할까 합니다.

 

예제로 만든 DirectX 10 소스는 간단하게 검은 화면을 출력합니다. 여기서는 Direct3D 를 초기화하고 간단하게 화면의 배경색만 설정하는 것으로 마무리하니까요.

 

우선 창모드로 프로그램을 실행합니다. 창모드의 경우 창에 있는 클라이언트 영역의 해상도와 Direct3D의 해상도를 맞추어 주어야 합니다. 그렇지 않다면, 마우스 입력 등에서 치명적 문제를 발생시킵니다. 그 외에도 화면에 이미지를 표시할 때, 보간을 하기 때문에 출력 영상이 올바르게 나오지 않습니다.

 

클라이언트 영역을 원하는 해상도로 맞추어주기 위해서 저는 AdjustWindowRect 함수를 사용합니다. 이 윈도즈 API 함수는 원하는 윈도우 스타일에 따라서 주어진 클라이언트 영역을 생성해주는 창 해상도를 계산해줍니다. 옛날 책으로 공부한 사람들은 아직도 복잡한 방법을 사용하고 있더군요.

 

    //    Get window resolution parameters using AdjustWindowRect function.
    //    Clinet Area : 640x480
    int nWidth = 640, nHeight = 480;
    RECT rt = { 0, 0, nWidth, nHeight };
    AdjustWindowRect(&rt, WS_OVERLAPPEDWINDOW, TRUE);

    //    Create window for direct3d rendering
    HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, rt.right-rt.left, rt.bottom-rt.top, NULL, NULL, hInstance, NULL);
    if(!hWnd)
    {
        MessageBoxW(hWnd, L"Creating a window is failed", L"Error", MB_OK);
        return FALSE;
    }

 

다음으로는 Direct3D device를 생성하고 Swap Chain을 생성합니다. Swap Chain이라는 것은 여러장의 이미지 버퍼를 말합니다. 그림을 그릴 때, 화면의 출력시점과 그리는 시점이 맞지 않으면 화면이 분리되는 현상(Screen tearing)이 벌어집니다. 더 심각한 경우에는 그리는 과정이 보일 수도 있습니다. 그것을 해결하기 위해서 일반적으로 더블 버퍼링을 사용합니다. DirectX 10 에서는 Swap Chain을 이용하여 백 버퍼(Back buffer)를 생성합니다.

 

참고로 DirectX 9에서는 CreateDevice 메소드를 통해서 디바이스를 생성하는데, 프레임버퍼도 자동으로 생성합니다. DirectX 10에서는 프레임 버퍼를 일일이 만들어줍니다. 그렇게 함으로써 보다 쉐이더 사용 등에서 보다 유연해집니다.

 

DirectX 10에서는 버퍼를 만들 때, 버퍼에 대한 설정을 DESC 라 붙여진 파라미터를 통해서 전달됩니다. Swap chain의 경우에는 DXGI_SWAP_CHAIN_DESC 구조체가 그 역할을 합니다. 이 구조체에서 BufferDesc 항목은 프레임 버퍼를 만드는 역할을 합니다. 프레임 버퍼는 그림이 그려지는 공간입니다. SampleDesc는 프레임 버퍼에 Multi-sampling을 설정하게 됩니다. Multi-sampling은 하나의 점을 표시하기 위해서 Direct3D에서는 몇 개의 점을 그릴 것인가입니다. 일반적으로 x1 과 x4를 가장 많이 사용합니다. x1은 그리는 해상도와 표시되는 해상도가 같은 경우이고요. x4는 그리는 해상도가 표시되는 해상도보다 4배 많은 경우입니다. 즉 4개의 점이 하나의 점으로 표시되기 때문에 자연스럽게 계단현상을 제거해줍니다. 이런 것을 FSAA(Full Scene Anti Aliasing)이라고 합니다.

 

CreateDeviceAndSwapChain 함수를 통해서 Direct3D Device와 Swap chain을 생성합니다.

 

    //    Create direct3d device and swap chain.
    DXGI_SWAP_CHAIN_DESC sd;
    HRESULT hr;

    //    Buffer description
    sd.BufferDesc.Width = nWidth;
    sd.BufferDesc.Height = nHeight;
    sd.BufferDesc.RefreshRate.Numerator = 60;
    sd.BufferDesc.RefreshRate.Denominator = 1;
    sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
    sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;

    //    Multisampling description
    sd.SampleDesc.Count = 1;
    sd.SampleDesc.Quality = 0;

    //    Buffer usage & count, etc.
    sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    sd.BufferCount = 1;
    sd.OutputWindow = hWnd;
    sd.Windowed = TRUE;
    sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
    sd.Flags = 0;

    ID3D10Device *pD3DDevice;
    IDXGISwapChain *pSwapChain;
    hr = D3D10CreateDeviceAndSwapChain(0, D3D10_DRIVER_TYPE_HARDWARE, 0, 0, D3D10_SDK_VERSION, &sd, &pSwapChain, &pD3DDevice);
    if( FAILED(hr) )
    {
        MessageBoxW(hWnd, L"Creating direct3d device is failed", L"Error", MB_OK);
        return FALSE;
    }

 

다음은 렌더 타겟 뷰(Render Target View)를 생성합니다. 렌더링을 할 수 있는 타겟은 여러 개를 생성할 수 있기 때문에 렌더 타겟 뷰를 생성해주어야 합니다. DirectX 9에서는 프레임 버퍼와 렌더 타겟을 구분해서 사용했지만, DirectX 10에서는 이러한 구분이 없습니다. 프레임 버퍼 자체도 하나의 렌더 타겟이 됩니다.

pSwapChain->GetBuffer 함수에서 Swap chain으로부터 백 버퍼를 가져옵니다. 이 때, 버퍼의 참조횟수가 증가합니다. 그래서 CreateRenderTarget을 한 후에 해당 백 버퍼를 Release함으로써 버퍼의 참조횟수를 줄여주어야 합니다.

 

    //    Create the render target view
    ID3D10RenderTargetView *pRenderTargetView;
    ID3D10Texture2D *pBackBuffer;
    pSwapChain->GetBuffer(0, __uuidof(ID3D10Texture2D), (void **)&pBackBuffer);
    hr = pD3DDevice->CreateRenderTargetView(pBackBuffer, 0, &pRenderTargetView);
    if( FAILED(hr) )
    {
        MessageBoxW(hWnd, L"Creating render target view is failed", L"Error", MB_OK);
        return FALSE;
    }
    pBackBuffer->Release();

 

여기는 깊이 버퍼와 스텐실 버퍼를 만듭니다. 왜 깊이 버퍼와 스텐실 버퍼는 같이 다닐까요? 스텐실 버퍼를 보다 잘 이해하기 위해서는 이 질문에 대한 답변을 아셔야 합니다. 일단 그 질문에 대한 답은 나중에 제가 또 글을 쓸 때가 있을 듯 합니다. 깊이 버퍼의 해상도나 멀티 샘플링 파라미터는 Swap chain에서 생성하는 백 버퍼와 동일한 값을 사용해야 합니다. 깊이 버퍼는 텍스처 버퍼를 사용합니다. 대신 BindFlags에서 이 버퍼가 깊이 버퍼에 연결되어 있다는 것을 알려줍니다. 이 말은 깊이 버퍼의 내용을 손쉽게 화면에 출력해줄 수 있다는 것이겠죠.

 

    //    Create the Depth/Stencil Buffer and View
    D3D10_TEXTURE2D_DESC dsd;
    dsd.Width = nWidth;
    dsd.Height = nHeight;
    dsd.MipLevels = 1;
    dsd.ArraySize = 1;
    dsd.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
    dsd.SampleDesc.Count = 1;
    dsd.SampleDesc.Quality = 1;
    dsd.Usage = D3D10_USAGE_DEFAULT;
    dsd.BindFlags = D3D10_BIND_DEPTH_STENCIL;
    dsd.CPUAccessFlags = 0;
    dsd.MiscFlags = 0;

    ID3D10Texture2D *pDepthStencilBuffer;
    ID3D10DepthStencilView *pDepthStencilView;

    hr = pD3DDevice->CreateTexture2D( &dsd, 0, &pDepthStencilBuffer );
    if( FAILED(hr) )
    {
        MessageBoxW(hWnd, L"Creating Depth/Stencil Buffer is failed", L"Error", MB_OK);
        return FALSE;
    }
    hr = pD3DDevice->CreateDepthStencilView(pDepthStencilBuffer, 0, &pDepthStencilView);
    if( FAILED(hr) )
    {
        MessageBoxW(hWnd, L"Creating Depth/Stencil View is failed", L"Error", MB_OK);
        return FALSE;
    }

 

깊이 버퍼와 깊이 버퍼 뷰가 생성되었으면, 앞에서 생성한 렌더 타겟 뷰에 깊이 버퍼 뷰를 연결시켜줍니다. 여기서 보면 DirectX 9에서는 사용되지 않았던 생소한 함수 이름들을 발견하게 됩니다. OMSetRenderTargets 함수인데, DirectX 10에서 사용하는 새로운 함수 카테고리라고 생각하시면 좋습니다. OM은 Output Merger라고 출력을 담당하는 부분입니다.

 

    //    Bind the Views to the Output Merger Stage
    pD3DDevice->OMSetRenderTargets(1, &pRenderTargetView, pDepthStencilView);

 

뷰포트를 설정합니다. RSSetViewports 를 통하여 설정하는데 RS는 Rasterization Stage로 Screen space로 투영된 삼각형 내부의 픽셀들을 처리해줍니다. 뷰포트에 따라서 클리핑을 수행하게 됩니다.

 

    //    Set the Viewport
    D3D10_VIEWPORT vp = { 0, 0, nWidth, nHeight, 0.0f, 1.0f };
    pD3DDevice->RSSetViewports(1, &vp);

 

Direct3D 9에서도 마찬가지였지만, GetMessage 함수는 메시지가 도착할 때까지 무한히 기다리기 때문에 계속 그림을 그려주어야 하는데에서는 적합치 않습니다. 실제 GetMessage를 쓰고자 한다면 쓰레드를 통해서 그림을 그려주어야 합니다.

Clear*View 함수는 뷰들을 초기화합니다. Direct3D에서는 기본적으로 컬러값을 실수형을 사용합니다. 그래서 컬러값을 설정하기 위해서는 각각의 컴포넌트 값을 0.0 ~ 1.0 사이에 설정해주어야 합니다.

 

    //    Message loop
    msg.message = 0;
    while( msg.message != WM_QUIT )
    {
        if( PeekMessage(&msg, 0, 0, 0, PM_REMOVE) )
        {
            if(!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
            {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
        }
        else
        {
            static D3DXCOLOR kColor(0.0f, 0.0f, 0.0f, 1.0f);
            pD3DDevice->ClearRenderTargetView(pRenderTargetView, kColor);
            pD3DDevice->ClearDepthStencilView(pDepthStencilView, D3D10_CLEAR_DEPTH | D3D10_CLEAR_STENCIL, 1.0f, 0);
            pSwapChain->Present(0, 0);
        }
    }

    pD3DDevice->ClearState();

 

마지막으로 루프를 빠져나오면 ClearState 함수를 호출해서 Direct3D device를 종료하게 됩니다.

 

일단 무식하게 프로그램을 짰지만, 이렇게 작성하는 것은 좋은 방법이 아닙니다. 제일 먼저 해주어야 하는 것이 프레임웍을 작성해주는 것이라 생각합니다. 다음에 프레임웍을 통해서 Direct3D 10을 설명할께요.

 

 

728x90

'Programming > etc' 카테고리의 다른 글

PC 스크린 레코더  (0) 2014.12.09
Direct3D 10 글쓰기  (0) 2011.09.28

댓글