|
| 1 | +export const multiscatter_functions = /* glsl */` |
| 2 | +
|
| 3 | +// Multiscattering energy compensation for GGX microfacet BRDF |
| 4 | +// Based on Kulla & Conty 2017 - "Revisiting Physically Based Shading at Imageworks" |
| 5 | +// https://blog.selfshadow.com/publications/s2017-shading-course/imageworks/s2017_pbs_imageworks_slides_v2.pdf |
| 6 | +
|
| 7 | +// Computes the directional albedo E(mu, alpha) for GGX |
| 8 | +// This represents the total energy reflected for a given view angle and roughness |
| 9 | +// mu = cos(theta), alpha = roughness^2 |
| 10 | +vec3 ggxDirectionalAlbedo( float mu, float alpha, vec3 F0 ) { |
| 11 | +
|
| 12 | + // Analytical approximation from Lazanyi & Szirmay-Kalos 2005 |
| 13 | + // Refined by Fdez-Aguera 2018 |
| 14 | + // This approximates the integral of the GGX BRDF over the hemisphere |
| 15 | +
|
| 16 | + float a = alpha * alpha; |
| 17 | + float a2 = a * a; |
| 18 | +
|
| 19 | + // Compute the directional albedo for dielectric (F0=0.04) case |
| 20 | + // This is an analytical fit to pre-integrated tables |
| 21 | + float cosTheta = clamp( mu, 0.0, 1.0 ); |
| 22 | + vec4 X = vec4( 1.0, cosTheta, cosTheta * cosTheta, cosTheta * cosTheta * cosTheta ); |
| 23 | + vec4 Y = vec4( 1.0, a, a2, a2 * a ); |
| 24 | +
|
| 25 | + // Polynomial approximation |
| 26 | + vec2 AB = vec2( |
| 27 | + dot( X, vec4( 0.0, 0.0, 0.2121, 1.0 - 0.2121 ) * Y.x + |
| 28 | + vec4( 0.0, 1.0, -1.0, 0.0 ) * Y.y + |
| 29 | + vec4( 1.0, -1.0, 0.0, 0.0 ) * Y.z ), |
| 30 | + dot( X, vec4( 0.0, 0.0, 0.0, 0.0 ) * Y.x + |
| 31 | + vec4( 0.0, 0.0, 0.0, 0.0 ) * Y.y + |
| 32 | + vec4( 0.0, -1.0, 1.0, 0.0 ) * Y.z ) |
| 33 | + ); |
| 34 | +
|
| 35 | + // Better analytical fit from Kulla & Conty supplemental material |
| 36 | + // E(mu) ≈ r0 + (r90 - r0) * (1 - mu)^5 for each roughness |
| 37 | + // We use a simpler direct computation here |
| 38 | +
|
| 39 | + float c = 1.0 - cosTheta; |
| 40 | + float c3 = c * c * c; |
| 41 | + float c5 = c3 * c * c; |
| 42 | +
|
| 43 | + // Approximate E for metallic/dielectric blending |
| 44 | + float Ess = 1.0 - c5; |
| 45 | + Ess = Ess - a * ( Ess - ( 1.0 - c3 ) ); |
| 46 | + Ess = Ess - a2 * 0.5 * ( Ess - ( 1.0 - c ) ); |
| 47 | +
|
| 48 | + // Fresnel adjustment - interpolate between dielectric and perfect mirror |
| 49 | + vec3 Eavg = F0 + ( vec3( 1.0 ) - F0 ) * Ess; |
| 50 | +
|
| 51 | + return Eavg; |
| 52 | +
|
| 53 | +} |
| 54 | +
|
| 55 | +// Computes the average albedo E_avg(alpha) for GGX |
| 56 | +// This is the hemispherical-hemispherical reflectance |
| 57 | +float ggxAverageAlbedo( float alpha ) { |
| 58 | +
|
| 59 | + // Analytical approximation of the average directional albedo |
| 60 | + // This represents the average energy reflected across all view angles |
| 61 | +
|
| 62 | + float a = alpha * alpha; |
| 63 | +
|
| 64 | + // Simple analytical fit from multiple importance sampling integration |
| 65 | + // For GGX, this decreases with roughness |
| 66 | + float Eavg = 1.0 - 0.0275 * a / ( 1.0 + 0.4265 * a ); |
| 67 | + Eavg = 1.0 - 0.3607 * a / ( 1.0 + 0.6487 * a ); |
| 68 | +
|
| 69 | + // Alternative: more accurate fit from Kulla & Conty |
| 70 | + // Uses polynomial approximation |
| 71 | + float a2 = a * a; |
| 72 | + Eavg = 1.0 + a * ( -0.0428 + a * ( -0.2952 + a * 0.3375 ) ); |
| 73 | + Eavg = max( 0.0, Eavg ); |
| 74 | +
|
| 75 | + return Eavg; |
| 76 | +
|
| 77 | +} |
| 78 | +
|
| 79 | +// Computes the multiscatter contribution color |
| 80 | +// wo = outgoing direction (view), wi = incoming direction (light) |
| 81 | +// Returns the additional energy that should be added to compensate for multiple scattering |
| 82 | +vec3 ggxMultiScatterCompensation( vec3 wo, vec3 wi, float alpha, vec3 F0 ) { |
| 83 | +
|
| 84 | + float mu_o = abs( wo.z ); |
| 85 | + float mu_i = abs( wi.z ); |
| 86 | +
|
| 87 | + // Compute directional albedos for both directions |
| 88 | + vec3 Eo = ggxDirectionalAlbedo( mu_o, alpha, F0 ); |
| 89 | + vec3 Ei = ggxDirectionalAlbedo( mu_i, alpha, F0 ); |
| 90 | +
|
| 91 | + // Compute average albedo |
| 92 | + // For colored F0 (metals), we use the average of the color |
| 93 | + float avgF0 = ( F0.r + F0.g + F0.b ) / 3.0; |
| 94 | + float Eavg = ggxAverageAlbedo( alpha ); |
| 95 | +
|
| 96 | + // Kulla-Conty multiscatter formula: |
| 97 | + // f_ms = (1 - Eo) * (1 - Ei) / (PI * (1 - Eavg)) |
| 98 | + // This redistributes the missing energy as a diffuse-like lobe |
| 99 | +
|
| 100 | + vec3 numerator = ( vec3( 1.0 ) - Eo ) * ( vec3( 1.0 ) - Ei ); |
| 101 | + float denominator = PI * max( 1.0 - Eavg, 0.001 ); // Prevent division by zero |
| 102 | +
|
| 103 | + vec3 fms = numerator / denominator; |
| 104 | +
|
| 105 | + // The multiscatter compensation uses the base reflectance color |
| 106 | + // For metals, this preserves the colored reflection |
| 107 | + // For dielectrics, this is nearly white (F0 ≈ 0.04) |
| 108 | +
|
| 109 | + // The average Fresnel for the multiscatter term |
| 110 | + // This accounts for the colored reflection of metals |
| 111 | + vec3 Favg = F0 + ( vec3( 1.0 ) - F0 ) / 21.0; // Analytical average for Fresnel |
| 112 | +
|
| 113 | + return fms * Favg; |
| 114 | +
|
| 115 | +} |
| 116 | +
|
| 117 | +// Alternative: Single function that returns both single-scatter and multi-scatter |
| 118 | +// This can be more efficient as it reuses calculations |
| 119 | +void ggxEvalWithMultiScatter( |
| 120 | + vec3 wo, |
| 121 | + vec3 wi, |
| 122 | + float alpha, |
| 123 | + vec3 F0, |
| 124 | + float D, |
| 125 | + float G, |
| 126 | + vec3 F, |
| 127 | + out vec3 singleScatter, |
| 128 | + out vec3 multiScatter |
| 129 | +) { |
| 130 | +
|
| 131 | + // Single scatter term (standard Cook-Torrance) |
| 132 | + float denom = 4.0 * abs( wo.z * wi.z ); |
| 133 | + singleScatter = F * G * D / max( denom, 0.001 ); |
| 134 | +
|
| 135 | + // Multi scatter compensation |
| 136 | + multiScatter = ggxMultiScatterCompensation( wo, wi, alpha, F0 ); |
| 137 | +
|
| 138 | +} |
| 139 | +
|
| 140 | +// Simplified version: Returns just the energy compensation factor |
| 141 | +// Can be used to scale existing single-scatter results |
| 142 | +float ggxEnergyCompensation( float mu, float alpha ) { |
| 143 | +
|
| 144 | + // Returns a factor >= 1.0 that compensates for energy loss |
| 145 | + // Multiply your existing BRDF by this factor |
| 146 | +
|
| 147 | + vec3 F0 = vec3( 0.04 ); // Assume dielectric for this calculation |
| 148 | + vec3 E = ggxDirectionalAlbedo( mu, alpha, F0 ); |
| 149 | + float Eavg_scalar = ( E.r + E.g + E.b ) / 3.0; |
| 150 | +
|
| 151 | + float Eavg = ggxAverageAlbedo( alpha ); |
| 152 | +
|
| 153 | + // The compensation factor accounts for missing energy |
| 154 | + // At grazing angles and high roughness, this can be significant |
| 155 | + float compensation = 1.0 / max( Eavg, 0.001 ); |
| 156 | +
|
| 157 | + return compensation; |
| 158 | +
|
| 159 | +} |
| 160 | +
|
| 161 | +`; |
0 commit comments