Files
UnrealEngine/Engine/Shaders/Private/ShadingEnergyConservation.ush
2025-05-18 13:04:45 +08:00

168 lines
7.6 KiB
HLSL

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
// If EnergyConservation flag is not enabled explicitly then:
// * If Substrate is enabled: ON by default
// * If Substrate is disabled: OFF by default
#if SUBSTRATE_ENABLED || LEGACY_MATERIAL_ENERGYCONSERVATION
#undef USE_ENERGY_CONSERVATION
#if SUPPORTS_INDEPENDENT_SAMPLERS
#define USE_ENERGY_CONSERVATION 1
#else
// Platforms that do not support shared sampler will in fact quickly reach the limit of samplers in material pixel shaders.
// This is mostly true for OpenGLES. In this case, we enforce disabling energy conservation to be able to compile substrate materials.
#define USE_ENERGY_CONSERVATION 0
#endif
#else
#undef USE_ENERGY_CONSERVATION
#define USE_ENERGY_CONSERVATION 0
#endif
#if SUPPORTS_INDEPENDENT_SAMPLERS
#define SharedShadingEnergySampler View.SharedBilinearClampedSampler
#else
#define SharedShadingEnergySampler View.ShadingEnergySampler
#endif
// Energy compensation/preservation for the various BxDFs. This file provides utility functions to tweak lobe weights
//
// References:
// [1] "Revisiting physically based shading at Imageworks" - Christopher Kulla & Alejandro Conty
// https://blog.selfshadow.com/publications/s2017-shading-course/imageworks/s2017_pbs_imageworks_slides_v2.pdf
// [2] "Practical multiple scattering compensation for microfacet models" - Emmanuel Turquin
// https://blog.selfshadow.com/publications/turquin/ms_comp_final.pdf
// [3] "A Multiple-Scattering Microfacet Model for Real-Time Image Based Lighting" - Carmelo J. Fdez-Aguera
// http://jcgt.org/published/0008/01/03/
//
// We follow the terminology from [1]:
// Energy compensation: adjust individual BxDFs so that they have unit albedo from all viewing angles, compensating for (for example) the lack of multiple scattering in microfacet BxDFs
// Energy preservation: adjust the combination of lobes (such as diffuse+specular) to ensure we never create additional energy from any angle
// The suggested implementation in [1] is cumbersome for a GPU implementation because it requires table lookups during sampling to achieve low variance. We instead sacrifice reciprocity and
// simply divide the BxDF by its (tabulated/fit) directional albedo to enforce conservation of energy. We follow the approach outlined in [2] to account for spec/cloth and glass, and use
// the split-sum approach from [3] for the diffuse/specular energy preservation case (which generalizes to other lobes as well).
// USE_ENERGY_CONSERVATION controls the implementation details of this file
// 0: disable conservation, all bsdfs are assumed to already have unit albedo from all viewing angles
// 1: use a small baked texture to store the directional albedo (and fresnel weighted albedo)
// 2: use an analytic approximation to directional albedo (doesn't require any textures to be bound and can be faster -- WIP)
///////////////////////////////////////////////////////////////////////////////////////////////////
// LUT Lookups
float2 GGXEnergyLookup(float Roughness, float NoV)
{
#if USE_ENERGY_CONSERVATION == 1
return View.ShadingEnergyGGXSpecTexture.SampleLevel(SharedShadingEnergySampler, float2(NoV, Roughness), 0);
#elif USE_ENERGY_CONSERVATION == 2
const float r = Roughness;
const float c = NoV;
const float E = 1.0 - saturate(pow(r, c / r) * ((r * c + 0.0266916) / (0.466495 + c)));
const float Ef = Pow5(1 - c) * pow(2.36651 * pow(c, 4.7703 * r) + 0.0387332, r);
return float2(E, Ef);
#else
return float2(1, 0);
#endif
}
float GGXEnergyLookup(float Roughness, float NoV, float Eta)
{
// Following [2] Eq 18, we simply divide the whole bsdf by its single scattering albedo to conserve energy
// This break reciprocity, but is much simpler than the approach outlined in [1] which requires substantially more computation.
#if USE_ENERGY_CONSERVATION == 1
// NOTE: Eta is encoded for [1,3] range only, energy loss will happens above 3.0 - but this keeps more resolution on the visually significant portion of the range
float2 E = View.ShadingEnergyGGXGlassTexture.SampleLevel(SharedShadingEnergySampler, float3(NoV, Roughness, max(Eta, rcp(Eta)) * 0.5 - 0.5), 0);
return Eta >= 1.0 ? E.x : E.y;
#else
// TODO: find an analytic fit for this case
return 1.0;
#endif
}
float2 ClothEnergyLookup(float Roughness, float NoV)
{
#if USE_ENERGY_CONSERVATION == 1
return View.ShadingEnergyClothSpecTexture.SampleLevel(SharedShadingEnergySampler, float2(NoV, Roughness), 0);
#elif USE_ENERGY_CONSERVATION == 2
// Simple analytical fit (avoids building a table)
// NOTE: this fit is not exact near grazing angles for roughness ~0.7
const float c = NoV;
const float r = Roughness;
return float2(
(0.526422 / ((-0.227114 + r) * (-0.968835 + r) * ((5.38869 - 20.2835 * c) * r) - (-1.18761 - ((2.58744 - c) * c)))) + 0.0615456,
0.0 // TODO: is it worth capturing this? this term is close to 0 (and experiments with the tabulated form show it doesn't have much impact)
);
#else
return float2(1, 0);
#endif
}
float DiffuseEnergyLookup(float Roughness, float NoV)
{
#if USE_ENERGY_CONSERVATION == 1
//return View.ShadingEnergyDiffuseTexture.SampleLevel(SharedShadingEnergySampler, float2(NoV, Roughness), 0);
// For now we do not apply Chan diffuse energy preservation on diffuse ambiant.
// This is because Chan is built for F=0.04 and unfortunately this causes ambient to darken a grazing angles.
// SUBSTRATE_TODO Apply the inverse of Fresnel with F=0.04 on Chan when building the table.
return 1.0f;
#elif USE_ENERGY_CONSERVATION == 2
// TODO
return 1.f;
#else
return 1.f;
#endif
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// BxDFs Energy terms & Energy Preservation/Conservation utils
// Note:
// * For performance reason, we maintain both a *chromatic* and an *achromatic* version of the energy terms.
// * Chromatic and achromatic version are respectively accessible with xxxRGB or xxxA suffix.
// * USE_ACHROMATIC_BXDF_ENERGY determines which version should be used by default
// Chromatic version
#define BXDF_ENERGY_TYPE float3
#define BXDF_ENERGY_SUFFIX(N) N##RGB
#define IS_BXDF_ENERGY_TYPE_ACHROMATIC 0
#include "ShadingEnergyConservationTemplate.ush"
#undef BXDF_ENERGY_TYPE
#undef BXDF_ENERGY_TYPE_SUFFIX
#undef IS_BXDF_ENERGY_TYPE_ACHROMATIC
// Achromatic version
#define BXDF_ENERGY_TYPE float
#define BXDF_ENERGY_SUFFIX(N) N##A
#define IS_BXDF_ENERGY_TYPE_ACHROMATIC 1
#include "ShadingEnergyConservationTemplate.ush"
#undef BXDF_ENERGY_TYPE
#undef BXDF_ENERGY_TYPE_SUFFIX
#undef IS_BXDF_ENERGY_TYPE_ACHROMATIC
///////////////////////////////////////////////////////////////////////////////////////////////////
// Default implementation
#ifndef USE_ACHROMATIC_BXDF_ENERGY
#define USE_ACHROMATIC_BXDF_ENERGY 0
#endif
#if USE_ACHROMATIC_BXDF_ENERGY
#define FBxDFEnergyType float
#define FBxDFEnergyTerms FBxDFEnergyTermsA
#define ComputeFresnelEnergyTerms ComputeFresnelEnergyTermsA
#define ComputeGGXSpecEnergyTerms ComputeGGXSpecEnergyTermsA
#define ComputeGGXSpecEnergyTerms ComputeGGXSpecEnergyTermsA
#define ComputeClothEnergyTerms ComputeClothEnergyTermsA
#define ComputeClothEnergyTerms ComputeClothEnergyTermsA
#define ComputeDiffuseEnergyTerms ComputeDiffuseEnergyTermsA
#else
#define FBxDFEnergyType float3
#define FBxDFEnergyTerms FBxDFEnergyTermsRGB
#define ComputeFresnelEnergyTerms ComputeFresnelEnergyTermsRGB
#define ComputeGGXSpecEnergyTerms ComputeGGXSpecEnergyTermsRGB
#define ComputeGGXSpecEnergyTerms ComputeGGXSpecEnergyTermsRGB
#define ComputeClothEnergyTerms ComputeClothEnergyTermsRGB
#define ComputeClothEnergyTerms ComputeClothEnergyTermsRGB
#define ComputeDiffuseEnergyTerms ComputeDiffuseEnergyTermsRGB
#endif