Programming/컴퓨터그래픽스 (DX 11)

[DirectX11] PCF방식을 통한 부드러운 그림자 매핑(Soft Shadow Mapping)

양양줘 2025. 11. 11. 17:30

 

이번 글은

기본적으로 그림자 매핑 방식에 대해

이해하고 있어야 한다.

 

[DirectX11] 그림자 매핑 (Shadow Mapping)

멀티 패스 렌더링 (Multi-Pass Rendering) 기본적으로 멀티 패스 렌더링이란 하나의 장면(Scene)을 완성하기 위해 여러 단계(Pass)로 나누어 렌더링하는 기법을 말한다. 여기서 패스(Pass) 는 한 번의 DrawCall

wooj22.tistory.com

 

 

 

 

그림자 팩터 (Shadow Factor)


그림자 팩터(Shadow Factor)는 픽셀이 광원에서 차단되는 정도를 나타내는 0~1 사이의 값이다.

 

의미
0 그림자에 완전히 가려짐 → direct light 없음
1 완전히 광원에 노출됨 → direct light 완전 적용
0~1 반쯤 그림자(Soft Shadow) → direct light 일부 적용

 

 

 

PCF (Percentage Closer Filtering)


이전 글에서 설명한 PixelShader는 shadowFactor를 0아니면 1로 처리하고 있어 그림자 음영에 단계가 없이 딱딱하게 처리된다. 이때 PCF방식을 사용하면 부드러운 그림자 처리가 가능하다.

 

PCF(Percentage closer Filtering)는 단순히 한 점에서 깊이를 비교하는 대신, 그림자맵 주변 여러 샘플을 참조하여 각각의 비교 결과를 평균 내는 확률적 필터링 기법이다. 즉, 그림자맵 샘플링 시 주변 texel들을 여러 번 샘플링하고, 각 결과를 더해 평균값을 내어 최종 그림자 팩터(Shadow Factor)로 사용한다. 이때 Shadow Factor는 0~1 사이의 연속적인 값이 되어, 그림자 경계가 부드럽고 자연스럽게 표현된다.

 

DirectX에서는하드웨어 레벨 PCF를 수행하는 SamplerComparisonState 샘플러를 지원한다.

ID3D11SamplerState* samplerComparison = nullptr;

sampDesc = {};
sampDesc.Filter = D3D11_FILTER_COMPARISON_MIN_MAG_MIP_LINEAR;
sampDesc.ComparisonFunc = D3D11_COMPARISON_LESS_EQUAL;
sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
HR_T(device->CreateSamplerState(&sampDesc, samplerComparison));

 

이 샘플러를 사용하면 GPU가 내부적으로 주변 texel(최대 4개)을 bilinear 필터링한 깊이값과 currentShadowDepth를 비교하여, 그 결과를 0~1 사이의 부드러운 실수값으로 반환한다. 즉, 한 번의 샘플링(SampleCmpLevelZero)으로 이미 하드웨어 PCF가 내장된 결과값을 얻을 수 있다. 아래 코드는 9개의 texel 오프셋을 수동으로 지정하여 9회 하드웨어 PCF를 수행하고, 그 평균을 통해 더욱 부드러운 shadowFactor를 계산하는 예시이다.

Texture2D shadowMap : register(t6);
SamplerComparisonState samplerComparison : register(s1);

float4 main(PS_INPUT input) : SV_TARGET
{
    float shadowFactor = 1.0f;
		
    // 1. Current Shadow Depth
    float currentShadowDepth = input.posShadow.z / input.posShadow.w;
    
    // Shadow map UV (0~1)
    float2 uv = input.posShadow.xy / input.posShadow.w;
    uv.y = -uv.y;
    uv = uv * 0.5f + 0.5f;

    // 2. ShadowMap Depth
    if(uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0)
    {
        float2 offsets[9] = {
            float2(-1, -1), float2(0, -1), float2(1, -1),
            float2(-1,  0), float2(0,  0), float2(1,  0),
            float2(-1,  1), float2(0,  1), float2(1,  1)
        };
       float2 texelSize = 1.0 / ShadowMapSize;  // 텍셀 크기 (ShadowMap 해상도 기준)
       shadowFactor = 0.0f;
       
       // ⭐ PCF - 9 texel 평균으로 그림자 팩터 계산
       [unroll]
       for(int i=0; i<9; i++)
       {
	       float2 sampleUV = uv + offsets[i] * texelSize;
	       shadowFactor += shadowMap.SampleCmpLevelZero(samplerComparison, sampleUV, currentShadowDepth - 0.001);
       }
       shadowFactor = shadowFactor / 9.0f;
    }
		
		
    ...기타 텍스처맵핑 및 라이트 계산 생략

    float final = (directLighting * shadowFactor) + ambientLighting + emissive;
    return float4(final.xxx, opacity);
}