DirectX 9 개발 핵심 정리

DirectX 9는 오래된 그래픽 API지만 게임 개발과 3D 렌더링의 기본 구조를 이해하기에는 좋은 기술이다.

요즘은 DirectX 11, DirectX 12, Vulkan 같은 API를 사용하는 경우가 많지만 Device, Vertex Buffer, Index Buffer, Texture, Shader, Render State 같은 개념은 이후 그래픽 API에서도 형태만 달라질 뿐 계속 등장한다.

DirectX 9를 공부할 때 중요한 것은 API 함수를 많이 외우는 것이 아니다.

게임의 데이터가 어떻게 GPU로 전달되고, 렌더링 파이프라인을 거쳐 화면에 출력되는가를 이해하는 것이 핵심이다.

Game -> Direct3D 9 -> Device -> GPU -> Back Buffer -> Present -> 화면

DirectX란

DirectX는 Windows 환경에서 게임과 멀티미디어 프로그램을 개발하기 위한 API 집합이다.

그래픽만 담당하는 것이 아니라 입력, 사운드 등 다양한 기능을 포함한다.

그중 3D 그래픽을 담당하는 핵심 API가 Direct3D다.

DirectX 9에서는 Direct3D 9를 이용해 GPU를 제어하고 3D 장면을 렌더링한다.

Game -> DirectX -> Direct3D -> GPU

그래픽 프로그래밍을 공부할 때는 DirectX 전체보다 Direct3D의 렌더링 구조를 먼저 이해하는 것이 중요하다.


Direct3D Device

DirectX 9에서 중심이 되는 객체가 IDirect3DDevice9다.

Device는 렌더링 작업을 GPU에 전달하기 위한 인터페이스다.

IDirect3DDevice9* device;

화면을 지우고, 리소스를 설정하고, Shader와 Render State를 지정하고, 실제 Draw 호출을 수행하는 작업이 모두 Device를 통해 이루어진다.

기본적인 렌더링 흐름은 다음과 같다.

BeginScene -> Clear -> Set State -> Set Resource -> Draw -> EndScene -> Present

Device 자체가 게임의 모든 것을 관리하는 것은 아니다.

정확하게는 현재 렌더링에 필요한 GPU 상태와 리소스를 설정하고 Draw 명령을 전달하는 중심 인터페이스라고 이해하면 된다.


Game Loop

게임은 렌더링을 한 번만 수행하지 않는다.

입력을 처리하고 게임 상태를 업데이트한 다음 현재 상태를 화면에 렌더링하는 과정을 계속 반복한다.

Input -> Update -> Render -> Input -> Update -> Render -> 반복

Render 단계에서는 다시 Direct3D의 렌더링 과정이 실행된다.

Render -> BeginScene -> Clear -> Set State -> Draw -> EndScene -> Present

결국 게임의 한 프레임은 다음 구조로 이해할 수 있다.

Game Loop -> Input -> Update -> Render -> Present -> 다음 Frame

Vertex

3D 그래픽의 가장 기본적인 데이터가 Vertex다.

Vertex는 하나의 정점을 표현한다.

struct Vertex
{
    float x;
    float y;
    float z;
};

삼각형 하나를 만들려면 정점 세 개가 필요하다.

Vertex 0 -> Vertex 1 -> Vertex 2 -> Triangle

실제 게임에서는 위치만 저장하지 않는다.

법선, 색상, UV 등의 데이터가 함께 들어갈 수 있다.

struct Vertex
{
    float x, y, z;
    float nx, ny, nz;
    float u, v;
};

따라서 Vertex는 단순한 좌표 하나가 아니라 GPU가 정점을 처리하는 데 필요한 입력 데이터라고 볼 수 있다.


Vertex Buffer

Vertex 데이터를 GPU가 사용할 수 있도록 저장하는 것이 Vertex Buffer다.

DirectX 9에서는 IDirect3DVertexBuffer9를 사용한다.

Vertex Data -> Vertex Buffer -> GPU

많은 정점을 매번 개별적으로 전달하는 것이 아니라 버퍼에 저장해 두고 필요할 때 GPU가 사용하도록 한다.

CPU -> Vertex Buffer -> Vertex Processing

Index Buffer

3D 모델에서는 같은 Vertex를 여러 삼각형이 공유하는 경우가 많다.

이때 Vertex 데이터를 중복해서 저장하는 대신 Index를 사용한다.

Vertex Buffer -> 실제 Vertex 데이터
Index Buffer -> 사용할 Vertex 번호

예를 들어 사각형을 두 개의 삼각형으로 표현할 수 있다.

Vertices -> 0, 1, 2, 3
Indices -> 0, 1, 2 -> 2, 1, 3

이렇게 하면 공유되는 정점을 중복 저장하지 않아도 된다.

Vertex Buffer + Index Buffer -> Triangle

모델의 규모가 커질수록 Index Buffer의 의미가 커진다.


Primitive

GPU는 Vertex를 그대로 화면에 그리는 것이 아니라 Primitive라는 기본 도형 단위로 해석한다.

대표적인 Primitive가 Triangle이다.

Vertex -> Primitive -> Triangle

DirectX 9에서는 다음과 같은 Draw API를 사용한다.

device->DrawPrimitive(...);
device->DrawIndexedPrimitive(...);

대부분의 3D 모델은 결국 수많은 삼각형의 집합으로 표현된다.

Mesh -> Vertex / Index -> Triangle -> Rasterization -> Pixel

Vertex Declaration

GPU가 Vertex Buffer의 데이터를 어떤 구조로 해석해야 하는지도 알려줘야 한다.

DirectX 9에서는 Vertex Declaration을 사용한다.

예를 들어 Vertex에 위치와 UV가 있다면 다음과 같이 구조를 정의할 수 있다.

Vertex -> Position + TexCoord

GPU 입장에서는 Vertex Buffer가 단순한 데이터 배열일 뿐이다.

따라서 각각의 데이터가 위치인지, 색상인지, UV인지 알려줘야 한다.

Vertex Buffer -> Vertex Declaration -> GPU가 Vertex 해석

Transform

3D 모델의 정점은 처음부터 화면 좌표를 가지고 있는 것이 아니다.

여러 공간을 거치면서 최종적인 화면 위치가 결정된다.

Local Space -> World Space -> View Space -> Clip Space -> Screen

DirectX 9에서는 대표적으로 세 가지 Matrix를 사용한다.

World Matrix -> 모델의 위치 / 회전 / 크기
View Matrix -> 카메라 기준으로 변환
Projection Matrix -> 3D 공간을 화면에 투영

전체적인 흐름은 다음과 같다.

Vertex -> World Matrix -> View Matrix -> Projection Matrix -> Rasterization

이 구조는 3D 그래픽을 이해하는 데 핵심적인 부분이다.


World Matrix

모델의 로컬 좌표를 월드 공간에 배치한다.

Model -> World Matrix -> World Space

위치, 회전, 크기 등의 정보를 통해 게임 월드에서 모델이 어디에 존재하는지를 결정한다.


View Matrix

카메라를 기준으로 월드 공간을 바라보는 좌표계로 변환한다.

World Space -> View Matrix -> Camera Space

카메라의 위치와 방향이 바뀌면 같은 모델이라도 화면에서 보이는 위치가 달라진다.


Projection Matrix

카메라 공간의 3D 데이터를 화면에 투영한다.

View Space -> Projection Matrix -> Clip Space

Perspective Projection을 사용하면 가까운 물체는 크게 보이고 먼 물체는 작게 보인다.

3D Space -> Projection -> 2D Screen

Texture

모델 표면에 이미지를 입히기 위해 Texture를 사용한다.

DirectX 9에서는 IDirect3DTexture9를 사용한다.

Mesh + Texture -> Textured Object

렌더링 전에 Device에 Texture를 연결한다.

device->SetTexture(0, texture);

하지만 단순히 Texture를 연결하는 것만으로는 어떤 부분에 어떤 위치의 이미지를 사용할지 알 수 없다.

여기에서 UV 좌표가 필요하다.


UV Coordinate

UV는 Texture의 어느 위치를 사용할 것인지 나타내는 좌표다.

일반적으로 0~1 범위로 표현한다.

(0,0) -> Texture의 왼쪽 위
(1,0) -> Texture의 오른쪽 위
(0,1) -> Texture의 왼쪽 아래
(1,1) -> Texture의 오른쪽 아래

Vertex마다 UV 좌표를 가지고 있으면 GPU가 삼각형 내부의 픽셀에 적절한 Texture 좌표를 계산한다.

Vertex -> UV -> Texture Sampling -> Pixel Color

모델링 툴에서 UV를 펼치는 작업도 결국 이 구조를 위한 것이다.


Shader

Shader는 GPU에서 실행되는 프로그램이다.

DirectX 9에서는 대표적으로 Vertex Shader와 Pixel Shader를 사용한다.

Vertex Data -> Vertex Shader -> Rasterization -> Pixel Shader -> Output

Vertex Shader

정점 단위의 처리를 담당한다.

대표적으로 Matrix를 이용한 좌표 변환을 수행한다.

Vertex -> Vertex Shader -> Transformed Vertex

Pixel Shader

Rasterization을 통해 생성된 Pixel 단위의 색상을 계산한다.

Texture, Lighting, Special Effect 등을 처리할 수 있다.

Pixel Data + Texture + Lighting -> Pixel Shader -> Final Color

DirectX 9에서는 HLSL로 Shader를 작성할 수 있다.

HLSL -> Compile -> Shader -> GPU

Fixed Function Pipeline

DirectX 9에서는 Shader를 사용하지 않고 미리 정해진 기능을 사용하는 Fixed Function Pipeline도 존재한다.

Vertex -> Fixed Function -> Rasterization -> Fixed Function -> Pixel

Shader 기반으로 작성하면 일부 렌더링 과정을 직접 제어할 수 있다.

Vertex -> Vertex Shader -> Rasterization -> Pixel Shader -> Pixel

DirectX 9는 두 방식을 모두 볼 수 있기 때문에 그래픽 API의 발전 과정을 이해하기에도 좋은 편이다.


Render State

DirectX 9는 상태 기반으로 동작한다.

현재 Device에 어떤 상태가 설정되어 있는지가 이후 Draw 결과에 영향을 준다.

예를 들어 Alpha Blending을 활성화할 수 있다.

device->SetRenderState(
    D3DRS_ALPHABLENDENABLE,
    TRUE
);

Depth Test 역시 Render State를 통해 설정한다.

Set Render State -> Set Texture -> Set Shader -> Draw

따라서 DirectX 9 렌더링 코드를 볼 때는 현재 Device에 어떤 상태가 설정되어 있는가를 같이 봐야 한다.


Depth Buffer

3D 공간에서는 여러 물체가 같은 화면 영역에 겹쳐질 수 있다.

이때 카메라와 가까운 물체가 앞에 보이도록 처리해야 한다.

Depth Buffer, 흔히 Z-Buffer가 이 역할을 한다.

Pixel -> Depth Test -> 통과 -> 화면에 기록
Pixel -> Depth Test -> 실패 -> 버림

각 Pixel의 깊이 값을 비교하기 때문에 3D 물체의 앞뒤 관계를 처리할 수 있다.

Geometry -> Rasterization -> Depth Test -> Final Pixel

Back Buffer

게임에서 화면에 직접 렌더링하면 렌더링 중간 결과가 사용자에게 보일 수 있다.

그래서 Back Buffer에 먼저 한 Frame을 완성한다.

Rendering -> Back Buffer -> 완성된 Frame

완성된 Frame을 Present()를 통해 화면에 표시한다.

Back Buffer -> Present -> Display

전체적으로 보면 Double Buffering은 다음과 같다.

Render -> Back Buffer -> Present -> Display

렌더링 파이프라인

DirectX 9의 핵심을 하나로 묶으면 다음과 같은 흐름으로 볼 수 있다.

Vertex / Index Data -> Vertex Processing -> Rasterization -> Pixel Processing -> Depth / Blend -> Back Buffer -> Present -> Screen

Shader를 사용하는 일반적인 구조는 다음과 같다.

Vertex Buffer / Index Buffer -> Vertex Shader -> Rasterization -> Pixel Shader -> Depth / Blend -> Back Buffer -> Present -> Screen

여기서 Texture는 Pixel Shader에서 샘플링되어 최종 색상 계산에 사용될 수 있다.

Vertex + Index -> Vertex Shader -> Rasterization -> Pixel Shader + Texture -> Depth / Blend -> Back Buffer -> Present -> Screen

Resource 관리

DirectX 9에서 사용하는 GPU 리소스는 명시적으로 생성하고 관리해야 한다.

대표적인 리소스는 다음과 같다.

Device -> Vertex Buffer / Index Buffer / Texture / Shader

DirectX 9의 COM 기반 객체는 Reference Counting을 사용한다.

따라서 사용이 끝난 객체는 적절하게 Release해야 한다.

texture->Release();
vertexBuffer->Release();
device->Release();

C++에서는 이를 RAII 방식으로 감싸서 수명 관리를 자동화하는 방법도 사용할 수 있다.


Lost Device

DirectX 9에서 특징적인 개념 중 하나가 Device Lost다.

특정 상황에서 Device를 정상적으로 사용할 수 없는 상태가 발생할 수 있으며, 이 경우 Device 상태를 확인하고 필요한 리소스를 다시 준비해야 한다.

Rendering -> Device Lost -> Rendering 중단 -> Device Reset -> Resource 복구 -> Rendering 재개

특히 D3DPOOL_DEFAULT에 속하는 리소스는 Device Reset과 관련하여 다시 생성해야 하는 경우가 있다.

DirectX 9 코드에서 Device Reset 처리가 복잡해지는 이유다.


Win32와 DirectX 9

DirectX 9 자체가 Windows의 Window 시스템을 대신하는 것은 아니다.

일반적으로 Win32를 통해 창을 만들고 Direct3D를 해당 창에 연결한다.

Win32 -> Window / HWND -> Direct3D Device -> Rendering -> Back Buffer -> Present -> Window

게임 프로그램 전체를 보면 다음과 같은 관계가 된다.

Win32 -> Window / Input
Game -> Game Logic / Resource / Scene
Direct3D -> Rendering
GPU -> 실제 그래픽 처리

따라서 Windows 게임 개발에서는 Win32와 DirectX를 함께 이해하면 구조가 명확해진다.


DirectX 9 개발 구조

실제 게임 프로그램을 단순화하면 다음과 같은 구조로 볼 수 있다.

Game -> Input -> Update -> Scene -> Render -> Present

Render 내부에서는 다음과 같은 작업이 수행된다.

Scene -> Mesh -> Vertex / Index -> Transform -> Texture / Shader / State -> Draw -> GPU Pipeline -> Back Buffer -> Present

이를 조금 더 세분화하면 다음과 같다.

Game State -> World / Camera -> Mesh -> Vertex / Index Buffer -> Transform -> Shader / Texture / Render State -> Draw -> Vertex Processing -> Rasterization -> Pixel Processing -> Depth / Blend -> Back Buffer -> Present -> Screen

이것이 DirectX 9를 공부할 때 가장 중요한 전체 흐름이다.


DirectX 9에서 잡아야 할 핵심

DirectX 9의 API를 전부 외울 필요는 없다.

다음 관계를 이해하는 것이 훨씬 중요하다.

Device -> Resource -> State -> Draw -> GPU Pipeline -> Back Buffer -> Present

Resource에는 대표적으로 다음이 들어간다.

Vertex Buffer
Index Buffer
Texture
Shader

State에는 다음과 같은 것들이 들어간다.

Render State
Transform
Texture State
Shader State

결국 게임이 GPU에 렌더링을 요청하는 구조는 다음으로 압축할 수 있다.

Data -> Resource -> State -> Draw -> GPU -> Back Buffer -> Present -> Screen

정리

DirectX 9를 이해하는 데 필요한 핵심 개념은 다음과 같다.

Direct3D Device
Vertex
Vertex Buffer
Index Buffer
Primitive
Vertex Declaration
Transform
Texture
UV
Shader
Render State
Depth Buffer
Back Buffer
Present
Resource Management
Device Lost

이 개념들은 각각 따로 존재하는 것이 아니다.

하나의 렌더링 과정에서 연결된다.

Mesh -> Vertex / Index -> Buffer -> Transform -> Shader -> Rasterization -> Pixel -> Depth / Blend -> Back Buffer -> Present -> Screen

게임 루프까지 포함하면 다음과 같다.

Input -> Update -> Render -> Vertex / Index -> Transform -> Shader -> Rasterization -> Pixel -> Depth / Blend -> Back Buffer -> Present -> 반복

DirectX 9의 핵심은 API 함수 암기가 아니다.

게임 데이터를 GPU가 처리할 수 있는 Resource로 준비하고, 필요한 State를 설정한 다음 Draw 명령을 통해 렌더링 파이프라인을 실행하는 구조를 이해하는 것이다.

이 구조를 잡아두면 DirectX 11이나 DirectX 12로 넘어가도 핵심 개념이 완전히 새로 등장하는 것이 아니라, 같은 그래픽 파이프라인을 더 명시적이고 세밀하게 제어하는 방향으로 발전했다는 것을 이해할 수 있다.