Photoshop Blend Modes in HLSL

I recently needed to mimic all of the Photoshop color blend modes on the GPU so here we are. These were written for simplicity and correctness rather than performance. That isn't to say they are guaranteed to be 100% perfect (e.g. I'm not sure if the implementation for Darker Color should use a < or <=), but they did the trick for me!

IMPORTANT: When using blend modes in Photoshop, you are likely operating with sRGB colors. In a shader, you are probably operating on linear colors. If that is true and you want to match the results of Photoshop, you will need to convert your inputs to sRGB and then convert the output back to linear. 

License

This software is available under 2 licenses. Choose whichever you prefer.

------------------------------------------------------------------------------
 ZLIB License
------------------------------------------------------------------------------
Copyright (c) 2020 Ryan Juckett
http://www.ryanjuckett.com/

This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.

Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:

1. The origin of this software must not be misrepresented; you must not
   claim that you wrote the original software. If you use this software
   in a product, an acknowledgment in the product documentation would be
   appreciated but is not required.

2. Altered source versions must be plainly marked as such, and must not be
   misrepresented as being the original software.

3. This notice may not be removed or altered from any source
   distribution.

------------------------------------------------------------------------------
 Public Domain
------------------------------------------------------------------------------
This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <http://unlicense.org/>

Separable Blend Modes

The blend modes can be split into two sets: separable and non-separable. Separable blend modes operate on each color channel individually. This means you can technically run them on colors of arbitrary dimension, etc. My goal was to write functions that run on float3 types, but I also added some helper functions that run on singular float values when it was useful.

The separable color blend modes are:

  • Normal
  • Darken
  • Multiply
  • Color Burn
  • Linear Burn
  • Lighten
  • Screen
  • Color Dodge
  • Linear Dodge (Add)
  • Overlay
  • Soft Light
  • Hard Light
  • Vivid Light
  • Linear Light
  • Pin Light
  • Hard Mix
  • Difference
  • Exclusion
  • Subtract
  • Divide
//******************************************************************************
// Selects the blend color, ignoring the base.
//******************************************************************************
float3 BlendMode_Normal(float3 base, float3 blend)
{
	return blend;
}

//******************************************************************************
// Looks at the color information in each channel and selects the base or blend 
// color—whichever is darker—as the result color.
//******************************************************************************
float3 BlendMode_Darken(float3 base, float3 blend)
{
	return min(base, blend);
}

//******************************************************************************
// Looks at the color information in each channel and multiplies the base color
// by the blend color.
//******************************************************************************
float3 BlendMode_Multiply(float3 base, float3 blend)
{
	return base*blend;
}

//******************************************************************************
// Looks at the color information in each channel and darkens the base color to 
// reflect the blend color by increasing the contrast between the two.
//******************************************************************************
float BlendMode_ColorBurn(float base, float blend)
{
	return blend > 0 ? 1 - min(1, (1-base) / blend) : 0;
}

float3 BlendMode_ColorBurn(float3 base, float3 blend)
{
	return float3(  BlendMode_ColorBurn(base.r, blend.r), 
					BlendMode_ColorBurn(base.g, blend.g), 
					BlendMode_ColorBurn(base.b, blend.b) );
}

//******************************************************************************
// Looks at the color information in each channel and darkens the base color to 
// reflect the blend color by decreasing the brightness.
//******************************************************************************
float BlendMode_LinearBurn(float base, float blend)
{
	return max(0, base + blend - 1);
}

float3 BlendMode_LinearBurn(float3 base, float3 blend)
{
	return float3(  BlendMode_LinearBurn(base.r, blend.r), 
					BlendMode_LinearBurn(base.g, blend.g), 
					BlendMode_LinearBurn(base.b, blend.b) );
}

//******************************************************************************
// Looks at the color information in each channel and selects the base or blend 
// color—whichever is lighter—as the result color.
//******************************************************************************
float3 BlendMode_Lighten(float3 base, float3 blend)
{
	return max(base, blend);
}

//******************************************************************************
// Looks at each channel’s color information and multiplies the inverse of the
// blend and base colors.
//******************************************************************************
float3 BlendMode_Screen(float3 base, float3 blend)
{
	return base + blend - base*blend;
}

//******************************************************************************
// Looks at the color information in each channel and brightens the base color 
// to reflect the blend color by decreasing contrast between the two. 
//******************************************************************************
float BlendMode_ColorDodge(float base, float blend)
{
	return blend < 1 ? min(1, base / (1-blend)) : 1;
}

float3 BlendMode_ColorDodge(float3 base, float3 blend)
{
	return float3(  BlendMode_ColorDodge(base.r, blend.r), 
					BlendMode_ColorDodge(base.g, blend.g), 
					BlendMode_ColorDodge(base.b, blend.b) );
}

//******************************************************************************
// Looks at the color information in each channel and brightens the base color 
// to reflect the blend color by decreasing contrast between the two. 
//******************************************************************************
float BlendMode_LinearDodge(float base, float blend)
{
	return min(1, base + blend);
}

float3 BlendMode_LinearDodge(float3 base, float3 blend)
{
	return float3(  BlendMode_LinearDodge(base.r, blend.r), 
					BlendMode_LinearDodge(base.g, blend.g), 
					BlendMode_LinearDodge(base.b, blend.b) );
}

//******************************************************************************
// Multiplies or screens the colors, depending on the base color. 
//******************************************************************************
float BlendMode_Overlay(float base, float blend)
{
	return (base <= 0.5) ? 2*base*blend : 1 - 2*(1-base)*(1-blend);
}

float3 BlendMode_Overlay(float3 base, float3 blend)
{
	return float3(  BlendMode_Overlay(base.r, blend.r), 
					BlendMode_Overlay(base.g, blend.g), 
					BlendMode_Overlay(base.b, blend.b) );
}

//******************************************************************************
// Darkens or lightens the colors, depending on the blend color. 
//******************************************************************************
float BlendMode_SoftLight(float base, float blend)
{
	if (blend <= 0.5)
	{
		return base - (1-2*blend)*base*(1-base);
	}
	else
	{
		float d = (base <= 0.25) ? ((16*base-12)*base+4)*base : sqrt(base);
		return base + (2*blend-1)*(d-base);
	}
}

float3 BlendMode_SoftLight(float3 base, float3 blend)
{
	return float3(  BlendMode_SoftLight(base.r, blend.r), 
					BlendMode_SoftLight(base.g, blend.g), 
					BlendMode_SoftLight(base.b, blend.b) );
}

//******************************************************************************
// Multiplies or screens the colors, depending on the blend color.
//******************************************************************************
float BlendMode_HardLight(float base, float blend)
{
	return (blend <= 0.5) ? 2*base*blend : 1 - 2*(1-base)*(1-blend);
}

float3 BlendMode_HardLight(float3 base, float3 blend)
{
	return float3(  BlendMode_HardLight(base.r, blend.r), 
					BlendMode_HardLight(base.g, blend.g), 
					BlendMode_HardLight(base.b, blend.b) );
}

//******************************************************************************
// Burns or dodges the colors by increasing or decreasing the contrast, 
// depending on the blend color. 
//******************************************************************************
float BlendMode_VividLight(float base, float blend)
{
	return (blend <= 0.5) ? BlendMode_ColorBurn(base,2*blend) : BlendMode_ColorDodge(base,2*(blend-0.5));
}

float3 BlendMode_VividLight(float3 base, float3 blend)
{
	return float3(  BlendMode_VividLight(base.r, blend.r), 
					BlendMode_VividLight(base.g, blend.g), 
					BlendMode_VividLight(base.b, blend.b) );
}

//******************************************************************************
// Burns or dodges the colors by decreasing or increasing the brightness, 
// depending on the blend color.
//******************************************************************************
float BlendMode_LinearLight(float base, float blend)
{
	return (blend <= 0.5) ? BlendMode_LinearBurn(base,2*blend) : BlendMode_LinearDodge(base,2*(blend-0.5));
}

float3 BlendMode_LinearLight(float3 base, float3 blend)
{
	return float3(  BlendMode_LinearLight(base.r, blend.r), 
					BlendMode_LinearLight(base.g, blend.g), 
					BlendMode_LinearLight(base.b, blend.b) );
}

//******************************************************************************
// Replaces the colors, depending on the blend color.
//******************************************************************************
float BlendMode_PinLight(float base, float blend)
{
	return (blend <= 0.5) ? min(base,2*blend) : max(base,2*(blend-0.5));
}

float3 BlendMode_PinLight(float3 base, float3 blend)
{
	return float3(  BlendMode_PinLight(base.r, blend.r), 
					BlendMode_PinLight(base.g, blend.g), 
					BlendMode_PinLight(base.b, blend.b) );
}

//******************************************************************************
// Adds the red, green and blue channel values of the blend color to the RGB 
// values of the base color. If the resulting sum for a channel is 255 or 
// greater, it receives a value of 255; if less than 255, a value of 0.
//******************************************************************************
float BlendMode_HardMix(float base, float blend)
{
	return (base + blend >= 1.0) ? 1.0 : 0.0;
}

float3 BlendMode_HardMix(float3 base, float3 blend)
{
	return float3(  BlendMode_HardMix(base.r, blend.r), 
					BlendMode_HardMix(base.g, blend.g), 
					BlendMode_HardMix(base.b, blend.b) );
}

//******************************************************************************
// Looks at the color information in each channel and subtracts either the 
// blend color from the base color or the base color from the blend color, 
// depending on which has the greater brightness value. 
//******************************************************************************
float3 BlendMode_Difference(float3 base, float3 blend)
{
	return abs(base-blend);
}

//******************************************************************************
// Creates an effect similar to but lower in contrast than the Difference mode.
//******************************************************************************
float3 BlendMode_Exclusion(float3 base, float3 blend)
{
	return base + blend - 2*base*blend;
}

//******************************************************************************
// Looks at the color information in each channel and subtracts the blend color 
// from the base color.
//******************************************************************************
float3 BlendMode_Subtract(float3 base, float3 blend)
{
	return max(0, base - blend);
}

//******************************************************************************
// Looks at the color information in each channel and divides the blend color 
// from the base color.
//******************************************************************************
float BlendMode_Divide(float base, float blend)
{
	return blend > 0 ? min(1, base / blend) : 1;
}

float3 BlendMode_Divide(float3 base, float3 blend)
{
	return float3(  BlendMode_Divide(base.r, blend.r), 
					BlendMode_Divide(base.g, blend.g), 
					BlendMode_Divide(base.b, blend.b) );
}

Non-separable Blend Modes

The non-separable blend modes operate on the full set of RGB channels in unison. Specifically they operate on hue, saturation and luminosity which means we are going to need some more helper functions.

//******************************************************************************
//******************************************************************************
float Color_GetLuminosity(float3 c)
{
	return 0.3*c.r + 0.59*c.g + 0.11*c.b;
}

//******************************************************************************
//******************************************************************************
float3 Color_SetLuminosity(float3 c, float lum)
{
    float d = lum - Color_GetLuminosity(c);
    c.rgb += float3(d,d,d);

	// clip back into legal range
	lum = Color_GetLuminosity(c);
    float cMin = min(c.r, min(c.g, c.b));
    float cMax = max(c.r, max(c.g, c.b));

    if(cMin < 0)
        c = lerp(float3(lum,lum,lum), c, lum / (lum - cMin));

    if(cMax > 1)
        c = lerp(float3(lum,lum,lum), c, (1 - lum) / (cMax - lum));

    return c;
}

//******************************************************************************
//******************************************************************************
float Color_GetSaturation(float3 c)
{
	return max(c.r, max(c.g, c.b)) - min(c.r, min(c.g, c.b));
}

//******************************************************************************
// Set saturation if color components are sorted in ascending order.
//******************************************************************************
float3 Color_SetSaturation_MinMidMax(float3 cSorted, float s)
{
	if(cSorted.z > cSorted.x)
	{
		cSorted.y = (((cSorted.y - cSorted.x) * s) / (cSorted.z - cSorted.x));
		cSorted.z = s;
	}
	else
	{
		cSorted.y = 0;
		cSorted.z = 0;
	}

	cSorted.x = 0;

	return cSorted;
}

//******************************************************************************
//******************************************************************************
float3 Color_SetSaturation(float3 c, float s)
{
	if (c.r <= c.g && c.r <= c.b)
	{
		if (c.g <= c.b)
			c.rgb = Color_SetSaturation_MinMidMax(c.rgb, s);
		else
			c.rbg = Color_SetSaturation_MinMidMax(c.rbg, s);
	}
	else if (c.g <= c.r && c.g <= c.b)
	{
		if (c.r <= c.b)
			c.grb = Color_SetSaturation_MinMidMax(c.grb, s);
		else
			c.gbr = Color_SetSaturation_MinMidMax(c.gbr, s);
	}
	else
	{
		if (c.r <= c.g)
			c.brg = Color_SetSaturation_MinMidMax(c.brg, s);
		else
			c.bgr = Color_SetSaturation_MinMidMax(c.bgr, s);
	}
    
	return c;
}

We can now implement the non-separable color blend modes:

  • Hue
  • Saturation
  • Color
  • Luminosity
  • Lighter Color
  • Darker Color
//******************************************************************************
// Creates a color with the hue of the blend color and the saturation and
// luminosity of the base color.
//******************************************************************************
float3 BlendMode_Hue(float3 base, float3 blend)
{
	return Color_SetLuminosity(Color_SetSaturation(blend, Color_GetSaturation(base)), Color_GetLuminosity(base));
}

//******************************************************************************
// Creates a color with the saturation of the blend color and the hue and
// luminosity of the base color. 
//******************************************************************************
float3 BlendMode_Saturation(float3 base, float3 blend)
{
	return Color_SetLuminosity(Color_SetSaturation(base, Color_GetSaturation(blend)), Color_GetLuminosity(base));
}

//******************************************************************************
// Creates a color with the hue and saturation of the blend color and the 
// luminosity of the base color.
//******************************************************************************
float3 BlendMode_Color(float3 base, float3 blend)
{
	return Color_SetLuminosity(blend, Color_GetLuminosity(base));
}

//******************************************************************************
// Creates a color with the luminosity of the blend color and the hue and 
// saturation of the base color. 
//******************************************************************************
float3 BlendMode_Luminosity(float3 base, float3 blend)
{
	return Color_SetLuminosity(base, Color_GetLuminosity(blend));
}

//******************************************************************************
// Compares the total of all channel values for the blend and base color and 
// displays the lower value color.
//******************************************************************************
float3 BlendMode_DarkerColor(float3 base, float3 blend)
{
	return Color_GetLuminosity(base) <= Color_GetLuminosity(blend) ? base : blend;
}

//******************************************************************************
// Compares the total of all channel values for the blend and base color and 
// displays the higher value color. 
//******************************************************************************
float3 BlendMode_LighterColor(float3 base, float3 blend)
{
	return Color_GetLuminosity(base) > Color_GetLuminosity(blend) ? base : blend;
}

Comments