blender/intern/cycles/app/cycles_precompute.cpp
Sebastian 88b7cc8a52 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
2026-08-27 09:42:40 +02:00

322 lines
11 KiB
C++

/* SPDX-FileCopyrightText: 2023 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include <map>
#include "util/string.h"
#include "util/array.h"
#include "util/hash.h"
#include "util/tbb.h"
#include "kernel/closure/bsdf_microfacet.h"
#include "kernel/sample/sobol_burley.h"
#include <iostream>
CCL_NAMESPACE_BEGIN
static float precompute_ggx_E(const float rough, const float mu, const float3 rand)
{
MicrofacetBsdf bsdf;
bsdf.weight = one_float3();
bsdf.sample_weight = 1.0f;
bsdf.N = make_float3(0.0f, 0.0f, 1.0f);
bsdf.alpha_x = bsdf.alpha_y = sqr(rough);
bsdf.ior = 1.0f;
bsdf.T = make_float3(1.0f, 0.0f, 0.0f);
bsdf_microfacet_ggx_setup(&bsdf);
float3 omega_in;
Spectrum eval;
float pdf = 0.0f;
float sampled_eta;
float2 sampled_roughness;
bsdf_microfacet_ggx_sample(nullptr,
(ShaderClosure *)&bsdf,
make_float3(0.0f, 0.0f, 1.0f),
make_float3(sqrtf(1.0f - sqr(mu)), 0.0f, mu),
rand,
&eval,
&omega_in,
&pdf,
&sampled_roughness,
&sampled_eta);
if (pdf != 0.0f) {
return average(eval) / pdf;
}
return 0.0f;
}
static float precompute_ggx_glass_E(const float rough,
const float mu,
const float eta,
const float3 rand)
{
MicrofacetBsdf bsdf;
bsdf.weight = one_float3();
bsdf.sample_weight = 1.0f;
bsdf.N = make_float3(0.0f, 0.0f, 1.0f);
bsdf.alpha_x = bsdf.alpha_y = sqr(rough);
bsdf.ior = eta;
bsdf.T = make_float3(1.0f, 0.0f, 0.0f);
bsdf_microfacet_ggx_glass_setup(&bsdf);
float3 omega_in;
Spectrum eval;
float pdf = 0.0f;
float sampled_eta;
float2 sampled_roughness;
bsdf_microfacet_ggx_sample(nullptr,
(ShaderClosure *)&bsdf,
make_float3(0.0f, 0.0f, 1.0f),
make_float3(sqrtf(1.0f - sqr(mu)), 0.0f, mu),
rand,
&eval,
&omega_in,
&pdf,
&sampled_roughness,
&sampled_eta);
if (pdf != 0.0f) {
return average(eval) / pdf;
}
return 0.0f;
}
static float precompute_ggx_gen_schlick_s(
const float rough, const float mu, const float eta, const float exponent, const float3 rand)
{
MicrofacetBsdf bsdf;
bsdf.weight = one_float3();
bsdf.sample_weight = 1.0f;
bsdf.N = make_float3(0.0f, 0.0f, 1.0f);
bsdf.alpha_x = bsdf.alpha_y = sqr(rough);
bsdf.ior = eta;
bsdf.T = make_float3(1.0f, 0.0f, 0.0f);
bsdf_microfacet_ggx_setup(&bsdf);
FresnelGeneralizedSchlick fresnel;
fresnel.tint = {one_float3(), one_float3()};
fresnel.f0 = make_float3(0.0f, 1.0f, 0.0f);
fresnel.f90 = make_float3(1.0f, 1.0f, 0.0f);
fresnel.exponent = exponent;
bsdf.fresnel_type = MicrofacetFresnel::GENERALIZED_SCHLICK;
bsdf.fresnel = &fresnel;
float3 omega_in;
Spectrum eval;
float pdf = 0.0f;
float sampled_eta;
float2 sampled_roughness;
bsdf_microfacet_ggx_sample(nullptr,
(ShaderClosure *)&bsdf,
make_float3(0.0f, 0.0f, 1.0f),
make_float3(sqrtf(1.0f - sqr(mu)), 0.0f, mu),
rand,
&eval,
&omega_in,
&pdf,
&sampled_roughness,
&sampled_eta);
if (pdf != 0.0f) {
/* The idea here is that the resulting Fresnel factor is always bounded by
* F0..F90, so it's enough to precompute and store the interpolation factor. */
return saturatef(eval.x / eval.y);
}
return 0.0f;
}
inline float ior_parametrization(const float z)
{
/* This parametrization ensures that the entire [1..inf] range of IORs is covered
* and that most precision is allocated to the common areas (1-2). */
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;
std::function<float(float, float, float, float3)> evaluation;
};
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,
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,
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,
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,
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);
}};
/* Overall albedo of the GGX microfacet BSDF with dielectric Fresnel,
* depending on cosI and roughness, for IOR<1. */
precompute_terms["ggx_glass_inv_E"] = {
1 << 23,
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,
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);
}};
/* Interpolation factor between F0 and F90 for the generalized Schlick Fresnel,
* depending on cosI and roughness, for IOR>1, using dielectric Fresnel mode. */
precompute_terms["ggx_gen_schlick_ior_s"] = {
1 << 20,
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);
}};
/* Interpolation factor between F0 and F90 for the generalized Schlick Fresnel,
* depending on cosI and roughness, for IOR>1. */
precompute_terms["ggx_gen_schlick_s"] = {
1 << 20,
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);
}};
if (!precompute_terms.contains(name)) {
return false;
}
const PrecomputeTerm &term = precompute_terms[name];
const int samples = term.samples;
const int nz = term.nz;
const int ny = term.ny;
const int nx = term.nx;
std::cout << "static const float table_" << name << "[" << nz * ny * nx << "] = {" << std::endl;
for (int z = 0; z < nz; z++) {
array<float> data(nx * ny);
parallel_for(0, nx * ny, [&](int64_t i) {
const int y = i / nx;
const int x = i % nx;
const uint seed = hash_uint2(x, y);
double sum = 0.0;
for (int sample = 0; sample < samples; sample++) {
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);
/* 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));
if (isnan(value)) {
value = 0.0f;
}
sum += (double)value;
}
data[y * nx + x] = saturatef(float(sum / double(samples)));
});
/* Print data formatted as C++ array */
for (int y = 0; y < ny; y++) {
std::cout << " ";
for (int x = 0; x < nx; x++) {
std::cout << std::to_string(data[y * nx + x]);
if (x + 1 < nx) {
/* Next number will follow in same line */
std::cout << "f, ";
}
else if (y + 1 < ny || z + 1 < nz) {
/* Next number will follow in next line */
std::cout << "f,";
}
else {
/* No next number */
std::cout << "f";
}
}
std::cout << std::endl;
}
/* If the array is three-dimensional, put an empty line between each slice. */
if (ny > 1 && z + 1 < nz) {
std::cout << std::endl;
}
}
std::cout << "};" << std::endl;
return true;
}
CCL_NAMESPACE_END
int main(const int argc, const char **argv)
{
if (argc < 2) {
return 1;
}
return ccl::cycles_precompute(argv[1]) ? 0 : 1;
}