1. 설치
윈도우 VS2015부터(윈도우 10 SDK가 설치되어 있다면) DirectX SDK가 포함되어 있기에 별도의 설치가 필요하지는 않다. 따라서 윈도우 데스크톱 애플리케이션 템플릿을 사용해서 프로젝트를 생성하면 DirectX 관련 헤더들을 바로 사용할 수 있다.
2. 헤더 추가
DirectX 11을 사용하기 위해 필요한 기본적인 헤더들은 다음과 같다. 각각의 헤더에 대한 마이크로소프트 문서 설명을 간단하게 주석으로 추가하였다. 헤더들을 추가하고 간단하게 사용하고 싶다면 아래의 using
구문을 사용해주면 된다.
#include <d3d11.h> // DX11 전반적으로 사용된다.
#include <d3dcompiler.h> // HLSL에서 사용된다.
#include <DirectXMath.h> // DX에서 사용할 수 있는 일반적인 수학 관련 클래스, 함수들을 제공한다.
#include <wrl.h> // 윈도우 런타임 C++ 템플릿 라이브러리
using namespace DirectX;
using namespace Windows::WRL;
텍스처링 작업이 필요하면
DirectXTex
라이브러리를 추가해서 사용할 것이다.
3. 라이브러리 추가
여기에서는 #pragma comment
전처리 지시자를 사용해서 라이브러리를 추가한다. 해당 전처리기를 사용하면 소스코드 내에서 명시적으로 정의를 하여 라이브러리 링크를 시켜줄 수 있다. 기본적으로 필요한 라이브러리는 다음과 같다.
#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "d3dcompiler.lib")
4. 초기화 과정
DirectX 프로그래밍은 윈도우 데스크톱 애플리케이션 개발을 위한 템플릿을 기반으로 진행된다. 프로젝트 빌드 후 실행이 되면 나타나는 윈도우에 대한 참조는 HWND
핸들러를 사용하면 된다.
DirectX 11 프로그래밍을 할 때 필요한 객체들을 COM 인터페이스 기반으로 사용한다. C++의 일반적인 객체 사용법과 비슷하지만, COM 객체는 new
나 delete
와 같은 키워드를 이용해 객체를 생성하지 않고 COM 객체 생성을 위한 별도의 API를 호출해주어야 한다. DX11에서 사용하는 다양한 클래스들 역시 COM 객체인데, 별도의 해제 과정을 추가적으로 작성하는 번거로움을 피하기 위해 ComPtr
을 사용해줄 것이다.
4.1 Device/DeviceContext, IDXGISwapChain
가장 먼저 초기화를 해주어야 할 대상은 Device와 DeviceContext이다. DX11에서는 다음과 같이 사용하게 된다.
ComPtr<ID3D11Device> _device = nullptr;
ComPtr<ID3D11DeviceContext> _deviceContext = nullptr;
ID3D11Device
인터페이스는 주로 기능 지원 및 점검, 자원 할당을 위해 사용한다. ID3D11DeviceContext
인터페이스는 렌더링 대상을 설정하고, 필요한 자원을 렌더링 파이프 라인에 추가하며, GPU가 수행할 렌더링 명령들을 제어할 때 사용한다.
디바이스에 대한 기본적인 설정과 함께 진행하면 좋은 것이 스왑 체인에 관한 내용들이다. 더블 버퍼링을 DX에서 사용하기 위해 필요한 인터페이스로 다음과 같이 ComPtr을 함께 사용해주면 된다.
ComPtr<IDXGISwapChain> _swapChain = nullptr;
스왑 체인의 경우 IDXGISwapChain
이라는 D3D11 접두사가 붙지 않은 별도의 인터페이스를 사용하는데, DXGI
는 DX11 등의 그래픽스 라이브러리의 기능들이나 다른 애플리케이션으로부터 오는 기능들을 받아 커널 모드 드라이버나 하드웨어와 통신을 하는 역할을 한다. 즉, 애플리케이션과 하드웨어 사이에서 다양한 역할을 수행할 수 있는 인터페이스를 제공해주는 것이다. IDXGISwapChain
은 그 중에서 더블 버퍼링을 하기 위해 필요한 인터페이스라고 할 수 있다.
디바이스와 스왑 체인을 함께 초기화하기 위해 다음과 같은 COM 객체 생성 함수를 사용해주면 된다.
// https://learn.microsoft.com/ko-kr/windows/win32/api/d3d11/nf-d3d11-d3d11createdeviceandswapchain
HRESULT D3D11CreateDeviceAndSwapChain(
[in, optional] IDXGIAdapter *pAdapter,
D3D_DRIVER_TYPE DriverType,
HMODULE Software,
UINT Flags,
[in, optional] const D3D_FEATURE_LEVEL *pFeatureLevels,
UINT FeatureLevels,
UINT SDKVersion,
[in, optional] const DXGI_SWAP_CHAIN_DESC *pSwapChainDesc,
[out, optional] IDXGISwapChain **ppSwapChain,
[out, optional] ID3D11Device **ppDevice,
[out, optional] D3D_FEATURE_LEVEL *pFeatureLevel,
[out, optional] ID3D11DeviceContext **ppImmediateContext
);
pAdaper
: 디바이스를 만들 때 사용할 비디어 어댑터에 대한 포인터다. NULL을 사용하면IDXGIFactory1::EnumAdapters
로 열거된 첫 번째 어댑터인 기본 어탭터를 사용하게 된다.DriverType
: 생성하려고 하는 드라이버 유형을 결정한다. 그래픽 카드를 사용하면D3D_DRIVER_TYPE_HARDWARE
를 입력해주면 된다.Software
: 소프트웨어 래스터라이저를 구현하는 DLL에 대한 핸들이다. 그래픽 카드를 사용하고 있기 때문에 NULL을 사용해주면 된다.Flags
: 사용할 DX11의 API 레이어를 설정한다.pFeatureLevels
: 지원하는 DX11 피처 레벨의 배열을 설정한다. NULL이면 DX11 -> DX10 -> DX9 순으로 설정된다.FeatureLevels
: 위에서 설정한 배열의 개수이다.SDKVersion
: 사용하는 DX SDK 버전을 입력해주면 된다.(D3D11_SDK_VERSION
)pSwapChainDesc
: 스왑체인의 설정값들을 저장한 구조체의 주소를 넘겨준다.ppSwapChain
: 생성된 스왑체인 인터페이스를 담을 변수를 입력한다.ppDevice
: 생성된 디바이스 인터페이스를 담을 변수를 입력한다.pFeatureLevel
: 생성에 성공했다면, 피처 레벨에서 지정한 배열의 첫 번째 값을 리턴하는 포인터를 입력한다. 실패하면 0이 반환된다.ppImmediateContext
: 생성된 디바이스 컨텍스트의 인터페이스를 담을 변수를 입력한다.
스왑체인의 설정값들은 다음과 같이 만들어주면 된다.
// https://learn.microsoft.com/en-us/windows/win32/api/dxgi/ns-dxgi-dxgi_swap_chain_desc
typedef struct DXGI_SWAP_CHAIN_DESC {
DXGI_MODE_DESC BufferDesc;
DXGI_SAMPLE_DESC SampleDesc;
DXGI_USAGE BufferUsage;
UINT BufferCount;
HWND OutputWindow;
BOOL Windowed;
DXGI_SWAP_EFFECT SwapEffect;
UINT Flags;
} DXGI_SWAP_CHAIN_DESC;
BufferDesc
: 백 버퍼의 속성들을 지정하는 구조체이다.SampleDesc
: 멀티 샘플링 관련 속성들을 지정하는 구조체이다.BufferUsage
: 버퍼의 사용 용도를 지정한다. 화면에 출력하기 위한 백버퍼로 사용하려면DXGI_USAGE_RENDER_TARGET_OUTPUT
를 사용하면 된다.BufferCount
: 스왑체인이 사용할 버퍼 개수를 지정한다. 더블 버퍼링에서는 2를 지정해주면 된다.OutputWindow
: 렌더링 결과를 표시할 윈도우 핸들러를 입력한다.Windowed
: 창모드 여부를 설정한다.SwapEffect
: 백버퍼 스왑 처리 효과를 지정한다.DXGI_SWAP_EFFECT_DISCARD
를 사용하면 스왑 후에 백버퍼의 콘텐츠를 삭제하게 된다.Flags
: 추가 플래그들을 지정할 수 있다.
스왑체인 디스크립션을 만드는 방법은 다음과 같다.
DXGI_SWAP_CHAIN_DESC desc;
ZeroMemory(&desc, sizeof(desc));
{
// 버퍼 사이즈 지정
desc.BufferDesc.Width = _width;
desc.BufferDesc.Height = _height;
// 주사율 설정
desc.BufferDesc.RefreshRate.Numerator = 60;
desc.BufferDesc.RefreshRate.Denominator = 1;
// 포맷
desc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
// 화면 출력 방식 지정 - 스캔라인의 방향 정하기
// DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED이면 위에서부터 한줄씩 차례로 출력함
desc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
// 디스플레이 비례모드
// DXGI_MODE_SCALING_UNSPECIFIED이면 스케일링 지정하지 않음
// DXGI_MODE_SCALING_CENTERED이면 디스플레이 중앙에 위치
// DXGI_MODE_SCALING_STRETCHED이면 스트레치 시킴
desc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
// 멀티 샘플링 관련 설정
desc.SampleDesc.Count = 1;
desc.SampleDesc.Quality = 0;
// 버퍼 사용 용도 설정: 최종 화면 그리기
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
desc.BufferCount = 1;
desc.OutputWindow = _hwnd;
desc.Windowed = true;
desc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
}
매개변수가 많이 있지만, 디폴트 값들을 지정해서 함수를 사용하면 다음과 같이 사용할 수 있다.
HRESULT hr = ::D3D11CreateDeviceAndSwapChain(
nullptr,
D3D_DRIVER_TYPE_HARDWARE, // 드라이버: 그래픽 카드 사용 옵션
nullptr,
0,
nullptr,
0,
D3D11_SDK_VERSION,
&desc,
_swapChain.GetAddressOf(),
_device.GetAddressOf(),
nullptr,
_deviceContext.GetAddressOf()
);
assert(SUCCEEDED(hr));
4.2 렌터 타겟 뷰(RenderTargetView)
위에서 만든 디바이스와 디바이스 컨텍스트는 그래픽 카드(하드웨어)와 관련된 인터페이스라고 보면 된다. 우리가 모니터를 통해 볼 수 있는 화면은 디바이스에 설정된 렌더 타겟에 렌더링된다.
디바이스와 디바이스 컨텍스트, 스왑체인을 만드는 과정에서 디바이스 관련 인터페이스와 백버퍼 관련 인터페이스들이 생성은 되어있지만, 렌더 타겟으로 설정되지 않은 상태이다. 그래서 생성한 스왑체인으로부터 백버퍼를 얻어와서 이 백버퍼를 보는 렌더 타겟 뷰를 만들어 사용해야 한다.
렌더 타겟 뷰와 관련된 인터페이스는 다음과 같이 사용하면 된다.
ComPtr<ID3D11RenderTargetView> _renderTargetView;
먼저, 스왑체인으로부터 백버퍼의 포인터를 받아오기 위해 GetBuffer
메서드를 사용해주면 된다. 백버퍼는 2차원의 모니터이기 때문에 기본적으로 ID3DTexture2D
인터페이스로 되어 있다. 스왑체인에서 다음 함수를 호출해주자.
HRESULT GetBuffer(
UINT Buffer, // 접근할 버퍼 인덱스
REFIID riid, // 백버퍼를 받을 인터페이스 타입
void **ppSurface // 반환된 백버퍼 인터페이스를 받을 포인터
);
그 다음으로 백버퍼를 통해 렌더 타겟 뷰를 생성해야 한다. 위에서 텍스쳐 인터페이스를 받았는데, 텍스쳐는 파이프라인으로부터 뷰를 통해 접근이 가능하다. 그래서 하드웨어에 존재하는 백버퍼를 렌더 타겟으로 삼았을 때, 여기에 접근하기 위해 뷰를 이용하는 것이다. 디바이스에서 다음 함수를 호출하면 렌더 타겟 뷰를 만들 수 있다.
HRESULT CreateRenderTargetView(
ID3D11Resource* pResource, // 렌더 타겟 뷰에서 액세스할 리소스(백버퍼)
const D3D11_RENDER_TARGET_VIEW_DESC* pDesc, // 렌더 타겟 뷰를 정의하는 디스크립션. NULL을 사용하면 리소스가 만들어졌을 때의 포맷을 그대로 사용함
ID3D11RenderTargetView** ppRTView // 렌더 타겟 뷰 인터페이스를 받을 포인터 변수.
);
실제로 스왑체인으로부터 백버퍼를 받아와 렌더 타겟 뷰를 만드는 과정은 다음과 같다.
HRESULT hr;
// backBuffer를 ID3D11Texture2D로 받아오기
ComPtr<ID3D11Texture2D> backBuffer = nullptr;
hr = _swapChain->GetBuffer(
0, // desc.BufferCount = 1이라 0번째 버퍼 지정
__uuidof(ID3D11Texture2D), // 텍스처 타입 지정
(void**)backBuffer.GetAddressOf() // 저장할 백버퍼 주소
);
CHECK(hr);
_device->CreateRenderTargetView(
backBuffer.Get(), // 렌더 타겟 뷰에서 접근할 리소스 지정
nullptr, // NULL을 사용하여 리소스 생성 시점의 포맷을 그대로 사용하도록 지정
_renderTargetView.GetAddressOf() // 렌더 타겟 뷰 인터페이스 저장할 변수
);
5. 기본 렌더링 순서
디바이스, 디바이스 컨텍스트, 스왑체인, 렌더 타겟 뷰에 대한 설정이 끝났다면 기본적으로 어떤 방식으로 렌더링을 할지 설정해볼 수 있다. 더블 버퍼링을 사용하기 때문에 백버퍼에서 그려진 내용을 프론트버퍼로 옮긴다는 것이 기본 개념이며, 매 프레임마다 이 작업이 반복된다.
이를 순서대로 코드로 표현하면 다음과 같다.
// 1. 디바이스 컨텍스트에서 OM 단계에서 사용할 렌더 타겟을 지정한다.
_deviceContext->OMSetRenderTargets(
1, // 렌더 타겟의 수(렌더 타겟 뷰 배열의 수와 같음)
_renderTargetView.GetAddressOf(), // 렌더 타겟 뷰 배열
nullptr // 깊이, 스텐실 버퍼 설정(일단 지정 안함)
);
// 2. 렌더 타겟 뷰 초기화: 이전 프레임에 그려진 내용들을 초기화하는 작업이다.
_deviceContext->ClearRenderTargetView(
_renderTargetView.Get(),
_clearColor
);
// 3. 뷰포트 설정: 뷰포트는 렌더 타겟의 렌더링 영역에 대한 설정이다. 렌더타겟의 넓이, 높이, 깊이 등이다.
// 뷰포트는 각 렌더 타겟마다 별도로 존재해야 한다.
// 일단 렌더 타겟을 HWND 핸들러 초기화 과정에서 설정한 값과 동일하게 설정했다고 가정한다.
_deviceContext->RSSetViewports(1, &_viewport); // 뷰포트 개수 및 뷰포트 구조체 배열
// do something - 무언가 렌더링 작업 진행
// 4. 백버퍼에 있는 데이터를 프론트버퍼로 이동
HRESULT hr = _swapChain->Present(1, 0);
'그래픽스 > DirectX11' 카테고리의 다른 글
RenderTargetView의 의미 (0) | 2024.04.19 |
---|---|
DXGI_SWAP_CHAIN_DESC에 관하여 (1) | 2024.04.19 |
ID3D11Device 초기화, 생성 팁 (0) | 2024.04.16 |
DirectX에서의 COM(Component Object Model) (0) | 2024.04.15 |
DirectX 11 도형 출력(버텍스 버퍼, 버텍스 쉐이더, 픽셀 쉐이더 설정) (0) | 2024.04.12 |