-
[DirectX11] Specular BRDFDirectX11 2023. 12. 11. 17:09
Diffuse의 경우에는 간단하게 램버트의 코사인 법칙을 적용했다.
다음으로 Specular를 계산해야하는데 Phong 반사 모델 같은 간단한 방법도 있지만 그것보다는 더 자연스럽게 표현하고싶어서 Cook-Torrance 방식을 적용했다.
코드와 수식 참고한곳:
https://blog.naver.com/canny708/221551990444
GGX와 Schlick Fresnel, 그리고 자주 쓰이는 쿡-토런스 함수
이전 쿡-토런스 게시글에서 가장 핵심이 되는 건 다음 수식입니다. 사실, 나머지 수식과 소스코드는 쿡-토...
blog.naver.com
https://graphicrants.blogspot.com/2013/08/specular-brdf-reference.html?m=0
Specular BRDF Reference
$$ \newcommand{\nv}{\mathbf{n}} \newcommand{\lv}{\mathbf{l}} \newcommand{\vv}{\mathbf{v}} \newcommand{\hv}{\mathbf{h}} \newcommand{\mv}{\m...
graphicrants.blogspot.com
Cook-Torrance의 specular 계산식은 아래와 같다.
D는 Normal Distribution Function으로 NDF라고 줄여서 쓴다.
F는 Fresnel
G는 Geometric Shadowing이다.
l은 빛 벡터
v는 카메라 벡터
n은 물체의 노멀 벡터
h는 Half 벡터다.
Half 벡터는 빛 벡터와 카메라 벡터를 더하고 정규화한 벡터다.
NDF
D 함수는 GGX를 사용했다.
float D_GGX(float a2, float NdotH) { float d = (NdotH * a2 - NdotH) * NdotH + 1; return a2 / (PI * d * d); }
여기에서 a는 Roughness의 제곱이고, a2는 a의 제곱이다.
왼쪽부터 Roughness 0.8 -> 0.6 -> 0.4 -> 0.2
Fresnel
Fresnel 함수는 Cook-Torrance의 함수와 Schlick의 함수가 있다.
F0은 반사율(reflectance)이다.
float3 F_Schlick(float3 SpecularColor, float VdotH) { float Fc = pow(1 - VdotH, 5); return saturate(50.f * SpecularColor.g) * Fc + (1 - Fc) * SpecularColor; } float3 F_Schlick_2(float3 F0, float VdotH) { return F0 + (1 - F0) * pow(1 - VdotH, 5); } // F0 == Reflectance float3 F_Schlick_3(float F0, float VdotH) { return F0 + (1 - F0) * pow(1 - VdotH, 5); } float3 F_CookTorrance(float3 SpecularColor, float VdotH) { float3 SpecularColorSqrt = sqrt(clamp(SpecularColor, float3(0.f, 0.f, 0.f), float3(0.99f, 0.99f, 0.99f))); float3 n = (1 + SpecularColorSqrt) / (1 - SpecularColorSqrt); float3 g = sqrt(n * n + VdotH * VdotH - 1); return 0.5f * pow((g - VdotH) / (g + VdotH), 2) * (1 + pow(((g + VdotH) * VdotH - 1) / ((g - VdotH) * VdotH + 1), 2)); }
Specular에 색상을 입력받는 함수도 있고, F0값을 받는 함수도 있다.
Cook-Torrance보다는 Schlick이 더 가볍다.
왼쪽: Schlick, 오른쪽: Cook-Torrance Specular 색을 빨간색으로 한 결과다.
빛을 받는 면을 비교하면 두 식의 차이는 거의 없다.
왼쪽: Schlick, 오른쪽: Cook-Torrance 하지만 빛이 물체 뒤에 있는 경우에는 색을 다루는 방식의 차이 때문에 색상 차이가 있다.
최종 결과에서는 특정 각도에서 작은 부분에서만 차이가 나기 때문에 어느 것을 사용해도 문제되진 않을 듯 하다.
아래는 그래프로 비교한 결과다.
파란색: Schlick, 검은색: Cook-Torrance x축은 카메라 벡터와 Half벡터의 내적값
y축은 Fresnel 결과값
F0은 0에서부터 1으로 점점 늘어나게 했다.
https://www.desmos.com/calculator/v1vdlrvs1o
Desmos | 그래핑 계산기
www.desmos.com
Geometric Shadowing
G 함수에는 여러가지가 있지만 그 중에서 Smith의 방식으로는 G 함수가 두개의 요소로 나눠진다.
그리고 Smith의 G함수는 다음과 같다.
v를 대입해서 한번 계산하고 l을 대입해서 한번 더 계산한 다음 서로 곱해야 한다.
근사값을 계산하는 Schlick-Beckmann 함수도 있다.
Schlick-Beckmann의 G 함수
그런데 이 함수는 Smith의 잘못된 버전이라는 주장이 있어서 사용하기 전에 Smith의 방식과 비교해보라고 한다.
그래프로 비교해보면 이렇다.
파란색: Smith, 보라색: Schlick-Beckmann https://www.desmos.com/3d/a404adad7b
Desmos | 3D 그래핑 계산기
www.desmos.com
float G_Smith(float a2, float NdotV, float NdotL) { float G_SmithV = NdotV + sqrt(NdotV * (NdotV - NdotV * a2) + a2); float G_SmithL = NdotL + sqrt(NdotL * (NdotL - NdotL * a2) + a2); return (4 * NdotV * NdotL) / (G_SmithV * G_SmithL); } float G_SchlickBeckmann(float a, float NdotV, float NdotL) { float k = a * sqrt(2 / PI); float G_SchlickV = NdotV * (1 - k) + k; float G_SchlickL = NdotL * (1 - k) + k; return (NdotV / G_SchlickV) * (NdotL / G_SchlickL); }
위: Schlick-Beckmann, 아래: Smith, 왼쪽부터 Roughness 0.8 -> 0.6 -> 0.4 -> 0.2 Schlick-Beckmann의 경우 빛을 받지 못하는 쪽은 밝게되는데 이 부분은 최종 결과에 영향을 주지 않는다.
마지막으로 각 값을 곱하면 된다.
float3 HalfDir = normalize(LightDir + ViewDir); float a = pow(Roughness, 2); float a2 = pow(a, 2); float NdotV = max(dot(Normal, ViewDir), 0); float NdotL = max(dot(Normal, LightDir), 0); float VdotH = max(dot(ViewDir, HalfDir), 0); float NdotH = max(dot(Normal, HalfDir), 0); float D = D_GGX(a2, NdotH); float3 F = F_Schlick_2(SpecColor, VdotH); float G = G_SchlickBeckmann(a, NdotV, NdotL); float3 Rs = (D * F * G) * saturate(1 / (4 * NdotL * NdotV + 0.0001));
값과 식을 잘못 적용하면 문제가 생길 수 있다.
최대한 문제가 발생하지 않도록 내적 값과 식을 위와 같이 적용했다.
참고로 Geometric Shadowing의 Smith 식을 보면 분자에 4 * (n · l) * (n · v)가 들어가는데 Specular 계산 식을 보면 동일한 값으로 나누게 되어있다.
이걸 이용해서 다음과 같이 식을 간소화 할 수도 있다고 한다.
float G_SmithOptimized(float a2, float NdotV, float NdotL) { float G_SmithV = NdotV + sqrt(NdotV * (NdotV - NdotV * a2) + a2); float G_SmithL = NdotL + sqrt(NdotL * (NdotL - NdotL * a2) + a2); return 1.f / (G_SmithV * G_SmithL); } // ... float3 Rs = D * F * G_SmithOptimized(a2, NdotV, NdotL);
개인적인 경험으로는 아래처럼 잘못된 결과가 나와서 사용하지 않기로 했다.
잘못 적용된 결과
최종 결과는 아래와 같다.
D: GGX
F: Schlick
G: Schlick-Beckmann
왼쪽부터 Roughness 0.8 -> 0.6 -> 0.4 -> 0.2 'DirectX11' 카테고리의 다른 글
[DirectX11] 쉐도우 맵과 PCF(Percentage Closer Filtering) (3) 2023.12.15 [DirectX11] 텍스쳐와 노멀매핑 (1) 2023.12.13 [DirectX11] Disney Diffuse (0) 2023.12.12 [DirectX11] DrawIndexedInstanced로 여러개의 인스턴스 그리기 (0) 2023.12.07 [DirectX11] obj 파일로 모델 임포트 (2) 2023.12.07