Direct lighting diffuse/specular energy preservation and IBL consistency
Summary
Filament's BRDF documentation already identifies two related energy issues:
- Energy gain in diffuse reflectance
- Energy loss in specular reflectance
The second issue, specular energy loss, is already addressed in the standard shading model through the multiscattering specular energy compensation term:
pixel.energyCompensation = 1.0 + pixel.f0 * (1.0 / pixel.dfg.y - 1.0);
Fr *= pixel.energyCompensation;
This scales the single-scattering GGX specular lobe to compensate for energy lost by not explicitly simulating multiple microfacet scattering.
However, the first issue, energy gain in diffuse reflectance, is still unresolved for direct lighting. The standard shading model currently evaluates diffuse and specular independently and adds them:
vec3 Fr = specularLobe(pixel, light, h, NoV, NoL, NoH, LoH);
vec3 Fd = diffuseLobe(pixel, NoV, NoL, LoH);
// TODO: attenuate the diffuse lobe to avoid energy gain
vec3 color = Fd + Fr * pixel.energyCompensation;
The problem is that the specular lobe is compensated for multiscattering, but the diffuse lobe is still evaluated as if the full incident light reached the diffuse scattering event. In Filament's own terminology, the specular-side energy loss is compensated, but the diffuse-side energy gain is not removed.
This issue proposes exposing the effective specular directional albedo after multiscattering compensation:
vec3 compensatedSpecularDirectionalAlbedo(const PixelParams pixel) {
return saturate(specularDFG(pixel) * pixel.energyCompensation);
}
and using it to attenuate the diffuse lobe:
Fd *= 1.0 - compensatedSpecularDirectionalAlbedo(pixel);
This completes the diffuse-side counterpart of the existing specular energy compensation and makes direct lighting, prefiltered IBL, and importance-sampled IBL use the same energy accounting.
Background: compensation vs preservation
Filament's documentation describes two different problems:
1. Energy gain in diffuse reflectance
The documentation states that the Lambert diffuse BRDF does not account for light that reflects at the surface and therefore cannot participate in the diffuse scattering event.
In a layered interpretation of the standard material, incident light first interacts with the specular interface. Only the part not reflected by the specular layer should reach the diffuse substrate.
Conceptually:
incoming energy
= specular reflected energy
+ energy transmitted to diffuse scattering
Therefore, if the specular layer reflects an amount E, the diffuse lobe should receive approximately:
of the incident energy.
2. Energy loss in specular reflectance
The Cook-Torrance GGX BRDF used by Filament is a single-scattering microfacet model. At high roughness, light can bounce between microfacets before escaping. A single-scattering model discards these paths through masking and shadowing, causing rough specular materials, especially metals, to lose energy.
Filament already compensates this by scaling the single-scattering specular lobe:
pixel.energyCompensation = 1.0 + pixel.f0 * (1.0 / pixel.dfg.y - 1.0);
This can be written as:
$$
W(v, \alpha, f_0) = 1 + f_0 \left( \frac{1}{r(v,\alpha)} - 1 \right)
$$
where:
$$
r(v,\alpha) = \int_{\Omega} D(l,v,\alpha) V(l,v,\alpha) \langle n \cdot l \rangle dl
$$
In Filament's multiscattering DFG LUT, this term is stored in:
so:
$$
r(v,\alpha) = pixel.dfg.y
$$
The current implementation applies:
$$
f_r(l,v) = W(v,\alpha,f_0) f_{ss}(l,v)
$$
where f_ss is the original single-scattering specular lobe.
This part is correct and matches the documentation: it compensates the specular energy loss.
The missing part is that once the specular lobe has been compensated, the diffuse lobe should be reduced by the energy reflected by the compensated specular layer.
Directional albedo terms
To avoid ambiguity, this issue uses the following terms:
W: specular energy compensation weight
vec3 W = pixel.energyCompensation;
This is the multiplier applied to the single-scattering specular BRDF.
It is not itself the reflected specular energy. It is a lobe scale factor.
E_ss: single-scattering specular directional albedo
The directional albedo of a BRDF is the fraction of unit incident radiance reflected by that BRDF for a given view direction:
$$
E(v) = \int_{\Omega} f(l,v) \langle n \cdot l \rangle dl
$$
For the single-scattering specular lobe with Schlick Fresnel, Filament already reconstructs this from the DFG LUT:
vec3 specularDFG(const PixelParams pixel) {
#if defined(SHADING_MODEL_CLOTH)
return pixel.f0 * pixel.dfg.z;
#else
#if defined(MATERIAL_HAS_SPECULAR_COLOR_FACTOR) || defined(MATERIAL_HAS_SPECULAR_FACTOR)
return mix(pixel.dfg.xxx, pixel.dfg.yyy, pixel.f0) * pixel.specular;
#else
return mix(pixel.dfg.xxx, pixel.dfg.yyy, pixel.f0);
#endif
#endif
}
For the standard shading model, this corresponds to:
$$
E_{ss}(v,\alpha,f_0) = (1 - f_0) DFG_1(v,\alpha) + f_0 DFG_2(v,\alpha)
$$
or in shader terms:
vec3 E_ss = specularDFG(pixel);
In Filament's multiscattering DFG pre-integration:
dfg.x = DFG_1 = Fresnel Fc-weighted term
dfg.y = DFG_2 = non-Fresnel term, also used as r
and the reconstruction is:
mix(dfg.xxx, dfg.yyy, f0)
which is:
$$
(1 - f_0) DFG_1 + f_0 DFG_2
$$
Where specularDFG(pixel) comes from
specularDFG(pixel) is Filament's reconstruction of the Fresnel-weighted specular DFG term used by the split-sum approximation for specular image-based lighting.
The specular IBL integral is:
$$
L_{spec}(v) = \int_{\Omega} L_i(l) f_{spec}(l,v) \langle n \cdot l \rangle dl
$$
For the GGX Cook-Torrance specular BRDF, this is approximately:
$$
L_{spec}(v) = \int_{\Omega} L_i(l) D(l,v,\alpha) V(l,v,\alpha) F(v,h,f_0) \langle n \cdot l \rangle dl
$$
The split-sum approximation separates the filtered lighting term from the BRDF integration term:
$$
L_{spec}(v) \simeq LD(n,\alpha) \cdot DFG(\langle n \cdot v \rangle,\alpha,f_0)
$$
where LD is the prefiltered environment radiance and DFG is the pre-integrated BRDF factor. In a white furnace environment, LD is approximately one, so the DFG term itself represents the specular directional albedo of the lobe.
With Schlick Fresnel:
$$
F(v,h) = f_0 + (f_{90} - f_0) F_c(\langle v \cdot h \rangle)
$$
and:
$$
F_c(\langle v \cdot h \rangle) = (1 - \langle v \cdot h \rangle)^5
$$
Filament's multiscattering DFG reconstruction assumes:
$$
f_{90} = 1
$$
so:
$$
F(v,h) = f_0 + (1 - f_0)F_c
$$
or equivalently:
$$
F(v,h) = f_0 \cdot 1 + (1 - f_0) \cdot F_c
$$
Substituting this into the DFG integral gives:
$$
DFG(\langle n \cdot v \rangle,\alpha,f_0)
= \int_{\Omega} D V \left[f_0 + (1 - f_0)F_c\right] \langle n \cdot l \rangle dl
$$
After separating the terms that depend on f0:
$$
DFG(\langle n \cdot v \rangle,\alpha,f_0)
= f_0 \int_{\Omega} D V \langle n \cdot l \rangle dl + (1 - f_0) \int_{\Omega} D V F_c \langle n \cdot l \rangle dl
$$
Filament stores these two terms in the multiscattering DFG LUT as:
dfg.x = DFG_1 = integral(D * V * Fc * NoL)
dfg.y = DFG_2 = integral(D * V * NoL)
The DFG pre-integration code follows this layout:
float Fc = pow(1 - VoH, 5.0f);
r.x += Gv * Fc;
r.y += Gv;
The shader reconstruction is therefore:
mix(dfg.xxx, dfg.yyy, f0)
which expands to:
$$
(1 - f_0)DFG_1 + f_0DFG_2
$$
This is exactly what specularDFG(pixel) returns for the standard shading model:
vec3 E_ss = specularDFG(pixel);
This value is the Fresnel-weighted directional albedo of the single-scattering specular lobe. It is not yet the directional albedo of the lobe that is finally used for lighting, because direct lighting and IBL later multiply the specular lobe by:
Therefore:
corresponds to:
$$
E_{ss}
$$
while:
saturate(specularDFG(pixel) * pixel.energyCompensation)
corresponds to:
$$
E_{comp}
$$
This distinction is important: specularDFG(pixel) tells us how much energy the uncompensated single-scattering lobe reflects, while E_comp tells us how much energy the compensated lobe used by the renderer reflects.
E_comp: compensated specular directional albedo
The specular BRDF actually used for lighting is not f_ss; it is:
$$
f_r(l,v) = W(v,\alpha,f_0) f_{ss}(l,v)
$$
Therefore, the effective directional albedo of the specular lobe used by the renderer is approximately:
$$
E_{comp}(v,\alpha,f_0) = \mathrm{saturate} \left( W(v,\alpha,f_0) E_{ss}(v,\alpha,f_0) \right)
$$
or:
vec3 E_comp = saturate(specularDFG(pixel) * pixel.energyCompensation);
This is the amount of energy reflected by the compensated specular layer. The diffuse lobe should receive the remaining energy:
$$
1 - E_{comp}
$$
The saturate is important because the DFG reconstruction, multiscattering approximation, colored specular factors, and roughness remapping are approximate. Clamping prevents negative diffuse energy in edge cases.
Direct lighting
The current standard direct lighting path is effectively:
$$
L_{direct} \propto F_d + W F_{ss}
$$
or in code:
vec3 Fr = specularLobe(pixel, light, h, NoV, NoL, NoH, LoH);
vec3 Fd = diffuseLobe(pixel, NoV, NoL, LoH);
vec3 color = Fd + Fr * pixel.energyCompensation;
This compensates the specular lobe but still lets the diffuse lobe use the full incident energy. The result can over-preserve energy, especially for rough dielectric materials under strong direct lighting or grazing views.
The proposed direct lighting model is:
$$
L_{direct} \propto (1 - E_{comp}) F_d + W F_{ss}
$$
where:
$$
E_{comp} = \mathrm{saturate} \left( specularDFG(pixel) \cdot pixel.energyCompensation \right)
$$
In shader form:
vec3 compensatedSpecularDirectionalAlbedo(const PixelParams pixel) {
return saturate(specularDFG(pixel) * pixel.energyCompensation);
}
vec3 surfaceShading(const PixelParams pixel, const Light light, ...) {
vec3 Fr = specularLobe(pixel, light, h, NoV, NoL, NoH, LoH);
vec3 Fd = diffuseLobe(pixel, NoV, NoL, LoH);
vec3 E = compensatedSpecularDirectionalAlbedo(pixel);
Fd *= 1.0 - E;
return Fd + Fr * pixel.energyCompensation;
}
This resolves the existing TODO:
// TODO: attenuate the diffuse lobe to avoid energy gain
The important distinction is:
pixel.energyCompensation scales the specular lobe.
compensatedSpecularDirectionalAlbedo estimates how much energy that scaled lobe reflects.
1 - compensatedSpecularDirectionalAlbedo is the energy left for diffuse.
IBL consistency
The prefiltered cubemap IBL path already attenuates diffuse by a specular DFG term:
vec3 E = specularDFG(pixel);
Fr = E * prefilteredRadiance(...);
Fr *= pixel.energyCompensation;
Fd = pixel.diffuseColor * diffuseIrradiance * (1.0 - E) * diffuseBRDF;
However, this uses two different energy values:
specular IBL contribution uses E * pixel.energyCompensation
diffuse IBL attenuation uses 1 - E
For consistency, diffuse attenuation should use the same effective specular directional albedo as the specular lobe:
vec3 E = specularDFG(pixel);
vec3 E_comp = saturate(E * pixel.energyCompensation);
Fr = E * prefilteredRadiance(...);
Fr *= pixel.energyCompensation;
Fd = pixel.diffuseColor * diffuseIrradiance * (1.0 - E_comp) * diffuseBRDF;
Equivalently:
$$
L_{specular}^{IBL} \propto E_{comp}
$$
and:
$$
L_{diffuse}^{IBL} \propto 1 - E_{comp}
$$
This makes the IBL path use the same energy accounting as direct lighting.
Importance-sampled IBL
The importance-sampling IBL path currently has:
vec3 E = vec3(0.0); // TODO: fix for importance sampling
Even when the specular radiance is evaluated through importance sampling rather than a prefiltered cubemap, the DFG terms are still available through pixel.dfg.
Therefore, the diffuse attenuation can still use:
vec3 E = compensatedSpecularDirectionalAlbedo(pixel);
instead of leaving E = 0.
This keeps the energy accounting consistent across:
direct lighting
prefiltered cubemap IBL
importance-sampled IBL
White furnace intuition
Under a white furnace environment, ignoring AO, clear coat, sheen, transmission, subsurface, and other secondary effects, an energy-preserving dielectric material should not return more than the incident unit radiance.
Without diffuse attenuation, the simplified accounting is:
$$
L \approx 1 + E_{comp}
$$
because the diffuse lobe still receives full energy while the specular lobe also reflects its compensated energy.
With the proposed attenuation:
$$
L \approx (1 - E_{comp}) + E_{comp} = 1
$$
This is the expected behavior: the compensated specular layer reflects E_comp, and only the remaining energy reaches the diffuse scattering event.
RGB vs scalar attenuation
The smallest change consistent with Filament's current IBL implementation is vector attenuation:
This matches the existing IBL pattern:
and preserves channel-wise energy accounting for colored specular materials.
A scalar attenuation is also possible:
float e = luminance(E_comp);
Fd *= 1.0 - e;
or:
float e = max3(E_comp);
Fd *= 1.0 - e;
A scalar form can reduce hue shifts for colored specular materials because the diffuse substrate is attenuated uniformly. However, it is a larger behavioral change relative to Filament's current vector IBL attenuation.
This issue proposes the vector form as the smallest internally consistent change, but the choice is worth calling out explicitly.
Implementation outline
The implementation can be very small because all required data is already available in PixelParams:
pixel.dfg
pixel.f0
pixel.energyCompensation
No new LUT, material parameter, uniform, or pre-integration pass is required.
One possible implementation is to expose a small helper next to the existing DFG utilities:
vec3 compensatedSpecularDirectionalAlbedo(const PixelParams pixel) {
return saturate(specularDFG(pixel) * pixel.energyCompensation);
}
Then the standard direct lighting path can replace the existing TODO with:
Fd *= 1.0 - compensatedSpecularDirectionalAlbedo(pixel);
The IBL path can keep using specularDFG(pixel) for specular reconstruction, while using the compensated value for diffuse energy preservation:
vec3 E = specularDFG(pixel);
vec3 E_comp = compensatedSpecularDirectionalAlbedo(pixel);
Fr = E * prefilteredRadiance(...);
Fr *= pixel.energyCompensation;
Fd = pixel.diffuseColor * diffuseIrradiance * (1.0 - E_comp) * diffuseBRDF;
For importance-sampled IBL, the same helper can be used for diffuse attenuation even though the specular radiance itself is sampled:
vec3 E_comp = compensatedSpecularDirectionalAlbedo(pixel);
Fd *= 1.0 - E_comp;
This keeps the change cheap:
one helper
one multiply for direct diffuse attenuation
one multiply for indirect diffuse attenuation
no additional texture fetches beyond the existing DFG lookup
no new precomputed data
The existing pixel.energyCompensation path remains unchanged for the specular lobe. The proposed change only uses that already-computed compensation factor to make the diffuse lobe's energy accounting consistent with the specular lobe that is actually rendered.
Related prior art
This proposal is also informed by the energy accounting used in Unreal Engine 5's shading model, which distinguishes conceptually between:
a compensation weight that scales a specular BxDF
an effective directional albedo used to attenuate lower lobes such as diffuse
In the terminology used by this issue, that distinction corresponds to:
W = specular lobe compensation weight
E_comp = compensated specular directional albedo
The relevant prior-art idea is the same energy-accounting split:
first compensate the specular lobe for single-scattering energy loss,
then use the compensated specular directional albedo to determine how much energy remains for diffuse.
This maps naturally onto Filament's existing concepts:
pixel.energyCompensation -> W
specularDFG(pixel) -> E_ss
saturate(E_ss * W) -> E_comp
The proposed Filament change is expressed entirely in terms of Filament's existing DFG LUT, specularDFG(pixel), and pixel.energyCompensation. It does not require new precomputed data.
Engineering tradeoff
This proposal is an approximation, not a full layered BSDF integration.
For direct lighting, the attenuation term:
$$
E_{comp}(v,\alpha,f_0)
$$
depends on view direction and roughness through the DFG parameterization. A more complete physical model could depend more explicitly on both light direction and view direction, and would require a more detailed treatment of transmission through the specular microfacet interface into the diffuse substrate.
However, Filament already uses the DFG LUT for:
specular IBL split-sum reconstruction
specular multiscattering energy compensation
Reusing the same DFG terms for diffuse/specular energy preservation has several advantages:
no new LUT
low shader cost
same directional albedo basis as IBL
same compensation term as direct specular lighting
consistent white furnace behavior
Expected benefits
This should improve energy preservation for standard materials under:
- white furnace tests;
- high roughness;
- grazing angles;
- strong direct lighting;
- mixed diffuse/specular dielectrics;
- IBL paths where the specular lobe is compensated but diffuse attenuation still uses the uncompensated DFG term.
The intended accounting becomes:
$$
\text{specular reflected energy} \approx E_{comp}
$$
$$
\text{diffuse remaining energy} \approx 1 - E_{comp}
$$
with:
$$
E_{comp} = \mathrm{saturate} \left( specularDFG(pixel) \cdot pixel.energyCompensation \right)
$$
In short, this completes the diffuse-side counterpart of Filament's existing specular multiscattering compensation using DFG data that is already available.
Direct lighting diffuse/specular energy preservation and IBL consistency
Summary
Filament's BRDF documentation already identifies two related energy issues:
The second issue, specular energy loss, is already addressed in the standard shading model through the multiscattering specular energy compensation term:
This scales the single-scattering GGX specular lobe to compensate for energy lost by not explicitly simulating multiple microfacet scattering.
However, the first issue, energy gain in diffuse reflectance, is still unresolved for direct lighting. The standard shading model currently evaluates diffuse and specular independently and adds them:
The problem is that the specular lobe is compensated for multiscattering, but the diffuse lobe is still evaluated as if the full incident light reached the diffuse scattering event. In Filament's own terminology, the specular-side energy loss is compensated, but the diffuse-side energy gain is not removed.
This issue proposes exposing the effective specular directional albedo after multiscattering compensation:
and using it to attenuate the diffuse lobe:
This completes the diffuse-side counterpart of the existing specular energy compensation and makes direct lighting, prefiltered IBL, and importance-sampled IBL use the same energy accounting.
Background: compensation vs preservation
Filament's documentation describes two different problems:
1. Energy gain in diffuse reflectance
The documentation states that the Lambert diffuse BRDF does not account for light that reflects at the surface and therefore cannot participate in the diffuse scattering event.
In a layered interpretation of the standard material, incident light first interacts with the specular interface. Only the part not reflected by the specular layer should reach the diffuse substrate.
Conceptually:
Therefore, if the specular layer reflects an amount
E, the diffuse lobe should receive approximately:of the incident energy.
2. Energy loss in specular reflectance
The Cook-Torrance GGX BRDF used by Filament is a single-scattering microfacet model. At high roughness, light can bounce between microfacets before escaping. A single-scattering model discards these paths through masking and shadowing, causing rough specular materials, especially metals, to lose energy.
Filament already compensates this by scaling the single-scattering specular lobe:
This can be written as:
where:
In Filament's multiscattering DFG LUT, this term is stored in:
so:
The current implementation applies:
where
f_ssis the original single-scattering specular lobe.This part is correct and matches the documentation: it compensates the specular energy loss.
The missing part is that once the specular lobe has been compensated, the diffuse lobe should be reduced by the energy reflected by the compensated specular layer.
Directional albedo terms
To avoid ambiguity, this issue uses the following terms:
W: specular energy compensation weightThis is the multiplier applied to the single-scattering specular BRDF.
It is not itself the reflected specular energy. It is a lobe scale factor.
E_ss: single-scattering specular directional albedoThe directional albedo of a BRDF is the fraction of unit incident radiance reflected by that BRDF for a given view direction:
For the single-scattering specular lobe with Schlick Fresnel, Filament already reconstructs this from the DFG LUT:
For the standard shading model, this corresponds to:
or in shader terms:
In Filament's multiscattering DFG pre-integration:
and the reconstruction is:
which is:
Where
specularDFG(pixel)comes fromspecularDFG(pixel)is Filament's reconstruction of the Fresnel-weighted specular DFG term used by the split-sum approximation for specular image-based lighting.The specular IBL integral is:
For the GGX Cook-Torrance specular BRDF, this is approximately:
The split-sum approximation separates the filtered lighting term from the BRDF integration term:
where
LDis the prefiltered environment radiance andDFGis the pre-integrated BRDF factor. In a white furnace environment,LDis approximately one, so the DFG term itself represents the specular directional albedo of the lobe.With Schlick Fresnel:
and:
Filament's multiscattering DFG reconstruction assumes:
so:
or equivalently:
Substituting this into the DFG integral gives:
After separating the terms that depend on
f0:Filament stores these two terms in the multiscattering DFG LUT as:
The DFG pre-integration code follows this layout:
The shader reconstruction is therefore:
mix(dfg.xxx, dfg.yyy, f0)which expands to:
This is exactly what
specularDFG(pixel)returns for the standard shading model:This value is the Fresnel-weighted directional albedo of the single-scattering specular lobe. It is not yet the directional albedo of the lobe that is finally used for lighting, because direct lighting and IBL later multiply the specular lobe by:
Therefore:
corresponds to:
while:
saturate(specularDFG(pixel) * pixel.energyCompensation)corresponds to:
This distinction is important:
specularDFG(pixel)tells us how much energy the uncompensated single-scattering lobe reflects, whileE_comptells us how much energy the compensated lobe used by the renderer reflects.E_comp: compensated specular directional albedoThe specular BRDF actually used for lighting is not
f_ss; it is:Therefore, the effective directional albedo of the specular lobe used by the renderer is approximately:
or:
This is the amount of energy reflected by the compensated specular layer. The diffuse lobe should receive the remaining energy:
The
saturateis important because the DFG reconstruction, multiscattering approximation, colored specular factors, and roughness remapping are approximate. Clamping prevents negative diffuse energy in edge cases.Direct lighting
The current standard direct lighting path is effectively:
or in code:
This compensates the specular lobe but still lets the diffuse lobe use the full incident energy. The result can over-preserve energy, especially for rough dielectric materials under strong direct lighting or grazing views.
The proposed direct lighting model is:
where:
In shader form:
This resolves the existing TODO:
The important distinction is:
IBL consistency
The prefiltered cubemap IBL path already attenuates diffuse by a specular DFG term:
However, this uses two different energy values:
For consistency, diffuse attenuation should use the same effective specular directional albedo as the specular lobe:
Equivalently:
and:
This makes the IBL path use the same energy accounting as direct lighting.
Importance-sampled IBL
The importance-sampling IBL path currently has:
Even when the specular radiance is evaluated through importance sampling rather than a prefiltered cubemap, the DFG terms are still available through
pixel.dfg.Therefore, the diffuse attenuation can still use:
instead of leaving
E = 0.This keeps the energy accounting consistent across:
White furnace intuition
Under a white furnace environment, ignoring AO, clear coat, sheen, transmission, subsurface, and other secondary effects, an energy-preserving dielectric material should not return more than the incident unit radiance.
Without diffuse attenuation, the simplified accounting is:
because the diffuse lobe still receives full energy while the specular lobe also reflects its compensated energy.
With the proposed attenuation:
This is the expected behavior: the compensated specular layer reflects
E_comp, and only the remaining energy reaches the diffuse scattering event.RGB vs scalar attenuation
The smallest change consistent with Filament's current IBL implementation is vector attenuation:
This matches the existing IBL pattern:
and preserves channel-wise energy accounting for colored specular materials.
A scalar attenuation is also possible:
or:
A scalar form can reduce hue shifts for colored specular materials because the diffuse substrate is attenuated uniformly. However, it is a larger behavioral change relative to Filament's current vector IBL attenuation.
This issue proposes the vector form as the smallest internally consistent change, but the choice is worth calling out explicitly.
Implementation outline
The implementation can be very small because all required data is already available in
PixelParams:No new LUT, material parameter, uniform, or pre-integration pass is required.
One possible implementation is to expose a small helper next to the existing DFG utilities:
Then the standard direct lighting path can replace the existing TODO with:
The IBL path can keep using
specularDFG(pixel)for specular reconstruction, while using the compensated value for diffuse energy preservation:For importance-sampled IBL, the same helper can be used for diffuse attenuation even though the specular radiance itself is sampled:
This keeps the change cheap:
The existing
pixel.energyCompensationpath remains unchanged for the specular lobe. The proposed change only uses that already-computed compensation factor to make the diffuse lobe's energy accounting consistent with the specular lobe that is actually rendered.Related prior art
This proposal is also informed by the energy accounting used in Unreal Engine 5's shading model, which distinguishes conceptually between:
In the terminology used by this issue, that distinction corresponds to:
The relevant prior-art idea is the same energy-accounting split:
This maps naturally onto Filament's existing concepts:
The proposed Filament change is expressed entirely in terms of Filament's existing DFG LUT,
specularDFG(pixel), andpixel.energyCompensation. It does not require new precomputed data.Engineering tradeoff
This proposal is an approximation, not a full layered BSDF integration.
For direct lighting, the attenuation term:
depends on view direction and roughness through the DFG parameterization. A more complete physical model could depend more explicitly on both light direction and view direction, and would require a more detailed treatment of transmission through the specular microfacet interface into the diffuse substrate.
However, Filament already uses the DFG LUT for:
Reusing the same DFG terms for diffuse/specular energy preservation has several advantages:
Expected benefits
This should improve energy preservation for standard materials under:
The intended accounting becomes:
with:
In short, this completes the diffuse-side counterpart of Filament's existing specular multiscattering compensation using DFG data that is already available.