OpenGL Colour Interpolation

11,481

Solution 1

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.

Solution 2

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;
}

Solution 3

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)
Share:
11,481
Will-of-fortune
Author by

Will-of-fortune

C++/OpenGL/Qt software developer.

Updated on June 05, 2022

Comments

  • Will-of-fortune
    Will-of-fortune almost 2 years

    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

  • Will-of-fortune
    Will-of-fortune over 11 years
    Thanks for your answer, I have very little experience with GLSL although I seem to remember reading if you want to use a shader you have to implement everything because it completely replaces the OpenGL pipeline? Is there a simple way of using the shader you provided and only applying it to one call of glDrawArrays()?
  • datenwolf
    datenwolf over 11 years
    @Will-of-fortune: As long as you stick with OpenGL-2.1 and below you can use shaders selectively. I.e. use the fixed function pipeline for most things, but a shader on only some. Only with OpenGL-3 core and later one must use shaders. But honestly: Using shaders makes life so much simpler, so I strongly suggest to use them everywhere.
  • num3ric
    num3ric over 11 years
    @Will-of-fortune I usually use frameworks or graphics engine which make managing shaders quite easier. However, I refer you to this: nehe.gamedev.net/article/glsl_an_introduction/25007 Skip to the "GLSL API How To Use GLSL In Your OpenGL Application" section. This section will show you how to incorporate glsl shaders better than I could in these comments.