I’m working on an app that creates images whose hue, saturation, and value change according to different parameters. For performance reasons, it would make sense to render the hue, saturation, and value components separately, and then composite them together using Photoshop-style blending modes (multiply, overlay, screen, hue, etc).
I already know how to do this for RGB images: split each channel into its own red, green, or blue image with values ranging from transparent to that channel’s color. Layer them together on top of black and set their blend mode to Screen, and hey presto, you have your color image:
How would I do this with an image defined by HSV values? My app often changes one of these channels without changing the other two, and it would speed up my rendering if I could composite existing images on the GPU instead of rendering a completely new image every time something changes.
Here’s an example:
In this example, the hue varies from 0º to 360º around the circumference, the saturation varies from 0% to 100% from the center to the edge, and the brightness (V) varies from 0% to 100% around the circumference. This is typical of the kind of image my app creates. Is there a combination of common blending modes I could use to create these channels separately and composite them in a mathematically perfect way?
Real World Example of the Color Blend Mode Simply add a new blank layer above your image and set the blend mode of the layer to Color. Select your Brush Tool from the Tools palette, choose the color you want to paint with, and begin painting on the layer to add your color.
1. HSV Color Scale: The HSV (which stands for Hue Saturation Value) scale provides a numerical readout of your image that corresponds to the color names contained therein. Hue is measured in degrees from 0 to 360. For instance, cyan falls between 181–240 degrees, and magenta falls between 301–360 degrees.
My app often changes one of these channels without changing the other two, and it would speed up my rendering if I could composite existing images on the GPU instead of rendering a completely new image every time something changes. [OP, @ZevEisenberg]
With regard to fastness and the GPU, I'd just throw a conversion function into a fragment shader (e.g.). This would read HSV stored in a texture or three different textures, do the conversion per-pixel and output RGB. Nice and easy. I can't see any benefit to not changing other layers since either H, S or V will affect all RGB channels. Perhaps storing intermediate RGB results such as hue=hsv2rgb(H,1,1)
, and updating with final=(hue*S+1-S)*V
, caching hue-to-rgb but I don't think it's worth it.
Anyway, each blend mode has a simple formula, and you could string them together for HSV involving an overly complex set of intermediate textures, but it will be much slower primarily because of the unnecessary temporary storage and memory bandwidth. Not to mention, trying to rewrite the formula into blend functions sounds pretty challenging, what with branching, divisions, fract
, clamping, absolutes etc...
I am very interested in an a solution for splitting an Image into its HSV components and recreate the original Image using blend modes in Photoshop. [Bounty, @phisch]
With regard to photoshop... I'm not made of money. So in gimp, there's Colours -> Components -> Compose/Decompose
which does this for you. I'd be kinda surprised if this doesn't exist in photoshop but then also kinda not. Perhaps there are photoshop scripts/plugins that could do it if not? But you did specifically say blending. Your question might get better attention at https://graphicdesign.stackexchange.com/. Below, I've given an idea of the complexity involved and I doubt photoshop can actually do it. There may be ways around pixel values outside 0 to 1, but then you might run into precision issues, it just shouldn't be done.
Anyway, a challenge is a challenge despite how impractical. The following is just for fun.
I'll start with the following function (from here) and three HSV textures...
vec3 hsv2rgb(vec3 c)
{
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
I only know OpenGL and I'm not sure how I'd do this without floating point textures or some of the extended blending functions, so I'm using them. But I'm only allowed to use blending (no shaders whatsoever). For the constants I'll make textures with (1,1,1), (1,2/3,1/3), (3,3,3), (6,6,6) (1/255,1/255,1/255), (255,255,255), (1/2,1/2,1/2) and (0,0,0) because I couldn't get GL_ZERO to scale with GL_DIFFERENCE_NV
.
find the fractional part
floor()
as I'm assuming GL rounds colours when converting to 8 bits. if not, skip this)scale back up by 255 (back into a floating point texture)
now we have the integer component. subtract this from what we started with
scale by 6
take the absolute of the value
I'm going to simply use GL_DIFFERENCE_NV
for this, but without it there might be a way using two separate clamps for the next step. since negatives will be clamped anyway, something along the lines of clamp(p-K.xxx,0,1) + clamp(-p-K.xxx,0,1)
.
subtract 1
well, that's hue done
could clamp by passing through a non-floating point texture, but just going to use GL_MIN
now I could use alpha blending for mix()
, but saturation is loaded as a B/W image without an alpha channel. since it's mixing white, doing it by hand is actually easier...
scale by the saturation
subtract the saturation
and saturation has been applied
scale by value
and there's the image
coffee break
All done using
glBlendEquation
with GL_FUNC_REVERSE_SUBTRACT
, GL_MIN
and GL_DIFFERENCE_NV
glBlendFunc
Here's my code...
//const tex init
constTex[0] = makeTex() with 1, 1, 1...
constTex[1] = makeTex() with 1, 2/3, 1/3...
constTex[2] = makeTex() with 3, 3, 3...
constTex[3] = makeTex() with 6, 6, 6...
constTex[4] = makeTex() with 1/255, 1/255, 1/255...
constTex[5] = makeTex() with 255, 255, 255...
constTex[6] = makeTex() with 1/2, 1/2, 1/2...
constTex[7] = makeTex() with 0, 0, 0...
...
fbo[0] = makeFBO() with GL_RGB
fbo[1] = makeFBO() with GL_RGB32F
fbo[2] = makeFBO() with GL_RGB32F
...
hsv[0] = loadTex() hue
hsv[1] = loadTex() value
hsv[2] = loadTex() saturation
...
fbo[1].bind();
glDisable(GL_BLEND);
draw(hsv[0]); //start with hue
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ONE, GL_ONE); //add
draw(constTex[1]); //(1, 2/3, 1/3)
glBlendFunc(GL_ONE, GL_ONE);
fbo[1].unbind();
//compute integer part
fbo[2].bind();
glDisable(GL_BLEND);
draw(*fbo[1].colour[0]); //copy the last bit
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_REVERSE_SUBTRACT);
glBlendFunc(GL_ONE, GL_ONE); //subtract
draw(constTex[6]); //0.5
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ZERO, GL_SRC_COLOR); //scale down
draw(constTex[4]); //1/255
fbo[2].unbind();
fbo[0].bind(); //floor to integer
glDisable(GL_BLEND);
draw(*fbo[2].colour[0]);
fbo[0].unbind();
fbo[2].bind(); //scale back up
glDisable(GL_BLEND);
draw(*fbo[0].colour[0]);
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ZERO, GL_SRC_COLOR); //scale up
draw(constTex[5]); //255
fbo[2].unbind();
//take integer part for fractional
fbo[1].bind();
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_REVERSE_SUBTRACT);
glBlendFunc(GL_ONE, GL_ONE); //subtract
draw(*fbo[2].colour[0]); //integer part
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ZERO, GL_SRC_COLOR); //scale
draw(constTex[3]); //6
glBlendEquation(GL_FUNC_REVERSE_SUBTRACT);
glBlendFunc(GL_ONE, GL_ONE); //subtract
draw(constTex[2]); //3
glBlendEquation(GL_DIFFERENCE_NV);
glBlendFunc(GL_ZERO, GL_ONE); //take the absolute
draw(constTex[7]); //0
glBlendEquation(GL_FUNC_REVERSE_SUBTRACT);
glBlendFunc(GL_ONE, GL_ONE); //subtract
draw(constTex[0]); //1
glBlendEquation(GL_MIN);
glBlendFunc(GL_ONE, GL_ONE); //clamp (<0 doesn't matter, >1 use min)
draw(constTex[0]); //1
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ZERO, GL_SRC_COLOR); //scale
draw(hsv[1]); //saturation
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ONE, GL_ONE); //add
draw(constTex[0]); //1
glBlendEquation(GL_FUNC_REVERSE_SUBTRACT);
glBlendFunc(GL_ONE, GL_ONE); //subtract
draw(hsv[1]); //saturation
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ZERO, GL_SRC_COLOR); //scale
draw(hsv[2]); //saturation
fbo[1].unbind();
fbo[1].blit(); //check result
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With