Part1

2025.09.09

wWinMain 설명

Windows c++ Destktop 프로젝트를 생성하면 wWinMain 에서 시작한다.

UNREFERENCED_PARAMETER 매크로는 아무 작업도 안하고 그대로 뱉어줌. 지워도됨

IDS_APP_TITLE 이런애들은 그냥 리소스뷰(Ctrl+Shift+E -> StringTable) 스트링테이블에 있는 값을 참조함

#define MAX_LOADSTRING 100
WCHAR szTitle[MAX_LOADSTRING];    // 전역변수 
// 스트링테이블로부터 문자열 로드해서 szTitle 전역배열에 저장 
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);

윈도우를 특정 설정값들로 등록하고, lpszClassName 를 키값으로 CreateWindowW 호출하면 설정했던 값으로 윈도우가 생성된다.

WNDCLASSEXW wcex;
// ... 설정값 지정
wcex.lpszClassName  = szWindowClass;    // 문자열로 키 값 세팅
RegisterClassExW(&wcex);      // 윈도우 등록

// 그 키 값으로 윈도우 생성하면 위에서 설정한 값으로 생성된다. 
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
   CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);

인텔리센스 반응 느릴땐 Debug 빌드 Release 빌드로 바꿨다가 다시돌아오면 빠르게인식됨

HWND는 윈도우 핸들값(커널 오브젝트 ID값)

#define DECLARE_HANDLE(name) struct name##__{int unused;}; typedef struct name##__ *name
DECLARE_HANDLE            (HWND);

// 아래와 동일한 선언
// int크기의 HWND__ 타입 구조체를 만들고 그 포인터형으로 사용하는게 HWND(포인터크기)이다.
struct HWND__ {
  int unused;
};
typedef struct HWND__ *HWND; 

커널오브젝트는 종류가 아주 많은데(HPEN, HBRUSH, HDC…), 모두 내부에 ID 값이 저장되지만 각 핸들 타입마다 인덱스를 겹쳐도 상관없도록 타입을 나누게 된것이다.

HINSTANCE도 사실 DECLARE_HANDLE로 만들어져 있고 인스턴스의 핸들이라는 느낌이다. 사실 인덱스가 저장되는 다른 애들과는 다르게, 이 포인터에는 모듈(프로세스)의 베이스 주소가 저장된다. HMODULE도 모듈의 베이스주소가 저장된다.

LoadAccelerators 는 리소스뷰에서 볼수있는 단축키테이블을 로드한다.
메시지루프 안 TranslateAccelerator 함수에서 단축키가 발생했는지 확인하고 만약 발생했다면 내부에서 메시지를 처리한다.

루프에서 GetMessage를 호출하면 메시지 없을때 블록되기 때문에 논블러킹의 PeekMessage로 변경해야한다.

GetMessage는 종료메시지인 경우에만 false 반환, PeekMessage는 없으면 false, 있으면 true.
메시지라는건 윈도우에서 프로세스에 전달해준 메시지들이고, PM_REMOVE 옵션으로 픽메시지로 메시지 가져오면 메시지큐에서 제거하게 만들어야한다.

2025.09.16

프로젝트 속성

헤더나 전처리기 설정

include directories : 헤더 -I 할 경로
-> 헤더에서 #include <Engine/Test.h> 형식으로 가져올 수 있음

Library Directories : 빌드된 라이브러리
-> 나중에 #pragma comment(lib, “Engine/Engine_d.lib”) 처럼 가져온다고 등록함 .

프로젝트 속성 -> C/C++ -> Preprocessor 이쪽으로 들어오면 전처리기가 기본으로 설정하는 DEFINE 값들을 확인할 수 있음

79c9a719-a721-4522-b6d9-cc06f486cdad
79c9a719-a721-4522-b6d9-cc06f486cdad

Debug 빌드일땐 _DEBUG 라는 정의가 생겨서 그걸로 ifdef 처리를 할 수 있다.

#ifdef _DEBUG
#pragma comment(lib, "Engine/Engine_d.lib")     // 디버그빌드된 라이브러리 추가
#else
#pragma comment(lib, "Engine/Engine.lib")       // 릴리즈빌드된 라이브러리 
#endif

빌드이벤트

프로젝트 속성 -> Build Events -> Pre-Build Event -> Command Line
빌드하기전에 실행시킬 작업

da5e4eb4-83c3-45fc-bac2-3d7840a86c3d
da5e4eb4-83c3-45fc-bac2-3d7840a86c3d

빌드 종속성

Engine이 빌드되고 그걸 Client가 사용하는것이면 Client가 Engine에 종속된다는 것임
프로젝트 우클릭 -> Build Dependencies -> Project Dependencies 에서 설정 가능함

ee148798-4773-48d5-888a-01b0524254ba
ee148798-4773-48d5-888a-01b0524254ba

싱글턴

힙 사용한 싱글턴

private static 으로 멤버변수 만들어두면 외부에서는 접근할 수 없는 전역 힙 싱글턴 포인터가 된다.

외부에서 생성할 수 없도록 생성자도 private으로 구현하고 GetInst로만 가져올 수 있다.
힙메모리니까 사용이 끝났을때 명시적으로 제거할 수 있도록 Destroy 함수를 제공해야한다.

class CEngine
{
private:
	static CEngine* g_This;

public:
	static CEngine* GetInst()
	{
		if (g_This == nullptr) {
			g_This = new CEngine;
		}
		return g_This;
	}

	static void Destroy()
	{
		if (g_This != nullptr)
		{
			delete g_This;
			g_This = nullptr;
		}
	}

private:
	CEngine();
    CEngine(const CEngine& _origin) = delete;
};

생성자를 프라이빗으로 막았다 하더라도 복사생성자로 비슷한 효과를 낼 수 있기 때문에 디폴트 복사생성자도 제거해줘야 한다.

CEngine* pEngine = CEngine::GetInst();
CEngine e = *pEngine;   // 디폴트 복사생성자를 호출해서 엔진을 무한 복사할 수 있다.

static 변수 싱글턴

마찬가지로 외부에서 마음대로 생성할 수 없도록 생성자는 private으로 구현한다.
GetInst 함수 안에서 static 변수로 data영역에 CEngine을 넣어두고 그 주소만 반환하는 형식으로 구현할수도 있다.

함수 안쪽의 static 변수는 초기에만 생성되고 이후엔 생성되지 않기 때문에 단 하나만 유지할 수 있게 된다.

이 방법은 Destroy를 제공하지 않아도 되지만, 프로그램이 실행중인 도중에 절대 제거할 수 없다.
힙 방식은 계속 제거하고 다시 생성하고가 가능하다.

이 방식으로 매니저를 너무 많이 만들면, 특정 이벤트때 사용한 싱글턴 매니저들이 아직가지 살아있을 수 있다는 코서운이야기..

class CEngine
{
public:
	static  CEngine* GetInst()
	{
		static CEngine mgr;
		return &mgr;
	}
private:
	CEngine();
};

DirectX

DLL끼리 교차 new/delete 금지

DLL에 요청해서 받아온 메모리를 우리쪽 코드에서 직접 delete 하면 안된다.
힙의 할당과 해제는 CRT(CRuntime)의 얼로케이터에서 진행하는데, 바이너리 빌드할때 /MT로 빌드하면 CRT를 정적라이브러리 형태로 빌드해서 직접 코드가 바이너리에 포함된다.

  • 바이너리의 타겟 빌드타입에 따라 디버깅이면 디버깅용 CRT 얼로케이터가 사용돼서 힙의 모양이 릴리즈와 달라 delete할때 다른모양으로 제거될 수 있어 위험하다.
  • 사실 이 부분은 자세히 이해가 안된다. 다시 시간내서 확인할필욕 ㅏ있음

CDevice

class CDevice
{
private:
	ID3D11Device*			m_Device;		// GPU 메모리 할당, Dx11 관련 객체(텍스쳐, 버퍼, 뷰 등) 생성 
	ID3D11DeviceContext*	m_Context;		// 생성된 리소스로 GPU 렌더링 관련 그래픽스 명령 기능

	IDXGISwapChain*			m_SwapChain;	// GPU메모리의 렌더 타겟 버퍼들을 소유하면서 화면에 최종 장면을 게시
    // ...
}

    // Device, Context 생성
	D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE,
					nullptr, iFlag, nullptr, 0, D3D11_SDK_VERSION,
					&m_Device, &level, &m_Context);

2025.10.19

DirectX

IDXGIDevice는 다렉 버전에 상관없이 공통적인 인터페이스(중간층) 역할을 한다

SwapChain

GPU 메모리의 버퍼를 스왑체인이 관리하는것이다. 프레임을 아무리 높여서 그려도 모니터의 주사율에 맞춰서 화면에 표시되기 때문에 많이 그릴필요는 없다.

우리는 스왑체인이 관리하는 버퍼에 그림을 그리고 스왑체인이 자동으로 화면에 출력하게 된다. 이때 버퍼는 그림을 그릴 대상 렌더타겟이라고 부른다. 그런데 스왑체인이 항상 화면출력용이 되지는 않아서 꼭 백버퍼가 렌더타겟인것은 아님

DX11까지는 스왑체인이 자동으로 관리해줘서 0번 버퍼가 백버퍼이다.

스왑체인이 데이터(버퍼)를 화면에 표시하는 방식

  • 비트블록전송(BitBlt) : 화면에 하나씩 복사시키는 방식. 백버퍼는 DWM(데스크톱창관리자) 에 버퍼를 복사하고 윈도우쪽에서 타이밍에 맞춰서 알아서 가져감. 프로그램의 프레임이 높더라도 어차피 화면에 표시되는건 주사율을 따라간다. 버퍼하나에 쓰고 DWM으로 복사하기 때문에 티어링발생할 수 있음. (레거시 호환용)
    • DXGI_SWAP_EFFECT_DISCARD : 화면에 출력 후 버퍼가 유지하지 않고 버림
    • DXGI_SWAP_EFFECT_SEQUENTIAL : 화면에 출력 후 버퍼에 유지
  • 대칭이동프레젠테이션 모델 : 백버퍼가 2개가 필요하고 V-Sync를 사용할 수 있는 방법임. 만들어놓고 DWM에 버퍼(렌더타겟)의 포인터를 전달함. 다음 그림은 반대쪽 버퍼에 그리면서 플리핑함. 복사가 아니라 포인터전달이기 때문에 성능상 이점이 있을 수 있음. 대신 프로그램 프레임이 고정된다.
    • DXGI_SWAP_EFFECT_FLIP_DISCARD
    • DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL

프로그램을 종료할 때 아래 에러가 발생하는건 DX11 라이브러리 객체 레퍼런스 카운트 문제가 발생한것. Release를 꼭 해줘야 한다. DX11 은 어떤 함수를 호출해서 다렉 라이브러리가 관리하는 특정 객체의 포인터를 가져올땐 스마트포인터로 가져오게된다. 그래서 레퍼런스카운트를 잘 관리해줘야한다.

1c396b60-1395-44d0-95e9-76d3e741557d
1c396b60-1395-44d0-95e9-76d3e741557d

View

  • RednerTargetView : 스왑체인에서 렌더타겟텍스쳐를 가져오면 렌더타겟뷰로 꺼내 사용해야한다. 스트링뷰 생각해보셈.
  • DepthStencilTexture : 렌더링파이프라인에서 실제 출력될 화면데이터(RenderTargetTexture)와 함께 깊이정보를 알려줘야해서 이걸 저장하는 버퍼(리소스원본)
  • DSView : 이걸 컨트롤하는 뷰 (전달자? 역할) 렌더링파이프라인에는 실제로 뷰 형태로 전달해야하기 때문

ComPtr

각각의 다렉객체를 직접 Release해야되는 부분이 있어서 불편함이 많다. 이때 관리하기 귀찮으니 ComPtr<타입> 으로 스마트포인터로 자동으로 관리되게 할 수 있다.

ComPtr은 생성자에서 자동으로 알아서 nullptr로 초기화해줌. 소멸할때도 자동으로 Release 해준다. (ComPtr이라서 COM객체인걸 아니까 Release를 할 수 있는것이다)

.Get 메서드는 포인터(멤버)를 리턴하는거고, .GetAddressOf 는 그 포인터(멤버)의 주소를 리턴.
사실 내부적으로 포인터 하나만 들고있는 객체이기 때문에 ComPtr p; &p; 와 p.GetAddressOf(); 는 같은 주소를 리턴할 것이지만, 구현체는 컴파일러마다 다를 수 있어서 확실히 하는게 좋다.

ViewPort

스왑체인이 생성될때 전달된 윈도우핸들에 맞는 윈도우에 렌더링할때 어느정도만 그려질지 정하기 위해 ViewPort를 지정해야한다.
내부적으로는 렌더링파이프라인에서 처음부터 렌더타겟에서 뷰포트에 해당되는 위치에만 해당되는 크기만큼 그리고 전체를 복사하는 식으로 진행된다.

GPU의 Color

원래 색상을 지정할때 255,255,255,0 이런식으로 RGBA를 정수로 줬는데, GPU로 넘어갈땐 0-1 정규화를 거친다. 그렇기 때문에 GPU에서는 1.f가 255를 의미한다.

그래픽스 파이프라인

IA (Input Assembler)

데이터를 전달하는 단계

  • Vertex Buffer (중요)
  • Index Buffer (중요)
  • Topology
  • Layout

Vertex Shader Stage

정점을 3차원 모델 좌표계에서 2차원 좌표로 맵핑하는 연산 진행.
각 정점마다 하나의 함수를 실행하는데 GPU에서 동시에 실행시킴

Tessellation

HullShader, DomainShader

Geometry Shader

Rasterizer

정점데이터를 넘기면 래스터화(정점데이터가 픽셀단위 데이터로 변환) 시켜서 픽셀데이터 연결하고 각 픽셀마다 픽셀쉐이더를 호출함

3D좌표계에서 정점3개면 하나의 면이 만들어진다. 그 면에 해당하는 픽셀들이 어떤것들인지 연산해서 데이터화하는 작업이 레스터화이다.

PixelShader

픽셀당 호출되는 함수. 각 픽셀의 색상을 렌더타겟에 출력함

OM(Output Merge State)

출력하려는 픽셀지점과 1:1 매핑되는 깊이정보를 이용해서 현재출력 되어있는 픽셀들과의 깊이 테스트(DepthSetencil State)를 진행함. 그래서 깊이가 더 깊으면 출력이안됨.
-> 그래서 깊이버퍼는 초기화되면 1로 세팅된다. (0~1 사이의 값만 넣을 수 있기때문)

출력은 BlenState 에서 명시한 블렌드 공식에 따라서 블렌딩완료후 최종 색상이 출력된다.

최종 출력

RenderTargetTexture 에는 픽셀쉐이더가 출력한 최종색상이 출력되고,
깊이값은 DepthStencilTexture에 출력

2025.10.30

DirectXTK

https://github.com/microsoft/DirectXTK/wiki/SimpleMath

다이렉트X 툴킷이다. 여기에서는 SimpleMath를 가져와서 쓸것이다.
다렉에서는 XMFLOAT2, XMFLOAT3 같은 구조체가 만들어져 있고, 이걸 XMVECTOR 라는 다렉 구조체로 변환해서 넣어줘야하는데, 이게 정말 불편하다.
그래서 누군가가 XMFLOAT 들을 상속받고 멤버는 동일하게 만든 후 미리 덧셈뺄셈곱셈비교내적등 연산 함수들을 구현해둔게 DirectXTK 프로젝트의 SimpleMath 이다.

https://github.com/assortrockstudio/AssortrockDX_Part1/tree/main/Class_07_08/DirectX11Engine/Project/Engine

그런데 강사가 또 추가한게 있기 때문에 udemy에서 가져온걸 쓰자.

좌표평면 정점

3차원 좌표평면에 점을 나타내는 구조체. 나중에 이 점을 3개를 연결해서 하나의 면을 만들건데 점이 두개라면 면이 나올수있는 경우의수가 무한대지만 3차원에서 단 하나만 존재할 수 있게 하는게 점이 3개일때이다.
Vector3로 구현하면 되는데, 이걸 정점(vertex)이라고 한다. 그런데 이 정점은 그래픽에서 면을 표현하기 위해 사용하는 것이기 때문에 xyz 좌표만 있는게 아니라 여러 정보가 더 포함되어야한다. (컬러값이나 텍스쳐좌표계UV 빛반사를 위한 노말값 등)

DX

  • ID3D11Resource: GPU 메모리에 데이터를 할당하고 관리하는 역할
  • ID3D11Texture2D : 2차원 텍스쳐(이미지)형태를 저장하는 클래스. 그런데 당연하게도 GPU메모리공간에서 관리되기 때문에 Resource를 상속받음
  • ID3D11Buffer: 용도가 결정되지 않은 메모리 덩어리라면 Buffer로 생성 -> 버텍스 저장할때도 씀 그런데 결국 이것도 GPU메모리에서 관리하는 것이기 때문에 ID3D11Resource를 상속받음. 둘다 공통 분모가 있다.

NDC 좌표계

노말라이즈 된 좌표계이기 때문에 어떤 해상도라도 정규화해서 -1~1 사이에서만 사용된다. 가로세로 2인 사각형이라고 생각하고 사용하는데, 해상도를 맞추려면 개발자가 계산해서 찌그려트려서 넣어줘야 한다.
윈도우 좌표는 아래가 플러스지만, NDC좌표계는 아래가 마이너스이다.

소멸자가 private이면?

외부에서 호출하지 못하게 막는거임. 힙전용 객체로 강제하거나 인터페이스로부터 외부 delete 금지하는 등.
참조카운팅 팩토리패턴에서 destroy()나 release()로만 파괴하게 하려고함

2025.10.30

HLSL

쉐이더쪽의 C++ 언어라고 생각하면된다.
원래 hlsl확장자로 하면 쉐이더당 하나씩 파일이 만들어져야 한다. .fx 확장자를 사용하면 하나의 파일안에서 함수만 나눠서 쉐이더를 나눌 수 있다. (옛날에 이펙트프레임워크 확장자인데 그때는 그랬다.)

속성 > 모든구성 > HLSL 컴파일러 > 쉐이더형식 /fs , 쉐이더모델 5.0

버텍스쉐이더 함수는 어떤 정점형태든 받을 수 있어야한다.
버텍스쉐이더는 정점의 모든 정보가 필요없다. 쉐이더별로 필요한 정보가 다름.
그래서 각 정점에서 쉐이더가 필요로하는 데이터가 어떤 오프셋에 있는지 구조를 알려줘야한다. (인풋레이아웃)

레이아웃은 ID3D11InputLayout 이걸로 받고, D3D11_INPUT_ELEMENT_DESC 여기에 전달할 엘리먼트당 하나씩 정보를 담아서 보낸다.

2025.11.02

쉐이더

D3DCompileFromFile 로 컴파일하고 ID3DBlob에 저장한다.
이후 CreateVertexShader 함수로 컴파일된 Blob을 VertexShader에 넣는다.

릴리즈일때 GetCurrentDirectory 함수를 통해 얻을 수 있는 경로는 실제 바이너리 실행 경로인데, VisualStudio로 실행시킬 땐 프로젝트 속성 > 디버깅 > 작업디렉터리 이 설정을따라간다. 그래서 이 경로를 실제 바이너리 컴파일 경로로 맞춰줘야 한다.

GetCurrentDirectory 함수를 기준으로 상대경로로 접근하면서 작업하면 된다.

2025.11.03

키매니저

틱 시작부분에서 키매니저가 모든 키를 한번 확인해서 키 상태를 확정지어놓고 모든 오브젝트에 적용하기 위해

헤더에다가 구현하면 인라인처리됨

인덱스버퍼

만약 사각형을 그리려면 정점이 4개가 아닌 6개가 필요함. 왜냐하면 트라이앵글리스트를 사용하고 있기 때문에 버텍스쉐이더가 무조건 3개씩 2번 총 6번 호출돼야 삼각형 두개의 범위를 파악할 수 있기 때문..

// 정점 배열 6개짜리 필요
0     3--4
| \    \ |
2--1     5

// 정점 배열 4개짜리 필요. 대신 인덱스버퍼를 추가로 써야됨
0     0--3
| \    \ |
2--1     1
IB [0,1,2,0,1,3]

하지만 중복되는 정점은 메모리가 아깝다. 이때 인덱스버퍼를 사용함
어차피 트라이앵글리스트를 사용하는 한 삼각형당 버텍스쉐이더 연산량은 똑같다.

백페이스 컬링

사각형을 그릴때 쉐이더에 들어가는 정점의 순서에 따라 렌더링 자체가 안될 수 있다.

	// 좌표는 그냥 수학적인 좌표평면계 처럼 왼쪽아래가 작다.
	//            0 (-.5,.5)
	//            | \ 
	// (-.5,-.5)  2--1  (.5,-.5)
	g_arrVtx[0].vPos = Vec3(-0.5f, 0.5f, 0.f);
	g_arrVtx[0].vColor = Vec4(1.f, 0.f, 0.f, 1.f);

	g_arrVtx[1].vPos = Vec3(0.5f, -0.5f, 0.f);
	g_arrVtx[1].vColor = Vec4(0.f, 1.f, 0.f, 1.f);

	g_arrVtx[2].vPos = Vec3(-0.5f, -0.5f, 0.f);
	g_arrVtx[2].vColor = Vec4(0.f, 0.f, 1.f, 1.f);

	// 이거 4랑 5랑 위치를 변경하면 렌더링이 안됨.. (백페이스 컬링?)
	// 렌더링은 위에서부터 아래로 되는것이기 때문에 y좌표가 낮아지는쪽이 후순위 정점이 되어야한다.
	// (-.5,.5)   3--4 (.5,.5)
	//             \ |
	//               5 (.5,-.5)
	g_arrVtx[3].vPos = Vec3(-0.5f, 0.5f, 0.f);
	g_arrVtx[3].vColor = Vec4(1.f, 0.f, 0.f, 1.f);

	g_arrVtx[5].vPos = Vec3(0.5f, -0.5f, 0.f);
	g_arrVtx[5].vColor = Vec4(0.f, 1.f, 0.f, 1.f);

	g_arrVtx[4].vPos = Vec3(0.5f, 0.5f, 0.f);
	g_arrVtx[4].vColor = Vec4(0.f, 0.f, 1.f, 1.f);

쉐이더파일

모딩할때 보통 쉐이더파일을 어차피 런타임에 소스코드안에서 컴파일하기 때문에 간단하게 분석해서 쉐이더파일만 수정하면 게임 전체적으로 분위기가 바뀜

현재까지의 동작구조

틱마다 키입력에 따라 시스템메모리에서 정점 4개의 좌표데이터를 수정한 후 GPU의 버텍스버퍼에 복사함
이후 렌더링 그래픽스 파이프라인에서 IA 과정할 때 버텍스버퍼의 정점들을 인덱스버퍼가 지칭하는 순서대로 버텍스쉐이더를 6번호출함
이 출력결과가 레스터라이저에 전달되고 이후 픽셀마다 픽셀쉐이더로 전달되어 파란색이 출력됨

이 방법은 같은 연산을 모든 정점에 대해서 적용하게 되는 문제도 있고(정점이 많아지면 노답임),
RAM -> GPU메모리 로 데이터를 틱마다 옮기기때문에 아주 느려지게된다.

그리고 같은 자원을 두개 그린다고 햇을때, 현재 구조로 보면 정점 포지션을 직접 수정해서 위치를 옮기기 때문에 같은 물체를 그리더라도 포지션이 다른 사각형이 필요해서 정점 수가 2배씩 늘어난다.

이 구조는 뭔가 잘못됨

요약: 정점을 수정한다는건 결국 에셋모양을 변경한다는것. 그리고 모든 정점을 이동시키는건 비효율적.

오브젝트를 그려놓고 이동량을 추가로 전달해서 버텍스쉐이더를 돌리면 사각형에 대한 정보를 수정하지 않고 각 정점의 위치를 전부 일괄적으로 이동시킬 수 잇게 된다.

이때 오브젝트의 변화량은 상수값으로 전달할 수 있기 때문에 상수버퍼를 사용한다.

2025.11.04

컨스턴트 버퍼

상수버퍼는 GPU가 16byte 단위로 접근하기 때문에 16byte로 정렬해야함.
쉐이더코드에서 상수버퍼는 이런식으로 cbuffer로 작성 가능하다.

결국엔 CPU에서 틱마다 키 입력을 받아서 계속해서 변경되는 시스템메모리의 상수버퍼를 Map Unmap을 통해 GPU메모리로 전달하고, 버텍스쉐이더에서 기본 좌표에 상수버퍼의 상대좌표를 더해 output.vPosition을 결정하게 된다.

이후 레스터라이저를 통해 픽셀 정보로 변환되고 픽셀 쉐이더로 색을 칠하게 된다.

cbuffer TRANSFORM : register(b0)	// 레지스터 번호 상수버퍼는 b로시작
{
	float4 g_Position;
}

VS_OUT VS_Std2D(VS_IN _in)
{
	VS_OUT output = (VS_OUT)0.f;

	output.vPosition = float4(_in.vPos + g_Position.xyz, 1.f);

	return output;
}

0번 레지스터에 g_CB의 상수버퍼 값이 세팅된다.

CONTEXT->VSSetConstantBuffers(0, 1, g_CB.GetAddressOf());

그러니까 버텍스쉐이더가 실행되는 시점까지 어떤 상수 버퍼 하나를 b0레지스터에 바인딩해놔야 버텍스쉐이더가 g_Position으로 사용할 수 있게 된다.
Map copy Unmap으로 GPU메모리에 올려두고 VSSetConstantBuffers 이걸로 레지스터에 바인딩

픽셀쉐이더

정점쉐이더에서 리턴시킨 값이 레스터라이저에서 픽셀데이터화 된다.
삼각형의 꼭짓점은 Color가 각 정점의 Color와 동일하게 들어올 수 있다는 것을 알게 되지만, 나머지 픽셀의 경우 아주 애매할 수 있다.

이때 보간(Clamp)된 값이 들어오게 된다. 각 정점에 가까울수록 영향을 크게 받게된다.
그래서 position 도 점이 연결된 삼각형에 맞춰서 보간된 값이 들어간것이 각 픽셀의 좌표인 것이라고도 볼 수 있다. Color도 마찬가지다. 정점에 가까울수록 해당 정점의 특징을 더 짙게 받게된다.

VS_OUT VS_Std2D(VS_IN _in)
{
	VS_OUT output = (VS_OUT)0.f;

	output.vPosition = float4(_in.vPos + g_Position.xyz, 1.f);
	output.vColor = _in.vColor;

	return output;
}

float4 PS_Std2D(VS_OUT _in) : SV_Target
{
	return float4(0.2f, 0.2f, 0.5f, 1.f);
}

Mesh

어떤 정점들이 모여서 하나의 형태가 될 수 있는 것. 여러 오브젝트에서 공유할 수 있다.
메쉬는 정점정보를 저장하는 버텍스버퍼, 인덱스버퍼 정보를 멤버로갖고, IA에 바인딩하는 함수와 렌더링하는 함수를 만들어야한다.

2025.11.05

추상클래스

순수가상함수로 만들어야 추상클래스로써 객체생성이 불가능해진다. Entity 같은 모든 객체의 최상위 클래스나 Shader 같이 중간단계지만 직접 만들 수 없게 하고싶다면 순수가상함수를 추가해야하는데, 가장 만만한게 Clone 함수이다.
virtual CEntity* Clone() = 0; 이걸 CEntity에서 작성하면 모든 하위 클래스도 추상클래스가 되며, 하위클래스에서 콘크리트클래스(구현클래스?)가 되고싶은 애들은 Clone을 구현하면 된다.

쉐이더들

  • GraphicShader : GPU를 통해 렌더링할때 사용하는 쉐이더
  • ComputeShader : GPU를 통해 대량의 연산을 처리할때 사용하는 쉐이더. 예를들어서 비트코인 채굴 등

2025.11.08

복사대입연산자가 만들어지지 않은 경우

class MyType 
{
public:
    MyType();
    MyType(int value);
    // MyType& operator=(int 10);   같은 대입연산자는 없음
    ~MyType();
}

MyType a;    // 이미 만들어진 객체
int b = 10;

a = b;       // 대입연산자가 만들어져있다면 호출됐겠지만, 그런건 없음

이런 경우 자동으로 컴파일러가 기본복사대입연산자 MyType& operator=(const MyType&) 를 만든다.
이후 만들어둔 생성자 MyType(10)가 호출되고, 기본 복사대입연산자 operator=(MyType(10))가 호출된다.

소멸자에 virtual

부모의 소멸자는 virtual을 붙여줘야 한다.
왜냐하면 업캐스팅후 소멸자를 호출할때 업캐스팅된 클래스의 소멸자가 호출돼서 문제가 발생함.
표준에서 정의되지 않은 동작(UB)이 되고, 잘못된 소멸자가 호출됐기 때문에 최소 메모리 누수가 발생하고 힙구조가 깨져 애매한 시점에 크래시가 발생한다.

사실 모든 소멸자에 virtual을 붙이는게 안전하다. 하지만 단점도 존재한다.
이미 가상함수가 있는 클래스라면 vtable에 소멸자용 엔트리 하나만 추가되지만, 가상함수가 없었다면 다형클래스가 돼서 쓸데없는 vptr이 생성되고 함수호출시 vtable을 거쳐 호출하게돼서 느려진다.

2025.11.09

에셋매니저 (typeinfo, constexpr 사용)

동일한 에셋을 여러곳에서 참조해서 배치하기 때문에 참조카운트 형태로 관리할 것이다. 에셋매니저는 모든 에셋들을 직접 관리하는 클래스이다.

FindAsset에 ASSET_TYPE을 넣어주면 바로 인덱스로 사용할 수 있지만 일단 type_info를 사용해서 할것이다.

class CAssetMgr
	: public CSingleton<CAssetMgr>
{
...
private:
	map <wstring, Ptr<CAsset>>	m_mapAsset[(UINT)ASSET_TYPE::END];

public:
	template<typename T>
	Ptr<T> FindAsset(const wstring& _strKey);
};

// 사실 이건 std::is_same_v 라는 걸로 동일한게 있다. 
template<typename T, typename U>
constexpr bool	mybool = false;
template<typename T>    // 특수화
constexpr bool	mybool<T, T> = true;

template<typename T>
ASSET_TYPE GetAssetType()
{
    // 템플릿으로 이렇게 만들면 각 타입이 맞는지 if else를 최악의 경우 에셋 수만큼 비교한다.
	const type_info& info = typeid(T);
	if (info.hash_code() == typeid(CMesh).hash_code())
		return ASSET_TYPE::MESH;
    else if (info.hash_code() == typeid(CComputeShader).hash_code())
		return ASSET_TYPE::COMPUTE_SHADER;
    // 하지만 T는 어차피 에셋타입만큼 생성되기 때문에 고정적이게 되어 컴파일타임으로 줄일 수 있을 것 같다. 
    // c++17부터 생겨난 constexpr 을 사용하면되지만, type_info는 런타임에 생성되는 RTTI를 이용하는 것이기 때문에 if constexpr을 사용할 수 없음. 
    // 변수템플릿으로 if문의 조건을 상수화하면 조건 자체가 사라지고 바로리턴함. 매크로처럼
	if constexpr (mybool<T, CMesh>)
		return ASSET_TYPE::MESH;
	if constexpr (mybool<T, CComputeShader>)
		return ASSET_TYPE::COMPUTE_SHADER;
}

template<typename T>
inline Ptr<T> CAssetMgr::FindAsset(const wstring& _strKey)
{
	ASSET_TYPE type = GetAssetType<T>();
	map<wstring, Ptr<CAsset>>::iterator iter = m_mapAsset[(UINT)type].find(_strKey);
	if (iter == m_mapAsset[(UINT)type].end())
		return nullptr;
#ifdef _DEBUG
	T* pAsset = dynamic_cast<T*>(iter->second.Get());
	return pAsset;
#else
	return (T*)iter->second.Get();
#endif
}

CMesh* rect = CAssetMgr::GetInst()->FindAsset<CMesh>(L"RectMesh");

const보다 constexpr이 더 엄격하다. const는 런타임에 상수가 되는건데, constexpr은 매크로처럼 컴파일 타임에 상수화가 된다. 그래서 const는 강제로 주소 따와서 직접 변경하면 수정되는데 constexpr은 안된다.

그리고 FindAsset이 Ptr 리턴인 이유는 외부에서 사용자가 T*로 리턴받아버리면 레퍼런스 카운트가 관리되지 않기 때문에 이걸 방지하기 위함 강제로 Ptr로 넣어버림

GameObject

새로운 기능을 추가하는데 베이스클래스를 상속받아가면서 구현하는 방법도 있지만, 인터페이스나 컴포넌트로 기능을 추가하는 방법도 있다.
예를들어 게임 오브젝트에서 UI는 애니메이션이 필요없는줄 알았다가 나중에 추가될때 상속구조로 구현하면 다시 처음부터 설계를 다시해야하는데, 컴포넌트 구조로 만들면 그렇지 않아도된다

언리얼은 상속+컴포넌트, 유니티는 극단적인 컴포넌트구조

게임 컨텐츠같은건 사실 컴포넌트로 구현하기 어렵다. 엔진 개발자가 범용 기능이 아닌 게임 컨텐츠에 따른 기능을 구현하는건 말이 안되기 때문에 컨텐츠 개발자는 스크립트 컴포넌트라는 녀석을 상속받아서 어떻게 구현해서 동작하게 만든다.

enum 값이 모든 컴포넌트가 분리됐지만, 사실 렌더링컴포넌트처럼 한개만 가질 수 있는애들도 있음. 2D 3D 도 마찬가지

컴포넌트끼리 소통해야할때가 있음. 예를들어서 콜라이더 컴포넌트는 위치를 알기 위해 transform 컴포넌트의 위치정보를 알아야한다. 그래서 컴포넌트는 자신을 소유하고있는 오브젝트를 얻을 수 있어야한다.

게임오브젝트는 begin(), tick(), finaltick(), render() 과 같은 엔진상태에 따른 함수를 가지고있으며 모든 컴포넌트에 전파하는 방식이다. 그런데 render는 렌더컴포넌트에만 구현할 필요가 있음.

스크립트 컴포넌트는 하나의 오브젝트라도 여러 스크립트 컴포넌트를 가질 수 있다. 그렇기 때문에 컴포넌트지만 컴포넌트 배열과는 별개로 만들어져야 한다. 대신 컴포넌트를 상속받았기 때문에 addcomponent로 된다.

몬스터스크립트같은건 틱에서 몬스터의 행동양식(AI)등을 구현한것이다. 플레이어 스크립트는 플레이어들이 하는 행동양식 구현.

대부분의 컴포넌트는 finaltick이 필요하기 때문에 컴포넌트 클래스에서 이걸 강제로 구현하도록 순수가상함수로 만들어놨는데, 이게 정리하는 작업이라서 여러 컴포넌트에서 작업하고난 뒤 finaltick을 호출하고 스크립트 컴포넌트에서 또 finaltick을 호출하면 문제가 발생할 수 있음
그래서 스크립트는 finaltick을 구현하지 않기 위해 최상단 Script 클래스에서 finaltick을 구현하고 하위 클래스에선 구현못하도록 막아야함
virtual void finaltick() final; 을 스크립트 클래스에서 작성
-> 사실 이게 무슨말인지 잘 이해못했다. override를 막는걸 가르쳐주려고 한건가? 왜 finaltick을 스크립트에서 실행하면 안되는거지? 나중에 fianltick을 구현하게되면 되면 알지도?

-> 이제 알겠다. transform 컴포넌트에서는 Binding을 호출해서 GPU에 ConstantBuffer를 통해 자신의 상수를 전달한다. 이렇게 컴포넌트에 영향을 주는 여러 작업들을 마치고나서 그 모든걸 자신의 컴포넌트 상태에 적용하는? 그런 역할을 하는게 finaltick인데, 스크립트에는 사실 이게 필요없다. 스크립트는 게임오브젝트의 상태를 관리하는게 아니라 행위를 지정하는것이기 때문에.. tick만 있으면 되고, 스크립트는 final에서 뭔가 작업할것도 사실 없다.

사실 오브젝트의 상수버퍼 바인딩은 렌더컴포넌트에서 호출하긴 해야된다. 왜냐하면 오브젝트가 여러개 있을때 finaltick에서 하면 렌더링 하기 전에 상수버퍼가 덮어쓰여져서 렌더링 타이밍에 나머지 오브젝트들의 Transform 값들을 얻을 수 없게된다.
렌더링할때 하면 오브젝트1..n 의 Transform -> Render 이렇게 반복해서 호출하기 때문에 자신의 좌표를 업데이트 하고 렌더링이 가능하다.

이전에는 1::tick -> 2::tick -> 1::finaltick -> 1::Binding -> 2::finaltick -> 2::Binding -> 1::render -> 2::render 순서였고, 지금은 1::tick -> 2::tick -> 1::finaltick -> 2::finaltick -> 1::Binding -> 1::render -> 2::Binding->2::render 순서이다.

Component

  • Transform : 위치, 크기, 회전량 정보
  • Camera
  • Animator
  • Light

Level

하나의 레벨에는 32개의 레이어가 있고, 하나의 레이어에는 게임오브젝트들이 여러개 있을 수 있음

레이어는 물체들의 분류 작업. (몬스터레이어, 플레이어 레이어 등)

모든 오브젝트를 검색하는게 아니라 특정 작업에 맞게 레이어를 관리하면 검색할 양이 줄어든다.

Comments

ESC
Type to search...