DirectX11/Eternal Return 모작

[DirectX 11 Eternal Return 모작] 4. Shader & Material

Vfly 2025. 9. 2. 04:57

전체 코드 : https://github.com/HyangRim/DirectX11-Engine-Client

 

GitHub - HyangRim/DirectX11-Engine-Client

Contribute to HyangRim/DirectX11-Engine-Client development by creating an account on GitHub.

github.com


이번에는 셰이더 프로그래밍과 Material 시스템에 대해 알아보겠다.

 

Shader란?

셰이더(Shader)는 GPU에서 실행되는 프로그램으로, 3D 그래픽스의 렌더링 파이프라인에서 정점(Vertex)과 픽셀(Fragment/Pixel)을 처리하는 역할을 한다.

  • Vertex Shader : 정점의 위치변환, Lighting 계산
  • Pixel Shader : 픽셀 색상 결정, 텍스쳐 샘플링
  • Geometry Shader :  정점 데이터를 기반으로 새로운 Geometry 생성

Material란?

Material은 오브젝트의 표면 특성을 정의하는 데이터 집합으로, 셰이더와 텍스쳐, 그리고 다양한 렌더링 속성들을 포함한다.

 

 

Material 시스템 구현

Material 클래스 구조

enum class RenderQueue 
{
    Opaque,      // 불투명 객체
    Cutout,      // 알파 테스트용
    Transparent, // 투명 객체
    Max
};

enum class RenderingMode 
{
    Forward,     // Forward 렌더링
    Deferred,    // Deferred 렌더링
    Transparent  // 투명 렌더링
};

class Material : public ResourceBase 
{
public:
    Material();
    virtual ~Material();

    // 쉐이더 설정
    void SetShader(shared_ptr<Shader> _shader);
    shared_ptr<Shader> GetShader() { return m_shader; }
    
    // 텍스처 설정
    void SetDiffuseMap(shared_ptr<Texture> _diffuseMap) { m_diffuseMap = _diffuseMap; }
    void SetNormalMap(shared_ptr<Texture> _normalMap) { m_normalMap = _normalMap; }
    void SetSpecularMap(shared_ptr<Texture> _specularMap) { m_specularMap = _specularMap; }
    
    // 렌더링 모드 설정
    void SetRenderingMode(RenderingMode _mode) { m_renderMode = _mode; }
    void SetRenderQueue(RenderQueue _renderQueue) { m_renderQueue = _renderQueue; }
    void SetTransparent(bool _transparent) { m_isTransparent = _transparent; }

private:
    MaterialDesc m_desc;
    RenderQueue m_renderQueue = RenderQueue::Opaque;
    RenderingMode m_renderMode = RenderingMode::Deferred;
    bool m_isTransparent = false;
    
    shared_ptr<Shader> m_shader;
    shared_ptr<Texture> m_diffuseMap;
    shared_ptr<Texture> m_normalMap;
    shared_ptr<Texture> m_specularMap;
};

 

 

HLSL 셰이더 시스템

글로벌 셰이더 설정 (Global.fx)

// 상수 버퍼 정의
cbuffer GlobalBuffer
{
    matrix V;           // View 행렬
    matrix P;           // Projection 행렬
    matrix VP;          // View-Projection 행렬
    matrix Vinv;        // View 역행렬
    float3 CamPos;      // 카메라 위치
    float padding;
};

cbuffer TransformBuffer
{
    matrix W;           // World 행렬
};

// 공통 샘플러 상태
SamplerState LinearSampler
{
    Filter = MIN_MAG_MIP_LINEAR;
    AddressU = Wrap;
    AddressV = Wrap;
};

// 블렌드 상태 정의
BlendState AlphaBlend
{
    AlphaToCoverageEnable = false;
    BlendEnable[0] = true;
    SrcBlend[0] = SRC_ALPHA;
    DestBlend[0] = INV_SRC_ALPHA;
    BlendOp[0] = ADD;
};

 

Lighting 시스템 (Light.fx)

struct LightDesc
{
    float4 ambient;
    float4 diffuse;
    float4 specular;
    float4 emissive;
    float3 direction;
    float padding;
};

// 라이팅 계산 함수
float4 ComputeLight(float3 _normal, float2 _uv, float3 _worldPosition, float shadow = 0)
{
    float4 ambientColor = 0;
    float4 diffuseColor = 0;
    float4 specularColor = 0;
    float4 emissiveColor = 0;
    
    float4 baseTexture = DiffuseMap.Sample(LinearSampler, _uv);
    
    // Ambient 계산
    ambientColor = baseTexture * GlobalLight.ambient * Material.ambient;
    
    // Diffuse 계산
    float value = saturate(dot(-GlobalLight.direction, normalize(_normal)));
    diffuseColor = baseTexture * value * GlobalLight.diffuse * Material.diffuse;
    
    // Specular 계산
    float3 R = reflect(GlobalLight.direction, _normal);
    float3 E = normalize(CameraPosition() - _worldPosition);
    float specular = pow(saturate(dot(R, E)), 10);
    specularColor = GlobalLight.specular * Material.specular * specular;
    
    // 최종 색상 합성
    float4 finalColor = ambientColor + (diffuseColor + specularColor) * shadow;
    return finalColor;
}

 

최종 색상 공식은 퐁 셰이딩 식을 이용하였다.

출처 : 위키피디아

 

 

Forward vs Deffered 렌더링 구현

Forward 렌더링은 각 객체를 순서대로 렌더링하며 즉시 Lighting을 계산한다.

// Forward 렌더링 픽셀 쉐이더
float4 PS_FOW(MeshOutput input) : SV_TARGET
{
    // 그림자 계산
    float shadow = CalcShadowFactor(ShadowMap, input.shadowPosH);
    
    // 즉시 라이팅 계산
    float4 baseColor = ComputeLight(input.normal, input.uv, input.worldPosition, shadow);
    
    // FOW 효과 적용
    float fogFactor = CalculateFogOfWar(input.worldPosition);
    float3 finalColor = baseColor.rgb * max(fogFactor, 0.4f);
    
    return float4(finalColor, baseColor.a);
}

 

Deffered 렌더링은 G-Buffer에 데이터를 저장한 후 나중에 계산한다.

struct GBufferOutput
{
    float4 albedo : SV_Target0;      // 알베도 색상
    float4 normal : SV_Target1;      // 노멀 벡터
    float4 position : SV_Target2;    // 월드 위치
    float4 material : SV_Target3;    // 머티리얼 속성
};

GBufferOutput PS_GBuffer(MeshOutput input) 
{
    GBufferOutput output;
    
    // 텍스처 샘플링
    float4 albedo = DiffuseMap.Sample(LinearSampler, input.uv);
    
    if (albedo.a < 0.05f)
        discard;
    
    // G-Buffer 데이터 출력
    output.albedo = albedo;
    output.normal = float4(normalize(input.normal), 1.0f);
    output.position = float4(input.worldPosition, input.position.z);
    output.material = float4(1, 1, 1, 1);
    
    return output;
}

 

G-Buffer의 데이터를 사용하여 Lighting을 계산하는 풀 스크린 패스

// 디퍼드 라이팅용 라이팅 계산
float4 ComputeDeferredLight(float4 albedo, float3 normal, float3 worldPos, float shadow)
{
    float4 ambientColor = 0;
    float4 diffuseColor = 0;
    float4 specularColor = 0;

    // Ambient
    ambientColor = albedo * GlobalLight.ambient * shadow * 3.0f;
    
    // Diffuse
    float3 lightDir = -normalize(GlobalLight.direction);
    float NdotL = saturate(dot(normal, lightDir));
    diffuseColor = albedo * GlobalLight.diffuse * Material.diffuse * NdotL;
    
    // Specular
    if (NdotL > 0)
    {
        float3 reflectDir = reflect(-lightDir, normal);
        float3 viewDir = normalize(CamPos - worldPos);
        float spec = pow(saturate(dot(viewDir, reflectDir)), 10);
        specularColor = GlobalLight.specular * Material.specular * spec;
    }
    
    // 최종 색상 계산
    float4 finalColor = ambientColor + (diffuseColor + specularColor) * shadow;
    
    return finalColor;
}

// 디퍼드 라이팅 + FOW 픽셀 셰이더
float4 PS_DeferredLightingWithFOW(VertexQuadOutput input) : SV_Target
{
    int2 screenPos = (int2)(input.position.xy);
    
    // G-Buffer에서 데이터 로드
    float4 albedo = GBufferAlbedo.Load(int3(screenPos, 0));
    float4 normalData = GBufferNormal.Load(int3(screenPos, 0));
    float4 positionData = GBufferPosition.Load(int3(screenPos, 0));
    
    if (albedo.a < 0.01f)
        discard;
    
    float3 worldPos = positionData.xyz;
    float3 normal = normalize(normalData.xyz);
    
    // 섀도우 계산
    float4 shadowPosH = mul(float4(worldPos, 1.0f), ShadowTransform);
    float shadow = CalcShadowFactor(ShadowMap, shadowPosH);
    
    // 디퍼드 라이팅 계산
    float4 baseColor = ComputeDeferredLight(albedo, normal, worldPos, shadow);
    
    // FOW 계산 및 적용
    float fogFactor = CalculateFogOfWar(worldPos);
    
    // FOW 효과 적용
    float3 grayColor = dot(baseColor.rgb, float3(0.299, 0.587, 0.114));
    grayColor = grayColor * float3(0.7, 0.7, 0.8);
    
    float grayIntensity = saturate(g_smoothness * 0.5f);
    float3 foggedColor = lerp(baseColor.rgb, grayColor, (1.0f - fogFactor) * grayIntensity);
    
    float minBrightness = max(g_darkness, 0.4f);
    float3 finalColor = foggedColor * max(fogFactor, minBrightness);
    
    return float4(finalColor, baseColor.a) * 2.f;
}

 

 

렌더링 시스템 통합

RenderManager에서의 분기 처리

void RenderManager::RenderGeometryPass(vector<shared_ptr<GameObject>>& _gameObjects)
{
    // Deferred 렌더링: G-Buffer에 데이터 저장
    RenderMeshRendererDeferred(_gameObjects);
    RenderModelRendererDeferred(_gameObjects);
    RenderAnimRendererDeferred(_gameObjects);
}

void RenderManager::RenderDeferredLighting()
{
    if (m_deferredLightingShader == nullptr)
        return;
    
    // 풀스크린 쿼드로 라이팅 계산
    m_deferredLightingShader->SetTechnique(L"DeferredLightingTech");
    
    // G-Buffer 텍스처들을 바인딩
    auto srv0 = GRAPHICS->GetGBufferSRV(0); // Albedo
    auto srv1 = GRAPHICS->GetGBufferSRV(1); // Normal  
    auto srv2 = GRAPHICS->GetGBufferSRV(2); // Position
    auto srv3 = GRAPHICS->GetGBufferSRV(3); // Material
    
    // 라이팅 패스 실행
    m_deferredLightingShader->DrawIndexed(0, 0, 6);
}

// 투명 객체는 Forward 렌더링
void RenderManager::RenderForward(vector<shared_ptr<GameObject>>& _gameObjects, bool _isShadowTech) 
{
    // 투명 객체들을 Forward 방식으로 렌더링
    for (auto& obj : _gameObjects) {
        if (obj->GetRenderer() && obj->GetRenderer()->GetMaterial()->IsTransparent()) {
            obj->GetRenderer()->Render(_isShadowTech);
        }
    }
}

 

 

Camera에서의 객체 분류

void Camera::SortGameObjects() 
{
    shared_ptr<Scene> scene = CURSCENE;
    const vector<shared_ptr<GameObject>>& gameObjects = scene->GetGameObjects();
    
    m_vecForward.clear();
    m_vecBackward.clear();
    
    for (auto& object : gameObjects) 
    {
        if (!object->GetActive()) continue;
        
        shared_ptr<Renderer> renderer = object->GetRenderer();
        if (renderer == nullptr) continue;
        
        RenderQueue renderQueue = renderer->GetMaterial()->GetRenderQueue();
        
        switch (renderQueue) 
        {
        case RenderQueue::Opaque:
        case RenderQueue::Cutout:
            m_vecForward.push_back(object);  // Deferred 렌더링 대상
            break;
        case RenderQueue::Transparent:
            m_vecBackward.push_back(object); // Forward 렌더링 대상
            break;
        }
    }
}

 

 

 

고급 셰이더 효과 구현

Fog of War 구현

float CalculateFogOfWar(float3 worldPos) {
    float distance = length(worldPos - g_playerWorldPos);
    
    if (distance > g_sightRange * 1.2f) {
        return max(g_darkness, 0.4f);
    }
    
    float fogFactor = 1.0f;
    
    if (distance > g_sightRange) {
        fogFactor = max(g_darkness, 0.4f);
    }
    else if (distance > (g_sightRange - g_fadeDistance)) {
        float fadeRatio = (distance - (g_sightRange - g_fadeDistance)) / g_fadeDistance;
        fadeRatio = smoothstep(0.0f, 1.0f, fadeRatio);
        fadeRatio = pow(fadeRatio, g_smoothness);
        fogFactor = lerp(1.0f, max(g_darkness, 0.4f), fadeRatio);
    }
    
    return fogFactor;
}

 

아웃라인 포스트 프로세싱

아이템 박스 등의 특별한 객체에 외곽선 효과를 적용할때 씀

float4 PS_OutlinePost(VertexQuadOutput input) : SV_Target {
    float2 screenSize = float2(1366, 768);
    float2 texelSize = 1.0f / screenSize;
    
    float3 normalSample[9];
    float3 positionSample[9];
    
    int idx = 0;
    [unroll]
    for (int y = -1; y <= 1; ++y) {
        [unroll]
        for (int x = -1; x <= 1; ++x) {
            float2 offset = float2(x, y) * texelSize * 0.01f;
            normalSample[idx] = gNormalBuffer.Sample(LinearSampler, input.uv + offset).xyz;
            positionSample[idx] = gPositionBuffer.Sample(LinearSampler, input.uv + offset).xyz;
            idx++;
        }
    }
    
    // 외곽선 판정
    float normalDiff = 0;
    float positionDiff = 0;
    
    [unroll]
    for (int i = 0; i < 9; i++) {
        if (i == 4) continue;
        normalDiff += distance(normalSample[4], normalSample[i]);
        positionDiff += distance(positionSample[4], positionSample[i]);
    }
    
    if (normalDiff > 0.1f || positionDiff > 0.5f) {
        return float4(1, 1, 1, 1); // 외곽선 색상
    }
    
    return float4(0, 0, 0, 0); // 투명
}

 

 

 

UI ScrollView 셰이더

// UI용 클리핑 시스템
cbuffer ScrollViewClippingBuffer : register(b10)
{
    float4 ScrollViewClippingRect; // x=left, y=top, z=right, w=bottom
    bool ScrollViewEnableClipping;
    float3 ScrollViewClippingPadding;
};

// 클리핑이 적용된 픽셀 셰이더
float4 PS_Clipped(VertexOutput2 input) : SV_TARGET
{
    // 스크린 좌표로 변환
    float2 screenPos = input.screenPos.xy / input.screenPos.w;
    screenPos = screenPos * 0.5f + 0.5f;
    screenPos.y = 1.0f - screenPos.y;
    
    screenPos.x *= 1366.0f;
    screenPos.y *= 768.0f;
    
    // 클리핑 영역 체크
    if (ScrollViewEnableClipping)
    {
        if (screenPos.x < ScrollViewClippingRect.x || screenPos.x > ScrollViewClippingRect.z ||
            screenPos.y < ScrollViewClippingRect.y || screenPos.y > ScrollViewClippingRect.w)
        {
            discard;
        }
    }
    
    float4 color = DiffuseMap.Sample(ImageSampler, input.uv);
    return color;
}

// UI용 헬스바 쉐이더
float4 PS_HealthBar(VertexOutput2 input) : SV_TARGET
{ 
    float4 color = DiffuseMap.Sample(ImageSampler, input.uv);
    if (input.uv.x > HealthRatio)
    {
        return float4(0, 0, 0, 1);
    }
    return color;
}

빨간색 사각형 부분을 위한 클리핑

 

 

셰이더 관리 시스템

Shader 클래스의 데이터 푸시 시스템

class Shader
{
public:
    // 다양한 데이터를 셰이더에 전송하는 함수들
    void PushGlobalData(const Matrix& _view, const Matrix& _projection);
    void PushTransformData(const TransformDesc& _desc);
    void PushLightData(const LightDesc& _desc);
    void PushMaterialData(const MaterialDesc& _desc);
    void PushFOWData(const FogOfWarData& _desc);
    void PushHealthBarData(float _healthRatio, float _manaRatio, int _type = 0);
    void PushScrollViewClippingData(const Vec4& clippingRect, bool enableClipping);

private:
    // 각 데이터 타입별 상수 버퍼
    shared_ptr<ConstantBuffer<GlobalDesc>> m_globalBuffer;
    shared_ptr<ConstantBuffer<TransformDesc>> m_transformBuffer;
    shared_ptr<ConstantBuffer<LightDesc>> m_lightBuffer;
    shared_ptr<ConstantBuffer<MaterialDesc>> m_materialBuffer;
    shared_ptr<ConstantBuffer<FogOfWarData>> m_fowBuffer;
    shared_ptr<ConstantBuffer<HealthBarData>> m_healthBarBuffer;
    shared_ptr<ConstantBuffer<ScrollViewClippingData>> m_scrollViewClippingBuffer;
};

// FOW 데이터 푸시 구현
void Shader::PushFOWData(const FogOfWarData& _desc)
{
    if (m_fowBuffer == nullptr)
    {
        m_fowBuffer = make_shared<ConstantBuffer<FogOfWarData>>();
        m_fowBuffer->Create();
        m_fowEffectBuffer = GetConstantBuffer("FogOfWarData");
    }
    
    m_fowDesc = _desc;
    m_fowBuffer->CopyData(m_fowDesc);
    
    if (m_fowEffectBuffer)
        m_fowEffectBuffer->SetConstantBuffer(m_fowBuffer->GetComPtr().Get());
}

 

 

결론

주요 구현 내용

  • Material 시스템 : 렌더링 큐와 모드를 활용한 체계적인 렌더링 관리
  • 전장의 안개 (FOW) 효과 : HLSL을 활용한 동적 가시성 제어 시스템
  • 디퍼드 렌더링 : G-Buffer를 활용한 효율적인 Lighting 계산
  • Forward/Deffered 분기 : 객체 특성에 따른 렌더링 파이프라인 자동 선택
  • 포스트 프로세싱 : 외곽선 검출 등의 화면 후처리 효과 ( 소벨 마스크 )
  • UI 셰이더 : 클리핑과 특수 효과가 적용된 UI 렌더링