initial commit
This commit is contained in:
58
shaders/clarity.frag
Normal file
58
shaders/clarity.frag
Normal 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
25
shaders/contrast.frag
Normal 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
198
shaders/dehaze.frag
Normal 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
44
shaders/exposure.frag
Normal 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);
|
||||
}
|
66
shaders/highlights_shadows.frag
Normal file
66
shaders/highlights_shadows.frag
Normal 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
45
shaders/histogram.comp
Normal 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
|
||||
}
|
16
shaders/linear_to_srgb.frag
Normal file
16
shaders/linear_to_srgb.frag
Normal 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
9
shaders/passthrough.frag
Normal 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
10
shaders/passthrough.vert
Normal 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
44
shaders/saturation.frag
Normal 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);
|
||||
}
|
16
shaders/srgb_to_linear.frag
Normal file
16
shaders/srgb_to_linear.frag
Normal 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
121
shaders/texture.frag
Normal 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
79
shaders/vibrance.frag
Normal 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
158
shaders/white_balance.frag
Normal 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);
|
||||
}
|
51
shaders/whites_blacks.frag
Normal file
51
shaders/whites_blacks.frag
Normal 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
|
||||
}
|
Reference in New Issue
Block a user