PROJECT REPORT
PROJECT REPORT // KRAFTON_JUNGLE_GAMETECH_PINBALL
크래프톤 정글 GameTech 1기 입학 전형 기술 시험으로 구현한 DirectX 11 2D/3D 탄성 충돌 물리 시뮬레이터 및 심층 면접 복기 포스트모텀
본 프로젝트는 크래프톤 정글 GameTech 1기 입학 전형의 기술 시험으로 치러진 실기 과제입니다. 상용 게임 엔진 프레임워크를 전혀 사용하지 않고 순수 C++와 DirectX 11, HLSL, Win32 API를 활용하여 정해진 시간 내에 요구사항인 2D/3D 탄성 충돌 물리 시뮬레이션, 침투 깊이 보정 및 벽면 반발, 2배 확장 동적 메모리 풀 관리를 밑바닥부터 완성했습니다. 또한 기술 시험 합격 후 진행된 심층 면접 과정에서의 시선 처리와 발화 구조에 대한 솔직한 자기반성 및 극복 다짐을 기록한 성장 포스트모텀(Post-Mortem)을 포함합니다.
1. 프로젝트 개요 및 기술 시험 요구사항
1.1 입학 시험 배경 및 목적
크래프톤 정글 GameTech는 현업 수준의 게임 클라이언트 및 코어 엔진 엔지니어를 양성하기 위한 몰입형 교육 과정입니다. 본 전형의 기술 시험은 지원자가 단순 라이브러리 사용자를 넘어 컴퓨터 그래픽스 파이프라인의 하부 원리(DirectX 11 / HLSL)와 게임 수학 및 물리 법칙(충돌 감지, 운동량 보존, 벡터 해석)을 직접 코드로 구현할 수 있는 하드웨어 친화적 엔지니어인지를 정밀 검증하는 타임어택 코딩 과제였습니다.
1.2 핵심 개발 요구사항
| 카테고리 | 상세 요구 사양 | 구현 대상 |
|---|---|---|
| Graphics API | DirectX 11 기반 윈도우 생성 및 스왑 체인, 버텍스 버퍼, 상수 버퍼 제어 | URenderer.h, URenderer.cpp |
| Shader Language | HLSL 기반 정점 셰이더(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 Management | C 스타일 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();
}
- 디바이스 및 스왑 체인 (
ID3D11Device,IDXGISwapChain):
하드웨어 가속 플래그(D3D_DRIVER_TYPE_HARDWARE)와 더블 버퍼링 스왑 체인(DXGI_SWAP_EFFECT_DISCARD)을 구성했습니다.
- 상수 버퍼(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;
}