컨텐츠 검색
[DirectX 11] 2. 2D 렌더링의 기초: Resource-View 분리 구조와 SpriteBatch

2026. 1. 26. 00:35DirectX 스터디

지난 포스팅에서는 Device, Context, SwapChain을 초기화하여 파란색 화면을 띄우는 데 성공했습니다. 이번에는 이 빈 캔버스 위에 2D 이미지를 그리는(Rendering) 과정을 분석해보았습니다.

 

단순히 Draw 함수를 호출하면 될 것 같지만, DirectX 11은 "리소스(데이터)와 뷰(해석)의 분리"라는 독특한 철학을 가지고 있어 이에 대한 이해가 선행되어야 합니다.


1. 핵심 학습 키워드

① Texture vs Shader Resource View (SRV)

  • Texture (리소스): 램(VRAM)에 올라간 순수한 이미지 데이터 덩어리입니다. 하지만 GPU는 이 데이터가 그림인지, 단순한 숫자인지, 깊이 정보인지 알 수 없습니다.
  • Shader Resource View (SRV): GPU(셰이더)에게 "이 데이터를 텍스처(이미지)로 해석해서 사용해라"라고 알려주는 기술서(Descriptor)입니다.
  • 핵심: DirectX 11에서는 텍스처를 직접 파이프라인에 바인딩할 수 없으며, 반드시 뷰(SRV)를 통해야 합니다.

② SpriteBatch (스프라이트 배치)

  • 정의: 2D 이미지를 효율적으로 그리기 위한 DirectXTK의 헬퍼 클래스입니다.
  • 역할: 매번 그리기 명령(Draw Call)을 보내는 대신, 여러 스프라이트를 모아두었다가 한 번에 처리(Batching)하여 CPU 오버헤드를 획기적으로 줄여줍니다. 또한 복잡한 셰이더 설정과 렌더링 상태(Render State) 설정을 자동으로 처리해줍니다.

③ WIC vs DDS

  • WIC (Windows Imaging Component): png, jpg 같은 일반적인 이미지 포맷을 로드할 때 사용합니다. CPU 친화적입니다.
  • DDS (DirectDraw Surface): GPU가 읽기 최적화된 포맷입니다. 압축 텍스처(BCn)나 밉맵(Mipmap)을 포함할 수 있어 실제 게임 개발에서는 DDS 사용이 권장됩니다.

2. 데이터의 로딩: CPU에서 GPU로

이미지를 화면에 띄우기 위해서는 하드디스크에 있는 파일을 읽어 GPU 메모리로 전송해야 합니다. DirectXTK의 WICTextureLoader가 이 복잡한 과정을 담당합니다.

// Game.h - 변수 선언
// 리소스(Texture2D)를 직접 들고 있는 것이 아니라, 
// 뷰(SRV)를 가리키는 스마트 포인터(ComPtr)를 멤버로 가집니다.
Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> m_texture;

// Game.cpp - CreateDeviceDependentResources()
// 로더를 이용해 cat.png 파일을 로드하고, m_texture에 SRV 주소를 저장합니다.
#include "directxtk/WICTextureLoader.h"

DX::ThrowIfFailed(
    CreateWICTextureFromFile(device, L"cat.png", nullptr, 
    m_texture.ReleaseAndGetAddressOf())
);

 

분석:
여기서 주목할 점은 CreateWICTextureFromFile 함수가 내부적으로 Texture2D 리소스를 생성하고, 이를 가리키는 SRV까지 생성해서 우리에게 돌려준다는 점입니다. 우리는 이 SRV 핸들만 있으면 언제든 셰이더에 텍스처를 공급할 수 있습니다.


3. 렌더링 파이프라인과 좌표계

Begin()과 End()의 미학

SpriteBatch를 사용할 때는 반드시 Begin()으로 시작해 End()로 끝나야 합니다. 이 구조는 단순한 문법이 아니라 렌더링 상태(Render State) 관리와 밀접한 연관이 있습니다.

  • Begin(): 렌더링 파이프라인을 2D 그리기에 적합한 상태(알파 블렌딩 켜기, 깊이 버퍼 끄기, 컬링 끄기 등)로 설정합니다.
// m_states는 std::unique_ptr<CommonStates> 객체라고 가정합니다.

m_spriteBatch->Begin(
    SpriteSortMode_Deferred,               // 0. 정렬 모드 (기본값)
    m_states->NonPremultiplied(),        // 1. 알파 블렌딩 켜기 (투명한 배경 처리)
    nullptr,                                                   // 2. 샘플러 (기본값 사용)
    m_states->DepthNone(),                  // 3. 깊이 버퍼 끄기 (Z-Buffer 무시, 항상 위에 그림)
    m_states->CullNone()                       // 4. 컬링 끄기 (앞/뒷면 모두 그림)
);
  • Draw(): 즉시 그리지 않고, 정점 정보를 메모리에 차곡차곡 쌓습니다(Batching).
  • End(): 쌓아둔 스프라이트들을 한꺼번에 GPU로 보내고(Flush), 원래의 렌더링 상태로 복구합니다.

화면 좌표계 (Screen Coordinate)

3D 공간과 달리, 2D 스프라이트는 스크린 좌표계를 따릅니다.

  • 원점 (0, 0): 화면의 왼쪽 위 (Top-Left)
  • X축: 오른쪽으로 갈수록 증가
  • Y축: 아래로 갈수록 증가 (일반적인 수학 좌표계와 반대)


4. 실전 코드 분석: Draw

실제 Render 함수에서 이미지를 출력하는 코드를 분석해보았습니다.

void Game::Render()
{
    m_deviceResources->Clear(); 

    // 1. 파이프라인 준비 (상태 설정)
    m_spriteBatch->Begin();

    // 2. 그리기 명령 예약 (Batching)
    // 텍스처 뷰(SRV), 위치(x, y), 색상(Tint)을 전달합니다.
    // Colors::White는 텍스처 원래 색상을 그대로 사용한다는 의미입니다.
    m_spriteBatch->Draw(m_texture.Get(), m_screenPos, Colors::White);

    // 3. 일괄 제출 및 상태 복구
    m_spriteBatch->End();

    m_deviceResources->Present();
}

 

ComPtr의 활용:
m_texture.Get()을 통해 Raw Pointer를 Draw 함수에 전달합니다. ComPtr은 리소스의 수명(Reference Counting)을 자동으로 관리해주므로, 메모리 누수(Memory Leak) 걱정 없이 안전하게 개발할 수 있습니다.


5. 마무리

이번 학습을 통해 다음과 같은 사실을 정립했습니다.

  1. DirectX 11에서 텍스처는 Resource(데이터)View(인터페이스)로 철저히 분리되어 관리된다.
  2. SpriteBatchBegin/End 쌍을 통해 값비싼 렌더링 상태 변경 비용을 최소화(Batching)한다.
  3. 좌표계는 좌상단이 (0,0)인 스크린 좌표계를 사용한다.

이제 정적인 이미지를 띄우는 데 성공했습니다. 다음 포스팅에서는 이 이미지의 일부분만 잘라내거나(RECT), 위치를 계속 변경하여 살아 움직이는 애니메이션을 구현하는 방법을 다루겠습니다.