4
votes

I am currently using this fragment shader in WebGL to apply highlights/shadows adjustments to photo textures.

The shader itself was pulled directly from the excellent GPUImage library for iOS.

uniform sampler2D inputImageTexture;
    varying highp vec2 textureCoordinate;

    uniform lowp float shadows;
    uniform lowp float highlights;

    const mediump vec3 luminanceWeighting = vec3(0.3, 0.3, 0.3);

    void main()
    {
        lowp vec4 source = texture2D(inputImageTexture, textureCoordinate);
        mediump float luminance = dot(source.rgb, luminanceWeighting);

        mediump float shadow = clamp((pow(luminance, 1.0/(shadows+1.0)) + (-0.76)*pow(luminance, 2.0/(shadows+1.0))) - luminance, 0.0, 1.0);
        mediump float highlight = clamp((1.0 - (pow(1.0-luminance, 1.0/(2.0-highlights)) + (-0.8)*pow(1.0-luminance, 2.0/(2.0-highlights)))) - luminance, -1.0, 0.0);
        lowp vec3 result = vec3(0.0, 0.0, 0.0) + ((luminance + shadow + highlight) - 0.0) * ((source.rgb - vec3(0.0, 0.0, 0.0))/(luminance - 0.0));

        gl_FragColor = vec4(result.rgb, source.a);
    }

This shader as it stands, will only reduce highlights on a scale of 0.0 - 1.0. However I would like it to also brighten the highlights on a scale of 1.0-2.0.

With the aim of having a complete filter that reduces the images highlights when the highlights uniform is less than 1.0 and increases the intensity of the highlights when it is above 1.0. The same goes for the darkness shadows uniform

Highlights:

0.0(duller) ---- 1.0 (default - original pixel values) ----- 2.0 (brighter)

I have tried simply changing the clamp on the highlights variable to 0.0,2.0, and although this does indeed increase the brightness of the highlights when the uniform is above 1.0 it also seriously messes up the colors.

My understanding of image processing and constructing fragment shaders is extremely weak at best as you my be able to tell.

I'm just hoping someone can point me in the right direction.

EDIT:

Here are some example screenshots:-

  1. The current filter with highlights set to 1.00 (basically the source image)

enter image description here

  1. The current filter with highlights set to 0.00 as you can see the highlights get flattened/removed.

enter image description here

  1. And finally here is what happens when I change the clamp in the fragment shader to allow values above 1.00 and set the highlights value to 2.00

enter image description here

I simply wish to be able to boost the highlights, making them brighter/more defined. i.e. the opposite of setting the value to 0.00

1
Could you add screenshots of the output of the shader with and without the modified clamp, to illustrate exactly how the colors are "messed up"? I think these examples would really help us understand the problem.Kevin Reid
@KevinReid i've added some screenshots as requested. cheers!gordyr

1 Answers

10
votes

I don't really understand the shadow and highlight equations, but I can see that they are set up to never enhance shadows and highlights, but rather to wash them out. So we need a secondary step for enhancement.

For the highlights, I think to handle brighter colors, you need to blend towards white instead of adding something, so you don't get hue-shifts. I used a basic contrast equation to pick out the highlights, and then cubed it to clip out the midtones and shadows. The whiteTarget is just pulling out the top half of the 0.0-2.0 range to use as a multiplier to determine the strength of the brightening effect.

For the shadows, we are changing our range from 0.0-1.0 (where 0 is unchanged and 1 is washed out) to 0.0-2.0 (where 1 is unchanged and 2 is washed out). Therefore, the +1.0's in the shadow equation should be removed. Then for the 0.0-1.0 range, I just copied what I did for the highlights, except blending toward black. Maybe that can be optimized to avoid a mix function (not sure).

So here is my unoptimized version of the shader, set up so both shadows and highlights are on 0.0-2.0 scales, with 1.0 being the nominal. You might want to play around with those lines where I cube the luminance, and also with the value I used for contrast (currently 1.5), but it seems pretty good to me the way it is now--I adjusted it to try to avoid any ugly overlap between shadows and highlight ranges when the input parameters are at the two extremes.

uniform sampler2D inputImageTexture;
varying highp vec2 textureCoordinate;

uniform lowp float shadows;
uniform lowp float highlights;

const mediump vec3 luminanceWeighting = vec3(0.3, 0.3, 0.3);

void main()
{
    lowp vec4 source = texture2D(inputImageTexture, textureCoordinate);
    mediump float luminance = dot(source.rgb, luminanceWeighting);

    //(shadows+1.0) changed to just shadows:
    mediump float shadow = clamp((pow(luminance, 1.0/shadows) + (-0.76)*pow(luminance, 2.0/shadows)) - luminance, 0.0, 1.0);
    mediump float highlight = clamp((1.0 - (pow(1.0-luminance, 1.0/(2.0-highlights)) + (-0.8)*pow(1.0-luminance, 2.0/(2.0-highlights)))) - luminance, -1.0, 0.0);
    lowp vec3 result = vec3(0.0, 0.0, 0.0) + ((luminance + shadow + highlight) - 0.0) * ((source.rgb - vec3(0.0, 0.0, 0.0))/(luminance - 0.0));

    // blend toward white if highlights is more than 1
    mediump float contrastedLuminance = ((luminance - 0.5) * 1.5) + 0.5;
    mediump float whiteInterp = contrastedLuminance*contrastedLuminance*contrastedLuminance;
    mediump float whiteTarget = clamp(highlights, 1.0, 2.0) - 1.0;
    result = mix(result, vec3(1.0), whiteInterp*whiteTarget);

    // blend toward black if shadows is less than 1
    mediump float invContrastedLuminance = 1.0 - contrastedLuminance;
    mediump float blackInterp = invContrastedLuminance*invContrastedLuminance*invContrastedLuminance;
    mediump float blackTarget = 1.0 - clamp(shadows, 0.0, 1.0);
    result = mix(result, vec3(0.0), blackInterp*blackTarget);

    gl_FragColor = vec4(result, source.a);
}

By the way, any idea why the original result line keeps adding 0's to everything? Seems like it could be simplified to

vec3 result = (luminance + shadow + highlight) * source.rgb / luminance;

But maybe it's a trick to cast to lowp within the calculation instead of after the calculation. Just a guess.