Notice
Recent Posts
Recent Comments
Link
반응형
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

RenderLog

PCSS(Percentage-Closer Soft Shadow) 본문

Graphics/Graphics

PCSS(Percentage-Closer Soft Shadow)

scahp 2020. 5. 22. 00:19

PCSS(Percentage-Closer Soft Shadow)

최초작성 : 2020-05-22

마지막수정 : 2020-05-22

최재호

목표

PCSS 구현의 이해와 실제 구현

 

내용

PCSS는 PCF의 확장입니다. PCF가 모든 쉐도우의 Edge에서 균일한 크기로 Soft Shadow를 만들었다면, PCSS의 경우는 Shadow Caster와 Receiver의 거리에 기반하여 PCF를 처리할 반경에 변화를 주는 것입니다.

 

그림자의 종류

PCF에 들어가기전에 Shadow 영역에 대해서 알아봅시다.

PCF 이전에 Hard Shadow의 경우 그림1에서 Umbra 부분에 해당합니다.

그리고 PCF의 Soft Shadow 영역은 Penumbra 를 흉내낸 것인데, 이 부분을 좀 더 잘 처리되도록 다루려고 하는 것이 PCSS의 목적입니다.

 

 

그림1. Shadow의 종류 (출처 : https://en.wikipedia.org/wiki/Umbra,_penumbra_and_antumbra)

 

 

사실 맑은날 태양광으로 부터 만들어지는 그림자의 경우 Hard Edge 형태로 나타납니다.

 

 

그림2. 태양광원의 그림자 예제 (출처 : https://pxhere.com/ko/photo/1043201)

 

 

하지만 조명과 가까이 있는 Shadow caster의 경우 penumbra가 생깁니다.

 

 

그림3. 근접한 위치에 있는 광원 예제 (출처 : https://www.pinterest.cl/pin/661395895250681571)

 

 

Antumbra의 경우는 여기서 다루지 않습니다.

 

알고리즘 소개

 

먼저 전체 코드를 봅시다. Directional Light에 적용한 PCSS 를 보겠습니다.

// SSM + PCF + Directional
float PCF(vec3 lightClipPos, vec2 radiusUV, sampler2DShadow shadow_object)
{
  float sum = 0.0;
  float pcf_count = 0.0;
  vec2 stepUV = radiusUV / PCF_FILTER_STEP_COUNT;
  for (float x = -PCF_FILTER_STEP_COUNT; x <= PCF_FILTER_STEP_COUNT; ++x)
  {
    for (float y = -PCF_FILTER_STEP_COUNT; y <= PCF_FILTER_STEP_COUNT; ++y)
    {
      vec2 offset = vec2(x, y) * stepUV;
      vec3 depthPos = lightClipPos + vec3(offset, 0.0);
      pcf_count += IsShadowing(depthPos, shadow_object);
    }
  }

  return (pcf_count / PCF_COUNT);
}

vec2 SearchRegionRadiusUV(float zEye, float zLightNear, vec2 texelSize)         // Z Shadow Camera Space
{
  return SEARCH_RADIUS_DIRECTIONAL * texelSize * ((zEye - zLightNear) / zEye);
}

vec2 PenumbraRadiusUV(float zReceiver, float zBlocker, vec2 texelSize)
{
  float value = ((zReceiver - zBlocker) / zBlocker);
  return SEARCH_RADIUS_DIRECTIONAL * texelSize * clamp(value, 0.0, 1.0);
}

void FindBlocker(out float accumBlockerDepth, out float numBlockers, vec3 lightClipPos, vec2 searchRegionRadiusUV, sampler2D shadow_object)
{
  vec2 stepUV = searchRegionRadiusUV / BLOCKER_SEARCH_STEP_COUNT;
  for(float x = -BLOCKER_SEARCH_STEP_COUNT; x <= BLOCKER_SEARCH_STEP_COUNT; ++x)
  {
    for(float y = -BLOCKER_SEARCH_STEP_COUNT; y <= BLOCKER_SEARCH_STEP_COUNT; ++y)
    {
      vec2 offset = vec2(x, y) * stepUV;
      vec3 depthPos = lightClipPos + vec3(offset, 0.0);
      if (IsInShadowMapSpace(depthPos))
      {
        float shadowMapDepth = texture(shadow_object, depthPos.xy).r;
        if (lightClipPos.z >= shadowMapDepth + SHADOW_BIAS_DIRECTIONAL)
        {
          accumBlockerDepth += shadowMapDepth;
          ++numBlockers;
        }
      }
    }
  }
}

// SSM + PCSS + Directional
float PCSS(vec3 lightClipPos, float shadowCameraDepth, sampler2D shadow_object_test, sampler2DShadow shadow_object, float zLightNear, vec2 texelSize)
{
  if (!IsInShadowMapSpace(lightClipPos))
    return 1.0;

  // 1. Blocker Search
  float accumBlockerDepth = 0.0;
  float numBlockers = 0.0;
  vec2 searchRegionRadiusUV = SearchRegionRadiusUV(shadowCameraDepth, zLightNear, texelSize);
  FindBlocker(accumBlockerDepth, numBlockers, lightClipPos, searchRegionRadiusUV, shadow_object_test);

  // early out
  if (numBlockers == 0.0)
    return 1.0;
  else if (numBlockers >= BLOCKER_SEARCH_COUNT)
    return 0.0;

  // 2. Penumbra size
  float avgBlockerDepth = accumBlockerDepth / numBlockers;
  vec2 penumbraRadiusUV = PenumbraRadiusUV(lightClipPos.z, avgBlockerDepth, texelSize);

  // 3. PCF Filtering
  return PCF(lightClipPos, penumbraRadiusUV, shadow_object);
}

 

1. Blocker Search Step

현재 보고 있는 픽셀에 들어오는 Light 중 Blocking 되어진 픽셀들을 모아 평균 Depth 값을 얻어냅니다.

이 스탭에서 목표는 아래 그림의 초록색 네모로 둘러쌓인 영역을 얻어내는 것입니다.

 

 

그림4. BlockerSearch (출처 : 레퍼런스 1번)

 

 

먼저 Blocking 여부를 파악하기 위해서 검사해야하는 반경을 구합니다. zEye 값이 커질수록 Radius 반경은 1.0에 가까워지고 작을수록 0.0에 가까워 집니다.

vec2 SearchRegionRadiusUV(float zEye, float zLightNear, vec2 texelSize)         // Z Shadow Camera Space
{
    return SEARCH_RADIUS_DIRECTIONAL * texelSize * ((zEye - zLightNear) / zEye);
}

 

 

 

 

그리고 이 반경을 Blocking Test 를 실시 합니다.

lightClipPos는 현재 처리중인 픽셀의 ShadowMap에서의 z 값을 담고 있습니다.

SearchRegionRadius는 위에서 구한 Blocker를 탐색하는 반경입니다.

반경내의 픽셀들을 모두 샘플링하여, 현재 처리중인 픽셀의 Z값보다 더 멀리있는 픽셀들의 개수를 numBlockers에 그리고 Z값을 accumBlockerDepth에 모읍니다.

후에 이 Depth값의 평균값을 활용합니다.

void FindBlocker(out float accumBlockerDepth, out float numBlockers, vec3 lightClipPos, vec2 searchRegionRadiusUV, sampler2D shadow_object)
{
  vec2 stepUV = searchRegionRadiusUV / BLOCKER_SEARCH_STEP_COUNT;
  for(float x = -BLOCKER_SEARCH_STEP_COUNT; x <= BLOCKER_SEARCH_STEP_COUNT; ++x)
  {
    for(float y = -BLOCKER_SEARCH_STEP_COUNT; y <= BLOCKER_SEARCH_STEP_COUNT; ++y)
    {
      vec2 offset = vec2(x, y) * stepUV;
      vec3 depthPos = lightClipPos + vec3(offset, 0.0);
      if (IsInShadowMapSpace(depthPos))
      {
        float shadowMapDepth = texture(shadow_object, depthPos.xy).r;
        if (lightClipPos.z >= shadowMapDepth + SHADOW_BIAS_DIRECTIONAL)
        {
          accumBlockerDepth += shadowMapDepth;
          ++numBlockers;
        }
      }
    }
  }
}

 

만약 numBlockers가 BLOCKER_SEARCH_COUNT와 같은 경우 반경내 모든 픽셀이 다 Blocking 된 경우이기 때문에 HardShadow 영역으로 판정할 수 있습니다.

반면에 numBlockers가 0이라면 현재 픽셀은 Blocker가 없으므로 쉐도우가 드리우지 않는다고 볼 수 있습니다.

  // early out
  if (numBlockers == 0.0)
    return 1.0;
  else if (numBlockers >= BLOCKER_SEARCH_COUNT)
    return 0.0;

 

2. Penumbra 계산

평균 Depth 값을 구하고 이 것을 통해 Penumbra의 Radius를 구합니다.

vec2 PenumbraRadiusUV(float zReceiver, float zBlocker, vec2 texelSize)
{
  float value = ((zReceiver - zBlocker) / zBlocker);
  return SEARCH_RADIUS_DIRECTIONAL * texelSize * clamp(value, 0.0, 1.0);
}


// 2. Penumbra size
float avgBlockerDepth = accumBlockerDepth / numBlockers;
vec2 penumbraRadiusUV = PenumbraRadiusUV(lightClipPos.z, avgBlockerDepth, texelSize);

 

아래의 공식이 사용됩니다.

현재 픽셀의 카메라 기준 Z값이 Dreceiver

평균 Blocker의 Depth 값이 Dblocker

라이트의 크기가 Wlight 입니다.

(여기서 라이트는 점광원(Punctual light)이 아니라 Area Light입니다. 하지만 현재 구현에서는 간단히 점광원을 사용하므로 임의의 값을 주었습니다.)

Penumbra의 크기는 Wpenumbra 입니다.

 

 

 

 

 

 

위의 그림을 보면, Dblocker가 클수록 (즉, Shadow Caster가 Shadow Receiver에 가까워질수록) Wpenumbra 가 작아집니다. 반대로 Dblocker가 광원의 위치와 가까워 질 수록 Wpenumbra는 커집니다. 이는 또한 광원이 Shadow Caster에 가까워질 수록 Dpenumbra가 커진다고도 볼 수 있습니다. PCSS의 효과를 극대화 하기 위해서는 광원과 Shadow Caster를 가까이 둘수록 더 큰 Penumbra를 만들 수 있다는 의미 입니다.

위에서 본 것과 같이 태양광에서 만들어지는 그림자가 왜 Hard Shadow를 만드는지도 이 식을 통해서 이해할 수 있습니다.

 

3. PCF Filtering

이제 최종적으로 PCF 에 Penumbra 크기를 넘겨서 해당 반경 만큼 픽셀들을 샘플링하여 Soft Shadow를 만들도록 합니다.

    // 3. PCF Filtering
    return PCF(lightClipPos, penumbraRadiusUV, shadow_object);

 

레퍼런스

1. https://developer.download.nvidia.com/shaderlibrary/docs/shadow_PCSS.pdf

2. http://developer.download.nvidia.com/SDK/10.5/direct3d/screenshots/samples/PercentageCloserSoftShadows.html

3. https://github.com/scahp/Shadows

 

 

반응형

'Graphics > Graphics' 카테고리의 다른 글

VolumeLight  (0) 2020.06.24
Forward Plus Rendering  (0) 2020.05.26
Tiled Forward Rendering  (0) 2020.05.12
Shadow Volume (Stencil Shadow) - 구현 (2/2)  (0) 2020.05.05
Shadow Volume (Stencil Shadow) - 원리 (1/2)  (0) 2020.04.29