initial commit

This commit is contained in:
2025-04-07 20:08:16 -04:00
commit 7e7207a87e
50 changed files with 86524 additions and 0 deletions

58
shaders/clarity.frag Normal file
View File

@ -0,0 +1,58 @@
#version 330 core
float luminance(vec3 color) {
return dot(color, vec3(0.2126, 0.7152, 0.0722));
}
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D InputTexture;
uniform float clarityValue; // -100 (blur) to 100 (sharpen)
// Simple Box Blur (approximates Gaussian for unsharp mask)
vec3 boxBlur(sampler2D tex, vec2 uv, vec2 texelSize, int radius) {
vec3 blurred = vec3(0.0);
float weightSum = 0.0;
float r = float(radius);
for (int x = -radius; x <= radius; ++x) {
for (int y = -radius; y <= radius; ++y) {
// Optional: Use Gaussian weights instead of box for better quality blur
// float weight = exp(-(float(x*x + y*y)) / (2.0 * r*r));
float weight = 1.0; // Box weight
blurred += texture(tex, uv + vec2(x, y) * texelSize).rgb * weight;
weightSum += weight;
}
}
return blurred / weightSum;
}
// Apply Clarity using Unsharp Masking
vec3 applyClarity(vec3 originalColor, vec2 uv, float clarity) {
if (abs(clarity) < 0.01) return originalColor; // No change
vec2 texelSize = 1.0 / textureSize(InputTexture, 0);
// Clarity targets mid-frequencies, use a moderate blur radius
int blurRadius = 2; // Adjust radius for desired frequency range (1-3 typical)
// 1. Create a blurred version (low-pass filter)
vec3 blurredColor = boxBlur(InputTexture, uv, texelSize, blurRadius);
// 2. Calculate the high-pass detail (Original - Blurred)
vec3 highPassDetail = originalColor - blurredColor;
// 3. Add the scaled detail back to the original
// Map clarity -100..100 to a strength factor, e.g., 0..2
float strength = clarity / 100.0; // Map to -1..1
vec3 clarifiedColor = originalColor + highPassDetail * strength;
return clarifiedColor;
}
void main() {
vec4 color = texture(InputTexture, TexCoord);
color.rgb = applyClarity(color.rgb, TexCoord, clarityValue);
FragColor = vec4(max(color.rgb, vec3(0.0)), color.a);
}

25
shaders/contrast.frag Normal file
View File

@ -0,0 +1,25 @@
#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D InputTexture;
uniform float contrastValue; // Expecting value e.g., -100 to 100
// Perceptually accurate contrast adjustment
vec3 applyContrast(vec3 color, float contrast) {
float factor = pow(2.0, contrast / 50.0); // Exponential scaling for more natural feel
// Use 0.18 as middle gray (photographic standard)
const vec3 midPoint = vec3(0.18);
// Apply contrast with proper pivot point
return midPoint + factor * (color - midPoint);
}
void main() {
vec4 color = texture(InputTexture, TexCoord);
color.rgb = applyContrast(color.rgb, contrastValue);
// Clamp results to valid range
FragColor = vec4(clamp(color.rgb, vec3(0.0), vec3(1.0)), color.a);
}

198
shaders/dehaze.frag Normal file
View File

@ -0,0 +1,198 @@
#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);
}

44
shaders/exposure.frag Normal file
View File

@ -0,0 +1,44 @@
#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D InputTexture;
uniform float exposureValue; // Expecting value in stops, e.g., -5.0 to 5.0
// Calculate perceptual luminance
float luminance(vec3 color) {
return dot(color, vec3(0.2126, 0.7152, 0.0722));
}
// Apply exposure by adjusting luminance while preserving color relationships
vec3 applyExposure(vec3 color, float exposureStops) {
// Get original luminance
float lum = luminance(color);
// Skip processing for very dark pixels to avoid division by zero
if (lum < 0.0001) return color;
// Calculate exposure factor
float exposureFactor = pow(2.0, exposureStops);
// Apply highlight compression when increasing exposure
float newLum = lum * exposureFactor;
if (exposureStops > 0.0 && newLum > 0.8) {
// Soft highlight roll-off to prevent harsh clipping
float excess = newLum - 0.8;
newLum = 0.8 + 0.2 * (1.0 - exp(-excess * 5.0));
}
// Scale RGB proportionally to maintain color relationships
return color * (newLum / lum);
}
void main() {
vec4 color = texture(InputTexture, TexCoord);
// Apply exposure adjustment
color.rgb = applyExposure(color.rgb, exposureValue);
// Ensure output is in valid range
FragColor = vec4(clamp(color.rgb, vec3(0.0), vec3(1.0)), color.a);
}

View File

@ -0,0 +1,66 @@
#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D InputTexture;
uniform float highlightsValue; // -100 to 100
uniform float shadowsValue; // -100 to 100
// More accurate perceptual luminance (Rec. 709)
float luminance(vec3 color) {
return dot(color, vec3(0.2126, 0.7152, 0.0722));
}
// Smoothstep with better performance than quintic
float smootherStep(float edge0, float edge1, float x) {
float t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
return t * t * (3.0 - 2.0 * t);
}
vec3 applyHighlightsShadows(vec3 color) {
float lum = luminance(color);
// Define threshold values similar to Lightroom
float shadowThreshold = 0.3;
float highlightThreshold = 0.7;
// Calculate adjustment weights with smoother falloff
float shadowWeight = 1.0 - smootherStep(0.0, shadowThreshold * 2.0, lum);
float highlightWeight = smootherStep(highlightThreshold, 1.0, lum);
// Calculate adaptive adjustment factors
float shadowFactor = shadowsValue > 0.0 ?
mix(1.0, 1.0 + (shadowsValue / 100.0), shadowWeight) :
mix(1.0, 1.0 + (shadowsValue / 150.0), shadowWeight);
float highlightFactor = highlightsValue > 0.0 ?
mix(1.0, 1.0 - (highlightsValue / 150.0), highlightWeight) :
mix(1.0, 1.0 - (highlightsValue / 100.0), highlightWeight);
// Apply adjustments while preserving colors
vec3 adjusted = color * shadowFactor * highlightFactor;
// Preserve some saturation characteristics like Lightroom
float newLum = luminance(adjusted);
float saturationFactor = 1.0;
// Boost saturation slightly when lifting shadows (like Lightroom)
// if (shadowsValue > 0.0 && lum < shadowThreshold) {
// saturationFactor = 1.0 + (shadowsValue / 300.0) * (1.0 - lum / shadowThreshold);
// }
// Reduce saturation slightly when recovering highlights (like Lightroom)
if (highlightsValue > 0.0 && lum > highlightThreshold) {
saturationFactor *= 1.0 - (highlightsValue / 400.0) * ((lum - highlightThreshold) / (1.0 - highlightThreshold));
}
// Apply saturation adjustment while preserving luminance
return mix(vec3(newLum), adjusted, saturationFactor);
}
void main() {
vec4 color = texture(InputTexture, TexCoord);
color.rgb = applyHighlightsShadows(color.rgb);
FragColor = vec4(clamp(color.rgb, 0.0, 1.0), color.a);
}

45
shaders/histogram.comp Normal file
View File

@ -0,0 +1,45 @@
#version 430 core // Need SSBOs and atomic counters
// Input Texture (the processed image ready for display)
// Binding = 0 matches glBindImageTexture unit
// Use rgba8 format as we assume display texture is 8-bit sRGB (adjust if needed)
layout(binding = 0, rgba8) uniform readonly image2D InputTexture;
// Output Histogram Buffer (SSBO)
// Binding = 1 matches glBindBufferBase index
// Contains 256 bins for R, 256 for G, 256 for B sequentially (total 768 uints)
layout(std430, binding = 1) buffer HistogramBuffer {
uint bins[]; // Use an unsized array
} histogram;
// Workgroup size (adjust based on GPU architecture for performance, 16x16 is often reasonable)
layout (local_size_x = 16, local_size_y = 16, local_size_z = 1) in;
void main() {
// Get the global invocation ID (like pixel coordinates)
ivec2 pixelCoord = ivec2(gl_GlobalInvocationID.xy);
ivec2 textureSize = imageSize(InputTexture);
// Boundary check: Don't process outside the image bounds
if (pixelCoord.x >= textureSize.x || pixelCoord.y >= textureSize.y) {
return;
}
// Load the pixel color (values are normalized float 0.0-1.0 from rgba8 image load)
vec4 pixelColor = imageLoad(InputTexture, pixelCoord);
// Calculate bin indices (0-255)
// We clamp just in case, although imageLoad from rgba8 should be in range.
uint rBin = uint(clamp(pixelColor.r, 0.0, 1.0) * 255.0);
uint gBin = uint(clamp(pixelColor.g, 0.0, 1.0) * 255.0);
uint bBin = uint(clamp(pixelColor.b, 0.0, 1.0) * 255.0);
// Atomically increment the counters in the SSBO
// Offset Green bins by 256, Blue bins by 512
atomicAdd(histogram.bins[rBin], 1u);
atomicAdd(histogram.bins[gBin + 256u], 1u);
atomicAdd(histogram.bins[bBin + 512u], 1u);
// Optional: Track Max Value (more complex, requires another SSBO or different strategy)
// Example: atomicMax(histogram.maxBinValue, histogram.bins[rBin]); // Needs careful sync
}

View File

@ -0,0 +1,16 @@
#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D InputTexture;
vec3 linearToSrgb(vec3 linearRGB) {
linearRGB = max(linearRGB, vec3(0.0)); // Ensure non-negative input
vec3 srgb = pow(linearRGB, vec3(1.0/2.4));
srgb = mix(linearRGB * 12.92, srgb * 1.055 - 0.055, step(vec3(0.0031308), linearRGB));
return clamp(srgb, 0.0, 1.0); // Clamp final output to display range
}
void main() {
vec3 linearColor = texture(InputTexture, TexCoord).rgb;
FragColor = vec4(linearToSrgb(linearColor), texture(InputTexture, TexCoord).a);
}

9
shaders/passthrough.frag Normal file
View File

@ -0,0 +1,9 @@
#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D InputTexture;
void main() {
FragColor = texture(InputTexture, TexCoord);
}

10
shaders/passthrough.vert Normal file
View File

@ -0,0 +1,10 @@
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
void main() {
gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0);
TexCoord = aTexCoord;
}

44
shaders/saturation.frag Normal file
View File

@ -0,0 +1,44 @@
#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D InputTexture;
uniform float saturationValue; // -100 (grayscale) to 100 (double sat)
// Perceptual luminance weights (Rec. 709)
const vec3 luminanceWeight = vec3(0.2126, 0.7152, 0.0722);
vec3 applySaturation(vec3 color, float saturation) {
// Get original luminance
float lum = dot(color, luminanceWeight);
// Skip processing for very dark or very bright pixels
if (lum < 0.001 || lum > 0.999) return color;
// Non-linear saturation response curve (more natural-looking)
float factor;
if (saturation >= 0.0) {
// Positive saturation with highlight protection
factor = 1.0 + (saturation / 100.0) * (1.0 - 0.3 * smoothstep(0.7, 1.0, lum));
} else {
// Negative saturation with shadow protection
factor = 1.0 + (saturation / 100.0) * (1.0 - 0.1 * smoothstep(0.0, 0.2, lum));
}
// Apply saturation while preserving luminance
vec3 adjusted = mix(vec3(lum), color, factor);
// Ensure we maintain original luminance exactly
float newLum = dot(adjusted, luminanceWeight);
return adjusted * (lum / max(newLum, 0.001));
}
void main() {
vec4 color = texture(InputTexture, TexCoord);
// Apply saturation
color.rgb = applySaturation(color.rgb, saturationValue);
// Proper clamping to valid range
FragColor = vec4(clamp(color.rgb, 0.0, 1.0), color.a);
}

View File

@ -0,0 +1,16 @@
#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D InputTexture;
vec3 linearToSrgb(vec3 linearRGB) {
linearRGB = max(linearRGB, vec3(0.0)); // Ensure non-negative input
vec3 srgb = pow(linearRGB, vec3(1.0/2.4));
srgb = mix(linearRGB * 12.92, srgb * 1.055 - 0.055, step(vec3(0.0031308), linearRGB));
return clamp(srgb, 0.0, 1.0); // Clamp final output to display range
}
void main() {
vec3 linearColor = texture(InputTexture, TexCoord).rgb;
FragColor = vec4(linearToSrgb(linearColor), texture(InputTexture, TexCoord).a);
}

121
shaders/texture.frag Normal file
View File

@ -0,0 +1,121 @@
#version 330 core
/*
* Advanced Texture Control Shader
* -------------------------------
* This shader emulates Adobe Lightroom/Photoshop's Texture slider by:
*
* 1. Using frequency separation techniques to isolate medium-frequency details
* 2. Employing multi-scale edge detection for more natural enhancement
* 3. Adding tone-aware processing to protect highlights and shadows
* 4. Implementing perceptual weighting to preserve color relationships
* 5. Using non-linear response curves similar to Lightroom's implementation
*
* Negative values: Smooth medium-frequency texture details (skin smoothing)
* Positive values: Enhance medium-frequency texture details (fabric, hair, etc.)
*
* Unlike Clarity (which affects larger contrast structures) or Sharpening (which
* affects fine details), Texture specifically targets medium-frequency details
* that represent surface textures while avoiding edges.
*/
float luminance(vec3 color) {
return dot(color, vec3(0.2126, 0.7152, 0.0722));
}
// Improved blur function with Gaussian weights for better quality
vec3 gaussianBlur(sampler2D tex, vec2 uv, vec2 texelSize, float radius) {
vec3 blurred = vec3(0.0);
float weightSum = 0.0;
float sigma = radius / 2.0;
float sigma2 = 2.0 * sigma * sigma;
int kernelSize = int(ceil(radius * 2.0));
for (int x = -kernelSize; x <= kernelSize; ++x) {
for (int y = -kernelSize; y <= kernelSize; ++y) {
float dist2 = float(x*x + y*y);
float weight = exp(-dist2 / sigma2);
vec3 sample = texture(tex, uv + vec2(x, y) * texelSize).rgb;
blurred += sample * weight;
weightSum += weight;
}
}
return blurred / max(weightSum, 0.0001);
}
// Edge-aware weight function to prevent halos
float edgeWeight(float lumaDiff, float threshold) {
return 1.0 - smoothstep(0.0, threshold, abs(lumaDiff));
}
// Sigmoid function for smoother transitions
float sigmoid(float x, float strength) {
return x * (1.0 + strength) / (1.0 + strength * abs(x));
}
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D InputTexture;
uniform float textureValue; // -100 (smooth) to 100 (enhance)
vec3 applyTexture(vec3 originalColor, vec2 uv, float textureAdj) {
if (abs(textureAdj) < 0.01) return originalColor;
vec2 texelSize = 1.0 / textureSize(InputTexture, 0);
float origLuma = luminance(originalColor);
// Multi-scale approach for medium frequency targeting
// For texture, we want medium frequencies (not too small, not too large)
float smallRadius = 2.0; // For high frequency details
float mediumRadius = 4.0; // For medium frequency details (texture)
float largeRadius = 8.0; // For low frequency details
vec3 smallBlur = gaussianBlur(InputTexture, uv, texelSize, smallRadius);
vec3 mediumBlur = gaussianBlur(InputTexture, uv, texelSize, mediumRadius);
vec3 largeBlur = gaussianBlur(InputTexture, uv, texelSize, largeRadius);
// Extract medium frequencies (texture details)
vec3 highFreq = smallBlur - mediumBlur;
vec3 mediumFreq = mediumBlur - largeBlur;
// Calculate local contrast for edge-aware processing
float smallLuma = luminance(smallBlur);
float mediumLuma = luminance(mediumBlur);
float largeLuma = luminance(largeBlur);
// Edge detection weights
float edgeMask = edgeWeight(smallLuma - mediumLuma, 0.1);
// Highlight & shadow protection
float highlightProtect = 1.0 - smoothstep(0.75, 0.95, origLuma);
float shadowProtect = smoothstep(0.05, 0.25, origLuma);
float tonalWeight = highlightProtect * shadowProtect;
// Map texture value to a perceptually balanced strength
// Use non-linear mapping to match Lightroom's response curve
float strength = sign(textureAdj) * pow(abs(textureAdj / 100.0), 0.8);
// Apply different processing for positive vs negative values
vec3 result = originalColor;
if (strength > 0.0) {
// Enhance texture (positive values)
float enhanceFactor = strength * 1.5 * tonalWeight * edgeMask;
result = originalColor + mediumFreq * enhanceFactor;
} else {
// Smooth texture (negative values)
float smoothFactor = -strength * tonalWeight;
result = mix(originalColor, mediumBlur, smoothFactor);
}
// Apply sigmoid function for more natural transitions
result = mix(originalColor, result, sigmoid(abs(strength), 0.5));
return result;
}
void main() {
vec4 color = texture(InputTexture, TexCoord);
color.rgb = applyTexture(color.rgb, TexCoord, textureValue);
FragColor = vec4(max(color.rgb, vec3(0.0)), color.a);
}

79
shaders/vibrance.frag Normal file
View File

@ -0,0 +1,79 @@
#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D InputTexture;
uniform float vibranceValue; // -100 to 100
const vec3 luminanceWeight = vec3(0.2126, 0.7152, 0.0722);
// Check if a color is potentially a skin tone (approximate)
float skinToneLikelihood(vec3 color) {
// Simple skin tone detection - check if in common skin tone range
// Based on normalized r/g ratio in RGB space
float total = color.r + color.g + color.b;
if (total < 0.001) return 0.0;
vec3 normalized = color / total;
// Detect skin tones based on red-green ratio and absolute red value
bool redEnough = normalized.r > 0.35;
bool redGreenRatio = normalized.r / normalized.g > 1.1 && normalized.r / normalized.g < 2.0;
bool notTooBlue = normalized.b < 0.4;
return (redEnough && redGreenRatio && notTooBlue) ? 1.0 - pow(normalized.b * 1.5, 2.0) : 0.0;
}
vec3 applyVibrance(vec3 color, float vibrance) {
float vibAmount = vibrance / 100.0; // Map to -1..1
// Calculate better saturation
float luma = dot(color, luminanceWeight);
vec3 chroma = max(color - luma, 0.0);
float sat = length(chroma) / (luma + 0.001);
// Get skin tone protection factor
float skinFactor = skinToneLikelihood(color);
// Calculate adjustment strength based on current saturation
// Less effect on already highly saturated colors
float satWeight = 1.0 - smoothstep(0.2, 0.8, sat);
// Apply less vibrance to skin tones
float adjustmentFactor = satWeight * (1.0 - skinFactor * 0.7);
// Create non-linear response curve for natural-looking adjustment
float strength = vibAmount > 0.0
? vibAmount * (1.0 - pow(sat, 2.0)) // Positive vibrance
: vibAmount; // Negative vibrance (desaturation)
// Fine-tune the saturation component-wise to preserve color relationships
vec3 satColor = color;
if (abs(vibAmount) > 0.001) {
// Get distance from gray for each channel
vec3 dist = color - luma;
// Adjust distance based on vibrance
dist *= (1.0 + strength * adjustmentFactor);
// Rebuild color from luma + adjusted chroma
satColor = luma + dist;
// Preserve color ratios for extreme adjustments
if (vibAmount > 0.5) {
float maxComponent = max(satColor.r, max(satColor.g, satColor.b));
if (maxComponent > 1.0) {
float scale = min(1.0 / maxComponent, 2.0); // Limit scaling
satColor = luma + (satColor - luma) * scale;
}
}
}
return satColor;
}
void main() {
vec4 color = texture(InputTexture, TexCoord);
color.rgb = applyVibrance(color.rgb, vibranceValue);
FragColor = vec4(max(color.rgb, vec3(0.0)), color.a);
}

158
shaders/white_balance.frag Normal file
View File

@ -0,0 +1,158 @@
#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D InputTexture;
uniform float temperature; // Target Correlated Color Temperature (Kelvin, e.g., 1000-20000)
uniform float tint; // Green-Magenta shift (-100 to +100)
// --- Constants ---
// sRGB Primaries to XYZ (D65)
const mat3 M_RGB_2_XYZ_D65 = mat3(
0.4124564, 0.3575761, 0.1804375,
0.2126729, 0.7151522, 0.0721750,
0.0193339, 0.1191920, 0.9503041
);
// XYZ (D65) to sRGB Primaries
const mat3 M_XYZ_2_RGB_D65 = mat3(
3.2404542, -1.5371385, -0.4985314,
-0.9692660, 1.8760108, 0.0415560,
0.0556434, -0.2040259, 1.0572252
);
// Standard Illuminant D65 XYZ (Normalized Y=1) - Our Target White
const vec3 WHITEPOINT_D65 = vec3(0.95047, 1.00000, 1.08883);
// Bradford CAT Matrices
const mat3 M_BRADFORD = mat3(
0.8951, 0.2664, -0.1614,
-0.7502, 1.7135, 0.0367,
0.0389, -0.0685, 1.0296
);
const mat3 M_BRADFORD_INV = mat3(
0.9869929, -0.1470543, 0.1599627,
0.4323053, 0.5183603, 0.0492912,
-0.0085287, 0.0400428, 0.9684867
);
// --- Helper Functions ---
// Approximate Kelvin Temperature to XYZ Chromaticity (xy) -> XYZ coordinates
// Uses simplified polynomial fits for different temperature ranges.
// Note: More accurate methods often use look-up tables or spectral calculations.
vec3 kelvinToXYZ(float kelvin) {
float temp = clamp(kelvin, 1000.0, 20000.0);
float x, y;
// Calculate xy chromaticity coordinates based on temperature
// Formulas from: http://www.brucelindbloom.com/index.html?Eqn_T_to_xy.html (with slight adaptations)
float t = temp;
float t2 = t * t;
float t3 = t2 * t;
// Calculate x coordinate
if (t >= 1000.0 && t <= 4000.0) {
x = -0.2661239e9 / t3 - 0.2343589e6 / t2 + 0.8776956e3 / t + 0.179910;
} else { // t > 4000.0 && t <= 25000.0
x = -3.0258469e9 / t3 + 2.1070379e6 / t2 + 0.2226347e3 / t + 0.240390;
}
// Calculate y coordinate based on x
float x2 = x * x;
float x3 = x2 * x;
if (t >= 1000.0 && t <= 2222.0) {
y = -1.1063814 * x3 - 1.34811020 * x2 + 2.18555832 * x - 0.18709;
} else if (t > 2222.0 && t <= 4000.0) {
y = -0.9549476 * x3 - 1.37418593 * x2 + 2.09137015 * x - 0.16748867;
} else { // t > 4000.0 && t <= 25000.0
y = 3.0817580 * x3 - 5.8733867 * x2 + 3.75112997 * x - 0.37001483;
}
// Convert xyY (Y=1) to XYZ
if (y < 1e-6) return vec3(0.0); // Avoid division by zero
float Y = 1.0;
float X = (x / y) * Y;
float Z = ((1.0 - x - y) / y) * Y;
return vec3(X, Y, Z);
}
// Apply Bradford Chromatic Adaptation Transform
vec3 adaptXYZ(vec3 xyzColor, vec3 sourceWhiteXYZ, vec3 destWhiteXYZ) {
vec3 sourceCone = M_BRADFORD * sourceWhiteXYZ;
vec3 destCone = M_BRADFORD * destWhiteXYZ;
// Avoid division by zero if source cone response is zero
// (shouldn't happen with typical white points but good practice)
if (sourceCone.r < 1e-6 || sourceCone.g < 1e-6 || sourceCone.b < 1e-6) {
return xyzColor; // Return original color if adaptation is impossible
}
vec3 ratio = destCone / sourceCone;
mat3 adaptationMatrix = M_BRADFORD_INV * mat3(ratio.x, 0, 0, 0, ratio.y, 0, 0, 0, ratio.z) * M_BRADFORD;
return adaptationMatrix * xyzColor;
}
// --- Main White Balance Function ---
vec3 applyWhiteBalance(vec3 linearSRGBColor, float tempK, float tintVal) {
// 1. Convert input Linear sRGB (D65) to XYZ D65
vec3 inputXYZ = M_RGB_2_XYZ_D65 * linearSRGBColor;
// 2. Calculate the XYZ white point of the source illuminant (from Kelvin temp)
// This is the white we want to adapt *FROM*
vec3 sourceWhiteXYZ = kelvinToXYZ(tempK);
// 3. Apply Bradford CAT to adapt from sourceWhiteXYZ to D65
vec3 adaptedXYZ = adaptXYZ(inputXYZ, sourceWhiteXYZ, WHITEPOINT_D65);
// 4. Convert adapted XYZ (now relative to D65) back to Linear sRGB
vec3 adaptedLinearSRGB = M_XYZ_2_RGB_D65 * adaptedXYZ;
// 5. Apply Tint adjustment (Post-CAT approximation)
// This shifts color balance along a Green<->Magenta axis.
// We scale RGB components slightly based on the tint value.
// A common method is to affect Green opposite to Red/Blue.
if (abs(tintVal) > 0.01) {
// Map tint -100..100 to a scaling factor, e.g., -0.1..0.1
float tintFactor = tintVal / 1000.0; // Smaller scale factor for subtle tint
// Apply scaling: Increase G for negative tint, Decrease G for positive tint
// Compensate R and B slightly in the opposite direction.
// Coefficients below are heuristic and may need tuning for perceptual feel.
float rScale = 1.0 + tintFactor * 0.5;
float gScale = 1.0 - tintFactor * 1.0;
float bScale = 1.0 + tintFactor * 0.5;
vec3 tintScaleVec = vec3(rScale, gScale, bScale);
// Optional: Normalize scale vector to preserve luminance (roughly)
// Calculate luminance of the scale vector itself
float scaleLum = dot(tintScaleVec, vec3(0.2126, 0.7152, 0.0722));
if (scaleLum > 1e-5) {
tintScaleVec /= scaleLum; // Normalize
}
adaptedLinearSRGB *= tintScaleVec;
}
return adaptedLinearSRGB;
}
void main() {
vec4 texColor = texture(InputTexture, TexCoord);
vec3 linearInputColor = texColor.rgb; // Assuming input texture is already linear sRGB
// Calculate white balanced color
vec3 whiteBalancedColor = applyWhiteBalance(linearInputColor, temperature, tint);
// Ensure output is non-negative
whiteBalancedColor = max(whiteBalancedColor, vec3(0.0));
FragColor = vec4(whiteBalancedColor, texColor.a);
}

View File

@ -0,0 +1,51 @@
#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D InputTexture;
uniform float whitesValue; // -100 (pull down) to 100 (push up)
uniform float blacksValue; // -100 (lift up) to 100 (push down)
const vec3 luminanceWeight = vec3(0.2126, 0.7152, 0.0722);
// Helper function to preserve color relationships when adjusting luminance
vec3 preserveColor(vec3 color, float newLum) {
float oldLum = dot(color, luminanceWeight);
return oldLum > 0.0 ? color * (newLum / oldLum) : vec3(newLum);
}
vec3 applyWhitesBlacks(vec3 color, float whites, float blacks) {
float lum = dot(color, luminanceWeight);
// Map slider values to more appropriate adjustment strengths
float whitesStrength = whites / 100.0;
float blacksStrength = blacks / 100.0;
// Create better perceptual masks with wider, smoother influence
// Whites affect primarily highlights but have some influence into midtones
float whiteMask = smoothstep(0.25, 1.0, lum);
whiteMask = pow(whiteMask, 2.0 - max(0.0, whitesStrength)); // Dynamic falloff
// Blacks affect primarily shadows but have some influence into midtones
float blackMask = 1.0 - smoothstep(0.0, 0.5, lum);
blackMask = pow(blackMask, 2.0 - max(0.0, -blacksStrength)); // Dynamic falloff
// Calculate adjustment curves with proper toe/shoulder response
float whitesAdj = 1.0 + whitesStrength * whiteMask * (1.0 - pow(1.0 - whiteMask, 3.0));
float blacksAdj = 1.0 - blacksStrength * blackMask * (1.0 - pow(1.0 - blackMask, 3.0));
// Apply adjustments with color preservation
float adjustedLum = lum * whitesAdj * blacksAdj;
adjustedLum = clamp(adjustedLum, 0.0, 2.0); // Allow some headroom for highlights
// Preserve color relationships by scaling RGB proportionally
vec3 result = preserveColor(color, adjustedLum);
return result;
}
void main() {
vec4 color = texture(InputTexture, TexCoord);
color.rgb = applyWhitesBlacks(color.rgb, whitesValue, blacksValue);
FragColor = vec4(max(color.rgb, vec3(0.0)), color.a); // Ensure non-negative
}