Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

OpenGL Colour Interpolation

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.

enter image description here

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.

http://i.imgur.com/wSJI2.png

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.

enter image description here

like image 847
Will-of-fortune Avatar asked Nov 03 '12 16:11

Will-of-fortune


3 Answers

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.

like image 160
datenwolf Avatar answered Oct 04 '22 13:10

datenwolf


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: enter image description here

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;
}
like image 44
num3ric Avatar answered Oct 04 '22 15:10

num3ric


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)
like image 44
M2tM Avatar answered Oct 04 '22 14:10

M2tM