[DirectX11]3. 초기화 – DirectX11 API를 이용한 빈 화면 렌더링 하기

DirectX11 API를 이용해서 빈 화면 렌더링 해보자. 앞서 이전글를 통해 기본적인 객체들을 생성했고 이 객체들을 이용해서 화면을 렌더링할 것이다.


DirectX11 API를 이용한 렌더링은 매 프레임마다 새로운 내용들을 항상 갱신해서 보여줘야 한다. 따라서 메시지 큐를 기반으로 동작하는 루프의 수정이 필요하다.

C++
while ( GetMessage( &msg, nullptr, 0, 0 ) )
{
    if ( !TranslateAccelerator( msg.hwnd, hAccelTable, &msg ) )
    {
        TranslateMessage( &msg );
        DispatchMessage ( &msg );
    }
}

수정 전 메시지 루프 코드

C++
while( true )  
{  
    if ( PeekMessage( &msg, nullptr, 0, 0, PM_REMOVE ) )  
    {  
       TranslateMessage( &msg );  
       DispatchMessage ( &msg );  
     
       if( msg.message == WM_QUIT ) break;  
    }  

    RenderFrame();
}

수정 후 메시지 루프 코드

수정된 메시지 루프는 간단하다. 어플리케이션이 종료되기 전까지 계속 무한 루프를 돌면서 메시지 처리를 하고, RenderFrame()을 호출하는 것이다. DirectX11 API를 이용해 렌더링 하는 코드는 RenderFrame()안에서 이루어진다고 보면 된다. 그리고 1초동안 호출되는 RenderFrame()함수의 호출 횟수가 곧 FPS( Frame Per Second )가 된다.


우선 DirectX11 API를 이용해 렌더링하는 과정에 대해 대략적으로 알아야 할 부분이 있다.

렌더링 과정

자세한 내용들은 차차 하나씩 설명이 될 것이다. 여기에서 중요한 점은 GPU에서 렌더링 파이프라인 스테이지들을 거쳐 만들어진 렌더링 최종 결과물을 Render Target의 형태로 가져올 수 있다는 것이다. 이 렌더 타겟은 화면에 보여지는 결과물이므로 Texture2D형태의 리소스가 된다.

C++
ID3D11DeviceContext::void OMSetRenderTargets
(
    [in] UINT NumViews,
    [in, optional] ID3D11RenderTargetView * const *ppRenderTargetViews, 
    [in, optional] ID3D11DepthStencilView *pDepthStencilView
);

OMSetRenderTargets가 렌더링 파이프라인의 최종 결과물 ( Output-Merge 스테이지 )을 받을 렌더 타겟을 지정하는 함수이다. 렌더 타겟은 리소스이다. 이 리소스를 파이프라인에 연결하기 위해서는 Resource View를 사용해야 한다. 렌더 타겟의 용도로서 렌더링 파이프라인에 연결하기 때문에 RenderTargetView로 파라메터를 넘겨주게 된다. 이 함수에 대한 레퍼런스는 OMSetRenderTargets에 있다.

RenderTargetView자체가 리소스는 아니기 때문에 이 렌더 타겟 뷰 객체를 생성하고 이 뷰가 가리키는 리소스를 연결해주어야 한다. 리소스도 생성이 먼저 되어 있어야 한다. 이전글에서 스왑 체인을 생성할 때 DXGI_SWAP_CHAIN_DESC를 통해서 화면에 표시할 내용을 담는 버퍼를 생성했다. 이 버퍼를 렌더링 파이프라인에 연결해서 렌더링된 최종 결과물을 화면에 렌더링할 것이다.

렌더링 파이프라인과 렌더 타겟, 스왑 체인간의 시퀀스

우선 스왑체인으로부터 백 버퍼를 가리키는 객체를 가져와야 한다.

C++
ID3D11Texture2D* Texture; 
SwapChain->GetBuffer( 0, __uuidof( ID3D11Texture2D ), ( LPVOID* )&Texture );

이제 Texture는 스왑 체인에 있는 0번 버퍼를 가리키게 된다. 이 Texture를 수정하게 되면 곧 백 버퍼의 내용이 수정되는 것이다. 이 버퍼를 렌더링 파이프라인에 연결하여 결과물이 반영되도록 하면 된다. 렌더링 파이프라인에 연결하기 위해서는 리소스 뷰를 사용해야 한다.

C++
ID3D11RenderTargetView* RenderTargetView;
Device->CreateRenderTargetView( Texture, nullptr, &RenderTargetView );

이제 RenderTargetViewTexture를 가리키게 된다. 이 RenderTargetView를 렌더링 파이프라인에 연결하면 렌더 타겟 관련 준비는 모두 끝이 난 것이다.

C++
DeviceContext->OMSetRenderTargets( 1, &RenderTargetView, nullptr );

Texture는 백 버퍼를 가리키는 객체로 SwapChain->GetBuffer()에서 새롭게 생성된 COM객체이므로 사용을 모두 다 했기 때문에 Release가 필요하다. 이 객체를 Release한다고 해서 백 버퍼 자체가 사라지는건 아니다. 어디까지나 백 버퍼를 가리키고 수정할 수 있는 객체를 해제시키는 것이다.

C++
Texture->Release();


C++
D3D11_VIEWPORT Viewport;
ZeroMemory( &Viewport, sizeof( D3D11_VIEWPORT ) );

Viewport.TopLeftX = 0;
Viewport.TopLeftY = 0;
Viewport.Width    = 1920;
Viewport.Height   = 1080;

DeviceContext->RSSetViewports( 1, &viewport );

뷰포트 속성들을 지정하고 렌더링 파이프라인에 설정하는 코드

뷰포트 설정은 렌더링 파이프라인에서 렌더링 결과물의 크기를 지정하는 것이다. 위 코드에 따르면 렌더링 파이프라인은 최종 레스터라이즈 단계에서 1920 x 1080크기로 결과물을 만들어낸다.

여기서 의문점이 하나 발생한다. 뷰 포트의 크기와 백 버퍼의 크기가 서로 다르면 어떻게 될까? 이건 큰 문제가 되지 않는다. GPU는 렌더링 파이프라인에서 뷰포트 크기의 결과물을 백 버퍼에 저장할 때 크기가 서로 다르면 스케일링 과정을 거치게 된다. 즉 뷰 포트 크기가 백 버퍼 크기보다 작으면 결과적으로 작은 이미지를 확대하게 되므로 흐릿한 결과물을 얻게 되고 뷰 포트 크기가 백 버퍼 크기보다 크면 큰 이미지를 작게 축소하게 되는 것이다.

백 버퍼의 크기 지정은 스왑 체인을 만들 때 DXGI_SWAP_CHAIN_DESC을 통해서 하게 된다.

C++
DXGI_SWAP_CHAIN_DESC scd;

// 생략
scd.BufferDesc.Width  = 1920;
scd.BufferDesc.Height = 1080;
// 생략

D3D11CreateDeviceAndSwapChain( nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, nullptr, nullptr, nullptr, D3D11_SDK_VERSION, &scd, &Swapchain, &Device, nullptr, &DeviceContext );

이처럼 크기를 지정할 수 있다. 따로 지정을 하지 않으면 내부에서 전달받은 윈도우 핸들을 가지고 해당 창의 크기만큼 백 버퍼의 크기가 지정된다. 창의 크기가 1920 x 1080의 크기이고 따로 BufferDesc.Width, BufferDesc.Height를 지정하지 않으면 백 버퍼도 1920 x 1080 크기로 지정되어 생성되는 것이다.


이제 렌더링 파이프라인의 결과물을 화면에 표시할 모든 준비는 끝났다. 일단은 따로 렌더링할 내용이 없기 때문에 빈 화면만 표시하도록 한다. 백 버퍼에 연결된 렌더 타겟 뷰를 특정 색상으로 초기화하고 화면에 표시 명령을 하면 된다.

C++
float color[ 4 ] = { 1.0f, 1.0f, 1.0f, 1.0f };
DeviceContext->ClearRenderTargetView( RenderTargetView, color );
SwapChain->Present( 0, 0 );


위 내용들을 포함하고 있는 클래스를 작성하였다.

C++
class CRD11Renderer  
{  
private:  
    ID3D11RenderTargetView* RenderTargetView = nullptr;  
      
public:  
    /// Initialize renderer.  
    void Initialize( unsigned int Width, unsigned int Height );  
  
    // Clear render target.  
    void ClearRenderTarget() const;  
  
    // Present.  
    void Present() const;  
  
private:  
    // Initialize render target.  
    void _InitializeRenderTarget();  
      
    // Initialize viewport.  
    void _InitializeViewport( float Width, float Height ) const;  
};

C++
//----------------------------------------------------------------------------------------------------------- 
/// Initialize renderer.  
//----------------------------------------------------------------------------------------------------------- 
void CRD11Renderer::Initialize( unsigned int Width, unsigned int Height )  
{  
    _InitializeRenderTarget();  
    _InitializeViewport( (float)( Width ), (float)( Height ) );  
}  
  
//----------------------------------------------------------------------------------------------------------- 
/// Clear render target.  
//-----------------------------------------------------------------------------------------------------------
void CRD11Renderer::ClearRenderTarget() const  
{  
    float color[ 4 ] = { 1.0f, 1.0f, 1.0f, 1.0f };  
    GD11.GetDeviceContext()->ClearRenderTargetView( RenderTargetView, color );  
}  
  
//-----------------------------------------------------------------------------------------------------------
/// Present.  
//-----------------------------------------------------------------------------------------------------------
void CRD11Renderer::Present() const  
{  
    GD11.GetSwapChain()->Present( 0, 0 );  
}  
  
//-----------------------------------------------------------------------------------------------------------
/// Initialize render target.  
//-----------------------------------------------------------------------------------------------------------
void CRD11Renderer::_InitializeRenderTarget()  
{  
    ID3D11Texture2D* texture = nullptr;  
    GD11.GetSwapChain()->GetBuffer( 0, __uuidof( ID3D11Texture2D ), ( LPVOID* )&texture );  
  
    if ( !texture ) return;  
  
    GD11.GetDevice()->CreateRenderTargetView( texture, nullptr, &RenderTargetView );  
  
    if ( !RenderTargetView ) return;  
  
    GD11.GetDeviceContext()->OMSetRenderTargets( 1, &RenderTargetView, nullptr );
    
    texture->Release();  
}  
  
//-----------------------------------------------------------------------------------------------------------
/// Initialize viewport.  
//-----------------------------------------------------------------------------------------------------------
void CRD11Renderer::_InitializeViewport( float Width, float Height ) const  
{  
    D3D11_VIEWPORT viewport;  
    ZeroMemory( &viewport, sizeof( D3D11_VIEWPORT ) );  
  
    viewport.TopLeftX = 0;  
    viewport.TopLeftY = 0;  
    viewport.Width    = Width;  
    viewport.Height   = Height;  
  
    GD11.GetDeviceContext()->RSSetViewports( 1, &viewport );  
}

디바이스들을 초기화한 후 렌더 타겟과 뷰포트 관련 설정도 초기화해주어야 한다.

C++
int APIENTRY wWinMain( _In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow )  
{  
    // 코드 생략

    constexpr int width  = 1920;  
    constexpr int height = 1080;  
  
    HWND hWnd = CreateWindowW( szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, width, height, nullptr, nullptr, hInstance, nullptr );  
    if ( !hWnd ) return false;
  
    GD11.Create( hWnd );
    GD11Renderer.Initialize( width, height ); // 추가된 코드

    // 코드 생략
}

수정된 메세지 큐 함수에 추가된 RenderFrame은 다음과 같이 매 프레임 렌더 타겟을 초기화 하고 화면에 표시하게 된다.

C++
void RenderFrame()  
{  
    // GD11Renderer은 CRD11Renderer의 전역 인스턴스이다.
    GD11Renderer.ClearRenderTarget();  
    GD11Renderer.Present();  
}

이렇게 DirectX11 API를 이용해서 빈 화면을 렌더링해 보았다. 다음은 간단한 삼각형 하나를 렌더링 해보도록 하자.

여기까지의 코드는 yunei0313/CRY at Empty-screen-rendering에서 확인할 수 있다.

이전글 : [DirectX11] 2. 초기화 – 디바이스 객체들을 만들어 보자.

Leave a Comment