랜더링에 대해 - GPU
컴퓨터는 여러 부품을 가지고 있지만 그 중 3대 부품을 꼽자면 CPU, 메모리, 보조기억장치(SSD) 일 것이다.
우리가 맨 처음 운영체제를 설치하고나면 보조기억장치에 저장이 될 것이다. 그리고 컴퓨터를 실행하면 그 것을 메모리에 옮겨서 차지하게 된다. 우리가 본격적으로 프로그래밍을 통해 프로그램을 실행하면 로직은 CPU가 처리하고 그 안에 데이터를 보관 있는 것은 메모리이다.
이런 식으로 기본적으로 뭔가 진행하는데 있어서 중요한 3대 부품은 위와 같지만, 게임을 좋아하는 사람이라면 당연히 알 수 있듯이 GPU 역시 매우 중요하다.
그림을 한번 바꿔서 CPU랑 GPU랑 무엇이 다르고 어떤 역할을 하는지 알아보자.
근본적으로 CPU는 무언가를 "연산" 하기 위함이고 메모리는 "저장"하기 위함이다. 그렇다면 GPU의 본질은 CPU에 가까울까 아님 메모리에 가까울까
정답은 GPU는 CPU 처럼 무언가를 "연산" 하는게 목적이다. 물론 저장하는 공간이 없진 않지만 비율이 99 : 1 일정도로 적다.
결국 CPU와 GPU의 차이는 위의 사진을 보면 알 수 있다. CPU보다 ALU(사실상 코어, 연산 담당)의 비율이 GPU가 월등하게 높다. 대신 캐시 등 저장하는 비율은 훨씬 적다.
결국 둘의 차이는 CPU는 범용적인 일을 전부 처리할 수 있는 고급인력이지만 사람 수가 적은 느낌이고 GPU는 범용적인 일을 처리할 순 없지만 많은 인력으로 처리하는 느낌인 것이다.
그래서 중요한 일이나 어려운 연산 같은 경우는 CPU가 처리하다 반복적이고 쉬운 작업이지만 연산이 많이 필요한 경우 GPU가 처리하게 외주주는 것이다.
렌더링에 대해 - 게임에서의 GPU
그렇다면 게임에서 GPU는 왜 필요한 것인가?
3D 공간에서 여러 오브젝트가 이동하고, 스킬쓰고 등 난리를 칠텐데 이걸 2D 화면에 매치시켜 특정 픽셀(화면이 커질 수록 더 많이)에 보여줘야한다. 거기서 60fps 라면 1초당 60번을 동기화 해야하기 때문에 엄청 많은 연산을 필요로 하기 때문이다.
이렇게만 보면 와닿지 않을 수 있다.
게임이 동작하는 방식에 대해 알아야한다.
- A 물체를 모델링 프로그램에서 만든다.(수 많은 Wireframe을 가지고 있을 것이다.)
- A 물체를 게임의 월드에 배치한다.
- 카메라는 자신이 비추고 있는 게임의 월드에서 현재 화면을 2D로 변환해야한다.
3번 과정에서 수 많은 Wireframe에서 정점마다 좌표를 전부 계산해야하며 이러한 오브젝트가 하나만 있는 것이 아닌 수백개가 될것이다. 그리고 빛에 따른 그림자나 물체에 대한 색상을 전부 계산해야한다.
게임은 영화와 비슷하다. 영화처럼 한 장면을 촬영하고 그걸 유저에게 보여주는 것과 같다. 다만 영화와 다른 점은 바로 "실시간" 반영이라는 것이다. 영화를 볼때 우리가 화면을 좌로 돌린다고 해도 안의 화면의 좌로 회전하진 않지만 게임은 다르다. 플레이어가 점프하거나 화면을 회전하면 그 때마다 전부 실시간으로 다시 계산을 해야한다.
이 처럼 수많은 연산을 하기 위해서는 CPU만으로는 부족하기에 GPU가 필요한 것이다.
결론은 3D 세상에서 2D 화면으로 실시간으로 투영하는 과정의 연산은 매우 많기 때문에 CPU 혼자만 할 수 없고 GPU가 필요하다는 것이다.
그러면 CPU가 GPU에게 연산을 해달라고 통신을 해야할 터인데 이걸 어떻게 할까?
렌더링 파이프라인을 이용한다.
파란색 영역은 우리가 실제로 코딩할 수 있진 않고 옵션을 주어 제어할 수 있는 부분이고 초록색 영역은 우리가 코딩할 수 있는 영역이다.
입문하는 입장에서 가장 중요한 단계는
- Input-Assembler Stage(물체에 대한 기하학적인 정보 등을 이 단계에 전달, 이러한 물체가 등장할 것이다 라는 느낌)
- Vertex Shader Stage(정점을 대상으로 좌표를 계산, 로컬좌표를 월드좌표로 바꾸고 월드좌표에서 다시 카메라 2D 좌표로 변환 계산, 행렬을 이용)
- Rasterizer Stage(정점으로 계선된 영역을 보간 하는 역할)
- Pixel Shader Stage(각 픽셀에 따라 색상을 골라주는 역할)
- Oupput-Merger Stage(화면에 송출하는 역할)
이 5개 이다. 렌더링 파이프라인 등 작업을 한번 작업하고 편리하게 묶어서 사용할 수 있게 한 것이 바로 게임 엔진이다.
결론적으로 렌더링을 공부한다는 것은 게임 엔진에 대한 이해도를 높이는 것이다.
기본 프레임워크
이제 실습하기 위해 프로젝트를 만들어보자.
Windows 데스크톱 애플리케이션으로 만든다.
소스 파일 들을 다음과 같은 규칙으로 정리하고
속성 페이지에 들어가 C++란 미리 컴파일된 헤더 칸에서 사용으로 바꿔준 다음에 pch.h로 바꿔준다.
이제 헤더 폴더에 pch 클래스를 만들어준다.
이렇게 하면 이제 언제 어디서든 우리가 공용으로 사용할 기능을 pch를 통해 이용할 수 있다.
그리고 여러 헤더파일들을 만들어 볼텐데 이는 우리가 편리하게 사용하기 위함이다.
가령 Types 같은 파일에는 이렇게 적용해 int를 편하게 불러오기 위함이다.
#pragma once
#include "DirectXMath.h"
using int8 = __int8;
using int16 = __int16;
using int32 = __int32;
using int64 = __int64;
using uint8 = unsigned __int8;
using uint16 = unsigned __int16;
using uint32 = unsigned __int32;
using uint64 = unsigned __int64;
// 벡터를 미리 지정
using Vec2 = DirectX::XMFLOAT2;
using Vec3 = DirectX::XMFLOAT3;
using Vec4 = DirectX::XMFLOAT4;
using Color = DirectX::XMFLOAT4;
이제 이걸 pch에 연동해서 사용하면 된다.
#pragma once
#include "Types.h"
#include "Values.h"
#include "Struct.h"
// STL
#include <vector>
#include <list>
#include <map>
#include <unordered_map>
using namespace std;
// WIN
#include <windows.h>
#include <assert.h>
// DX
#include <d3d11.h>
#include <d3dcompiler.h>
#include <wrl.h>
#include <DirectXMath.h>
#include <DirectXTex/DirectXTex.h>
#include <DirectXTex/DirectXTex.inl>
using namespace DirectX;
using namespace Microsoft::WRL;
#pragma comment(lib, "d3d11.lib");
#pragma comment(lib, "d3dcompiler.lib");
#ifdef _DEBUG
#pragma comment(lib, "DirectXTex\\DirectXTex_debug.lib")
#else
#pragma comment(lib, "DirectXTex\\DirectXTex.lib")
#endif
이렇게 pch에 적어두면 이제부터 생성되는 모든 cpp 파일은 알아서 pch.h가 들어갈 것이고 우린 아무런 제한 없이 사용할 수 있다.
밑의 DirectX 부분은 외부 라이브러리를 끌고 온 것이니 각자 세팅해서 끌어오면 된다.
이렇게 설정하고
#include "pch.h"
#include "framework.h"
#include "GameCoding.h"
#include "Game.h"
#define MAX_LOADSTRING 100
// 전역 변수:
HINSTANCE hInst;
HWND hWnd;
// 이 코드 모듈에 포함된 함수의 선언을 전달합니다:
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
// 1) 윈도우 창 정보 등록
MyRegisterClass(hInstance);
// 2) 윈도우 창 생성
if (!InitInstance(hInstance, nCmdShow))
return FALSE;
Game game;
game.Init(hWnd);
MSG msg = {};
// 기본 메시지 루프입니다:
while (msg.message != WM_QUIT)
{
if (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
game.Update();
game.Render();
}
}
return (int)msg.wParam;
}
//
// 함수: MyRegisterClass()
//
// 용도: 창 클래스를 등록합니다.
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEXW wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_GAMECODING));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = L"GameCoding";
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
return RegisterClassExW(&wcex);
}
//
// 함수: InitInstance(HINSTANCE, int)
//
// 용도: 인스턴스 핸들을 저장하고 주 창을 만듭니다.
//
// 주석:
//
// 이 함수를 통해 인스턴스 핸들을 전역 변수에 저장하고
// 주 프로그램 창을 만든 다음 표시합니다.
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance; // 인스턴스 핸들을 전역 변수에 저장합니다.
RECT windowRect = { 0, 0, GWinSizeX, GWinSizeY };
::AdjustWindowRect(&windowRect, WS_OVERLAPPEDWINDOW, false);
hWnd = CreateWindowW(L"GameCoding", L"Client", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, windowRect.right - windowRect.left, windowRect.bottom - windowRect.top, nullptr, nullptr, hInstance, nullptr);
if (!hWnd)
{
return FALSE;
}
::ShowWindow(hWnd, nCmdShow);
::UpdateWindow(hWnd);
return TRUE;
}
//
// 함수: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// 용도: 주 창의 메시지를 처리합니다.
//
// WM_COMMAND - 애플리케이션 메뉴를 처리합니다.
// WM_PAINT - 주 창을 그립니다.
// WM_DESTROY - 종료 메시지를 게시하고 반환합니다.
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// 메뉴 선택을 구문 분석합니다:
switch (wmId)
{
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: 여기에 hdc를 사용하는 그리기 코드를 추가합니다...
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
#pragma once
class Game
{
public:
Game();
~Game();
public:
void Init(HWND hwnd);
void Update();
void Render();
private:
HWND _hwnd;
uint32 _width = 0;
uint32 _height = 0;
private:
// DX
};
이렇게 함으로써 우리는 이제 Game에만 코드를 작성하면 원하는 작업을 할 수 있게 세팅이 된 것이다.
'C++ > DirectX 11' 카테고리의 다른 글
[DirectX] 프레임워크 제작기 - Graphics, IA, Geomotry (1) | 2024.09.02 |
---|---|
[DirectX] 행렬 - SRT 변환 행렬과 좌표계 변환 행렬 및 예제 (0) | 2024.08.31 |
[DirectX] DirectX 11 입문하기 - Constant Buffer와 State (0) | 2024.08.29 |
[DirectX] DirectX 11 입문하기 - 텍스쳐와 UV (2) | 2024.08.28 |
[DirectX] DirectX 11 입문하기 - 장치 초기화와 삼각형 띄우기 (0) | 2024.08.28 |