ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [GPU Gems 3]Advanced Techniques for Realistic Real-Time Skin Rendering
    Graphics/Graphics Study 자료 2020. 11. 10. 00:51

    Advanced Techniques for Realistic Real-Time Skin Rendering

     

    최초작성 : 2020-07-06
    마지막수정 : 2020-11-10
    최재호


    Updated 2020-11-10 : 전체 구현 설명 추가

     

    목차

    1. 목표
    2. 내용
    2.1 2가지 종류의 Reflectance
    2.1.1 Skin Surface Reflectance
    2.1.2 Skin Subsurface Reflectance
    2.2 Physically Based Specular Reflectance Model for Skin
    2.3. Scattering Theory
    2.3.1. Diffusion Profiles
    2.3.2 A Sum of Gaussians Diffuse Profile
    2.3.3. 가우시안이 가진 2가지 중요한 장점
    2.3.4. 가우시안의 합산으로 표현하는 Diffusion Profiles
    2.3.5. IrradianceMap의 생성
    2.3.6. 2D 에서 가우시안 적용 시 발생하는 단점과 Stretch Map
    2.3.7. 2D 에서 가우시안 적용 시 발생하는 단점과 접합부의 문제
    2.3.8. Combining Pre and Post Scattering
    2.4 에너지 보존
    2.5. Modified Translucent Shadow Maps
    2.6. FastBloom
    3. 실제 구현
    4. 구현결과
    5. 실제구현 코드
    6. 레퍼런스

    1. 목표

    Subsurface scattering에 대해서 이해하고 실제 구현해봅시다.

    그림1. Subsurface Scattering이 적용된 우측 이미지에 비해 좌측이미지는 플라스틱 같은 느낌이 듬. 또한 Subsurface Scattering 그림자 경계근처의 붉은색 빛을 확인 할 수 있음. (출처 : 구현코드)

     

    2. 내용

    알고리즘에 대한 설명이 추가 될 예정

    2.1 2가지 종류의 Reflectance

    2.1.1 Skin Surface Reflectance

    그림2은 피부를 3층의 레이어로 분리해놓은 것을 보여줍니다. 상단부터 Thin Oil Layer, Epidermis(표피), Dermis(진피)로 나뉩니다. (실제로 더 세부적으로 나뉠 수 있다고 합니다만 여기서는 3단계로만 나눔)
    빛이 피부의 표면으로 입사할때 가장 먼저 마주치는 Thin Oil Layer에서 Surface Reflectance가 일어나며, 흰색으로 반사됩니다.(금속이 아닌 경우는 빛의 모든 파장이 거의 동일한 양으로 반사되어 흰색이 나옴). 그리고 이 피부의 표면은 아주 미세하게 거친 표면이기 때문에 정반사(거울반사)가 발생하진 않습니다. 그래서 입사광은 표면에서 여러각도로 반사되어 나갑니다. 우리는 이것을 Specular BRDF 로 나타낼 것입니다.

    그림2. Multilayer Skin Model (출처 : 레퍼런스 1)

     

    2.1.2 Skin Subsurface Reflectance

    그림3 에서 보듯 입사광 중 피부표면에서 반사되고 남은 빛은 피부의 하위(Subsurface)로 들어가게 됩니다. 피부 내에서 빛은 흡수와 산란과정을 거칩니다. 이 과정에서 산란하던 빛은 다시 피부표면을 통해 나갈 수 있으며, 이때는 들어온 위치가 아닌 그 근처 처로 나갈 수 있습니다.

    그림3. Scattering and Absorption in Multiple Tissue Layers (출처 : 레퍼런스 1)

     

    2.2 Physically Based Specular Reflectance Model for Skin

    여기서는 Kelemen/Szirmay-Kalos model 의 BRDF를 사용합니다.
    - Fresnel Reflectance 은 Schlick's Fresnel approximation (Schlick 1993)를 사용
    - Beckmann NDF 를 사용함

    float fresnelReflectance(vec3 H, vec3 V, float F0)
    {
        float base = 1.0 - dot(V, H);
        float exponential = pow(base, 5.0);
        return exponential + F0 * (1.0 - exponential);
    }
    
    float PHBeckmann(float ndoth, float m)
    {
        float alpha = acos(ndoth);
        float ta = tan(alpha);
        float val = 1.0 / (m * m * pow(ndoth, 4.0)) * exp(-(ta * ta) / (m * m));
        return val;
    }
    
    float KS_Skin_Specular(
        vec3 N,     // Bumped surface normal
        vec3 L,     // Points to light
        vec3 V,     // Points to eye
        float m,    // Roughness
        float rho_s // Specular brightness
    )
    {
        float result = 0.0;
        float ndotl = dot(N, L);
        if (ndotl > 0.0)
        {
            vec3 h = L + V;
            // Unnormalized half-way vector
            vec3 H = normalize(h);
            float ndoth = clamp(dot(N, H), -1.0, 1.0); // PHBeckmann에 -1.0 ~ 1.0 범위를 벗어나는 값을 넣으면 노이즈 발생 주의
            float PH = PHBeckmann(ndoth, m);
            float F = fresnelReflectance(H, V, 0.02777778);
            float frSpec = max((PH * F) / dot(h, h), 0.0);
            result = ndotl * rho_s * frSpec; // BRDF * dot(N,L) * rho_s
        }
        return result;
    }

     

    2.3. Scattering Theory

    2.3.1. Diffusion Profiles

    Diffusion Profiles은 투과성이 높은 재질의 내부 산란 근사치를 제공합니다. 그림4에서 보는 것 처럼 피부의 가운데 점에 하얀색 라이트를 비추면 가운데 점을 중심으로 부터 빛이 얼마나 산란되는지 눈으로 확인 할 수 있습니다. 빛이 빨간색으로 보이지만 실제로 빛에는 다양한 파장의 빛들이 혼합되어 있습니다. 그림4의 우측 그림을 보게 되면, x축이 빛의 입사지점 부터 산란된 거리, y축이 반사 강도입니다. 그래프에서는 편의상 파장 전체가 아닌 RGB에 대해서만 계산하였습니다. R > G > B 순서로 중심에서 거리가 멀어지더라도 빛이 표면에서 방출되고 있는 것을 볼 수 있습니다. 그 결과 빛이 입사한 지점은 RGB 모두가 값이 높아 하얀색 점이 생기며, 거리가 멀어질 수록 빨간색 빛이 더 많이 남게 되어서 빛이 입사한 지점과 멀어지면서 빨간색 빛이 더 잘 보이게 됩니다.

    그림4. Visualizing a Diffusion Profile (출처 : 레퍼런스1)


    현재까지는 내부 산란이 발생 했을 때, 표면이 평평하다고 가정했습니다만 실제로는 피부는 표면에 굴곡이 있을 것입니다. 그래서 실제로 Diffusion Profiles로 인해 Subsurface로 입사한 광이 다시 산란되어 나오는 거리가 달라질 수 있습니다. 하지만 이런 부분은 크게 차이가 없으므로 이 구현에서는 무시합니다.

    추가사항
    대부분의 경우 Dipole model로 재질들의 산란을 표현할 수 있지만 피부와 같이 여러 층으로 구성된 재질의 경우 Multi pole model을 사용합니다. 또는 직접 표면에 그림4 에서 처럼 표면에 하얀색 레이져를 쏴서 결과를 측정할 수도 있습니다. 어떤 방식을 쓰던 그 결과를 측정하여 적절한 가우시안들을 찾아서 조합하여 비슷한 결과를 만들어 Diffusion Profile을 만들면 됩니다. 그래서 여기서는 Dipole과 Multipole에 대해서 더 알아보지는 않을 것입니다.

    2.3.2 A Sum of Gaussians Diffuse Profile

    그림4에서 봤던 Diffusion Profiles을 편리하게 사용하기 위해서 여러 가우시안의 합으로 근사해서 사용합니다. 그림8은 그림7의 내용으로 근사한 Diffusion Profile 입니다. 결과가 그림4과 유사한 것을 확인 할 수 있습니다.

    2.3.3. 가우시안이 가진 2가지 중요한 장점

    여기서 이야기하는 가우시안은 가우시안 블러와 동일한 것입니다. 여러 알고리즘 중 가우시안 블러를 사용하는 이유는 가우시안이 가진 2가지 중요한 장점 때문입니다.

    1). 가로와 세로 방향을 분리하여 처리 가능한 점.
    - 64x64 텍셀에 가우시안 블러 처리를 하기 위해서 우리는 4096 번 연산을 해야하지만 가로세로 방향이 분리 가능하면 가로 64 + 64 = 128 번 연산이 가능합니다. 그래서 연산을 상당히 아낄 수 있습니다.

    그림5. 가우시안이 X, Y축 별도로 연산이 가능한 이유 (출처 : 레퍼런스 2)



    2). 가우시안을 중첩하여 적용하면 더 큰 Variance의 가우시안을 생성할 수 있다는 점.
    - 가우시안 블러는 현재 픽셀을 기준으로 '특정 거리'만큼 떨어진 위치에서 픽셀을 Fetch 해서 가중치를 적용하여 합산하는 형태로 구현됩니다. 여기서는 이때 사용하는 '특정 거리'를 Variance라고 부릅니다. 만약 이 '특정 거리'가 너무 크다면 가우시안 결과의 디테일이 상당히 떨어지게 될 것입니다. 그래서 작은 가우시안 Variance를 중첩하여 더 큰 Variance의 가우시안 결과를 생성할 수 있다는 점은 디테일을 유지 할 수 있다는 점에서 좋은 특성입니다.

    그림6. 작은 Variance의 가우시안 두개를 합쳐 더 큰 Variance의 가우시안을 만들 수 있음 (레퍼런스3과 4에 증명에 대한 내용 있음) (출처 : 레퍼런스1)

     

    2.3.4. 가우시안의 합산으로 표현하는 Diffusion Profiles

    아래 그림7를 사용하여 총 6개의 가우시안을 구하고 합산합니다. 표의 Variance 열은 G(v, r)의 v에 해당합니다. Variance를 사용하여 구해진 가우시안을 각 Red, Green, Blue 채널에 서로 다른 가중치로 사용하는데, 그것이 표의 Red, Green, Blue에 해당합니다.

    그림7. Our Sum-of-Gaussians Parameters for a Three-Layer Skin Model (출처 : 레퍼런스 1)
    그림8. Plot of the Sum-of-Gaussians Fit for a Three-Layer Skin Model (출처 : 레퍼런스1)



    이러한 가우시안 블러 처리를 3D 공간에서 하게 되면 복잡해질 것입니다. 이것을 쉽게하기 위해서 2D IrradianceMap에 적용합니다. 3D Mesh의 Irradiance를 2D 텍스쳐에 Unwrap 하여 저장합니다. 그리고 총 6개의 서로다른 Variance를 가진 가우시안을 생성합니다. 이 가우시안들을 컬러별로 가중치를 주어 합하면 그림8의 모양이 됩니다.

    가우시안의 합은 위에서 언급 했듯이 RGB 각각의 채널(파장)에 대해서 적용하는 것을 주의해야 합니다. 이 과정을 간단하게 요약 한 것을 그림9에서 확인할 수 있습니다.

    그림9. Overview of the Improved Texture-Space Diffusion Algorithm (레퍼런스 1)



    그림10. IrradianceMap의 Linear combination을 수식으로 간단히 표시 (출처 : 레퍼런스1)

     

    2.3.5. IrradianceMap의 생성

    IrradianceMap은 3D Mesh를 렌더링하여 라이팅 연산등을 처리하고, 그 결과를 Diffuse 2D Texture로 Unwrap 하여 렌더링 합니다. 이 텍스쳐로 Subsurface Scattering 처리를 하기 때문에 IrradianceMap에는 Specular를 제외한 Diffuse Light의 연산결과만 저장됩니다.

    3D Mesh를 2D 텍스쳐로 Unwrap 해주는 코드는 아래와 같습니다.

    // TexCoord 정보가 있을 때, 3D Mesh를 2D 텍스쳐로 Unwrap 해주는 코드 
    gl_Position.xy = TexCoord_.xy * 2.0 - 1.0; 
    gl_Position.y *= -1.0; 
    gl_Position.z = 0.5; 
    gl_Position.w = 1.0;

     

    // 그림7에 나오는 가우시안 Variance(거리)를 나타냄. 
    // 가우시안은 포스트프로세스에서 처리되므로 거리는 픽셀의 거리를 기준으로 함. 
    // * 그림7에는 총 6개의 가우시안이지만 첫번재 가우시안의 경우 차이가 거의 없으므로 기본 IrradianceMap으로 대체함 
    float IrrBlurStep[5] = { 0.0484f * GaussianStepScale, 0.187f * GaussianStepScale, 0.567f * GaussianStepScale, 1.99f * GaussianStepScale, 7.41f * GaussianStepScale }; 
    
    // 가우시안 블러를 X와 Y를 분리하여 처리 + 작은 가우시안 Variance 2개를 중첩하여 더 큰 Variance의 가우시안을 만듬 
    // RENDERTARGET, SRCTEXTURE를 CurrentVariance 값으로 가우시안 블러하여 결과를 저장함 
    // * AccumulatedVariance 변수에 대해서 * 
    // 매크로에 내부적으로 AccumulatedVariance 변수를 사용하는데, 이전에 사용한 CurrentVariance를 누적시킴 
    // 매크로 내부에서는 CurrentVariance - AccumulatedVariance 로 얻어진 Variance를 사용 
    #define BLUR_DIFFUSION_PROFILE(RENDERTARGET, SRCTEXTURE, CurrentVariance, TextureSize) \ 
        BLUR(RENDERTARGET, SRCTEXTURE, CurrentVariance, TextureSize, "SkinGaussianBlurX", "SkinGaussianBlurY") AccumulatedVariance = 0.0f; 
    
    // 누적 Variance 정보 초기화 
    BLUR_DIFFUSION_PROFILE(IrrBlurTarget2, IrrTarget->GetTexture(), IrrBlurStep[0], TEXTURE_SIZE); 
    BLUR_DIFFUSION_PROFILE(IrrBlurTarget4, IrrBlurTarget2->GetTexture(), IrrBlurStep[1], TEXTURE_SIZE); 
    BLUR_DIFFUSION_PROFILE(IrrBlurTarget8, IrrBlurTarget4->GetTexture(), IrrBlurStep[2], TEXTURE_SIZE); 
    BLUR_DIFFUSION_PROFILE(IrrBlurTarget16, IrrBlurTarget8->GetTexture(), IrrBlurStep[3], TEXTURE_SIZE); 
    BLUR_DIFFUSION_PROFILE(IrrBlurTarget32, IrrBlurTarget16->GetTexture(), IrrBlurStep[4], TEXTURE_SIZE);


    IrradianceMap에 가우시안 적용한 결과는 그림11과 같습니다.

    그림11. 가우시안을 적용한 6개의 IrradianceMap의 예제, 좌상단이 기본 IrradianceMap 우측을 갈수록 Variance가 높아짐 (출처 : 구현코드)

     

    2.3.6. 2D 에서 가우시안 적용 시 발생하는 단점과 Stretch Map

    가우시안을 적용 시 텍스쳐 공간에서 처리합니다. 때문에 픽셀 거리 기준으로 만든 가우시안을 크기가 서로다른 Mesh에 입혔을 때, Subsurface Scattering 적용 범위가 제대로 반영되지 않아 잘못 된 결과가 나올 수 있습니다. 이 부분을 해결하기 위해서 StretchMap을 생성하여 가우시안을 적용할 때 반영합니다. StretchMap은 IrradianceMap을 만드는 것과 비슷한 방식으로 Mesh를 Unwrap하여 2D Texture에 그립니다. 이때 ddx, ddy 같은 함수들로 인접 픽셀과의 위치 차이의 비율을 기록합니다.

    그림12. Using a Stretch Texture (출처 : 레퍼런스 1)



    그림13. 구현한 StrechMap, 모델의 크기에 따라서 결과의 모양이 달라질 수 있음 (출처 : 구현코드)

     

    #version 330 core
    precision highp float; 
    
    uniform float ModelScale; 
    
    in vec3 Pos_; 
    out vec4 color; 
    
    void main() 
    { 
        // StretchMap은 모델의 사이즈에 종속접입니다. 
        // 왜냐하면 가우시안을 텍스쳐 공간에서 처리하는데 모델사이즈만 커지게 된다면, 
        // 빛이 피부로 들어갔다가 다시 나올 수 있는 거리는 더 작아지는 것과 마찬가지가 됩니다. 
        // 극단적인 경우 Subsurface scattering 효과가 없는 것 처럼 보일 수 있습니다. 
        vec3 derivu = dFdx(Pos_) / ModelScale; vec3 derivv = dFdy(Pos_) / ModelScale; // 0.001 scales the values to map into [0,1] 
        
        vec2 NomralizeContant = vec2(0.001); 
        float stretchU = NomralizeContant.x * 1.0 / length(derivu); 
        float stretchV = NomralizeContant.y * 1.0 / length(derivv); 
        color.xy = vec2(stretchU, stretchV); // A two-component texture 
        color.z = 1.0; 
    }

     

    2.3.7. 2D 에서 가우시안 적용 시 발생하는 단점과 접합부의 문제

    Irradiance Map은 2D 텍스쳐입니다. 텍스쳐의 가장자리 부분들에 가우시안을 적용하게 되는 경우, 가우시안 블러가 제대로 적용되지 않습니다. 이런 부분을 회피하기 위해서 접합부의 경우는 Subsurface Scattering을 적용하지 않도록 하여 해결할 수 있습니다. 하지만 이렇게 되면, Subsurface Scattering이 되는 부분과 아닌 부분이 급격하게 변할 수 있으므로 해당 부위가 부자연스러울 수 있습니다.

    이 부분은 제가 별도로 구현한 방법으로 해결하였습니다. 책에 있는 원본에서도 뒤통수를 보고있는 채로 라이트를 좌우로 움직이면 티가 나는 부분이 있어서 이부분을 해결하려고 시도했습니다. 한번 만들고 계속 사용할 WeightMap을 생성하여 이 부분을 해결하였습니다. 생성방식은 아래와 같습니다.
    1. Irradiance Map 처럼 3D Mesh를 2D로 Unwrap 하여 렌더링 하되, R 채널에 1.0만 기록하도록 합니다.

    • Mesh가 Unwrap이 된 부분이 아니면 모두 0.0 Weight가 됨, 그림13과 비슷한 모양이되, 검정색과 흰색으로만 나뉜다고 생각해도 좋을 것 같습니다.

    2. 이 WeightMap을 대상으로 Diffusion Profiles을 적용하는 것 처럼, WeightMap에 가우시안들을 만들고 Linear Combination 으로 결합합니다.

    • 이제 Unwrap된 텍스쳐의 테두리 부분의 경우 가우시안의 적용으로 Weight 값이 1.0 보다 적게 될 것입니다. 1.0에 가까울 수록 완전한 Subsurface Scattering 적용가능, 0.0에 가까울 수록 테두리의 접합부위에 가깝습니다. 여기서 IrradianceMap에 적용한 것과 같은 가우시안 Variance를 사용하였기 때문에 WieghtMap에 있는 값을 Subsurface Scattering과 IrradianceMap을 혼합하기 적절한 값입니다.


    3. 최종 쉐이더에서 이 WeightMap을 통해 Subsurface Scattering과 IrradianceMap를 혼합하여 사용합니다.

    • Weight를 그대로 사용하는 것보다 pow(ASeamWeight, 3)으로 처리하는 것이 더 좋은 결과를 주어 적용함.
    • 여기서 이야기하는 Subsurface Scattering은 가우시안들의 Linear Combination으로 구한 텍스쳐이며, IrradianceMap은 가우시안 처리 전 Surface Reflectance만 적용되어있는 원본 IrradianceMap입니다.

    아래 그림에서 초록색과 빨간색 영역이 나타나는 부분이 Weight 값이 1보다 작은 지점이고, 초록색일수록 Subsurface Scattering, 그리고 빨간색일수록 원본 IrradianceMap을 사용하고 있습니다.

    그림14. 접합부의 문제를 Subsurface scattering을 사용하는 가중치를 조절하여 해결 (출처 : 구현코드)

     

    // Vetex Shader : skin_irr_gen_vs.glsl 
    // 3D Mesh -> Unwrap 2D Texture 
    void main() 
    { 
        TexCoord_ = TexCoord; 
        ... 
        gl_Position.xy = TexCoord_.xy * 2.0 - 1.0; 
        gl_Position.y *= -1.0; 
        gl_Position.z = 0.5; 
        gl_Position.w = 1.0; 
    } 
        
    // Fragment Shader : skin_bluralphadistribution_fs.glsl 
    // Weight 값을 1로 설정 
    out vec4 color; 
    void main() 
    { 
        color.r = 1.0; 
    } 
    ... 
    
    // jGame.cpp 
    // WeightMap에 Diffusion Profile에 쓰는 것과 가우시안 적용 
    // 전체를 다 적용하지 않아도 충분하므로 적당한 4단계 까지만 적용. 
    BLUR_DIFFUSION_PROFILE(BlurAlphaDistributionTarget2, BlurAlphaDistributionTarget->GetTexture(), IrrBlurStep[0], TEXTURE_SIZE); 
    BLUR_DIFFUSION_PROFILE(BlurAlphaDistributionTarget, BlurAlphaDistributionTarget2->GetTexture(), IrrBlurStep[1], TEXTURE_SIZE); 
    BLUR_DIFFUSION_PROFILE(BlurAlphaDistributionTarget2, BlurAlphaDistributionTarget->GetTexture(), IrrBlurStep[2], TEXTURE_SIZE); 
    BLUR_DIFFUSION_PROFILE(BlurAlphaDistributionTarget, BlurAlphaDistributionTarget2->GetTexture(), IrrBlurStep[3], TEXTURE_SIZE); 
    
    // Final skin fragment shader : skin_final_fs.glsl 
    ... 
    // Make seamless UV 
    float SeamAlpha = texture2D(tex_object11, uv).r; 
    if (SeamAlpha < 1.0) 
    { 
        // Visualize Seam's problems 
        if (VisualizeRangeSeam > 0) 
        { 
            Irr1.xyz = vec3(1.0, 0.0, 0.0); 
            color.xyz = vec3(0.0, 1.0, 0.0); 
        } 
        SeamAlpha = clamp(pow(SeamAlpha, 3), 0.0, 1.0); 
        
        // 깔금한 결과를 위해 Weight를 ^3 하여 사용 
        color.xyz = mix(Irr1, color.xyz, SeamAlpha); 
    } 
    ...

     

    2.3.8. Combining Pre and Post Scattering

    DiffuseMap을 우리의 Scattering Model에 통합시키기 위해서 2가지 방법이 있습니다. Pre Scattering 과 Post Scattering 방식입니다. 두 방식 모두 완전히 물리기반 기술은 아닙니다.

    Post Scattering은 모든 Scattering 계산에 Diffuse Color를 포함하지 않습니다. 그래서 가우시안의 합은 그림15의 우측상단 이미지 처럼 하얀색이 됩니다. 그래서 Diffuse color texture를 나중에 적용하게 되면, Diffuse color texture의 디테일(high frequency detail)은 그대로 남아있습니다(가우시안 처리를 안했기 때문). Diffuse color texture에도 Scattering에 의해 발생하는 색상의 Blurruing을 적용해야 하는게 아닐까? 라고 생각 할 수도 있습니다. 그러나 Diffuse color texture를 카메라로 촬영할 때는 이미 Scattering이 완료되고 난 결과를 촬영하는 것이라 Scattering이 이미 고려되어 있다고 가정할 수 있습니다. 그래서 추가 적으로 Blurruing 현상이 필요하지 않습니다. 이런 이유에서 Post-Scattering 방식은 괜찮은 선택이 될 수 있습니다.

    Pre Scattering은 IrradianceMap에 Diffuse Color를 포함합니다. 이렇게 하면 Diffuse Color에 가우시안 처리가 되면서, Diffuse color가 Light와 같은 방식으로 산란되어져 Blurring 됩니다. 그래서 디테일(high frequency detail)을 조금 잃게 됩니다. 아래 그림15의 좌측 이미지를 보면 우측 이미지 보다 조금 디테일이 떨어지는 것을 볼 수 있습니다.

    그림15. Comparing Pre- and Post-Scatter Texturing Techniques (출처 : 레퍼런스 1)


    우리는 이 두가지를 적절히 합해서 사용할 수도 있습니다.
    Pre-Scattering를 위해서 IrradianceMap에 pow(diffuseColor, mix)를 곱하여 반영합니다. 그리고 Post-Scattering으로 가우시안 합의 결과에 pow(diffuseColor, 1 – mix) 곱합니다. 그러면 mix 값이 0인 경우 Post-Scattering, 1인 경우 Pre-Scattering이 됩니다.

    그림16. 구현한 Post &amp; Pre Scattering의 차이 비교 (출처 : 구현코드)



    2.4 에너지 보존

    입사한 라이트가 100% 라 했을 때, 입사광이 표면에 닿아 Specular Reflectance로 20%를 먼저 잃었다고 합시다. 그럼 입사광의 나머지 80%만 피부하위로 입사하게 되어 Subsurface scattering에 기여하게 됩니다. 여기서는 이것을 Specular BRDF에 사용되지 않은 에너지만 Subsurface scattering에 사용하는 것으로 표현합니다.

    L 로 부터 입사한 빛 중, 반구상으로 나간 모든 Specular light를 제외하고 남은 Diffuse light를 P라고 정의합니다. (Fr : Specular BRDF, L : x 에서의 라이트 벡터, N : 표면의 Normal, Wo : 반구의 방향의 벡터들)


    이 값은 특정 점 x의 위치에서의 Roughness와 N dot L 에 따라 변화합니다. 또한 반구에 대한 적분항이 들어가므로, Roughness와 N dot L 을 각각 텍스쳐의 y축, x축으로 두고 텍스쳐맵에 미리 계산해두고 사용합니다.

    그림17. A Precomputed Attenuation Texture (하단 부분의 검색 부분 낮은 roughness 값을 사용한 정밀도 문제로 까맣게 나왔지만 사용되지 않으므로 무시 가능) (출처 : 레퍼런스1)



    그림18. Skin Rendering and Energy Conservation (출처 : 레퍼런스 1)
    그림19. 에너지 보존 적용 차이 비교(Surface Reflectance는 입사각이 커질 수록 증가하기 때문에 빛의 방향과 평행한 면일 수록 Specular BRDF의 양이 더 많아짐) (출처 : 구현코드)

     

    2.5. Modified Translucent Shadow Maps

    TSM은 얇은 피부의 경우, 빛 방향의 반대면에 그림자가 완전히 까맣게 드리우는게 아니라 일부 빛이 투과되어 보이는 것을 표현합니다.

    그림20. 등뒤에 빛이 있어서 얼굴의 전면이 모두 그림자가 드리울 수 있지만, 귀와 같은 부분은 얇기 때문에 빛이 투과되어 보일 수 있음. (출처 : 구현코드)


    TSM은 라이트를 기준으로 그려지고, 카메라에서 바라본 지점과 TSM에 있는 위치의 거리 차이를 비교하여 이 거리가 가까운 경우 쉐도우의 강도를 조정합니다. 그림21의 m1과 m2를 보면 m2가 거리의 차이를 나타냅니다.

    TSM을 적용하는 순서는 아래와 같습니다.
    1. TSM에는 (라이트와 라이트를 받는 지점의 거리, Unwarp시 TexCoord.x, Unwrap시 TexCoord.y)를 저장합니다.
    2. 빛이 Subsurface로 들어간 이상 산란이 일어날 것입니다. 그림22를 보면, 라이트가 A -> B 위치로 나온다고 했을때, B위치에 영향을 주는 것은 A의 근처 Diffusion Profiles의 범위와 같을 것입니다. 그래서 IrradianceMap의 Alpah 컴포넌트에 TSM의 Tickness를 넣어 IrradianceMap과 같은 과정을 거치도록 합니다.

    • 만약 B지점이 아닐 C 지점을 바라보고 있다고 하면 카메라에서 C점 까지의 거리가 m 일 것입니다. 실제 빛이 수직으로 투과되는 거리 d를 구하기 위해서 그림22와 같이 d = m * cosΘ 을 사용하여 A - B 사이의 두께를 얻어냅니다.
    • C와 B의 거리 차이가 있지만 큰 오차는 아니므로 무시합니다.

    3. Final Shader에서 가우시안과 Linear Combination이 끝난 TSM Thickness로 쉐도우 여부를 판단합니다.

    • Thickness가 충분히 가까운 경우 라이트를 바라보는 면의 Texel 값과 IrradianceMap이 Subsurface Scattering 된 결과를 혼합하여 카메라에서 보는 지점에 반영되도록 합니다.

     

    그림21. Modified Translucent Shadow Maps (출처 : 레퍼런스 1)

     

    그림22. A Region in Shadow () Computes Transmitted Light at Position (출처 : 레퍼런스 1)

     

    2.6. FastBloom

    FastBloom은 가로세로에 가우시안 블러와 적절한 가중치를 적용하여 혼합한 후 더 해줍니다.

    // PostProcess 
    uniform sampler2D tex_object;  // 가우시안 Variance 0.008f 
    uniform sampler2D tex_object2; // 가우시안 Variance (0.0576f / RenderTargetAspect) 
    uniform float WeightA; 
    uniform float WeightB; 
    
    in vec2 TexCoord_; 
    out vec4 color; 
    
    void main() 
    { 
        vec4 A = texture2D(tex_object, TexCoord_); 
        vec4 B = texture2D(tex_object2, TexCoord_); 
        color = WeightA * A + WeightB * B; 
    }

     

    3. 실제 구현

    3.1 렌더링 전에 미리 만들어두는 데이터들

    3.1.1. Specular BRDF 를 계산하여 Lookup 텍스쳐로 제작
    3.1.2. 접합부 처리용 Weight distribution Map

    3.2 렌더링 패스와 순서

    3.2.1. Stretch Map 생성
    3.2.2. TSM Shadow Map 생성
    3.2.3. Irradiance Map 생성
    - 기본 Irradiance Map과 2, 4, 8, 16, 32 크기의 커널을 사용한 각각의 Irradiance Map을 만들어 Linear Combination 함.
    3.2.4. 현재까지 만든 Map들을 사용하여 최종 Skin 렌더링
    3.2.5. Postprocess
    - Bloom
    - Adaptive luminance (추가 구현 패스)
    - FastTonemap (추가 구현 패스)

    4. 구현결과

     

    그림23. 최종 결과

     

    영상1. 최종 결과

     

    5. 구현 코드

    https://github.com/scahp/Shadows/tree/Skin

    6. 레퍼런스

    1. Chapter 14. Advanced Techniques for Realistic Real-Time Skin Rendering
    2. CMPSCI 370HH: Intro to Computer Vision Advanced edge detection
    3. Convolution - Convolution의 표현방식 참고
    4. Sum of normally distributed random variables - 2개의 가우시안의 Convolution의 증명 (Proof using convolutions 항목)

    댓글

Designed by Tistory & scahp.