xash3d-fwgs/ref/vk/shaders/brdf.glsl

378 lines
12 KiB
GLSL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#ifndef BRDF_GLSL_INCLUDED
#define BRDF_GLSL_INCLUDED
#include "debug.glsl"
// TODO math|common.glsl
const float kPi = 3.1415926;
const float kOneOverPi = 1. / kPi;
//#define BRDF_COMPARE
#ifdef BRDF_COMPARE
bool g_mat_gltf2 = true;
#endif
#ifdef BRDF_COMPARE
#include "brdf.h"
#else
struct MaterialProperties {
vec3 base_color;
//vec3 normal;
float metalness;
float roughness;
};
#endif
float ggxD(float a2, float h_dot_n) {
if (h_dot_n <= 0.)
return 0.;
// Need to make alpha^2 non-zero to make sure that smooth surfaces get at least some specular reflections
// Otherwise it will just multiply by zero.
// This also helps with limiting denom to 1e-5
a2 = max(1e-5, a2);
// Limit in case H is "random" due to L~=-V
h_dot_n = clamp(h_dot_n, 1e-5, 1.);
const float denom = h_dot_n * h_dot_n * (a2 - 1.) + 1.;
return a2 * kOneOverPi / (denom * denom);
}
float ggxG(float a2, float l_dot_n, float h_dot_l, float n_dot_v, float h_dot_v) {
if (h_dot_l <= 0. || h_dot_v <= 0.)
return 0.;
//l_dot_n = max(1e-4, l_dot_n);
n_dot_v = max(1e-4, n_dot_v);
const float denom1 = abs(l_dot_n) + sqrt(a2 + (1. - a2) * l_dot_n * l_dot_n);
const float denom2 = abs(n_dot_v) + sqrt(a2 + (1. - a2) * n_dot_v * n_dot_v);
return 4. * abs(l_dot_n) * abs(n_dot_v) / (denom1 * denom2);
}
float ggxV(float a2, float l_dot_n, float h_dot_l, float n_dot_v, float h_dot_v) {
if (h_dot_l <= 0. || h_dot_v <= 0.)
return 0.;
//l_dot_n = max(1e-4, l_dot_n);
n_dot_v = max(1e-4, n_dot_v);
const float denom1 = abs(l_dot_n) + sqrt(a2 + (1. - a2) * l_dot_n * l_dot_n);
const float denom2 = abs(n_dot_v) + sqrt(a2 + (1. - a2) * n_dot_v * n_dot_v);
return 1. / (denom1 * denom2);
}
// glTF 2.0 BRDF sample implementation
// https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#appendix-b-brdf-implementation
// TODO oneMinusVdotH5
//vec3 gltf2_conductor_fresnel(vec3 f0, vec3 bsdf, float VdotH) {
// return bsdf * (f0 + (1. - f0) * pow(1. - abs(VdotH), 5.));
//}
//
//vec3 fresnel_mix(float ior, vec3 base, vec3 layer, float VdotH) {
// const float f0 = pow((1.-ior)/(1.+ior), 2.);
// const float fr = f0 + (1. - f0) * pow(1. - abs(VdotH), 5.);
// return mix(base, layer, fr)
//}
void brdfComputeGltfModel(vec3 N, vec3 L, vec3 V, MaterialProperties material, out float l_dot_n, out vec3 out_diffuse, out vec3 out_specular) {
// L facing away from N can happen fairly often, exit early if so.
l_dot_n = max(0., dot(L, N));
if (l_dot_n == 0.) {
out_diffuse = vec3(0.);
out_specular = vec3(0.);
return;
}
const float alpha = material.roughness * material.roughness;
const float a2 = alpha * alpha;
// If L ~= -V, then H will be roughly random, maybe mostly orthogonal to both. See ~03:00:00 E360
// If L == V, then H will be NaN.
const vec3 H = normalize(L + V);
const float h_dot_n = dot(H, N);
const float n_dot_v = dot(N, V);
const float h_dot_v = max(0., dot(H, V));
const float h_dot_l = max(0., dot(H, L));
/*
#ifdef DEBUG_VALIDATE_EXTRA
if (IS_INVALID(h_dot_v) || h_dot_v < 0.) {
debugPrintfEXT("INVALID h_dot_v=%f, L=(%f,%f,%f) V=(%f,%f,%f) N=(%f, %f, %f)",
h_dot_v, PRIVEC3(L), PRIVEC3(V), PRIVEC3(N));
}
if (IS_INVALID(h_dot_l) || h_dot_l < 0.) {
debugPrintfEXT("INVALID h_dot_l=%f, L=(%f,%f,%f) V=(%f,%f,%f) N=(%f, %f, %f)",
h_dot_l, PRIVEC3(L), PRIVEC3(V), PRIVEC3(N));
}
#endif
*/
/* Original for when the color is known already.
const vec3 diffuse_color = mix(material.base_color, vec3(0.), material.metalness);
const vec3 f0 = mix(vec3(.04), material.base_color, material.metalness);
const vec3 fresnel = f0 + (vec3(1.) - f0) * pow(1. - abs(h_dot_v), 5.);
*/
// Use white for diffuse color here, as compositing happens way later in denoiser/smesitel
const vec3 diffuse_color = mix(vec3(1.), vec3(0.), material.metalness);
// Specular does get the real color, as its contribution is light-direction-dependent
const vec3 f0 = mix(vec3(.04), material.base_color, material.metalness);
float fresnel_factor = max(0., pow(1. - abs(h_dot_v), 5.));
const vec3 fresnel = vec3(1.) * fresnel_factor + f0 * (1. - fresnel_factor);
// This is taken directly from glTF 2.0 spec. It seems incorrect to me: it should not include the base_color twice.
// Note: here diffuse_color doesn't include base_color as it is mixed later, but we can clearly see that
// base_color is still visible in the direct_diff term, which it shouldn't be. See E357 ~37:00
// TODO make a PR against glTF spec with the correct derivation.
//out_diffuse = (vec3(1.) - fresnel) * kOneOverPi * diffuse_color * l_dot_n;
// This is the correctly derived diffuse term that doesn't include the base_color twice
out_diffuse = diffuse_color * kOneOverPi * .96 * (1. - fresnel_factor);
float ggxd = ggxD(a2, h_dot_n);
#ifdef DEBUG_VALIDATE_EXTRA
if (IS_INVALID(ggxd) || ggxd < 0. /* || ggxd > 1.*/) {
debugPrintfEXT("N=(%f,%f,%f) L=(%f,%f,%f) V=(%f,%f,%f) a2=%f h_dot_n=%f INVALID ggxd=%f",
PRIVEC3(N), PRIVEC3(L), PRIVEC3(V), a2, h_dot_n, ggxd);
ggxd = 0.;
}
#endif
float ggxv = ggxV(a2, l_dot_n, h_dot_l, n_dot_v, h_dot_v);
#ifdef DEBUG_VALIDATE_EXTRA
if (IS_INVALID(ggxv) || ggxv < 0. /*|| ggxv > 1.*/) {
debugPrintfEXT("N=(%f,%f,%f) L=(%f,%f,%f) V=(%f,%f,%f) a2=%f h_dot_n=%f INVALID ggxv=%f",
PRIVEC3(N), PRIVEC3(L), PRIVEC3(V), a2, h_dot_n, ggxv);
ggxv = 0.;
}
#endif
out_specular = fresnel * ggxd * ggxv;
}
void evalSplitBRDF(vec3 N, vec3 L, vec3 V, MaterialProperties material, out vec3 out_diffuse, out vec3 out_specular) {
out_diffuse = vec3(0.);
out_specular = vec3(0.);
/* glTF 2.0 BRDF mixing model
https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#appendix-b-brdf-implementation
material = mix(dielectric_brdf, metal_brdf, metallic)
= (1.0 - metallic) * dielectric_brdf + metallic * metal_brdf
metal_brdf =
conductor_fresnel(
f0 = baseColor,
bsdf = specular_brdf(α = roughness ^ 2))
dielectric_brdf =
fresnel_mix(
ior = 1.5,
base = diffuse_brdf(color = baseColor),
layer = specular_brdf(α = roughness ^ 2))
*/
/* glTF 2.0 BRDF sample implementation
https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#appendix-b-brdf-implementation
metal_brdf = specular_brdf(roughness^2) * (baseColor.rgb + (1 - baseColor.rgb) * (1 - abs(VdotH))^5)
dielectric_brdf = mix(diffuse_brdf(baseColor.rgb), specular_brdf(roughness^2), 0.04 + (1 - 0.04) * (1 - abs(VdotH))^5)
*/
/* suggested simplification
const black = 0
c_diff = lerp(baseColor.rgb, black, metallic)
f0 = lerp(0.04, baseColor.rgb, metallic)
α = roughness^2
F = f0 + (1 - f0) * (1 - abs(VdotH))^5
f_diffuse = (1 - F) * (1 / π) * c_diff
f_specular = F * D(α) * G(α) / (4 * abs(VdotN) * abs(LdotN))
material = f_diffuse + f_specular
*/
#ifdef BRDF_COMPARE
if (g_mat_gltf2) {
#endif
float l_dot_n;
brdfComputeGltfModel(N, L, V, material, l_dot_n, out_diffuse, out_specular);
out_diffuse *= l_dot_n;
out_specular *= l_dot_n;
#ifdef BRDF_COMPARE
} else {
// Prepare data needed for BRDF evaluation - unpack material properties and evaluate commonly used terms (e.g. Fresnel, NdotL, ...)
BrdfData data = prepareBRDFData(N, L, V, material);
// Ignore V and L rays "below" the hemisphere
//if (data.Vbackfacing || data.Lbackfacing) return vec3(0.0f, 0.0f, 0.0f);
// Eval specular and diffuse BRDFs
out_specular = evalSpecular(data);
// Our renderer mixes base_color into diffuse component later in denoiser/smesitel
data.diffuseReflectance = baseColorToDiffuseReflectance(vec3(1.), material.metalness);
out_diffuse = evalDiffuse(data);
// Combine specular and diffuse layers
#if COMBINE_BRDFS_WITH_FRESNEL
// Specular is already multiplied by F, just attenuate diffuse
out_diffuse *= vec3(1.) - data.F;
#endif
}
#endif
}
//#define TEST_LOCAL_FRAME
#ifdef TEST_LOCAL_FRAME
#ifndef BRDF_H_INCLUDED
// brdf.h
// Calculates rotation quaternion from input vector to the vector (0, 0, 1)
// Input vector must be normalized!
vec4 getRotationToZAxis(vec3 v) {
// Handle special case when input is exact or near opposite of (0, 0, 1)
if (v.z < -0.99999f) return vec4(1.0f, 0.0f, 0.0f, 0.0f);
return normalize(vec4(v.y, -v.x, 0.0f, 1.0f + v.z));
}
// Returns the quaternion with inverted rotation
vec4 invertRotation(vec4 q)
{
return vec4(-q.x, -q.y, -q.z, q.w);
}
// Optimized point rotation using quaternion
// Source: https://gamedev.stackexchange.com/questions/28395/rotating-vector3-by-a-quaternion
vec3 rotatePoint(vec4 q, vec3 v) {
const vec3 qAxis = vec3(q.x, q.y, q.z);
return 2.0f * dot(qAxis, v) * qAxis + (q.w * q.w - dot(qAxis, qAxis)) * v + 2.0f * q.w * cross(qAxis, v);
}
// Samples a direction within a hemisphere oriented along +Z axis with a cosine-weighted distribution
// Source: "Sampling Transformations Zoo" in Ray Tracing Gems by Shirley et al.
vec3 sampleHemisphere(vec2 u, out float pdf) {
float a = sqrt(u.x);
float b = 2. * kPi * u.y;
vec3 result = vec3(
a * cos(b),
a * sin(b),
sqrt(1.0f - u.x));
pdf = result.z * kOneOverPi;
return result;
}
#endif // #ifndef BRDF_H_INCLUDED
vec3 sampleCosineHemisphereAroundVectorUnnormalizedLocalFrame(vec2 rnd, vec3 n) {
const vec4 qRotationToZ = getRotationToZAxis(n);
float pdf;
return rotatePoint(invertRotation(qRotationToZ), sampleHemisphere(rnd, pdf));
}
#endif // #ifdef TEST_LOCAL_FRAME
vec3 sampleCosineHemisphereAroundVectorUnnormalized(vec2 rnd, vec3 n) {
// Ray Tracing Gems, §16.6.2
const float a = 1. - 2. * rnd.x;
const float b = sqrt(1. - a * a);
const float phi = 2. * kPi * rnd.y;
// TODO const float pdf = a / kPi;
return n + vec3(b * cos(phi), b * sin(phi), a);
}
#define BRDF_TYPE_NONE 0
#define BRDF_TYPE_DIFFUSE 1
#define BRDF_TYPE_SPECULAR 2
int brdfGetSample(vec2 rnd, MaterialProperties material, vec3 view, vec3 geometry_normal, vec3 shading_normal, /*float alpha, */out vec3 out_direction, inout vec3 inout_throughput) {
#if 1
// Idiotic sampling, super noisy, bad distribution, etc etc
// But we need to start somewhere
// TODO pick diffuse-vs-specular based on expected contribution
const int brdf_type = BRDF_TYPE_DIFFUSE;// (rand01() > .5) ? BRDF_TYPE_DIFFUSE : BRDF_TYPE_SPECULAR;
if (any(isnan(geometry_normal))) {
inout_throughput = 10.*vec3(1.,0.,1.);
return brdf_type;
}
#if defined(BRDF_COMPARE) && defined(TEST_LOCAL_FRAME)
if (g_mat_gltf2) {
#endif
out_direction = sampleCosineHemisphereAroundVectorUnnormalized(rnd, shading_normal);
//out_direction = reflect(shading_normal, -view);
#if defined(BRDF_COMPARE) && defined(TEST_LOCAL_FRAME)
} else {
out_direction = sampleCosineHemisphereAroundVectorUnnormalizedLocalFrame(rnd, shading_normal);
}
#endif
if (dot(out_direction, out_direction) < 1e-5 || dot(out_direction, geometry_normal) <= 0.)
return BRDF_TYPE_NONE;
out_direction = normalize(out_direction);
vec3 diffuse = vec3(0.), specular = vec3(0.);
float l_dot_n;
brdfComputeGltfModel(shading_normal, out_direction, view, material, l_dot_n, diffuse, specular);
/*
if (any(isnan(diffuse))) {
inout_throughput = vec3(0., 1., 0.);
return brdf_type;
}
*/
inout_throughput *= (brdf_type == BRDF_TYPE_DIFFUSE) ? diffuse : specular;
const float throughput_threshold = 1e-3;
if (dot(inout_throughput, inout_throughput) < throughput_threshold)
return BRDF_TYPE_NONE;
// FIXME better sampling
return brdf_type;
#else
int brdf_type = BRDF_TYPE_DIFFUSE;
// FIXME address translucency properly
const float alpha = (base_a.a);
if (1. > alpha && rand01() > alpha) {
brdf_type = BRDF_TYPE_SPECULAR;
// TODO: when not sampling randomly: throughput *= (1. - base_a.a);
bounce_direction = normalize(refract(direction, geometry_normal, .95));
geometry_normal = -geometry_normal;
//throughput /= base_a.rgb;
} else {
if (material.metalness == 1.0f && material.roughness == 0.0f) {
// Fast path for mirrors
brdf_type = BRDF_TYPE_SPECULAR;
} else {
// Decide whether to sample diffuse or specular BRDF (based on Fresnel term)
const float brdf_probability = getBrdfProbability(material, -direction, shading_normal);
if (rand01() < brdf_probability) {
brdf_type = BRDF_TYPE_SPECULAR;
throughput /= brdf_probability;
}
}
const vec2 u = vec2(rand01(), rand01());
vec3 brdf_weight = vec3(0.);
if (!evalIndirectCombinedBRDF(u, shading_normal, geometry_normal, -direction, material, brdf_type, bounce_direction, brdf_weight))
return false;
throughput *= brdf_weight;
}
const float throughput_threshold = 1e-3;
if (dot(throughput, throughput) < throughput_threshold)
return false;
return true;
#endif
}
#endif //ifndef BRDF_GLSL_INCLUDED