Fix #158426: Improve energy conservation for multi-scatter glass BSDF

This patch improves the multi-scatter approximations for isotropic GGX
models to pass the furnace tests. The resolution of the look up tables
is increased as well as the mu (cosine(theta)) parametrization is
optimized to increase resolution at grazing angles.

Pull Request: https://projects.blender.org/blender/blender/pulls/159635
This commit is contained in:
Sebastian 2026-08-27 09:42:40 +02:00 • committed by Sebastian Herholz
parent c94b716c80
commit 88b7cc8a52
23 changed files with 4483 additions and 1145 deletions

View file

@ -138,6 +138,13 @@ inline float ior_parametrization(const float z)
return ior_from_F0(sqr(sqr(z)));
}
inline float mu_parametrization(const float y)
{
/* This parametrization increases the resolution at grazing angles, where the Fresnel
* can have a significant influence on the directional albedo. */
return y * y;
}
struct PrecomputeTerm {
int samples;
int nx, ny, nz;
@ -148,30 +155,42 @@ static bool cycles_precompute(std::string name)
{
std::map<string, PrecomputeTerm> precompute_terms;
/* Overall albedo of the GGX microfacet BRDF, depending on cosI and roughness. */
precompute_terms["ggx_E"] = {
1 << 23, 32, 32, 1, [](const float rough, const float mu, float, const float3 rand) {
return precompute_ggx_E(rough, mu, rand);
}};
precompute_terms["ggx_E"] = {1 << 23,
GGX_E_RES_ROUGH,
GGX_E_RES_MU,
1,
[](const float rough, const float y, float, const float3 rand) {
const float mu = mu_parametrization(y);
return precompute_ggx_E(rough, mu, rand);
}};
/* Overall albedo of the GGX microfacet BRDF, averaged over cosI */
precompute_terms["ggx_Eavg"] = {
1 << 26, 32, 1, 1, [](const float rough, const float mu, float, const float3 rand) {
return 2.0f * mu * precompute_ggx_E(rough, mu, rand);
}};
precompute_terms["ggx_Eavg"] = {1 << 26,
GGX_E_RES_ROUGH,
1,
1,
[](const float rough, const float mu, float, const float3 rand) {
return 2.0f * mu * precompute_ggx_E(rough, mu, rand);
}};
/* Overall albedo of the GGX microfacet BSDF with dielectric Fresnel,
* depending on cosI and roughness, for IOR>1. */
precompute_terms["ggx_glass_E"] = {
1 << 23,
16,
16,
16,
[](const float rough, const float mu, const float z, const float3 rand) {
GGX_GLASS_E_RES_ROUGH,
GGX_GLASS_E_RES_MU,
GGX_GLASS_E_RES_IOR,
[](const float rough, const float y, const float z, const float3 rand) {
const float ior = ior_parametrization(z);
const float mu = mu_parametrization(y);
return precompute_ggx_glass_E(rough, mu, ior, rand);
}};
/* Overall albedo of the GGX microfacet BSDF with dielectric Fresnel,
* averaged over cosI, for IOR>1. */
precompute_terms["ggx_glass_Eavg"] = {
1 << 26, 16, 1, 16, [](const float rough, const float mu, const float z, const float3 rand) {
1 << 26,
GGX_GLASS_E_RES_ROUGH,
1,
GGX_GLASS_E_RES_IOR,
[](const float rough, const float mu, const float z, const float3 rand) {
const float ior = ior_parametrization(z);
return 2.0f * mu * precompute_ggx_glass_E(rough, mu, ior, rand);
}};
@ -179,17 +198,22 @@ static bool cycles_precompute(std::string name)
* depending on cosI and roughness, for IOR<1. */
precompute_terms["ggx_glass_inv_E"] = {
1 << 23,
16,
16,
16,
[](const float rough, const float mu, const float z, const float3 rand) {
GGX_GLASS_E_RES_ROUGH,
GGX_GLASS_E_RES_MU,
GGX_GLASS_E_RES_IOR,
[](const float rough, const float y, const float z, const float3 rand) {
const float ior = ior_parametrization(z);
const float mu = mu_parametrization(y);
return precompute_ggx_glass_E(rough, mu, 1.0f / ior, rand);
}};
/* Overall albedo of the GGX microfacet BSDF with dielectric Fresnel,
* averaged over cosI, for IOR<1. */
precompute_terms["ggx_glass_inv_Eavg"] = {
1 << 26, 16, 1, 16, [](const float rough, const float mu, const float z, const float3 rand) {
1 << 26,
GGX_GLASS_E_RES_ROUGH,
1,
GGX_GLASS_E_RES_IOR,
[](const float rough, const float mu, const float z, const float3 rand) {
const float ior = ior_parametrization(z);
return 2.0f * mu * precompute_ggx_glass_E(rough, mu, 1.0f / ior, rand);
}};
@ -198,11 +222,12 @@ static bool cycles_precompute(std::string name)
* depending on cosI and roughness, for IOR>1, using dielectric Fresnel mode. */
precompute_terms["ggx_gen_schlick_ior_s"] = {
1 << 20,
16,
16,
16,
[](const float rough, const float mu, const float z, const float3 rand) {
GGX_GEN_SCHLICK_IOR_S_RES_ROUGH,
GGX_GEN_SCHLICK_IOR_S_RES_MU,
GGX_GEN_SCHLICK_IOR_S_RES_IOR,
[](const float rough, const float y, const float z, const float3 rand) {
const float ior = ior_parametrization(z);
const float mu = mu_parametrization(y);
return precompute_ggx_gen_schlick_s(rough, mu, ior, -1.0f, rand);
}};
@ -210,12 +235,13 @@ static bool cycles_precompute(std::string name)
* depending on cosI and roughness, for IOR>1. */
precompute_terms["ggx_gen_schlick_s"] = {
1 << 20,
16,
16,
16,
[](const float rough, const float mu, const float z, const float3 rand) {
GGX_GEN_SCHLICK_S_RES_ROUGH,
GGX_GEN_SCHLICK_S_RES_MU,
GGX_GEN_SCHLICK_S_RES_IOR,
[](const float rough, const float y, const float z, const float3 rand) {
/* Remap 0..1 to 0..inf, with 0.5 mapping to 5 (the default value). */
const float exponent = 5.0f * ((1.0f - z) / z);
const float mu = mu_parametrization(y);
return precompute_ggx_gen_schlick_s(rough, mu, 1.0f, exponent, rand);
}};
@ -242,7 +268,8 @@ static bool cycles_precompute(std::string name)
const float4 rand = sobol_burley_sample_4D(sample, 0, seed, 0xffffffff);
const float rough = (nx == 1) ? 0.0f : clamp(float(x) / float(nx - 1), 1e-4f, 1.0f);
const float mu = (ny == 1) ? rand.w : clamp(float(y) / float(ny - 1), 1e-4f, 1.0f);
/* We use 1e-2f as min for mu since to the y*y mu parametrization results in 1e-4f. */
const float mu = (ny == 1) ? rand.w : clamp(float(y) / float(ny - 1), 1e-2f, 1.0f);
const float ior = (nz == 1) ? 0.0f : clamp(float(z) / float(nz - 1), 1e-4f, 0.99f);
float value = term.evaluation(rough, mu, ior, make_float3(rand));

View file

@ -249,6 +249,7 @@ set(SRC_KERNEL_UTIL_HEADERS
)
set(SRC_KERNEL_TYPES_HEADERS
constants.h
data_arrays.h
data_template.h
features.h

View file

@ -9,6 +9,7 @@
#include "kernel/closure/bsdf_transparent.h"
#include "kernel/closure/bsdf_util.h"
#include "kernel/constants.h"
#include "kernel/sample/mapping.h"
#include "kernel/util/lookup_table.h"
@ -131,6 +132,30 @@ struct Coat {
Spectrum weight;
};
/* -------------------------------------------------------------------- */
/** \name GGX LUT Index Mapping Functions
*
* This section contains functions to map from GGX alphas, IOR, and mu to the indicies of
* 2D and 3D LUTs (e.g., for multis-scatter GGX energy compensation or scattering albedos).
* \{ */
ccl_device_forceinline float alpha_to_roughness_index(const float alpha_x, const float alpha_y)
{
return sqrtf(sqrtf(alpha_x * alpha_y));
}
ccl_device_forceinline float ior_to_z_index(const float ior)
{
return sqrtf(fabsf((ior - 1.0f) / (ior + 1.0f)));
}
ccl_device_forceinline float mu_to_y_index(const float mu)
{
return safe_sqrtf(mu);
}
/** \} */
ccl_device_inline void adjust_thin_film_ior_at_backface(ccl_private float &film_ior,
const float inv_bulk_ior)
{
@ -512,15 +537,17 @@ ccl_device_inline void microfacet_ggx_preserve_energy(KernelGlobals kg,
const Spectrum Fss)
{
const float mu = dot(wi, bsdf->N);
const float rough = sqrtf(sqrtf(bsdf->alpha_x * bsdf->alpha_y));
const float y = mu_to_y_index(mu);
const float rough = alpha_to_roughness_index(bsdf->alpha_x, bsdf->alpha_y);
float E;
float E_avg;
if (bsdf->type == CLOSURE_BSDF_MICROFACET_GGX_ID ||
bsdf->type == CLOSURE_BSDF_THIN_GLASS_TRANSMISSION_ID)
{
E = lookup_table_read_2D(kg, rough, mu, kernel_data.tables.ggx_E, 32, 32);
E_avg = lookup_table_read(kg, rough, kernel_data.tables.ggx_Eavg, 32);
E = lookup_table_read_2D(
kg, rough, y, kernel_data.tables.ggx_E, GGX_E_RES_ROUGH, GGX_E_RES_MU);
E_avg = lookup_table_read(kg, rough, kernel_data.tables.ggx_Eavg, GGX_E_RES_ROUGH);
}
else if (bsdf->type == CLOSURE_BSDF_MICROFACET_GGX_GLASS_ID) {
int ofs = kernel_data.tables.ggx_glass_E;
@ -531,10 +558,11 @@ ccl_device_inline void microfacet_ggx_preserve_energy(KernelGlobals kg,
ofs = kernel_data.tables.ggx_glass_inv_E;
avg_ofs = kernel_data.tables.ggx_glass_inv_Eavg;
}
/* TODO: Bias mu towards more precision for low values. */
const float z = sqrtf(fabsf((ior - 1.0f) / (ior + 1.0f)));
E = lookup_table_read_3D(kg, rough, mu, z, ofs, 16, 16, 16);
E_avg = lookup_table_read_2D(kg, rough, z, avg_ofs, 16, 16);
const float z = ior_to_z_index(ior);
E = lookup_table_read_3D(
kg, rough, y, z, ofs, GGX_GLASS_E_RES_ROUGH, GGX_GLASS_E_RES_MU, GGX_GLASS_E_RES_IOR);
E_avg = lookup_table_read_2D(
kg, rough, z, avg_ofs, GGX_GLASS_E_RES_ROUGH, GGX_GLASS_E_RES_IOR);
}
else {
kernel_assert(false);
@ -593,17 +621,31 @@ ccl_device Spectrum bsdf_microfacet_estimate_albedo(KernelGlobals kg,
* reflection approximation from the microfacet_fresnel call above in that case. */
}
else {
const float rough = sqrtf(sqrtf(bsdf->alpha_x * bsdf->alpha_y));
const float rough = alpha_to_roughness_index(bsdf->alpha_x, bsdf->alpha_y);
float s;
if (fresnel->exponent < 0.0f) {
const float z = sqrtf(fabsf((bsdf->ior - 1.0f) / (bsdf->ior + 1.0f)));
s = lookup_table_read_3D(
kg, rough, cos_NI, z, kernel_data.tables.ggx_gen_schlick_ior_s, 16, 16, 16);
const float z = ior_to_z_index(bsdf->ior);
const float y = mu_to_y_index(cos_NI);
s = lookup_table_read_3D(kg,
rough,
y,
z,
kernel_data.tables.ggx_gen_schlick_ior_s,
GGX_GEN_SCHLICK_IOR_S_RES_ROUGH,
GGX_GEN_SCHLICK_IOR_S_RES_MU,
GGX_GEN_SCHLICK_IOR_S_RES_IOR);
}
else {
const float z = 1.0f / (0.2f * fresnel->exponent + 1.0f);
s = lookup_table_read_3D(
kg, rough, cos_NI, z, kernel_data.tables.ggx_gen_schlick_s, 16, 16, 16);
const float y = mu_to_y_index(cos_NI);
s = lookup_table_read_3D(kg,
rough,
y,
z,
kernel_data.tables.ggx_gen_schlick_s,
GGX_GEN_SCHLICK_S_RES_ROUGH,
GGX_GEN_SCHLICK_S_RES_MU,
GGX_GEN_SCHLICK_S_RES_IOR);
}
coeff *= fresnel->tint * mix(fresnel->f0, fresnel->f90, s);
return coeff.sum();
@ -617,9 +659,16 @@ ccl_device Spectrum bsdf_microfacet_estimate_albedo(KernelGlobals kg,
* reflection approximation from the microfacet_fresnel call above in that case. */
}
else {
const float rough = sqrtf(sqrtf(bsdf->alpha_x * bsdf->alpha_y));
const float s = lookup_table_read_3D(
kg, rough, cos_NI, 0.5f, kernel_data.tables.ggx_gen_schlick_s, 16, 16, 16);
const float rough = alpha_to_roughness_index(bsdf->alpha_x, bsdf->alpha_y);
const float y = mu_to_y_index(cos_NI);
const float s = lookup_table_read_3D(kg,
rough,
y,
0.5f,
kernel_data.tables.ggx_gen_schlick_s,
GGX_GEN_SCHLICK_S_RES_ROUGH,
GGX_GEN_SCHLICK_S_RES_MU,
GGX_GEN_SCHLICK_S_RES_IOR);
/* TODO: Precompute B factor term and account for it here. */
const Spectrum reflectance = mix(fresnel->f0, one_spectrum(), s) * float(eval_reflection);
return reflectance;
@ -631,10 +680,17 @@ ccl_device Spectrum bsdf_microfacet_estimate_albedo(KernelGlobals kg,
{
/* We can re-use the ggx_gen_schlick_ior_s table here, since it's already precomputed for our
* exponent<0 corner case where we use the real dielectric Fresnel. */
const float rough = sqrtf(sqrtf(bsdf->alpha_x * bsdf->alpha_y));
const float z = sqrtf(fabsf((bsdf->ior - 1.0f) / (bsdf->ior + 1.0f)));
const float s = lookup_table_read_3D(
kg, rough, cos_NI, z, kernel_data.tables.ggx_gen_schlick_ior_s, 16, 16, 16);
const float rough = alpha_to_roughness_index(bsdf->alpha_x, bsdf->alpha_y);
const float z = ior_to_z_index(bsdf->ior);
const float y = mu_to_y_index(cos_NI);
const float s = lookup_table_read_3D(kg,
rough,
y,
z,
kernel_data.tables.ggx_gen_schlick_ior_s,
GGX_GEN_SCHLICK_IOR_S_RES_ROUGH,
GGX_GEN_SCHLICK_IOR_S_RES_MU,
GGX_GEN_SCHLICK_IOR_S_RES_IOR);
#if defined(__KERNEL_HIP__)
/* Temporary workaround for a HIP compiler bug under Windows in combination with AMD 6800 XT

View file

@ -8,6 +8,7 @@
#pragma once
#include "kernel/constants.h"
#include "kernel/types.h"
#include "kernel/closure/bsdf_microfacet.h"
@ -319,7 +320,10 @@ ccl_device_forceinline float bsdf_hair_huang_energy_scale(KernelGlobals kg,
const bool inv_table = (ior < 1.0f);
const int ofs = inv_table ? kernel_data.tables.ggx_glass_inv_E : kernel_data.tables.ggx_glass_E;
const float z = sqrtf(fabsf((ior - 1.0f) / (ior + 1.0f)));
return 1.0f / lookup_table_read_3D(kg, rough, mu, z, ofs, 16, 16, 16);
const float y = mu_to_y_index(mu);
return 1.0f /
lookup_table_read_3D(
kg, rough, y, z, ofs, GGX_GLASS_E_RES_ROUGH, GGX_GLASS_E_RES_MU, GGX_GLASS_E_RES_IOR);
}
/* Sample microfacets from a tilted mesonormal. */

View file

@ -12,6 +12,7 @@
#include "kernel/closure/alloc.h"
#include "kernel/sample/mapping.h"
#include "kernel/constants.h"
#include "kernel/util/lookup_table.h"
CCL_NAMESPACE_BEGIN
@ -46,10 +47,20 @@ ccl_device Spectrum bsdf_sheen_setup(KernelGlobals kg,
const float cosNI = dot(bsdf->N, sd->wi);
const int offset = kernel_data.tables.sheen_ltc;
bsdf->transformA = lookup_table_read_2D(kg, cosNI, bsdf->roughness, offset, 32, 32);
bsdf->transformB = lookup_table_read_2D(kg, cosNI, bsdf->roughness, offset + 32 * 32, 32, 32);
const float albedo = lookup_table_read_2D(
kg, cosNI, bsdf->roughness, offset + 2 * 32 * 32, 32, 32);
bsdf->transformA = lookup_table_read_2D(
kg, cosNI, bsdf->roughness, offset, SHEEN_RES_MU, SHEEN_RES_ROUGH);
bsdf->transformB = lookup_table_read_2D(kg,
cosNI,
bsdf->roughness,
offset + SHEEN_RES_MU * SHEEN_RES_ROUGH,
SHEEN_RES_MU,
SHEEN_RES_ROUGH);
const float albedo = lookup_table_read_2D(kg,
cosNI,
bsdf->roughness,
offset + 2 * SHEEN_RES_MU * SHEEN_RES_ROUGH,
SHEEN_RES_MU,
SHEEN_RES_ROUGH);
/* If the given roughness and angle result in an invalid LTC, skip the closure. */
if (fabsf(bsdf->transformA) < 1e-5f || albedo < 1e-5f) {

View file

@ -0,0 +1,25 @@
/* SPDX-FileCopyrightText: 2026 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
/* Constant defines for the resolutions of the albedo look up tables. */
#define GGX_E_RES_ROUGH 32
#define GGX_E_RES_MU 32
#define GGX_GLASS_E_RES_ROUGH 32
#define GGX_GLASS_E_RES_MU 32
#define GGX_GLASS_E_RES_IOR 32
#define GGX_GEN_SCHLICK_S_RES_ROUGH 32
#define GGX_GEN_SCHLICK_S_RES_MU 32
#define GGX_GEN_SCHLICK_S_RES_IOR 32
#define GGX_GEN_SCHLICK_IOR_S_RES_ROUGH 32
#define GGX_GEN_SCHLICK_IOR_S_RES_MU 32
#define GGX_GEN_SCHLICK_IOR_S_RES_IOR 32
#define SHEEN_RES_ROUGH 32
#define SHEEN_RES_MU 32

View file

@ -47,6 +47,7 @@ set(SRC
curves.cpp
scene.cpp
shader.cpp
shader.tables
shader_graph.cpp
shader_nodes.cpp
stats.cpp

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5924db1d9f863747a06e00eff444e6be588c2f86270cc13f2fe8cba4864d3f7d
size 13249

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:53a1d1b9647b53fd9334bd6f6b8a401b6700702a38bad3e4f37f7bd9e4ae2e59
size 11773

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7c82452cbaf98cfe0b1eeee0c686c7356f563e33b9d68dfc149542815ce9dbea
size 11775

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6be6cae83331f56f67199458d9feabf48b33997df2cd459fb6a85a685f0d615a
size 11205

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d9ebdacb9bb5a1ce81e5775191ed530b0a84eb930b33d925bec85b78c0077b63
size 10063

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6c7e41613bfb663438e6ab7ef8b80aec081c05d8594b340c49204b2cd255ff85
size 10065

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d002e58cd38824b80b56a80bc793f30df2eacd1c95ef1caa982207cef7cb1f8b
size 157064

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8e90e758b111456d42649bc2c8019aa4806c6b9345811fe2e843b2ba991efcf3
size 147787

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7aaeb0eb6821a1725b8e28543c72acb1d93e7778af038960061ba75bb8c61e3a
size 162220

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7230c5d76a5e59d6e9b6130b3b8ad38d1d5fc540065ff32085bafebdc9cf241a
size 12230

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:59072def3f56d5f012c26bad512fafec275c177f33e112b9b42ae71a763973a3
size 12216

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5d35fc8d6d714b10d56d974987c5326d4b93d24312e3c5b83e67e55d87853168
size 12218

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d68f84d135a2cc533b4d24479d7aaeef260a2bf7a3b1a68f39643d9b967c6257
size 12230

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:65c7205f58613ebd106d54864ce1035075d3c405d887c15723c7f8a65748c55e
size 12216

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e218da11e5ee961ddb772d169185e569e890e0e87d28d951d2b494709e938451
size 12218