tedit/shaders/dehaze.frag
2025-04-07 20:08:16 -04:00

198 lines
7.4 KiB
GLSL
Raw Permalink 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.

#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);
}