반응형
  1. 노멀과 방향광 (Blinn-Phong)
  2. HDR 렌더 타깃, 톤매핑, 감마 보정
  3. PBR (Cook-Torrance)
  4. 텍스처 로딩과 밉맵
  5. 머티리얼, 탄젠트, 노멀 맵

1. 노멀과 방향광

정점과 상수 버퍼

조명을 계산하려면 표면이 어느 쪽을 향하는지 알아야 합니다. 정점에 노멀을 추가했고, 큐브는 면마다 노멀이 달라야 해서 정점이 8개에서 24개로 늘었습니다.

보내 데이터가 늘면서 루트 상수 16개로는 부족해졌습니다. 상수 버퍼를 두 개로 나눴습니다.

버퍼 갱신 주기 내용
PerFrame(b0) 프레임당 1회 ViewProj, 카메라 위치, 라이트 방향/색, amblient
PerObject(b1) 드로우당 1회 World, WorldInvTranspose, 베이스 컬러, 머티리얼 값

 

UploadRing: 프레임별 상수 버퍼 할당기

CPU가 다음 프레임의 상수를 쓰는 동안 GPU는 아직 이전 프레임을 그리고 있을 수 있습니다. 같은 버퍼를 쓰면 GPU가 읽는 도중에 값이 바뀝니다. 그래서 프레임 인덱스별로 업로드 버퍼를 따로 두고, 256바이트 단위로 잘라 순서대로 나눠 씁니다. 자기 차례가 돌아오면(GPU가 이미 다 쓴 뒤) 처음부터 다시 씁니다.

 

WorldInvTranspose

노멀을 월드 행렬로 그대로 변환하면 비균등 스케일에서 방향이 틀어집니다. 노멀에는 월드 행렬의 역전치 행렬을 써야 표면에 수직인 상태가 유지됩니다.


2. HDR과 톤매핑

문제

조명 계산 결과는 1.0을 쉽게 넘습니다. 8비트 렌더 타켓에 바로 그리면 쓰는 순간 1.0에서 잘려서, 밝은 면이 색 구분 없이 하얗게 뭉개집니다.

구조

씬 렌더링 → [sceneRT: R16G16B16A16_FLOAT] → 톤매핑 패스 → [viewportRT: R8G8B8A8_UNORM] → ImGui
  sceneRT viewportRT
담는 값 실제 빛의 양 (1.0초과 가능) 모니터로 내보낼 최종 색
깊이 버퍼 있음 없음

같은 텍스처를 읽으면서 동시에 쓸 수느 없어서 입력과 출력이 분리돼야 합니다.

블룸이나 안티앨리어싱 같은 포스트프로세스도 이 사이에 단계가 하나씩 끼어드는 구조로 늘어납니다.

 

톤 매핑 패스

  • 풀 스크린 삼각형: 버텍스 버퍼 없이 SV_VertexID(0,1,2)만으로 화면보다 큰 삼각형 하나를 만들어 덮습니다.
  • ACES: 밝은 값을 잘라 내지 않고 곡선으로 눌러서 계조를 남깁니다.
  • 감마 보정: 모니터는 입력을 약2.2제곱해서 표시합니다. 계산은 선형 공간에서 하고, 마지막에 1/2.2제곱을 해서 내보냅니다. 반대로 색상 피커에서 고른 색은 셰이더에서 선형으로 바꾼 뒤 계산합니다.

3. PBR (Cook-Torrance)

표면을 아주 작은 거울 조각(미세면)이 모인 것으로 보는 모델입니다.

항 묻는것 화면에서 보이는 효과
D (GGX) 미세면 중 몇 개가 반사 방향을 향하는가 거칠기가 낮으면 작고 강한 하이라이트, 높으면 넓고 흐린 하이라이트
G (Smith) 미세면끼리 서로 가리는 정도 비스듬한 각도에서 과하게 밝아지는 걸 억제
F (Schlick) 보는 각도에 따른 반사율 변화 가장자리로 갈수록 반사가 강해짐
float3 F0       = lerp(0.04, albedo, metallic);              // 비금속 4%, 금속은 자기 색
float3 specular = (D * G * F) / max(4 * NdotV * NdotL, 1e-4);
float3 diffuse  = (1 - F) * (1 - metallic) * albedo / PI;    // 금속은 난반사 없음
  • 금속성은 두 가지를 바꿉니다. 금속은 난반사가 없고, 반사광에 자기 색이 묻어납니다.
  • 난반사는 π로 나누는 것은 에너지 보존을 위한 것입니다. Blinn-Phong 때보다 약 3배 어두워져서 기본 라이트 세기를 3.0으로 올렸습니다.
  • 현재 한계: 금속은 주변 환경을 반사해서 보이는 재질인데, 지금은 방향과 하나뿐이라 하이라이트 주변 말고는 어둡습니다. 환경 조명(IBL)이 들어가야 제대로 보입니다.

4. 텍스처와 밉맵

업로드 경로

CPU는 UPLOAD힙에만 쓸 수 있고, GPU는 DEFAULT힙에서 읽는게 빠릅니다. 그래서 두 단계를 거칩니다.

픽셀 데이터 → 업로드 버퍼(UPLOAD 힙) → CopyTextureRegion → 텍스처(DEFAULT 힙) → SRV
  • Renderer::Immediate: 초기화 시점에는 열린 커맨드 리스트가 없어서, 람다를 받아 즉시 실행하고 완료까지 기다리는 함수를 만들었습니다.
  • RowPitch: GPU가 요구하는 행 간격이 width x 4 보다 클 수 있습니다.
  • sRGB포맷: 색상 텍스처는 _SRGB포맷으로 만들면 샘플링할 때 GPU가 선형으로 바꿔 줍니다. 노멀이나 러프니스 같은 데이터 텍스처는 일반 포맷을 씁니다.
  • 기본 텍스처: 텍스처가 없는 슬롯에는 1x1흰색을 바인딩해서 셰이더를 한가지 경로로 유지합니다.

 

이미지 포맷

  일반 이미지(PNG, JPG, TGA) GPU전용(DDS, KTX2)
로딩 CPU에서 압축을 풀어 RGBA로 변환 거의 그대로 업로드
VRAM 큼 BC압축으로 4분의 1에서 8분의 1
밉맵 없음, 직접 생성 파일에 포함
용도 제작과 교환 런타임

지금은 stb_image로 일반 이미지를 읽습니다. 나중에 에셋 파이프라인에서 BC압축 DDS로 변환해 캐시하는 구조로 갈 계획입니다.

 

밉맵

멀리 있는 표면은 화면의 한 픽셀이 텍스처의 여러 칸을 덮습니다. 그중 한 점만 읽으면 자글자글하게 깨지고, 움직일 때 깜빡입니다. 절반씩 줄인 사본을 미리 만들어 두고 거리에 맞는 사본을 읽게 합니다.

  • DX12에는 밉맵 자동 생성 함수가 없어서 CPU에서 2x2평균으로 만들었습니다.
  • 밉 레벨 하나하나가 서브리소스이고, 복사 명령은 서브리소스 단위로 내립니다.
  • sRGB는 선형으로 바꾼 뒤 평균해야 합니다. 검정과 흰색을 바이트 그대로 평균하면 128이 되는데, 실제 밝기로는 약 22%입니다. 올바른 값은 약 188입니다.
  • 메모리는 원본의 약 1.33배가 됩니다.

5. 머티리얼과 노멀 맵

Material 구조

엔티티가 낱개로 들고 있던 값을 묶었습니다. Scene이 머티리얼을 소유하고, 엔티티는 포인터로 참조합니다.

여러 엔티티가 같은 머티리얼을 공유할 수 있습니다.

슬롯 내용 기본값
t0 Albedo 색 (sRGB) 흰색
t1 Normal 표면 요철 방향(선형) (128,128,255)
t2 Metallic-Roughness G = 거칠기, B = 금속성 (glTF 규약) 흰색

텍스처마다 SRV가 힙의 다른 위치에 있어서, 디스크립터 테이블을 슬롯별로 하나씩 따로 뒀습니다.

 

탄젠트 공간

노멀 맵의 RGB는 방향 벡터인데, 월드 기준이 아니라 표면에 붙은 좌표계 기준입니다.

  • T: 텍스처의 U가 증가하는 방향(정점 속성으로 저장)
  • B: V가 증가하는 방향. cross(N, T) x w로 계산
  • N: 표면 노멀
float3 nTS = gNormalMap.Sample(gSampler, uv).xyz * 2 - 1;
N = normalize(nTS.x * T + nTS.y * B + nTS.z * N);

이 좌표계를 쓰기 때문에 같은 노멀 맵을 큐브의 어느 면에든, 구에든 그대로 쓸 수 있습니다.

실제 지오메트리는 바뀌지 않고 조명 계산에 쓰는 노멀만 바뀌므로, 실루엣은 여전히 매끈합니다.


결과

반응형

'DirectX12 > 엔진 만들어보기' 카테고리의 다른 글

그리드, Scene/Entity, Hierarchy·Inspector, 기즈모  (0) 2026.10.05
서브모듈 정리, 카메라  (0) 2026.10.03
Viewport 만들기  (0) 2026.10.02
엔진 구상  (0) 2026.10.01
반응형

바닥 그리드로 공간 기준을 잡습니다.

하드코딩된 큐브를 Scene/Entity 구조로 바꿉니다.

Hierarchy/Inspector 패널에서 선택하고 값을 편집합니다.

InGuizmo로 뷰포트 안에서 직접 이동/회전/스케일을 조정합니다.


그리드 바닥

DX12의 PSO에는 PrimitiveTopologyType이 들어갑니다. TRIANGLE로 만든 PSO로는 선을 그릴 수 없습니다. 그래서 같은 셰이더와 루트 시그니처를 쓰더라도 PSO는 하나 더 만들어야 합니다.

auto linePso = pso;   // 삼각형 PSO 설정을 복사
linePso.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_LINE;
device->CreateGraphicsPipelineState(&linePso, IID_PPV_ARGS(&mLinePso));

드로우할 때는 IASetPrimitiveTopology(LINELIST)로 맞춥니다. PSO의 타입과 커맨드 리스트의 토폴로지(LIST/STRIP 등)는 별개 설정이고, 둘이 서로 호환돼야 합니다.

 


Scene / Entity 구조

라이트, 카메라, 스크립트처럼 컴포넌트가 늘어나는 시점에 옮길 계획입니다.

struct Entity
{
    std::string name;
    Transform   transform;
    const Mesh* mesh = nullptr;   // 소유하지 않음
    bool        visible = true;
};

모듈구성

모듈 역할
Mesh VB/IB와 뷰를 소유하고 Draw(cmd)를 제공. CreateBue()는 1x1x1 단위 큐브
Transform position, rotation(오일러, 도 단위), scale을 들고 Matrix()를 계산
Scene std::vector<Entity>와 Create(name)
SceneRenderer 그리드를 그린 뒤, 씬의 엔티티를 순회하며 mesh->Draw 호출

핵심은 메시와 인스턴스의 분리입니다. 큐브 버퍼는 하나인데, 엔티티 세 개가 각자 다른 Transform으로 그 메시를 그립니다.


Transform 행렬 순서

S * Rx * Ry * Rz * T
  • S->R->T 순서여야 물체가 제자리에서 돌고 커집니다(DX12에서는 SRT순으로 곱해짐). 순서가 바뀌면 원점을 중심으로 공전합니다.
  • Transform은 항상 메시 원점(피벗) 기준으로 적용됩니다. 큐브는 중심 피벗이라 scale을 키우면 위 아래로 같이 커집니다. 바닥에 붙이려면 position.y = 0.5 x scale.y로 맞춰야 합니다.
  • 피벗 위치는 Transform이 아니라 메시 정점을 어떻게 만들었느냐로 결정됩니다.

주의: vector 재할당과 참조

Scene::Create가 Entity&를 반환하는데, 다음 Create에서 vector가 재할당되면 이전 참조는 무효가 됩니다. 그래서 선택 상태는 포인터가 아니라 인덱스로 저장했습니다. 엔티티 삭제 기능이 들어가면 안정적인 ID방식으로 바꿔야 합니다.


Hierarchy / Inspector 패널

Hierarchy: 엔티티 목록을 ImGui::Selectable로 표시합니다. PushID(i)를 써서 이름이 같은 엔티티가 있어도 ID가 충돌하지 않게 했습니다. 빈 공간을 클릭하면 선택이 해제 됩니다.

Inspector: 선택한 엔티티의 이름(InputText), 표시 여부(Checkbox), Transform(DragFloat3 세 개)을 편집합니다.

 

DragFloat3에는 &transform.position.x를 넘기는데, 이유는 float* 구조체이기에 다음 참조로 넘어가면 x->y->z 순으로 넘어가기 때문에 x 원소만 넘겨주면 된다.


도킹 레이아웃

DockBuilder로 4분할 기본 배치를 만들었습니다. Hierarchy는 왼쪽, Viewport는 가운데, Inspector는 오른쪽 위, Stats는 오른쪽 아래입니다.

 


ImGuizmo

연결방식

ImGuizmo::SetDrawlist();
ImGuizmo::SetRect(min.x, min.y, size.x, size.y);   // Viewport 이미지 영역

if (ImGuizmo::Manipulate(view, proj, op, mode, world))
    ImGuizmo::DecomposeMatrixToComponents(world, &t.position.x, &t.rotation.x, &t.scale.x);
  • DirectXMath의 XMFLOAT4X4 메모리 배치는 ImGuizmo가 기대하는 float[16]과 같습니다. 그래서 변환 없이 포인터만 넘깁니다.
  • Mainpulate는 조작이 있었던 프레임에만 true를 반환합니다. 수정된 월드 행렬을 위치/회전/스케일로 분해해서 Transform으로 되돌립니다.
  • 회전 순서 일치: ImGuizmo는 X->Y->Z 순서로 오일러 각을 분해합니다. Transform::Matrix도 같은 순서로 맞춰야 기즈모를 놓을 때 물체가 튀지 않습니다.
  • 스케일은 로컬 축에서만 의미가 있어서 LOCAL 모드를 쓰고, 이동 회전은 WORLD 모드를 씁니다.

단축키

W(이동), E(회전), R(회전)로 전환합니다.

우클릭 중(카메라 플라이)이거나 텍스트 입력 중에는 무시해서, 카메라의 WASD나 이름 입력과 충돌하지 않습니다.


에디터 설정 파일 위치

imgui.ini는 기본적으로 작업 디렉터리에 저장됩니다. launch.vs.json의 currentDir이 Assets로 잡혀 있어서 콘텐츠 폴더에 설정 파일이 섞였습니다. 언리얼의 Saved/처럼 분리했습니다.

std::filesystem::create_directories(BAEK_ROOT_DIR "Saved");   // ImGui는 폴더를 만들어주지 않음
io.IniFilename = BAEK_ROOT_DIR "Saved/imgui.ini";

결과

 

반응형

'DirectX12 > 엔진 만들어보기' 카테고리의 다른 글

라이팅에서 PBR 머티리얼까지  (0) 2026.10.06
서브모듈 정리, 카메라  (0) 2026.10.03
Viewport 만들기  (0) 2026.10.02
엔진 구상  (0) 2026.10.01
반응형

서브모듈 정리

 

d3dx12.h는 2023년경부터 한 파일 구조에서 여러 파일로 분할됐습니다(d3dx12_core.h, d3dx12_barriers.h, d3dx12_root_signature.h 등). 파일을 손으로 복사해 쓰면 버전이 섞이거나 일부가 빠지기 쉽습니다. 게다가 신버전은 최신 d3d12.h를 전제로 작성돼 있어서, Windows SDK 버전과도 어긋날 수 있습니다.

해당 문제를 해결할 방법은 총 두가지입니다.

방식 내용 한계
DirectX-Headers 서브모듈 d3d12.h와 d3dx12를 같은 버전으로 통일 헤더만 최신, 런타임은 OS것(옛날 버전과 호환이 안될 수 있음)
Agility SDK 헤더와 D3D12Core.dll을 앱에 포함 DLL 배포와 export 심볼 선언이 필요

 

현재 상황에서는 기본 런타임으로 충분하기 때문에 서브모듈로 적용하여 다음과 같이 적용하여 수정했다.

 

bash.

git submodule add https://github.com/microsoft/DirectX-Headers.git ThirdParty/DirectX-Headers

 

CMakeLists.

# ThirdParty/CMakeLists.txt
add_subdirectory(DirectX-Headers)

# Engine/CMakeLists.txt
target_link_libraries(Engine PUBLIC DirectX-Headers DirectX-Guids d3d12 dxgi dxguid d3dcompiler imgui)

 

DxUtils.h 로 통일 후 기존 다른 파일의  <d3d12.h>, "d3dx12.h" 삭제.

// DxUtil.h
#include <directx/d3d12.h>     // 서브모듈 것, 반드시 맨 먼저
#include <directx/d3dx12.h>
#include <dxgi1_6.h>           // DXGI는 Windows SDK 것

 

이후 다른 환경에서 .git을 가져올때 재귀적으로 pull.

git pull
git submodule update --init --recursive

카메라

조작방식

입력 동작
우클릭 + 마우스 이동 시점 회전
우클릭 + WASD 플라이 이동 (Shift를 누르면 3배 가속)
Alt + 좌클릭 드래그 피벗 중심 오비트
휠 줌 (전진 / 후퇴)

 


Camera 클래스 (Engine/Scene/Camera)

카메라 상태는 위치와 yaw/pitch 두 각도로만 저장합니다. 행렬은 필요할 때마다 계산합니다.

멤버 의미
mPos 월드 위치.
mYaw 좌우 각도. 0이면 +Z를 봄
mPicth 상하 각도. 음수면 아래를 봄. ±89로 제한
mOrbitDist 오비트 피벗까지의 거리
mFovY, mAspect, mNear, mFar 투영 파라미터

 

전방 벡터는 구면좌표로 구합니다(왼손 좌표계 기준).

forward = (cos(pitch) * sin(yaw), sin(pitch), cos(pitch) * cos(yaw))
  • View: XMMatrixLookToLH(pos, forward, up). 목표 지점이 아니라 방향을 넘김
  • Right: normalize(cross(worldUp, forward)). 왼손 좌표계에서는 upxforward가 right입니다.
  • Pitch 제한: ±90을 넘으면 forward가 up과 평행해져서 화면이 뒤짚히므로 89로 제한합니다.

오비트는 피벗을 고정한 채 카메라를 돌린다는 뜻입니다. 총 세 단계로 구현됐습니다.

pivot = pos + forward * dist;   // 1. 현재 보고 있는 지점을 피벗으로
Rotate(dYaw, dPitch);           // 2. 각도 변경
pos = pivot - forward * dist;   // 3. 새 방향 기준으로 위치 재계산

줌을 전방으로 이동하면 mOrbitDist도 같이 줄어듭니다. 피벗을 뚫고 지나가지 않도록 최소 거리(0.2)를 둡니다.

 


입력 처리: Viewport 위에서만

에디터 카메라에서 가장 중요한 규칙은 다른 패널을 조작할때 카메라가 움직이면 안 된다는 것입니다.

// 시작은 Viewport 위에서만, 유지는 버튼을 누르고 있는 동안
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) flying = true;
if (!ImGui::IsMouseDown(ImGuiMouseButton_Right))              flying = false;
  • Camera 인스턴스는 main에 생성합니다. 그 후 ImGui 바인딩을 호출하며 Camera를 업데이트 합니다.
  • 시작 조건은 ImGui::IsWindowHovered() 로 Viewport 패널 위에서 클릭했는지 봅니다.
  • 유지 조건은 버튼을 누르고 있느지만 봅니다. 드래그하다 커서가 패널 밖으로 나가면 조작이 끊기지 않습니다.
  • 이동 속도에는 io.DeltaTime을 곱해서 프레임률과 무관하게 만듭니다.
  • 회전은 io.MouseDelta(프레임 간 이동량)에 감도(0.004rad/pixel)를 곱합니다.
  • io.ConfigWindowsMoveFromTitleBarOnly = true을 통해 Viewport를 플로팅 창으로 쓸때 Alt+좌클릭 드래그가 창 이동 되는것을 막습니다.

SceneRenderer 변경

Render(cmd, aspect, time)을 Render(cmd, viewProj)로 바꿨습니다.

카메라 행렬을 밖에서 받습니다. 렌더러가 카메라를 소유하지 않고 입력으로 받기 때문에, 나중에 뷰포트가 여러 개가 되어도 같은 렌더러를 재사용할 수 있습니다.

반응형

'DirectX12 > 엔진 만들어보기' 카테고리의 다른 글

라이팅에서 PBR 머티리얼까지  (0) 2026.10.06
그리드, Scene/Entity, Hierarchy·Inspector, 기즈모  (0) 2026.10.05
Viewport 만들기  (0) 2026.10.02
엔진 구상  (0) 2026.10.01
반응형

이번 단계에서는 씬을 별도 텍스처(오프스크린 RT)에 렌더링하고, 그 텍스처를 ImGui의 Viewport 패널 안에 이미지로 표시합니다.


프레임 흐름

[매 프레임]
 ├─ (패널 크기 변경 요청 시) WaitIdle → RT Resize
 ├─ ImGui NewFrame + UI 구성     ← Viewport 패널 크기를 읽고 ImGui::Image(SRV) 기록
 ├─ Renderer::BeginFrame         ← 백버퍼 PRESENT → RT, Clear
 ├─ ViewportRT.Begin             ← 텍스처 PSR → RT, Clear(color/depth), Viewport/Scissor
 │    └─ SceneRenderer::Render   ← 큐브 Draw
 ├─ ViewportRT.End               ← 텍스처 RT → PSR (셰이더에서 읽을 수 있게)
 ├─ Renderer::BindBackBuffer     ← 다시 백버퍼를 타깃으로
 ├─ ImGui Render                 ← Viewport 패널이 RT 텍스처를 샘플링
 └─ Renderer::EndFrame           ← RT → PRESENT, Execute, Present

 

핵심은 같은 텍스처가 한 프레임 안에서 두 가지 역할을 한다는 점입니다.
씬 패스에서는 RenderTarget으로 쓰이고, UI패스에서는 ShaderResource로 읽힙니다.

그래서 두 패스 사이에 리소스 배리어(RT<->PSR)가 반드시 필요합니다.


RenderTarget (Engine/RHI/RenderTarget)

항목 내용
소유 리소스 컬러 텍스처(R8G8B8A8_UNORM), 깊이 텍스처(D32_FLOAT)
디스크립터 RTV(RTV힙), DSV(DSV힙), SRV(shader-visible CBV_SRV_UAV 힙)
기본 상태 컬러는 PIXEL_SHADER_RESOURCE, 깊이는 DEPTH_WRITE
Init 디스크립터를 한 번만 Allocate해 두고 재사용
Resize(w, h) 텍스처 재생성 후 같은 슬롯에 RTV/DSV/SRV를 다시 기록
Begin / End 배리어, 바인딩, Clear,Viewport / Scissor 설정

Optimized Clear Value: 리소스 생성 시 D3D12_CLEAR_VALUE에 넣은 색과 실제 Clear 색을 맞춰야 경고가 나지 않고 Fast Clear 최적화가 적용됩니다.

리사이즈 시 동기화: GPU가 아직 사용 중인 텍스처를 해제하면 안 되므로, 현재는 WaitIdle 후 재 생성합니다.


Renderer 확장

RTV힙(64개)과 DSV힙(16개)을 추가했습니다. 둘 다 non-shader-visible입니다.

BindBackBuffer()는 씬 패스가 끝난 뒤 다시 백버퍼를 렌더 타킷으로 지정합니다.


SceneRenderer (Engine/Renderer/SceneRenderer)

루트 시그니처: Root Constants 16개 (b0=MVP행렬)만 사용합니다(가장 단순한 형태).

PSO: 기본 Rasterizer/Blend/Depth, RTV는 R8G8B8A8, DSV는 D32입니다.

지오메트리: 큐브 8개 정점에 정점 색을 넣고, 인덱스 36개(R16_UINT)를 씁니다.

MVP: Wolrd(회전) * LookAtLH * PerspectiveFovLH를 Transpose해서 전달합니다.aspect는 RT 크기 기준으로 계산합니다.


Shader 컴파일 헬퍼 (Engine/RHI/Shader)

D3DCompileFromFile을 래핑했습니다. Debug 빌드에서는 DEBUG | SKIP_OPTIMIZATION 을 켭니다.

에러 메시지를 OutputDebugStringA로 출력 창에 남기고, 예외로도 던집니다.

셰이더 경로는 CMake에서 BAEK_SHADER_DIR="${CMAKE_SOURCE_DIR}/Shaders/" 로 주입합니다. 이렇게 하면 실행 위치(working dir)와 무관하게 셰이더를 찾을 수 있습니다.


에디터 UI

Viewport 패널: 패딩을 0으로 두고, GetContentRegionAvail()로 얻은 크기를 다음 프레임 RT 크기로 요청합니다. 표시는 ImGui::Image(SRV GPU handle)로 합니다.

DockBuilder 기본 레이아웃: imgui.ini가 없을 때만 Viewport를 가운데, Status를 오른쪽 22%에 배치합니다.이후 사용자가 바꾼 배치는 ini에 저장돼 유지됩니다.

 

 


트러블 슈팅

 

Basic.hlsl(1,1): error X3000: Illegal character -> .editorconfig가 hlsl을 UTF-8 BOM으로 저장. FXC는 BOM을 처리하지 못함

해결(.editorconfig으로 hlsl만 utf-8로 저장)

root = true

[*.{h,hpp,cpp,txt,json}]
charset = utf-8-bom

[*.hlsl]
charset = utf-8

 

결과

반응형
반응형

엔진에 대한 이해도를 더 높이기 위해 AI의 힘을 빌려 실제 DirectX12를 구상하는 엔진을 만들어 보고자 한다.

CMake를 이용하여 sln의 의존성을 지우고 아래와 같이 여러 모듈들을 root로 묶어 실행하는 역할을 한다.

TP의 경우 ImGui라는 유명한 Ui 모듈을 이용하여 dx12와 win32를 연결한다.

https://github.com/ocornut/imgui

 

GitHub - ocornut/imgui: Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies

Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies - ocornut/imgui

github.com

 

BaekEngine/
├─ CMakeLists.txt           루트: C++20, /utf-8 /W4 /MP, UNICODE·NOMINMAX·WIN32_LEAN_AND_MEAN
├─ .editorconfig            모든 소스 UTF-8(BOM)
├─ ThirdParty/
│  ├─ CMakeLists.txt        imgui 정적 라이브러리 타깃
│  ├─ d3dx12.h
│  └─ imgui/                (서브모듈, docking 브랜치)
├─ Engine/  (정적 lib)       → d3d12, dxgi, dxguid, d3dcompiler, imgui 링크 (PUBLIC)
│  ├─ Core/     Window
│  ├─ RHI/      DxUtil, Device, SwapChain, DescriptorHeap
│  ├─ Renderer/ Renderer
│  └─ UI/       ImGuiLayer
├─ Editor/  (exe, WIN32)     → Engine 링크, main.cpp
└─ Assets/                   실행 작업 디렉터리 (launch.vs.json currentDir)

 

위와 같이 BaekEngine이라는 최상위 디렉터리와 루트 CMakeLists를 통하여 아래 서브 디렉터리(모듈)들을 묶어준다.

의존성은 Editor -> UI -> Renderer ->  RHI ->DxUtil 순으로 호출되어 시각화를 진행하며 Cor(Window)는 어디에도 의존하지 않는다. Window는 오로지 메시지만 전달하기 때문에, ImGui와도 연결되지 않는다.


Core / Window

Win32 창 생성, 메시지 처리, 리사이즈·최소화 상태를 관리합니다.

멤버 의미
mHwnd 창 핸들, WM_NCCREATE에서 바로 설정(CreateWindowEx 반환 전에도 유효)
mWidth, mHeight 클라이언트 크기, WM_SIZE의 LOWORD / HIWORD
mReSized 크기 변경 발생 플래그, 렌더라가 소비
mMinimized SIZE_MINIMMIZED 여부
mHook std::function<bool(HWND, UINT, WPARAM, LPARAM)>, true면 메시지 처리 완료

 

함수 동작
Create(title, w, h) 클랙스 등록 -> AdjustWindowRect -> CreateWindowExW(..., this)
PumpMessages() PeekMessage 루프, WM_QUIT이면 false
ConsumResize() mResized를 읽고 false로 초기화(non-const)
IsMinimized() mMinimized 반환
SetMessageHook() 외부(ImGui) 입력 처리기 연결

RHI / DxUtil

Comptr 별칭을 둡니다.

ThrowIfFailed(x) 매크로는 실패 시 식, 파일, 줄, HRESULT를 담아 std::runtime_error를 던집니다.main의 catch문에서 MessageBox로 표시합니다.


RHI / Device

D3D12 디바이스, 커맨드 큐, 펜스, DXGI 팩토리를 관리합니다.

멤버 의미
mFactory IDXGIFactory6 (디버그 시 DXGI_CREATE_FACTORY_DEBUG)
mDevice ID3D12Device, Feature Level11_0
mQueue DIRECT 커맨드 큐1개
mFence, mFenceValue 단조 증가 펜스
mFenceEvent 대기용 이벤트 (한 번 만들어서 재사용)
함수 동작
Init(debug) 디버그 레이어 -> 팩토리 -> 고성능 GPU 우선 어댑터 선택(소프트웨어 제외, 실패 시 WARP) -> InfoQueue 설정 -> 큐-펜스 생성
Signal() ++mFenceValue를 큐에 시그널하고 그 값을 반환
WaitForValue(v) 완료 값이 v미만이면 CPU대기
Flush() WaitForValue(Signal()), GPU 완전 유휴까지 대기
Shutdown() Flush 후 이벤트 핸들 해제

 

디버그 필드에서 SetBreakOnSeverity(ERROR/CORRUPTION)을 켭니다. 잘못된 API 호출이 그 주렝서 바로 디버거를 멈추게 합니다.


RHI / SwapChain

값 설정
BufferCount 2
Format DXGI_FORMAT_R8G8B8A8_UNORM
SwapEffect FLIP_DISCARD
Scaling STRETCH
Alt+Enter 비활성화(DXGI_MWA_NO_ALT_ENTER)
RTV힙 자체 보유, 버퍼 수만큼
함수 동작
Init() CreateSwapChainForHwnd -> IDXGISwapChain3로 변환 -> RTV 생성
Resize() 버퍼 참조 해제 -> ResizeBuffers -> RTV 재생성(호출 전 Flush 필수)
Present(vsync) Present(vsync ? 1 : 0, 0)
CuffentBuffer() / CurrentRtv() GetCurrentBackBuferIndex() 기준

 

백 버퍼 인덱스를 직접 증가시키지 않고 스왑체인에 물어봅니다. FLIP 모델에서 인덱스가 어긋날 일이 없습니다.


RHI / DescriptorHeap

멤버 의미
mHeap 디스크립터 힙
mCpuStart, mGpuStart 시작 핸들(GPU는 shader_visible일 때만)
mIncrement, mCapacity 슬롯 크기, 총 개수
mShaderVisible GPU 핸들 계산 여부
mFreeList 빈 슬롯 인덱스 스택 (0번부터 나오도록 역순 초기화)

 

DescriptorHandle { cpu, gpu, index }, IsValid()는 index가 UINT32_MAX가 아닌지 봅니다.

함수 동작
Allocate() free list에서 pop, 비었으면 예외
Free(handle) / Free(cpu) 인덱스를 free list에 반환(cpu 버전은 ImGui 콜백용)
At(index) 인덱스를 CPU/GPU 핸들로 변환

 

Renderer가 CBV/SRV/UAV, shader-visible, 1024칸 하나를 엔진 공용으로 소유합니다. ImGui 폰트가 첫 사용자입니다.

 


 

Renderer

프레임 링, 커맨드 리스트, 공용 SRV힙을 관리하고 한 프레임의 사직과 끝을 담당합니다.

멤버 의미
FrameCount 2 (CPU가 GPU보다 앞서갈 수 있는 프레임 수)
mDevice, mSwapChain, mSrvHeap 소유
mAllocator[2] 프레임별 커맨드 할당자
mFrameFence[2] 해당 슬롯이 마지막으로 제출한 펜스 값
mFrameIndex 현재 슬롯
mCmd 그래픽 커맨드 리스트 1개(재사용)
함수 동작
Init(window) 디바이스(디버그는 _DEBUG일 때) -> 스왑체인 -> SRV힙 -> 할당자-커맨드 리스트
BeginFrame(clear) 1. 리사이즈 플래그 소비 시 Flush + 스왑체인 리사이즈
2. 슬롯 펜스 대기
3. 할당자-리스트 Reset
4. PRESENT -> RT
5. 클리어
6. RT 바인딩
EndFrame() 1. RT -> PRESENT
2. Close-Execute
3. Present(vsync)
4. Signal 값을 슬롯에 저장
5. 슬롯 전진
WaitIdle() Flush()
GetDevice(), SrvHeap(),
CommandList()
외부(ImGui등) 접근용

 

로직을 할당자와 펜스 값 배열로 단순화했습니다. BeginFrame과 EndFrame 사이에 에디터나 씬 드로우 끼워 넣는 구조입니다.

 


 

UI / ImGuiLayer

함수 동작
Init(window, renderer) 컨택스트 생성, NavEnableKeyboard + DockingEnable, 다크 스타일 -> Win32 백엔드 + 메시지 훅 연결 -> DX12 백엔드 초기화
BeginFrame() DX12-Win32 NewFrame -> ImGui::NewFrame
EndFrame() InGui::Render -> SRV 힙 바인딩 -> RenderDrawData
Shutdown() DX12 -> Win32 -> 컨택스트 순서로 해제

 

DX12 백엔드에 넘기는 값.

필드 값
NumFramesInFlight Renderer::FrameCount(2)
RTVFormat / DSVFormat 스왑체인 포맷 / UNKNOWN
SrvDescriptorHeap + UserData 엔진 공용 힙
SrvDescriptorAllocFn / FreeFn 엔진 DescriptorHeap의 Allocate / Free로 연결

 

ImGui 1.92+ 는 텍스처 GPU 핸들을 텍스처ID로 사용합니다.
그래서 할당자가 GPU 핸들을 올바르게 채워야 합니다.

 


Editor / main.cpp

Window.Create
→ Renderer.Init
→ ImGuiLayer.Init
→ 루프: PumpMessages → (최소화면 Sleep)
        → BeginFrame(clear) → imgui.BeginFrame → DrawEditorUI → imgui.EndFrame(cmd) → EndFrame
→ WaitIdle → ImGui.Shutdown → Renderer.Shutdown
  • DrawEditorUI: DockSpaceOverViewport(PassthruCentralNode), Stats 패널(FPS, 프레임 ms, 데모 토글)
  • 클리어 색: (0.10, 0.10, 0.15)
  • 예외는 MB_TOPMOST MessageBox로 표시

종료 순서가 중요한 이유: GPU가 ImGui 리소스를 다 쓴 뒤에 해제해야 디버그 레이어 에러가 안납니다.


 

한 프레임의 흐름

[CPU] WaitForValue(slot) → Reset → PRESENT→RT → Clear → OMSetRT
      → ImGui 드로우 기록 → RT→PRESENT → Close → Execute → Present → Signal(slot)
[GPU]                                           └── 실행 ──┘

 


결과

반응형
반응형
#include <string>
#include <vector>
#include <algorithm>

using namespace std;

vector<string> answer;
vector<bool> used;

bool dfs(vector<vector<string>>& tickets, vector<string>& path, string current, int ticketCount)
{
    if(path.size() == ticketCount + 1)      // 모든 티켓을 사용했으면
    {
        answer = path;
        return true;
    }
    
    for(int i = 0; i < tickets.size(); ++i)
    {
        if(used[i]) continue;
        if(tickets[i][0] != current) continue;
        
        used[i] = true;
        path.push_back(tickets[i][1]);
        
        if(dfs(tickets, path, tickets[i][1], ticketCount)) return true;      // 성공하면 바로 종료
        
        path.pop_back();
        used[i] = false;
    }
    
    return false;
}

vector<string> solution(vector<vector<string>> tickets) 
{
    sort(tickets.begin(), tickets.end());
    
    used.assign(tickets.size(), false);
    vector<string> path;
    path.push_back("ICN");      // 항상 ICN에서 출발
    
    dfs(tickets, path, path[0], tickets.size());
    
    return answer;
}

 

해설:

dfs로 깊이 탐색.

반응형

+ Recent posts