사실 렌더러는 퐁쉐이딩만 하도록 대충 만들어두려고 했는데 보이는게 안이쁘니깐 애니메이션 작업도 흥미가 안생기더라... 그래서 PBR을 구현해보기로 했다. 인터넷에 자료가 많아서 많이 따라갔다.
PBR은 물리 기반(혹은 물리 근사)으로 빛-재질 상호작용을 모델링해, 환경/조명 변화에 대해 일관된 결과를 얻는 렌더링 방식이다.
실시간 PBR에서 핵심 구성은 보통 아래 3가지로 설명한다.
개념
표면은 완전히 매끈하지 않고, 미세한 면(거울 조각)들이 다양한 방향을 가진다고 가정한다.
픽셀 법선(N) 구성 흐름
실무적으로는 “기본 법선(지오메트리)” 위에 “노멀맵(탄젠트 공간)”을 적용해 최종 월드 법선을 만든다.
1) 버텍스 노멀 보간으로 픽셀 노멀 얻기
2) 노멀맵을 샘플링해 TBN으로 월드 공간 변환
float3 ApplyNormalMap(float2 uv, float3 N, float3 T, float3 B)
{
float3 nTS = DecodeNormalTS(gTexNormal.Sample(CommonSampler, uv).xyz);
float3x3 TBN = float3x3(normalize(T), normalize(B), normalize(N));
return normalize(mul(nTS, TBN)); // tangent->world
}
3) D(NDF): Roughness로 분포 폭 제어 (GGX)
Cook-Torrance 스페큘러의 첫 요소는 D (Normal Distribution Function).
여기서 roughness가 하이라이트의 모양/크기에 가장 크게 영향을 준다.
// D: GGX(Trowbridge-Reitz) Normal Distribution Function
// - microfacet들의 법선이 하프벡터 H 방향으로 얼마나 모여있는지(분포)를 근사
// - roughness ↓ -> 분포가 좁아짐(하이라이트 작고 날카로움)
// - roughness ↑ -> 분포가 넓어짐(하이라이트 넓고 흐림)
float DistributionGGX(float NdotH, float roughness)
{
// UE4 관례: roughness를 제곱해서 'a'로 사용 (반응을 더 자연스럽게 만듦)
// a가 작을수록 매우 매끈한 표면에 해당
float a = roughness * roughness;
float a2 = a * a;
// GGX 분모: ((N·H)^2 (a^2 - 1) + 1)^2
// NdotH가 1(=H가 N과 거의 같은 방향)일 때 D가 크게 나와 하이라이트가 생김
float denom = (NdotH * NdotH) * (a2 - 1.0f) + 1.0f;
// 정규화까지 포함한 GGX NDF
return a2 / max(PI * denom * denom, 1e-6);
}
개념
표면에서 나가는 빛(반사 + 확산)은 들어오는 빛을 넘을 수 없다.
Cook-Torrance는 보통 Specular 비율(kS)과 Diffuse 비율(kD)로 나눠 이를 만족시킨다.
kS: 반사(specular)로 나가는 비율kD: 확산(diffuse)로 나가는 비율Fresnel: kS(반사 비율)를 각도로 결정
Fresnel은 시선/입사 각도에 따라 반사가 증가하는 현상.
실시간에서는 보통 Schlick 근사를 사용한다.
F0 = lerp(0.04, baseColor, metallic)F = F0 + (1 - F0) * (1 - cosTheta)^5float NdotL = saturate(dot(N, L));
float NdotV = saturate(dot(N, V));
float NdotH = saturate(dot(N, H));
float VdotH = saturate(dot(V, H));
float3 F0 = lerp(float3(0.04, 0.04, 0.04), baseColor, metallic);
float3 F = FresnelSchlick(VdotH, F0);
float3 kS = F;
float3 kD = (1.0f - kS) * (1.0f - metallic);
개념
BRDF는 입사 방향(wi)와 출사 방향(wo)에 따라 반사되는 빛의 비율을 반환하는 함수다.
Cook-Torrance BRDF는 보통 다음처럼 구성한다.
diffuse = baseColor / PIkD를 곱해 사용pec = (D * G * F) / (4 * NdotV * NdotL)Geometry(G): microfacet 가림(Shadowing/Masking)
거칠수록 미세 요철이 서로를 더 가려서 스페큘러 기여가 줄어든다.
G(N,V,L) = G1(N,V) * G1(N,L)float GeometrySchlickGGX(float NdotV, float roughness) // NdotL이 들어올 수 있음
{
float r = roughness + 1.0f;
float k = (r * r) / 8.0f; // direct lighting용
return NdotV / max(NdotV * (1.0f - k) + k, 1e-6);
}
// G1: Schlick-GGX (direct lighting용 k 사용)
float GeometrySchlickGGX(float NdotV, float roughness)
{
float r = roughness + 1.0f;
float k = (r * r) / 8.0f; // direct lighting용
return NdotV / max(NdotV * (1.0f - k) + k, 1e-6);
}
// Smith: view 방향 가림 * light 방향 가림
float GeometrySmith(float NdotV, float NdotL, float roughness)
{
float ggxV = GeometrySchlickGGX(NdotV, roughness);
float ggxL = GeometrySchlickGGX(NdotL, roughness);
return ggxV * ggxL;
}
// Cook-Torrance specular BRDF 조립은 EvaluatePBR_Direct 내부에서:
// spec = (D*G*F) / (4*NdotV*NdotL)
// diffuse = baseColor/PI (에너지 보존 위해 kD 곱)
// 최종: (diffuse + spec) * radiance * NdotL
스켈레탈 메시

스태틱 메시
