From ee8ee0dafc6fac21a5bf3c8cbde8af0fa5a712a7 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Thu, 14 May 2026 15:35:52 +0200 Subject: [PATCH] Switch normalization from Blinn-Phong to GGX --- shaders/light_common.glsl | 78 ++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 14 deletions(-) diff --git a/shaders/light_common.glsl b/shaders/light_common.glsl index bee46be9..927229bf 100644 --- a/shaders/light_common.glsl +++ b/shaders/light_common.glsl @@ -75,25 +75,75 @@ float calc_shadow() #endif } +// ----------------------------------------------------------------------- +// GGX Microfacet BRDF helpers (Cook-Torrance) +// ----------------------------------------------------------------------- + +// Trowbridge-Reitz (GGX) Normal Distribution Function +// D(N,H,α) = α⁴ / (π · ((NdotH)²·(α⁴−1)+1)²) +// α = roughness² (perceptual remapping so the slider feels linear) +float D_GGX(float NdotH, float roughness) +{ + float a = roughness * roughness; // perceptual -> linear roughness + float a2 = a * a; + float d = (NdotH * NdotH) * (a2 - 1.0) + 1.0; + return a2 / (3.14159265359 * d * d); +} + +// Schlick-GGX single-term masking/shadowing (k remapped for direct lighting) +float G_SchlickGGX(float NdotX, float roughness) +{ + float r = roughness + 1.0; + float k = (r * r) * (1.0 / 8.0); // k_direct = (roughness+1)²/8 + return NdotX / (NdotX * (1.0 - k) + k); +} + +// Height-correlated Smith geometry term +// G(N,V,L) = G_SchlickGGX(NdotV) · G_SchlickGGX(NdotL) +float G_Smith(float NdotV, float NdotL, float roughness) +{ + return G_SchlickGGX(NdotV, roughness) * G_SchlickGGX(NdotL, roughness); +} + +// Returns vec2(diffuse, specular) for a single punctual light. +// +// diffuse – Lambert N·L (Fresnel-weighted diffuse is handled per-material +// in apply_lights, so we return raw N·L here). +// specular – Cook-Torrance GGX: D·G / (4·NdotL·NdotV). +// The Fresnel factor (F) is intentionally omitted here; +// apply_lights already carries a per-material Fresnel term +// that is applied to env reflections and can be routed to +// direct specular there. +// +// Roughness is derived identically to env_roughness in apply_lights so +// that direct and indirect specular highlights read consistently. vec2 calc_light(vec3 light_dir, vec3 fragnormal) { - vec3 view_dir = normalize(vec3(0.0f, 0.0f, 0.0f) - f_pos.xyz); - vec3 halfway_dir = normalize(light_dir + view_dir); + vec3 N = fragnormal; + vec3 L = light_dir; + vec3 V = normalize(-f_pos.xyz); + vec3 H = normalize(L + V); - float diffuse_v = max(dot(fragnormal, light_dir), 0.0); + float NdotL = max(dot(N, L), 0.0); + float NdotV = max(dot(N, V), 1e-4); + float NdotH = max(dot(N, H), 0.0); - // Energy-conserving Blinn-Phong normalization: - // (n+8)/(8*pi) ensures the specular lobe integrates to the same - // total energy regardless of glossiness — low glossiness stays dim - // and spreads wide (blurry), high glossiness is bright and tight (sharp). - // Capped at 4.0 so very high glossiness (n>~92) does not produce pinhole - // highlights that blow past the tonemap shoulder and read as burnt white. - float n = max(glossiness, 0.01); - float normalization = min((n + 8.0) / (8.0 * 3.14159265), 4.0); - float NdotH = max(dot(fragnormal, halfway_dir), 0.0); - float specular_v = normalization * pow(NdotH, n); + float diffuse_v = NdotL; - return vec2(diffuse_v, specular_v); + // Mirror the env-map roughness derivation so direct and indirect lobes match. + // glossiness == param[1].w → roughness == 0.04 (near-mirror) + // glossiness == 0 → roughness == 1.0 (fully diffuse) + float roughness = clamp(1.0 - glossiness / max(abs(param[1].w), 1.0), 0.04, 1.0); + + // Cook-Torrance specular (no Fresnel — see above): + // f_spec = D(N,H,α) · G(N,V,L,α) / (4 · NdotL · NdotV) + float D = D_GGX(NdotH, roughness); + float G = G_Smith(NdotV, NdotL, roughness); + float specular_v = (NdotL > 0.0) + ? (D * G) / max(4.0 * NdotL * NdotV, 1e-4) + : 0.0; + + return vec2(diffuse_v, specular_v); } vec2 calc_point_light(light_s light, vec3 fragnormal)