프로젝트 소개

DirectX GameEngine Banner

이 프로젝트는 DirectX 11을 기반으로 한 자체 게임 엔진 개발 프로젝트입니다. 엔진은 렌더링, 메쉬 가져오기, 오브젝트 및 컴포넌트 관리 등 게임 엔진에 필요한 기본적인 핵심 기능들을 탑재하고 있습니다. 씬 그래프와 인스펙터를 제공하여 사용자가 모델을 직관적으로 조절할 수 있도록 구성되었으며, Mesh, Physics, Camera 등 다양한 컴포넌트를 자유롭게 추가하고 삭제할 수 있는 유연한 구조를 갖추고 있습니다. 또한 Unity와 Unreal Engine의 내부 메소드를 참고하여 엔진의 성능을 최적화하고 사용자 친화적인 인터페이스를 구현하는 데 중점을 두었습니다.

📖 읽기 전에 : 이 문서는 엔진의 전체 구조와 구현 세부사항을 상세히 다루고 있어 내용이 길어 보일 수 있습니다. 왼쪽 네비게이션을 활용하여 핵심 부분만 보실 수 있습니다.

DirectX GameEngine

본 프로젝트의 전체 소스 코드와 개발 히스토리를 GitHub에서 확인하실 수 있습니다.

GitHub 저장소 방문하기
프로젝트 빌드 방법, 의존성, 실행 방법은 README를 참고하세요

프로젝트 개요 및 기술 스택

개발 환경

IDE Visual Studio
언어 C++, Python, HLSL
플랫폼 Windows

그래픽스 & 렌더링

렌더링 API DirectX 11
셰이더 HLSL
텍스처 처리 DirectXTex

물리 & 3D 리소스

물리 엔진 NVIDIA PhysX
모델 로더 Assimp
데이터 입출력 Cnpy

UI & 유틸리티

GUI ImGui

프로젝트 정보

개발 기간 2023.12.21 ~
개발 인원 1명 (서정현)

데모 영상

위 영상은 엔진의 시연 모습을 중점적으로 제작한 데모 영상입니다. 각 기능에 대한 자세한 설명과 구현 내용은 아래 글을 확인해 주세요.

기능 1 (UI 시스템)

이 게임 엔진은 Scene Graph, Folder View, Gizmo Control, Inspector, Other 등 5가지 UI를 지원합니다.

1.1 Folder View

Folder View

Folder View는 하단에 위치하며, 프로젝트 루트 폴더를 기준으로 파일을 탐색하고 수정할 수 있는 영역이 있습니다.

1.2 Scene Graph

Scene Graph

왼쪽을 보면 Scene Graph가 있습니다. 씬에 있는 모든 오브젝트를 확인할 수 있으며, 자식 오브젝트도 탐색할 수 있습니다. 오브젝트를 선택하면 기즈모로 표시되며 인스펙터에서 그 오브젝트의 정보를 표시합니다.

인스펙터의 하나의 오브젝트를 누르고 다른 오브젝트를 옮길 수 있습니다. 아래 테이블은 이동 키입니다.

기능
상위 오브젝트로 이동
하위 오브젝트 열기
같은 레벨 기준으로 이전 오브젝트로 이동
같은 레벨 기준으로 다음 오브젝트로 이동

1.3 Gizmo Control

Gizmo Controls에서는 Gizmo Mode, Coordinate System, Snap Setting을 설정할 수 있습니다. Gizmo Mode를 변경하면 그에 맞춰 기즈모 UI가 변경됩니다. Coordinate System에서는 World 좌표계와 Local 좌표계 간 전환이 가능하며, Snap Setting에서는 스냅 기능을 설정할 수 있습니다.

Gizmo Translate Mode

Translate Mode

Gizmo Rotate Mode

Rotate Mode

Gizmo Scale Mode

Scale Mode

1.4 Inspector

Inspector

Inspector는 상단에 Position, Rotation, Scale을 설정하는 Transform 영역이 있고, 중단에는 오브젝트에 포함된 Component 목록 및 각 컴포넌트의 설정 영역이 있습니다. 하단에는 오브젝트의 활성화 여부와 이름을 설정하는 영역이 위치합니다.

1.5 Other Settings

Simulation Speed and Settings

기타 설정에는 게임 실행 속도를 조절하는 기능, 오브젝트 선택 시 표시되는 glow 효과의 강도 설정, 그리고 SkyBox 설정이 포함되어 있습니다. SkyBox에서는 스카이박스의 형태를 Sphere 또는 Cube 중에서 선택할 수 있습니다. 마지막으로 Shadow 탭에서는 그림자 렌더링을 확인하기 위한 디버그(Dump) 기능을 제공합니다.

기능 2 (기본 엔진 기능)

엔진의 기본 조작 방식과 오브젝트 선택 기능을 설명합니다.

2.1 마우스 Lock 모드

ESC 키를 누르면 마우스 Lock 기능이 활성화됩니다. 마우스 Lock 상태에서는 커서가 고정되지만, 마우스 움직임을 통해 카메라를 회전하고 시점을 이동할 수 있습니다. ESC 키를 다시 누르면 마우스 Lock이 해제되어 커서를 자유롭게 움직이며 엔진의 UI를 조작할 수 있습니다.

2.2 키보드 이동

키보드 이동 조작 마우스 Lock 상태에서는 키보드 입력을 통해 월드를 이동할 수 있습니다. 아래는 각 키에 대한 이동 방향입니다. SHIFT 키를 함께 누르면 이동 속도가 2배로 증가합니다.

이동 방향
Q 위로 이동
W 앞으로 이동
E 아래로 이동
A 왼쪽으로 이동
S 뒤로 이동
D 오른쪽으로 이동

2.3 오브젝트 선택

오브젝트 선택 마우스 Lock이 해제된 상태에서는 오브젝트를 클릭하여 선택할 수 있습니다.

기능 3 (Component)

Transform은 모든 오브젝트에 대해 항상 존재하며, Inspector에서 조절할 수 있습니다. Unity나 Unreal의 Transform과 동일한 방식으로 동작합니다.

3.1 Transform

오브젝트는 Gizmo를 이용해 이동, 회전, 크기 조절이 가능하며, 회전은 내부적으로 쿼터니언을 사용해 짐벌락이 발생하지 않도록 구현되어 있습니다.

Inspector에서도 Transform을 직접 조절할 수 있습니다. Inspector 방식은 직관적이지만 회전 값은 오일러 방식으로 표시되기 때문에 특정 상황에서는 짐벌락이 발생할 수 있습니다.

Transform은 부모/자식 관계를 가질 수 있습니다. 부모 Transform이 변경되면 자식의 World Transform도 함께 변경됩니다.

3.2 Model / Mesh Component

Model Component Inspector

Model 컴포넌트는 FBX나 OBJ 등 3D 모델 파일을 가져오는 컴포넌트입니다. Mesh Component는 모델의 자식 오브젝트에 포함된 머티리얼과 메시 정보를 담당하며, 필요한 경우 엔진이 자동으로 Mesh 컴포넌트를 추가합니다.

Model Component에는 기본적으로 Component 활성화 토글이 있으며, Model Hierarchy에서 Mesh가 포함된 모든 자식 오브젝트를 나열합니다. Hierarchy에서 자식 오브젝트를 선택하면 해당 Mesh Component의 상세 설정이 표시됩니다.

Mesh 설정 항목 예시: Mesh 활성화/비활성화, 빛 반사 색상, Specular 사용 여부 및 강도, Normal 맵 사용 여부 및 강도, Shadow 사용 여부 등.

Model - all features

기본 설정 (All)

Model - no specular

Specular 비활성화

Model - no normal

Normal 맵 비활성화

Model - no shadow

Shadow 비활성화

Model - specular power 50

Specular Power = 50

3.3 ColorXXObject Component (Geometric Primitives)

ColorXXObject Component Inspector

ColorXXObject는 텍스처가 없고 단일 색상만 가지는 기하학적 기본 도형 오브젝트입니다. 모델 파일을 불러오는 대신, 코드를 통해 정점과 인덱스를 조립하여 화면에 표시하는 컴포넌트입니다.

기본적으로 Mesh Component와 마찬가지로 선택 시 나타나는 Outline의 색상과 Shadow 사용 여부를 설정할 수 있습니다. ColorXXObject만의 고유한 기능으로는 오브젝트의 색상을 직접 지정할 수 있습니다.

지원되는 기하학적 도형

ColorXXObject는 총 5가지의 기본 도형 모델을 지원합니다:

Color Cube Object

Cube (정육면체)

Color Cone Object

Cone (원뿔)

Color Cylinder Object

Cylinder (원기둥)

Color Plane Object

Plane (평면)

Color Sphere Object

Sphere (구)

Lit / UnLit 모드

ColorXXObject는 Lit과 UnLit 두 가지 렌더링 모드를 지원합니다. Lit 모드에서는 조명의 영향을 받아 음영이 표현되며, UnLit 모드에서는 조명과 무관하게 순수한 색상만 표시됩니다.

Lit Mode

Lit 모드 (조명 영향 O)

UnLit Mode

UnLit 모드 (조명 영향 X)

3.4 Camera Component

Camera Component는 Unity와 Unreal Engine의 Camera Component와 유사하게 동작하며, 메인 카메라를 변경하거나 프로젝션 설정을 할 수 있습니다.

Camera Component Inspector

Camera Component는 크게 Camera SelectionCamera Properties 두 가지 주요 기능을 제공합니다.

Camera Selection은 현재 활성화된 카메라를 설정하는 기능입니다. 드롭다운 메뉴에는 씬에 존재하는 모든 카메라의 이름이 표시되며, 선택하면 해당 카메라 시점으로 전환됩니다. 이 값은 static으로 구현되어 모든 Camera Component 인스턴스에서 공유됩니다.

드롭다운에서 다른 카메라를 선택하면 즉시 해당 카메라의 시점으로 화면이 전환됩니다.

Camera Properties에서는 카메라의 프로젝션(Projection) 설정과 카메라 인디케이터(Indicator)를 조정할 수 있습니다.

  • Enable Camera Indicator: 활성화하면 현재 카메라의 위치를 프레임으로 표시합니다.
  • Enable Frustum Indicator: 활성화하면 카메라의 시야(View Frustum)를 프레임으로 시각화하여 표시합니다.

카메라 인디케이터를 활성화하면 씬에서 카메라의 위치와 시야 범위를 시각적으로 확인할 수 있습니다.

3.5 Light Component

Light Component는 현재 Point Light 컴포넌트만 지원합니다. Point Light는 특정 지점에서 모든 방향으로 빛을 방출하며, Intensity(밝기), Diffuse(확산 색상), Ambient(환경광), Falloff(감쇠) 설정을 통해 조명 효과를 조절할 수 있습니다.

Intensity (밝기)

Intensity 값을 조정하여 빛의 밝기를 제어할 수 있습니다. 값이 클수록 더 밝은 빛이 방출됩니다.

Point Light Intensity 1

Intensity = 1

Point Light Intensity 3

Intensity = 3

Point Light Intensity 5

Intensity = 5

Diffuse Color (확산 색상)

Diffuse Color를 설정하여 빛의 색상을 변경할 수 있습니다. 이 색상은 물체 표면에 반사되어 전체적인 조명 분위기를 결정합니다.

Point Light Diffuse Color Green

Diffuse Color = Green

Point Light Diffuse Color Red

Diffuse Color = Red

Ambient (환경광)

Ambient 값을 조정하여 그림자 영역에서도 빛이 닿는 기본 밝기를 설정할 수 있습니다. 이를 통해 지나치게 어두운 그림자를 방지하고 자연스러운 조명을 구현할 수 있습니다.

Point Light Ambient Change

Ambient 값 변화 비교

Falloff (감쇠)

Falloff 값을 설정하여 광원으로부터 거리에 따른 빛의 감쇠율을 조정할 수 있습니다. 높은 값은 빠른 감쇠를, 낮은 값은 먼 거리까지 빛이 도달하도록 합니다.

Point Light Falloff

Falloff 값에 따른 빛의 범위 변화

3.6 Physics Component

Physics Component Inspector

Physics Component는 충돌 및 물리 작용 기능을 담당하는 컴포넌트입니다. ColorXXObject 또는 Model 컴포넌트를 가진 오브젝트에 Physics Component를 추가하면, 해당 오브젝트의 Vertex 데이터를 기반으로 콜라이더가 자동으로 생성됩니다.

인스펙터 설정 기능

Base Settings

  • Is Kinematic: 활성화하면 물리 엔진의 힘을 받지 않고 스크립트나 애니메이션으로만 이동하는 Kinematic 모드로 설정됩니다.
  • Use Gravity: 중력 작용 여부를 설정합니다.
  • Mass: 오브젝트의 질량을 설정합니다.
  • Collision Detection Mode: 충돌 감지 방식을 선택합니다 (Discrete, Continuous 등).

Material Properties

  • Static Friction: 정지 마찰력을 설정합니다.
  • Dynamic Friction: 동적 마찰력을 설정합니다.
  • Bounciness: 탄성(반발력)을 설정합니다.

Material Presets

  • Linear Damping: 선형 감쇠를 설정하여 이동 속도를 점차 감소시킵니다.
  • Angular Damping: 각속도 감쇠를 설정하여 회전 속도를 점차 감소시킵니다.
  • Drag Coefficient: 공기 저항 계수를 설정합니다.

Constraints

위치와 회전을 각 축별로 고정(Freeze)시킬 수 있습니다. 이를 통해 특정 축에서만 이동하거나 회전하도록 제한할 수 있습니다.

Global Gravity

Global Gravity를 설정하면 모든 오브젝트에 대해 중력을 설정할 수 있습니다. 이 설정은 static으로 구현되어 모든 Physics Component 인스턴스에서 공유됩니다. 중력의 방향과 강도를 설정할 수 있습니다.

다양한 설정 예시

각 Material Properties, Material Presets, Constraints를 설정한 결과는 다음과 같습니다:

  • Cube: X, Y, Z 축 이동 Freeze
  • Cone: X, Y, Z 축 회전 Freeze
  • Plane: Damping & Drag 설정
  • Sphere: Material Properties 설정

기능 4 (씬 이동)

만들어진 씬을 통해서 LoadScene 함수를 사용하여 씬을 이동할 수 있습니다. 이를 통해 게임의 다양한 레벨이나 장면 간에 전환할 수 있습니다.

예시로 Sponza 씬과 오브젝트가 없는 빈 씬을 만들었고, T 키를 눌렀을 때 빈 씬으로 정상적으로 이동하는 것을 확인할 수 있습니다.

기능 5 (엔진 스크립팅 메뉴얼)

DirectX GameEngine의 스크립팅 시스템을 활용하여 게임 로직과 씬을 구성하는 방법을 안내합니다. 이 메뉴얼을 통해 개발자는 엔진의 핵심 기능을 확장하고 커스터마이징할 수 있습니다.

5.1 씬(Scene) 생성

씬(Scene)은 게임의 각 장면이나 레벨을 나타내는 기본 단위입니다. 새로운 씬을 만들려면 Scene 클래스를 상속받아 커스텀 씬 클래스를 작성해야 합니다.

씬 클래스 선언

먼저 헤더 파일에서 Scene 클래스를 상속받는 새로운 씬 클래스를 선언합니다.

#include "Core/Scene/Base/Scene.h"

class EmptyScene : public Scene
{
public:
    EmptyScene(std::string sceneName);

    static std::shared_ptr<Scene> Create(std::string sceneName);

    void Initialize() override;
};

필수 구성 요소:

  • 생성자: 씬 이름을 받아 부모 클래스 Scene을 초기화합니다.
  • Create 메서드: 씬 인스턴스를 생성하고 shared_ptr로 반환하는 정적 팩토리 메서드입니다.
  • Initialize 메서드: 씬에 필요한 오브젝트, 카메라, 라이트 등을 초기화하는 메서드입니다.

씬 클래스 구현

구현 파일(.cpp)에서 각 메서드를 정의합니다.

생성자 구현
EmptyScene::EmptyScene(std::string sceneName) : Scene(sceneName)
{
    // 씬별 초기 설정이 필요한 경우 여기에 작성
}

생성자는 씬 이름(sceneName)을 부모 클래스 Scene의 생성자에 전달하여 초기화합니다.

Create 메서드 구현
std::shared_ptr<Scene> EmptyScene::Create(std::string sceneName)
{
    activeScene = std::make_shared<EmptyScene>(sceneName);

    return activeScene;
}

Create 메서드는 씬의 인스턴스를 생성하고, 이를 activeScene(현재 활성 씬)으로 설정한 후 반환합니다. 이 패턴을 통해 씬 생성과 관리를 일관되게 처리할 수 있습니다.

Initialize 메서드 구현
void EmptyScene::Initialize()
{
    Scene::Initialize();

    // 씬에 필요한 오브젝트, 카메라, 라이트 등을 여기서 생성 및 설정
    // 예: CreateGameObject("Player");
    //     AddCamera(...);
    //     AddLight(...);
}

Initialize 메서드에서는 먼저 부모 클래스의 Initialize()를 호출하여 기본 초기화를 수행한 후, 씬에 필요한 게임 오브젝트, 카메라, 조명 등을 생성하고 설정합니다.

씬 사용 예제

생성한 씬을 실제로 사용하는 방법:

// 씬 생성 및 활성화
LoadScene(EmptyScene::Create("MyGameScene"));

LoadScene 함수에 씬 클래스의 Create 메서드를 호출하여 생성한 씬을 전달하면, 해당 씬이 로드되고 활성화됩니다. 씬 이름은 디버깅이나 씬 전환 시 식별 용도로 사용됩니다.

💡 팁
씬을 설계할 때는 Initialize 메서드에서 모든 게임 오브젝트와 컴포넌트를 생성하고 설정하는 것이 좋습니다. 이렇게 하면 씬의 초기 상태를 명확하게 관리할 수 있으며, 씬 전환 시 일관된 동작을 보장할 수 있습니다.

5.2 이벤트 메소드의 실행 순서

본 엔진은 Unity 엔진에서 사용하는 이벤트 메소드를 기반으로 하되, 더욱 세분화된 제어를 위해 추가 메소드를 제공합니다. 모든 씬(Scene), 오브젝트(Object), 컴포넌트(Component)는 EngineLoop 인터페이스를 상속받아 동일한 생명주기 메소드를 사용합니다.

이벤트 메소드 목록

EngineLoop 인터페이스는 다음과 같은 가상 메소드들을 정의합니다:

class EngineLoop
{
public:
    virtual void Initialize() = 0;      // 초기화 - 씬/오브젝트 생성 직후 1회 실행
    virtual void BeforeFrame() = 0;     // 첫 프레임 시작 전 1회 실행
    virtual void Start() = 0;           // 첫 Update 전 1회 실행
    virtual void LateStart() = 0;       // Start 이후 1회 실행
    virtual void Update(float deltaTime) = 0;      // 매 프레임 실행 (메인 로직)
    virtual void LateUpdate() = 0;      // Update 이후 매 프레임 실행
    virtual void Finalize() = 0;        // 씬 종료 시 정리 작업
    virtual void Destroy() = 0;         // 오브젝트 파괴 시 실행
    
    virtual void OnEnable() = 0;        // 오브젝트/컴포넌트 활성화 시 실행
    virtual void OnDisable() = 0;       // 오브젝트/컴포넌트 비활성화 시 실행
    virtual void Reset() = 0;           // 리셋 요청 시 실행
};

주요 메소드 설명:

  • Initialize: 씬이나 오브젝트가 생성된 직후 1회 호출되며, 초기 설정 및 자원 할당에 사용됩니다.
  • BeforeFrame: 게임 루프의 첫 프레임이 시작되기 전 1회 호출됩니다.
  • Start / LateStart: Update가 시작되기 전 1회 호출되며, 초기 상태 설정에 사용됩니다.
  • Update: 매 프레임마다 호출되는 메인 게임 로직 메소드입니다. deltaTime 파라미터로 프레임 간 시간을 받습니다.
  • LateUpdate: Update 이후에 호출되며, Update에서 변경된 값을 기반으로 추가 작업을 수행할 때 사용됩니다.
  • Finalize: 씬이 종료되거나 전환될 때 호출되어 정리 작업을 수행합니다.
  • Destroy: 오브젝트가 파괴될 때 호출되어 자원을 해제합니다.
  • OnEnable / OnDisable: 오브젝트나 컴포넌트가 활성화/비활성화될 때마다 호출됩니다.

씬(Scene) 로드 시 실행 순서

씬이 처음 로드되거나 전환될 때 다음 순서로 메소드가 호출됩니다:

[씬 로드 단계]
1. Scene::Create()          // 씬 인스턴스 생성
2. Scene::Initialize()      // 씬 초기화
   └─> Object::Initialize() (모든 오브젝트)
       └─> Component::Initialize() (활성화된 컴포넌트)
3. Scene::BeforeFrame()     // 첫 프레임 전 준비
   └─> Object::BeforeFrame()
       └─> Component::BeforeFrame()
4. Scene::Start()           // 씬 시작
   └─> Object::Start()
       └─> Component::Start()
5. Scene::LateStart()       // 시작 후처리
   └─> Object::LateStart()
       └─> Component::LateStart()

[게임 루프 시작]
6. Scene::Update(deltaTime)      // 매 프레임 반복
   └─> Object::Update(deltaTime)
       └─> Component::Update(deltaTime)
7. Scene::LateUpdate()           // 매 프레임 반복
   └─> Object::LateUpdate()
       └─> Component::LateUpdate()

[씬 종료 단계]
8. Scene::Finalize()        // 씬 종료 정리
   └─> Object::Finalize()
       └─> Component::Finalize()
9. Scene::Destroy()         // 씬 파괴
   └─> Object::Destroy()
       └─> Component::Destroy()

코드 예시 (App.cpp)

씬 로드 시퀀스를 명시적으로 호출하는 예제:

// 씬 로드 시퀀스
currentScene = SponzaScene::Create("Sponza");
currentScene->Initialize();
currentScene->BeforeFrame();
currentScene->Start();
currentScene->LateStart();

// 게임 루프
while (isRunning)
{
    float deltaTime = CalculateDeltaTime();
    
    currentScene->Update(deltaTime);
    currentScene->LateUpdate();
    
    Render();
}

// 씬 종료
currentScene->Finalize();
currentScene->Destroy();

5.3 오브젝트(Object) 생성

오브젝트는 게임 세계에 존재하는 모든 개체(캐릭터, 건물, 카메라, 라이트 등)를 나타냅니다. 오브젝트는 씬(Scene) 내의 모든 이벤트 메소드에서 자유롭게 생성할 수 있으며, 생성된 오브젝트는 자동으로 씬의 생명주기에 포함됩니다.

기본 오브젝트 생성 방법

오브젝트를 생성하려면 Object::Create() 정적 메소드를 사용하여 오브젝트 인스턴스를 만들고, AddObject() 메소드를 통해 씬에 추가합니다.

기본 문법:

std::shared_ptr<Object> 변수이름 = AddObject(Object::Create("오브젝트 이름"));

설명:

  • Object::Create("오브젝트 이름"): 지정한 이름으로 새로운 오브젝트 인스턴스를 생성합니다.
  • AddObject(...): 생성된 오브젝트를 현재 씬에 추가하고, 해당 오브젝트의 shared_ptr를 반환합니다.
  • 반환된 shared_ptr<Object>를 통해 생성된 오브젝트를 조작할 수 있습니다.

AddObject() 메소드의 역할

AddObject() 메소드는 Scene 클래스의 멤버 함수로, 생성된 오브젝트를 씬에 등록하는 역할을 합니다. 씬에 등록된 오브젝트는 자동으로 생명주기 이벤트(Initialize, Start, Update 등)를 받게 됩니다.

실전 예제

예제 1: 빈 오브젝트 생성
void MyScene::Initialize()
{
    Scene::Initialize();
    
    // 빈 오브젝트 생성 (TransformComponent만 가짐)
    std::shared_ptr<Object> emptyObject = AddObject(Object::Create("Empty GameObject"));
}
예제 2: 컴포넌트가 있는 오브젝트 생성
void MyScene::Initialize()
{
    Scene::Initialize();
    
    // 플레이어 오브젝트 생성 및 컴포넌트 추가
    std::shared_ptr<Object> player = AddObject(Object::Create("Player"));
    player->AddComponent<Model>("Model/Player.fbx");
    player->AddComponent<PhysicsComponent>();
    player->transform->SetPosition(0.0f, 5.0f, 0.0f);
}

생성된 오브젝트 참조 저장

자주 접근해야 하는 오브젝트는 멤버 변수로 저장하여 사용할 수 있습니다.

class MyScene : public Scene
{
public:
    void Initialize() override
    {
        Scene::Initialize();
        
        // 플레이어 오브젝트 생성 및 참조 저장
        player = AddObject(Object::Create("Player"));
        player->AddComponent<Model>("Model/Player.fbx");
        
        // 적 오브젝트 생성
        enemy = AddObject(Object::Create("Enemy"));
        enemy->AddComponent<Model>("Model/Enemy.fbx");
    }
};

오브젝트 검색

씬에 추가된 오브젝트는 이름 또는 인덱스를 통해 검색할 수 있습니다.

이름으로 검색
// 이름으로 오브젝트 검색
std::shared_ptr<Object> player = GetObject("Player");

if (player != nullptr)
{
    // 오브젝트가 존재할 때만 실행
    player->transform->SetPosition(0.0f, 5.0f, 0.0f);
}
인덱스로 검색
// 인덱스로 오브젝트 검색 (0부터 시작)
std::shared_ptr<Object> firstObject = GetObject(0);

// 모든 오브젝트 순회
std::vector<std::shared_ptr<Object>> allObjects = GetObjects();

for (auto& obj : allObjects)
    std::cout << "Object Name: " << obj->GetName() << std::endl;
오브젝트 존재 확인
// 특정 이름의 오브젝트가 씬에 있는지 확인
if (HasObject("Enemy"))
{
    // Enemy 오브젝트가 존재하는 경우
    auto enemy = GetObject("Enemy");
    // ...
}

오브젝트 제거

씬에서 오브젝트를 제거하려면 RemoveObject() 메소드를 사용합니다.

// 이름으로 제거
std::shared_ptr<Object> removed = RemoveObject("Enemy");

// 인덱스로 제거
std::shared_ptr<Object> removed = RemoveObject(0);

// 제거된 오브젝트는 더 이상 씬의 업데이트 루프에서 호출되지 않음

오브젝트 활성화/비활성화

오브젝트를 제거하지 않고 일시적으로 비활성화할 수 있습니다.

// 오브젝트 비활성화 (업데이트 루프에서 제외됨)
myObject->SetActive(false);

// 오브젝트 활성화
myObject->SetActive(true);

// 활성화 상태 확인
bool isActive = myObject->GetActive();

5.4 컴포넌트(Component) 시스템

컴포넌트는 오브젝트에 기능을 추가하는 모듈형 시스템입니다. Unity의 컴포넌트 시스템과 유사하게 설계되어 있으며, 오브젝트에 원하는 컴포넌트를 추가하여 다양한 기능을 구현할 수 있습니다.

컴포넌트 기본 개념

컴포넌트는 Component 클래스를 상속받아 구현되며, 모든 컴포넌트는 EngineLoop 인터페이스의 이벤트 메소드를 사용합니다.

컴포넌트의 특징:

  • 오브젝트에 종속되어 동작합니다.
  • 여러 컴포넌트를 조합하여 복잡한 기능을 구현할 수 있습니다.
  • 컴포넌트 간 transform 포인터를 공유하여 위치/회전/크기 정보에 접근합니다.
  • 활성화/비활성화가 가능합니다.

컴포넌트 추가 (AddComponent)

오브젝트에 컴포넌트를 추가하려면 AddComponent<>() 템플릿 메소드를 사용합니다.

// 기본 문법
std::shared_ptr<컴포넌트클래스> 변수이름 = 오브젝트->AddComponent<컴포넌트클래스>();

// 예제: Model 컴포넌트 추가
std::shared_ptr<Model> modelComponent = player->AddComponent<Model>("Model/Player.fbx", 2.0f);

// 예제: PhysicsComponent 추가
auto physics = box->AddComponent<PhysicsComponent>(10.0f, true);
physics->SetMaterial(0.6f, 0.4f, 0.5f);

AddComponent의 특징:

  • 생성된 컴포넌트의 shared_ptr를 반환합니다.
  • 이미 동일한 타입의 컴포넌트가 존재하면 기존 컴포넌트를 반환합니다 (중복 방지).
  • 컴포넌트 생성 시 필요한 매개변수를 전달할 수 있습니다.
  • 추가된 컴포넌트는 오브젝트의 생명주기에 자동으로 포함됩니다.

컴포넌트 가져오기 (GetComponent)

이미 추가된 컴포넌트를 가져오려면 GetComponent<>() 템플릿 메소드를 사용합니다.

// TransformComponent 가져오기
auto transform = player->GetComponent<TransformComponent>();
transform->SetPosition(0.0f, 5.0f, 0.0f);

// 컴포넌트 존재 확인 후 사용
auto model = enemy->GetComponent<Model>();
if (model != nullptr)
{
    model->Submit(RenderingChannel::main);
}

// transform은 모든 오브젝트가 기본으로 가지므로 직접 접근 가능
player->transform->SetRotationFromEuler(0.0f, Math::PI, 0.0f);

기본 제공 컴포넌트

본 엔진은 다음과 같은 컴포넌트를 기본 제공합니다:

컴포넌트 설명 주요 용도
TransformComponent 위치, 회전, 크기 관리 모든 오브젝트의 변환 정보
Camera 카메라 뷰 및 투영 씬 렌더링, 시점 제어
Model 3D 모델 렌더링 FBX, OBJ 등 외부 모델 로드
PhysicsComponent 물리 시뮬레이션 중력, 충돌, 강체 동역학
PointLight 포인트 라이트 조명 효과, 그림자
Color Objects 프리미티브 렌더링 큐브, 구, 원뿔, 원통, 평면

주요 컴포넌트 사용 예제

TransformComponent
// 위치 설정
player->transform->SetPosition(0.0f, 5.0f, 10.0f);

// 회전 설정 (오일러 각도, 라디안)
player->transform->SetRotationFromEuler(0.0f, Math::PI / 2.0f, 0.0f);

// 크기 설정
player->transform->SetScale(2.0f, 2.0f, 2.0f);

// 방향 벡터 가져오기
Vector3 forward = player->transform->GetForward();
Vector3 right = player->transform->GetRight();

// 부모-자식 관계 설정
child->transform->SetParent(parent->transform);
Camera 컴포넌트
// 카메라 생성 및 등록
std::shared_ptr<Object> camera = AddObject(Object::Create("Main Camera"));
cameras.AddCamera(camera->AddComponent<Camera>());

// 카메라 위치 및 회전 설정
camera->transform->SetPosition(-22.0f, 4.0f, 0.0f);
camera->transform->SetRotationFromEuler(0.0f, Math::PI / 2.0f, 0.0f);

// 활성 카메라 전환
cameras.SetActiveCamera(0);
Model 컴포넌트
// 모델 로드 (기본 스케일 1.0)
player->AddComponent<Model>("Model/Character.fbx");

// 모델 로드 (스케일 2배)
enemy->AddComponent<Model>("Model/Enemy.obj", 2.0f);

// 모델 로드 (스케일 0.05배 축소)
building->AddComponent<Model>("Model/Building.gltf", 0.05f);
PhysicsComponent
// 동적 객체 (질량 10kg)
auto physics = box->AddComponent<PhysicsComponent>(10.0f, true);

// 물리 재질 설정 (정적마찰, 동적마찰, 반발계수)
physics->SetMaterial(0.6f, 0.4f, 0.5f);

// 중력 설정
physics->SetGravity(true);

// 힘 적용
physics->AddForce(physx::PxVec3(100.0f, 0.0f, 0.0f));

// 제약 조건 설정 (X, Z축 위치 고정)
physics->SetPositionConstraint(true, false, true);
프리미티브 오브젝트
// 큐브 생성
std::shared_ptr<Object> cube = AddObject(Object::Create("Cube"));
auto cubeComp = cube->AddComponent<ColorCubeObject>();
cubeComp->SetColor(GraphicResource::Image::Color(255, 0, 0));  // 빨강
cubeComp->SetLit(true);  // 라이팅 적용

// 구 생성
std::shared_ptr<Object> sphere = AddObject(Object::Create("Sphere"));
sphere->AddComponent<ColorSphereObject>();

컴포넌트 관리

컴포넌트 존재 확인
// 템플릿 방식
if (player->HasComponent<Model>())
{
    auto model = player->GetComponent<Model>();
    // 모델 관련 작업
}

// 문자열 방식
if (player->HasComponent("PhysicsComponent"))
{
    // 물리 관련 작업
}
컴포넌트 활성화/비활성화
// 컴포넌트 비활성화
auto model = player->GetComponent<Model>();
model->SetEnable(false);  // 렌더링 중지

// 컴포넌트 활성화
model->SetEnable(true);   // 렌더링 재개

// 활성화 상태 확인
bool isActive = model->GetEnable();
컴포넌트 제거
// 컴포넌트 제거 (클래스 이름 사용)
player->RemoveComponent("Model");
player->RemoveComponent("PhysicsComponent");

// TransformComponent는 제거할 수 없음 (필수 컴포넌트)
모든 컴포넌트 가져오기
// 모든 컴포넌트 순회
std::vector<std::shared_ptr<Component>> allComponents = player->GetAllComponents();

for (auto& component : allComponents)
{
    std::cout << "Component: " << component->GetClassName() << std::endl;
}

⚠️ 주의사항
GetComponent는 컴포넌트가 없으면 nullptr를 반환하므로, 사용 전 반드시 nullptr 체크를 해야 합니다. 또한 TransformComponent는 모든 오브젝트의 필수 컴포넌트이므로 제거할 수 없습니다.

5.5 키보드 및 마우스 입력 처리

이 엔진은 이벤트 기반 입력 시스템을 제공합니다. Windows 메시지를 통해 입력을 받아 큐에 저장하고, 사용자 코드에서 필요할 때 이벤트를 가져와 처리합니다.

키보드 입력

키보드 입력은 Keyboard 클래스를 통해 처리됩니다.

주요 메소드:

  • IsPressed(unsigned char keycode): 특정 키가 현재 누려있는지 확인
  • ReadKey(): 키보드 이벤트 큐에서 다음 이벤트를 가져옴 (optional<Event> 반환)
  • KeyIsEmpty(): 이벤트 큐가 비어있는지 확인
  • FlushKey(): 이벤트 큐 비우기
  • EnableAutorepeat(): 키를 누르고 있을 때 연속 이벤트 발생 활성화
  • DisableAutorepeat(): 키를 누르고 있을 때 연속 이벤트 발생 비활성화

키보드 이벤트 종류:

  • Press: 키를 누렀을 때
  • Release: 키를 떼을 때
  • Invalid: 유효하지 않은 이벤트
사용 예시
// App.cpp의 KeyBoardInput() 메소드 예시
void App::KeyBoardInput()
{
    KeyBoard& keyBoard = GetKeyBoard();

    // ESC 키를 누르면 커서 표시 토글
    if (keyBoard.IsPressed(VK_ESCAPE))
        GetWindow().ToggleCursorShow();

    // F1 키를 누르면 ImGui 데모 표시
    if (keyBoard.IsPressed(VK_F1))
        GetWindow().ToggleImGuiDemoShow();

    // R 키를 누르면 첫 번째 씬으로 전환
    if (keyBoard.IsPressed('R'))
        GetWindow().ChangeActiveScene("ShadowMapTest");

    // T 키를 누르면 두 번째 씬으로 전환
    if (keyBoard.IsPressed('T'))
        GetWindow().ChangeActiveScene("NormalOffsetTest");

    // 이벤트 큐에서 모든 키보드 이벤트를 처리
    while (auto keyEvent = keyBoard.ReadKey())
    {
        if (keyEvent->GetType() == KeyBoard::Event::Type::Press)
        {
            unsigned char keyCode = keyEvent->GetCode();
            // 키 Press 이벤트 처리
        }
        else if (keyEvent->GetType() == KeyBoard::Event::Type::Release)
        {
            unsigned char keyCode = keyEvent->GetCode();
            // 키 Release 이벤트 처리
        }
    }
}

Windows 가상 키 코드:

  • VK_ESCAPE: ESC 키
  • VK_F1, VK_F2, ...: F1, F2, ... 기능 키
  • 'A' ~ 'Z': 알파벳 키 (대문자로 표현)
  • '0' ~ '9': 숫자 키
  • VK_SPACE: 스페이스 바
  • VK_RETURN: Enter 키
  • VK_SHIFT, VK_CONTROL, VK_MENU: Shift, Ctrl, Alt 키
  • VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN: 방향키

마우스 입력

마우스 입력은 Mouse 클래스를 통해 처리됩니다.

주요 메소드:

  • GetPos(): 마우스 위치 (pair<int, int>) 반환
  • GetPosX(): 마우스 X 좌표 반환
  • GetPosY(): 마우스 Y 좌표 반환
  • LeftIsPressed(): 왼쪽 버튼이 누려있는지 확인
  • RightIsPressed(): 오른쪽 버튼이 누려있는지 확인
  • Read(): 마우스 이벤트 큐에서 다음 이벤트를 가져옴 (optional<Event> 반환)
  • IsEmpty(): 이벤트 큐가 비어있는지 확인
  • Flush(): 이벤트 큐 비우기
  • IsInWindow(): 마우스가 창 안에 있는지 확인
  • EnableRaw(): Raw 입력 활성화 (FPS 카메라 등에 사용)
  • DisableRaw(): Raw 입력 비활성화
  • ReadRawDelta(): Raw 마우스 이동량 읽기 (optional<pair<int, int>> 반환)

마우스 이벤트 종류:

  • LPress: 왼쪽 버튼 누름
  • LRelease: 왼쪽 버튼 떨
  • RPress: 오른쪽 버튼 누름
  • RRelease: 오른쪽 버튼 떨
  • WheelUp: 마우스 휠 위로
  • WheelDown: 마우스 휠 아래로
  • Move: 마우스 이동
  • Enter: 마우스가 창 안으로 들어옴
  • Leave: 마우스가 창 밖으로 나감
사용 예시
// 마우스 위치 확인
Mouse& mouse = GetMouse();
std::pair<int, int> pos = mouse.GetPos();  // [x, y]

// 마우스 버튼 상태 확인
if (mouse.LeftIsPressed())
{
    // 왼쪽 버튼이 누려있을 때의 처리
}

if (mouse.RightIsPressed())
{
    // 오른쪽 버튼이 누려있을 때의 처리
}

// 마우스 이벤트 처리
while (auto mouseEvent = mouse.Read())
{
    switch (mouseEvent->GetType())
    {
    case Mouse::Event::Type::LPress:
        // 왼쪽 버튼 클릭 처리
        break;
    case Mouse::Event::Type::Move:
        std::pair<int, int> pos = mouseEvent->GetPos();
        // 마우스 이동 처리
        break;
    case Mouse::Event::Type::WheelUp:
        // 휠 업 처리 (예: 줌 인)
        break;
    case Mouse::Event::Type::WheelDown:
        // 휠 다운 처리 (예: 줌 아웃)
        break;
    }
}

// FPS 카메라를 위한 Raw 입력 사용
mouse.EnableRaw();
while (auto delta = mouse.ReadRawDelta())
{
    auto d = *delta;
    // 마우스 델타 값으로 카메라 회전 처리
    camera.Rotate(d.first * sensitivity, d.second * sensitivity);
}

📝 Raw 입력
Raw 입력은 마우스의 실제 하드웨어 이동량을 제공합니다. 화면 해상도나 마우스 가속도의 영향을 받지 않습니다. FPS 게임의 카메라 제어에 적합하며, EnableRaw()로 활성화하고 ReadRawDelta()로 읽습니다.

입력 처리 흐름

Window 클래스의 HandleMsg() 메소드가 Windows 메시지를 받아 Keyboard/Mouse 클래스로 전달합니다.

Windows 메시지 흐름:

  • WM_KEYDOWNkeyBoard.OnKeyPressed(wParam)
  • WM_KEYUPkeyBoard.OnKeyReleased(wParam)
  • WM_MOUSEMOVEmouse.OnMouseMove(...)
  • WM_LBUTTONDOWNmouse.OnLeftPressed(...)
  • WM_LBUTTONUPmouse.OnLeftReleased(...)
  • WM_MOUSEWHEELmouse.OnWheelUp() / mouse.OnWheelDown()
  • WM_INPUT → raw input 처리

입력 처리 권장 패턴

1. 이벤트 루프: 매 프레임 모든 이벤트를 처리
while (auto event = keyboard.ReadKey()) 
{ 
    // 이벤트 처리 
}
2. 상태 체크: 현재 누려있는지 확인
if (keyboard.IsPressed('W')) 
{ 
    // 이동 처리 
}
3. 혼합 사용: 상태 체크로 연속 동작, 이벤트로 단발성 동작
// 연속 이동
if (keyboard.IsPressed('W')) MoveForward();

// 단발성 점프
while (auto event = keyboard.ReadKey())
{
    if (event->GetType() == KeyBoard::Event::Type::Press && 
        event->GetCode() == VK_SPACE)
        Jump();
}

렌더링 시스템 아키텍처

본 엔진은 Render Graph 패턴을 활용하여 복잡한 렌더링 파이프라인을 효율적으로 관리합니다. 렌더링 프로세스는 BeginFrame, Update, RenderGraph Execute, EndFrame, Reset의 5단계로 구성되어 있으며, 각 단계가 명확히 분리되어 있어 유지보수와 확장이 용이합니다.

메인 렌더링 루프 (App::DoFrame)

모든 렌더링 작업은 App 클래스의 DoFrame 메소드를 통해 관리됩니다. 이 메소드는 물리 업데이트, 게임 로직 업데이트, RenderGraph 실행, UI 렌더링, 화면 출력까지 하나의 프레임을 완성하는 전체 파이프라인을 조율합니다.

// Core/App.cpp
void App::DoFrame(float deltaTime)
{
    const float t = timer.TotalTime();
    Window::ShowGameFrame(wnd.GetHWnd());
    
    // [1] BeginFrame 호출
    wnd.GetDxGraphic().BeginFrame(0.07f, 0.0f, 0.12f);
    
    // [2] 물리 시스템 업데이트
    PhysicsSystem::GetInstance().Update(deltaTime);
    
    // [3] 씬 업데이트
    currentScene->Update(deltaTime);
    
    // [4] RenderGraph 실행
    App::GetRenderGraph().Execute();
    
    // [5] UI 렌더링
    CreateSimulationWindow();
    CreateDemoWindows();
    currentScene->LateUpdate();
    App::GetRenderGraph().RenderWindows();
    
    Engine::FolderView::instance->RenderFolderView();
    Engine::MenuBar::menuBar->RenderMenuBar();
    Engine::Inspector::instance->Update(deltaTime);
    
    // [6] EndFrame 및 Present
    wnd.GetDxGraphic().EndFrame();
    
    // [7] Reset
    App::GetRenderGraph().Reset();
}

RenderGraph 아키텍처

RenderGraph는 렌더링 파이프라인을 그래프 형태로 관리하는 핵심 시스템입니다. 각 RenderPass는 데이터 의존성을 명시적으로 관리하여 불필요한 렌더링 패스를 제거하고, 복잡한 멀티패스 렌더링을 체계적으로 구성할 수 있습니다.

주요 구성 요소

RenderGraph (Core/RenderingPipeline/RenderGraph/RenderGraph.h)
├─ RenderPass (렌더 패스 기본 클래스)
│  ├─ RenderingPass (렌더링 작업을 수행하는 패스)
│  │  ├─ RenderQueuePass (렌더 작업 큐를 관리하는 패스)
│  │  │  ├─ LambertianRenderPass (램버시안 라이팅)
│  │  │  ├─ ShadowMapPass (그림자 맵 생성)
│  │  │  ├─ OutlineMaskPass (아웃라인 마스크)
│  │  │  └─ OutlineDrawPass (아웃라인 그리기)
│  │  └─ PostProcessFullScreenRenderPass (전체 화면 포스트 프로세싱)
│  │     ├─ HorizontalBlurPass (수평 블러)
│  │     └─ VerticalBlurPass (수직 블러)
│  └─ BufferPassClear (버퍼 초기화)
├─ PipelineDataProvider (데이터 제공자)
└─ PipelineDataConsumer (데이터 소비자)
// Core/RenderingPipeline/RenderGraph/RenderGraph.cpp
// RenderGraph 생성자 - 백 버퍼와 마스터 깊이 버퍼 초기화
RenderGraph::RenderGraph()
    : backBufferTarget(Window::GetDxGraphic().GetRenderTarget()),
      masterDepth(std::make_shared<Graphic::OutputOnlyDepthStencil>())
{
    // 글로벌 제공자들 등록 (모든 패스에서 접근 가능한 기본 리소스들)
    AddGlobalProvider(
        DirectBufferPipelineDataProvider<Graphic::RenderTarget>::Create(
            "backbuffer", backBufferTarget
        )
    );
    AddGlobalProvider(
        DirectBufferPipelineDataProvider<Graphic::DepthStencil>::Create(
            "masterDepth", masterDepth
        )
    );
    
    // 글로벌 소비자 등록 (최종 출력을 받을 소비자)
    AddGlobalConsumer(
        DirectBufferDataConsumer<Graphic::RenderTarget>::Create(
            "backbuffer", backBufferTarget
        )
    );
}

// 모든 렌더 패스를 순서대로 실행
void RenderGraph::Execute() NOEXCEPTRELEASE
{
    // 최종화되지 않은 그래프는 실행 불가
    if (!isFinalized)
        throw RENDER_GRAPHIC_EXCEPTION("최종화되지 않은 렌더 그래프는 실행할 수 없습니다.");
    
    // 등록된 순서대로 모든 패스 실행
    for (auto& pass : renderPasses)
        pass->Execute();
}

// 모든 렌더 패스를 리셋 (다음 프레임 준비)
void RenderGraph::Reset() noexcept
{
    assert(isFinalized);
    
    // 모든 패스를 리셋하여 다음 프레임 준비
    for (auto& pass : renderPasses)
        pass->Reset();
}

BlurOutlineRenderGraph 예시

본 엔진의 기본 RenderGraph는 BlurOutlineRenderGraph로, 다음과 같은 10개의 패스로 구성됩니다:

Pass 1-2: 버퍼 초기화
  • clearRenderTarget: 백버퍼 초기화
  • clearDepthStencil: 깊이-스텐실 버퍼 초기화
Pass 3-5: 기본 렌더링
  • ShadowMap: 라이트 시점에서 그림자 맵 생성
  • Lambertian: 램버시안 반사 모델로 오브젝트 렌더링 (ShadowMap 입력 사용)
  • Skybox: 배경 스카이박스 렌더링
Pass 6-9: 아웃라인 효과
  • OutlineMask: 선택된 오브젝트의 마스크를 스텐실 버퍼에 렌더링
  • OutlineDraw: 블러를 적용할 아웃라인 데이터 생성
  • HorizontalBlur: 가우시안/박스 블러 커널로 수평 블러
  • VerticalBlur: 수직 블러로 최종 아웃라인 효과를 메인 렌더 타겟에 합성
Pass 10: 최종 출력
  • WireFrame: 카메라 frustum 와이어프레임을 최종 결과에 오버레이하여 백버퍼로 출력

전역 리소스: $.backbuffer (최종 출력 백버퍼), $.masterDepth (메인 깊이 버퍼), $.blurKernel (블러 커널 계수), $.blurDirection (블러 방향 제어)

Technique 및 RenderJob 시스템

Technique는 여러 RenderStep의 조합으로 특정 렌더링 기법을 정의합니다. 각 Technique는 채널 비트마스크를 사용하여 선택적으로 렌더링할 수 있으며, 이를 통해 메인 렌더링, 그림자 렌더링, 아웃라인 렌더링 등을 독립적으로 제어할 수 있습니다.

Technique 구조

Technique
├─ name: 기법 이름 (예: "Phong", "ShadowMap")
├─ channel: 렌더링 채널 (비트마스크, 예: 0x01, 0x02, 0x04)
├─ isActive: 활성화 상태 (true/false)
└─ renderSteps[]: RenderStep 배열
    └─ RenderStep
        ├─ targetPassName: 목표 RenderPass 이름
        └─ renderPipeline: 파이프라인 상태 (셰이더, 버퍼 등)
// Core/RenderingPipeline/RenderingManager/Technique/Technique.cpp
// 렌더링 제출 - 채널 필터에 맞는 경우만 제출
void Technique::Submit(const Drawable& drawable, size_t channelFilter) const noexcept
{
    // 기법이 비활성화되어 있으면 제출하지 않음
    if (!isActive)
        return;
    
    // 채널 필터와 일치하는지 확인 (비트마스크 AND 연산)
    // 예: channel=0x01, channelFilter=0x03 → 0x01 & 0x03 = 0x01 (제출)
    if (channelFilter & channel)
    {
        // 이 기법의 모든 렌더 스텝에 제출
        for (auto& step : renderSteps)
            step.Submit(drawable);
    }
}

// RenderGraph와 연결
void Technique::Link(RenderGraphNameSpace::RenderGraph& renderGraph)
{
    // 각 렌더 스텝을 RenderGraph의 해당 패스와 연결
    for (auto& step : renderSteps)
        step.Link(renderGraph);
}

RenderJob 실행 과정

RenderJob은 실제 렌더링을 수행하는 최소 단위 작업입니다. 각 RenderJob은 렌더링할 객체(drawable)와 렌더링 방법(renderStep)을 포함합니다.

1 Frustum Culling 체크

카메라 절두체 외부에 있는 객체는 렌더링을 생략하여 성능을 향상시킵니다.

2 렌더링 파이프라인 설정
  • drawable->SetRenderPipeline()
    • VertexBuffer, IndexBuffer 설정
    • InputLayout 설정
  • renderStep->SetRenderPipeline()
    • VertexShader, PixelShader 설정
    • ConstantBuffer, SamplerState 설정
    • RasterizerState, DepthStencilState 설정
3 DrawIndexed 호출

DirectX의 DrawIndexed 명령을 실행하여 GPU에서 실제 렌더링을 수행합니다.

// Core/RenderingPipeline/RenderingManager/Pass/Base/RenderJob.cpp
// 렌더링 작업 실행
void RenderJob::Excute(bool isPassFrustumCulling) NOEXCEPTRELEASE
{
    // [1] 절두체 컬링 체크 (패스별 설정 && 전역 설정 && 실제 절두체 내 존재)
    if (isPassFrustumCulling && useViewFrustum && !IsInViewFrustum())
    {
        // 절두체 밖에 있으면 렌더링 건너뛰기 (성능 최적화)
        return;
    }
    
    // [2] 렌더링 파이프라인 설정
    drawable->SetRenderPipeline();   // 객체의 버텍스/인덱스 버퍼 설정
    renderStep->SetRenderPipeline(); // 렌더 스텝의 셰이더/상태 설정
    
    // [3] 실제 드로우 콜 실행
    Window::GetDxGraphic().DrawIndexed(drawable->GetIndexCount());
}

// 절두체 내부에 있는지 확인
bool RenderJob::IsInViewFrustum() const
{
    // drawable의 Bounding Volume을 가져와서 절두체와 교차 테스트
    return viewFrustumCulling.IsInFrustum(drawable->GetBoundingVolume());
}

RenderQueuePass 작동 방식

RenderQueuePass는 제출된 모든 RenderJob을 수집하여 일괄 실행합니다.

┌─── 제출 단계 ───┐
Model::Submit() 
├─> Technique::Submit() 
│   └─> RenderStep::Submit() 
│       └─> RenderQueuePass::Accept(RenderJob)
│           └─> renderJobs.push_back(renderJob)

┌─── 실행 단계 ───┐
RenderQueuePass::Execute()
├─> for (auto& job : renderJobs)
│   └─> job.Execute()
│       ├─ Frustum/Occlusion Culling
│       ├─ 렌더링 파이프라인 설정
│       └─ DrawIndexed 호출

┌─── 리셋 단계 ───┐
RenderQueuePass::Reset()
└─> renderJobs.clear()

DirectX 렌더링 파이프라인 단계

본 엔진은 DirectX 11의 렌더링 파이프라인을 다음 5개의 주요 단계로 구성합니다. 각 단계는 독립적인 파이프라인 컴포넌트로 구현되어 있으며, Core/RenderingPipeline/Pipeline/ 폴더의 하위 폴더에 체계적으로 구성되어 있습니다.

IA - Input Assembler

  • InputLayout: 정점 데이터 레이아웃 정의
  • VertexBuffer: 정점 데이터
  • IndexBuffer: 인덱스 데이터
  • PrimitiveTopology: 기본 도형 유형
Pipeline/IA/

VS - Vertex Shader

  • VertexShader: 정점 셰이더 실행
  • ConstantBuffer: 변환 행렬 등 상수 데이터
Pipeline/VSPS/

RS - Rasterizer

  • Viewport: 뷰포트 설정
  • RasterizerState: 컬링 모드, 채우기 모드
Pipeline/Rasterizer/

PS - Pixel Shader

  • PixelShader: 픽셀 셰이더 실행
  • Texture: 텍스처 샘플링
  • SamplerState: 샘플링 방식
Pipeline/VSPS/

OM - Output Merger

  • RenderTarget: 렌더 타겟 설정
  • DepthStencil: 깊이/스텐실 버퍼
  • BlendState: 블렌딩 설정
Pipeline/OM/

파이프라인 단계별 코드

IA Input Assembler 설정
// Core/RenderingPipeline/Pipeline/IA/VertexBuffer.cpp
void VertexBuffer::SetRenderPipeline() NOEXCEPTRELEASE
{
    const UINT offset = 0u;
    GetDeviceContext()->IASetVertexBuffers(
        0u,                          // 시작 슬롯
        1u,                          // 버퍼 개수
        vertexBuffer.GetAddressOf(), // 버퍼 포인터
        &stride,                     // 정점 크기
        &offset                      // 오프셋
    );
}

// Core/RenderingPipeline/Pipeline/IA/IndexBuffer.cpp
void IndexBuffer::SetRenderPipeline() NOEXCEPTRELEASE
{
    GetDeviceContext()->IASetIndexBuffer(
        indexBuffer.Get(),           // 인덱스 버퍼
        DXGI_FORMAT_R32_UINT,       // 인덱스 포맷 (32비트 unsigned int)
        0u                           // 오프셋
    );
}

// Core/RenderingPipeline/Pipeline/IA/PrimitiveTopology.cpp
void PrimitiveTopology::SetRenderPipeline() NOEXCEPTRELEASE
{
    GetDeviceContext()->IASetPrimitiveTopology(topology);
    // topology 예시: D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST
}
VS Vertex Shader 설정
// Core/RenderingPipeline/Pipeline/VSPS/VertexShader.cpp
void VertexShader::SetRenderPipeline() NOEXCEPTRELEASE
{
    GetDeviceContext()->VSSetShader(
        vertexShader.Get(),     // 정점 셰이더
        nullptr,                // 클래스 인스턴스
        0u                      // 인스턴스 개수
    );
}

// TransformConstantBuffer.cpp - Transform 행렬 업데이트
void TransformConstantBuffer::SetRenderPipeline() NOEXCEPTRELEASE
{
    // Transform 행렬 계산 및 업데이트
    const auto modelView = parent->GetTransformMatrix() * 
                           camera->GetViewMatrix();
    const Transforms tf = {
        DirectX::XMMatrixTranspose(modelView),
        DirectX::XMMatrixTranspose(
            modelView * camera->GetProjectionMatrix()
        )
    };
    
    // ConstantBuffer에 데이터 업데이트
    Update(tf);
    
    // Vertex Shader에 바인딩 (슬롯 0)
    GetDeviceContext()->VSSetConstantBuffers(
        0u, 1u, constantBuffer.GetAddressOf()
    );
}
PS Pixel Shader 및 텍스처 설정
// Core/RenderingPipeline/Pipeline/VSPS/PixelShader.cpp
void PixelShader::SetRenderPipeline() NOEXCEPTRELEASE
{
    GetDeviceContext()->PSSetShader(
        pixelShader.Get(),      // 픽셀 셰이더
        nullptr,                // 클래스 인스턴스
        0u                      // 인스턴스 개수
    );
}

// Core/Draw/Base/Image/Texture.cpp
void Texture::SetRenderPipeline() NOEXCEPTRELEASE
{
    GetDeviceContext()->PSSetShaderResources(
        slot,                   // 텍스처 슬롯 (0, 1, 2...)
        1u,                     // 리소스 개수
        shaderResourceView.GetAddressOf()  // 텍스처 SRV
    );
}

// SamplerState.cpp
void SamplerState::SetRenderPipeline() NOEXCEPTRELEASE
{
    GetDeviceContext()->PSSetSamplers(
        0u,                     // 샘플러 슬롯
        1u,                     // 샘플러 개수
        samplerState.GetAddressOf()  // 샘플러 스테이트
    );
}
OM Output Merger 설정
// RenderTarget.cpp
void RenderTarget::BindAsTarget(const DepthStencil& depthStencil) NOEXCEPTRELEASE
{
    // 렌더 타겟과 깊이 스텐실을 Output Merger 단계에 바인딩
    GetDeviceContext()->OMSetRenderTargets(
        1u,                                      // 렌더 타겟 개수
        renderTargetView.GetAddressOf(),         // 렌더 타겟 뷰
        depthStencil.depthStencilView.Get()      // 깊이 스텐실 뷰
    );
}

// Pipeline/OM/DepthStencil.cpp
void DepthStencilState::SetRenderPipeline() NOEXCEPTRELEASE
{
    GetDeviceContext()->OMSetDepthStencilState(
        depthStencilState.Get(),  // 깊이 스텐실 스테이트
        stencilRef                // 스텐실 참조 값
    );
}

// Pipeline/OM/ColorBlend.cpp
void ColorBlend::SetRenderPipeline() NOEXCEPTRELEASE
{
    GetDeviceContext()->OMSetBlendState(
        blendState.Get(),         // 블렌드 스테이트
        nullptr,                  // 블렌드 팩터
        0xFFFFFFFFu               // 샘플 마스크
    );
}

렌더링 파이프라인 상세 플로우

DoFrame 메소드의 각 단계를 세부적으로 분석하면 다음과 같은 순서로 렌더링이 진행됩니다:

1 BeginFrame - 프레임 초기화

ImGui 프레임을 초기화하고 이전 프레임의 셰이더 리소스를 언바인딩합니다.

// Core/DxGraphic.cpp
void DxGraphic::BeginFrame() noexcept
{
    // ImGui 새 프레임 시작
    if (imGuiEnable)
    {
        ImGui_ImplDX11_NewFrame();
        ImGui_ImplWin32_NewFrame();
        ImGui::NewFrame();
    }
    
    // 이전 프레임의 셰이더 리소스 뷰 초기화 (텍스처 언바인딩)
    ID3D11ShaderResourceView* const nullTexture = nullptr;
    deviceContext->PSSetShaderResources(0, 1, &nullTexture);
    deviceContext->PSSetShaderResources(3, 1, &nullTexture);
}

2-3 시스템 업데이트 - 물리 및 게임 로직

  • PhysicsSystem::Update: NVIDIA PhysX를 통한 물리 시뮬레이션 (충돌 감지, 강체 동역학)
  • Scene::Update: 모든 오브젝트의 Update() 메소드 호출, Transform 계산, 애니메이션 업데이트

4 RenderGraph::Execute - 렌더 패스 실행

등록된 순서대로 모든 RenderPass를 실행하여 화면을 그립니다:

  1. clearRenderTarget & clearDepthStencil - 백버퍼와 깊이 버퍼 초기화
  2. ShadowMapPass - 라이트 시점에서 깊이 정보를 렌더링하여 그림자 맵 생성
  3. LambertianRenderPass - 램버시안 반사 모델로 메인 오브젝트 렌더링 (그림자 맵 사용)
  4. SkyboxPass - 배경 스카이박스 렌더링
  5. OutlineMaskPass - 선택된 오브젝트의 마스크를 스텐실 버퍼에 렌더링
  6. OutlineDrawPass - 아웃라인 초기 데이터 생성
  7. HorizontalBlurPass - 가우시안 블러 커널로 수평 방향 블러 적용
  8. VerticalBlurPass - 수직 방향 블러 적용 및 최종 합성
  9. WireFramePass - 카메라 절두체 와이어프레임 오버레이

5 UI 렌더링 - ImGui 인터페이스

  • CreateSimulationWindow & CreateDemoWindows: 시뮬레이션 제어 창
  • Scene::LateUpdate: 후처리 로직 실행
  • RenderGraph::RenderWindows: 디버그 윈도우 (커널 설정, 섀도우 설정)
  • FolderView, MenuBar, Inspector: 에디터 UI 렌더링

6 EndFrame - 화면 출력

ImGui 렌더링 데이터를 생성하고 백/프론트 버퍼를 교환하여 화면에 출력합니다.

// Core/DxGraphic.cpp
void DxGraphic::EndFrame()
{
    // ImGui는 마지막에 처리를 해야 화면 맨 앞으로 나옴
    if (imGuiEnable)
    {
        ImGui::Render();
        ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
    }
    
    // 백 버퍼와 프론트 버퍼 교환 (화면에 출력)
    // 첫 번째 인자: VSync (0 = 비활성화, 1 = 활성화)
    HRESULT hr = swapChain->Present(0, 0);
    
    if (FAILED(hr))
    {
        if (hr == DXGI_ERROR_DEVICE_REMOVED)
            throw GRAPHIC_REMOVE_EXCEPT(device->GetDeviceRemovedReason());
        else
            throw GRAPHIC_EXCEPT(hr);
    }
}

7 Reset - 다음 프레임 준비

모든 RenderQueuePass의 renderJobs 큐를 비워 다음 프레임을 위한 깨끗한 상태로 초기화합니다.

셰이더 시스템 (HLSL)

본 엔진은 다양한 HLSL 셰이더를 지원하며, 런타임에 D3DX11CompileFromFile로 컴파일하여 사용합니다. 디버그 빌드에서는 디버그 정보를 포함하고 최적화를 생략하여 셰이더 디버깅을 용이하게 합니다.

지원 셰이더 목록 (Shader/ 폴더)

  • ColorShader.hlsl - 단색 렌더링
  • LitColor.hlsl - 조명이 적용된 컬러
  • LitTextureDiffuse.hlsl - 디퓨즈 텍스처 + 조명
  • LitTextureDiffuseNormal.hlsl - 노멀 매핑
  • LitTextureDiffuseSpecular.hlsl - 스페큘러 맵
  • LitTextureDiffuseSpecularNormal.hlsl - 모든 맵
  • ShadowVS/PS.hlsl - 그림자 맵 생성
  • Skybox.hlsl - 스카이박스 렌더링
  • PostProcessing/ - 포스트 프로세싱 셰이더들
// 셰이더 컴파일 및 생성 예시 (DxGraphic.cpp)
ComPtr<ID3D11VertexShader> vertexShader;
ComPtr<ID3DBlob> shaderCode;

DWORD shaderFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
    shaderFlags |= D3D10_SHADER_DEBUG;           // 디버그 정보 포함
    shaderFlags |= D3D10_SHADER_SKIP_OPTIMIZATION; // 최적화 생략
#endif

ID3D10Blob* compiledShader = 0;
ID3D10Blob* errorMessage = 0;

// HLSL 파일 컴파일
HRESULT hr = D3DX11CompileFromFileA(
    "Shader/ColorShader.hlsl",  // 셰이더 파일 경로
    nullptr,                     // 매크로 정의
    nullptr,                     // Include 파일
    "VS",                        // 엔트리 포인트 함수명
    "vs_5_0",                    // 셰이더 모델 (Vertex Shader 5.0)
    shaderFlags,                 // 컴파일 플래그
    0,                           // 효과 플래그
    0,                           // 스레드 펌프
    &shaderCode,                 // 컴파일된 코드
    &errorMessage,               // 에러 메시지
    nullptr                      // 결과
);

// 에러 체크
if (FAILED(hr))
{
    if (errorMessage != 0)
    {
        MessageBoxA(0, (char*)errorMessage->GetBufferPointer(), 0, 0);
        ReleaseCOM(errorMessage);
    }
    DXTrace(__FILE__, (DWORD)__LINE__, hr, "D3DX11CompileFromFile", true);
}

// VertexShader 객체 생성
GRAPHIC_THROW_INFO(device->CreateVertexShader(
    shaderCode->GetBufferPointer(),  // 바이트코드
    shaderCode->GetBufferSize(),     // 바이트코드 크기
    nullptr,                         // 클래스 링커
    &vertexShader                    // 생성된 셰이더
));

// Vertex Shader 단계를 렌더링 파이프라인에 바인딩
deviceContext->VSSetShader(vertexShader.Get(), nullptr, 0);

HLSL 셰이더 코드

본 엔진에서 실제로 사용하는 LitTextureDiffuse.hlsl 셰이더는 디퓨즈 텍스처, 조명 계산, 그림자 매핑을 지원합니다. 셰이더 헤더 파일들을 include하여 재사용 가능한 구조로 설계되었습니다.

// Shader/LitTextureDiffuse.hlsl
#include "ShaderHeader/PointLight.hlsli"
#include "ShaderHeader/LightShader.hlsli"
#include "ShaderHeader/LightVector.hlsli"
#include "ShaderHeader/Transform.hlsli"
#include "ShaderHeader/ShadowVS.hlsli"
#include "ShaderHeader/ShadowStaticPS.hlsli"

// Vertex Shader 출력 구조체
struct VertexOut
{
    float3 viewPosition : Position;
    float3 viewNormal : Normal;
    float2 textureCoord : TEXCOORD;
    float4 shadowHomoPosition : ShadowPosition;
    float4 position : SV_Position;
};    

// 오브젝트별 재질 속성
cbuffer ObjectColor : register(b1)
{
    float3 specularColor;   // Specular 색상
    float specularPower;    // Specular 강도
    float specularGlass;    // Specular 반짝임 정도
};

Texture2D tex : register(t0);
SamplerState state : register(s0);

// Vertex Shader
VertexOut VS(float3 localPosition : Position, float3 normal : Normal, float2 textureCoord : TEXCOORD)
{
    VertexOut vertexOut;
    
    // 로컬 좌표를 뷰 좌표로 변환
    vertexOut.viewPosition = (float3) mul(float4(localPosition, 1.0f), cameraTransform);
    vertexOut.viewNormal = mul(normal, (float3x3) cameraTransform);
    vertexOut.position = mul(float4(localPosition, 1.0f), worldViewProjection);
    
    vertexOut.textureCoord = textureCoord;
    vertexOut.shadowHomoPosition = GetShadowHomoSpace(localPosition, model);
    
    return vertexOut;
}

// Pixel Shader
float4 PS(float3 viewPosition : Position, float3 viewNormal : Normal, 
          float2 textureCoord : TEXCOORD, float4 shadowPosition : ShadowPosition) : SV_Target
{
    float3 diffuse;
    float3 specular;
    
    // 그림자 체크
    const float shadow = GetShadow(shadowPosition);
    if (shadow != 0.0f)
    {
        viewNormal = normalize(viewNormal);
        
        // 조명 벡터 계산
        const LightVector lightVector = GetLightVector(lightViewPosition, viewPosition);
        
        // 거리 감쇠 계산
        const float attResult = GetAttenuate(attConst, attLin, attQuad, lightVector.distance);
        
        // Diffuse 조명 계산
        diffuse = GetDiffuse(diffuseColor, diffuseIntensity, attResult, 
                            lightVector.vertexToLightDir, viewNormal);
        
        // Specular 조명 계산
        specular = GetSpecular(diffuseColor * diffuseIntensity * specularColor, 
                               specularPower, viewNormal, lightVector.vertexToLight, 
                               viewPosition, attResult, specularGlass);
        
        // 그림자 적용
        diffuse *= shadow;
        specular *= shadow;
    }
    else
        diffuse = specular = 0.0f;
    
    // 텍스처 샘플링 및 최종 색상 계산
    return float4(saturate((diffuse + ambient) * tex.Sample(state, textureCoord).rgb + specular), 1.0f);
}
셰이더 헤더 파일 구조
// ShaderHeader/Transform.hlsli - 변환 행렬
cbuffer ObjectTransform : register(b0)
{
    matrix model;                  // 모델 행렬
    matrix cameraTransform;        // 카메라 변환 행렬
    matrix worldViewProjection;    // 월드-뷰-프로젝션 행렬
};

// ShaderHeader/PointLight.hlsli - 조명 정보
cbuffer LightInfoConstant : register(b0)
{
    float3 lightViewPosition;   // 조명 위치
    float3 ambient;             // 앰비언트 조명
    float3 diffuseColor;        // 디퓨즈 색상
    float diffuseIntensity;     // 디퓨즈 강도
    float attConst;             // 감쇠 상수
    float attLin;               // 선형 감쇠
    float attQuad;              // 제곱 감쇠
};

// ShaderHeader/LightShader.hlsli - 조명 계산 함수들
float GetAttenuate(float attConst, float attLin, float attQuad, float distance)
{
    return 1.0f / (attConst + attLin * distance + attQuad * (distance * distance));
}

float3 GetDiffuse(float3 diffuseColor, float diffuseIntensity, float attResult, 
                  float3 vertexToLightDir, float3 viewNormal)
{
    return diffuseColor * diffuseIntensity * attResult * 
           max(0.0f, dot(vertexToLightDir, viewNormal));
}

float3 GetSpecular(float3 diffuseColor, float specularPower, float3 viewNormal, 
                   float3 vertexToLight, float3 viewPosition, float attResult, float specularGlass)
{
    // 빛이 반사되는 벡터 계산
    const float3 reflectAngle = viewNormal * dot(vertexToLight, viewNormal);
    const float3 reflectVector = normalize(reflectAngle * 2.0f - vertexToLight);
    
    // 반사광 강도 계산
    return attResult * diffuseColor * 
           pow(max(0.0f, dot(-reflectVector, normalize(viewPosition))), specularPower);
}

설계 특징: 셰이더 헤더 파일(*.hlsli)을 사용하여 조명 계산, 변환, 그림자 등의 공통 기능을 재사용하며, 이를 통해 코드 중복을 줄이고 유지보수성을 향상시켰습니다. 엔진은 LitTextureDiffuse 외에도 Normal Mapping, Specular Mapping 등을 지원하는 다양한 셰이더를 제공합니다.

주요 최적화 기법: Frustum Culling

카메라의 시야 절두체(View Frustum) 외부에 있는 객체는 렌더링하지 않음으로써 성능을 크게 향상시킵니다. 이는 RenderJob::Execute()에서 각 객체의 Bounding Volume이 절두체 내부에 있는지 체크하여 구현됩니다.

Frustum Culling 동작 원리

  1. 절두체 평면 계산: 카메라의 View/Projection 행렬로부터 6개의 절두체 평면 추출
  2. AABB 교차 테스트: 각 객체의 Axis-Aligned Bounding Box가 절두체 내부에 있는지 체크
  3. 컬링 결정: 외부 객체는 DrawIndexed 호출 생략하여 GPU 부하 감소
// Core/Camera/CameraViewFrustumCulling.cpp
// View/Projection 행렬로 절두체 업데이트
void CameraViewFrustumCulling::UpdateFromMatrices(
    const DirectX::XMMATRIX& viewMatrix, 
    const DirectX::XMMATRIX& projMatrix)
{
    // View * Projection 행렬 계산
    DirectX::XMMATRIX viewProj = viewMatrix * projMatrix;
    
    // 6개의 절두체 평면 추출 (Near, Far, Left, Right, Top, Bottom)
    // 각 평면은 4D 벡터로 표현 (ax + by + cz + d = 0)
    
    // Left 평면 = 4번째 행 + 1번째 행
    frustumPlanes[0] = DirectX::XMPlaneNormalize(
        DirectX::XMVectorAdd(viewProj.r[3], viewProj.r[0])
    );
    
    // Right 평면 = 4번째 행 - 1번째 행
    frustumPlanes[1] = DirectX::XMPlaneNormalize(
        DirectX::XMVectorSubtract(viewProj.r[3], viewProj.r[0])
    );
    
    // Bottom 평면 = 4번째 행 + 2번째 행
    frustumPlanes[2] = DirectX::XMPlaneNormalize(
        DirectX::XMVectorAdd(viewProj.r[3], viewProj.r[1])
    );
    
    // Top 평면 = 4번째 행 - 2번째 행
    frustumPlanes[3] = DirectX::XMPlaneNormalize(
        DirectX::XMVectorSubtract(viewProj.r[3], viewProj.r[1])
    );
    
    // Near 평면 = 3번째 행
    frustumPlanes[4] = DirectX::XMPlaneNormalize(viewProj.r[2]);
    
    // Far 평면 = 4번째 행 - 3번째 행
    frustumPlanes[5] = DirectX::XMPlaneNormalize(
        DirectX::XMVectorSubtract(viewProj.r[3], viewProj.r[2])
    );
}

// AABB(Axis-Aligned Bounding Box)가 절두체 내부에 있는지 확인
bool CameraViewFrustumCulling::IsInFrustum(const BoundingBox& bbox) const
{
    DirectX::XMVECTOR center = DirectX::XMLoadFloat3(&bbox.center);
    DirectX::XMVECTOR extents = DirectX::XMLoadFloat3(&bbox.extents);
    
    // 6개의 평면 모두에 대해 체크
    for (int i = 0; i < 6; i++)
    {
        // 평면의 노멀 방향으로 가장 먼 점 계산
        DirectX::XMVECTOR planeNormal = frustumPlanes[i];
        
        // AABB의 중심에서 평면까지의 거리 계산
        float distance = DirectX::XMVectorGetX(
            DirectX::XMPlaneDotCoord(planeNormal, center)
        );
        
        // AABB의 반경 계산 (평면 방향으로의 최대 거리)
        float radius = DirectX::XMVectorGetX(
            DirectX::XMVector3Dot(
                DirectX::XMVectorAbs(planeNormal), 
                extents
            )
        );
        
        // 평면 밖에 있으면 렌더링 불필요
        if (distance < -radius)
            return false;
    }
    
    // 모든 평면을 통과하면 절두체 내부에 있음
    return true;
}

// RenderJob에서 사용 예시 (RenderJob.cpp)
void RenderJob::Excute(bool isPassFrustumCulling) NOEXCEPTRELEASE
{
    // 절두체 컬링 체크
    if (isPassFrustumCulling && useViewFrustum && !IsInViewFrustum())
    {
        // 절두체 밖에 있으면 렌더링 건너뛰기
        return; // ← 성능 향상! DrawIndexed 호출 안 함
    }
    
    // 절두체 내부에 있는 객체만 렌더링
    drawable->SetRenderPipeline();
    renderStep->SetRenderPipeline();
    Window::GetDxGraphic().DrawIndexed(drawable->GetIndexCount());
}

물리 시스템 (NVIDIA PhysX)

본 엔진은 NVIDIA PhysX를 통합하여 실시간 물리 시뮬레이션을 제공합니다. PhysicsSystem은 싱글톤 패턴으로 구현되어 전역적으로 접근 가능하며, 고정 시간 간격(Fixed Timestep) 업데이트로 안정적인 물리 계산을 보장합니다.

PhysicsSystem 구조

// Core/Component/Physics/PhysicsSystem.h
class PhysicsSystem
{
public:
    // 싱글톤 인스턴스
    static PhysicsSystem& GetInstance();
    
    // 시스템 생명주기
    bool Initialize();
    void Shutdown();
    void Update(float deltaTime);
    
    // PhysX 객체 접근
    physx::PxPhysics* GetPhysics() const { return physics; }
    physx::PxScene* GetScene() const { return scene; }
    
    // 메시 콜라이더 생성
    physx::PxTriangleMesh* CreateTriangleMeshCollider(
        const std::vector<physx::PxVec3>& vertices,
        const std::vector<uint32_t>& indices);
    
    physx::PxConvexMesh* CreateConvexMeshCollider(
        const std::vector<physx::PxVec3>& vertices,
        bool autoGenerateHull = true,
        uint32_t vertexLimit = 256);
    
private:
    // PhysX 핵심 객체들
    physx::PxFoundation* foundation = nullptr;
    physx::PxPhysics* physics = nullptr;
    physx::PxDefaultCpuDispatcher* dispatcher = nullptr;
    physx::PxScene* scene = nullptr;
    physx::PxPvd* pvd = nullptr;  // PhysX Visual Debugger
    
    float timeAccumulator = 0.0f;  // 고정 시간 간격 시뮬레이션용
};

초기화 및 업데이트 과정

// PhysicsSystem 초기화
bool PhysicsSystem::Initialize()
{
    // 1) Foundation 생성 (PhysX 기반)
    foundation = PxCreateFoundation(PX_PHYSICS_VERSION, allocator, errorCallback);
    
    // 2) Physics 객체 생성
    physics = PxCreatePhysics(PX_PHYSICS_VERSION, *foundation, PxTolerancesScale());
    
    // 3) CPU Dispatcher 생성 (멀티스레딩, 2개 스레드)
    dispatcher = PxDefaultCpuDispatcherCreate(2);
    
    // 4) Scene 생성
    PxSceneDesc sceneDesc(physics->getTolerancesScale());
    sceneDesc.gravity = PxVec3(0.0f, -9.81f, 0.0f);  // 중력 설정
    sceneDesc.cpuDispatcher = dispatcher;
    sceneDesc.filterShader = PxDefaultSimulationFilterShader;
    
    scene = physics->createScene(sceneDesc);
    
    return true;
}

// 고정 시간 간격 업데이트 (60 FPS)
void PhysicsSystem::Update(float deltaTime)
{
    const float fixedTimestep = 1.0f / 60.0f;  // 60 FPS
    
    timeAccumulator += deltaTime;
    
    // 고정 시간 간격으로 여러 번 시뮬레이션
    while (timeAccumulator >= fixedTimestep)
    {
        scene->simulate(fixedTimestep);
        scene->fetchResults(true);  // 결과 대기
        
        timeAccumulator -= fixedTimestep;
    }
}

입력 시스템 (Keyboard & Mouse)

본 엔진은 키보드와 마우스 입력을 이벤트 기반으로 처리합니다. 이벤트 큐 방식과 상태 확인 방식을 모두 지원하여 다양한 입력 처리 패턴을 구현할 수 있습니다.

Keyboard 클래스

주요 기능
  • 이벤트 큐: ReadKey() - Press/Release 이벤트
  • 상태 확인: IsPressed() - 실시간 키 상태
  • 문자 입력: ReadChar() - 텍스트 입력용
  • Autorepeat: 키 반복 입력 제어
// 이벤트 큐 방식
while (const auto key = wnd.keyBoard.ReadKey())
{
    if (key->IsPress())
    {
        switch (key->GetCode())
        {
        case VK_ESCAPE:
            // ESC 키 처리
            break;
        }
    }
}

// 실시간 상태 확인
if (wnd.keyBoard.IsPressed('W'))
    camera->MoveForward(deltaTime);

Mouse 클래스

주요 기능
  • 위치 추적: GetPos() - 마우스 좌표
  • 버튼 상태: LeftIsPressed(), RightIsPressed()
  • Raw Input: ReadRawDelta() - FPS 카메라용
  • 휠 스크롤: ReadWheel() - 줌 제어
// 마우스 이동으로 카메라 회전
while (const auto delta = wnd.mouse.ReadRawDelta())
{
    camera->Rotate(
        delta->x * sensitivity, 
        delta->y * sensitivity
    );
}

// 마우스 버튼
if (wnd.mouse.LeftIsPressed())
{
    // 클릭 처리
}

Material 시스템

Material은 3D 모델의 외형을 결정하는 재질 정보를 관리합니다. Assimp를 통해 모델 파일에서 자동으로 텍스처와 속성을 로드하며, 사용 가능한 텍스처 맵에 따라 적절한 셰이더 Technique를 자동으로 선택합니다.

Assimp에서 Material 로드

// Core/Draw/Base/Material.cpp
Material::Material(const aiMaterial& material, const std::filesystem::path& path)
{
    // 1) Diffuse 텍스처 로드 (기본 색상)
    aiString texturePath;
    if (material.GetTexture(aiTextureType_DIFFUSE, 0, &texturePath) == AI_SUCCESS)
    {
        std::string fullPath = path.parent_path().string() + "\\" + texturePath.C_Str();
        diffuseTexture = Texture::Create(fullPath);
    }
    
    // 2) Normal Map 로드 (노멀 매핑)
    if (material.GetTexture(aiTextureType_NORMALS, 0, &texturePath) == AI_SUCCESS)
    {
        std::string fullPath = path.parent_path().string() + "\\" + texturePath.C_Str();
        normalMap = Texture::Create(fullPath);
    }
    
    // 3) Specular Map 로드 (반사광)
    if (material.GetTexture(aiTextureType_SPECULAR, 0, &texturePath) == AI_SUCCESS)
    {
        std::string fullPath = path.parent_path().string() + "\\" + texturePath.C_Str();
        specularMap = Texture::Create(fullPath);
    }
    
    // 4) Technique 자동 선택 (사용 가능한 텍스처에 따라)
    if (diffuseTexture && normalMap && specularMap)
    {
        // 모든 맵을 사용하는 셰이더
        techniques.push_back(Technique("LitTextureDiffuseSpecularNormal", channel));
    }
    else if (diffuseTexture && normalMap)
    {
        techniques.push_back(Technique("LitTextureDiffuseNormal", channel));
    }
    else if (diffuseTexture)
    {
        techniques.push_back(Technique("LitTextureDiffuse", channel));
    }
}
지원하는 Material 속성
텍스처 맵
  • Diffuse Texture
  • Normal Map
  • Specular Map
재질 속성
  • Ambient Color
  • Emissive Color
  • Shininess

Object와 Component 시스템

본 엔진은 Unity와 유사한 Entity-Component 패턴을 채택하여 게임 오브젝트를 구성합니다. Object는 게임 세계의 엔티티를 나타내며, Component는 Object에 기능을 추가하는 모듈입니다. C++20 Concept을 활용한 타입 안전성과 스마트 포인터를 통한 자동 메모리 관리로 안정적인 구조를 제공합니다.

EngineLoop 인터페이스 - 통일된 생명주기

모든 Object와 Component는 EngineLoop 인터페이스를 구현하여 일관된 생명주기를 가집니다.

// Core/Object/EngineLoop.h
class EngineLoop
{
public:
    virtual void Initialize() = 0;         // 초기화 (리소스 로드 등)
    virtual void BeforeFrame() = 0;        // 첫 프레임 전 실행
    virtual void Start() = 0;              // 게임 시작 시 1회 실행
    virtual void LateStart() = 0;          // Start 이후 1회 실행
    virtual void Update(float deltaTime) = 0;  // 매 프레임 업데이트
    virtual void LateUpdate() = 0;         // Update 이후 실행
    virtual void Finalize() = 0;           // 종료 전 정리
    virtual void Destroy() = 0;            // 객체 파괴 시 실행
    
    virtual void OnEnable() = 0;           // 활성화 시
    virtual void OnDisable() = 0;          // 비활성화 시
    
    virtual void Reset() = 0;              // 리셋
    
    virtual ~EngineLoop() = default;
};
생명주기 순서
  1. Initialize() - 객체 생성 직후
  2. BeforeFrame() - 첫 프레임 시작 전
  3. Start() - 게임 시작 시 1회
  4. LateStart() - Start 직후 1회
  5. Update(deltaTime) - 매 프레임 (게임 로직)
  6. LateUpdate() - Update 이후 (카메라, 후처리 등)
  7. Finalize() - 씬 종료 시
  8. Destroy() - 객체 파괴 시

Object 클래스 - Component 컨테이너

Object는 게임 세계의 기본 엔티티로, Component들의 컨테이너 역할을 합니다. 모든 Object는 기본적으로 TransformComponent를 가지며, shared_ptr로 안전하게 참조됩니다.

// C++20 Concept으로 타입 안전성 보장
template<typename T>
concept ComponentChild = std::is_base_of_v<Component, T>;

class Object : public EngineLoop, public std::enable_shared_from_this<Object>
{
public:
    // Object 생성 팩토리 메소드
    static std::shared_ptr<Object> Create(std::string name)
    {
        std::shared_ptr<Object> newObject = std::make_shared<Object>(name);
        
        // 모든 Object는 TransformComponent를 기본으로 가짐
        auto transformComponent = std::make_shared<TransformComponent>(newObject);
        newObject->components[transformComponent->GetClassName()] = transformComponent;
        newObject->transform = transformComponent;
        
        return newObject;
    }
    
    // Component 추가 (템플릿, 타입 안전성 보장)
    template <ComponentChild ComponentClass, typename... Args>
    std::shared_ptr<ComponentClass> AddComponent(Args&&... args)
    {
        // TransformComponent 중복 방지
        if constexpr (std::is_same_v<ComponentClass, TransformComponent>)
            return std::dynamic_pointer_cast<ComponentClass>(transform);
        
        // 이미 존재하는 컴포넌트는 재사용
        std::string componentName = ComponentClass::GetStaticClassName();
        if (components.find(componentName) != components.end())
            return std::dynamic_pointer_cast<ComponentClass>(components[componentName]);
        
        // 새 컴포넌트 생성
        auto component = std::make_shared<ComponentClass>(
            shared_from_this(), std::forward<Args>(args)...
        );
        
        component->SetObject(shared_from_this());
        component->transform = this->transform;
        components[component->GetClassName()] = component;
        
        return component;
    }
    
    // Component 가져오기
    template <ComponentChild ComponentClass>
    std::shared_ptr<ComponentClass> GetComponent()
    {
        for (auto& component : components)
        {
            if (component.second->GetClassName() == ComponentClass::GetStaticClassName())
                return std::dynamic_pointer_cast<ComponentClass>(component.second);
        }
        return nullptr;
    }
    
    std::shared_ptr<TransformComponent> transform;  // 항상 존재
    
protected:
    std::unordered_map<std::string, std::shared_ptr<Component>> components;
    std::string name;
    bool isActive = true;
};

TransformComponent - 핵심 Component

모든 Object가 가지는 필수 Component로, 3D 공간에서의 위치/회전/크기를 관리합니다. 쿼터니언 기반 회전으로 짐벌락을 방지하고, 부모-자식 계층 구조를 지원합니다.

월드 좌표계
  • SetPosition() / GetPosition()
  • SetRotation() / GetRotation()
  • SetScale() / GetScale()
  • GetRight(), GetUp(), GetForward()
로컬 좌표계
  • SetLocalPosition() / GetLocalPosition()
  • SetLocalRotation() / GetLocalRotation()
  • SetLocalScale() / GetLocalScale()
  • 부모 기준 상대 좌표
계층 구조
  • SetParent() / RemoveParent()
  • AddChild() / RemoveChild()
  • GetChildrens() / GetChildCount()
  • UpdateTransform() - 계층 업데이트
// Transform 구조체 (쿼터니언 기반)
struct Transform
{
    Position position;      // 위치 (Vector3)
    Quaternion rotation;    // 회전 (쿼터니언)
    Scale scale;           // 크기 (Vector3)
    
    // SRT 행렬 생성 (Scale -> Rotation -> Translation)
    DirectX::XMMATRIX GetMatrix() const
    {
        DirectX::XMMATRIX scaleMatrix = DirectX::XMMatrixScaling(
            scale.x, scale.y, scale.z
        );
        
        DirectX::XMMATRIX rotationMatrix = 
            DirectX::XMMatrixRotationQuaternion(
                DirectX::XMLoadFloat4(&rotation)
            );
        
        DirectX::XMMATRIX translationMatrix = 
            DirectX::XMMatrixTranslation(
                position.x, position.y, position.z
            );
        
        return scaleMatrix * rotationMatrix * translationMatrix;
    }
};
TransformComponent 핵심 구현
1. Transform 구조체 - 기본 구조

Transform 구조체는 위치, 회전(쿼터니언), 크기를 관리하며 다양한 변환 행렬을 생성합니다.

// Core/Component/Transform/Transform.h
struct Transform final
{
    Position position;      // 위치 (Vector3)
    Quaternion rotation;    // 회전 (쿼터니언)
    Scale scale;           // 크기 (Vector3)
    
    // 생성자 - Identity Transform으로 초기화
    Transform() 
        : position(Vector3::zero),           // (0,0,0)
          rotation(Quaternion::identity),    // 회전 없음
          scale(Vector3::one)                // (1,1,1)
    {
    }
    
    // SRT 변환 행렬 생성 (Scale * Rotation * Translation)
    XMMATRIX GetTransformMatrix() const
    {
        // 1. 스케일 행렬
        XMMATRIX scaleMatrix = XMMatrixScaling(scale.x, scale.y, scale.z);
        
        // 2. 회전 행렬 (쿼터니언 → 행렬)
        XMVECTOR rotQuat = XMVectorSet(rotation.x, rotation.y, rotation.z, rotation.w);
        XMMATRIX rotationMatrix = XMMatrixRotationQuaternion(rotQuat);
        
        // 3. 이동 행렬
        XMMATRIX translationMatrix = XMMatrixTranslation(
            position.x, position.y, position.z
        );
        
        // SRT 순서로 행렬 곱셈 (오른쪽에서 왼쪽으로 적용됨)
        return scaleMatrix * rotationMatrix * translationMatrix;
    }
};
2. GetTransformMatrix() - 계층 구조 고려한 최종 행렬 계산

부모가 있으면 로컬 행렬 × 부모 월드 행렬로 계층 구조를 적용합니다.

// Core/Component/Transform/TransformComponent.cpp
DirectX::XMMATRIX TransformComponent::GetTransformMatrix() const noexcept
{
    if (HasParent())
    {
        // 부모가 있는 경우: 로컬 행렬 × 부모 월드 행렬
        XMMATRIX localMatrix = GetLocalTransformMatrix();
        XMMATRIX parentMatrix = parent->GetTransformMatrix();
        
        return localMatrix * parentMatrix;  // 행렬 곱셈으로 변환 결합
    }
    else
    {
        // 부모가 없는 경우: 월드 Transform으로 직접 행렬 생성
        const Quaternion& rot = worldTransform.GetRotation();
        XMVECTOR rotQuat = XMVectorSet(rot.x, rot.y, rot.z, rot.w);
        
        // SRT 순서로 행렬 생성
        return
            XMMatrixScaling(worldTransform.GetScale().x, worldTransform.GetScale().y, worldTransform.GetScale().z) *
            XMMatrixRotationQuaternion(rotQuat) *
            XMMatrixTranslation(worldTransform.GetPosition().x, worldTransform.GetPosition().y, worldTransform.GetPosition().z);
    }
}
3. SetPosition() vs SetLocalPosition() - 월드/로컬 좌표 변환

SetPosition()은 월드 절대 좌표를 설정하고 로컬 좌표를 역계산하며, SetLocalPosition()은 로컬 좌표를 설정하고 월드 좌표를 계산합니다.

// 월드 위치 설정 (절대 좌표)
void TransformComponent::SetPosition(Position position) noexcept
{
    worldTransform.SetPosition(position);
    
    if (HasParent())
    {
        // 부모가 있으면: 로컬 위치 = inv(부모 월드 행렬) × 월드 위치
        XMVECTOR worldPosVec = Vector::ConvertXMVECTOR(position);
        XMMATRIX parentWorldToLocalMatrix = XMMatrixInverse(nullptr, parent->GetTransformMatrix());
        XMVECTOR localPosVec = XMVector3TransformCoord(worldPosVec, parentWorldToLocalMatrix);
        
        XMFLOAT3 localPosFloat3;
        XMStoreFloat3(&localPosFloat3, localPosVec);
        localTransform.SetPosition(localPosFloat3);
    }
    else
    {
        // 부모가 없으면: 로컬 = 월드
        localTransform.SetPosition(position);
    }
}

// 로컬 위치 설정 (부모 기준 상대 좌표)
void TransformComponent::SetLocalPosition(Position position) noexcept
{
    localTransform.SetPosition(position);
    
    if (HasParent())
    {
        // 부모가 있으면: 월드 위치 = 부모 월드 행렬 × 로컬 위치
        XMVECTOR localPosVec = Vector::ConvertXMVECTOR(position);
        XMMATRIX parentWorldMatrix = parent->GetTransformMatrix();
        XMVECTOR worldPosVec = XMVector3TransformCoord(localPosVec, parentWorldMatrix);
        
        XMFLOAT3 worldPosFloat3;
        XMStoreFloat3(&worldPosFloat3, worldPosVec);
        worldTransform.SetPosition(worldPosFloat3);
    }
    else
    {
        // 부모가 없으면: 월드 = 로컬
        worldTransform.SetPosition(position);
    }
}

핵심 포인트: XMVector3TransformCoord는 점(위치) 변환에 사용되며 w=1로 처리합니다. 벡터 변환에는 XMVector3TransformNormal을 사용합니다.

4. UpdateWorldRotation() / UpdateLocalRotation() - 쿼터니언 회전 동기화

월드 회전과 로컬 회전을 쿼터니언 곱셈으로 동기화합니다. SetRotation() 호출 시 자동으로 실행됩니다.

// 로컬 회전 변경 → 월드 회전 계산
void TransformComponent::UpdateWorldRotation() noexcept
{
    if (HasParent())
    {
        // 월드 회전 = 부모 월드 회전 × 로컬 회전
        const Quaternion& localRot = localTransform.GetRotation();
        const Quaternion& parentRot = parent->GetRotation();
        
        XMVECTOR localRotQ = XMVectorSet(localRot.x, localRot.y, localRot.z, localRot.w);
        XMVECTOR parentRotQ = XMVectorSet(parentRot.x, parentRot.y, parentRot.z, parentRot.w);
        
        // 쿼터니언 곱셈으로 회전 결합 (순서 중요: Parent * Local)
        XMVECTOR worldRotQ = XMQuaternionMultiply(parentRotQ, localRotQ);
        worldRotQ = XMQuaternionNormalize(worldRotQ);  // 정규화로 수치 오차 방지
        
        Quaternion& worldQuat = worldTransform.GetRotation();
        worldQuat.x = XMVectorGetX(worldRotQ);
        worldQuat.y = XMVectorGetY(worldRotQ);
        worldQuat.z = XMVectorGetZ(worldRotQ);
        worldQuat.w = XMVectorGetW(worldRotQ);
    }
    else
    {
        // 부모가 없으면: 로컬 회전 = 월드 회전
        worldTransform.SetRotation(localTransform.GetRotation());
    }
}

// 월드 회전 변경 → 로컬 회전 계산
void TransformComponent::UpdateLocalRotation() noexcept
{
    if (HasParent())
    {
        // 로컬 회전 = 부모 월드 회전의 역 × 월드 회전
        const Quaternion& worldRot = worldTransform.GetRotation();
        const Quaternion& parentRot = parent->GetRotation();
        
        XMVECTOR worldRotQ = XMVectorSet(worldRot.x, worldRot.y, worldRot.z, worldRot.w);
        XMVECTOR parentRotQ = XMVectorSet(parentRot.x, parentRot.y, parentRot.z, parentRot.w);
        XMVECTOR parentRotInvQ = XMQuaternionInverse(parentRotQ);  // 부모 회전의 역원
        
        // 로컬 회전 계산 (부모 회전 제거)
        XMVECTOR localRotQ = XMQuaternionMultiply(parentRotInvQ, worldRotQ);
        localRotQ = XMQuaternionNormalize(localRotQ);
        
        Quaternion& localQuat = localTransform.GetRotation();
        localQuat.x = XMVectorGetX(localRotQ);
        localQuat.y = XMVectorGetY(localRotQ);
        localQuat.z = XMVectorGetZ(localRotQ);
        localQuat.w = XMVectorGetW(localRotQ);
    }
    else
    {
        // 부모가 없으면: 월드 회전 = 로컬 회전
        localTransform.SetRotation(worldTransform.GetRotation());
    }
}

쿼터니언 곱셈 순서: XMQuaternionMultiply(A, B)는 "B 회전 후 A 회전"을 의미합니다. 따라서 월드 회전 = Parent × Local 순서로 곱해야 합니다.

5. UpdateTransform() - 계층 구조 전체 재귀 업데이트

부모의 Transform이 변경되면 모든 자식의 월드 Transform을 재귀적으로 업데이트합니다.

void TransformComponent::UpdateTransform() noexcept
{
    if (HasParent())
    {
        // 1. 위치 업데이트: 로컬 위치를 부모의 월드 변환으로 변환
        XMVECTOR localPosVec = Vector::ConvertXMVECTOR(localTransform.GetPosition());
        XMMATRIX parentWorldMatrix = parent->GetTransformMatrix();
        XMVECTOR worldPosVec = XMVector3TransformCoord(localPosVec, parentWorldMatrix);
        
        Position& worldPos = worldTransform.GetPosition();
        worldPos.x = XMVectorGetX(worldPosVec);
        worldPos.y = XMVectorGetY(worldPosVec);
        worldPos.z = XMVectorGetZ(worldPosVec);
        
        // 2. 회전 업데이트: 쿼터니언 곱셈으로 부모 회전과 로컬 회전 결합
        UpdateWorldRotation();
        
        // 3. 스케일 업데이트: 부모 스케일과 로컬 스케일 곱셈
        const Scale& parentScale = parent->GetScale();
        const Scale& localScale = localTransform.GetScale();
        worldTransform.SetScale(
            localScale.x * parentScale.x,
            localScale.y * parentScale.y,
            localScale.z * parentScale.z
        );
    }
    else
    {
        // 부모가 없으면: 로컬 변환 = 월드 변환
        worldTransform = localTransform;
    }
    
    // 4. 모든 자식도 재귀적으로 업데이트
    for (const auto& child : children)
    {
        if (child != nullptr)
            child->UpdateTransform();
    }
}
6. SetParent() - 부모 설정 및 계층 구조 관리

부모를 설정하면 현재 월드 좌표를 유지하면서 로컬 좌표를 재계산합니다.

void TransformComponent::SetParent(std::shared_ptr<TransformComponent> newParent) noexcept
{
    // 1. 기존 부모에서 자신을 제거
    if (parent)
        parent->RemoveChild(shared_from_this());
    
    parent = newParent;
    
    if (parent)
    {
        // 2. 새 부모의 자식으로 추가
        parent->AddChild(shared_from_this());
        
        // 3. 현재 월드 좌표를 유지하면서 로컬 좌표 재계산
        // (부모가 바뀌어도 화면상 위치는 변하지 않음)
        SetPosition(worldTransform.GetPosition());  // 위치 로컬 재계산
        SetRotation(worldTransform.GetRotation());  // 회전 로컬 재계산
        SetScale(worldTransform.GetScale());        // 스케일 로컬 재계산
    }
    else
    {
        // 부모가 제거되면 로컬 = 월드
        localTransform = worldTransform;
    }
}

// RemoveParent - 부모 관계 해제
void TransformComponent::RemoveParent() noexcept
{
    if (parent == nullptr)
        return;
    
    // 부모 제거 전에 현재 월드 변환을 로컬 변환으로 설정
    // (부모가 사라지면 현재 월드 상태가 새로운 로컬 상태가 됨)
    worldTransform.SetPosition(GetPosition());
    worldTransform.SetRotation(GetRotation());
    worldTransform.SetScale(GetScale());
    localTransform = worldTransform;
    
    parent->RemoveChild(shared_from_this());
    parent = nullptr;
}

중요: SetPosition/SetRotation/SetScale은 내부적으로 월드 좌표를 설정하고 로컬 좌표를 자동 계산합니다. 따라서 부모를 변경해도 화면상 위치는 변하지 않습니다.

Component 기본 클래스 구조

모든 Component는 공통 인터페이스를 상속하며, Object에 대한 참조와 Transform 캐시를 보유합니다.

// Core/Component/Component.h
class Component : public EngineLoop
{
public:
    // Component가 속한 Object
    std::shared_ptr<Object> GetObject() const { return object.lock(); }
    void SetObject(std::shared_ptr<Object> obj) { object = obj; }
    
    // Transform 빠른 접근용 캐시
    std::shared_ptr<TransformComponent> transform;
    
    // Component 타입 식별
    virtual std::string GetClassName() const = 0;
    static std::string GetStaticClassName() { return "Component"; }
    
    // 활성화 상태
    bool IsActive() const { return isActive; }
    void SetActive(bool active) 
    { 
        if (active != isActive)
        {
            isActive = active;
            if (active) OnEnable();
            else OnDisable();
        }
    }
    
protected:
    std::weak_ptr<Object> object;  // 순환 참조 방지 (weak_ptr)
    bool isActive = true;
};

UI 시스템 (ImGui)

본 엔진은 Dear ImGui를 사용하여 개발자 도구 UI를 제공합니다. Inspector, FolderView, MenuBar 등의 에디터 인터페이스를 통해 실시간으로 Object와 Component를 편집하고, 씬 계층 구조를 관리할 수 있습니다.

Inspector (속성 편집기)

선택한 Object의 Component들을 표시하고 실시간으로 편집합니다.

  • Transform 위치/회전/크기 실시간 편집
  • Component 목록 표시
  • 각 Component의 속성 표시/수정
  • Component 추가/제거

FolderView (씬 계층 구조)

Scene의 모든 Object를 계층 구조로 시각화합니다.

  • Scene의 Object 목록
  • 부모-자식 계층 시각화
  • Object 선택 → Inspector 업데이트
  • Object 추가/삭제

MenuBar (상단 메뉴)

주요 기능에 빠르게 접근할 수 있는 메뉴를 제공합니다.

  • File: New/Open/Save Scene, Exit
  • Edit: Undo/Redo, Preferences
  • GameObject: Create Empty, 3D Object, Light
  • Window: Inspector, Hierarchy, Demo Window

ImGui 통합 코드

// Window 초기화 시 ImGui 초기화
Window::Window(int width, int height, const char* name)
{
    // ... 윈도우 생성
    
    // ImGui 초기화
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO& io = ImGui::GetIO();
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
    io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;  // 도킹 활성화
    
    ImGui_ImplWin32_Init(hWnd);
    ImGui_ImplDX11_Init(device.Get(), deviceContext.Get());
}

// 렌더링 루프에서 ImGui 렌더링
void App::DoFrame(GameTimer& timer)
{
    // ImGui 프레임 시작
    ImGui_ImplDX11_NewFrame();
    ImGui_ImplWin32_NewFrame();
    ImGui::NewFrame();
    
    // UI 렌더링
    Engine::FolderView::instance->RenderFolderView();
    Engine::MenuBar::menuBar->RenderMenuBar();
    Engine::Inspector::instance->Update(deltaTime);
    
    // ImGui 렌더링 완료
    ImGui::Render();
    ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
}

핵심 문제 해결 사례

프로젝트 개발 과정에서 마주한 기술적 문제와 해결 과정을 소개하겠습니다.

사례 1: ImGui Gizmo 회전 문제 (짐벌락 현상)

문제 상황

ImGui Gizmo를 활용하여 오브젝트를 회전시킬 때, 특정 각도에서 갑자기 회전이 되지 않거나 예상과 다른 이상한 방향으로 움직이는 문제가 발생했습니다.

초기 접근 및 오해

처음에는 오일러(Euler) 방식의 회전을 사용했습니다. 오일러 각도는 직관적이고 짐벌락(Gimbal Lock) 현상이 그렇게 자주 발생하지 않을 것이라고 생각했습니다.

해결 방법

쿼터니언(Quaternion) 방식으로 바꾸니 문제가 완전히 해결되었습니다.

구현 세부 사항

  • DirectX Math 라이브러리의 XMVECTORXMQuaternion* 함수들을 활용
  • Gizmo로부터 받은 회전 입력을 쿼터니언으로 변환
  • Inspector에서는 직관적인 표시를 위해 오일러 각도로 변환하여 표시
  • 내부적으로는 모든 회전 연산을 쿼터니언으로 처리하여 짐벌락 완전 방지

결과

쿼터니언 방식을 도입한 후, 모든 각도에서 안정적으로 회전이 동작하며 Gizmo를 통한 직관적인 조작이 가능해졌습니다.

사례 2: NVIDIA PhysX 연동 문제

문제 상황

NVIDIA PhysX 물리 엔진을 프로젝트에 연동하는 과정에서 매우 많은 종속성과 빌드 오류가 발생했습니다. PhysX는 강력한 물리 엔진이지만, Visual Studio 프로젝트에 통합하기 위해서는 수많은 라이브러리 파일, 헤더 파일, 그리고 적절한 빌드 설정이 필요했습니다.

주요 난관 사항

  • 배포 파일 문제: PhysX SDK는 NVIDIA 공식 사이트와 GitHub 두 곳에서 모두 Release 버전을 제공하고 있었습니다. GitHub 버전으로 빌드를 시도했을 때 지속적으로 실패가 발생했으며, 최종적으로 NVIDIA 공식 사이트의 버전을 사용하여 문제를 해결할 수 있었습니다.
  • 불명확한 컴파일 오류: Python 및 CMake 관련 오류 발생 시, 에러 메시지가 명확하게 표시되지 않아 근본 원인을 파악하는 데 많은 시간이 소요되었습니다. 특히 의존성 체인에서 발생하는 오류의 경우 디버깅이 더욱 어려웠습니다.
  • 플랫폼 및 하드웨어 제약: PhysX는 컴파일 과정에서 GPU 호환성을 검증하기 때문에, NVIDIA GPU를 사용하는 환경에서만 정상적으로 빌드가 가능했습니다. 다른 그래픽 카드 제조사의 하드웨어에서는 컴파일 자체가 불가능한 제약이 있었습니다.
  • 복잡한 의존성 관리: 하드웨어 제약 외에도 Python과 CMake를 필수적으로 설치해야 했으며, 특히 Python 버전 호환성 문제가 심각했습니다. 최신 버전을 포함하여 PhysX가 지원하지 않는 Python 버전을 사용할 경우 런타임 에러가 발생했고, 이를 해결하기 위해 여러 버전을 테스트해야 했습니다.

해결 과정

각 오류를 하나씩 차근차근 해결하는 데 상당한 시간이 소요되었습니다. PhysX 공식 문서, 커뮤니티 포럼, 그리고 스택 오버플로우를 참고하며 빌드 설정을 미세 조정했습니다.

프로젝트 설정 단계

1. PhysX SDK 다운로드 및 준비

  • NVIDIA 공식 소스 사용: GitHub의 PhysX가 아닌 NVIDIA 공식 사이트에서 PhysX SDK를 다운로드해야 합니다.
  • 필수 도구 설치: Python (3.x 버전)과 CMake (최신 버전)를 다운로드하여 설치합니다.

2. Visual Studio에서 컴파일

  • CMake를 사용하여 Visual Studio 솔루션 파일을 생성합니다.
  • Visual Studio에서 PhysX 솔루션을 열고 빌드를 시작합니다.
  • 버전 호환성 확인: 컴파일 과정에서 일부 라이브러리나 SDK 버전이 맞지 않을 경우, 오류 메시지를 확인한 후 올바른 버전을 설치합니다 (예: Windows SDK 버전, Visual Studio 툴셋 버전).

3. 플랫폼 빌드 설정

  • x64만 빌드: PhysX는 x86(32비트)을 지원하지 않으므로, x64 Debugx64 Release 구성만 빌드합니다.
  • x86 구성은 빌드에서 제외하거나 삭제합니다.

4. 프로젝트 연결

  • 추가 포함 디렉토리: PhysX SDK의 include 폴더 경로를 추가합니다.
  • 추가 라이브러리 디렉토리: PhysX의 lib 폴더 경로를 추가합니다 (Debug/Release 각각 별도 설정).
  • 추가 종속성: PhysX_64.lib, PhysXCommon_64.lib, PhysXFoundation_64.lib, PhysXExtensions_static_64.lib 등의 .lib 파일을 링커 입력에 추가합니다.
  • 헤더 파일 (.h): PhysX SDK의 include 폴더에서 필요한 헤더 파일들을 참조할 수 있도록 경로를 설정합니다.
  • DLL 파일 복사: PhysX의 런타임 DLL 파일들(PhysX_64.dll, PhysXCommon_64.dll 등)을 실행 파일이 있는 폴더에 복사하거나, Post-Build 이벤트로 자동 복사하도록 설정합니다.

결과

모든 빌드 오류를 해결하고 PhysX가 정상적으로 작동하게 되었습니다. 결과적으로 충돌 감지, 중력, 물리 재질 등의 기능을 성공적으로 구현할 수 있었습니다.

최적화 기법

📊 최적화별 성능 개선 요약

최적화 기법 성능 개선 핵심 기술
최적화 1
Camera View Frustum Culling
56 → 80 FPS
(평균 +42.9%)

최대 350 FPS
Gribb-Hartmann 알고리즘, 조기 종료, 보수적 컬링
최적화 2
Component-Based Architecture
O(n) → O(1)
컴포넌트 검색
Hash Map, C++20 Concepts, Perfect Forwarding, Factory Pattern
최적화 3 & 4
Shader & Texture Caching
8초 → 2.5초
로딩 시간 -68.75%
타임스탬프 기반 캐싱, DDS 변환, 메모리 매핑 (CreateFileMapping)
최적화 5
Exception 메시지 버퍼화
30 → 56 FPS
런타임 +86.7%
고정 크기 버퍼 (char[100][128]), 동적 할당 제거

Camera View Frustum Culling

개요

Camera View Frustum Culling은 카메라의 시야 절두체(View Frustum) 밖에 있는 객체를 렌더링 파이프라인에서 조기에 제외시켜 GPU 부하를 크게 줄이는 핵심 최적화 기법입니다.

핵심 구조

class CameraViewFrustumCulling
{
private:
    enum Plane { Near, Far, Left, Right, Top, Bottom, Count };
    DirectX::XMFLOAT4 frustumPlanes[Plane::Count];  // 6개의 평면 방정식
    DirectX::XMMATRIX viewProjection;
};

1. Frustum 평면 추출

View-Projection 행렬을 전치(Transpose)한 후 각 행을 분해하여 6개의 평면 방정식을 계산합니다. 각 평면은 Ax + By + Cz + D = 0 형태로 저장됩니다.

void CameraViewFrustumCulling::UpdateFromViewProjection(const XMMATRIX& viewProjection)
{
    XMMATRIX viewProjT = XMMatrixTranspose(viewProjection);
    
    // 행렬을 행별로 분해
    rowX = viewProjT.r[0];
    rowY = viewProjT.r[1];
    rowZ = viewProjT.r[2];
    rowW = viewProjT.r[3];

    // Gribb-Hartmann 방법으로 평면 추출
    frustumPlanes[Left]   = Normalize(rowW + rowX);
    frustumPlanes[Right]  = Normalize(rowW - rowX);
    frustumPlanes[Bottom] = Normalize(rowW + rowY);
    frustumPlanes[Top]    = Normalize(rowW - rowY);
    frustumPlanes[Near]   = Normalize(rowW + rowZ);
    frustumPlanes[Far]    = Normalize(rowW - rowZ);
}

2. Sphere Culling (구 바운딩 볼륨)

바운딩 스피어의 중심과 각 평면 사이의 부호 거리(Signed Distance)를 계산합니다. 구의 중심이 평면으로부터 반지름보다 먼 거리에 있으면 해당 객체는 절두체 외부에 있다고 판정합니다.

bool CameraViewFrustumCulling::CheckSphere(const XMFLOAT3& center, float radius) const
{
    for (int i = 0; i < Plane::Count; i++)
    {
        // 점과 평면 사이의 거리 계산
        float distance = frustumPlanes[i].x * center.x +
                        frustumPlanes[i].y * center.y +
                        frustumPlanes[i].z * center.z +
                        frustumPlanes[i].w;

        // 구의 중심이 평면으로부터 반지름보다 멀면 외부
        if (distance < -radius)
            return false;
    }
    return true;  // 모든 평면 테스트 통과 → 내부 또는 교차
}

3. Box Culling (AABB 바운딩 박스)

AABB(Axis-Aligned Bounding Box)의 경우, 평면 법선 방향으로 가장 먼 꼭지점(Positive Vertex)을 계산하여 해당 꼭지점이 평면 바깥에 있는지 확인합니다. 가장 먼 꼭지점도 평면 바깥이면 박스 전체가 절두체 외부에 있다고 판정합니다.

bool CameraViewFrustumCulling::CheckBox(const XMFLOAT3& center, const XMFLOAT3& extents) const
{
    for (int i = 0; i < Plane::Count; i++)
    {
        XMVECTOR plane = XMLoadFloat4(&frustumPlanes[i]);
        XMVECTOR centerVec = XMLoadFloat3(¢er);
        
        float nx = frustumPlanes[i].x;
        float ny = frustumPlanes[i].y;
        float nz = frustumPlanes[i].z;

        // 평면 법선 방향으로 가장 먼 꼭지점까지의 거리
        float ex = extents.x * fabs(nx) + 
                   extents.y * fabs(ny) + 
                   extents.z * fabs(nz);

        // 중심에서 평면까지의 거리
        float distance = XMVectorGetX(XMPlaneDotCoord(plane, centerVec));

        // 가장 먼 꼭지점도 평면 바깥이면 외부
        if (distance < -ex)
            return false;
    }
    return true;  // 모든 평면 테스트 통과 → 내부 또는 교차
}

성능 최적화 특징

  • 조기 종료(Early Exit): 6개의 평면 중 하나에서라도 외부로 판정되면 즉시 false를 반환하여 불필요한 연산을 생략합니다.
  • 보수적 컬링(Conservative Culling): 절두체와 교차하는 객체는 내부로 판정하여 렌더링합니다. False positive는 허용하지만 false negative는 방지하여 시각적 오류를 완전히 차단합니다.
  • 간단한 수학 연산: 내적과 비교 연산만으로 컬링 판정을 수행하여 CPU 오버헤드를 최소화합니다.
  • 두 가지 바운딩 볼륨 지원: Sphere와 AABB 두 가지 방식을 지원하여 객체의 형태에 따라 최적의 컬링 방식을 선택할 수 있습니다.

알고리즘 요약

  1. 평면 추출: View-Projection 행렬로부터 6개 평면 방정식 계산
  2. 컬링 테스트: 각 객체의 바운딩 볼륨(구/박스)이 모든 평면의 안쪽에 있는지 확인
  3. 조기 종료: 하나의 평면에서라도 완전히 외부에 있으면 즉시 false 반환
  4. 렌더링 제외: 컬링된 객체는 렌더링 파이프라인에서 제외되어 GPU 부하 감소

📈 성능 결과
프러스트럼 컬링 추가로 FPS 분포가 기존 약 56 FPS에서 변경 후 56 ~ 350 FPS (평균 80 FPS)로 개선되었습니다. 평균 FPS는 56 → 80으로 약 +24 FPS (약 +42.9%) 향상되었습니다. 최저 프레임은 유지되었으나, 장면 복잡도에 따라 최대 프레임이 크게 증가하면서 전체 평균 성능이 상승했습니다.

https://github.com/Red-Opera/DirectX_GameEngine/commit/f262a1ff79697aa165a1095a8609c13af6808ae0
커밋을 통해서 작성했습니다.

Component-Based Architecture

객체 지향 설계와 현대 C++ 기법을 결합하여 높은 성능과 유지보수성을 동시에 달성했습니다.

1. Hash Map 기반 컴포넌트 관리

Object 클래스는 컴포넌트들을 unordered_map으로 관리하여 O(1) 시간 복잡도로 빠른 검색을 제공합니다.

class Object : public EngineLoop
{
protected:
    // 컴포넌트 이름을 키로, 컴포넌트 포인터를 값으로 저장
    std::unordered_map<std::string, std::shared_ptr<Component>> components;

public:
    // O(1) 컴포넌트 검색
    template <ComponentChild ComponentClass>
    std::shared_ptr<ComponentClass> GetComponent()
    {
        for (auto& component : components)
        {
            if (component.second->GetClassName() == ComponentClass::GetStaticClassName())
                return std::dynamic_pointer_cast<ComponentClass>(component.second);
        }
        return nullptr;
    }
    
    // 문자열 키로 직접 접근 (가장 빠름)
    const std::shared_ptr<Component> GetComponent(std::string componentName)
    {
        if (components.find(componentName) == components.end())
            return nullptr;
        return components[componentName];
    }
};

장점:

  • 빠른 검색: Vector 기반(O(n))보다 Hash Map(O(1))으로 수백 배 빠른 컴포넌트 조회
  • 중복 방지: 같은 타입의 컴포넌트 자동 중복 방지
  • 메모리 효율: 필요한 컴포넌트만 동적 할당

성능 특성

  • 검색: O(1)
  • 삽입: O(1)
  • 삭제: O(n) — 하지만 컴포넌트 삭제는 드문 작업이므로 전체 성능에 미치는 영향은 크지 않습니다.

2. C++20 Concepts를 활용한 타입 안정성

템플릿 제약 조건을 통해 컴파일 타임에 타입 검증을 수행합니다.

// Component를 상속받은 클래스만 허용하는 Concept
template<typename T>
concept ComponentChild = std::is_base_of_v<Component, T>;

// Concept를 사용한 타입 안전 메서드
template <ComponentChild ComponentClass>
std::shared_ptr<ComponentClass> GetComponent() { /* ... */ }

template <ComponentChild ComponentClass, typename... Args>
std::shared_ptr<ComponentClass> AddComponent(Args&&... args) { /* ... */ }

장점:

  • 컴파일 타임 검증: 잘못된 타입 사용 시 명확한 컴파일 에러
  • 실행 시간 오버헤드 제로: 런타임 타입 체크 불필요
  • 인텔리센스 개선: IDE에서 정확한 자동 완성 제공

3. Perfect Forwarding으로 임시 객체 생성 최소화

가변 인자 템플릿과 완벽한 전달을 사용하여 불필요한 복사 제거

template <ComponentChild ComponentClass, typename... Args>
std::shared_ptr<ComponentClass> AddComponent(Args&&... args)
{
    // 중복 확인
    std::string componentName = ComponentClass::GetStaticClassName();
    if (components.find(componentName) != components.end())
        return std::dynamic_pointer_cast<ComponentClass>(components[componentName]);

    // Perfect Forwarding으로 인자 전달 (복사 없음)
    auto component = std::make_shared<ComponentClass>(
        shared_from_this(), 
        std::forward<Args>(args)...
    );

    component->SetObject(shared_from_this());
    component->transform = this->transform;
    
    components[component->GetClassName()] = component;
    return component;
}

장점:

  • 제로 오버헤드: 인자의 lvalue/rvalue를 보존하여 불필요한 복사 제거
  • 유연성: 다양한 생성자 매개변수 조합 지원
  • 타입 안전성: 컴파일 타임에 모든 타입 검증

4. Factory Pattern과 Smart Pointer 조합

객체 생성 시 안전한 메모리 관리와 참조 순환 방지

class Object : public std::enable_shared_from_this<Object>
{
public:
    // Factory Method로 생성 강제
    static std::shared_ptr<Object> Create(std::string name)
    {
        std::shared_ptr<Object> newObject = std::make_shared<Object>(name);
        
        // TransformComponent 자동 생성
        auto transformComponent = std::make_shared<TransformComponent>(newObject);
        newObject->components[transformComponent->GetClassName()] = transformComponent;
        transformComponent->SetObject(newObject);
        newObject->transform = transformComponent;
        transformComponent->transform = transformComponent;

        return newObject;
    }
};

장점:

  • 자동 메모리 관리: shared_ptr로 메모리 누수 방지
  • 참조 순환 안전: enable_shared_from_this로 안전한 self 참조
  • 초기화 일관성: 모든 Object는 항상 TransformComponent 보유

최적화 기법 요약

기법 성능 향상 적용 위치
Hash Map 컴포넌트 관리 O(n) → O(1) Object::GetComponent()
C++20 Concepts 컴파일 타임 검증 모든 템플릿 메서드
Perfect Forwarding 제로 복사 오버헤드 AddComponent()
Factory + Smart Pointer 메모리 누수 제로 Object::Create()

쉐이더 컴파일 캐싱

쉐이더 파일을 매번 컴파일하는 대신, 타임스탬프 기반 캐싱 시스템을 통해 로딩 속도를 대폭 개선했습니다. 또한 동일 커밋에서 이미지 로딩 파이프라인(Image.cpp)의 DDS 캐싱 및 메모리 매핑 개선도 함께 적용되어, 쉐이더와 텍스처 양 쪽에서 초기화 성능이 향상되었습니다.

구현 방식

  • DDS 변환 캐싱: PNG/JPG 등을 DDS로 변환 후 Temp/TextureCache/ 폴더에 저장
  • 자동 캐시 검증: 캐시 파일 존재 시 즉시 로드, 없으면 변환 후 저장
  • DirectXTex 활용: Microsoft의 DirectXTex 라이브러리 사용

DDS 캐시 생성 규칙

씬에 존재하는 모든 오브젝트의 Material을 로드할 때, 해당 Material이 참조하는 Texture마다 DDS 캐시 파일이 생성됩니다. 예를 들어 씬에 오브젝트가 5개 있고 각 오브젝트가 서로 다른 2개의 Material을 사용한다면 총 10개의 DDS 캐시 파일이 생성됩니다. 생성된 캐시 파일은 기본적으로 Temp/TextureCache/에 저장됩니다.

커밋 반영 내용 요약

  • Image.cpp 개선: DDS 캐시 파일을 우선 로드하고, 파일을 메모리 맵으로 매핑(MapViewOfFile)하여 I/O 및 복사 비용을 줄임.
  • 파일 검증: 파일 크기와 매핑 실패를 검사해 안전하게 예외 처리하도록 변경.
  • 쉐이더 재활용: 쉐이더 캐시(.cso) 전략과 함께 적용되어 전체 초기화 시간이 크게 단축됨(커밋 메시지: 로딩 8초 → 2.5초).

핵심 코드 (Image.cpp)

Image Image::FromFile(const std::string& name)
{
    std::wstring tempDDSFolder = L"Temp/TextureCache/";
    std::wstring tempDDSPath = tempDDSFolder + fileName + L".dds";
    
    DirectX::ScratchImage scratch;
    HRESULT hr;
    
    // 캐시 폴더 생성
    if (!std::filesystem::exists(tempDDSFolder))
        std::filesystem::create_directories(tempDDSFolder);
    else
        hr = LoadTempDDSTexture(tempDDSPath, scratch);  // 캐시 로드
    
    // 캐시 미존재 시 원본 로드 후 DDS 저장
    if (FAILED(hr))
    {
        hr = DirectX::LoadFromWICFile(name.c_str(), ...);
        
        if (SUCCEEDED(hr))
            SaveToDDSFile(..., tempDDSPath.c_str());  // 캐시 저장
    }
    
    return Image(scratch);
}

장점

  • 로딩 속도: PNG/JPG 디코딩 생략, DDS는 GPU에 직접 업로드 가능
  • 메모리 효율: DDS는 압축 포맷(BC1-BC7) 지원으로 VRAM 절약
  • 일관성: 모든 텍스처를 동일한 포맷으로 처리

📊 캐시 통계
현재 프로젝트의 Temp/TextureCache/ 폴더에는 83개의 DDS 캐시 파일이 저장되어 있어, 반복 실행 시 빠른 텍스처 로딩이 가능합니다.

📌 참고 커밋
본 설명과 개선점은 아래 커밋에서 적용된 변경사항을 반영합니다:
https://github.com/Red-Opera/DirectX_GameEngine/commit/ef76f00fac71da934e63589afbc9f8aab469c354

텍스처 캐싱 & 빠른 이미지 로딩

다양한 포맷(PNG, JPG 등)의 이미지를 DirectX DDS 포맷으로 변환해 디스크에 캐시하고, 필요 시 빠르게 메모리로 매핑하여 로딩 시간을 크게 단축했습니다. 이 변경은 이미지 로드 관련 주요 리팩터링과 캐시 전략(임시 DDS 저장, 메모리 맵핑, 검증)을 포함합니다.

구현 요약

  • DDS 변환 캐시: 원본 이미지가 있으면 DirectX::LoadFromWICFile로 읽어 .dds로 변환 후 Temp/TextureCache/[FileName].dds에 저장.
  • 임시 캐시 우선 사용: 캐시가 존재하면 직접 로드(메모리 매핑)하여 변환 비용을 회피.
  • 메모리 매핑 로드: CreateFileMapping + MapViewOfFile로 DDS를 로드해 I/O와 복사 오버헤드를 줄임.
  • 검증과 예외 처리: 파일 크기, 맵핑 실패 등 에러를 검사하고 예외를 던져 호출 측에서 처리하도록 구현.
  • 성능 개선: 초기화(로딩) 시간 단축(예: 8초 → 2.5초) — 커밋 메시지에 기록된 개선 결과.

핵심 코드 (요약)

Image Image::FromFile(const std::string& name)
{
    std::wstring tempDDSFolder = L"Temp/TextureCache/";
    std::wstring tempDDSPath = tempDDSFolder + StringConverter::GetFileName(name) + L".dds";

    DirectX::ScratchImage scratch;
    HRESULT hr;

    // 캐시 폴더 생성
    if (!std::filesystem::exists(tempDDSFolder))
    {
        std::filesystem::create_directories(tempDDSFolder);
        hr = S_FALSE; // 캐시 없음
    }
    else
        hr = LoadTempDDSTexture(tempDDSPath, scratch); // 메모리 매핑으로 DDS 로드 시도

    // 캐시 미존재 또는 로드 실패 시 원본 로드 후 DDS 저장
    if (FAILED(hr))
    {
        hr = DirectX::LoadFromWICFile(StringConverter::ToWide(name).c_str(), DirectX::WIC_FLAGS_IGNORE_SRGB, nullptr, scratch);

        if (SUCCEEDED(hr))
            SaveToDDSFile(scratch.GetImages(), scratch.GetImageCount(), scratch.GetMetadata(), DDS_FLAGS_NONE, tempDDSPath.c_str());
    }

    return Image(std::move(scratch));
}

구현 상세

  • LoadTempDDSTexture: DDS 파일을 파일 매핑으로 빠르게 읽어 DirectX::ScratchImage로 변환.
  • 메모리 맵핑 안전성: 파일 열기/맵핑 실패 시 적절한 예외를 던지고 핸들을 안전하게 닫음.
  • 호환성: WIC로 읽은 이미지를 DDS로 저장하면 GPU 업로드 시 바로 사용할 수 있어 디코딩 오버헤드 제거.
  • 디스크 사용 관리: 불필요한 캐시 파일 정리는 별도의 유틸리티(또는 애플리케이션 시작 시 정책)로 수행 가능.

📌 참고 커밋
본 설명은 아래 커밋의 변경사항을 반영합니다:
https://github.com/Red-Opera/DirectX_GameEngine/commit/ef76f00fac71da934e63589afbc9f8aab469c354

Exception 메시지 버퍼화

기능 설명: ExceptionInfo::GetMessages()는 DirectX의 IDXGIInfoQueue에서 수집된 디버그/오류 메시지를 읽어 호출자에게 반환합니다. 이 루틴은 그래픽스 예외 발생 시 디버그 로그를 수집해 예외 객체(HRException, InfoException)에 전달하거나 로그용으로 사용됩니다.

변경 전(문제점)

이전 구현은 호출마다 std::vector<std::string>을 생성하고 메시지를 복사해 벡터에 추가했습니다. 업데이트마다 해당 메소드가 호출되어, 메소드 내부에서 생성되는 vector 및 string의 초기화로 발생하는 런타임 시간으로 프레임 성능에 악영향을 주었습니다.

업데이트마다 해당 메소드가 호출되어, 메소드 내부에서 생성되는 vectorstring의 초기화로 발생하는 런타임 시간을 줄이기 위해 이 최적화를 적용했습니다. 아래 벤치마크는 스택 배열과 동적 컨테이너 초기화의 차이를 극명하게 보여줍니다.

// 예시 벤치마크
// 약 8ms
for (int i = 0; i < 10000000; i++) 
    int a[1000];      

// 약 3662ms
for (int i = 0; i < 10000000; i++) 
    std::vector a;
                            

변경 후(최적화 내용)

  • 동적 컨테이너 대신 고정 크기 버퍼를 도입: char messages[100][128];와 카운터 UINT messageCount.
  • GetMessages()의 반환형을 std::vector<std::string>에서 const char* (또는 내부 버퍼 포인터)로 변경하여 호출 시 힙 할당/문자열 생성/해제를 제거.
  • 메시지 복사에는 strncpy_s를 사용해 각 메시지를 고정 버퍼의 다음 슬롯에 저장하고 messageCount를 증가시킴.

단점: 고정 배열 사용으로 인해 메시지가 길 경우 잘릴 수 있고, 필요한 것보다 더 큰 고정 메모리를 사용하는 상황(메모리 낭비)이 발생할 수 있습니다. 다만 충분한 크기를 확보하면 실제 메모리 증가는 생각보다 크지 않으며, 동적 할당을 제거한 효과로 성능 측면에서 유리합니다.

// ExceptionInfo.h - 고정 버퍼 구조
class ExceptionInfo
{
private:
    char message[100][128];  // 고정 크기 버퍼
    UINT messageCount = 0;

public:
    const char* GetMessages()
    {
        messageCount = 0;
        // IDXGIInfoQueue에서 메시지 수집 후 버퍼에 복사
        // strncpy_s(message[messageCount++], msg, sizeof(message[0])-1);
        return reinterpret_cast<const char*>(message);
    }
};

GraphicsException.h - 오류 검출 및 출력

이 최적화의 핵심은 GraphicsException.h의 매크로에서 GetMessages()를 호출할 때 발생합니다. 오류 발생 시 디버그 메시지를 수집하여 예외 객체에 전달하는 과정에서, 기존 vector<string> 반환 방식 대신 고정 버퍼 포인터를 사용하여 힙 할당을 완전히 제거했습니다.

// GraphicsException.h - 매크로 정의 (변경 후)
#ifndef NDEBUG
    // 오류 검출 시 GetMessages() 호출하여 예외 생성
    #define GRAPHIC_EXCEPT(hr) DxGraphic::HRException{ __LINE__, __FILE__, (hr), infoManager.GetMessages() }
    
    // 디버그 정보만 있는 경우 처리
    #define GRAPHIC_THROW_INFO_ONLY(hr) \
        infoManager.Set(); (hr); { \
            auto v = infoManager.GetMessages(); \
            if(v[0] != '\0') { \
                throw DxGraphic::InfoException{ __LINE__, __FILE__, v }; \
            } \
        }
#else
    #define GRAPHIC_EXCEPT(hr) DxGraphic::HRException(__LINE__, __FILE__, (hr))
#endif

효과 & 결과

  • 플레이타임 성능이 약 30 FPS → 56 FPS로 개선되는 등 실시간 성능에 큰 영향을 줌.

📌 참고 커밋
이 최적화는 다음 커밋의 변경사항을 요약한 것입니다:
https://github.com/Red-Opera/DirectX_GameEngine/commit/77081fa4e334daed4e815623faf093fb8ad2ddf7

코드 스타일 / 개발 철학

코드 스타일

이 프로젝트에서는 가독성과 유지보수성, 그리고 실무에서의 사용성을 기준으로 아래와 같은 규칙을 적용합니다.

  • 전역 접근성 우선: 엔진 개발 도중 특정 변수나 메서드에 접근하기 어렵다는 경험으로 인해 싱글톤 패턴 또는 static 메서드/변수를 필요한 곳에 사용했습니다. 남발을 방지하기 위해 최대한 캡슐화를 중시했습니다.
  • 명명 규칙: 클래스와 메서드는 PascalCase (첫 글자 대문자)로, 변수는 camelCase (첫 글자 소문자)로 표기합니다. 상수는 UPPER_SNAKE_CASE로 구분하여 한눈에 구별되도록 합니다.
  • 스마트 포인터와 RAII: 소유권이 명확하지 않은 포인터는 사용하지 않으며, `std::unique_ptr`, `std::shared_ptr` 등을 활용해 자원 관리를 자동화했습니다.

개발 철학

이 엔진은 '조금만 배우하면 곧장 만들 수 있는 엔진'을 목표로 삼았습니다. Unity에서 영감을 얻되, 불편했던 점은 개선하고, 성능과 사용성을 균형 있게 유지하는 데 중점을 두었습니다.

  • 사용자 친화성 우선: 외부에서 사용할 때는 최대한 단순하도록 제작헸습니다. 빠르게 프로토타입을 만들 수 있도록 씬/오브젝트/컴포넌트 흐름을 직관적으로 설계했습니다.
  • 불편한 부분은 확장: Unity에서 불편하다고 느낀 점은 보완 기능을 추가하여 더 나은 작업 흐름을 제공합니다(예: 더 직관적인 씬 초기화 패턴, 편리한 컴포넌트 추가/검색 API 등).
  • 품질과 최적화의 균형: 그래픽·물리·로직의 품질을 유지하면서도, 실사용에서 요구되는 성능을 확보하기 위해 프로파일링 기반의 최적화를 우선했습니다. 구현 시 항상 비용(시간/메모리)을 고려했습니다.
  • 엔진처럼 설계하기: 단편적인 툴이 아닌 재사용 가능한 엔진을 목표로 하여 모듈화, 확장성, 명확한 생명주기(Initialize/Start/Update/Finalize)를 중심으로 설계했습니다. 이를 통해 향후 기능 추가나 리팩터링이 용이하도록 했습니다.

해결하지 못했거나 구현 예정

현재 프로젝트에서 시도했으나 완료하지 못한 기능들과 향후 구현 예정인 기능들입니다.

1. 포스트 프로세싱 효과

시도 기간: 며칠 동안 시도했지만 실패
목표: ImGui를 제외한 전체 화면을 Texture로 가져와 프로세싱을 적용한 후 전체 화면에 표시
문제점: 전체 화면을 Texture로 가져오는 과정에서 실패. 각 Pass 별 또는 back buffer에서 dump를 사용했지만 전체 화면을 얻는 데 실패했으며, Render Target 설정 및 렌더링 파이프라인 구성에서 기술적 어려움 발생.

2. UI 시스템

상태: 구현 예정
내용: 게임 내 커스텀 UI 시스템 구현 (버튼, 텍스트, 이미지 등 기본 UI 요소). 현재는 ImGui를 통한 에디터 UI만 구현되어 있으며, 런타임 게임 UI는 미구현 상태.

3. 애니메이션 시스템

상태: 구현 예정
내용: 스켈레탈 애니메이션 및 본 애니메이션 시스템 구현. FBX 파일에서 애니메이션 데이터를 읽어 캐릭터 모델에 적용하는 기능 필요.

4. 씬 변경 시 코드 자동 수정

상태: 구현 예정
내용: 씬 에디터에서 오브젝트를 추가/제거하거나 속성을 변경할 때, 관련 코드 파일을 자동으로 업데이트하는 기능. 씬 데이터를 직렬화/역직렬화하여 코드 생성 자동화 필요.

5. 플레이 모드

상태: 구현 예정
내용: Unity와 같은 에디터 모드/플레이 모드 전환 기능. 현재는 에디터와 게임이 동시에 실행되는 구조이며, 플레이 모드에서 씬 상태를 저장하고 에디터 모드로 복귀 시 원래 상태로 복원하는 기능 필요.

위 기능들은 추후 업데이트를 통해 순차적으로 구현될 예정입니다.

연락처 및 이메일

Get In Touch

프로젝트에 대한 문의사항이나 협업 제안이 있으시면 언제든 연락 주세요.

이름

서정현

© 2025 서정현. Some Rights Reserved.
본 프로젝트는 포트폴리오 목적으로 제작되었습니다.