Files
test2/intern/cycles/kernel/closure/bsdf_util.h
Lukas Stockner 888bdc1419 Cycles: Remove MultiGGX code, replace with albedo scaling
While the multiscattering GGX code is cool and solves the darkening problem at higher roughnesses, it's also currently buggy, hard to maintain and often impractical to use due to the higher noise and render time.

In practice, though, having the exact correct directional distribution is not that important as long as the overall albedo is correct and we a) don't get the darkening effect and b) do get the saturation effect at higher roughnesses.

This can simply be achieved by adding a second lobe (https://blog.selfshadow.com/publications/s2017-shading-course/imageworks/s2017_pbs_imageworks_slides_v2.pdf) or scaling the single-scattering GGX lobe (https://blog.selfshadow.com/publications/turquin/ms_comp_final.pdf). Both approaches require the same precomputation and produce outputs of comparable quality, so I went for the simple albedo scaling since it's easier to implement and more efficient.

Overall, the results are pretty good: All scenarios that I tested (Glossy BSDF, Glass BSDF, Principled BSDF with metallic or transmissive = 1) pass the white furnace test (a material with pure-white color in front of a pure-white background should be indistinguishable from the background if it preserves energy), and the overall albedo for non-white materials matches that produced by the real multi-scattering code (with the expected saturation increase as the roughness increases).

In order to produce the precomputed tables, the PR also includes a utility that computes them. This is not built by default, since there's no reason for a user to run it (it only makes sense for documentation/reproducibility purposes and when making changes to the microfacet models).

Pull Request: https://projects.blender.org/blender/blender/pulls/107958
2023-06-05 02:20:57 +02:00

202 lines
6.9 KiB
C

/* SPDX-License-Identifier: BSD-3-Clause
*
* Adapted from Open Shading Language
* Copyright (c) 2009-2010 Sony Pictures Imageworks Inc., et al.
* All Rights Reserved.
*
* Modifications Copyright 2011-2022 Blender Foundation. */
#pragma once
CCL_NAMESPACE_BEGIN
ccl_device float fresnel_dielectric(
float eta, const float3 N, const float3 I, ccl_private float3 *T, ccl_private bool *is_inside)
{
float cos = dot(N, I), neta;
float3 Nn;
// check which side of the surface we are on
if (cos > 0) {
// we are on the outside of the surface, going in
neta = 1 / eta;
Nn = N;
*is_inside = false;
}
else {
// we are inside the surface
cos = -cos;
neta = eta;
Nn = -N;
*is_inside = true;
}
float arg = 1 - (neta * neta * (1 - (cos * cos)));
if (arg < 0) {
*T = make_float3(0.0f, 0.0f, 0.0f);
return 1; // total internal reflection
}
else {
float dnp = max(sqrtf(arg), 1e-7f);
float nK = (neta * cos) - dnp;
*T = -(neta * I) + (nK * Nn);
// compute Fresnel terms
float cosTheta1 = cos; // N.R
float cosTheta2 = -dot(Nn, *T);
float pPara = (cosTheta1 - eta * cosTheta2) / (cosTheta1 + eta * cosTheta2);
float pPerp = (eta * cosTheta1 - cosTheta2) / (eta * cosTheta1 + cosTheta2);
return 0.5f * (pPara * pPara + pPerp * pPerp);
}
}
ccl_device float fresnel_dielectric_cos(float cosi, float eta)
{
// compute fresnel reflectance without explicitly computing
// the refracted direction
float c = fabsf(cosi);
float g = eta * eta - 1 + c * c;
if (g > 0) {
g = sqrtf(g);
float A = (g - c) / (g + c);
float B = (c * (g + c) - 1) / (c * (g - c) + 1);
return 0.5f * A * A * (1 + B * B);
}
return 1.0f; // TIR(no refracted component)
}
ccl_device Spectrum fresnel_conductor(float cosi, const Spectrum eta, const Spectrum k)
{
Spectrum cosi2 = make_spectrum(cosi * cosi);
Spectrum one = make_spectrum(1.0f);
Spectrum tmp_f = eta * eta + k * k;
Spectrum tmp = tmp_f * cosi2;
Spectrum Rparl2 = (tmp - (2.0f * eta * cosi) + one) / (tmp + (2.0f * eta * cosi) + one);
Spectrum Rperp2 = (tmp_f - (2.0f * eta * cosi) + cosi2) / (tmp_f + (2.0f * eta * cosi) + cosi2);
return (Rparl2 + Rperp2) * 0.5f;
}
ccl_device float ior_from_F0(float f0)
{
const float sqrt_f0 = sqrtf(clamp(f0, 0.0f, 0.99f));
return (1.0f + sqrt_f0) / (1.0f - sqrt_f0);
}
ccl_device float F0_from_ior(float ior)
{
return sqr((ior - 1.0f) / (ior + 1.0f));
}
ccl_device float schlick_fresnel(float u)
{
float m = clamp(1.0f - u, 0.0f, 1.0f);
float m2 = m * m;
return m2 * m2 * m; // pow(m, 5)
}
/* Calculate the fresnel color, which is a blend between white and the F0 color */
ccl_device_forceinline Spectrum interpolate_fresnel_color(float3 L,
float3 H,
float ior,
Spectrum F0)
{
/* Compute the real Fresnel term and remap it from real_F0..1 to F0..1.
* The reason why we use this remapping instead of directly doing the
* Schlick approximation mix(F0, 1.0, (1.0-cosLH)^5) is that for cases
* with similar IORs (e.g. ice in water), the relative IOR can be close
* enough to 1.0 that the Schlick approximation becomes inaccurate. */
float real_F = fresnel_dielectric_cos(dot(L, H), ior);
float real_F0 = fresnel_dielectric_cos(1.0f, ior);
return mix(F0, one_spectrum(), inverse_lerp(real_F0, 1.0f, real_F));
}
/* If the shading normal results in specular reflection in the lower hemisphere, raise the shading
* normal towards the geometry normal so that the specular reflection is just above the surface.
* Only used for glossy materials. */
ccl_device float3 ensure_valid_specular_reflection(float3 Ng, float3 I, float3 N)
{
const float3 R = 2 * dot(N, I) * N - I;
const float Iz = dot(I, Ng);
kernel_assert(Iz > 0);
/* Reflection rays may always be at least as shallow as the incoming ray. */
const float threshold = min(0.9f * Iz, 0.01f);
if (dot(Ng, R) >= threshold) {
return N;
}
/* Form coordinate system with Ng as the Z axis and N inside the X-Z-plane.
* The X axis is found by normalizing the component of N that's orthogonal to Ng.
* The Y axis isn't actually needed.
*/
const float3 X = normalize(N - dot(N, Ng) * Ng);
/* Calculate N.z and N.x in the local coordinate system.
*
* The goal of this computation is to find a N' that is rotated towards Ng just enough
* to lift R' above the threshold (here called t), therefore dot(R', Ng) = t.
*
* According to the standard reflection equation,
* this means that we want dot(2*dot(N', I)*N' - I, Ng) = t.
*
* Since the Z axis of our local coordinate system is Ng, dot(x, Ng) is just x.z, so we get
* 2*dot(N', I)*N'.z - I.z = t.
*
* The rotation is simple to express in the coordinate system we formed -
* since N lies in the X-Z-plane, we know that N' will also lie in the X-Z-plane,
* so N'.y = 0 and therefore dot(N', I) = N'.x*I.x + N'.z*I.z .
*
* Furthermore, we want N' to be normalized, so N'.x = sqrt(1 - N'.z^2).
*
* With these simplifications, we get the equation
* 2*(sqrt(1 - N'.z^2)*I.x + N'.z*I.z)*N'.z - I.z = t,
* or
* 2*sqrt(1 - N'.z^2)*I.x*N'.z = t + I.z * (1 - 2*N'.z^2),
* after rearranging terms.
* Raise both sides to the power of two and substitute terms with
* a = I.x^2 + I.z^2,
* b = 2*(a + Iz*t),
* c = (Iz + t)^2,
* we obtain
* 4*a*N'.z^4 - 2*b*N'.z^2 + c = 0.
*
* The only unknown here is N'.z, so we can solve for that.
*
* The equation has four solutions in general, two can immediately be discarded because they're
* negative so N' would lie in the lower hemisphere; one solves
* 2*sqrt(1 - N'.z^2)*I.x*N'.z = -(t + I.z * (1 - 2*N'.z^2))
* instead of the original equation (before squaring both sides).
* Therefore only one root is valid.
*/
const float Ix = dot(I, X);
const float a = sqr(Ix) + sqr(Iz);
const float b = 2.0f * (a + Iz * threshold);
const float c = sqr(threshold + Iz);
/* In order that the root formula solves 2*sqrt(1 - N'.z^2)*I.x*N'.z = t + I.z - 2*I.z*N'.z^2,
* Ix and (t + I.z * (1 - 2*N'.z^2)) must have the same sign (the rest terms are non-negative by
* definition). */
const float Nz2 = (Ix < 0) ? 0.25f * (b + safe_sqrtf(sqr(b) - 4.0f * a * c)) / a :
0.25f * (b - safe_sqrtf(sqr(b) - 4.0f * a * c)) / a;
const float Nx = safe_sqrtf(1.0f - Nz2);
const float Nz = safe_sqrtf(Nz2);
return Nx * X + Nz * Ng;
}
/* Do not call #ensure_valid_specular_reflection if the primitive type is curve or if the geometry
* normal and the shading normal is the same. */
ccl_device float3 maybe_ensure_valid_specular_reflection(ccl_private ShaderData *sd, float3 N)
{
if ((sd->type & PRIMITIVE_CURVE) || isequal(sd->Ng, N)) {
return N;
}
return ensure_valid_specular_reflection(sd->Ng, sd->wi, N);
}
CCL_NAMESPACE_END