I'm currently working on an little project in C++ and OpenGL and am trying to implement a colour selection tool similar to that in photoshop, as below.
However I am having trouble with interpolation of the large square. Working on my desktop computer with a 8800 GTS the result was similar but the blending wasn't as smooth.
This is the code I am using:
GLfloat swatch[] = { 0,0,0, 1,1,1, mR,mG,mB, 0,0,0 };
GLint swatchVert[] = { 400,700, 400,500, 600,500, 600,700 };
glVertexPointer(2, GL_INT, 0, swatchVert);
glColorPointer(3, GL_FLOAT, 0, swatch);
glDrawArrays(GL_QUADS, 0, 4);
Moving onto my laptop with Intel Graphics HD 3000, this result was even worse with no change in code.
I thought it was OpenGL splitting the quad into two triangles, so I tried rendering using triangles and interpolating the colour in the middle of the square myself but it still doesnt quite match the result I was hoping for.
OpenGL uses barycentric interpolation on the values emitted from a vertex shader to the per-fragment input values of a fragment shader. What you see there is the effect of splitting the quad into triangles as you already did guess right.
Now look at it: There's the top right triangle which has red in it, and hence interpolates nicely down with a red amount in it. And then there's the bottom left triangle with only gray in it. Of course the red of the top right triangle will not contribute to the gray of the lower left one.
The problem is, that the interpolation happens in RGB space, but for your desired outcome would have to be placed in HSV or HSL space, where H (Hue) would be kept constant.
However it's not just a matter of interpolation. What gets in your way as well is, that colours don't interpolate linearly; most displays have a nonlinear function applied, also known as the "gamma" (actually gamma is the exponent of the power applied to the input values).
Your OpenGL context may be in a color corrected color space or not. You need to test for this. Then knowing in which color space the framebuffer resides, you must apply a per fragment transform of the barycentric coordinates (evaluated using a vertex shader) into per fragment values using a fragment shader.
I've quickly made a vertex/fragment shader that successfully interpolates the colors:
#ifdef GL_ES
precision highp float;
#endif
uniform vec2 resolution;
void main(void)
{
vec2 p = gl_FragCoord.xy / resolution.xy;
float gray = 1.0 - p.x;
float red = p.y;
gl_FragColor = vec4(red, gray*red, gray*red, 1.0);
}
Here's the result:
Applying it on a quad now yields the correct result because the interpolation is truly done over the entire surface using x
and y
coordinates. See @datenwolf's detailed explanation as to why this works.
EDIT 1 In order to obtain the full range of colors in a functional color picker, it is possible to modify the hue interactively (see https://stackoverflow.com/a/9234854/570738).
Live online demo: http://goo.gl/Ivirl
#ifdef GL_ES
precision highp float;
#endif
uniform float time;
uniform vec2 resolution;
const vec4 kRGBToYPrime = vec4 (0.299, 0.587, 0.114, 0.0);
const vec4 kRGBToI = vec4 (0.596, -0.275, -0.321, 0.0);
const vec4 kRGBToQ = vec4 (0.212, -0.523, 0.311, 0.0);
const vec4 kYIQToR = vec4 (1.0, 0.956, 0.621, 0.0);
const vec4 kYIQToG = vec4 (1.0, -0.272, -0.647, 0.0);
const vec4 kYIQToB = vec4 (1.0, -1.107, 1.704, 0.0);
const float PI = 3.14159265358979323846264;
void adjustHue(inout vec4 color, float hueAdjust) {
// Convert to YIQ
float YPrime = dot (color, kRGBToYPrime);
float I = dot (color, kRGBToI);
float Q = dot (color, kRGBToQ);
// Calculate the hue and chroma
float hue = atan (Q, I);
float chroma = sqrt (I * I + Q * Q);
// Make the user's adjustments
hue += hueAdjust;
// Convert back to YIQ
Q = chroma * sin (hue);
I = chroma * cos (hue);
// Convert back to RGB
vec4 yIQ = vec4 (YPrime, I, Q, 0.0);
color.r = dot (yIQ, kYIQToR);
color.g = dot (yIQ, kYIQToG);
color.b = dot (yIQ, kYIQToB);
}
void main(void)
{
vec2 p = gl_FragCoord.xy / resolution.xy;
float gray = 1.0 - p.x;
float red = p.y;
vec4 color = vec4(red, gray*red, gray*red, 1.0);
adjustHue(color, mod(time, 2.0*PI));
gl_FragColor = color;
}
EDIT 2: If needed, shaders for use with texture coordinates (to apply on quads with texture coordinates from 0 to 1) should look something like this. Untested.
Fragment shader:
void main(void)
{
vec2 p = gl_TexCoord[0].st;
float gray = 1.0 - p.x;
float red = p.y;
gl_FragColor = vec4(red, gray*red, gray*red, 1.0);
}
A passthrough vertex shader:
void main()
{
gl_TexCoord[0]=gl_MultiTexCoord0;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
I didn't like the top voted answer since it is so literal and only really focuses on red in the simple answer so I wanted to come in with a simple alternative based on vertex color:
#version 330 core
smooth in vec4 color;
smooth in vec2 uv;
out vec4 colorResult;
void main(){
float uvX = 1.0 - uv.st.x;
float uvY = uv.st.y;
vec4 white = vec4(1, 1, 1, 1);
vec4 black = vec4(0, 0, 0, 1);
colorResult = mix(mix(color, white, uvX), black, uvY);
}
This is simple to explain and reason about. mix is a linear interpolation, or lerp in glsl. So I mix between the vertex color and white along the inverse x axis, this gives me white in the top left and pure color in the top right. With the result of that I mix to black along the y axis giving me black on the bottom.
Hope this helps, I came across this answer and found it wasn't very helpful in getting anything other than hard-baked red, green, or blue palettes, and the adjustHue bit was simply not what I wanted at all.
Basically to get this to work you want to set the all of the vertex colors to your desired color, and ensure your UV coordinates are set:
TL: UV(0, 0), COLOR(YOUR DESIRED COLOR)
BL: UV(0, 1), COLOR(YOUR DESIRED COLOR)
BR: UV(1, 1), COLOR(YOUR DESIRED COLOR)
TR: UV(1, 0), COLOR(YOUR DESIRED COLOR)
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