← 프로젝트 목록으로

PROJECT REPORT

PROJECT REPORT // KRAFTON_JUNGLE_GAMETECH_PINBALL

크래프톤 정글 GameTech 1기 입학 전형 기술 시험으로 구현한 DirectX 11 2D/3D 탄성 충돌 물리 시뮬레이터 및 심층 면접 복기 포스트모텀

DirectX 11C++HLSLPhysics SimulationDear ImGuiRetrospective
PROJECT IDENTITY // TIMED CODING TEST & RETROSPECTIVE

본 프로젝트는 크래프톤 정글 GameTech 1기 입학 전형의 기술 시험으로 치러진 실기 과제입니다. 상용 게임 엔진 프레임워크를 전혀 사용하지 않고 순수 C++와 DirectX 11, HLSL, Win32 API를 활용하여 정해진 시간 내에 요구사항인 2D/3D 탄성 충돌 물리 시뮬레이션, 침투 깊이 보정 및 벽면 반발, 2배 확장 동적 메모리 풀 관리를 밑바닥부터 완성했습니다. 또한 기술 시험 합격 후 진행된 심층 면접 과정에서의 시선 처리와 발화 구조에 대한 솔직한 자기반성 및 극복 다짐을 기록한 성장 포스트모텀(Post-Mortem)을 포함합니다.

MODULE 01
DirectX 11 파이프라인 밑바닥 구축
Device, SwapChain, RTV, Vertex/Pixel Shader 런타임 컴파일, Constant Buffer 변환 행렬 전송 파이프라인을 독자 구축했습니다.
MODULE 02
2D/3D 완전 탄성 충돌 물리 엔진
운동량 보존 법칙과 반발계수(e = 1.0) 기반 충격량(Impulse) 연산 및 질량비 침투 보정(Penetration Resolution)을 적용했습니다.
MODULE 03
벽면 반발 및 경계 클램핑
화면 경계([-1.0, 1.0]) 벽면 충돌 시 에너지 감쇠 반발계수(e = 0.9)와 반경 보정 클램핑으로 오브젝트 이탈을 완벽히 방지합니다.
MODULE 04
동적 메모리 풀 용량 2배 확장
원시 기하체 배열 용량 초과 시 2배 크기로 안전하게 재할당하고 포인터를 스왑하는 C 스타일 메모리 확장 풀을 구현했습니다.
● IN-ENGINE REALTIME SIMULATION DEMO DirectX 11 // Win32

▲ DirectX 11 렌더링 파이프라인 및 강체 탄성 충돌 물리 시뮬레이션 시연 1080p 30FPS CAPTURE

1. 프로젝트 개요 및 기술 시험 요구사항

1.1 입학 시험 배경 및 목적

크래프톤 정글 GameTech는 현업 수준의 게임 클라이언트 및 코어 엔진 엔지니어를 양성하기 위한 몰입형 교육 과정입니다. 본 전형의 기술 시험은 지원자가 단순 라이브러리 사용자를 넘어 컴퓨터 그래픽스 파이프라인의 하부 원리(DirectX 11 / HLSL)와 게임 수학 및 물리 법칙(충돌 감지, 운동량 보존, 벡터 해석)을 직접 코드로 구현할 수 있는 하드웨어 친화적 엔지니어인지를 정밀 검증하는 타임어택 코딩 과제였습니다.

1.2 핵심 개발 요구사항

카테고리상세 요구 사양구현 대상
Graphics APIDirectX 11 기반 윈도우 생성 및 스왑 체인, 버텍스 버퍼, 상수 버퍼 제어URenderer.h, URenderer.cpp
Shader LanguageHLSL 기반 정점 셰이더(VS) 및 픽셀 셰이더(PS) 작성과 런타임 컴파일Shader.hlsl
Object Architecture다형성 기반 객체 지향 설계 (UPrimitive 추상 기반 클래스)main.cpp (UPrimitive, UBall)
Rigid Body Physics원형/구체 간 탄성 충돌(Impulse Resolution) 및 질량비 침투 깊이 보정UBall::HandlePrimitiveCollision
Boundary Physics뷰포트 경계 벽면 충돌 감지 및 에너지 감쇠 반발($e = 0.9$) 적용UBall::HandleWallCollision
Memory ManagementC 스타일 2배 확장(Capacity Doubling) 동적 메모리 풀 관리EnsurePrimitiveCapacity

2. DirectX 11 렌더링 파이프라인 및 HLSL 셰이더

2.1 렌더러 아키텍처 (URenderer)

상용 엔진 없이 DirectX 11 API를 직접 제어하기 위해 렌더링 라이프사이클을 담당하는 URenderer 클래스를 설계했습니다:

코드 보기: URenderer::Init — 렌더링 파이프라인 초기화
// URenderer 핵심 파이프라인 구조
void URenderer::Init(HWND hWnd)
{
    m_hWnd = hWnd;
    CreateDeviceAndSwapChain();
    CreateRenderTargetView();
    CreateInputLayout();
    CreateVertexShader();
    CreateConstantBuffer();
    CreateRasterizerState();
    CreatePixelShader();
}
  1. 디바이스 및 스왑 체인 (ID3D11Device, IDXGISwapChain):

하드웨어 가속 플래그(D3D_DRIVER_TYPE_HARDWARE)와 더블 버퍼링 스왑 체인(DXGI_SWAP_EFFECT_DISCARD)을 구성했습니다.

  1. 상수 버퍼(Constant Buffer)를 통한 트랜스폼 전달:

각 구체의 월드 좌표 오프셋(Location)과 반지름 스케일(Scale)을 16바이트 정렬된 구조체로 묶어 매 프레임 GPU 레지스터 b0으로 업데이트합니다.

코드 보기: URenderer::UpdateConstant — 상수 버퍼 GPU 전송
void URenderer::UpdateConstant(FVector offset, FVector scale)
{
    D3D11_MAPPED_SUBRESOURCE mappedResource;
    m_deviceContext->Map(m_constantBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);
    FTransformData* data = (FTransformData*)mappedResource.pData;
    data->offset = offset;
    data->scale = scale;
    m_deviceContext->Unmap(m_constantBuffer, 0);
}

2.2 HLSL 셰이더 구조 (Shader.hlsl)

정점 셰이더에서 상수 버퍼의 오프셋과 스케일을 적용하여 투영 변환 없이 화면 정규화 좌표계(NDC [-1.0, 1.0]) 상에 구체 지오메트리를 직관적으로 배치합니다:

코드 보기: Shader.hlsl — 정점 셰이더 & 픽셀 셰이더 전체 구조
cbuffer TransformData : register(b0)
{
    float3 offset;
    float pad0;
    float3 scale;
    float pad1;
}

struct VS_INPUT
{
    float4 position : POSITION; 
    float4 color : COLOR;
};

struct PS_INPUT
{
    float4 position : SV_POSITION; 
    float4 color : COLOR;
};

PS_INPUT mainVS(VS_INPUT input)
{
    PS_INPUT output;
    output.position = float4(offset, 0.0f) + float4(input.position.xyz * scale, 1.0f);
    output.color = input.color;
    return output;
}

float4 mainPS(PS_INPUT input) : SV_TARGET
{
    return input.color;
}

3. 강체 탄성 충돌 물리 엔진 및 침투 해결 수식

3.1 완전 탄성 충돌 모델 (Momentum & Impulse Conservation)

두 구체 $A$와 $B$가 접촉했을 때 충돌 수직 축(Normal) 상에서의 상대 속도 $v_{rel}$와 충격량 $J$는 완전 탄성 충돌(반발계수 $e = 1.0$) 원리를 따릅니다:

$$v_{rel} = (\vec{v}_B - \vec{v}_A) \cdot \vec{n}$$

$$J = \frac{-(1 + e) \cdot v_{rel}}{\frac{1}{m_A} + \frac{1}{m_B}}$$

$$\vec{v}_A' = \vec{v}_A - \frac{J \cdot \vec{n}}{m_A}, \quad \vec{v}_B' = \vec{v}_B + \frac{J \cdot \vec{n}}{m_B}$$

코드 보기: UBall::HandlePrimitiveCollision — 충격량 기반 충돌 응답
// main.cpp: UBall::HandlePrimitiveCollision 발췌
const float rvx = otherBall->Velocity.x - Velocity.x;
const float rvy = otherBall->Velocity.y - Velocity.y;
const float rvz = otherBall->Velocity.z - Velocity.z;

const float velocityAlongNormal = rvx * nx + rvy * ny + rvz * nz;
if (velocityAlongNormal > 0.0f) return; // 멀어지는 상태면 무시

const float restitution = 1.0f; // 완전 탄성 충돌
const float impulse = -(1.0f + restitution) * velocityAlongNormal / (1.0f / Mass + 1.0f / otherBall->Mass);

Velocity.x -= impulse * nx / Mass;
Velocity.y -= impulse * ny / Mass;
Velocity.z -= impulse * nz / Mass;

otherBall->Velocity.x += impulse * nx / otherBall->Mass;
otherBall->Velocity.y += impulse * ny / otherBall->Mass;
otherBall->Velocity.z += impulse * nz / otherBall->Mass;

3.2 질량비 기반 침투 깊이 위치 보정 (Penetration Depth Resolution)

이산 시간 시뮬레이션(Discrete Time-step)에서 공들이 고속으로 이동할 때 한 프레임 사이에 두 공이 서로의 반경 안으로 파고드는 겹침 현상이 발생합니다. 이를 방지하기 위해 두 공의 질량비(Mass Ratio)를 산출하여 질량이 가벼운 공이 더 많은 거리를 밀려나도록 즉각적인 위치 보정을 가했습니다:

코드 보기: 질량비 침투 깊이 보정 (Penetration Depth Resolution)
const float overlap = Radius + otherBall->Radius - distance;
if (overlap > 0.0f)
{
    const float totalMass = Mass + otherBall->Mass;
    const float myRatio = otherBall->Mass / totalMass;
    const float otherRatio = Mass / totalMass;

    Location.x -= nx * overlap * myRatio;
    Location.y -= ny * overlap * myRatio;
    Location.z -= nz * overlap * myRatio;

    otherBall->Location.x += nx * overlap * otherRatio;
    otherBall->Location.y += ny * overlap * otherRatio;
    otherBall->Location.z += nz * overlap * otherRatio;
}

4. 벽면 충돌 반발 계수 및 동적 버퍼 용량 관리

4.1 경계면 충돌 및 에너지 감쇠 반발 (HandleWallCollision)

뷰포트 정규화 좌표계($[-1.0, 1.0]$) 내에서 구체가 화면 밖으로 이탈하지 않도록 구체의 반경(Radius)을 고려한 경계 클램핑 및 법선 속도 반전 반발계수($e = 0.9$)를 구현했습니다:

코드 보기: UBall::HandleWallCollision — 벽면 충돌 및 에너지 감쇠 반발
void UBall::HandleWallCollision()
{
    const float wallRestitution = 0.9f;

    if (Location.x < LEFT + Radius)
    {
        Location.x = LEFT + Radius;
        if (Velocity.x < 0.0f)
        {
            Velocity.x = -Velocity.x * wallRestitution;
        }
    }
    else if (Location.x > RIGHT - Radius)
    {
        Location.x = RIGHT - Radius;
        if (Velocity.x > 0.0f)
        {
            Velocity.x = -Velocity.x * wallRestitution;
        }
    }

    if (Location.y < BOTTOM + Radius)
    {
        Location.y = BOTTOM + Radius;
        if (Velocity.y < 0.0f)
        {
            Velocity.y = -Velocity.y * wallRestitution;
        }
    }
    else if (Location.y > TOP - Radius)
    {
        Location.y = TOP - Radius;
        if (Velocity.y > 0.0f)
        {
            Velocity.y = -Velocity.y * wallRestitution;
        }
    }
}

4.2 C 스타일 동적 버퍼 2배 확장 아키텍처 (EnsurePrimitiveCapacity)

표준 템플릿 라이브러리(STL)의 오버헤드를 배제하고 메모리 재할당 비용을 최적화하기 위해, 벡터의 기하급수적 확장(Exponential Growth, 2배 용량 증가) 패턴을 직접 C 포인터 배열로 설계했습니다:

코드 보기: EnsurePrimitiveCapacity — C 스타일 2배 동적 메모리 풀 확장
void EnsurePrimitiveCapacity(UPrimitive*** primitiveList, int* capacity, int requiredCapacity)
{
    if (requiredCapacity <= *capacity)
    {
        return;
    }

    int newCapacity = *capacity;
    if (newCapacity < 1)
    {
        newCapacity = 1;
    }

    while (newCapacity < requiredCapacity)
    {
        newCapacity *= 2;
    }

    UPrimitive** newPrimitiveList = new UPrimitive*[newCapacity];
    for (int i = 0; i < newCapacity; ++i)
    {
        newPrimitiveList[i] = nullptr;
    }

    for (int i = 0; i < *capacity; ++i)
    {
        newPrimitiveList[i] = (*primitiveList)[i];
    }

    delete[](*primitiveList);
    *primitiveList = newPrimitiveList;
    *capacity = newCapacity;
}