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

Exponential Shadow Map(ESM) 본문

Graphics/Graphics

Exponential Shadow Map(ESM)

scahp 2020. 4. 23. 21:57

 

Exponential Shadow Map(ESM)

 

최초작성 : 2020-04-23

마지막수정 : 2020-04-23

최재호

 

목표

ESM Shadow 에 대해 알아보고 실제 구현해봅니다.

 

개요

Standard shadow map 의 Shadow edge를 부드럽게 처리할 수 있는 기법 중 하나입니다.

구현방법도 VSM보다 간단하고, ShadowMap으로 R32 텍스쳐만 있으면 되므로 VSM 보다 더 적은 메모리를 사용합니다.

 

구현

SSM(Standard Shadow Map)의 경우 ShadowMap에 Depth 값을 저장합니다. 하지만 ESM의 경우 저장하는 값이 달라질 뿐 나머지 과정은 모두 동일합니다. 그리고 ShadowMap 의 필터링도 미리 처리해둘 수 있는 장점이 있습니다.

 

아래 그림에서 기존 SSM과 ESM의 차이를 잘보여줍니다.

기존 SSM의 그래프를 가리키는 빨간색 선을 주목해주세요.

  • D < Z 식을 보면, D가 현재 Depth값 Z가 Shadow Map의 Depth 값인 것을 확인할 수 있습니다. 0.5 근처에서 Lit 여부가 판정되는데 거리의 비교이다 보니, Lit or Unlit 두 상태만 존재합니다. (Step Function)

ESM의 그래프를 가리키는 검은색 선을 주목해봅시다.

  • exp(10*(d-z))식을 보면 0.5 근처에서 Lit 여부가 천천히 증가합니다.
    • 이 부분은 상수 K값으로 천천히 혹은 급격하게 변경될 수 있습니다. 이 부분이 쉐도우의 Artifacts를 만들어낼 수 있는데, 쉐도우가 더 밝게 보이는 현상을 만듭니다. 이 부분은 뒤에서 왜 발생하는지 알아보겠습니다.

 

레퍼런스1에서 가져온 그림, SSM과 ESM의 Shadow Lit 여부를 보여준다.

ESM의 또다른 특성으로 exp(k*(z-d)) = exp(k*z) * exp(k*d) 처럼 식을 분리할 수 있는점 입니다. Shadow map에 Depth 값으로 exp(k*z)를 저장하고, 실제 Shadow를 드리울때는 exp(-k*d) 값을 생성하여 Shadow map의 exp(k*z)와 곱하여 Lit 여부를 판정합니다.

 

exp(k*(z-d)) 두 값으로 분리할 수 있는 점은 Shadow map 을 미리 필터링 할 수 있다는 장점을 제공합니다. Gaussian Blur를 사용하여 Shadow map을 필터링 하였으며, 바로 이러한 이유 때문에 ESM이 Shadow map edge가 부드럽게 처리 됩니다.

 

Geogebra에서 exp(k*z) * exp(k*d) 를 사용해보면 아래와 같은 결과를 얻을 수 있습니다. 결과를 한번 분석 해봅시다.

상수 K = 5.5, Z = 0
상수 K = 66.9, Z = 0, K가 높아질수록 Step function 처럼 변화가 급격해진다.

 

상수 K = 66.9, Z = 0.41, Z의 값에 따라서 Lit or Unlit 되는 값이 정해지는 것을 확인할 수 있다.
상수 K 값에 따라 발생하는 쉐도우 밝기 차이, K가 너무 낮으면 Z와 D 값 차이가 별로 나지 않는 경우 쉐도우가 밝게 나타난다. 문제를 해결하기 위해서 K를 많이 높이면 오버플로가 발생할 수 있기 때문에 적절한 K 값을 설정하는 것이 중요하다.

 

실제 렌더링시에도 쉐도우를 만드는 오브젝트와 쉐도우가 드리우는 위치가 가까우면 쉐도우가 옅어지는 현상이 있다.

SSM과 상수 K = 10 사용한 ESM 비교

실제 구현

SSM에서 약간만 변경하면 바로 ESM을 사용할 수 있습니다. 아래와 같은 순서로 ESM을 사용합니다.

1. ESM Shadow map을 만든다. Depth 를 저장하는 대신 exp(K * Z) 저장

2. Gaussian blur로 ESM 쉐도우를 필터링 합니다.

3. Base pass의 Lighting 과정에서 ESM Shadow map을 사용하여 Shadow를 만듭니다.

  - 현재 Shading 하고있는 픽셀의 exp(-K * Z) 값을 만들어 Shado w map에 있는 값과 곱하여 결과를 얻습니다.

 

ESM Shadow 생성 PixelShader

#version 330 core

#include "common.glsl"

precision mediump float;

in vec3 Pos_;

uniform float ESM_C;

layout (std140) uniform DirectionalLightShadowMapBlock
{
	mat4 ShadowVP;
	mat4 ShadowV;
	vec3 LightPos;      // Directional Light Pos 임시
	float LightZNear;
	float LightZFar;
};

void main()
{
    vec3 lightDir = Pos_ - LightPos;
    float distSquared = dot(lightDir.xyz, lightDir.xyz);
    float distFromLight = (sqrt(distSquared) - LightZNear) / (LightZFar - LightZNear);
    
    gl_FragData[0].x = exp(distFromLight * ESM_C);
    gl_FragData[0].w = 1.0;
}

 

ESM Shadow를 Gaussan Blur 하는 과정

#version 330 core
precision mediump float;

uniform sampler2D tex_object;
uniform vec2 PixelSize;
uniform float IsVertical;
uniform float MaxDist;

#define FILTER_STEP_COUNT 5.0
#define FilterSize vec2(FILTER_STEP_COUNT, FILTER_STEP_COUNT)
#define COUNT (FILTER_STEP_COUNT * 2.0 + 1.0)

in vec2 TexCoord_;
out vec4 FragColor;

void main()
{
    vec2 radiusUV = (FilterSize * PixelSize) / FILTER_STEP_COUNT;

    if (IsVertical > 0.0)
    {
        radiusUV = vec2(0.0, radiusUV.y);
    }
    else
    {
        radiusUV = vec2(radiusUV.x, 0.0);
    }

    vec4 color = vec4(0);
    for (float x = -FILTER_STEP_COUNT; x <= FILTER_STEP_COUNT; ++x)
	{
        vec2 offset = vec2(x, x) * radiusUV;
        vec2 tex = TexCoord_ + offset;
        
        if (tex.x < 0.0 || tex.x > 1.0 || tex.y < 0.0 || tex.y > 1.0)
        {
            color.x += exp(MaxDist);
            color.y += exp(MaxDist * MaxDist);
            color.w += 1.0;
        }
        else
        {
            color += texture(tex_object, tex);
        }
    }

    FragColor = color / COUNT;
}

 

ESM Shadow 사용하여 그림자를 만드는 부분의 PixelShader

// ESM + Directional
float ESM(vec3 lightClipPos, vec3 lightPos, vec3 pos, float near, float far, float ESM_C, sampler2D shadow_object)
{
    float lit = 1.0;
    if (IsInShadowMapSpace(lightClipPos))
    {
        vec3 toLight = lightPos - pos;
        float distFromLight = (sqrt(dot(toLight, toLight)) - near) / (far - near);
        float expCZ = texture(shadow_object, lightClipPos.xy).r;
        lit = clamp(exp(-distFromLight * ESM_C) * expCZ, 0.0, 1.0);
    }
    return lit;
}

 

최종 렌더링 결과

Directional & OmniDirectional Light에 반영한 ESM Shadow

 

구현 코드

https://github.com/scahp/Shadows

 

레퍼런스

1. http://developer.download.nvidia.com/presentations/2008/GDC/GDC08_SoftShadowMapping.pdf

2. http://jankautz.com/publications/esm_gi08.pdf

 

 

반응형

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

Shadow Volume (Stencil Shadow) - 구현 (2/2)  (0) 2020.05.05
Shadow Volume (Stencil Shadow) - 원리 (1/2)  (0) 2020.04.29
Variance Shadow Map(VSM)  (0) 2020.04.16
Signed Distance Fields  (2) 2020.04.13
Light Indexed Deferred Rendering  (0) 2020.04.03