오늘은 이제 DirectX 11을 초기화 해보고자 한다.
ID3D11Device와
ID3D11DeviceContext를 초기화해주어야 화면에 그리라는 명령을 할 수 있기 때문이 먼저 이 2개를 초기화해보도록 한다.
https://docs.microsoft.com/en-us/windows/win32/api/d3d11/nn-d3d11-id3d11device
ID3D11Device (d3d11.h) - Win32 apps
The device interface represents a virtual adapter; it is used to create resources.
docs.microsoft.com
D3D11.h 에 정의된 인터페이스로 가상 어댑터(virtual adapter)를 대표하는 장치 인터페이스라고 한다. 즉 해석하자면 우리가 흔히 쓰는 드라이버 같은 것이라고 보면 된다. 그래픽 카드는 컴퓨터마다 다르지만 이 인터페이스를 통해 마치 한 개의 그래픽 장치를 사용하듯 사용하기 편하게 해주는 거라고 보면된다.
위 내용은 예전에 대충 이해한 것이고 더 깊게 들어가보자. ID3D11Device는 일종의 COM객체이다. COM객체는 COM규약을 준수하는 컴포넌트라고 하는데, 내가 모르는 단어가 많다. 먼저 컴포넌트는 유니티를 쓰면서 대략 짐작은 하지만 정확한 정의는 모르고 있었다. 컴포넌트란 언제든지 교환가능하고 하드웨어 부품같이 독립적인 기능을 수행하도록 하는 독립적인 단위모듈이다. 그러니까 유니티에서 Rigidbody를 적용하면 물리법칙이 적용되지만 없다고 해서 실행이 불가능한 것은 아닌 것 처럼 컴포넌트란 다른 애들이랑 같이 연계를 꼭 하지 않고 그 자체로 다해처먹을 수 있는 놈이라고 볼 수 있다.
COM(Component Object Model)은 컴포넌트끼리 상호작용할 수 있도록 하는 메커니즘이다.
COM 객체는 다음 조건을 만족해야 한다.
언어독립성
-> 무슨 언어로 만들던 간에 다른 언어로 만든 프로그램의 모듈처럼 동작할 수 있도록 한다.
Binaray Standard
-> 바이너리로 되어있어 이걸 쓰기 위해 특정 언어의 소스가 필요없다. 어찌보면 위 내용이랑 똑같은 말이다.
Version control
->예전 버전 쓰는 애들도 계속 쓸 수 있고, 새로운 기능이 업데이트 되면 그 새로운 기능도 동시에 제공할 수 있도록 버전의 높낮음에 관계없이 호환가능하도록 한다.
Location Transparency
-> 해당 컴포넌트는 위치에 관계없이 다른 컴포넌트나 프로그램에 의해 사용될 수 있다.
이외에 재사용성, 객체 지향 등의 특징과 기능을 가지고 있어야 한다.
그러니까 컴포넌트 스타일의 객체를 만들기 위해선 위 조건을 만족해야 한다~ 이런 걸 알려주는 일종의 사양서 같은 것이다. 구체적으로 어떤 걸 반드시 추가해야한다는 규칙이 있을 것 같지만 여기까지 찾아보지는 않았다.
그리고 이 컴포넌트가 제공해야 하는 것이 바로 인터페이스(Interface)이다. 그렇다고 C++의 순수가상함수를 생각하지말고 인터페이스라는 본래 뜻만 생각하자.
COM객체가 가지고 있는 기능을 클라이언트에게 알려주는 방법이 있어야 함을 의미한다. 그러니까 D3D11Device라는 COM객체가 제공하는 기능(함수)를 가지고 있는 것이 바로 I(Interface)D3D11Device라는 뜻이다.
하지만 Interface가 어떻게 그런 기능을 제공하는지 원리와 개념같은 건 알려주지 않는다. 그러니까 회사에서 dll파일 주고 여깄는 함수가지고 만들어랑. 니 맘대로 하지말고. 이게 어떻게 만들어졌는지 모르겠다고? 공부하러 왔냐? 어쩌라고. 그냥 써~ 이런 느낌인 듯 하다..
위 설명은 다음 블로그에서 보고 작성했다.
https://abipictures.tistory.com/115
COM(Component Object Model) 개념잡기
1. COM 1.1 COM의 개념 COM(Component Object Model)이란 한마디로 어떤 프로그램이나 시스템을 이루는 컴포넌트들이 상호 통신할 수 있도록 하는 메커니즘이라고 할 수 있다. 여기서 컴포넌트란 .ocx, .dll, .exe..
abipictures.tistory.com
어쨌든 정리하자면 ID3D11Device는 그래픽 카드에 접근해서 여러가지 명령을 내릴 수 있도록 만들어주는 컴포넌트의 인터페이스 객체라고 볼 수 있다.
그리고 또 중요한 인터페이스인 ID3D11DeviceContext에 대해 알아보자.
Context는 번역기를 돌리면 문맥이라고 나온다. 장치의 문맥? 장난하나 아오..
https://stackoverflow.com/questions/6145091/the-term-context-in-programming
The term "Context" in programming?
I have been programming for some months now and a frequently used word is "context" in classes. Like ServletContext (Java), Activity (Android), Service (Java, Android), NSManagedContext (Objective-...
stackoverflow.com
https://diehard98.tistory.com/entry/Device-Context%EC%9D%98-%EA%B0%9C%EB%85%90-GetDC-ReleaseDC
Device Context의 개념 + GetDC / ReleaseDC
OpenGL 프로그램을 주로 작성하면 사실 Windows API에 대해서 많은 지식이 필요하지 않지만 늘 듣는 말에 대해서는 알아둘 필요가 있는데 아무래도 넘버 1이 Device Context가 아닌가 싶다. 알고 있으면 쉽지만 모..
diehard98.tistory.com
여러 군데의 블로그 와 스택오버플로우의 스레드를 읽어본 결과,
장치라 함은 우리가 흔히 생각하는 그 장치가 아니라는 것. device라고 그냥 원문으로 이해하자. device의 예시로 동영상 파일, 윈도우 창과 같은 독립적인 오브젝트를 device라고 하고 그 device에 대한 내용이 deviceContext이다. 그러니까 어떠한 목적을 이루기 위해서 우리가 식별 정보 등등을 제공한다면 그러한 정보들이 context가 된다. 어떠한 Event에 대하여 상태 정보를 제공하는 오브젝트가 있다면 그 오브젝트가 바로 Context Object 이다. 그러므로 ID3D11DeviceContext란 DirectX11 Device내부 정보에 대한 인터페이스라고 이해하면 될 듯 하다.
ID3D11Device를 보니 Windows10 Creator Update? 를 타게팅하는 사용자는 ID3D11Device5(d3d11__4.h)를 사용하라는데 나는 저 업데이트가 뭔지 모르고 왜 써야하는지 필요성을 모르겠으므로 그냥 ID3D11Device를 사용하고자 한다.
Directx 11 초기화는 현재 rastertek.com을 보고 해보고 있다.
그리고 Directx11에서 제공하는 COM객체는 객체의 주소의 주소...를 받아야 하는 함수들이 있어서 그냥 shared_ptr같은걸 쓰기가 힘들다. 그래서 Microsoft::wrl::Comptr를 사용해보고자 한다.
D3D11CreateDeviceAndSwapChain함수를 이용해서 인터페이스들을 모두 초기화할 수 있는데, 이 때 들어가는 파라미터에 대해 분석해보자.
https://docs.microsoft.com/en-us/windows/win32/api/d3d11/nf-d3d11-d3d11createdeviceandswapchain
D3D11CreateDeviceAndSwapChain function (d3d11.h) - Win32 apps
Creates a device that represents the display adapter and a swap chain used for rendering.
docs.microsoft.com
HRESULT D3D11CreateDeviceAndSwapChain(
IDXGIAdapter *pAdapter,
D3D_DRIVER_TYPE DriverType,
HMODULE Software,
UINT Flags,
const D3D_FEATURE_LEVEL *pFeatureLevels,
UINT FeatureLevels,
UINT SDKVersion,
const DXGI_SWAP_CHAIN_DESC *pSwapChainDesc,
IDXGISwapChain **ppSwapChain,
ID3D11Device **ppDevice,
D3D_FEATURE_LEVEL *pFeatureLevel,
ID3D11DeviceContext **ppImmediateContext
);
첫번째 파라미터는 그래픽 카드를 대표하는 COM객체의 인터페이스인 IDXGIAdapter의 포인터를 받고 있다. 컴퓨터에 그래픽 카드가 여러 개 있을 경우 어떤 그래픽 카드에게 명령을 내릴 것인지 이 파라미터를 이용해 결정할 수 있다. 물론 난 그래픽 카드가 1개 밖에 없다. nullptr을 전달하면 default로 되어있는 첫번째 그래픽 카드를 사용하기 때문에 굳이 Adapter를 생성해 줄 필요는 없을 것 같다. nullptr을 전달하자.
두 번째는 드라이버 타입인데, 잘 모르겠고 하드웨어 가속, 짱짱 빠름 이 글귀만 보고 HardWare로 했다.
세 번째도 뭐.... dll파일같은건 안 갖고 있고 소프트웨어 드라이버도 아니니까 nullptr.
네 번째는 UINT로 비트 연산을 통해 생성시 여러가지 옵션을 집어넣을 수 있는데, 음... 잘모르겠다. 이 옵션으로 디버깅을 수월하게 하기 위해 멀티 쓰레딩을 끌 수도 있다고 설명에 적혀있다. 일단 지역변수로 UINT 선언후 0으로 초기화후 집어넣자.
다섯 번째, D3D 피쳐 레벨이다. 그냥 어떤 버전의 DirectX의 기능을 대상으로 하는 지 넣는 건데... nullptr로 둬도 될려나?
여섯 번째, 피쳐 레벨에 몇 개를 넣었냐는 건데, 일단 nullptr로 뒀으니 이것도 0이라 하자.
일곱 번째, SDK 버전인데, D3D11_SDK_VERSION 쓰라고 문서에 아예 표시가 되어있다 ㅋㅋ
여덟 번째, 스왑체인에 대한 설명(description)을 넣으라고 한다. 그러면 구조체를 만들어서 초기화를 해주어야 겠다.
다음과 같이 초기화를 해주었다.
DXGI_SWAP_CHAIN_DESC sd = {};
sd.BufferCount = 1;
sd.BufferDesc.Width = window.width;
sd.BufferDesc.Height = window.height;
sd.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
sd.BufferDesc.RefreshRate.Numerator = 1;
sd.BufferDesc.RefreshRate.Denominator = 60;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.OutputWindow = window.handle;
sd.SampleDesc.Count = 1;
sd.SampleDesc.Quality = 0;
sd.Windowed = TRUE;
여기서 버퍼 카운트가 1인 이유는 백버퍼를 뜻하기 때문이다. 이미 프론트 버퍼는 디폴트로 제공되고, 추가로 생성할 버퍼가 몇 개인지 묻는거라 1로 두었다. 그리고 멀티 샘플링에 대한 옵션이 있는데, 멀티 샘플링은 별건 아니고 화면을 한번만 그리지 않고 몇 차례 그려서 보정하는.. 그런 기술이다. 그게 뭐야... 돌려줘요. 안쓸꺼니까 SampleDesc에서 카운트를 1, 퀄리티를 0으로 뒀다. 이러면 한번만 화면에 그린다.
이렇게 해서 함수 한개로 3개의 인터페이스를 모두 초기화 했다. 디바이스, 디바이스 콘텍스트, 스왑체인.
이제 추가로스왑체인에 생성된 백버퍼를 화면에 그려내라~ 하고 알려줘야 한다. 안알려주면 어떤 버퍼에 있는 걸 그려야 하는지 모르니까 어찌보면 필수적인 코스라 할 수 있겠다.
CreateRenderTargetView 함수를 사용한다.
먼저 스왑체인에서 BackBuffer의 포인터를 받아와야 하는데, 이것도 Comptr로 받아왔다.
그리고 이 백버퍼를 위 함수에 집어넣어서 렌더타겟뷰라는 구조체를 생성한다. 아직 화면에 그리라는 명령은 나오지 않았다.
마지막으로 해주어야 하는 게 바로 OMSetRenderTargets(Context에 있는 멤버함수임) 함수를 호출해서 이 렌더타겟뷰가 바로 그래픽카드 니가 그려야 하는 화면이다! 라고 전달해주는 함수인 것이다.
지금까지 초기화 상황은 다음과 같다.
class Graphics
{
MEMBERVARIABLES:
Microsoft::WRL::ComPtr<ID3D11RenderTargetView> pRenderTargetView;
Microsoft::WRL::ComPtr<IDXGISwapChain> pSwapChain;
Microsoft::WRL::ComPtr<ID3D11Device> pDevice;
Microsoft::WRL::ComPtr<ID3D11DeviceContext> pContext;
public:
public:
/*DX 초기화*/
Graphics(Window& window);
virtual ~Graphics();
};
DXGI_SWAP_CHAIN_DESC sd = {};
sd.BufferCount = 2;
sd.BufferDesc.Width = window.width;
sd.BufferDesc.Height = window.height;
sd.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
sd.BufferDesc.RefreshRate.Numerator = 1;
sd.BufferDesc.RefreshRate.Denominator = 60;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.OutputWindow = window.handle;
sd.SampleDesc.Count = 1;
sd.SampleDesc.Quality = 0;
sd.Windowed = TRUE;
HRESULT hr;
UINT createFlag = 0;
if (FAILED(hr = D3D11CreateDeviceAndSwapChain(
nullptr,
D3D_DRIVER_TYPE_HARDWARE,
nullptr,
createFlag,
nullptr,
0,
D3D11_SDK_VERSION,
&sd,
&pSwapChain,
&pDevice,
nullptr,
&pContext
)))
{
Gohen::Console::Print("Graphic 디바이스 생성 실패!");
}
Microsoft::WRL::ComPtr<ID3D11Resource> pBackBuffer;
if (FAILED(hr = pSwapChain->GetBuffer(
0,
__uuidof(ID3D11Texture2D),
(LPVOID*)&pBackBuffer
)))
{
Gohen::Console::Print("스왑 체인으로부터 백버퍼 받아오기 실패!");
}
if (FAILED(hr = pDevice->CreateRenderTargetView(
pBackBuffer.Get(),
nullptr,
&pRenderTargetView
)))
{
Gohen::Console::Print("백버퍼로부터 렌더타겟뷰 생성후 구조체 초기화 실패!");
}
pContext->OMSetRenderTargets(1, pRenderTargetView.GetAddressOf(), nullptr);
다음으로 할 일은 depth - stencil 버퍼를 만드는 일이다. depth 버퍼는 화면 상에 있는 물체가 얼마나 멀리 있는지를 0~1 사이 숫자로 노멀라이즈해서 저장하는 버퍼이다. 그러니까 흑~백 으로 하얀색일 수록 가까운 물체다. 한마디로 x,y 로만 이루어진 화면(2D)에는 Z축(깊이)를 저장할 수 없기 때문에 Color를 가지고 Z값을 따로 버퍼를 만들어 저장하는 것이다.
그러면 스텐실 버퍼는 무엇일까?
백버퍼에 대한 마스킹을 하기 위한 버퍼라고 생각할 수 있다. 거울 반사나 평면 그림자 등에 사용할 수 있다는데 일단 아직 이정도로만 파악하고 넘어가자.