Substrate가 정식 버전이 되면서 5.7 버전으로 시작하는 프로젝트에는 Substrate가 기본적으로 활성화 된다고 한다.
기존 셰이딩 모델은 Diffuse에 Lambert를 사용했지만, Substrate는 다른 모델을 사용한다는 글을 봐서 찾아봤다.
// Shaders/Private/Substrate/SubstrateEvaluation.ush
#if MATERIAL_ROUGHDIFFUSE
if (Settings.bRoughDiffuseEnabled && any(DiffuseColor > 0))
{
Sample.DiffusePathValue = Diffuse_GGX_Rough(DiffuseColor, SafeRoughness, NoV, AreaLight.NoL, VoH, NoH, GetAreaLightDiffuseMicroReflWeight(AreaLight));
}
else
#endif
{
Sample.DiffusePathValue = Diffuse_Lambert(DiffuseColor);
}
Material 설정의 Rough Diffuse는 기본적으로 비활성화 되어있지만, Substrate가 활성화 되면 이 옵션 또한 강제로 활성화 된다.
따라서 DiffuseColor가 float3(0.0, 0.0, 0.0)만 아니라면 Diffuse_GGX_Rough 함수로 디퓨즈를 계산한다.
// Shaders/Private/BRDF.ush
#define ROUGH_DIFFUSE_BRDF_VERSION 2
// This models a rough surface that has a GGX NDF where each microfacet has a lambertian response. Various models have been proposed
// to try and approximate this behavior.
float3 Diffuse_GGX_Rough( float3 DiffuseColor, float Roughness, float NoV, float NoL, float VoH, float NoH, float RetroReflectivityWeight )
{
// We saturate each input to avoid out of range negative values which would result in weird darkening at the edge of meshes (resulting from tangent space interpolation).
NoV = saturate(NoV);
NoL = saturate(NoL);
VoH = saturate(VoH);
NoH = saturate(NoH);
#if ROUGH_DIFFUSE_BRDF_VERSION == 3
// It turns out the EON model in the range [0, 0.4] is nearly a perfect match to a ground truth
// simulation of diffuse microfacets oriented with a GGX NDF.
float VoL = 2 * VoH * VoH - 1; // double angle identity to keep signature above consistent with other models
return Diffuse_EON(DiffuseColor, RetroReflectivityWeight * Roughness * 0.4, NoV, NoL, VoL);
#elif ROUGH_DIFFUSE_BRDF_VERSION == 2
// [ Chan 2024, "Multiscattering Diffuse and Specular BRDFs", Unpublished manuscript ]
Roughness *= RetroReflectivityWeight;
const float Alpha = Roughness * Roughness;
// The original writeup uses an FSmooth term inspired by Burley diffuse to balance energy between spec/diffuse.
// However in our implementation the energy balance between diffuse and spec is handled externally, so we stick
// to a plain lambertian for the Roughness=0 limit.
const float FSmooth = 1;
const float Scale = max(0.55 - 0.2 * Roughness, 1.25 - 1.6 * Roughness);
const float Bias = saturate(4 * Alpha);
const float FRough = Scale * (NoH + Bias) * rcp(NoH + 0.025) * VoH * VoH;
const float DiffuseSS = lerp(FSmooth, FRough, Roughness);
const float DiffuseMS = Alpha * 0.38;
return (1 / PI) * DiffuseColor * (DiffuseSS + DiffuseMS);
#else
// [ Chan 2018, "Material Advances in Call of Duty: WWII" ]
// It has been extended here to fade out retro reflectivity contribution from area light in order to avoid visual artefacts.
float a2 = Pow4(Roughness);
// a2 = 2 / ( 1 + exp2( 18 * g )
float g = saturate( (1.0 / 18.0) * log2( 2 * rcpFast(a2) - 1 ) );
float F0 = VoH + Pow5( 1 - VoH );
float FdV = 1 - 0.75 * Pow5( 1 - NoV );
float FdL = 1 - 0.75 * Pow5( 1 - NoL );
// Rough (F0) to smooth (FdV * FdL) response interpolation
float Fd = lerp( F0, FdV * FdL, saturate( 2.2 * g - 0.5 ) );
// Retro reflectivity contribution.
float Fb = ( (34.5 * g - 59 ) * g + 24.5 ) * VoH * exp2( -max( 73.2 * g - 21.2, 8.9 ) * sqrtFast( NoH ) );
// It fades out when lights become area lights in order to avoid visual artefacts.
Fb *= RetroReflectivityWeight;
float Lobe = (1 / PI) * (Fd + Fb);
// We clamp the BRDF lobe value to an arbitrary value of 1 to get some practical benefits at high roughness:
// - This is to avoid too bright edges when using normal map on a mesh and the local bases, L, N and V ends up in an top emisphere setup.
// - This maintains the full proper rough look of a sphere when not using normal maps.
// - This also fixes the furnace test returning too much energy at the edge of a mesh.
Lobe = min(1.0, Lobe);
return DiffuseColor * Lobe;
#endif
}
Diffuse_GGX_Rough 함수에서는 Chan 모델을 사용하고 있는걸 확인할 수 있다.
ROUGH_DIFFUSE_BRDF_VERSION 값은 기본적으로 2로 지정되어있고 Chan 2024 모델을,
3이라면 EON(Energy-Preserving Oren–Nayar) 모델을,
그 외의 값이면 Chan 2018 모델을 사용한다.
// Shaders/Private/BRDF.ush
// [Portsmouth et al. 2025, "EON: A Practical Energy-Preserving Rough Diffuse BRDF"]
float3 Diffuse_EON( float3 DiffuseColor, float Roughness, float NoV, float NoL, float VoL )
{
// Albedo inversion for EON model to maintain a consistent color with lambert
float3 Rho = DiffuseColor * (1.0 + (0.189468 - 0.189468 * DiffuseColor) * Roughness);
// This is the main shaping term from the Oren-Nayar model (with tweaks by Fujii)
float S = VoL - NoV * NoL;
float SOverT = max(S * rcp(max(1e-6, max(NoV, NoL))), S);
const float constant1_FON = 0.5f - 2.0f / (3.0f * PI);
// AF = rcp(1 + Roughness * constant1_FON) is nearly a straight line, so approximate it as such
float AF = 1 - Roughness * (1 - 1 / (1 + constant1_FON));
float f_ss = AF * (1 + Roughness * SOverT);
// 4th Order approximation from the paper is a bit too heavy, first order seems to work just as well
const float g1 = 0.262048f;
float GoverPi_V = g1 - g1 * NoV;
// Use (1 - Eo) only as a non-reciprocal approach to energy conservation
float f_ms = 1.0f - AF * (1 + Roughness * GoverPi_V);
// The Rho_ms term from the paper can be approximated as just Rho^2
return Rho * (f_ss + Rho * f_ms) * (1.0 / PI);
}
다음 커밋 기록을 보면, 이후 EON 모델로 바꿀 가능성이 있어 보인다.
https://github.com/EpicGames/UnrealEngine/commit/72c33452e9e6db428b72281571dcac7b16fe312b
'언리얼엔진 > 그 외' 카테고리의 다른 글
| 플러그인 호환 작업을 하며 언리얼 엔진에 기여한 경험 (0) | 2026.01.31 |
|---|---|
| [UE5] GameState의 PlayerArray가 동기화되는 방법 (1) | 2024.09.09 |
| [UE5] C++로 애니메이션 시퀀스 변경, 커브 추가 (0) | 2024.07.03 |
| [UE5] 캐릭터 무브먼트 컴포넌트 작동 순서 정리 (0) | 2024.06.13 |
| [UE5] USTRUCT의 NetSerialize (1) | 2024.03.12 |