DirectX11 API를 이용해서 빈 화면 렌더링 해보자. 앞서 이전글를 통해 기본적인 객체들을 생성했고 이 객체들을 이용해서 화면을 렌더링할 것이다.
메시지 루프 수정
DirectX11 API를 이용한 렌더링은 매 프레임마다 새로운 내용들을 항상 갱신해서 보여줘야 한다. 따라서 메시지 큐를 기반으로 동작하는 루프의 수정이 필요하다.
while ( GetMessage( &msg, nullptr, 0, 0 ) )
{
if ( !TranslateAccelerator( msg.hwnd, hAccelTable, &msg ) )
{
TranslateMessage( &msg );
DispatchMessage ( &msg );
}
}
수정 전 메시지 루프 코드
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
형태의 리소스가 된다.
ID3D11DeviceContext::void OMSetRenderTargets
(
[in] UINT NumViews,
[in, optional] ID3D11RenderTargetView * const *ppRenderTargetViews,
[in, optional] ID3D11DepthStencilView *pDepthStencilView
);
OMSetRenderTargets
가 렌더링 파이프라인의 최종 결과물 ( Output-Merge 스테이지 )을 받을 렌더 타겟을 지정하는 함수이다. 렌더 타겟은 리소스이다. 이 리소스를 파이프라인에 연결하기 위해서는 Resource View
를 사용해야 한다. 렌더 타겟의 용도로서 렌더링 파이프라인에 연결하기 때문에 RenderTargetView
로 파라메터를 넘겨주게 된다. 이 함수에 대한 레퍼런스는 OMSetRenderTargets에 있다.
리소스 뷰
DirectX11에서 리소스는 형태로는 Buffer와 Texture( 1D, 2D, 3D )로 나눌 수 있으나 용도에 따라 더 세분화할 수 있다. 이 세분화된 개념의 리소스중에서는 렌더링 파이프라인에 직접 연결할 수 없는 타입이 있다. SHADER_RESOURCE, RENDER_TARGET, DEPTH_STENCIL, UNORERED_ACCESS이들이다. 이들은 직접 렌더링 파이프라인에 연결할 수 없기 때문에 리소스 뷰라는 객체를 통해서 간접 연결한다.
RenderTargetView
자체가 리소스는 아니기 때문에 이 렌더 타겟 뷰 객체를 생성하고 이 뷰가 가리키는 리소스를 연결해주어야 한다. 리소스도 생성이 먼저 되어 있어야 한다. 이전글에서 스왑 체인을 생성할 때 DXGI_SWAP_CHAIN_DESC
를 통해서 화면에 표시할 내용을 담는 버퍼를 생성했다. 이 버퍼를 렌더링 파이프라인에 연결해서 렌더링된 최종 결과물을 화면에 렌더링할 것이다.

우선 스왑체인으로부터 백 버퍼를 가리키는 객체를 가져와야 한다.
ID3D11Texture2D* Texture;
SwapChain->GetBuffer( 0, __uuidof( ID3D11Texture2D ), ( LPVOID* )&Texture );
이제 Texture
는 스왑 체인에 있는 0번 버퍼를 가리키게 된다. 이 Texture
를 수정하게 되면 곧 백 버퍼의 내용이 수정되는 것이다. 이 버퍼를 렌더링 파이프라인에 연결하여 결과물이 반영되도록 하면 된다. 렌더링 파이프라인에 연결하기 위해서는 리소스 뷰를 사용해야 한다.
ID3D11RenderTargetView* RenderTargetView;
Device->CreateRenderTargetView( Texture, nullptr, &RenderTargetView );
이제 RenderTargetView
는 Texture
를 가리키게 된다. 이 RenderTargetView
를 렌더링 파이프라인에 연결하면 렌더 타겟 관련 준비는 모두 끝이 난 것이다.
DeviceContext->OMSetRenderTargets( 1, &RenderTargetView, nullptr );
Texture
는 백 버퍼를 가리키는 객체로 SwapChain->GetBuffer()
에서 새롭게 생성된 COM객체이므로 사용을 모두 다 했기 때문에 Release
가 필요하다. 이 객체를 Release
한다고 해서 백 버퍼 자체가 사라지는건 아니다. 어디까지나 백 버퍼를 가리키고 수정할 수 있는 객체를 해제시키는 것이다.
Texture->Release();
뷰 포트의 설정
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
을 통해서 하게 된다.
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 크기로 지정되어 생성되는 것이다.
화면에 표시하기
이제 렌더링 파이프라인의 결과물을 화면에 표시할 모든 준비는 끝났다. 일단은 따로 렌더링할 내용이 없기 때문에 빈 화면만 표시하도록 한다. 백 버퍼에 연결된 렌더 타겟 뷰를 특정 색상으로 초기화하고 화면에 표시 명령을 하면 된다.
float color[ 4 ] = { 1.0f, 1.0f, 1.0f, 1.0f };
DeviceContext->ClearRenderTargetView( RenderTargetView, color );
SwapChain->Present( 0, 0 );
CRD11Renderer
위 내용들을 포함하고 있는 클래스를 작성하였다.
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;
};
//-----------------------------------------------------------------------------------------------------------
/// 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 );
}
디바이스들을 초기화한 후 렌더 타겟과 뷰포트 관련 설정도 초기화해주어야 한다.
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
은 다음과 같이 매 프레임 렌더 타겟을 초기화 하고 화면에 표시하게 된다.
void RenderFrame()
{
// GD11Renderer은 CRD11Renderer의 전역 인스턴스이다.
GD11Renderer.ClearRenderTarget();
GD11Renderer.Present();
}
이렇게 DirectX11 API를 이용해서 빈 화면을 렌더링해 보았다. 다음은 간단한 삼각형 하나를 렌더링 해보도록 하자.
여기까지의 코드는 yunei0313/CRY at Empty-screen-rendering에서 확인할 수 있다.