본문 바로가기

Graphics , Rendering

06. BRDF 구현하기(Implementing BRDF)

 

Physically Based Shader Development for Unity 2017: Develop Custom Lighting Systems, 저자: Claudia Doppioslash - Google Play 도서

Physically Based Shader Development for Unity 2017: Develop Custom Lighting Systems - 저자가 Claudia Doppioslash인 eBook입니다. PC, Android, iOS 기기에서 Google Play 북 앱을 사용해 이 책을 읽어 보세요. 책을 다운로드하여 오프라인으로 읽거나 Physically Based Shader Development for Unity 2017: Develo

play.google.com

  • 번역본이 나오기 이전에 개인적으로 했던 것이라 오역 많음

BRDF 구현

 

9장에서, 우리는 Phong을 에너지를 보존하도록 수정하였다. 이전 장에서는, 구현 가능한 많은 BRDF들을 소개하였다.

이번 장에서는 Unity의 Surface shader로 그 중 하나의 BRDF들을 구현해 볼 것이다.

 

어느 BRDF를 구현할까?

 

게임계에서 가장 많이 사용되는 BRDF는 현재 CookTorrence와 Disney BRDF이다. 그 둘은 모두 다른 면모에서 물리 기반 brdf이다. 디즈니 BRDF는 완전히 물리 기반이라기보다는 원칙에 입각한(Principled) BRDF이다.

 

* Disney "Principled BRDF란? (아래 Brand Burley 슬라이드 내용에서 발견함)

1. Intuitive rather than physical parameters should be used.

물리적인 파라미터보다는 직관적인 파라미터를 사용해야 한다.

2. There should be as few parameters as possible.

가능한 한 적은 파라미터들을 사용해야 한다.

3. Parameters should be zero to one over their plausible range.

파라미터들은 타당한 범위 내에서 0에서 1 사이여야 한다.

4. Parameters should be allowed to push beyond where it makes sense.

파라미터들은 타당한 경우 범위를 벗어날 수 있다.

5. All combinations of parameters should be plausible.

파라미터의 모든 조합은 이치에 맞아야 한다.

 

Disney BRDF를 개발할 당시 주된 목적은 이 대부분의 재질을 구현할 수 있을 뿐만 아니라, 아티스트들이 다루기에 아주 직관적인 BRDF를 개발하려는 목적도 있었다. 물리와 직관이 서로 모순될 때는, 직관이 이긴다. 이 둘을 BRDF 익스플로러에서 분석해 보면, 디즈니의 로브가 얼마나 직관적인지 눈치챌 것이다. 로브들은 아주 복잡한 모양으로 변형될 수 있다. 반면 CookTorrence의 로브들은 두께와 크기에서 다양한 변화를 줄 수 있지만, 그 특유의 모양을 쭉 유지한다.

CookTorrance는 미세면 기반의 BRDF의 좋은 표본이지만, 디즈니 BRDF가 대체로 더 흥미롭고, 아티스트들이 사용하기 위해 고안된 BRDF의 좋은 표본이다. 두 가지 모두 실용적인 만큼 학습의 목적으로, 두 가지 모두를 구현햐 보는 것으로 첫 번째 스텝을 시작하도록 하자. 이것이 "레퍼런스 모으기(gathering references)" 라고 불리는 단계이다. 완전한 디즈니 BRDF는 구현하기 꽤나 복잡하므로, 당신의 첫번째 BRDF 구현으로는 적합하지 않다.

우리는 CookTorrance의 스페큘러 항을 구현하고, 디즈니 BRDF의 디퓨즈 항 부분만 구현해볼 것이다.

 

미세면 BRDF는 스페큘러만 있으므로, 항상 디퓨즈 BRDF로 보완되어야 한다. 보통, 이 부분이 램버트가 된다. 하지만, OrenNayar 와 디즈니 BRDF의 디퓨즈 부분도 역시 좋은 선택지이다.

 

 

레퍼런스 찾기

 

BRDF를 구현할 때의 첫 번째 문제는 좋으느 소스를 찾는것이다. 먼저 원본 공식을 찾은 다음, 이것을 배리에이션 해 보고, 레퍼런스 구현을 찾고, 아니면 최소한 다른 사람의 구현 시도를 찾는다. 더 오래된 BRDF들은 아주 많은 구현들이 널려있지만, 어떤 특정한 플랫폼이나 조건에 맞게끔 조정될 필요가 있다. 오래된 BRDF를 완전히 신뢰하기보다는 얼마나 원본 공식을 따라갔는가를 체크해야 한다.

대부분의 경우, 당신이 찾은 코드는 당신이 BRDF를 구현하고자 하는 플랫폼에 타겟팅되지 않은 경우가 대부분일 것이므로, 구현된 brdf를 읽기 위해서 가장 흔히 쓰이는 셰이딩 언어를 배워야 한다.

셰이딩 언어는 꽤나 비슷하므로, 그렇게 많은 공수가 들지는 않을 것이다. HLSL과 GLSL은 가장 많이 쓰이는 셰이딩 언어 중 두 가지이다.

레퍼런스 재질을 찾을 때, 거의 버려진(semi-abandoned) 레퍼지토리나, BRDF를 거의 절반 정도 바꿔버린 구현자들이나, 때때로 쓸모있지만 당신을 헷갈리게 하고 잘못된 길로 이끄는 재질들을 찾게 될 것이다. 이 주의사항을 유념하두고, 가능한 한 원본 페이퍼를 참고하도록 하자.

경고를 했으니, 이제 레퍼런스를 모아보자.

 

CookTorrance

CookTorrance BRDF들은 1981년에 연구 논문에 의해 소개되었다. 아주 인기있어서 많은 사람들이 이것에 대해 연구하고 개량했다. 미세면 이론으로부터 도출되었다.

우리의 주된 레퍼런스는 아래와 같다:

 

 

좀더 깊게는, 다음 문헌들을 참고할 수 있다:

 

보다시피, 우리는 AAA게임의 렌더링 테크닉에 관한 최신 연구를 포함하는 최근 참고자료들로부터 정보를 얻고 있다. 그러나 만약 시간이 있다면, BRDF의 역사를 훓어보고 어디서, 누구에게 의해 어떤 변혁이 만들어졌는지 추적하는 것도 좋다. 이를 위해, SIGGRAPH 멤버십이 아주 가치가 있겠지만, 그냥 이 자료들을 구글링하고 무료 버전을 찾아내는 것도 좋다.

많은 그래픽 프로그래머와 연구자들은 블로그를 가지고 있고, 거기에다 유용한 정보를 출판하고 논문이나 기술들을 설명한다. 18장에서 이들 일부를 소개하고 있다.

 

Disney

Disney BRDF는 2012년 Brent Burley에 의해 발간되었다. 아주 최신의 BRDF이다 - 이것은 즉 원본 소스 이외에 이를 사용한 재질이 아직 많지는 않다는 것을 의미한다.

Disney BRDF에 대한 참고 논문은 다음과 같다:

 

 

다른 쓸모있는 레퍼런스들은 아래와 같다:

 

http://simon-kallweit.me/rendercompo2015/report/

 

 

페이퍼에서부터 시작하기

이 섹션에서는 사용 가능한 BRDF들의 특성과 공식들을 훑어볼 것이다. 논문에 포함된 공식 중 아주 일부분을 보여줄 것이다. 왜냐하면, 어떤 점에서는 BRDF는 수학을 코드로 변환하는 것으로 결론이 나기 때문이다.

그러므로 주의하라, 수학이 다가올 것이다. 하지만 이 공식들에 딱히 수학적으로 특별히 대단한 것은 없다. 그냥 덧셈과 곱셈, 뺄샘, 나눗셈, 거듭제곱, 분수, 그리고  - 코드에서는 내적이 될 - 코사인 등을 사용할 것이다. 아마도 가장 최악의 부분은 그들이 문자 하나인 변수로 흩뿌려져 있을 때일 것이다(이것에 익숙해질 필요가 있다). 분석의 용이함을 위해, 대부분의 공식에 코드 예제를 추가해 두었다.

 

CookTorrance(또는 미세면) BRDF

8장의 미세면 이론에서 배운 모든 것들이 CookTorrance에 적용된다. CookTorrance는 이 점에서 미세면 이론과 거의 동의어이다. 왜냐하면 그 골조가 새로운 "파트" 들과 다시 맞추어지면서 재사용되기 때문이다. 미세면 이론에서 도출된 BRDF들이 가진 용어들을 기억할 것이다: 분포(D), 프레넬(F), 그리고 기하(G, 마스킹과 그림자를 주로 다루는 부분)..

저 용어들을 대신해 다른 근사치들로 대체해도 된다. 예를 들어, 프레넬에는 여러 가지 근사값이 있다(Schlick이 가장 인기 있다). 마찬가지로, 기하 용어에는(Geometry term), 가장 많이 쓰이는 선택지는 Smith의 그림자 함수에 대한 Schlick의 근사값이다. 같은 함수에 대해 Walter의 근사값도 있고, 다른 함수에 대한 근사값도 있다.

그리고 마지막으로(가장 중요함), 당신이 고를 수 있는 많은 분포 함수(NDF, Normal Distribution Function) 가 있다. 분포 함수는 BRDF 결과물에 아주 큰 변화를 가져온다. 적합한 NDF로는 Beckmann, Phong, 그리고 GGX가 있다.

CookTorrance 구현을 하기 위해, 각각의 용어에 대해 어떤 선택을 할지 정해야 한다. 보통 이것이 셰이더를 빠르게 하면서, 동시에 가능한 한 셰이더를 좋아 보이게끔 하는 문제가 된다. 선택은 타겟 플랫폼에 따라 달라지게 된다. 우리는 가장 보편적인 선택인, GGX, 프레넬 항으로 Schlick, 그리고 기하 항으로 역시 Schlick을 사용할 것이다.

아래는 최근 논문들에서 보게 될 CookTorrance 공식이다.

 

D는 분포 함수(Distribution Function), G는 기하 항(Geometry term), 그리고 F는 프레넬 항이다. 이 공식에서, NDF가 하프 벡터에 의존적이고

, 프레넬은 빛 방향 & 하프벡터

, 기하 항은 빛 방향 & 시점방향 & 하프벡터에 의존적

이라는 것을 알 수 있다.

이미 이 공식의 각 항들에 대해 쭉 살펴보았으므로, 공식을 각 항에 적용해보자. 먼저, 프레넬에 Schlick을 대입해 보자:

눈치챘겠지만, v나 wo 이나 다른 나가는 빛 방향을 나타내는 변수보다는 하프 벡터에 의존적이다. 이것이 BRDF에 쓰이는 버전이기 때문이다. F0은 스페큘러 컬러를 나타낸다.

나이브(?)한 구현으로는, 프레넬 항이 아래와 같을 것이다:

 

float SchlickFresnel(float4 SpecColor, float lightDir, float3 halfVector){

	return SpecColor + ( 1 - SpecColor ) * pow( 1 - (dot( lightDIr, halfVector )), 5);

}

 

다음은 Smith 그림자 함수의 Schlick 근사값이다. Unreal 페이퍼의 수정된 버전을 사용할 것인데, 우리가 선택한 NDF인 GGX와 함께 잘 동작하도록 맞춰져 있기 때문이다.

먼저 K를 정의한다:

코드로 나타내면 다음과 같다:

 

float modifiedRoughness = _Roughness + 1;
float k = sqr(modifiedRoughness) / 8;

G1 함수에서 요렇게 사용된다.

코드로 나타내면 다음과 같다:

 

float G1(float k, float NDotV)
{
 
    return NDotV / (NDotV * (1 - k) + k);
}

이것을 빛방향에 한번, 시점방향에 한번 적용해야 하고, 아래와 같이 곱해진다:

 코드로 나타내면 다음과 같다:

 

float g1L = G1(k, NdotL);
float g1V = G1(k, NdotV);
G = g1L * g1V;

마지막으로, NDF, 즉 분포함수를 위한 방정식이 필요하다. GGX를 사용할 것이므로, 언리얼 페이퍼의 약간 변형된 버젼을 사용할 것이다.

 

코드로 나타내면 다음과 같다:

 

float alphaSqr = sqr(alpha);
float denominator = sqr(NdotH) * (alphaSqr - 1.0) + 1.0f;
D = alphaSqr / (PI * sqr( denominator ));

우리가 디퓨즈를 언급하지 않았다는 것을 눈치챘을 것이다. 이것은 CookTorrance가 디퓨즈를 포함하지 않기 때문이다. 다른 소스에서 하나를 집어넣어줘야 한다. 흔히 램버트(Lambert)가 쓰이지만, 다른 선택지도 있다.

이것으로, BRDF를 구현하기 위한 모든 조각들이 모아졌다. 이제 구현을 시작할 차례이다.

 

Disney BRDF

 

CookTorrance와 마찬가지로, 이 섹션은 Disney BRDF를 살펴볼 것이다.

어느 BRDF에나 적용될 수 있는 부분이지만, 페이퍼에서 가장 중요한 점은 모든 파라미터들이 0~1의 범위를 공유해야 한다는 것이다. 이 점은 많은 BRDF들이 비직관적으로 아무렇게나 나뉜 범위들을 갖는 것에 대해 아주 좋은 지적이 된다.

이 BRDF에서는, 많은 파라미터들이 서로 다른 로브 모양을 보간하고 있는데, 이것은 아주 흥미로운 접근이다. 이 점은 BRDF를 실제 세계의 재질들의 다양성에 더욱 잘 맞출 수 있게끔 해 준다.

방정식을 보면, 이 BRDF는 미세면 이론으로부터 곧바로 도출되지는 않았지만, 이에서 영감을 얻었다. 따라서 방정식을 이루는 항(tern) 들이 우리가 CookTorrance에서 본 것과 유사하게 느껴질 것이다.

디퓨즈 부분은 램버트에서 더욱 향상시키려 했다.  Fresnel Schlick 근사값을 기억할 것이다 : 아래가 디퓨즈에 사용할 방정식이다.

램버트와는 다르게, Disney 디퓨즈방정식은 프레넬을 사용한다:

여기에서 

 

우리가 CookTorrance에서 보았던 실제 구현 코드와는 완전히 다른 방정식이 원본 페이퍼에 정의되어 있다. 왜냐하면 그들은 서로 다른 로브들 사이의 보간을 포함하지 않기 때문이다.

나이브한 구현으로는, 아래와 같을 것이다.

 

float fresnelDiffuse = 0.5 +  * sqr(LdotH) * roughness;
float fresnelL = 1 + (fresnelDiffuse - 1) * pow(1 - NdotL, 5);
float fresnelV = 1 + (fresnelDiffuse - 1) * pow(1 - NdotV, 5);
float3 Fd = (BaseColor / PI) * fresnelL * fresnelV

이것은 경험에 의거한 모델이다 : 목적은 재질의 Roughness에 따라 다르게 동작하게끔 하는 것이다. 부드러운(smooth) 재질은 조금 어두워야 하는데, 이것은 프레넬 그림자에 의해 얻어진다. 그리고 거친(Rough) 재질은 약간 밝게끔 해야 한다. Subsurface 파라미터는이 디퓨즈 모양과 다른 BRDF 사이를 블렌딩한다. 이것은 Hanrahan-Krueger 의 subsurface BRDF에서 기안되었다.

 

그 다음, 완벽을 기하기 위해 이 BRDF의 스페큘러 부분을 요악하겠만, 이것을 구현하지는 않을 것이다.

D, 즉 분포 함수부터 시작하자.

 

 

 

 

이것은 Generalized-Trowbridge-Reitz 라고 하는데, 이름에서 추측할 수 있듯이 Trowbridge-Reitz 분포에서 아이디어를 얻은 것이다. 이것은 보다 긴 tail을 얻을 목적으로 기안되었는데, 여기서 tail은 가장 밝은 하이라이트 바깥쪽으로의 더 부드러운 falloff를 의미한다. a는 이전 장에서 본 CookTorrance에서 수정된 버전과 같이 러프니스 파라미터이고, c는 스케일링하는 상수다. a를 a = roughness2로 리맵핑하는 것을 선호하는 이유는 좀 더 선형적으로 느껴지는 결과물을 도출해내기 때문이다. 선형적으로 보이는 착시를 얻어 비주얼 퀄리티를 향상시키는 것은 꼭 쉽거나 직관적인 것만은 아니다. 예를 들어, 비선형 형식의 색상에서 우리의 눈은 밝은 색조보다는 어두운 색조들을 더 많이 볼 수 있다.

디즈니 BRDF는 세 개의 스페큘러 로브를 선택했다. 하나는 메인 스페큘러, 하나는 Clear Coat(두 가지 모두 GTR Model을 사용), 그리고 하나는 Sheen이다. sheen 스페큘러 로브는 Fresnel Schilick 모양을 사용한다. 스페큘러 파라미터는 입사하는 스페큘러 양을 결정한다. 이 파라미터는 대부분의 보통 재질들의 범주에 모두 리맵된다. Clear Coat 층은 폴리우레탄의 굴절율과 상응하는 고정된 스페큘러 값을 갖고 있다.

다음으로, 프레넬 항이다. 다시 Schlick 근사치이다 : 여기에서는 새롭게 볼 게 없다. 다음은 G 항인데, GGX에서 도출된 것의 수정된 버전을 사용한다. Clear Coat 레이어의 G는 고정된 값이다.

 

구현

 

CookTorrance 스페큘러를 먼저 구현해보도록 하자. 여기에 Disney의 디퓨즈를 더할 것이다.

 

프로퍼티


Properties 블락과 SubShader 블럭부터 시작하도록 하자. 추가로 필요한 프로퍼티는 _Roughness이다. 만약 우리가 Disney BRDF 전체를 구현한다면, 여기에 많은 프로퍼티들이 선언되어야 한다는 것을 기억하자. surf 함수에도 대단한 것이 없으므로 생략하도록 하자.

 

Shader "Custom/CookTorranceSurface" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _ColorTint("Color", Color) = (1,1,1,1)
        _SpecColor("Specular Color", Color) = (1,1,1,1)
        _BumpMap("Normal Map", 2D) = "bump"
        _Roughness ("Roughness", Range(0,1)) = 0.5
        _Subsurface ("Subsurface", Range(0,1)) = 0.5
    }
 
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
 
        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf CookTorrance
 
        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0
 
 
        struct Input {
            float2 uv_MainTex;
        };
 
        #define PI 3.14159265358979323846F
 
        sampler2D _MainTex;
        sampler2D _BumpMap;
        half _Roughness;
        float _Subsurface;
        fixed4 _ColorTint;
 
 
        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
            UNITY_INSTANCING_BUFFER_END(Props)
 
        struct SurfaceOutputCustom {
        
            fixed3 Albedo;
            fixed3 Normal;
            fixed3 Emission;
            fixed Alpha;
        
        }
 
        void surf (Input IN, inout SurfaceOutputCustom o) {
 
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _ColorTint;
            o.Albedo = c.rgb;
            o.Normal = UnpackNormal(tex2D(_BumpMap, IN, uv_MainTex));
            o.Alpha = c.a;
 
        }

SurfaceOutputCustom 구조체가 SurfaceOutputStandard보다 멤버 변수가 더 적은 것에 주의하자. 우리는 커스텀 라이팅 함수를 넘겨주고 있지만, 아직 그걸 작성하지 않아서 코드가 컴파일되지는 않을 것이다.

변수 중에서, _Roughness 는 CookTorrance 스페큘러와 Disney 디퓨즈에서 쓰이고 있다. _Subsurface 는 Disney 디퓨즈에서만 쓰인다. 우리는 선택적으로 subsurface 근사값을 스킵할 수 있지만, 좋아 보이니까 그냥 두도록 하자.

위 그림에서 보는 것처럼 파이를 #define으로 선언해두는 것을 잊지 말자. 그렇지 않으면 사용하는 플랫폼에 따라 끔찍한 셰이딩 에러를 일으킬 것이다.

 

커스텀 라이팅 함수 구현

이제 CookTorrance와 DIsney 디퓨즈의 구현에 관한 정보들을 압축해내는 작업을 할 것이다. 먼저, 서피스 셰이더에서 커스텀 라이트를 구현하기 위해 필요한 두 개의 함수를 만들 것이다. 이 경우에는 아래와 같은 함수명을 사용한다:

  • inline void LightingCookTorrance_GI
  • inline fixed4 LightingCookTorrance

지금은 이전 장에서 작성한 커스텀 GI 함수를 그냥 붙여넣기 할 것이다. 이것이 어떻게 작동하는지는 다음 장에서 자세히 살펴볼 것이다. 나중에 추가로 유틸리티 함수가 필요하게 될 것인데, 몇몇 반복되는 작업을 abstract away(?) 시키기 위해서이다. 

필자는 커스텀 라이팅 함수를 가장 중요한 값들(NdotL, 등등)을 계산하는 곳으로써 두고, 디퓨즈와 스페큘러를 (아래 코드와 같이) 함께 묶는 곳으로 사용하는 것을 선호한다. 실제 스페큘러와 디퓨즈 계산은 각자의 함수에서 가지고 있도록 하면, 아주 나이스하고 모듈화도 된다.

이 방법대로 하면 코드를 최소한으로 바꾸면서 디퓨즈와 스페큘러를 쉽게 다른 것으로 바꿔칠 수 있다.

 

inline float4 LightingCookTorrance(SurfaceOutputCustom s, float3 viewDir, UnityGI gi)
        {
            
            // Needed Values
 
            UnityLight light = gi.light;
 
            viewDir = normalize(viewDir);
            float3 lightDir = normalize(light.dir);
            s.Normal = normalize(s.Normal);
 
            float3 halfV = normalize(lightDir + viewDir);
            float NdotL = saturate(dot( s.Normal, lightDir));
            float NdotH = saturate(dot( s.Normal, halfV));
            float NdotV = saturate(dot( s.Normal, viewDir));
            float VdotH = saturate(dot( viewDir, halfV));
            float LdotH = saturate(dot( lightDir, halfV));
 
            // BRDFs
 
            float3 diff = DisneyDiff(s.Albedo, NdotL, NdotV, LdotH, _Roughness, _SpecColor);
            float3 spec = CookTorranceSpec(NdotL, LdotH, NdotH, NdotV, _Roughness, _SpecColor);
 
            // Adding diffuse, speculat and tints (Light, Specular)
 
            float3 firstLayer = (diff + spec * _SpecColor) * _LightColor0.rgb;
            float4 c = float4(firstLayer, s.Alpha);
 
#ifdef UNITY_LIGHT_FUNCTION_APPLY_INDIRECT
            c.rgb += s.Albedo * gi.indirect.diffuse;
#endif
 
            return c;        
        
        
        }

 

필요한 대부분의 변수들을 계산해서 디퓨즈와 스페큘러 함수에 넘겨주고 있다. 이것은 값들을 한 번씩만 계산하면 된다는 이점이 있고, 값들이 실제로 사용되는 부분이 어떤 부분인지 상기시켜주기도 한다. 코드를 헷갈리게 하는, 사용하지 않는 변수들을 잊어버리기 쉽다.

 

유틸리티 함수

 

다음 순서는 유틸리티 함수들을 꺼내는 것이다.

 

float sqr(float value)
        {
            
            return value * value;
        
        }
 
        float schlickFresnel(float value)
        {
 
            float m = clamp(1 - value, 0, 1);
            return pow(m, 5);
            
        }
 
        // CookTorrance Geometry Function
        float G1 (float k, float x)
        {
            return x / (x * (1 - k) + k);
        }

sqr은 단순히 2제곱 함수이다 : 코드를 더 읽기쉽게 하기 위해 사용하였다. 그리고 SchlickFresnel 함수는 디퓨즈와 스페큘러 모두에게 사용되었다. 그리고 CookTorrance 기하 함수에 사용될 G1 함수가 있다.

 

CookTorrance 구현

이제 셰이더의 기반 구조가 세워졌으니, CookTorrance 구현을 시작해보도록 하자. 우리눈 n•l, l•h, n•h, n•v, roughness, 그리고 굴절률을 의미하는 스페큘러 색인 F0들을 받는 별도의 함수들을 만들 것이다. 그리고 그 안에서, 이전 장에서 봤던 방저식대로 F, D, G 항을 계산할 것이다.

 

수정된 GGX 분포 항

GGX가 a = roughness2라는 파라미터를 사용하기 때문에 "수정된" GGX라고 부른다. 이 GGX 방정식은 비교적 구현하기 쉽다.

 

 // D
 
        float alphaSqr = sqr(alpha);
        float denominator = sqr(NdotH) * (alphaSqr - 1.0) + 1.0f;
        D = alphaSqr / (PI * sqr(denominator));

코드를 자세히 살펴보면, 서로 꽤 잘 들어맞는 것을 확인할 수 있다.

 

Schlick 프레넬 항

 

CookTorrance 에서 사용되는 Shilick Fresnel 항이다. 보다시피, 일부는 DIsney  디퓨즈와 공통된 부분은 코드가 반복되는 것을 피하기 위해 유틸리티 함수로 구현되었다. 더 직관적은 Specular Color 대신 F0을 변수로 사용함으로써,  함수와 코드 사이의 유사점을 더 쉽게 볼 수 있다.

 

// F (Fresnel Function)
    float LdotH5 = ShlickFreenel(LdotH);
    F = F0 _(1.0 - F0) * LdotH5;
 

수정된 Schlick 기하 항

이것은 약간 더 복잡한 항이다. Smith의 기하 항을 더욱 잘 따르기 위해, 변수 k 가 

 로 번경되었디. Disney BRDF에서부터 온 또 다른 변경은 Roughness가 제곱되기 전 

로 변경되었다는 점이다.

변경의 결과는 아래와 같다:

Disney BRDF에서 온 마지막 변경은 아래와 같다. 이것은 Disney 디퓨즈와 CookTorrance 구현을 더욱 조화롭게 만들어준다.

 

 

 

아래 코드는 비교적 직접적(Straightforward) 이고 이전 몇 섹션 전에 구현한 예제와 아주 비슷하다. G1이 유틸리티 함수임을 기억하자.

 

// G (Geometry term, Schlick's Approximation of Smith
    float r = _Roughness + 1;
    float k = sqr(r) / 8;
    float glL = G1(k, NdotL);
    float glV = G1(k, NdotV);
    G = glL * glV;

CookTorrance를 합치기

 

이제 모든 항을 구현했으니, 이것을 하나의 함수로 합치자.

 

 inline float3 CookTorranceSpec(float NdotL, float LdotH, float NdotH, float NdotV, float roughness, float F0)
        {
            float alpha = sqr(roughness);
            float F, D, G;
 
            // D (Distribution Function)
 
            float alphaSqr = sqr(alpha);
            float denominator = sqr(NdotH) * (alphaSqr - 1.0) + 1.0f;
            D = alphaSqr / (PI * sqr(denominator));
 
            // F (Fresnel Function)
            float LdotH5 = ShlickFreenel(LdotH);
            F = F0 + (1.0 - F0) * LdotH5;
 
            // G (Geometry term, Schlick's Approximation of Smith
            float r = _Roughness + 1;
            float k = sqr(r) / 8;
            float glL = G1(k, NdotL);
            float glV = G1(k, NdotV);
            G = glL * glV;
 
            float specular = NdotL * D * F * G;
            return specular;
        
        }

임시적인 결과를 보자. 아래 그림에서, 최대 Roughness에서 스페큘러가 어떻게 보이는지 관찰할 수 있다. 디퓨즈가 없기때문에 약간 어두운 것이 정상임을 알아두자. 다음 그림은 Roughness가 0.2일때 스페큘러가 어떻게 보이는지 보여준다. 하이라이트가 작아질 때 더 밝아지는 것으로 이 셰이더가 물리 기반임을 알 수 있다.

 

Roughness가 0.2일때

CookTorrance 스페큘러가 구현되었으니, 이제 다음 단계로 Disney 디퓨즈를 구현해보자.

 

Disney 디퓨즈

 

우리는 Disney 디퓨즈를 구현하기로 선택하였다. 더 단순한 Lambert를 선택할 수도 있었지만, 이미 앞 장에서 시도하였다. 그리고 이쪽이 더 흥미롭다.

DIsney BRDF는 꽤나 광범위한 BRDF지만, 디퓨즈 부분은 우리의 목적에 알맞게 소규모이다. 디퓨즈에 관련된 방정식은 아래와 같다:

 

 

 

유틸리티 함수로 구현한 Schlick 프레넬 함수를 여기에 사용할 것이다. 또한, 페이퍼에 포함되지 않지만 BDRF 익스플로러에 포함된 참고 구현을 고려해야 한다. 아래 구현 코드를 보면, 많은 lerp들이 사용된 것을 알 수 있다. 이 중 많은 것들을 단순화시켜 뺄 수 있지만, 그냥 그걸 두기로 결정했다. 왜냐하면 퍼포먼스 이슈도 없고, 결과물도 보기 좋기 때문이다. 현실의 상업 게임에서는, 시각적인 변화를 주는 것들에 대해 더 인정사정없어야 하고(ruthless), 나머지를 근사하거나 삭제해야 한다.

 

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
    // Disney Diffuse
        inline float3 DisneyDiff(float3 albedo, float NdotL, float NdotV, float LdotH, float roughness)
        {
 
            // luminance approximation
            float albedoLuminosity = 0.3 * albedo.r
                + 0.6 * albedo.g
                + 0.1 * albedo.b;
            // normalize luminosity to isolate hue and saturation
            float3 albedoTint = albedoLuminosity > 0 ?
                                albedo / albedoLuminosity :
                                float3(111);
 
            float fresnelL = shlickFresnel(NdotL);
            float fresnelV = shlickFresnel(NdotV);
 
            float fresnelDiffuse = 0.5 + 2 * sqr(LdotH) * roughness;
 
            float diffuse = albedoTint
                            * lerp(1.0, fresnelDiffuse, fresnelL)
                            * lerp(1.0, fresnelDiffuse, fresnelV);
 
            float fresnelSubsurface90 = sqr(LdotH) * roughness;
 
            float fresnelSubsurface = lerp(1.0, fresnelSubsurface90, fresnelL)
                                    * lerp(1.0, fresnelSubsurface90, fresnelV);
 
            float ss = 1.25 * (fresnelSubsurface * (1 / (NdotL + NdotV) - 0.5+ 0.5);
 
            return saturate(lerp(diffuse, ss, _Subsurface) * (1 / PI) * albedo);
 
 
        }
cs

위가 Disney 디퓨즈 함수의 완전판이다. 공식과 맞춰보는 것은 CookTorrance가 공식과 완전하게 맞아떨어졌던 것에 비해 조금 어렵다. 그러나 기능의 핵심 부분은 공식들에서 찾을 수 있으며, 나머지 부분들은 참고 구현 코드에서 가져온 것이다.

이제 셰이더가 완성되었다. 아래 그림은 최대 Roughness와 최대 Subsurface 일때 / 최대 Roughness일때와 최소 Subsurface 일때이다. 보다시피, 램버트보다 디퓨즈 부분이 더 다채롭고 흥미롭다. Subsurface 근사도 꽤 좋다.

 

완벽을 기하기 위해, 가장 낮은 Roughness에서의 셰이더도 살펴보자. 아래 그림에서는 잘 동작하는 것 같지만, 아주 낮은 Roughness에서는 하이라이트가 아예 사라져버린다. Roughness의 범위를 0.1이나 그정도로부터 시작하게끔 하는 것이 나을 것이다.

 

 Disney BRDF의 디퓨즈는 세 가지 로브를 가질 수 있는 Disney BRDF와 함께 쓰일 수 있도록 개발되었다:

clear coat, sheen 그리고 스페큘러. 결과적으로 낮은 Roughness에서는 너무 어두워지는 경향이 있다. 약간의 트위킹을 하면 CookTorrance 스페큘러 로브와 같이 동작하게끔 도울 수 있지만, 이 책의 범위를 벗어난다 - 훨씬 많은 수학적인 내용과 BRDF를 도출하는 방법에 대해 이야기해야 하기 때문이다.

그러나 더 복잡한 수학 계산 없이도 뭔가 할 수 있다. 우리는 Frostbite 페이퍼에서의 수정된 Disney 디퓨즈를 구현해 볼 수 있다. 그들은 그것을 GGX와 함께 사용했고, Disney BRDF의 많은 추가적인 로브들 없이 사용하고 있었으므로, 낮은 roughness에서도 너무 어두워지지 않았으면 했다. 이 문제를 피하기 위해 결국 그것을 근본적으로 리노멀라이즈(Renormalize) 했고, 에너지 보존을 다시 되찾을 수 있었다. 더 많은 정보는 Frostbite 페이퍼를 참고하라.

 

Disney 디퓨즈의 또다른 구현

 

시간을 너무 많이 들이지 않고도, Frostbite 페이퍼에 있는 예제 코드를 적용하고 우리의 기반 구조에서 작동하게 할 수 있다. (아래 코드를 보자). 

Frostbite의 기반 구조는 유니티와는 아주 다르다. Frostbite는 area 라이트에 집중되어 있는데, 유니티는 현재 이를 실시간으로는 포함하지 않는다. 이 차이점이 Frostbite의 코드를 쉽게 유니티로 바로 포팅하지 못하게끔 만든다. 그러므로 유니티 안에서 의도 그대로 작동하도록 코드를 트위킹하려면 Frosibite 페이퍼를 자세히 읽어보아야 한다.

여기서의 목적은 Disney 디퓨즈의 다른 구현을 시도해보고 우리의 케이스에 더 잘 맞는지 알아보는 것이다.

 

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
        float3 FresnelSchlickFrostbite(float3 F0, float F90, float u)
        {
 
            return F0 + (F90 - F0) * pow(1 - u, 5);
 
 
        }
 
        // Disney Diffuse Frostbite version
 
        inline float DisneyFrostbiteDiff(float NdotL, float NdotV, float LdotH, float roughness)
        {
 
            float energyBias = lerp(00.5, roughness);
            float energyFactor = lerp(1.01.0 / 1.51, roughness);
            float Fd90 = energyBias + 2.0 * sqr(LdotH) * roughness;
            float3 F0 = float3(111);
            float lightScatter = FresnelSchlickFrostbite(F0, Fd90, NdotL).r;
            float viewScatter = FresnelSchlickFrostbite(F0, Fd90, NdotV).r;
            return lightScatter * viewScatter * energyFactor;
 
        }
 
 
 
// 커스텀 라이팅 함수 안에서..
 
float3 diff = (DisneyFrostbiteDiff(NdotL, NdotV, LdotH, _Roughness) * s.Albedo) / PI;
cs

당신은 이 방정식을 더 선호할 수도 있다. 원래의 것보다 더 평이하고 저렴하기 때문이다. 목적에 따라 선택할 수 있다. 우리의 입장에는, 디퓨즈와 스페큘러를 포함하는 BRDF가 있으므로, 유니티에 이식해 동작하도록 할 수 있다.

그렇게 하기 위해, "Shader Standart Library" 코드를 다음 장에서 분석해보도록 할 것이다.

 

모두 합치기

 

이 셰이더의 대부분을 이 챕터에 싣긴 했지만, 책으로 인쇄하는 것은 지면 낭비이다. 책의 소스 코드에서 찾아볼 수 있다.

꽤 덩치가 큰 셰이더이지만, 기기들이 더욱 강력해지기 때문에 셰이더들은 더욱 복잡해지고 또한 아름다워질 것이다. 그러므로 우리는 더욱 복잡하더라도 더 보기 좋은 셰이더를 구현해야 한다.

 

요약

 

이 챕터에서는 CookTorrance와 Disney BRDF를 자세히 살펴보고, CookTorrance 스페큘러와 Disney 디퓨즈를 구현해 보았다. 또 레퍼런스를 고르고 모아서 어떤 BRDF든지 구현하는 방법을 살펴보았다.

 

다음 장에서는

 

다음 장에서는 이 커스텀 BRDF를 나머지 다른 유니티 셰이더 서브시스템에 이식하는 방법을 소개한다. 유니티 셰이더 코드의 리버스 엔지니어링을 필요로 하는데, 이것은 나중에 바뀔 수도 있다.