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

142 lines
5.0 KiB
HLSL

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Common.ush"
#include "BRDF.ush"
#include "ShadingCommon.ush"
#include "/Engine/Shared/PathTracingDefinitions.h"
// Move these utility functions into a common/non-path-tracer specific place
#include "PathTracing/Material/PathTracingGlossy.ush"
#include "PathTracing/Material/PathTracingFresnel.ush"
#include "PathTracing/Utilities/PathTracingRandomSequence.ush"
// This shader computes the directional albedo (basically a furnace test) of a given BxDF to determine how much energy is "missing"
// from a given viewing direction and roughness. These tables can later be used to compensate for the missing energy during shading.
// "Energy" here refers to the directional albedo. Energy loss is simply 1-Energy since we assume the raw BxDF should reflect all
// incoming light.
// Number of samples used for integrating directional albedo
uint NumSamples;
uint EnergyTableResolution;
// x: directional albedo for rough diffuse
RWTexture2D<float> Output1Texture2D;
// x: directional albedo of pure GGX lobe
// y: directional albedo of GGX+Schlick fresnel term
RWTexture2D<float2> Output2Texture2D;
// x: directional albedo from outside->in, y: directional albedo from inside->out
RWTexture3D<float2> OutputTexture3D;
[numthreads(THREADGROUPSIZE_X, THREADGROUPSIZE_Y, 1)]
void BuildEnergyTableCS(uint3 DispatchThreadId : SV_DispatchThreadID)
{
const float3 Index = (float3(DispatchThreadId) + 0.5) / float(EnergyTableResolution);
// Map from thread ID to the exact roughness/viewing angle we want to compute
const float NoV = max(Index.x, 1e-8);
const float Roughness = Index.y;
#if BUILD_ENERGY_TABLE == 1
const float EtaIn = 1.0 + 2.0 * Index.z; // only handle Ior in [1,3] range
const float EtaOut = rcp(EtaIn);
// NOTE: Baking a table for all possible F0 values would greatly complicate things
// Instead, we aim for optimal results when the Ior values is consistent with F0.
// In practice, it appears that this gives very good results, and the penalty when
// Ior and F0 are mismatched does not appear to be that large.
const float F0 = DielectricIorToF0(EtaIn); // NOTE: this is always equal to DielectricIorToF0(EtaOut)
#endif
float2 Alpha; GetGGXBasis(Roughness, float3(0, 0, 1), Alpha);
float3 V = float3(sqrt(1 - NoV * NoV), 0, NoV);
RandomSequence RandSequence;
float2 Result = 0;
// use plenty of samples -- this is computed on startup once and at fairly low resolution
LOOP
for (int Sample = 0; Sample < NumSamples; Sample++)
{
// draw a random sample
RandomSequence_Initialize(RandSequence, DispatchThreadId.xy, Sample, DispatchThreadId.z, NumSamples);
float3 RandSample = RandomSequence_GenerateSample3D(RandSequence);
float2 Weight = 0;
#if BUILD_ENERGY_TABLE == 0
const float3 H = ImportanceSampleVisibleGGX(RandSample.xy, Alpha, V).xyz;
const float3 L = reflect(-V, H);
if (L.z > 0)
{
const float2 GGXResult = GGXEvalReflection(L, V, H, Alpha);
Weight = float2(GGXResult.x, GGXResult.x * Pow5(1 - dot(V, H)));
}
#elif BUILD_ENERGY_TABLE == 1
const float3 H = ImportanceSampleVisibleGGX(RandSample.xy, Alpha, V, false).xyz;
{
// compute weight for outside -> in case
float3 L = 0;
float F = 0;
if (SampleRefraction(-V, H, EtaIn, F0, RandSample.z, L, F))
{
Weight.x = GGXEvalRefraction(L, V, H, Alpha, EtaIn).x;
}
else
{
Weight.x = GGXEvalReflection(L, V, H, Alpha, false).x;
}
}
{
// compute weight for inside -> out case
float3 L = 0;
float F = 0;
if (SampleRefraction(-V, H, EtaOut, F0, RandSample.z, L, F))
{
Weight.y = GGXEvalRefraction(L, V, H, Alpha, EtaOut).x;
}
else
{
Weight.y = GGXEvalReflection(L, V, H, Alpha, false).x;
}
}
#elif BUILD_ENERGY_TABLE == 2
const float3 L = CosineSampleHemisphere(RandSample.xy).xyz;
const float NoV = V.z;
const float NoL = L.z;
const float3 H = normalize(V + L);
const float NoH = H.z;
#if SUBSTRATE_ENABLED
const float DCloth = D_Charlie(Roughness, NoH);
const float VCloth = Vis_Ashikhmin(NoV, NoL);
#else
const float DCloth = D_InvGGX(Alpha.x * Alpha.y, NoH);
const float VCloth = Vis_Cloth(NoV, NoL);
#endif
const float ClothWeight = NoL > 0 && NoV > 0 ? (PI * DCloth * VCloth) : 0.0;
Weight = float2(ClothWeight, ClothWeight * Pow5(1 - dot(V, H)));
#elif BUILD_ENERGY_TABLE == 3
const float3 L = CosineSampleHemisphere(RandSample.xy).xyz;
const float NoV = V.z;
const float NoL = L.z;
const float3 H = normalize(V + L);
const float VoH = dot(V, H);
const float NoH = H.z;
const float DiffuseWeight = NoL > 0 && NoV > 0 ? PI * Diffuse_Chan(float3(1, 1, 1), Alpha.x, NoV, NoL, VoH, NoH, 1.0f).x : 0.0f;
Weight = float2(DiffuseWeight, 1.f);
#endif
// average in
Result = lerp(Result, Weight, 1.0 / (Sample + 1));
}
#if BUILD_ENERGY_TABLE == 0 || BUILD_ENERGY_TABLE == 2
Output2Texture2D[DispatchThreadId.xy] = Result;
#endif
#if BUILD_ENERGY_TABLE == 1
OutputTexture3D[DispatchThreadId] = Result;
#endif
#if BUILD_ENERGY_TABLE == 3
Output1Texture2D[DispatchThreadId.xy] = Result.x;
#endif
}