// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "DeferredShadingCommon.ush" #include "BRDF.ush" #include "FastMath.ush" #include "CapsuleLight.ush" #include "RectLight.ush" #include "AreaLightCommon.ush" #include "TransmissionCommon.ush" #include "HairBsdf.ush" #include "ShadingEnergyConservation.ush" #include "ParticipatingMediaCommon.ush" #include "ColorSpace.ush" #ifndef HAIR_BSDF_BACKLIT #define HAIR_BSDF_BACKLIT 1 #endif #if SHADING_PATH_MOBILE #include "MobileGGX.ush" #ifndef MOBILE_SHADOW_QUALITY #define MOBILE_SHADOW_QUALITY 2 #endif //It's always 0 in mobile deferred lighting shaders #ifndef FULLY_ROUGH #define FULLY_ROUGH 0 #endif //It's always 0 in mobile deferred lighting shaders #ifndef NONMETAL #define NONMETAL 0 #endif //It's always 0 in mobile deferred lighting shaders #ifndef MATERIAL_SHADINGMODEL_SINGLELAYERWATER #define MATERIAL_SHADINGMODEL_SINGLELAYERWATER 0 #endif // Always enabled in mobile deferred lighting shaders #ifndef MOBILE_USE_PREINTEGRATED_GF #define MOBILE_USE_PREINTEGRATED_GF 1 #endif //It's always 1 in mobile deferred lighting shaders #ifndef MOBILE_DEFERRED_LIGHTING #define MOBILE_DEFERRED_LIGHTING 0 #endif #ifndef DEFERRED_SHADING_PATH #define DEFERRED_SHADING_PATH 0 #endif /*------------------------------------------------------------------------------ Mobile Shading Model Functions ------------------------------------------------------------------------------*/ half4 GetSSProfilePreIntegratedValue(uint SubsurfaceProfileInt, half NoL, half Curvature) { float3 UV = float3((NoL * .5 + .5), Curvature, SubsurfaceProfileInt); return Texture2DArraySampleLevel(View.SSProfilesPreIntegratedTexture, View.SSProfilesPreIntegratedSampler, UV, 0); } half3 GetEnvBRDF(half3 SpecularColor, half Roughness, half NoV) { #if FULLY_ROUGH return 0.0f; #elif MOBILE_USE_PREINTEGRATED_GF return EnvBRDF(SpecularColor, Roughness, NoV); #elif NONMETAL // If nothing is hooked up to Metalic and Specular, // then defaults are the same as a non-metal, // so this define is safe. return EnvBRDFApproxNonmetal(Roughness, NoV).xxx; #else return EnvBRDFApprox(SpecularColor, Roughness, NoV); #endif } #endif // SHADING_PATH_MOBILE #if 0 void StandardShadingShared( float3 DiffuseColor, float3 SpecularColor, float Roughness, float3 V, half3 N ) { float NoV = saturate( abs( dot(N, V) ) + 1e-5 ); // Diffuse_Lambert Shared.DiffuseMul = DiffuseColor * (1.0 / PI); // D_GGX, Vis_SmithJointApprox float m = Roughness * Roughness; Shared.m2 = m * m; Shared.SpecularMul = (0.5 / PI) * Shared.m2; Shared.VisMad = float2( 2 * NoV * ( 1 - m ) + m, NoV * m ); // F_Schlick Shared.SpecularMul *= saturate( 50.0 * SpecularColor.g ); } void StandardShadingPerLight( Shared, float3 L, float3 V, half3 N ) { float3 H = normalize(V + L); // 3 add, 2 mad, 4 mul, 1 rsqrt float NoL = saturate( dot(N, L) ); // 2 mad, 1 mul float NoH = saturate( dot(N, H) ); // 2 mad, 1 mul float VoH = saturate( dot(V, H) ); // 2 mad, 1 mul // D_GGX, Vis_SmithJointApprox float d = ( NoH * Shared.m2 - NoH ) * NoH + 1; // 2 mad float v = NoL * Shared.VisMad.x + Shared.VisMad.y; // 1 mad float D_Vis = Shared.SpecularMul * rcp( d * d * v ); // 3 mul, 1 rcp // F_Schlick float Fc = pow( 1 - VoH, 5 ); // 1 sub, 3 mul float3 F = Fc + (1 - Fc) * SpecularColor; // 1 sub, 3 mad return Shared.DiffuseMul + D_Vis * F; // 3 mad } #endif struct FDirectLighting { float3 Diffuse; float3 Specular; float3 Transmission; }; struct FShadowTerms { half SurfaceShadow; half TransmissionShadow; half TransmissionThickness; FHairTransmittanceData HairTransmittance; }; FDirectLighting HairBxDF(FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, half NoL, FAreaLight AreaLight, FShadowTerms Shadow) { const float3 BsdfValue = HairShading(GBuffer, L, V, N, Shadow.TransmissionShadow, Shadow.HairTransmittance, HAIR_BSDF_BACKLIT, 0, uint2(0, 0)); FDirectLighting Lighting; Lighting.Diffuse = 0; Lighting.Specular = 0; Lighting.Transmission = AreaLight.FalloffColor * Falloff * BsdfValue; return Lighting; } float New_a2( float a2, float SinAlpha, float VoH ) { return a2 + 0.25 * SinAlpha * (3.0 * sqrtFast(a2) + SinAlpha) / ( VoH + 0.001 ); //return a2 + 0.25 * SinAlpha * ( saturate(12 * a2 + 0.125) + SinAlpha ) / ( VoH + 0.001 ); //return a2 + 0.25 * SinAlpha * ( a2 * 2 + 1 + SinAlpha ) / ( VoH + 0.001 ); } float EnergyNormalization( inout float a2, float VoH, FAreaLight AreaLight ) { if( AreaLight.SphereSinAlphaSoft > 0 ) { // Modify Roughness a2 = saturate( a2 + Pow2( AreaLight.SphereSinAlphaSoft ) / ( VoH * 3.6 + 0.4 ) ); } float Sphere_a2 = a2; float Energy = 1; if( AreaLight.SphereSinAlpha > 0 ) { Sphere_a2 = New_a2( a2, AreaLight.SphereSinAlpha, VoH ); Energy = a2 / Sphere_a2; } if( AreaLight.LineCosSubtended < 1 ) { #if 1 float LineCosTwoAlpha = AreaLight.LineCosSubtended; float LineTanAlpha = sqrt( ( 1.0001 - LineCosTwoAlpha ) / ( 1 + LineCosTwoAlpha ) ); float Line_a2 = New_a2( Sphere_a2, LineTanAlpha, VoH ); Energy *= sqrt( Sphere_a2 / max(Line_a2, 1e-5) ); #else float LineCosTwoAlpha = AreaLight.LineCosSubtended; float LineSinAlpha = sqrt( 0.5 - 0.5 * LineCosTwoAlpha ); float Line_a2 = New_a2( Sphere_a2, LineSinAlpha, VoH ); Energy *= Sphere_a2 / max(Line_a2, 1e-5); #endif } return Energy; } float3 SpecularGGX(float Roughness, float Anisotropy, float3 SpecularColor, BxDFContext Context, float NoL, FAreaLight AreaLight) { float Alpha = Roughness * Roughness; float a2 = Alpha * Alpha; FAreaLight Punctual = AreaLight; Punctual.SphereSinAlpha = 0; Punctual.SphereSinAlphaSoft = 0; Punctual.LineCosSubtended = 1; Punctual.Rect = (FRect)0; Punctual.IsRectAndDiffuseMicroReflWeight = 0; float Energy = EnergyNormalization(a2, Context.VoH, Punctual); float ax = 0; float ay = 0; GetAnisotropicRoughness(Alpha, Anisotropy, ax, ay); // Generalized microfacet specular float3 D = D_GGXaniso(ax, ay, Context.NoH, Context.XoH, Context.YoH) * Energy; float3 Vis = Vis_SmithJointAniso(ax, ay, Context.NoV, NoL, Context.XoV, Context.XoL, Context.YoV, Context.YoL); float3 F = F_Schlick( SpecularColor, Context.VoH ); return (D * Vis) * F; } float3 SpecularGGX( float Roughness, float3 SpecularColor, BxDFContext Context, half NoL, FAreaLight AreaLight ) { float a2 = Pow4( Roughness ); float Energy = EnergyNormalization( a2, Context.VoH, AreaLight ); // Generalized microfacet specular float D = D_GGX( a2, Context.NoH ) * Energy; float Vis = Vis_SmithJointApprox( a2, Context.NoV, NoL ); float3 F = F_Schlick( SpecularColor, Context.VoH ); return (D * Vis) * F; } half3 DualSpecularGGX(half AverageRoughness, half Lobe0Roughness, half Lobe1Roughness, half LobeMix, half3 SpecularColor, BxDFContext Context, half NoL, FAreaLight AreaLight) { float AverageAlpha2 = Pow4(AverageRoughness); float Lobe0Alpha2 = Pow4(Lobe0Roughness); float Lobe1Alpha2 = Pow4(Lobe1Roughness); float Lobe0Energy = EnergyNormalization(Lobe0Alpha2, Context.VoH, AreaLight); float Lobe1Energy = EnergyNormalization(Lobe1Alpha2, Context.VoH, AreaLight); // Generalized microfacet specular float D = lerp(D_GGX(Lobe0Alpha2, Context.NoH) * Lobe0Energy, D_GGX(Lobe1Alpha2, Context.NoH) * Lobe1Energy, LobeMix); float Vis = Vis_SmithJointApprox(AverageAlpha2, Context.NoV, NoL); // Average visibility well approximates using two separate ones (one per lobe). float3 F = F_Schlick(SpecularColor, Context.VoH); return (D * Vis) * F; } FDirectLighting DefaultLitBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, half NoL, FAreaLight AreaLight, FShadowTerms Shadow ) { BxDFContext Context; FDirectLighting Lighting; Lighting.Diffuse = 0; Lighting.Specular = 0; Lighting.Transmission = 0; BRANCH if (NoL > 0.0f) { #if SUPPORTS_ANISOTROPIC_MATERIALS bool bHasAnisotropy = HasAnisotropy(GBuffer.SelectiveOutputMask); #else bool bHasAnisotropy = false; #endif float NoV, VoH, NoH; BRANCH if (bHasAnisotropy) { half3 X = GBuffer.WorldTangent; half3 Y = normalize(cross(N, X)); Init(Context, N, X, Y, V, L); NoV = Context.NoV; VoH = Context.VoH; NoH = Context.NoH; } else { #if SHADING_PATH_MOBILE InitMobile(Context, N, V, L, NoL); #else Init(Context, N, V, L); #endif NoV = Context.NoV; VoH = Context.VoH; NoH = Context.NoH; SphereMaxNoH(Context, AreaLight.SphereSinAlpha, true); } Context.NoV = saturate(abs( Context.NoV ) + 1e-5); #if MATERIAL_ROUGHDIFFUSE // Chan diffuse model with roughness == specular roughness. This is not necessarily a good modelisation of reality because when the mean free path is super small, the diffuse can in fact looks rougher. But this is a start. // Also we cannot use the morphed context maximising NoH as this is causing visual artefact when interpolating rough/smooth diffuse response. Lighting.Diffuse = Diffuse_Chan(GBuffer.DiffuseColor, Pow4(GBuffer.Roughness), NoV, NoL, VoH, NoH, GetAreaLightDiffuseMicroReflWeight(AreaLight)); #else Lighting.Diffuse = Diffuse_Lambert(GBuffer.DiffuseColor); #endif Lighting.Diffuse *= AreaLight.FalloffColor * (Falloff * NoL); BRANCH if (bHasAnisotropy) { //Lighting.Specular = GBuffer.WorldTangent * .5f + .5f; Lighting.Specular = AreaLight.FalloffColor * (Falloff * NoL) * SpecularGGX(GBuffer.Roughness, GBuffer.Anisotropy, GBuffer.SpecularColor, Context, NoL, AreaLight); } else { if( IsRectLight(AreaLight) ) { Lighting.Specular = RectGGXApproxLTC(GBuffer.Roughness, GBuffer.SpecularColor, N, V, AreaLight.Rect, AreaLight.Texture); } else { Lighting.Specular = AreaLight.FalloffColor * (Falloff * NoL) * SpecularGGX(GBuffer.Roughness, GBuffer.SpecularColor, Context, NoL, AreaLight); } } FBxDFEnergyTerms EnergyTerms = ComputeGGXSpecEnergyTerms(GBuffer.Roughness, Context.NoV, GBuffer.SpecularColor); // Add energy presevation (i.e. attenuation of the specular layer onto the diffuse component Lighting.Diffuse *= ComputeEnergyPreservation(EnergyTerms); // Add specular microfacet multiple scattering term (energy-conservation) Lighting.Specular *= ComputeEnergyConservation(EnergyTerms); Lighting.Transmission = 0; } return Lighting; } // Simple default lit model used by Lumen float3 SimpleShading( float3 DiffuseColor, float3 SpecularColor, float Roughness, float3 L, float3 V, half3 N ) { const float NoV = saturate(dot(N, V)); const FBxDFEnergyTerms EnergyTerms = ComputeGGXSpecEnergyTerms(Roughness, NoV, SpecularColor); float3 H = normalize(V + L); float NoH = saturate( dot(N, H) ); // Generalized microfacet specular float D = D_GGX( Pow4(Roughness), NoH ); float Vis = Vis_Implicit(); float3 F = F_None( SpecularColor ); return Diffuse_Lambert( DiffuseColor ) * ComputeEnergyPreservation(EnergyTerms) + (D * Vis) * F * ComputeEnergyConservation(EnergyTerms); } float RefractBlend(float VoH, float Eta) { // Refraction blend factor for normal component of VoH float k = 1.0 - Eta * Eta * (1.0 - VoH * VoH); return Eta * VoH - sqrt(k); } half RefractBlendClearCoatApprox(half VoH) { // Polynomial approximation of refraction blend factor for normal component of VoH with fixed Eta (1/1.5): return (0.63 - 0.22 * VoH) * VoH - 0.745; } float3 Refract(float3 V, float3 H, float Eta) { // Assumes V points away from the point of incidence float VoH = dot(V, H); return RefractBlend(VoH, Eta) * H - Eta * V; } BxDFContext RefractClearCoatContext(BxDFContext Context) { // Reference: Propagation of refraction through dot-product NoV // Note: This version of Refract requires V to point away from the point of incidence // NoV2 = -dot(N, Refract(V, H, Eta)) // NoV2 = -dot(N, RefractBlend(VoH, Eta) * H - Eta * V) // NoV2 = -(RefractBlend(VoH, Eta) * NoH - Eta * NoV) // NoV2 = Eta * NoV - RefractBlend(VoH, Eta) * NoH // NoV2 = 1.0 / 1.5 * NoV - RefractBlendClearCoatApprox(VoH) * NoH BxDFContext RefractedContext = Context; half Eta = 1.0 / 1.5; half RefractionBlendFactor = RefractBlendClearCoatApprox(Context.VoH); half RefractionProjectionTerm = RefractionBlendFactor * Context.NoH; RefractedContext.NoV = clamp(Eta * Context.NoV - RefractionProjectionTerm, 0.001, 1.0); // Due to CalcThinTransmission and Vis_SmithJointAniso, we need to make sure RefractedContext.NoL = clamp(Eta * Context.NoL - RefractionProjectionTerm, 0.001, 1.0); // those values are not 0s to avoid NaNs. RefractedContext.VoH = saturate(Eta * Context.VoH - RefractionBlendFactor); RefractedContext.VoL = 2.0 * RefractedContext.VoH * RefractedContext.VoH - 1.0; RefractedContext.NoH = Context.NoH; return RefractedContext; } FDirectLighting ClearCoatBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, half NoL, FAreaLight AreaLight, FShadowTerms Shadow ) { const float ClearCoat = GBuffer.CustomData.x; const float ClearCoatRoughness = max(GBuffer.CustomData.y, 0.02f); FDirectLighting Lighting = { float3(0.0, 0.0, 0.0), float3(0.0, 0.0, 0.0), float3(0.0, 0.0, 0.0) }; BxDFContext Context; half3 Nspec = N; if (CLEAR_COAT_BOTTOM_NORMAL) { Nspec = GBuffer.WorldNormal; } #if SUPPORTS_ANISOTROPIC_MATERIALS bool bHasAnisotropy = HasAnisotropy(GBuffer.SelectiveOutputMask); #else bool bHasAnisotropy = false; #endif half3 X = 0; half3 Y = 0; ////////////////////////////// /// Top Layer ////////////////////////////// // No anisotropy for the top layer Init(Context, Nspec, V, L); // Modify SphereSinAlpha, knowing that it was previously manipulated by roughness of the under coat // Note: the operation is not invertible for GBuffer.Roughness = 1.0, so roughness is clamped to 254.0/255.0 float SphereSinAlpha = AreaLight.SphereSinAlpha; float RoughnessCompensation = 1 - Pow2(GBuffer.Roughness); half Alpha = ClearCoatRoughness * ClearCoatRoughness; RoughnessCompensation = RoughnessCompensation > 0.0 ? (1 - Alpha) / RoughnessCompensation : 0.0; AreaLight.SphereSinAlpha = saturate(AreaLight.SphereSinAlpha * RoughnessCompensation); SphereMaxNoH(Context, AreaLight.SphereSinAlpha, CLEAR_COAT_BOTTOM_NORMAL == 0); Context.NoV = saturate(abs(Context.NoV) + 1e-5); const bool bIsRect = IsRectLight(AreaLight); Context.VoH = bIsRect ? Context.NoV : Context.VoH; // Hard-coded Fresnel evaluation with IOR = 1.5 (for polyurethane cited by Disney BRDF) float F0 = 0.04; float Fc = Pow5(1 - Context.VoH); float F = Fc + (1 - Fc) * F0; FBxDFEnergyTerms EnergyTermsCoat = ComputeGGXSpecEnergyTerms(ClearCoatRoughness, Context.NoV, F0); if (bIsRect) { Lighting.Specular = ClearCoat * RectGGXApproxLTC(ClearCoatRoughness, F0, Nspec, V, AreaLight.Rect, AreaLight.Texture); } else { // Generalized microfacet specular float a2 = Pow2(Alpha); float ClearCoatEnergy = EnergyNormalization(a2, Context.VoH, AreaLight); half Vis = Vis_SmithJointApprox(a2, Context.NoV, NoL); float D = D_GGX(a2, Context.NoH) * ClearCoatEnergy; half Fr1 = (D * Vis) * F; Lighting.Specular = ClearCoat * AreaLight.FalloffColor * (Falloff * NoL * Fr1); } Lighting.Specular *= ComputeEnergyConservation(EnergyTermsCoat); // Restore previously changed SphereSinAlpha for the top layer. // Alpha needs to also be restored to the bottom layer roughness. AreaLight.SphereSinAlpha = SphereSinAlpha; Alpha = Pow2(GBuffer.Roughness); // Incoming and exiting Fresnel terms are identical to incoming Fresnel term (VoH == HoL) // float FresnelCoeff = (1.0 - F1) * (1.0 - F2); #if USE_ENERGY_CONSERVATION float3 FresnelCoeff = ComputeEnergyPreservation(EnergyTermsCoat); // 1.0 - F; #else // Preserve old behavior when energy conservation is disabled half FresnelCoeff = 1.0 - F; #endif FresnelCoeff *= FresnelCoeff; ////////////////////////////// /// Bottom Layer ////////////////////////////// if (CLEAR_COAT_BOTTOM_NORMAL) { BxDFContext TempContext; BRANCH if (bHasAnisotropy) { Init(TempContext, N, X, Y, V, L); } else { Init(TempContext, Nspec, V, L); } // If bottom-normal, update normal-based dot products: float3 H = normalize(V + L); Context.NoH = saturate(dot(N, H)); Context.NoV = saturate(dot(N, V)); Context.NoL = saturate(dot(N, L)); Context.VoL = saturate(dot(V, L)); Context.VoH = saturate(dot(V, H)); Context.XoV = TempContext.XoV; Context.XoL = TempContext.XoL; Context.XoH = TempContext.XoH; Context.YoV = TempContext.YoV; Context.YoL = TempContext.YoL; Context.YoH = TempContext.YoH; if (!bHasAnisotropy) { bool bNewtonIteration = true; SphereMaxNoH(Context, AreaLight.SphereSinAlpha, bNewtonIteration); } Context.NoV = saturate(abs(Context.NoV) + 1e-5); } // Propagate refraction through dot-products rather than the original vectors: // Reference: // float Eta = 1.0 / 1.5; // float3 H = normalize(V + L); // float3 V2 = Refract(V, H, Eta); // float3 L2 = reflect(V2, H); // V2 = -V2; // BxDFContext BottomContext; // Init(BottomContext, N, X, Y, V2, L2); if (bHasAnisotropy) { // Prepare the anisotropic Context to refract. X = GBuffer.WorldTangent; Y = normalize(cross(N, X)); Init(Context, Nspec, X, Y, V, L); } BxDFContext BottomContext = RefractClearCoatContext(Context); BottomContext.VoH = bIsRect ? BottomContext.NoV : BottomContext.VoH; FBxDFEnergyTerms EnergyTermsBottom = ComputeGGXSpecEnergyTerms(GBuffer.Roughness, BottomContext.NoV, GBuffer.SpecularColor); // Absorption float3 Transmission = SimpleClearCoatTransmittance(BottomContext.NoL, BottomContext.NoV, GBuffer.Metallic, GBuffer.BaseColor); // Default Lit half3 DefaultDiffuse = (Falloff * NoL) * AreaLight.FalloffColor * Diffuse_Lambert(GBuffer.DiffuseColor) * ComputeEnergyPreservation(EnergyTermsBottom); half3 RefractedDiffuse = FresnelCoeff * Transmission * DefaultDiffuse; Lighting.Diffuse = lerp(DefaultDiffuse, RefractedDiffuse, ClearCoat); if (!bHasAnisotropy && bIsRect) { // Note: V is used instead of V2 because LTC integration is not tuned to handle refraction direction float3 DefaultSpecular = RectGGXApproxLTC(GBuffer.Roughness, GBuffer.SpecularColor, N, V, AreaLight.Rect, AreaLight.Texture); float3 RefractedSpecular = FresnelCoeff * Transmission * DefaultSpecular; Lighting.Specular += lerp(DefaultSpecular, RefractedSpecular, ClearCoat); } else { float a2 = Pow4(GBuffer.Roughness); half D2 = 0; half Vis2 = 0; // The energy factor for correcting area light shape needs to be computed "before" the actual D_GGX term, // as calling EnergyNormalization() modify the "a2" value. float Energy = 0; BRANCH if (bHasAnisotropy) { FAreaLight Punctual = AreaLight; Punctual.SphereSinAlpha = 0; Punctual.SphereSinAlphaSoft = 0; Punctual.LineCosSubtended = 1; Punctual.Rect = (FRect)0; Punctual.IsRectAndDiffuseMicroReflWeight = 0; Energy = EnergyNormalization(a2, Context.VoH, Punctual); } else { Energy = EnergyNormalization(a2, Context.VoH, AreaLight); } BRANCH if (bHasAnisotropy) { float ax = 0; float ay = 0; GetAnisotropicRoughness(Alpha, GBuffer.Anisotropy, ax, ay); D2 = D_GGXaniso(ax, ay, Context.NoH, Context.XoH, Context.YoH); Vis2 = Vis_SmithJointAniso(ax, ay, BottomContext.NoV, BottomContext.NoL, BottomContext.XoV, BottomContext.XoL, BottomContext.YoV, BottomContext.YoL); } else { // NoL is chosen to provide better parity with DefaultLit when ClearCoat=0 Vis2 = Vis_SmithJointApprox(a2, BottomContext.NoV, NoL); D2 = D_GGX(a2, BottomContext.NoH); } half3 F_Bot = F_Schlick(GBuffer.SpecularColor, BottomContext.VoH); half3 F_DefaultLit = F_Schlick(GBuffer.SpecularColor, Context.VoH); // Note: reusing D and V from refracted context to save computation when ClearCoat < 1 float3 CommonSpecular = (Energy * Falloff * NoL * D2 * Vis2) * AreaLight.FalloffColor; float3 DefaultSpecular = F_DefaultLit; float3 RefractedSpecular = FresnelCoeff * Transmission * F_Bot; Lighting.Specular += CommonSpecular * lerp(DefaultSpecular, RefractedSpecular, ClearCoat); } return Lighting; } // HG phase function approximated float ApproximateHG(float cosJ, float g) { float g2 = g * g; float gcos2 = 1.0f - (g * cosJ); gcos2 *= gcos2; const float ISO_PHASE_FUNC_Normalized = 0.5; return (ISO_PHASE_FUNC_Normalized * (1.0f - g2) / max( 1e-5, gcos2)); } void GetProfileDualSpecular(uint SubsurfaceProfileInt, half Roughness, half Opacity, out half LobeRoughness0, out half LobeRoughness1, out half LobeMix) { #if !FORWARD_SHADING GetSubsurfaceProfileDualSpecular(SubsurfaceProfileInt, Roughness, Opacity, LobeRoughness0, LobeRoughness1, LobeMix); #else // Disable dual lobe, as subsurface profile doesn't work with forward. LobeRoughness0 = Roughness; LobeRoughness1 = Roughness; LobeMix = 0.f; #endif } FDirectLighting SubsurfaceProfileBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, half NoL, FAreaLight AreaLight, FShadowTerms Shadow ) { BxDFContext Context; #if SHADING_PATH_MOBILE InitMobile(Context, N, V, L, NoL); #else Init( Context, N, V, L ); #endif SphereMaxNoH( Context, AreaLight.SphereSinAlpha, true ); Context.NoV = saturate( abs( Context.NoV ) + 1e-5 ); uint SubsurfaceProfileId = ExtractSubsurfaceProfileInt(GBuffer); half Opacity = GBuffer.CustomData.a; half Roughness = GBuffer.Roughness; half Lobe0Roughness = 0; half Lobe1Roughness = 0; half LobeMix = 0; GetProfileDualSpecular(SubsurfaceProfileId, Roughness, Opacity, Lobe0Roughness, Lobe1Roughness, LobeMix); half AverageRoughness = lerp(Lobe0Roughness, Lobe1Roughness, LobeMix); // Take the average roughness instead of compute a separate energy term for each roughness const FBxDFEnergyTerms EnergyTerms = ComputeGGXSpecEnergyTerms(AverageRoughness, Context.NoV, GBuffer.SpecularColor); FDirectLighting Lighting; #if SHADING_PATH_MOBILE half Curvature = GBuffer.Curvature; half UnClampedNoL = dot(GBuffer.WorldNormal, L); half ShadowFactor = 1.0f - sqrt(Shadow.SurfaceShadow); // Rotate the world normal based on the shadow value, it's just a experimental value half UnClampedRotatedNoL = max(UnClampedNoL - max(2.0f * UnClampedNoL, 0.4f) * ShadowFactor, -1.0f); half4 BurleyDiffuse = GetSSProfilePreIntegratedValue(SubsurfaceProfileId, UnClampedRotatedNoL, Curvature); // asset specific color half3 Tint = GetSubsurfaceProfileTexture(SSSS_TINT_SCALE_OFFSET, SubsurfaceProfileId).rgb; // Needs to apply shadow at here, since the preintegrated value has already counted it, and we need to skip applying shadow outside. Lighting.Diffuse = lerp(AreaLight.FalloffColor * (Falloff * NoL) * Shadow.SurfaceShadow, BurleyDiffuse.rgb, Tint) * Diffuse_Lambert(GBuffer.DiffuseColor); #else #if MATERIAL_ROUGHDIFFUSE // Use Chan's diffuse model. It reduces flatness look and has better match, e.g., at the edge of human face skin when compared to GT. const float3 DiffuseReflection = Diffuse_Chan(GBuffer.DiffuseColor, Pow4(GBuffer.Roughness), Context.NoV, NoL, Context.VoH, Context.NoH, GetAreaLightDiffuseMicroReflWeight(AreaLight)); #else const float3 DiffuseReflection = Diffuse_Burley(GBuffer.DiffuseColor, GBuffer.Roughness, Context.NoV, NoL, Context.VoH); #endif Lighting.Diffuse = AreaLight.FalloffColor * (Falloff * NoL) * DiffuseReflection; #endif if (IsRectLight(AreaLight)) { float3 Lobe0Specular = RectGGXApproxLTC(Lobe0Roughness, GBuffer.SpecularColor, N, V, AreaLight.Rect, AreaLight.Texture); float3 Lobe1Specular = RectGGXApproxLTC(Lobe1Roughness, GBuffer.SpecularColor, N, V, AreaLight.Rect, AreaLight.Texture); Lighting.Specular = lerp(Lobe0Specular, Lobe1Specular, LobeMix); } else { Lighting.Specular = AreaLight.FalloffColor * (Falloff * NoL) * DualSpecularGGX(AverageRoughness, Lobe0Roughness, Lobe1Roughness, LobeMix, GBuffer.SpecularColor, Context, NoL, AreaLight); } Lighting.Diffuse *= ComputeEnergyPreservation(EnergyTerms); Lighting.Specular *= ComputeEnergyConservation(EnergyTerms); #if USE_TRANSMISSION const uint ProfileId = ExtractSubsurfaceProfileInt(GBuffer); FTransmissionProfileParams TransmissionParams = GetTransmissionProfileParams(ProfileId); float Thickness = Shadow.TransmissionThickness; Thickness = DecodeThickness(Thickness); Thickness *= SSSS_MAX_TRANSMISSION_PROFILE_DISTANCE; float3 Profile = GetTransmissionProfile(ProfileId, Thickness).rgb; float3 RefracV = refract(V, -N, TransmissionParams.OneOverIOR); float PhaseFunction = ApproximateHG( dot(-L, RefracV), TransmissionParams.ScatteringDistribution ); Lighting.Transmission = AreaLight.FalloffColor * Profile * (Falloff * PhaseFunction); // TODO: This probably should also include cosine term (NoL) #else // USE_TRANSMISSION Lighting.Transmission = 0; #endif // USE_TRANSMISSION return Lighting; } FDirectLighting ClothBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, half NoL, FAreaLight AreaLight, FShadowTerms Shadow ) { const float3 FuzzColor = ExtractSubsurfaceColor(GBuffer); const float Cloth = saturate(GBuffer.CustomData.a); BxDFContext Context; #if SHADING_PATH_MOBILE InitMobile(Context, N, V, L, NoL); #else Init( Context, N, V, L ); #endif SphereMaxNoH( Context, AreaLight.SphereSinAlpha, true ); Context.NoV = saturate( abs( Context.NoV ) + 1e-5 ); float3 Spec1; if(IsRectLight(AreaLight)) Spec1 = RectGGXApproxLTC( GBuffer.Roughness, GBuffer.SpecularColor, N, V, AreaLight.Rect, AreaLight.Texture ); else Spec1 = AreaLight.FalloffColor * (Falloff * NoL) * SpecularGGX( GBuffer.Roughness, GBuffer.SpecularColor, Context, NoL, AreaLight ); const FBxDFEnergyTerms EnergyTerms1 = ComputeGGXSpecEnergyTerms(GBuffer.Roughness, Context.NoV, GBuffer.SpecularColor); Spec1 *= ComputeEnergyConservation(EnergyTerms1); // Cloth - Asperity Scattering - Inverse Beckmann Layer float D2 = D_InvGGX( Pow4( GBuffer.Roughness ), Context.NoH ); float Vis2 = Vis_Cloth( Context.NoV, NoL ); #if SHADING_PATH_MOBILE Vis2 = saturate(Vis2); #endif float3 F2 = F_Schlick( FuzzColor, Context.VoH ); float3 Spec2 = AreaLight.FalloffColor * (Falloff * NoL) * (D2 * Vis2) * F2; const FBxDFEnergyTermsA EnergyTerms2 = ComputeClothEnergyTermsA(GBuffer.Roughness, Context.NoV); Spec2 *= ComputeEnergyConservation(EnergyTerms2); FDirectLighting Lighting; Lighting.Diffuse = AreaLight.FalloffColor * (Falloff * NoL) * Diffuse_Lambert( GBuffer.DiffuseColor ); Lighting.Specular = lerp( Spec1, Spec2, Cloth ); Lighting.Transmission = 0; Lighting.Diffuse *= lerp(ComputeEnergyPreservation(EnergyTerms1), ComputeEnergyPreservation(EnergyTerms2), Cloth); return Lighting; } FDirectLighting SubsurfaceBxDF(FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, half NoL, FAreaLight AreaLight, FShadowTerms Shadow ) { FDirectLighting Lighting = DefaultLitBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow); half3 SubsurfaceColor = ExtractSubsurfaceColor(GBuffer); half Opacity = GBuffer.CustomData.a; // to get an effect when you see through the material // hard coded pow constant half InScatter = pow(saturate(dot(L, -V)), 12) * lerp(3, .1f, Opacity); // Wrap around lighting, // * /(PI*2) to be energy consistent (hack do get some view dependnt and light dependent effect) // * Opacity of 0 gives no normal dependent lighting, Opacity of 1 gives strong normal contribution // * Simplified version (with w=.5, n=1.5): // half WrappedDiffuse = 2 * pow(saturate((dot(N, L) + w) / (1.0f + w)), n) * (n + 1) / (2 * (1 + w)); // NormalContribution = WrappedDiffuse * Opacity + 1 - Opacity; const half WrappedDiffuse = pow(saturate(dot(N, L) * (1.f / 1.5f) + (0.5f / 1.5f)), 1.5f) * (2.5f / 1.5f); const half NormalContribution = lerp(1.f, WrappedDiffuse, Opacity); const half BackScatter = GBuffer.GBufferAO * NormalContribution / (PI * 2); // Transmission // * Emulate Beer-Lambert absorption by retrieving extinction coefficient from SubSurfaceColor. Subsurface is interpreted as a 'transmittance color' // at a certain 'normalized' distance (SubSurfaceColorAsTransmittanceAtDistanceInMeters). This is a coarse approximation for getting hue-shiting. // * TransmittedColor is computed for the 1-normalized distance, and then transformed back-and-forth in HSV space to preserving the luminance value // of the original color, but getting hue shifting const half3 ExtinctionCoefficients = TransmittanceToExtinction(SubsurfaceColor, View.SubSurfaceColorAsTransmittanceAtDistanceInMeters); const half3 RawTransmittedColor = ExtinctionToTransmittance(ExtinctionCoefficients, 1.0f /*At 1 meters, as we use normalized units*/); const half3 TransmittedColor = HSV_2_LinearRGB(half3(LinearRGB_2_HSV(RawTransmittedColor).xy, LinearRGB_2_HSV(SubsurfaceColor).z)); // lerp to never exceed 1 (energy conserving) Lighting.Transmission = AreaLight.FalloffColor * (Falloff * lerp(BackScatter, 1, InScatter)) * lerp(TransmittedColor, SubsurfaceColor, Shadow.TransmissionThickness); return Lighting; } FDirectLighting TwoSidedBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, half NoL, FAreaLight AreaLight, FShadowTerms Shadow ) { FDirectLighting Lighting = DefaultLitBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow ); half3 SubsurfaceColor = ExtractSubsurfaceColor(GBuffer); // http://blog.stevemcauley.com/2011/12/03/energy-conserving-wrapped-diffuse/ half Wrap = 0.5; half WrapNoL = saturate( ( -dot(N, L) + Wrap ) / Square( 1 + Wrap ) ); // Scatter distribution half VoL = dot(V, L); float Scatter = D_GGX( 0.6*0.6, saturate( -VoL ) ); Lighting.Transmission = AreaLight.FalloffColor * (Falloff * WrapNoL * Scatter) * SubsurfaceColor; return Lighting; } FDirectLighting EyeBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, half NoL, FAreaLight AreaLight, FShadowTerms Shadow ) { #if IRIS_NORMAL const float2 CausticNormalDelta = float2( GBuffer.StoredMetallic, GBuffer.StoredSpecular ) * 2 - (256.0/255.0); const float2 IrisNormalDelta = float2( GBuffer.CustomData.y, GBuffer.CustomData.z ) * 2 - (256.0/255.0); const float IrisMask = 1.0f - GBuffer.CustomData.w; const float2 WorldNormalOct = UnitVectorToOctahedron( GBuffer.WorldNormal ); const float3 CausticNormal = OctahedronToUnitVector( WorldNormalOct + CausticNormalDelta ); const float3 IrisNormal = OctahedronToUnitVector( WorldNormalOct + IrisNormalDelta ); #else const float3 IrisNormal = OctahedronToUnitVector( GBuffer.CustomData.yz * 2 - 1 ); const float IrisDistance = GBuffer.StoredMetallic; const float IrisMask = 1.0f - GBuffer.CustomData.w; // Blend in the negative intersection normal to create some concavity // Not great as it ties the concavity to the convexity of the cornea surface // No good justification for that. On the other hand, if we're just looking to // introduce some concavity, this does the job. const float3 CausticNormal = normalize(lerp(IrisNormal, -N, IrisMask*IrisDistance)); #endif BxDFContext Context; #if SHADING_PATH_MOBILE InitMobile(Context, N, V, L, NoL); #else Init( Context, N, V, L ); #endif SphereMaxNoH( Context, AreaLight.SphereSinAlpha, false ); Context.NoV = saturate( abs( Context.NoV ) + 1e-5 ); const bool bIsRect = IsRectLight(AreaLight); Context.VoH = bIsRect ? Context.NoV : Context.VoH; // F_Schlick float F0 = GBuffer.Specular * 0.08; float Fc = Pow5( 1 - Context.VoH ); float F = Fc + (1 - Fc) * F0; const FBxDFEnergyTerms EnergyTerms = ComputeGGXSpecEnergyTerms(GBuffer.Roughness, Context.NoV, F0); FDirectLighting Lighting; if( bIsRect ) { Lighting.Specular = RectGGXApproxLTC( GBuffer.Roughness, F0, N, V, AreaLight.Rect, AreaLight.Texture ); } else { float a2 = Pow4( GBuffer.Roughness ); float Energy = EnergyNormalization( a2, Context.VoH, AreaLight ); // Generalized microfacet specular float Vis = Vis_SmithJointApprox(a2, Context.NoV, NoL); float D = D_GGX(a2, Context.NoH) * Energy; Lighting.Specular = AreaLight.FalloffColor * (Falloff * NoL) * D * Vis * F; } float IrisNoL = saturate( dot( IrisNormal, L ) ); float Power = lerp( 12, 1, IrisNoL ); float Caustic = 0.8 + 0.2 * ( Power + 1 ) * pow( saturate( dot( CausticNormal, L ) ), Power ); float Iris = IrisNoL * Caustic; float Sclera = NoL; Lighting.Specular *= ComputeEnergyConservation(EnergyTerms); #if USE_ENERGY_CONSERVATION const float3 EnergyPreservation = ComputeEnergyPreservation(EnergyTerms); #else // Preserve old behavior when energy conservation is disabled const float EnergyPreservation = 1.0f - F; #endif Lighting.Diffuse = 0; Lighting.Transmission = AreaLight.FalloffColor * ( Falloff * lerp( Sclera, Iris, IrisMask ) * EnergyPreservation ) * Diffuse_Lambert( GBuffer.DiffuseColor ); #if SHADING_PATH_MOBILE uint SubsurfaceProfileId = ExtractSubsurfaceProfileInt(GBuffer); half Curvature = GBuffer.Curvature; half ShadowFactor = 1.0f - sqrt(Shadow.SurfaceShadow); // Rotate the world normal based on the shadow value, it's just a experimental value half UnClampedNoL = dot(GBuffer.WorldNormal, L); half UnClampedRotatedNoL = max(UnClampedNoL - max(2.0f * UnClampedNoL, 0.4f) * ShadowFactor, -1.0f); half4 BurleyDiffuse = GetSSProfilePreIntegratedValue(SubsurfaceProfileId, UnClampedRotatedNoL, Curvature); // Rotate the world normal based on the shadow value, it's just a experimental value half UnClampedIrisNoL = dot(IrisNormal, L); half UnClampedRotatedIrisNoL = max(UnClampedIrisNoL - max(2.0f * UnClampedIrisNoL, 0.4f) * ShadowFactor, -1.0f); half4 IrisBurleyDiffuse = GetSSProfilePreIntegratedValue(SubsurfaceProfileId, UnClampedRotatedIrisNoL * Caustic, Curvature); // asset specific color half3 Tint = GetSubsurfaceProfileTexture(SSSS_TINT_SCALE_OFFSET, SubsurfaceProfileId).rgb; // Needs to apply shadow at here, since the preintegrated value has already counted it, and we need to skip applying shadow outside. Lighting.Transmission = lerp(Lighting.Transmission * Shadow.SurfaceShadow, Falloff * lerp(BurleyDiffuse.rgb, IrisBurleyDiffuse.rgb, IrisMask) * Diffuse_Lambert(GBuffer.DiffuseColor), Tint); #endif return Lighting; } FDirectLighting PreintegratedSkinBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, half NoL, FAreaLight AreaLight, FShadowTerms Shadow ) { FDirectLighting Lighting = DefaultLitBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow ); half3 SubsurfaceColor = ExtractSubsurfaceColor(GBuffer); half Opacity = GBuffer.CustomData.a; half3 PreintegratedBRDF = Texture2DSampleLevel(View.PreIntegratedBRDF, View.PreIntegratedBRDFSampler, float2(saturate(dot(N, L) * .5 + .5), 1 - Opacity), 0).rgb; Lighting.Transmission = AreaLight.FalloffColor * Falloff * PreintegratedBRDF * SubsurfaceColor; return Lighting; } FDirectLighting IntegrateBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, half NoL, FAreaLight AreaLight, FShadowTerms Shadow ) { switch( GBuffer.ShadingModelID ) { case SHADINGMODELID_DEFAULT_LIT: case SHADINGMODELID_SINGLELAYERWATER: case SHADINGMODELID_THIN_TRANSLUCENT: return DefaultLitBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow ); case SHADINGMODELID_SUBSURFACE: return SubsurfaceBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow ); case SHADINGMODELID_PREINTEGRATED_SKIN: return PreintegratedSkinBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow ); case SHADINGMODELID_CLEAR_COAT: return ClearCoatBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow ); case SHADINGMODELID_SUBSURFACE_PROFILE: return SubsurfaceProfileBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow ); case SHADINGMODELID_TWOSIDED_FOLIAGE: return TwoSidedBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow ); case SHADINGMODELID_HAIR: return HairBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow ); case SHADINGMODELID_CLOTH: return ClothBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow ); case SHADINGMODELID_EYE: return EyeBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow ); default: return (FDirectLighting)0; } } FDirectLighting EvaluateBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float NoL, FShadowTerms Shadow ) { FAreaLight AreaLight; AreaLight.SphereSinAlpha = 0; AreaLight.SphereSinAlphaSoft = 0; AreaLight.LineCosSubtended = 1; AreaLight.FalloffColor = 1; AreaLight.Rect = (FRect)0; AreaLight.IsRectAndDiffuseMicroReflWeight = 0; AreaLight.Texture = InitRectTexture(); return IntegrateBxDF( GBuffer, N, V, L, 1, NoL, AreaLight, Shadow ); }