#version 330 core /* * Advanced Atmospheric Dehazing Shader * ------------------------------------ * This shader simulates the behavior of Adobe Lightroom/Photoshop's Dehaze feature using: * * 1. Dark Channel Prior (DCP) technique for estimating atmospheric light and transmission * - In photography, haze appears as a low-contrast, brightened, desaturated overlay * - DCP assumes that haze-free regions have at least one RGB channel with very low intensity * - By detecting regions where all RGB channels are high, we can identify hazy areas * * 2. Multi-scale contrast enhancement with perceptual considerations * - Detail recovery is based on local contrast improvement * - Preserves color relationships similar to Lightroom by boosting both contrast and saturation * * 3. Adaptive atmospheric light estimation * - Uses a simplified version of sky/atmospheric light detection * - Approximates the global atmospheric light color (typically grayish-blue) * * 4. Tone-aware processing * - Protects highlights from clipping, similar to Lightroom's implementation * - Preserves natural shadows while improving visibility * * 5. Implements a non-linear response curve that matches the perceptual effect of Lightroom's * dehaze slider within the -100 to +100 range */ out vec4 FragColor; in vec2 TexCoord; uniform sampler2D InputTexture; uniform float dehazeValue; // -100 to 100 (negative values add haze) // Perceptual luminance weights (Rec. 709) const vec3 luminanceWeight = vec3(0.2126, 0.7152, 0.0722); float luminance(vec3 color) { return dot(color, luminanceWeight); } // Improved box blur with edge-aware weighting vec3 boxBlur(sampler2D tex, vec2 uv, vec2 texelSize, int radius) { vec3 blurred = vec3(0.0); float weightSum = 0.0; float r = float(radius); vec3 centerColor = texture(tex, uv).rgb; for (int x = -radius; x <= radius; ++x) { for (int y = -radius; y <= radius; ++y) { vec2 offset = vec2(x, y) * texelSize; vec3 sampleColor = texture(tex, uv + offset).rgb; // Edge-aware weight: lower weight for pixels that are very different float colorDist = distance(centerColor, sampleColor); float edgeWeight = exp(-colorDist * 10.0); // Spatial weight: Gaussian float spatialWeight = exp(-(float(x*x + y*y)) / (2.0 * r*r)); float weight = spatialWeight * edgeWeight; blurred += sampleColor * weight; weightSum += weight; } } return blurred / max(weightSum, 0.0001); } // Calculates dark channel (minimum of RGB channels) for a local region float getDarkChannel(sampler2D tex, vec2 uv, vec2 texelSize, int radius) { float darkChannel = 1.0; for (int x = -radius; x <= radius; ++x) { for (int y = -radius; y <= radius; ++y) { vec2 offset = vec2(x, y) * texelSize; vec3 sampleColor = texture(tex, uv + offset).rgb; float minChannel = min(min(sampleColor.r, sampleColor.g), sampleColor.b); darkChannel = min(darkChannel, minChannel); } } return darkChannel; } // Estimates atmospheric light (typically the brightest pixel in hazy regions) vec3 estimateAtmosphericLight(sampler2D tex, vec2 uv, vec2 texelSize) { // Default slightly bluish-gray haze color, similar to most atmospheric conditions vec3 defaultAtmosphericLight = vec3(0.85, 0.9, 1.0); // Find brightest area in a larger region vec3 brightest = vec3(0.0); float maxLum = 0.0; int searchRadius = 10; for (int x = -searchRadius; x <= searchRadius; x += 2) { for (int y = -searchRadius; y <= searchRadius; y += 2) { vec2 offset = vec2(x, y) * texelSize; vec3 sampleColor = texture(tex, uv + offset).rgb; float lum = luminance(sampleColor); if (lum > maxLum) { maxLum = lum; brightest = sampleColor; } } } // Blend between default and detected atmospheric light return mix(defaultAtmosphericLight, brightest, 0.7); } // Tone protection function to preserve highlights and shadows vec3 protectTones(vec3 color, vec3 enhanced, float amount) { float lum = luminance(color); // Highlight and shadow protection factors float highlightProtection = 1.0 - smoothstep(0.75, 0.98, lum); float shadowProtection = smoothstep(0.0, 0.2, lum); float protection = mix(1.0, highlightProtection * shadowProtection, min(abs(amount) * 2.0, 1.0)); return mix(color, enhanced, protection); } // Apply advanced dehazing algorithm vec3 applyDehaze(vec3 originalColor, vec2 uv, float dehaze) { // Convert dehaze from -100...100 to -1...1 float amount = dehaze / 100.0; if (abs(amount) < 0.01) return originalColor; vec2 texelSize = 1.0 / textureSize(InputTexture, 0); // --- Atmospheric Scattering Model: I = J × t + A × (1-t) --- // I is the observed hazy image, J is the dehazed image, // A is atmospheric light, t is transmission // Calculate dark channel (key to estimating haze) int darkChannelRadius = 3; float darkChannel = getDarkChannel(InputTexture, uv, texelSize, darkChannelRadius); // Estimate atmospheric light vec3 atmosphericLight = estimateAtmosphericLight(InputTexture, uv, texelSize); // Calculate transmission map using dark channel prior float omega = 0.95; // Preserves a small amount of haze for realism float transmission = 1.0 - omega * darkChannel; vec3 result; if (amount > 0.0) { // Remove haze (positive dehaze) float minTransmission = 0.1; float adjustedTransmission = max(transmission, minTransmission); adjustedTransmission = mix(1.0, adjustedTransmission, amount); // Apply dehaze formula: J = (I - A) / t + A result = (originalColor - atmosphericLight) / adjustedTransmission + atmosphericLight; // Additional local contrast enhancement vec3 localAverage = boxBlur(InputTexture, uv, texelSize, 3); vec3 localDetail = originalColor - localAverage; result = result + localDetail * (amount * 0.5); // Saturation boost (typical of dehaze) float satBoost = 1.0 + amount * 0.3; float resultLum = luminance(result); result = mix(vec3(resultLum), result, satBoost); } else { // Add haze (negative dehaze) float hazeAmount = -amount; // Blend with atmospheric light result = mix(originalColor, atmosphericLight, hazeAmount * 0.5); // Reduce contrast to simulate haze vec3 localAverage = boxBlur(InputTexture, uv, texelSize, 5); result = mix(result, localAverage, hazeAmount * 0.3); // Reduce saturation float resultLum = luminance(result); result = mix(result, vec3(resultLum), hazeAmount * 0.4); } // Protect highlights and shadows result = protectTones(originalColor, result, amount); // Non-linear response curve similar to Lightroom float blendFactor = 1.0 / (1.0 + exp(-abs(amount) * 3.0)); result = mix(originalColor, result, blendFactor); return result; } void main() { vec4 color = texture(InputTexture, TexCoord); color.rgb = applyDehaze(color.rgb, TexCoord, dehazeValue); FragColor = vec4(max(color.rgb, vec3(0.0)), color.a); }