DirectX11 Shader 시스템 – Shader · VertexShader · PixelShader (블로그 기반 Step-by-step)

본 교재는 사용자가 제공한 블로그 글의 흐름을 최대한 보존하여, DirectX11에서 Shader 계층(Shader/VertexShader/PixelShader) 을 설계·구현·적용하는 전 과정을 step-by-step으로 정리했습니다. 코드/설명/점검/퀴즈/실습이 한 세트로 구성되어 있어, 그대로 프로젝트에 반영하거나 면접 대비 요약으로 사용할 수 있습니다.


0. 목표와 큰그림 (Big Picture)

  • 문제점: Game 클래스가 _vsBlob/_psBlob, ComPtr<ID3D11VertexShader/PixelShader>를 직접 들고 있고, HLSL 컴파일/바인딩 로직이 분산되어 관리가 어렵다.

  • 해결: 공통 기능을 묶은 추상 기반 클래스 Shader 와 파생 클래스 VertexShader, PixelShader 를 도입해 책임을 분리하고, 런타임 컴파일(Blob) → 셰이더 생성 → 바인딩 흐름을 일원화한다.

  • 핵심 아이디어

    1. Shader_device / _blob / 경로/이름 보관 + LoadShaderFromFile() 공통화
    2. 파생 클래스가 Create()에서 _blob을 이용해 실제 DX 객체 생성
    3. Game은 shared_ptr로 수명 관리, InputLayoutVS.GetBlob()으로 연결

결과: Game의 렌더링 초기화 코드가 짧아지고, Shader 교체/확장(Geometry/Material/CB/SRV 연계)이 쉬워진다.


1. 배경지식 압축

  • VS/PS: 정점 셰이더(Vertex Shader)는 정점 변환/속성 전달을, 픽셀 셰이더(Pixel Shader)는 픽셀 색 계산을 담당.
  • Blob: D3DCompileFromFile()의 출력. 컴파일된 바이트코드로, CreateVertexShader()/CreatePixelShader()의 입력.
  • 모델 버전: vs_5_0, ps_5_0 등. (DX11 기준 일반적으로 5_0/5_1 사용)
  • 컴파일 플래그: D3DCOMPILE_DEBUG | D3DCompile_SKIP_OPTIMIZATION 등. 개발/릴리즈 분기 권장.
  • 주의: C++에는 abstract 키워드가 없다. 순수 가상= 0으로 표기한다.

2. 설계: Shader 계층 구조

2.1 추상 기반 클래스 Shader

  • 책임: 공용 상태와 공용 기능 보유

    • _device (ComPtr)
    • _blob (ComPtr)
    • _path (wstring), _name (string)
    • LoadShaderFromFile(path, name, version)
    • GetBlob() (InputLayout 생성에 사용)
  • 파생 클래스가 구현할 것: Create(path, name, version)

// Shader.h
#pragma once

class Shader {
public:
    Shader(ComPtr<ID3D11Device> device);
    virtual ~Shader();

    // C++ 표준: abstract 키워드 대신 순수 가상 = 0
    virtual void Create(const std::wstring& path,
                        const std::string&  name,
                        const std::string&  version) = 0;

    ComPtr<ID3DBlob> GetBlob() { return _blob; }

protected:
    void LoadShaderFromFile(const std::wstring& path,
                            const std::string&  name,
                            const std::string&  version);

protected:
    std::wstring         _path;   // HLSL 파일 경로
    std::string          _name;   // 엔트리(예: "VS", "PS")
    ComPtr<ID3D11Device> _device;
    ComPtr<ID3DBlob>     _blob = nullptr;
};
// Shader.cpp
#include "pch.h"
#include "Shader.h"

Shader::Shader(ComPtr<ID3D11Device> device) : _device(device) {}
Shader::~Shader() {}

void Shader::LoadShaderFromFile(const std::wstring& path,
                                const std::string&  name,
                                const std::string&  version)
{
    _path = path; _name = name;

    UINT compileFlag = D3DCOMPILE_ENABLE_STRICTNESS;
#ifdef _DEBUG
    compileFlag |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif

    HRESULT hr = ::D3DCompileFromFile(
        path.c_str(),
        nullptr,
        D3D_COMPILE_STANDARD_FILE_INCLUDE,
        name.c_str(),
        version.c_str(),
        compileFlag,
        0,
        _blob.GetAddressOf(),
        nullptr);
    CHECK(hr);
}

블로그 맥락 유지: D3DCompileFromFile()를 공통화하고 Blob을 Shader가 보관. 이후 파생 클래스에서 이 Blob으로 DX 객체를 만든다.


2.2 파생: VertexShader / PixelShader

// Shader.h 하단에 함께 선언 (혹은 별도 파일)
class VertexShader : public Shader {
    using Super = Shader;
public:
    VertexShader(ComPtr<ID3D11Device> device);
    ~VertexShader();

    ComPtr<ID3D11VertexShader> GetComPtr() { return _vertexShader; }

    void Create(const std::wstring& path,
                const std::string&  name,
                const std::string&  version) override;

protected:
    ComPtr<ID3D11VertexShader> _vertexShader = nullptr;
};

class PixelShader : public Shader {
    using Super = Shader;
public:
    PixelShader(ComPtr<ID3D11Device> device);
    ~PixelShader();

    ComPtr<ID3D11PixelShader> GetComPtr() { return _pixelShader; }

    void Create(const std::wstring& path,
                const std::string&  name,
                const std::string&  version) override;

protected:
    ComPtr<ID3D11PixelShader> _pixelShader = nullptr;
};
// VertexShader.cpp
#include "pch.h"
#include "Shader.h"

VertexShader::VertexShader(ComPtr<ID3D11Device> device) : Super(device) {}
VertexShader::~VertexShader() {}

void VertexShader::Create(const std::wstring& path,
                          const std::string&  name,
                          const std::string&  version)
{
    LoadShaderFromFile(path, name, version); // _blob 채움

    HRESULT hr = _device->CreateVertexShader(
        _blob->GetBufferPointer(),
        _blob->GetBufferSize(),
        nullptr,
        _vertexShader.GetAddressOf());
    CHECK(hr);
}
// PixelShader.cpp
#include "pch.h"
#include "Shader.h"

PixelShader::PixelShader(ComPtr<ID3D11Device> device) : Super(device) {}
PixelShader::~PixelShader() {}

void PixelShader::Create(const std::wstring& path,
                         const std::string&  name,
                         const std::string&  version)
{
    LoadShaderFromFile(path, name, version); // _blob 채움

    HRESULT hr = _device->CreatePixelShader(
        _blob->GetBufferPointer(),
        _blob->GetBufferSize(),
        nullptr,
        _pixelShader.GetAddressOf());
    CHECK(hr);
}

블로그의 흐름과 동일: LoadShaderFromFile()로 Blob 생성 후, Create*Shader()로 DX 객체를 만든다.


2.3 (선행 정의) ShaderScope 비트 플래그

  • 상수 버퍼/리소스(SRV)를 어느 스테이지에 바인딩할지를 가리키는 용도. (후속 파트에서 활용)
// ShaderScope.h (선택)
enum ShaderScope : uint32_t {
    SS_None         = 0,
    SS_VertexShader = 1 << 0,
    SS_PixelShader  = 1 << 1,
    SS_Both         = SS_VertexShader | SS_PixelShader,
};

3. Game 통합 – 단계별 마이그레이션

3.1 pch.h 추가

#include "Shader.h"      // 기반/파생 모두 포함
#include <d3dcompiler.h>  // D3DCompileFromFile 사용

3.2 Game.h 교체

  • 이전(삭제)
// VS
ComPtr<ID3D11VertexShader> _vertexShader = nullptr;
ComPtr<ID3DBlob>           _vsBlob       = nullptr;
// PS
ComPtr<ID3D11PixelShader>  _pixelShader = nullptr;
ComPtr<ID3DBlob>           _psBlob      = nullptr;
  • 이후(도입)
std::shared_ptr<VertexShader> _vertexShader;
std::shared_ptr<PixelShader>  _pixelShader;

3.3 Game::Init 생성 & 컴파일

_vertexShader = std::make_shared<VertexShader>(_graphics->GetDevice());
_pixelShader  = std::make_shared<PixelShader>(_graphics->GetDevice());

// HLSL 컴파일 + DX 객체 생성
_vertexShader->Create(L"Default.hlsl", "VS", "vs_5_0");
_pixelShader->Create(L"Default.hlsl", "PS", "ps_5_0");

3.4 InputLayout 생성 연결 (vsBlob → GetBlob)

// 기존: _inputLayout->Create(layout, _vsBlob);
_inputLayout->Create(VertexTextureData::descs, _vertexShader->GetBlob());

3.5 Render 단계 바인딩 수정

// 기존: dc->VSSetShader(_vertexShader.Get(), nullptr, 0);
//      dc->PSSetShader(_pixelShader.Get(),  nullptr, 0);

auto* dc = _graphics->GetDeviceContext().Get();

dc->VSSetShader(_vertexShader->GetComPtr().Get(), nullptr, 0);
dc->PSSetShader(_pixelShader->GetComPtr().Get(),  nullptr, 0);

여기까지 적용하면, 기존과 동일하게 화면이 출력되어야 한다. (InputLayout/VB/IB/Topology/DrawIndexed 흐름은 동일)


4. 자주 난관 & 해결 체크

  1. LINK 에러(정의 누락)
  • 원인: VertexShader/PixelShader 생성자/소멸자/함수 정의 누락
  • 해결: .cpp에 구현 추가 (위 예시처럼)
  1. D3DCompileFromFile 실패
  • 원인: 경로/작업 디렉터리 문제, 파일 인코딩, #include 경로
  • 해결: 절대경로/리소스 경로 확인, 프로젝트 작업 디렉터리 설정, D3D_COMPILE_STANDARD_FILE_INCLUDE 유지
  1. InputLayout 불일치
  • 원인: 정점 구조체 ↔ 레이아웃 ↔ HLSL 시그니처 불일치
  • 해결: 시맨틱명(POSITION,TEXCOORD,COLOR 등), 포맷, 오프셋(또는 D3D11_APPEND_ALIGNED_ELEMENT) 1:1 매칭
  1. 성능 이슈(디버그 플래그)
  • 원인: D3DCOMPILE_DEBUG | SKIP_OPTIMIZATION
  • 해결: Release에서는 최적화 플래그 사용, 디버그 플래그 제외
  1. 셰이더 모델/드라이버 이슈
  • 원인: 오래된 하드웨어/드라이버, 잘못된 SM 버전
  • 해결: vs_5_0/ps_5_0 사용, 드라이버 최신화

5. 학습 점검 체크리스트

  • Game이 VS/PS Blob과 ComPtr을 직접 들고 있지 않고, Shader 계층이 보유하는가
  • LoadShaderFromFile() 호출이 파생 클래스 Create() 내부로 몰려있는가
  • InputLayout->Create(..., VS.GetBlob())Blob 전달 경로가 일원화되었는가
  • Render에서 VSSetShader/PSSetShader래퍼의 GetComPtr()으로 호출하는가
  • 디버그/릴리즈 컴파일 플래그 분기를 적용했는가

6. 45초 면접 답변 스크립트

“우리는 Shader 기반 클래스로 HLSL 컴파일과 Blob 관리를 공통화했고, VertexShader/PixelShader 파생 클래스가 Create에서 Blob으로 실제 DX 객체를 생성합니다. Game은 shared_ptr로 VS/PS를 보유하고, InputLayout은 VS.GetBlob()으로 시그니처를 검증합니다. Render에서는 VSSetShader/PSSetShader만 호출하면 되기 때문에 Game 초기화 코드는 간결해지고, 셰이더 교체나 확장이 쉬워졌습니다.”


7. 미니 퀴즈

  1. C++에서 추상 메서드를 선언하는 올바른 표기는?
  • 정답: virtual void Create(...) = 0;
  1. InputLayout 생성 시 필요한 핵심 정보 2가지는?
  • 정답: D3D11_INPUT_ELEMENT_DESC 배열, 정점 셰이더 Blob
  1. 디버그 빌드에서 유용한 D3DCompile 플래그 2가지는?
  • 정답: D3DCOMPILE_DEBUG, D3DCOMPILE_SKIP_OPTIMIZATION
  1. VS/PS 객체 생성 시 사용되는 Blob의 실제 필드 2가지는?
  • 정답: GetBufferPointer(), GetBufferSize()
  1. PSSetShader 호출 전 텍스처가 바인딩되지 않았다면 화면이 검게 나오는 이유는?
  • 정답: 픽셀 셰이더가 샘플링할 SRV가 없어서 유효한 색을 생성하지 못함

8. 실습 – 바로 적용하기

실습 A) 기존 Game의 VS/PS 필드를 Shader 계층으로 마이그레이션

  1. pch.h#include "Shader.h" 추가
  2. Game.h의 _vsBlob/_psBlobComPtr<ID3D11*Shader> 제거
  3. shared_ptr<VertexShader/PixelShader> 추가
  4. Game::Init에서 make_shared + Create() 호출
  5. InputLayout->Create(..., _vertexShader->GetBlob())로 변경
  6. Render에서 VSSetShader/PSSetShader 바인딩 변경

실습 B) VS/PS 엔트리 교체 실험

  • Default.hlsl의 VS/PS 엔트리 이름을 각각 VS2/PS2로 바꾸고, Create(..., "VS2") 등으로 교체해 동작 확인

실습 C) InputLayout-정점 구조-셰이더 시그니처 동시 수정

  • VertexTextureData{pos,uv}VertexColorData{pos,color}로 교체 후

    • descs를 COLOR 포맷으로 변경
    • VS 입력 시그니처도 COLOR로 일치시켜 정상 렌더 검증

9. 다음 단계 예고 (연계 편)

  • ConstantBuffer 템플릿으로 WVP 등 변환 행렬을 VS에 전달하고, Game::Update에서 CopyData()로 매 프레임 갱신
  • Texture 클래스로 SRV 생성/관리, PS에 바인딩 (PSSetShaderResources)
  • Rasterizer/Sampler/BlendState 클래스로 파이프라인 상태를 객체화
  • Material 레벨에서 VS/PS + CB/SRV + State를 묶어, MeshRenderer가 한 줄 바인딩으로 그리도록 확장

10. 한 장 요약 (Cheat Sheet)

  • Shader = 공통: LoadShaderFromFile()로 Blob 보관
  • VertexShader/PixelShader = Create()에서 Blob → DX 객체화
  • Game = VS/PS를 shared_ptr로 보유, InputLayout은 VS Blob 사용
  • Render = VSSetShader/PSSetShader 바인딩
  • 불일치/경로/플래그/LINK 에러 규칙적으로 점검

이 문서는 블로그 예제의 코드/흐름/명명을 기준으로 구성되었습니다. 그대로 적용하면 Shader 계층 정리가 끝나며, 이어지는 ConstantBuffer/Texture/상태 객체/머티리얼 통합까지 무리 없이 확장할 수 있습니다.

profile
李家네_공부방

0개의 댓글