struct SurfaceState { // inputs vec4 p; // position world vec3 a; // albedo color, normalized (0-1 in R^3) float m; // metallic, 0-1 linear (usually binary) float r; // user roughness, 0-1 linear float ao; // ambient occlusion (from both viewspace and any maps) vec3 n; // surface normal, normalized, worldspace vec3 v; // surface-to-view, normalized, float spec; // specular multiplier // precomputed vec3 F0; // base reflectance at normal incidence vec3 d; // precomputed diffuse: (1-m) * a float nv; // ndotv float r2; // r^2 (aka α in Unreal/Disney terminology) float r4; // r^4 (aka α^2 in Unreal/Disney terminology) }; struct LightState { vec3 l; // surf-to-light, normalized, worldspace vec3 lc; // light color, HDR normalized (0-1 in R^3) float ls; // light strength, introduce physical units later vec3 h; // half-way vector between ln, normalized, worlspace float nl; // ndotl float nh; // ndoth float vh; // vdoth }; SurfaceState GetSurfaceState(vec3 a, float m, float r, vec3 n, vec3 v, vec4 p, float ao) { SurfaceState state; state.p = p; state.a = a; state.m = m; state.r = r; state.ao = ao; state.spec = 1.0; state.n = normalize(n); state.v = normalize(v); // precompute state.F0 = mix(vec3(0.04), a, m); state.d = a * (1.0 - m); state.nv = max(EPS, dot(v, n)); state.r2 = r * r; state.r4 = state.r2 * state.r2; return state; } LightState GetLightState(SurfaceState surf, vec3 l, vec3 lc, float ls) { LightState state; state.l = normalize(l); state.lc = lc; state.ls = ls; state.h = normalize(l + surf.v); state.nl = max(EPS, dot(l, surf.n)); state.nh = max(EPS, dot(state.h, surf.n)); state.vh = max(EPS, dot(state.h, surf.v)); return state; } // lambert diffuse BRDF, normalized via 1/π vec3 DirectDiffuseBRDF(SurfaceState surf, LightState light) { return surf.d * light.lc * light.ls * light.nl * ONE_OVER_PI * surf.ao; } // Cook-Torrance D Term: GGX/Trowbridge-Reitz float D_GGX_Trowbridge_Reitz(float r4, float nh) { float D_inner = ((nh*nh)*(r4 - 1.0) + 1.0); float D_denom = PI * D_inner * D_inner; return r4 / max(D_denom, EPS); } // Cook-Torrance G Term: Schlick, modified to match Smith at α=1 via (r+1)/2 (DONT use this modification for IBL lights!) float G_Schlick(float r, float nl, float nv) { float k = ((r+1.0)*(r+1.0)) * 0.125; float G_l = nl / max(EPS, (nl * (1.0-k) + k)); float G_v = nv / max(EPS, (nv * (1.0-k) + k)); return G_l * G_v; } // Cook-Torrance G term: Proper Smith, for IBL float G_Smith(float r2, float nl, float nv) { float r4 = r2 * r2; float lambdaV = nl * sqrt(r4 + (1.0 - r4) * nv * nv); float lambdaL = nv * sqrt(r4 + (1.0 - r4) * nl * nl); return 2.0 * nv * nl / max(EPS, lambdaV + lambdaL); } // Cook-Torrance F Term: Schlick’s approx., with spherical gaussian approximation vec3 F_Schlick_SG(vec3 F0, float vh) { return F0 + (1.0 - F0) * exp2((-5.55473 * vh -6.98316) * vh); } vec3 CookTorrance(SurfaceState surf, float nl, float D, float G, vec3 F) { return (F*D*G) / max(EPS, (4.0 * nl * surf.nv)); } // Cook-Torrance specular, using D=GGX/Trowbridge-Reitz, G=Schlick-Smith, F=Schlick SG vec3 DirectSpecularBRDF(SurfaceState surf, LightState light) { // calculate D float D = D_GGX_Trowbridge_Reitz(surf.r4, light.nh); // calculate G float G = G_Schlick(surf.r, light.nl, surf.nv); // calculate F vec3 F = F_Schlick_SG(surf.F0, light.vh); if(dot(surf.n, surf.v) < 0.0) return vec3(0.0); // prevents hard specular cutoffs, encourages cinematic backlit fresnel, looks good but not physically correct float term = smoothstep(-0.2, 0.0, dot(surf.n, light.l)); return term * light.lc * CookTorrance(surf, light.nl, D, G, F) * light.ls * surf.ao * surf.spec; } vec3 DirectLighting(SurfaceState surf, LightState light) { vec3 diffuse = DirectDiffuseBRDF(surf, light); vec3 specular = DirectSpecularBRDF(surf, light); return diffuse + specular; } vec2 sampleIntegratedBrdf(sampler2D brdf, float r, float nv) { return texture(brdf, vec2(r, nv)).xy; } vec3 sampleRadiance(samplerCube rad, vec3 n, float r) { return textureLod(rad, n, r * 8.0).rgb; } vec3 sampleIrradiance(samplerCube irr, vec3 n) { return texture(irr, n).rgb; } vec3 ImageBasedSpecular(sampler2D brdftex, samplerCube rad, SurfaceState surf) { vec3 refl = reflect(-surf.v, surf.n); vec2 brdf = sampleIntegratedBrdf(brdftex, surf.r, surf.nv); vec3 prefiltered = sampleRadiance(rad, refl, surf.r); return prefiltered * (surf.F0 * brdf.x + brdf.y) * surf.ao; } vec3 ImageBasedDiffuse(samplerCube irrtex, SurfaceState surf) { vec3 irr = sampleIrradiance(irrtex, surf.n); return surf.d * irr * ONE_OVER_PI * surf.ao; } vec3 ImageLighting(sampler2D brdftex, samplerCube irr, samplerCube rad, SurfaceState surf) { return ImageBasedDiffuse(irr, surf) + ImageBasedSpecular(brdftex, rad, surf); }