4.5 예제 프로그램 프레임 워크
클래스 소개
d3dutil.h, cpp : 유용한 편의용 코드
d3dapp.h, cpp : Directx 응용 프로그램을 캡슐화 하는데 쓰이는 핵심 응용 코드
프레임 워크 목적 : 창 생성 코드 와 Directx 초기화 소스를 숨기는 것
D3Dapp 클래스 : 응용 클래스 들의 기반 클래스(부모 클래스) 로 사용
기능 목록 : 주창 생성, 메시지 루프 실행, window 메시지 처리, directx 초기화
여섯 가상 함수를 제공
이 구조의 장점 : 초기화 및 메시지 처리는 D3Dapp 클래스에 구현,
파생 클래스는 필요한 구체적인 코드만 작성 할 수 있다.
class D3DApp
{
protected:
D3DApp(HINSTANCE hInstance);
D3DApp(const D3DApp& rhs) = delete;
D3DApp& operator=(const D3DApp& rhs) = delete;
//com 인터페이스 해제
//GPU 참조 중 메모리 해제 해버리면 충돌 발생, 명령 대기열 비우기
virtual D3DApp::~D3DApp()
{
if(md3dDevice != nullptr)
FlushCommandQueue();
}
public:
static D3DApp* GetApp();
HINSTANCE AppInst()const; //응용프로그램 인스턴스 핸들 리턴
HWND MainWnd()const; //주 창 핸들 리턴
float D3DApp::AspectRatio()const //종횡비 리턴
{
return static_cast<float>(mClientWidth) / mClientHeight;
}
//4X msaa 다중 표본화 활성화 및 비활성화 리턴
bool Get4xMsaaState()const;
// 활성화 및 비활성화 지정
void Set4xMsaaState(bool value);
//window 메시지가 없을 경우에도 게임 로직 동작 하는 함수
//내부에서 PeekMessage 처리
int Run();
//기반 클래스의 InitMainWindow(), InitDirect3d 호출
//파생 클래스의 자원 할당, 장면 물체 초기화 역할
virtual bool Initialize()
{
if(!D3DApp::Initialize())
return false;
//고유 초기화 코드
}
//윈도우 메시지 처리
virtual LRESULT MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
protected:
//RTV, DSV 각각의 힙 생성
virtual void CreateRtvAndDsvDescriptorHeaps();
//클라이언트 창 크기 변경 시 호출
//창크기에 따라 후면 버퍼, 깊이 스텐실 버퍼 수정
//후면 버퍼는 함수로 수정, 깊이 스텐실 버퍼는 파괴 후 재생성
//뷰 또한 재생성, 버퍼 외에도 화면 크기에 의존 하는 부분을 모두 수정
virtual void OnResize();
//매 프레임 호출 애니메이션 수행, 카메라 이동, 충돌 검사, 사용자 입력
virtual void Update(const GameTimer& gt)=0;
//현재 프레임을 후면 버퍼에 렌더링
//IDXGISwapChain::Present 후면 버퍼를 화면에 제시
virtual void Draw(const GameTimer& gt)=0;
// Convenience overrides for handling mouse input.
virtual void OnMouseDown(WPARAM btnState, int x, int y){ }
virtual void OnMouseUp(WPARAM btnState, int x, int y) { }
virtual void OnMouseMove(WPARAM btnState, int x, int y){ }
protected:
//응용 프로그램이 주 창을 초기화
bool InitMainWindow();
//Direct3D 초기화
bool InitDirect3D();
//명령 대기열, 명령 목록, 할당자 생성
void CreateCommandObjects();
//교환 사슬 생성
void CreateSwapChain();
//GPU 가 명령 대기열 처리 중 CPU 대기
void FlushCommandQueue();
//교환 사슬의 현재 후면 버퍼 리턴
ID3D12Resource* CurrentBackBuffer()const;
//현재 후면 버퍼의 RTV 렌더 대상 뷰 리턴
D3D12_CPU_DESCRIPTOR_HANDLE CurrentBackBufferView()const;
//현재 후면 버퍼의 DSV 깊이 스텐실 뷰 리턴
D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView()const;
//평균 프레임수, 평균 프레임당 밀리초 계산
void CalculateFrameStats();
void LogAdapters(); //모든 디스플레이어 어댑터 열거( 그래픽 카드)
void LogAdapterOutputs(IDXGIAdapter* adapter);//모든 출력 열거( 모니터)
//디스플레이 모드 나열
void LogOutputDisplayModes(IDXGIOutput* output, DXGI_FORMAT format);
protected:
static D3DApp* mApp;
HINSTANCE mhAppInst = nullptr; // application instance handle
HWND mhMainWnd = nullptr; // main window handle
bool mAppPaused = false; // 일시 정지 상태
bool mMinimized = false; // 최소화
bool mMaximized = false; // 최대화
bool mResizing = false; // 사용자가 크기 조정용 테두리를 끌고 있는 상태
bool mFullscreenState = false;// 전체 화면
// Set true to use 4X MSAA (?.1.8). The default is false.
bool m4xMsaaState = false; // 4X MSAA enabled
UINT m4xMsaaQuality = 0; // quality level of 4X MSAA
// 경과 시간 및 게임 전체 시간
GameTimer mTimer;
Microsoft::WRL::ComPtr<IDXGIFactory4> mdxgiFactory;
Microsoft::WRL::ComPtr<IDXGISwapChain> mSwapChain;
Microsoft::WRL::ComPtr<ID3D12Device> md3dDevice;
Microsoft::WRL::ComPtr<ID3D12Fence> mFence;
UINT64 mCurrentFence = 0;
Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
Microsoft::WRL::ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList> mCommandList;
static const int SwapChainBufferCount = 2;
int mCurrBackBuffer = 0;
Microsoft::WRL::ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount];
Microsoft::WRL::ComPtr<ID3D12Resource> mDepthStencilBuffer;
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mRtvHeap;
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mDsvHeap;
D3D12_VIEWPORT mScreenViewport;
D3D12_RECT mScissorRect;
UINT mRtvDescriptorSize = 0;
UINT mDsvDescriptorSize = 0;
UINT mCbvSrvUavDescriptorSize = 0;
// 파생클래스는 자신의 생성자에서 변수를 재설정
std::wstring mMainWndCaption = L"d3d App";
D3D_DRIVER_TYPE md3dDriverType = D3D_DRIVER_TYPE_HARDWARE;
DXGI_FORMAT mBackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
DXGI_FORMAT mDepthStencilFormat = DXGI_FORMAT_D24_UNORM_S8_UINT;
int mClientWidth = 800;
int mClientHeight = 600;
};
중요 메서드
FPS : 초당 프레임 수
FPS 평균 프레임 수 = n /t (t = 1 두면 나누기 없이 구할 수 있다.) t : 초 단위 n : t 동안 처리한 프레임 수
하나의 프레임당 걸리는 시간 = (1s, 1000ms) /FPS 평균 시간
프레임 당 걸리는 시간은 FPS보다 직관적으로 파악 하기 쉽다.
void D3DApp::CalculateFrameStats()
{
//평균 프레임 수 계산
//하나의 프레임 당 렌더링 걸리는 평균 시간
//창의 제목에 작성
static int frameCnt = 0; //프레임 개수
static float timeElapsed = 0.0f; // 초 단위
frameCnt++; //프레임 개수 증가
// 전체 시간 - 초 단위( ex : 1,2,3)
if( (mTimer.TotalTime() - timeElapsed) >= 1.0f )
{
float fps = (float)frameCnt; // fps = frameCnt / 1
float mspf = 1000.0f / fps; //1s = 1000ms
wstring fpsStr = to_wstring(fps);
wstring mspfStr = to_wstring(mspf);
wstring windowText = mMainWndCaption +
L" fps: " + fpsStr +
L" mspf: " + mspfStr;
SetWindowText(mhMainWnd, windowText.c_str());
//프레임 개수 초기화 및 초 단위 증가
frameCnt = 0;
timeElapsed += 1.0f;
}
}
메시지 처리부
//응용 프로그램 활성화 비활성화 전달
//mappPaused 활성화 시 Run()에서 Sleep 호출(cpu 주기를 os 돌려준다.)
case WM_ACTIVATE:
if( LOWORD(wParam) == WA_INACTIVE )
{
mAppPaused = true;
mTimer.Stop();//시간 정지
}
else
{
mAppPaused = false;
mTimer.Start();//시간 재개
}
return 0;
//크기 변경 테두리 잡으면 전달
case WM_ENTERSIZEMOVE:
mAppPaused = true;
mResizing = true;
mTimer.Stop();
return 0;
//사용자가 크기 변경 테두리 놓으면 전달
//창 크기에 따라 변화되는 함수 호출
case WM_EXITSIZEMOVE:
mAppPaused = false;
mResizing = false;
mTimer.Start();
OnResize(); // 창크기에 따라 영향 받는 directx 자원 및 뷰 수정
return 0;
// 창 파괴 시 메시지 프로시저 파괴
case WM_DESTROY:
PostQuitMessage(0);
return 0;
//메뉴가 활성화 되어서 사용자 키가 눌렀지만 그키가 그 어떤 이벤트를 해당하지 않을때 발생
case WM_MENUCHAR:
// alt - enter 삐소리가 나지 않게 한다.
return MAKELRESULT(0, MNC_CLOSE);
//창이 너무 작아지지 않게 메시지 처리
case WM_GETMINMAXINFO:
((MINMAXINFO*)lParam)->ptMinTrackSize.x = 200;
((MINMAXINFO*)lParam)->ptMinTrackSize.y = 200;
return 0;
//마우스 다운
case WM_LBUTTONDOWN:
case WM_MBUTTONDOWN:
case WM_RBUTTONDOWN:
OnMouseDown(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
return 0;
case WM_LBUTTONUP:
case WM_MBUTTONUP:
case WM_RBUTTONUP:
OnMouseUp(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
return 0;
case WM_MOUSEMOVE:
OnMouseMove(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
return 0;
파생 클래스 생성 하는 규칙
//D3Dapp 상속 받은 파생 클래스 형태 및 구조
class InitDirect3DApp : public D3DApp
{
public:
InitDirect3DApp(HINSTANCE hInstance);
~InitDirect3DApp();
virtual bool Initialize()override; //초기화
private:
virtual void OnResize()override; //창 사이즈 변경 시 수정 사항
virtual void Update(const GameTimer& gt)override; // 매 프레임 동작 관련
virtual void Draw(const GameTimer& gt)override;//매 프레임 렌더링
};
//window 창 생성 시 호출, Directx 초기화, run 실행
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance,
PSTR cmdLine, int showCmd)
{
// Enable run-time memory check for debug builds.
#if defined(DEBUG) | defined(_DEBUG)
_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
#endif
try
{
InitDirect3DApp theApp(hInstance);
if(!theApp.Initialize())
return 0;
return theApp.Run();
}
catch(DxException& e)
{
MessageBox(nullptr, e.ToString().c_str(), L"HR Failed", MB_OK);
return 0;
}
}
//매 프레임 렌더링
void InitDirect3DApp::Draw(const GameTimer& gt)
{
//명령 할당자 초기화, gpu 관련 명령 목록 모두 처리 후 호출 된다.
ThrowIfFailed(mDirectCmdListAlloc->Reset());
//명령 목록 재설정
ThrowIfFailed(mCommandList->Reset(mDirectCmdListAlloc.Get(), nullptr));
//current backbuffer 의 자원 상태 전이를 directx 통지
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));
//뷰포트 및 가위 직 사각형 설정( 명령 목록 을 재설정 시 )
mCommandList->RSSetViewports(1, &mScreenViewport);
mCommandList->RSSetScissorRects(1, &mScissorRect);
// 후면 버퍼와 깊이 버퍼 초기화
//렌더 대상, 지우는 색상,
// prects 배열의 원소(사용 안함 0), 렌더 대상에서 지울 직사각형 영역
mCommandList->ClearRenderTargetView(CurrentBackBufferView(),
Colors::LightSteelBlue, 0, nullptr);
//깊이 스텐실 버퍼 dsv, 깊이 또는 스텐실 중 지우는 요소(깊이, 스텐실, 둘다)
//깊이 버퍼 지우는 값, 스텐실 버퍼 지우는값,
//prects 배열 원소, 렌더 대상에서 지울 직사각혀 영역
mCommandList->ClearDepthStencilView(DepthStencilView(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
//렌더링 결과가 기뢱될 렌더 대상 버퍼을 지정
//파이프 라인에 렌더 대상, 깊이 스텐실 버퍼 묶음
//RTV 개수, rtv, rtv 서술자 힙에 연속적 저장시 true, dsv
mCommandList->OMSetRenderTargets(1, &CurrentBackBufferView(), true, &DepthStencilView());
//currentbackbuffer 자원 상태 전이
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));
// 명령 기록을 마친다
ThrowIfFailed(mCommandList->Close());
// 명록 기록을 명령 대기열 추가
ID3D12CommandList* cmdsLists[] = { mCommandList.Get() };
mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
// 화면에 렌더링, 후면 전면 버퍼 교환
ThrowIfFailed(mSwapChain->Present(0, 0));
//후면 버퍼 색인 갱신
mCurrBackBuffer = (mCurrBackBuffer + 1) % SwapChainBufferCount;
//해당 프레임 명령 처리 동안 대기(비 효율적 방법)
FlushCommandQueue();
}
~D3dapp() : 소멸자 에서는 GPU 의 명령 대기열을 비운 는 함수 호출
gpu에서 참조 하는 자원들을 안정적으로 파괴
Handle : 핸들 테이블<리소스 주소 , 핸들 값(int) >을 이용 해서 운영체제 의 리소스를 참조 한다.
프로그램의 안정성을 높일 수 있다.
Flush command queue() : GPU 처리중 CPU를 대기 하게 만든다.
여섯 가상 함수를 재정의 후 사용
Initialize() : 프로그램의 고유 초기화 코드 작성
msgproc : 윈도위 메시지 처리 및 메시치 처리를 안할 때 렌더링 함수 호출
Creatertvanddsvdescriptorheaps : RTV 서술자 및 DSV 서술자 생성 및 각 서술자에 맞는 힙을 생성
OnResize : 화면 사이즈가 변경 될 떄 호출
후면 버퍼와 깊이 스텐실 버퍼의 사이즈를 변경 한다 후면 버퍼의 경우 함수 호출로 변경 하지만
깊이 스텐실 버퍼는 파괴 후 생성 한다
후면 버퍼와 깊이 스텐실 버퍼를 데이터가 수정 후 각각의 뷰를 새롭게 구성
그 외에도 화면 크기에 연관된 투영 행렬도 수정
update : 시간의 흐름에 따른 응용 프로그램 갱신 (애니메이션, 카메라, 충돌 검출, 사용자 입력)
Draw : 렌더링 명령을 직접적으로 호출
프레임 통계치
1초당 평균 프레임 수 = 프레임 /t : t를 1로 지정 하면 나누기 연산을 패스
평균 프레임 당 처리 속도 = 1000 / 1초당 프레임
평균 프레임 당 처리 속도가 중요한 이유는 프로젝트 개발 시 처리 속도를 명확하게 알 수 있다.
메시지 처리부
응용 프로그램의 핵심부 는 유후 처리 도중에 실행 된다. 윈도우 메시지가 발생하지 않을 경우
WM_ACTIVATE
응용 프로그램이 비활성 화 시 타이머 정지, 응용 프로그램의 상태를 갱신 하지 않는다.
CPU 주기를 낭비 하는 일 없다.
WM_SIZE
응용 프로그램 창 크기가 변경 시 발생
만약 프로그램 창을 드래그 시 계속해서 후면 버퍼, 깊이 스텐실 버퍼를 수정 하는 건 매우 비효율적
그래서 드래그가 종료되는 시점에 후면 버퍼 스텐실 버퍼를 수정 한다.
초기화 예제 중요 함수
Draw 함수 처리 순서
- 명령 기록에 관련된 메모리 재활용 으로 명령 할당자 재설정(GPU 명령 목록 처리 후 reset)
- 명령 목록 재 설정(GPU 명령 대기열에 추가 된 후 )
- 뷰포트, 가위 직사각형 설정
- 후면 버퍼와 깊이 버퍼를 초기화(현재 후면 버퍼의 색인을 이용해서 후면 버퍼 초기화)
- 렌더링 결과가 기록 될 렌더 대상 버퍼 저장
- 렌더링에 사용할 렌더 대상과 깊이 스텐실 버퍼를 파이프라인 에 묶는다.
- 명령들의 기록 마친다
- 명령 목록을 명령 대기열에 추가
- 후면 버퍼와 전면 버퍼 교환 후 후면 버퍼의 색인을 갱신
- 명령 들이 모두 처리하길 대기, 비효율적 flushcommandqueue()
디버깅
HRESULT 오류 부호 error code를 돌려 준다.
TrowIfFailed : 매크로 함수, 일반 함수로 구현 되면 일반 함수의 파일과 행 파일로 치환 되기 때문에 매크로를 사용 한다.
참고 및 내용 인용
프랭크 D 루나 지음, 류광 옮김
한빛 미디어
Directx 12를 이용한 3D 게임 프로그래밍 입문(2017)