Home 13. 아웃라인과 BRDF
Post
Cancel

13. 아웃라인과 BRDF

Outline

Sobel Mask Filter

(Post Processing - Render Feature)

이미지 처리에서, 한 픽셀에 대해 주변 9가지 픽셀을 검사하여,

변위를 크기화 하여 외곽선을 찾아낼 수 있습니다.

즉 마스킹을 해서 처리하는 방식이라 볼 수 있는데, 그에 따른 마스크는 다음과 같습니다.

Untitled

어느방향에 대한 경계선 성분을 검사할 것 인지에 대해 설정하는데,

저렇게 만들어진 필터를 사용해, 처리합니다.

어떤 점에 대해, 주변 픽셀의 광량을 모두 합산해 저장합니다.

(색상의 강도 혹은 원하는 변조된 값도 넣을 수 있겠죠. depth라던지)

그런데 한쪽 방향의 데이터와, 반대쪽 데이터의 부호가 반대이므로,

양 방향의 차이가 크다면 명확하게 큰 값(부호는 다를 수 있음)을 저장하게 됩니다.

이때 이제 구해진 부호있는 값의 부호를 삭제하기 위해 자기자신으로 내적한 후 제곱근 연산하여 크기를 반환합니다.

Untitled

코드가 한국어보다 쉽습니다.

Untitled

만약 어떤 성분에 대해 극단적으로 찾고싶다면,

Untitled

필터를 이렇게 걸어도 됩니다.

Untitled

BRDF(양방향반사도분포함수)

Cook-Torrance Light Model

Cook - Torrance 라이트 모델은 오브젝트의 표면이

매우 많은 미세면에 의해 구성되어있다고 가정합니다.

이 미세면이 각자 개별적으로 들어오는 빛을 반사할 때, 거칠기에 따라 반사패턴이 달라지죠.

I=ambient+l(nl)×(k+(1k)×Ispec)I = ambient + \sum_l(n \cdot l)\times (k+(1-k)\times I_{spec})

k는 확산비율, NoL은 난반사, I(spec)은 Microfacet BRDF, 극소면 BRDF라고 합니다.

그 중 specular에 해당하는 항은 다음과 같이 구성되는데,

Ispec=F×D×Gπ(NL)(NV)I_{spec} = \frac{F \times D \times G} {\pi \cdot (N \cdot L ) \cdot (N \cdot V )}

추후 여기적힌 π는 4로 나누는게 에너지 보존법칙에 더 적합하다며 4로 대체됩니다.

https://www.cs.cornell.edu/~srm/publications/EGSR07-btdf.pdf

F(fresnel) 프레넬,

G(geometrix Attenuation Factor), 기하학 감쇠요소 (거칠기에 따른 입사광차단, 자체 그림자)

혹은 V라는 약칭으로 불립니다.

D(NDF, Normal Distribution Fuction), 정규 분포 함수

입니다.

Untitled

Untitled

그중 살펴볼 것은 Diffuse 과 Specular(사진의 Glossy)입니다.

어떻게 빛을 확산하는지 알아봅시다.

Diffuse BRDF: Lambert Light model

램버트는 Diffuse만 처리하는 간단한 라이트 모델입니다.

노멀과 빛의 내적(N dot L)을 통해 음영을 표기합니다.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
Shader "Hidden/Lambert"
{
    Properties
    {
        _BaseColor ("Base Color", Color) = (1, 1, 1, 1)
    }

    HLSLINCLUDE
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

        struct Attributes
        {
            float4 positionOS   : POSITION; 
            float3 normal : NORMAL;                
        };

        struct Varyings
        {
            float4 positionHCS  : SV_POSITION;
            float3 normal : NORMAL;
        };            

        Varyings vert(Attributes IN)
        {
            Varyings OUT;
            OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
            OUT.normal = TransformObjectToWorldNormal(IN.normal);
            return OUT;
        }

    ENDHLSL

    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalRenderPipeline" }

        HLSLINCLUDE
            #pragma multi_compile _MAIN_LIGHT_SHADOWS
            #pragma multi_compile _MAIN_LIGHT_SHADOWS_CASCADE
            #pragma multi_compile _SHADOWS_SOFT

            CBUFFER_START(UnityPerMaterial)
                float4 _BaseColor;
            CBUFFER_END
        ENDHLSL

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            half4 frag(Varyings IN) : SV_Target
            {
                Light light = GetMainLight();
                float lambertian = saturate(dot(IN.normal, light.direction));
                return _BaseColor * lambertian;
            }

            ENDHLSL
        }
    }
}

Untitled

Diffuse BRDF: Half Lambert

하지만 램버트는 어두운면이 너무 어두워, 디테일이 모두 날라간다는 점이 있습니다.

그래서 절반을 곱하고 절반을 더한, 하프라이프 시절 만들어진 방식이 Half Lambert입니다.

Untitled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Pass
{
    HLSLPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    
    half4 frag(Varyings IN) : SV_Target
    {
        Light light = GetMainLight();
        float lambertian = saturate(dot(IN.normal, light.direction));

        lambertian = lambertian * 0.5f + 0.5f;
        
        return _BaseColor * lambertian;
    }

    ENDHLSL
}

Untitled

Specular BRDF: Phong Light Model(Phong Reflection Model)

퐁 라이트 모델의 연산방법은 다음과 같습니다.

일단 노멀과 빛의 반사 빛 벡터를 구합니다.

이후 반사 빛 벡터를 시선벡터의 역과 내적하여 빛이 눈에 도달하는 정도를 구합니다.

빛의 방향은 표면으로 들어오는 방향,

반사되는 방향은 표면에서 나가는 방향, 시선벡터는 들어오는 방향이므로

시선벡터의 역을 반사벡터와 내적하거나 , 혹은 반사벡터의 역을 시선벡터와 내적해야 합니다.

같은 나가거나-들어오는 방향을 이룰 수 있게 말이죠.

Untitled

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
Pass
{
    HLSLPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    
    half4 frag(Varyings IN) : SV_Target
    {
        IN.normal = normalize(IN.normal);

        //Half lambert
        Light light = GetMainLight();
        float lambertian = saturate(dot(IN.normal, light.direction));
        lambertian = lambertian * 0.5f + 0.5f;

        //Phong
        float3 view = GetWorldSpaceNormalizeViewDir(IN.PositionWS);
        float3 reflectDirection = reflect(light.direction, IN.normal);
        half spec = saturate(dot(reflectDirection, -view));
        spec = pow(spec,5);
        return _BaseColor * lambertian + spec;
    }

    ENDHLSL
}

Untitled

Specular BRDF: Blinn - Phong Light Model

Bline - Phong 라이트 모델은 위 방법을 좀 더 계산에 최적화 했습니다.

표면 기준, 빛으로 나가는 방향과 시야로 나가는 방향을 더해, 하프벡터를 만듭니다.

이 하프 벡터의 방향과 표면의 방향을 내적한 값이 1에 가까울 때, 시야와 반사벡터가 같다는 뜻이죠.

Untitled

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
Pass
{
    HLSLPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    
    half4 frag(Varyings IN) : SV_Target
    {
        IN.normal = normalize(IN.normal);

        //lambert
        Light light = GetMainLight();
        float lambertian = saturate(dot(IN.normal, light.direction));
        lambertian = lambertian * 0.5f + 0.5f;

        //BPhong
        float3 view = GetWorldSpaceNormalizeViewDir(IN.PositionWS);    //ShaderVariablesFunctions.hlsl
        float3 halfDirection = SafeNormalize(view + light.direction);  //common.hlsl
        
        half spec = saturate(dot(halfDirection, IN.normal));
        spec = pow(spec,5);
        return _BaseColor * lambertian + spec;
    }

    ENDHLSL
}

Untitled

Specular BRDF: Minimalist Cook-Torrance BRDF

아까 위에서 확인했던

Ispec=F×D×Gπ(NL)(NV)I_{spec} = \frac{F \times D \times G} {\pi \cdot (N \cdot L ) \cdot (N \cdot V )}

이 식을 근사합니다.

이때, 두 부분으로 분리하는데, D 와 G x F 입니다.

Untitled

Untitled

D=roughness2((NH)2(roughness21)+1)2D = \frac{ roughness^2}{((N \cdot H)^2 \cdot (roughness^2 - 1) + 1 )^2} V×F=1(LH)5(LH)2(1roughness2)+roughness2V \times F = \frac{1-(L\cdot H)^5} {(L\cdot H)^2 \cdot (1-roughness^2) + roughness^2}

이렇게 나온 그래프는

Untitled

이런 형태를 띄는데,

이를 이제 근사합니다.

V×Fapprox=1.0((LH)2(roughness+0.5))V \times F_{approx} = \frac{1.0 }{ ( (L\cdot H)^2 \cdot (roughness + 0.5) )}

Untitled

최종적으로 이를 합쳐 처리합니다.

Ispec=roughness44π((NH)2(roughness41)+1)2(LH)2(roughness+0.5)I_{spec} = \frac{roughness^4}{4 \cdot \pi \cdot ((N \cdot H)^2 \cdot (roughness^4-1)+1)^2 \cdot (L \cdot H)^2 \cdot (roughness+0.5)}
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
32
33
34
35
36
Pass
{
    HLSLPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    
    half4 frag(Varyings IN) : SV_Target
    {
        IN.normal = normalize(IN.normal);

        //lambert
        Light light = GetMainLight();
        float lambertian = saturate(dot(IN.normal, light.direction));
        lambertian = lambertian * 0.5f + 0.5f;

        //BPhong
        float3 view = GetWorldSpaceNormalizeViewDir(IN.PositionWS);    //ShaderVariablesFunctions.hlsl
        float3 halfDirection = SafeNormalize(view + light.direction);  //common.hlsl
        
        float NoH = saturate(dot(halfDirection, IN.normal));
        half LoH = half(saturate(dot(light.direction, halfDirection)));
				
				//approx GGX
        float roughness2 = _Roughness * _Roughness; //Property

        float d = NoH * NoH * (roughness2 - 1) + 1.00001f;
        half d2 = half(d * d);

        half LoH2 = LoH * LoH;
        half specularTerm = roughness2 / (d2 * max(half(0.1), LoH2) * (roughness * 4.0 + 2.0));

        return _BaseColor * lambertian + specularTerm;
    }

    ENDHLSL
}

Untitled

Image Based Lighting

큐브맵 샘플링입니다.

[https://learnopengl.com/Advanced-OpenGL/Cubemaps](https://learnopengl.com/Advanced-OpenGL/Cubemaps)

https://learnopengl.com/Advanced-OpenGL/Cubemaps

사실 개념자체는 간단합니다. 큐브의 전개도를 펼쳐 각 이미지를 작성한 다음,

아까 Phong 모델에서 사용하던것과 같이 반사벡터를 구해

해당 반사벡터가 가르키는 큐브의 색상을 가져오면 됩니다.

Spherical Harmonics (SH, 구면조화)

라이트프로브 혹은 스카이박스로 부터 가져오는 방법입니다.

보통 L에 대해 대역(Band)이라고 말하며,

각 대역은 해당차수의 다항식과 동일(0대역 = 0차 = 상수)하며,

주어진 대역에는 2L+1개의 함수가 존재합니다.

라이트 정보는 L2 구형 고조파라고도 하는 3차 다항식을 이용해,

Untitled

기저함수의 다항식 형식은 다음과 같습니다 기저함수의 다항식 형식은 다음과 같습니다

Untitled

각 색상채널에 대해 9개, 총 27개의 부동소숫점값을 사용하여 저장합니다.

이를 큐브맵 시각화(붉은색 - 양수, 녹색 - 0, 파란색 - 음수)하면 다음과 같습니다.

Untitled

유니티에서 사용하는 정보는 Vector4를 7개 사용합니다.

마지막 값을 제외한 27개 맞습니다.

Untitled

해당데이터는

Untitled

다음과 같이 샘플링되어 사용됩니다.

여기서 각 대역에 대해

Untitled

와 같이 계산해서 던지고 있습니다.

대충 찾아봅시다.

Untitled

SHA에는 각 R,G,B에 대한 x,y,z 기저 및 강도(w)를 설정하고 있습니다.

SH를 직접 가져와서 넣어봤습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void SetSH()
{
    LightProbes.GetInterpolatedProbe(target.transform.position, target, out sh);
    
    for (var i = 0; i < 3; i++)
        SHA[i] = new Vector4(sh[i, 3], sh[i, 1], sh[i, 2], sh[i, 0] - sh[i, 6]);

    for (var i = 0; i < 3; i++)
        SHB[i] = new Vector4(sh[i, 4], sh[i, 5], sh[i, 6] * 3.0f, sh[i, 7]);

    SHC = new Vector4(sh[0, 8], sh[1, 8], sh[2, 8], 1);

    mat.SetVector("_SHAr", SHA[0]);
    mat.SetVector("_SHAg", SHA[1]);
    mat.SetVector("_SHAb", SHA[2]);
    mat.SetVector("_SHBr", SHB[0]);
    mat.SetVector("_SHBg", SHB[1]);
    mat.SetVector("_SHBb", SHB[2]);
    mat.SetVector("_SHC", SHC);
}
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
32
33
34
35
36
37
38
39
half4 frag(Varyings IN) : SV_Target
{
    IN.normal = normalize(IN.normal);

    //lambert
    Light light = GetMainLight();
    float lambertian = saturate(dot(IN.normal, light.direction));
    lambertian = lambertian * 0.5f + 0.5f;

    //BPhong
    float3 view = GetWorldSpaceNormalizeViewDir(IN.PositionWS);    //ShaderVariablesFunctions.hlsl
    float3 halfDirection = SafeNormalize(view + light.direction);  //common.hlsl
    
    float NoH = saturate(dot(halfDirection, IN.normal));
    half LoH = half(saturate(dot(light.direction, halfDirection)));

    //GGX
    float roughness2 = _Roughness * _Roughness;

    float d = NoH * NoH * (roughness2 - 1) + 1.00001f;
    half d2 = half(d * d);

    half LoH2 = LoH * LoH;
    half specularTerm = roughness2 / (d2 * max(half(0.1), LoH2) * (_Roughness * 4.0 + 2.0));
    
    //SH
    
    real4 SHCoefficients[7];
    SHCoefficients[0] = _SHAr;
    SHCoefficients[1] = _SHAg;
    SHCoefficients[2] = _SHAb;
    SHCoefficients[3] = _SHBr;
    SHCoefficients[4] = _SHBg;
    SHCoefficients[5] = _SHBb;
    SHCoefficients[6] = _SHC;

    half4 ambient = float4(SampleSH9(SHCoefficients, IN.normal), 1);
    return _BaseColor * lambertian + specularTerm + ambient;
}

Untitled

이제 더 알고싶다면 GDC 2008에 소개된 Stupid SH Tricks를 읽으면 됩니다.

Untitled

참조

https://learnopengl.com/Lighting/Basic-Lighting

https://jcgt.org/published/0007/04/01/paper.pdf

https://pharr.org/matt/blog/images/average-irregularity-representation-of-a-rough-surface-for-ray-reflection.pdf

http://www.cs.cornell.edu/~srm/publications/EGSR07-btdf.pdf

  • 해당 문서에는 일부 오류 수정사항이 적용되지 않았습니다.

아래 원본 문서를 확인해주세요

https://www.graphics.cornell.edu/~bjw/microfacetbsdf.pdf

Untitled

Untitled

http://www.ppsloan.org/publications/StupidSH36.pdf

This post is licensed under CC BY 4.0 by the author.
Contents