Metal Shaders: Vibrance

In this blog post we will cover how to increase vibrance in your images without blowing out the already bright colors that might be present. The code comes from the Metal GPUImage 3 framework.

Saturation vs Vibrance

In a previous blog post, I covered a saturation filter. As a quick recap, the saturation filter takes the luminance value and adds it to the original pixel color. This operation is applied uniformly to every pixel regardless of the saturation already present in the image.

This means that if you already have some saturated colors in your image, they can get blown out with a saturation adjustment:

Blown out colors cause by saturation adjustment.

Vibrance, on the other hand, is a slightly less blunt instrument. The equation takes into consideration how saturated the original value is. The higher the value, the less the equation impacts it.

Shader Code

Here is the Vibrance fragment function (with the VibranceUniform structure omitted):

fragment half4 vibranceFragment(
	SingleInputVertexIO fragmentInput [[stage_in]],
	texture2d inputTexture [[texture(0)]],
	constant VibranceUniform& uniform [[ buffer(1) ]])
{
	constexpr sampler quadSampler;
	half4 color = inputTexture.sample(quadSampler, fragmentInput.textureCoordinate);
	
	half average = (color.r + color.g + color.b) / 3.0;
	half mx = max(color.r, max(color.g, color.b));
	half amt = (mx - average) * (-uniform.vibrance * 3.0);
	color.rgb = mix(color.rgb, half3(mx), amt);
	
	return half4(color);
}

First, we are finding the average luminance of the current pixel. We are doing this by adding the red, green, and blue values together and dividing the sum by three.

Next, we are determining which color is most prominent in the currently sampled pixel. This is a nested max() function. It checks if blue or green is greater, then it pits that value against red’s value and whichever value is larger emerges as the victor.

Next we need to determine the weight we will apply to our final color mix. This is determined using the following components:

  • The maximum color value
  • The average color value
  • The user input value from the UI determining the degree of vibrance

You need to determine the difference between the brightest color and the average color. The larger this difference, the more of an impact the shader has on that specific pixel.

Like the saturation shader, the vibrance function also uses a mix function mixing in the maximum color value. However, instead of directly pulling in that value from the slider, it’s applying the color difference value calculated above.

Increased vibrance without the blow out.

Conclusions

Color operations on images are interesting in that there are varying degrees of fidgeting you can do with the operations. There are blunt force operations that tend to be good enough for quick and dirty processing, but there are also options available if you want more control over the output of your image.