EEVEE: use Schlick's approximation instead of real Fresnel for glass #112390

Merged
Weizhen Huang merged 3 commits from weizhen/blender:eevee-schlick-transmission into main 2023-09-18 14:26:27 +02:00
11 changed files with 43182 additions and 22596 deletions

View File

@ -192,10 +192,12 @@ static void eevee_init_util_texture()
for (int j = 0; j < 16; j++) {
for (int x = 0; x < 64; x++) {
for (int y = 0; y < 64; y++) {
/* BSDF LUT for `IOR < 1`. */
texels_layer[y * 64 + x][0] = blender::eevee::lut::bsdf_ggx[j][y][x][0];
texels_layer[y * 64 + x][1] = blender::eevee::lut::bsdf_ggx[j][y][x][1];
texels_layer[y * 64 + x][2] = 0.0; /* UNUSED */
texels_layer[y * 64 + x][3] = 0.0; /* UNUSED */
texels_layer[y * 64 + x][2] = blender::eevee::lut::bsdf_ggx[j][y][x][2];
weizhen marked this conversation as resolved

Maybe note that this doesn't share the same parametrization.

Maybe note that this doesn't share the same parametrization.
/* BTDF LUT for `IOR > 1`, parametrized differently as above. See `eevee_lut_comp.glsl`. */
texels_layer[y * 64 + x][3] = blender::eevee::lut::btdf_ggx[j][y][x][0];
}
}
texels_layer += 64 * 64;

View File

@ -66,6 +66,18 @@ vec4 sample_3D_texture(sampler2DArray tex, vec3 coords)
return mix(tex_low, tex_high, interp);
}
/* Return texture coordinates to sample Surface LUT. */
vec3 lut_coords_btdf(float cos_theta, float roughness, float ior)
{
vec3 coords = vec3(sqrt((ior - 1.0) / (ior + 1.0)), sqrt(1.0 - cos_theta), roughness);
/* scale and bias coordinates, for correct filtered lookup */
coords.xy = coords.xy * (LUT_SIZE - 1.0) / LUT_SIZE + 0.5 / LUT_SIZE;
coords.z = coords.z * lut_btdf_layer_count + lut_btdf_layer_first;
return coords;
}
/* Return texture coordinates to sample BSDF LUT. */
vec3 lut_coords_bsdf(float cos_theta, float roughness, float ior)
{
@ -89,37 +101,45 @@ vec3 lut_coords_bsdf(float cos_theta, float roughness, float ior)
return coords;
}
/* Returns GGX transmittance in first component and reflectance in second. */
/* Computes the reflectance and transmittance based on the BSDF LUT. */
vec2 bsdf_lut(float cos_theta, float roughness, float ior, float do_multiscatter)
{
if (ior <= 1e-5) {
if (ior == 1.0) {
return vec2(0.0, 1.0);
}
vec2 split_sum;
float transmission_factor;
float F0 = F0_from_ior(ior);
float F90 = 1.0;
if (ior >= 1.0) {
vec2 split_sum = brdf_lut(cos_theta, roughness);
float f0 = F0_from_ior(ior);
split_sum = brdf_lut(cos_theta, roughness);
transmission_factor = sample_3D_texture(utilTex, lut_coords_btdf(cos_theta, roughness, ior)).a;
/* Gradually increase `f90` from 0 to 1 when IOR is in the range of [1.0, 1.33], to avoid harsh
* transition at `IOR == 1`. */
float f90 = fast_sqrt(saturate(f0 / 0.02));
float brdf = F_brdf_multi_scatter(vec3(f0), vec3(f90), split_sum).r;
/* Energy conservation. */
float btdf = 1.0 - brdf;
/* Assuming the energy loss caused by single-scattering is distributed proportionally in the
* reflection and refraction lobes. */
return vec2(btdf, brdf) * ((do_multiscatter == 0.0) ? sum(split_sum) : 1.0);
F90 = saturate(2.33 / 0.33 * (ior - 1.0) / (ior + 1.0));
}
else {
vec3 bsdf = sample_3D_texture(utilTex, lut_coords_bsdf(cos_theta, roughness, ior)).rgb;
split_sum = bsdf.rg;
transmission_factor = bsdf.b;
}
vec2 btdf_brdf = sample_3D_texture(utilTex, lut_coords_bsdf(cos_theta, roughness, ior)).rg;
float reflectance = F_brdf_single_scatter(vec3(F0), vec3(F90), split_sum).r;
float transmittance = (1.0 - F0) * transmission_factor;
if (do_multiscatter != 0.0) {
/* For energy-conserving BSDF the reflection and refraction lobes should sum to one. Assuming
* the energy loss of single-scattering is distributed proportionally in the two lobes. */
btdf_brdf /= (btdf_brdf.x + btdf_brdf.y);
if (do_multiscatter) {
float Ess = F0 * split_sum.x + split_sum.y + (1.0 - F0) * transmission_factor;
/* TODO: maybe add saturation for higher roughness similar as in `F_brdf_multi_scatter()`.
* However, it is not necessarily desirable that the users see a different color than they
* picked. */
float scale = 1.0 / Ess;
reflectance *= scale;
transmittance *= scale;
}
return btdf_brdf;
return vec2(reflectance, transmittance);
}
/** \} */

File diff suppressed because it is too large Load Diff

View File

@ -20,8 +20,10 @@ extern const float ltc_mag_ggx[64][64][2];
extern const float ltc_disk_integral[64][64][1];
/* Precomputed integrated split fresnel term of the GGX BRDF. */
extern const float brdf_ggx[64][64][2];
/* Precomputed reflectance and transmittance of glass material with IOR < 1. */
extern const float bsdf_ggx[16][64][64][2];
/* Precomputed Schlick reflectance and transmittance factor of glass material with IOR < 1. */
extern const float bsdf_ggx[16][64][64][3];
/* Precomputed Schlick transmittance factor of glass material with IOR > 1. */
extern const float btdf_ggx[16][64][64][1];
/* 4 different blue noise, one per channel. */
extern const float blue_noise[64][64][4];

View File

@ -419,6 +419,8 @@ class UtilityTexture : public Texture {
for (auto y : IndexRange(lut_size)) {
layer.data[y][x][0] = lut::bsdf_ggx[layer_id][y][x][0];
layer.data[y][x][1] = lut::bsdf_ggx[layer_id][y][x][1];
layer.data[y][x][2] = lut::bsdf_ggx[layer_id][y][x][2];
layer.data[y][x][3] = lut::btdf_ggx[layer_id][y][x][0];
}
}
}

View File

@ -88,7 +88,8 @@ enum eDebugMode : uint32_t {
enum PrecomputeType : uint32_t {
LUT_GGX_BRDF_SPLIT_SUM = 0u,
LUT_GGX_BSDF_SPLIT_SUM = 1u,
LUT_GGX_BTDF_IOR_GT_ONE = 1u,
LUT_GGX_BSDF_SPLIT_SUM = 2u,
};
/** \} */
@ -1292,7 +1293,7 @@ BLI_STATIC_ASSERT_ALIGN(UniformData, 16)
#define UTIL_LTC_MAT_LAYER 2
#define UTIL_LTC_MAG_LAYER 3
#define UTIL_BSDF_LAYER 3
#define UTIL_BTDF_LAYER 4
#define UTIL_BTDF_LAYER 5
#define UTIL_DISK_INTEGRAL_LAYER 4
#define UTIL_DISK_INTEGRAL_COMP 3

View File

@ -14,7 +14,7 @@
/* Generate BRDF LUT following "Real shading in unreal engine 4" by Brian Karis
* https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
* Parametrizing with `x = roughness` and `y = sqrt(1.0 - cos(theta))`.
* The result is interpreted as: `integral = f0 * scale + f90 * bias`. */
* The result is interpreted as: `integral = F0 * scale + F90 * bias`. */
vec4 ggx_brdf_split_sum(vec3 lut_coord)
{
/* Squaring for perceptually linear roughness, see [Physically Based Shading at Disney]
@ -54,7 +54,12 @@ vec4 ggx_brdf_split_sum(vec3 lut_coord)
return vec4(scale, bias, 0.0, 0.0);
}
/* Generate BSDF LUT for `IOR < 1`. Returns the transmittance and the reflectance. */
/* Generate BSDF LUT for `IOR < 1` using Schlick's approximation. Returns the transmittance and the
* scale and bias for reflectance.
*
* The result is interpreted as:
* `reflectance = F0 * scale + F90 * bias`,
* `transmittance = (1 - F0) * transmission_factor`. */
vec4 ggx_bsdf_split_sum(vec3 lut_coord)
{
float ior = sqrt(lut_coord.x);
@ -78,9 +83,9 @@ vec4 ggx_bsdf_split_sum(vec3 lut_coord)
vec3 V = vec3(sqrt(1.0 - square(NV)), 0.0, NV);
/* Integrating BSDF */
float transmittance = 0.0;
float reflectance = 0.0;
float scale = 0.0;
float bias = 0.0;
float transmission_factor = 0.0;
const uint sample_count = 512u * 512u;
for (uint i = 0u; i < sample_count; i++) {
vec2 rand = hammersley_2d(i, sample_count);
@ -88,14 +93,17 @@ vec4 ggx_bsdf_split_sum(vec3 lut_coord)
/* Microfacet normal. */
vec3 H = sample_ggx(Xi, roughness, V);
float fresnel = F_eta(ior, dot(V, H));
float HL = 1.0 - (1.0 - square(dot(V, H))) / square(ior);
float s = saturate(pow5f(1.0 - saturate(HL)));
/* Reflection. */
vec3 R = -reflect(V, H);
float NR = R.z;
if (NR > 0.0) {
/* Assuming sample visible normals, accumulating `brdf * NV / pdf.` */
reflectance += fresnel * bxdf_ggx_smith_G1(NR, roughness_sq);
/* Assuming sample visible normals, `weight = brdf * NV / (pdf * fresnel).` */
float weight = bxdf_ggx_smith_G1(NR, roughness_sq);
scale += (1.0 - s) * weight;
bias += s * weight;
}
/* Refraction. */
@ -103,16 +111,63 @@ vec4 ggx_bsdf_split_sum(vec3 lut_coord)
float NT = T.z;
/* In the case of TIR, `T == vec3(0)`. */
if (NT < 0.0) {
/* Assuming sample visible normals, accumulating `btdf * NV / pdf.` */
transmittance += (1.0 - fresnel) * bxdf_ggx_smith_G1(NT, roughness_sq);
/* Assuming sample visible normals, accumulating `btdf * NV / (pdf * (1 - F0)).` */
transmission_factor += (1.0 - s) * bxdf_ggx_smith_G1(NT, roughness_sq);
}
}
transmittance /= float(sample_count);
reflectance /= float(sample_count);
transmission_factor /= float(sample_count);
scale /= float(sample_count);
bias /= float(sample_count);
/* There is place to put multi-scatter result (which is a little bit different still)
* and / or lobe fitting for better sampling of. */
return vec4(transmittance, reflectance, 0.0, 0.0);
return vec4(scale, bias, transmission_factor, 0.0);
}
/* Generate BTDF LUT for `IOR > 1` using Schlick's approximation. Only the transmittance is needed
* because the scale and bias does not depend on the IOR and can be obtained from the BRDF LUT.
*
* Parametrize with `x = sqrt((ior - 1) / (ior + 1))` for higher precision in 1 < IOR < 2,
* and `y = sqrt(1.0 - cos(theta))`, `z = roughness` similar to BRDF LUT.
*
* The result is interpreted as:
* `transmittance = (1 - F0) * transmission_factor`. */
vec4 ggx_btdf_gt_one(vec3 lut_coord)
{
float f0 = square(lut_coord.x);
float inv_ior = (1.0 - f0) / (1.0 + f0);
float NV = clamp(1.0 - square(lut_coord.y), 1e-4, 0.9999);
vec3 V = vec3(sqrt(1.0 - square(NV)), 0.0, NV);
/* Squaring for perceptually linear roughness, see [Physically Based Shading at Disney]
* (https://media.disneyanimation.com/uploads/production/publication_asset/48/asset/s2012_pbs_disney_brdf_notes_v3.pdf)
* Section 5.4. */
float roughness = square(lut_coord.z);
float roughness_sq = square(roughness);
/* Integrating BTDF. */
float transmission_factor = 0.0;
const uint sample_count = 512u * 512u;
for (uint i = 0u; i < sample_count; i++) {
vec2 rand = hammersley_2d(i, sample_count);
vec3 Xi = sample_cylinder(rand);
/* Microfacet normal. */
vec3 H = sample_ggx(Xi, roughness, V);
/* Refraction. */
vec3 L = refract(-V, H, inv_ior);
float NL = L.z;
if (NL < 0.0) {
/* Schlick's Fresnel. */
float s = saturate(pow5f(1.0 - saturate(dot(V, H))));
/* Assuming sample visible normals, accumulating `btdf * NV / (pdf * (1 - F0)).` */
transmission_factor += (1.0 - s) * bxdf_ggx_smith_G1(NL, roughness_sq);
}
}
transmission_factor /= float(sample_count);
return vec4(transmission_factor, 0.0, 0.0, 0.0);
}
void main()
@ -125,6 +180,9 @@ void main()
case LUT_GGX_BRDF_SPLIT_SUM:
result = ggx_brdf_split_sum(lut_normalized_coordinate);
break;
case LUT_GGX_BTDF_IOR_GT_ONE:
result = ggx_btdf_gt_one(lut_normalized_coordinate);
break;
case LUT_GGX_BSDF_SPLIT_SUM:
result = ggx_bsdf_split_sum(lut_normalized_coordinate);
break;

View File

@ -314,38 +314,54 @@ vec3 lut_coords_bsdf(float cos_theta, float roughness, float ior)
return saturate(coords);
}
/* Return texture coordinates to sample Surface LUT. */
vec3 lut_coords_btdf(float cos_theta, float roughness, float ior)
{
return vec3(sqrt((ior - 1.0) / (ior + 1.0)), sqrt(1.0 - cos_theta), roughness);
}
/* Computes the reflectance and transmittance based on the BSDF LUT. */
vec2 bsdf_lut(float cos_theta, float roughness, float ior, float do_multiscatter)
{
if (ior <= 1e-5) {
#ifdef EEVEE_UTILITY_TX
if (ior == 1.0) {
return vec2(0.0, 1.0);
}
vec2 split_sum;
float transmission_factor;
float F0 = F0_from_ior(ior);
float F90 = 1.0;
if (ior >= 1.0) {
vec2 split_sum = brdf_lut(cos_theta, roughness);
float f0 = F0_from_ior(ior);
split_sum = brdf_lut(cos_theta, roughness);
vec3 coords = lut_coords_btdf(cos_theta, roughness, ior);
transmission_factor = utility_tx_sample_bsdf_lut(utility_tx, coords.xy, coords.z).a;
/* Gradually increase `f90` from 0 to 1 when IOR is in the range of [1.0, 1.33], to avoid harsh
* transition at `IOR == 1`. */
float f90 = fast_sqrt(saturate(f0 / 0.02));
float brdf = F_brdf_multi_scatter(vec3(f0), vec3(f90), split_sum).r;
/* Energy conservation. */
float btdf = 1.0 - brdf;
/* Assuming the energy loss caused by single-scattering is distributed proportionally in the
* reflection and refraction lobes. */
return vec2(btdf, brdf) * ((do_multiscatter == 0.0) ? sum(split_sum) : 1.0);
F90 = saturate(2.33 / 0.33 * (ior - 1.0) / (ior + 1.0));
}
else {
vec3 coords = lut_coords_bsdf(cos_theta, roughness, ior);
vec3 bsdf = utility_tx_sample_bsdf_lut(utility_tx, coords.xy, coords.z).rgb;
split_sum = bsdf.rg;
transmission_factor = bsdf.b;
}
#ifdef EEVEE_UTILITY_TX
vec3 coords = lut_coords_bsdf(cos_theta, roughness, ior);
vec2 btdf_brdf = utility_tx_sample_bsdf_lut(utility_tx, coords.xy, coords.z).rg;
float reflectance = F_brdf_single_scatter(vec3(F0), vec3(F90), split_sum).r;
float transmittance = (1.0 - F0) * transmission_factor;
if (do_multiscatter != 0.0) {
/* For energy-conserving BSDF the reflection and refraction lobes should sum to one. Assuming
* the energy loss of single-scattering is distributed proportionally in the two lobes. */
btdf_brdf /= (btdf_brdf.x + btdf_brdf.y);
if (do_multiscatter) {
float Ess = F0 * split_sum.x + split_sum.y + (1.0 - F0) * transmission_factor;
/* TODO: maybe add saturation for higher roughness similar as in `F_brdf_multi_scatter()`.
* However, it is not necessarily desirable that the users see a different color than they
* picked. */
float scale = 1.0 / Ess;
reflectance *= scale;
transmittance *= scale;
}
return btdf_brdf;
return vec2(reflectance, transmittance);
#else
return vec2(0.0);
#endif

View File

@ -1449,13 +1449,16 @@ static void test_eevee_lut_gen()
/* Check if LUT generation matches the header version. */
auto brdf_ggx_gen = Precompute(manager, LUT_GGX_BRDF_SPLIT_SUM, {64, 64, 1}).data<float2>();
auto bsdf_ggx_gen = Precompute(manager, LUT_GGX_BSDF_SPLIT_SUM, {64, 64, 16}).data<float2>();
auto btdf_ggx_gen = Precompute(manager, LUT_GGX_BTDF_IOR_GT_ONE, {64, 64, 16}).data<float1>();
auto bsdf_ggx_gen = Precompute(manager, LUT_GGX_BSDF_SPLIT_SUM, {64, 64, 16}).data<float3>();
Span<float2> brdf_ggx_lut((const float2 *)&eevee::lut::brdf_ggx, 64 * 64);
Span<float2> bsdf_ggx_lut((const float2 *)&eevee::lut::bsdf_ggx, 64 * 64 * 16);
Span<float1> btdf_ggx_lut((const float1 *)&eevee::lut::btdf_ggx, 64 * 64 * 16);
Span<float3> bsdf_ggx_lut((const float3 *)&eevee::lut::bsdf_ggx, 64 * 64 * 16);
EXPECT_NEAR_ARRAY_ND(brdf_ggx_lut.data(), brdf_ggx_gen.data(), brdf_ggx_gen.size(), 2, 1e-4f);
EXPECT_NEAR_ARRAY_ND(bsdf_ggx_lut.data(), bsdf_ggx_gen.data(), bsdf_ggx_gen.size(), 2, 1e-4f);
EXPECT_NEAR_ARRAY_ND(btdf_ggx_lut.data(), btdf_ggx_gen.data(), btdf_ggx_gen.size(), 1, 1e-4f);
EXPECT_NEAR_ARRAY_ND(bsdf_ggx_lut.data(), bsdf_ggx_gen.data(), bsdf_ggx_gen.size(), 3, 1e-4f);
GPU_render_end();
}

View File

@ -17,13 +17,13 @@ void node_bsdf_glass(vec4 color,
vec2 bsdf = bsdf_lut(NV, roughness, ior, do_multiscatter);
ClosureReflection reflection_data;
reflection_data.weight = bsdf.y * weight;
reflection_data.weight = bsdf.x * weight;
reflection_data.color = color.rgb;
reflection_data.N = N;
reflection_data.roughness = roughness;
ClosureRefraction refraction_data;
refraction_data.weight = bsdf.x * weight;
refraction_data.weight = bsdf.y * weight;
refraction_data.color = color.rgb;
refraction_data.N = N;
refraction_data.roughness = roughness;

View File

@ -84,7 +84,7 @@ void node_bsdf_principled(vec4 base_color,
coat_data.N = CN;
coat_data.roughness = coat_roughness;
float coat_NV = dot(coat_data.N, V);
float reflectance = bsdf_lut(coat_NV, coat_data.roughness, coat_ior, 0.0).y;
float reflectance = bsdf_lut(coat_NV, coat_data.roughness, coat_ior, 0.0).x;
coat_data.weight = weight * coat * reflectance;
coat_data.color = vec3(1.0);
/* Attenuate lower layers */
@ -127,9 +127,9 @@ void node_bsdf_principled(vec4 base_color,
if (true) {
vec2 bsdf = bsdf_lut(NV, roughness, ior, do_multiscatter);
reflection_data.color += weight * transmission * bsdf.y * reflection_tint;
reflection_data.color += weight * transmission * bsdf.x * reflection_tint;
refraction_data.weight = weight * transmission * bsdf.x;
refraction_data.weight = weight * transmission * bsdf.y;
refraction_data.color = base_color.rgb * coat_tint.rgb;
refraction_data.N = N;
refraction_data.roughness = roughness;