3 분 소요

Fresnel(프레넬) 효과

Image

  • 물체의 ‘표면’을 볼 때, ‘시선’과 표면의 법선(Normal) 사이의 각도에 따라
    반사율이 달라지는 현상
    (그러한 계산을 ‘프레넬 방정식’에 따라 하기에 프레넬 효과라 부르기도 함)

  • 물이나 금속 같은 재질의 경우
    똑바로 보면 ‘물’ 안이 투과되어 보이지만
    (시야각과 수직에 가까움)
    사선으로 갈수록 수면 위의 여러 요소들이 반사되어 비춰진다
    (시야각과 수평에 가까움)

  • 시야각에 의존하기에 Rim Lighting 이나 Outline glow 등의 효과처럼 사용할 수 있음

예제

pixel shader에서 다룬다

#include "Common.hlsli" // 쉐이더에서도 include 사용 가능

Texture2D g_texture0 : register(t0);
TextureCube g_diffuseCube : register(t1);
TextureCube g_specularCube : register(t2);
SamplerState g_sampler : register(s0);

cbuffer BasicPixelConstantBuffer : register(b0)
{
    float3 eyeWorld;
    bool useTexture;
    Material material;
    Light light[MAX_LIGHTS];
    float3 rimColor;
    float rimPower;
    float rimStrength;
    bool useSmoothstep;
};

// Schlick approximation: Eq. 9.17 in "Real-Time Rendering 4th Ed."
// fresnelR0는 물질의 고유 성질
// Water : (0.02, 0.02, 0.02)
// Glass : (0.08, 0.08, 0.08)
// Plastic : (0.05, 0.05, 0.05)
// Gold: (1.0, 0.71, 0.29)
// Silver: (0.95, 0.93, 0.88)
// Copper: (0.95, 0.64, 0.54)
float3 SchlickFresnel(float3 fresnelR0, float3 normal, float3 toEye)
{
    // 참고 자료들
    // THE SCHLICK FRESNEL APPROXIMATION by Zander Majercik, NVIDIA
    // http://psgraphics.blogspot.com/2020/03/fresnel-equations-schlick-approximation.html
    
    float normalDotView = saturate(dot(normal, toEye));

    float f0 = 1.0f - normalDotView; // 90도이면 f0 = 1, 0도이면 f0 = 0

    // 1.0 보다 작은 값은 여러 번 곱하면 더 작은 값이 됩니다.
    // 0도 -> f0 = 0 -> fresnelR0 반환
    // 90도 -> f0 = 1.0 -> float3(1.0) 반환
    // 0도에 가까운 가장자리는 Specular 색상, 90도에 가까운 안쪽은 고유 색상(fresnelR0)
    return fresnelR0 + (1.0f - fresnelR0) * pow(f0, 5.0);
}

float4 main(PixelShaderInput input) : SV_TARGET
{
    float3 toEye = normalize(eyeWorld - input.posWorld);

    float3 color = float3(0.0, 0.0, 0.0);
    
    int i = 0;
    
    // https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-for
    // https://forum.unity.com/threads/what-are-unroll-and-loop-when-to-use-them.1283096/
    
    [unroll] // warning X3557: loop only executes for 1 iteration(s), forcing loop to unroll
    for (i = 0; i < NUM_DIR_LIGHTS; ++i)
    {
        color += ComputeDirectionalLight(light[i], material, input.normalWorld, toEye);
    }
    
    [unroll]
    for (i = NUM_DIR_LIGHTS; i < NUM_DIR_LIGHTS + NUM_POINT_LIGHTS; ++i)
    {
        color += ComputePointLight(light[i], material, input.posWorld, input.normalWorld, toEye);
    }
    
    [unroll]
    for (i = NUM_DIR_LIGHTS + NUM_POINT_LIGHTS; i < NUM_DIR_LIGHTS + NUM_POINT_LIGHTS + NUM_SPOT_LIGHTS; ++i)
    {
        color += ComputeSpotLight(light[i], material, input.posWorld, input.normalWorld, toEye);
    }

    // 쉽게 이해할 수 있는 간단한 구현입니다.
    // IBL과 다른 쉐이딩 기법(예: 퐁 쉐이딩)을 같이 사용할 수도 있습니다.
    
    float4 diffuse = g_diffuseCube.Sample(g_sampler, input.normalWorld);
    float4 specular = g_specularCube.Sample(g_sampler, reflect(-toEye, input.normalWorld));
    
    diffuse *= float4(material.diffuse, 1.0);
    specular *= pow((specular.r + specular.g + specular.b)/3.0, material.shininess);
    specular *= float4(material.specular, 1.0);
    
    // 참고: https://www.shadertoy.com/view/lscBW4
    float3 f = SchlickFresnel(material.fresnelR0, input.normalWorld, toEye);
    specular.xyz *= f;
    
    if (useTexture) {
        diffuse *= g_texture0.Sample(g_sampler, input.texcoord);
        // Specular texture를 별도로 사용할 수도 있습니다.
    }
    
    return diffuse + specular;
    
    //return useTexture ? float4(color, 1.0) * g_texture0.Sample(g_sampler, input.texcoord) : float4(color, 1.0);
}

핵심인 부분은 다음과 같다

// Schlick approximation: Eq. 9.17 in "Real-Time Rendering 4th Ed."
// fresnelR0는 물질의 고유 성질
// Water : (0.02, 0.02, 0.02)
// Glass : (0.08, 0.08, 0.08)
// Plastic : (0.05, 0.05, 0.05)
// Gold: (1.0, 0.71, 0.29)
// Silver: (0.95, 0.93, 0.88)
// Copper: (0.95, 0.64, 0.54)
float3 SchlickFresnel(float3 fresnelR0, float3 normal, float3 toEye)
{
    // 참고 자료들
    // THE SCHLICK FRESNEL APPROXIMATION by Zander Majercik, NVIDIA
    // http://psgraphics.blogspot.com/2020/03/fresnel-equations-schlick-approximation.html
    
    float normalDotView = saturate(dot(normal, toEye));

    float f0 = 1.0f - normalDotView; // 90도이면 f0 = 1, 0도이면 f0 = 0

    // 1.0 보다 작은 값은 여러 번 곱하면 더 작은 값이 됩니다.
    // 0도 -> f0 = 0 -> fresnelR0 반환
    // 90도 -> f0 = 1.0 -> float3(1.0) 반환
    // 0도에 가까운 가장자리는 Specular 색상, 90도에 가까운 안쪽은 고유 색상(fresnelR0)
    return fresnelR0 + (1.0f - fresnelR0) * pow(f0, 5.0);
}
...

float3 f = SchlickFresnel(material.fresnelR0, input.normalWorld, toEye);
specular.xyz *= f;
  • fresnelR0?
    물질 고유의 ‘특성’ 같은 것
    표현하고 싶은 고유의 float3 값을 입력하여 사용
  • normal과 시야 벡터를 내적한 후,
    saturate로 0~1 사이로 보간

  • 이후 1에 빼주어
    ‘시야각’에 ‘수평’에 가까운 ‘테두리’ 부분을 1
    직각에 가까운 정면 부분을 0으로 잡아 f0에 저장
    (이전에 Rim Lighting 할때도 사용한 방식)

  • f0을 pow하여 값을 테두리에 더 가깝게 한 후,
    직교에 가깝다면(f0 : 0) fresnelR0의 값을,
    아니라면 float3(1.0)의 값을 반환

  • 그렇게 반환한 값을 specular 값에 곱해준다
    (테두리의 빛은 반사광이므로?)

  • IBL 에서 Specular는 반사방향이 R로 샘플링을 하는 편
    여기서는 표면 법선인 Normal과 시점 V를 이용
    RdotN과 NdotV가 SchlickFresnel에선 결과가 같다하여
    여기서는 NdotV를 사용

  • 결과적으로 ‘테두리’가 아닌 값은 fresnelR0의 수치에 영향을 받음

  • SchlickFresnel?
    일반 Fresnel 보다 더 빠르게 사용할 수 있는 ‘근사’식
    (실시간 렌더링에선 보통 이걸로 Fresnel을 구현하여 사용)

일반 버전

Image

fresnelR0의 색을 붉게 조정한 버전

Image

식의 의미?

Image

pow(5.0)을 한 이유?
그래프를 보듯
값이 ‘빠르게’ 내려와 완만하게 값을 유지하게 된다
(우린 Schlick 사용 중)

실제로 정확하게 구현하려면
저 아래쪽처럼 내려가는 과정이 필요

우리는 ‘근사식’을 사용하기에 빠르지만 살짝은
정교하지 못한 결과를 얻는것과 같다

댓글남기기