How do I draw a rainbow in Freeglut?
Asked Answered
T

2

8

I'm trying to draw a rainbow-coloured plot legend in openGL. Here is what I've got so far:

glBegin(GL_QUADS);
for (int i = 0; i != legendElements; ++i)
{
    GLfloat const cellColorIntensity = (GLfloat) i / (GLfloat) legendElements;
    OpenGL::pSetHSV(cellColorIntensity*360.0f, 1.0f, 1.0f);

    // draw the ith legend element
    GLdouble const xLeft = xBeginRight - legendWidth;
    GLdouble const xRight = xBeginRight;
    GLdouble const yBottom = (GLdouble)i * legendHeight /
    (GLdouble)legendElements + legendHeight;
    GLdouble const yTop = yBottom + legendHeight;

    glVertex2d(xLeft, yTop); // top-left
    glVertex2d(xRight, yTop); // top-right
    glVertex2d(xRight, yBottom); // bottom-right
    glVertex2d(xLeft, yBottom); // bottom-left
}

glEnd();

legendElements is the number of discrete squares that make up the "rainbow". xLeft,xRight,yBottom and yTop are the vertices that make up each of the squared.

where the function OpenGL::pSetHSV looks like this:

void pSetHSV(float h, float s, float v) 
{
    // H [0, 360] S and V [0.0, 1.0].
    int i = (int)floor(h / 60.0f) % 6;
    float f = h / 60.0f - floor(h / 60.0f);
    float p = v * (float)(1 - s);
    float q = v * (float)(1 - s * f);
    float t = v * (float)(1 - (1 - f) * s);
    switch (i) 
    {
        case 0: glColor3f(v, t, p);
            break;
        case 1: glColor3f(q, v, p);
            break;
        case 2: glColor3f(p, v, t);
            break;
        case 3: glColor3f(p, q, v);
            break;
        case 4: glColor3f(t, p, v);
            break;
        case 5: glColor3f(v, p, q);
    }
}

I got that function from http://forum.openframeworks.cc/t/hsv-color-setting/770

However, when I draw this it looks like this:

enter image description here

What I would like is a spectrum of Red,Green,Blue,Indigo,Violet (so I want to iterate linearly through the Hue. However, this doesn't really seem to be what's happening.

I don't really understand how the RGB/HSV conversion in pSetHSV() works so it's hard for me to identify the problem..

EDIT: Here is the fixed version, as inspired by Jongware (the rectangles were being drawn incorrectly):

// draw legend elements
glBegin(GL_QUADS);
for (int i = 0; i != legendElements; ++i)
{
    GLfloat const cellColorIntensity = (GLfloat) i / (GLfloat) legendElements;
    OpenGL::pSetHSV(cellColorIntensity * 360.0f, 1.0f, 1.0f);

    // draw the ith legend element
    GLdouble const xLeft = xBeginRight - legendWidth;
    GLdouble const xRight = xBeginRight;
    GLdouble const yBottom = (GLdouble)i * legendHeight /
    (GLdouble)legendElements + legendHeight + yBeginBottom;
    GLdouble const yTop = yBottom + legendHeight / legendElements;

    glVertex2d(xLeft, yTop); // top-left
    glVertex2d(xRight, yTop); // top-right
    glVertex2d(xRight, yBottom); // bottom-right
    glVertex2d(xLeft, yBottom); // bottom-left
}

glEnd();
Teaching answered 3/3, 2014 at 7:43 Comment(7)
what legendElements was used for the picture? and on a random sidenote:GLdouble const yBottom = (1.0f + cellColorIntensity) * legendHeight;Nomination
@Nomination legendElements is the number of discrete squares that make up the "rainbow". xLeft,xRight,yBottom and yTop are the vertices that make up each of the squared. (Added this to the OP as well).Teaching
Can you try with each x coordinate offset a bit? Your calculation of the color seems alright, so I am wondering if your rectangles may be the wrong size, and the last one drawn would be the huge red one at the top.Paderewski
@ArmanSchwarz that much was clear. i meant what value of legendElements was used for the picture aboveNomination
added some images for comparison to my answer ...Pearlpearla
@Nomination 25 I think...Teaching
@Paderewski you were right...Teaching
P
8

I generate spectral colors like this:

void spectral_color(double &r,double &g,double &b,double l) // RGB <- lambda l = < 380,780 > [nm]
    {
         if (l<380.0) r=     0.00;
    else if (l<400.0) r=0.05-0.05*sin(M_PI*(l-366.0)/ 33.0);
    else if (l<435.0) r=     0.31*sin(M_PI*(l-395.0)/ 81.0);
    else if (l<460.0) r=     0.31*sin(M_PI*(l-412.0)/ 48.0);
    else if (l<540.0) r=     0.00;
    else if (l<590.0) r=     0.99*sin(M_PI*(l-540.0)/104.0);
    else if (l<670.0) r=     1.00*sin(M_PI*(l-507.0)/182.0);
    else if (l<730.0) r=0.32-0.32*sin(M_PI*(l-670.0)/128.0);
    else              r=     0.00;
         if (l<454.0) g=     0.00;
    else if (l<617.0) g=     0.78*sin(M_PI*(l-454.0)/163.0);
    else              g=     0.00;
         if (l<380.0) b=     0.00;
    else if (l<400.0) b=0.14-0.14*sin(M_PI*(l-364.0)/ 35.0);
    else if (l<445.0) b=     0.96*sin(M_PI*(l-395.0)/104.0);
    else if (l<510.0) b=     0.96*sin(M_PI*(l-377.0)/133.0);
    else              b=     0.00;
    }
  • l is input wavelength [nm] < 380,780 >
  • r,g,b is output RGB color < 0,1 >

This is simple rough sin wave approximation of real spectral color data. You can also create table from this and interpolate it or use texture ... output colors are:

spectral_colors

there are also different approaches like:

  1. linear color - composite gradients

  2. human eye X,Y,Z sensitivity curves integration

    you have to have really precise X,Y,Z curves, even slight deviation causes 'unrealistic' colors like in this example

    human eye XYZ sensitivity

To make it better you have to normalize colors and add exponential sensitivity corrections. Also these curves are changing with every generation and are different in different regions of world. So unless you are doing some special medical/physics softs it is not a good idea to go this way.

spectral colors comparison

| <- 380nm ----------------------------------------------------------------- 780nm -> |

[edit1] here is mine new physically more accurate conversion

I strongly recommend to use this approach instead (it is more accurate and better in any way)

Pearlpearla answered 3/3, 2014 at 14:16 Comment(2)
Cool, I'm on the road at the moment but I'll check this out when I get home. You have taken the "rainbow" concept a little more literally than I had (I really just wanted to iterate through "Hue" values but this great).Teaching
I found the problem myself and updated the post. This is still a very good answer though, so I'll mark it as the answer. Thanks!Teaching
K
2

Well, not completely right. Here I made a javascript example of it. Sodium yellow (589nm) is too orange and Halpha red (656nm) is too brown....

Save this example into an HTML file (jquery needed) and load it into a browser: page.html?l=[nanometers]

<!DOCTYPE html>
<html><head>
<script src='jquery.js'></script>
<script>


/*
 * Return parameter value of name (case sensitive !)
 */
function get_value(parametername)
{
    readvalue=(location.search ? location.search.substring(1) : false);

    if (readvalue)
    {
       parameter=readvalue.split('&');
       for (i=0; i<parameter.length; i++)
       {
           if (parameter[i].split('=')[0] == parametername)
             return parameter[i].split('=')[1];
       }
    }
    return false;
}

function spectral_color(l) // RGB <- lambda l = < 380,780 > [nm]
{
  var M_PI=Math.PI;
  var r=0,g,b;
  if (l<380.0) r=     0.00;
  else if (l<400.0) r=0.05-0.05*Math.sin(M_PI*(l-366.0)/ 33.0);
  else if (l<435.0) r=     0.31*Math.sin(M_PI*(l-395.0)/ 81.0);
  else if (l<460.0) r=     0.31*Math.sin(M_PI*(l-412.0)/ 48.0);
  else if (l<540.0) r=     0.00;
  else if (l<590.0) r=     0.99*Math.sin(M_PI*(l-540.0)/104.0);
  else if (l<670.0) r=     1.00*Math.sin(M_PI*(l-507.0)/182.0);
  else if (l<730.0) r=0.32-0.32*Math.sin(M_PI*(l-670.0)/128.0);
  else              r=     0.00;
       if (l<454.0) g=     0.00;
  else if (l<617.0) g=     0.78*Math.sin(M_PI*(l-454.0)/163.0);
  else              g=     0.00;
       if (l<380.0) b=     0.00;
  else if (l<400.0) b=0.14-0.14*Math.sin(M_PI*(l-364.0)/ 35.0);
  else if (l<445.0) b=     0.96*Math.sin(M_PI*(l-395.0)/104.0);
  else if (l<510.0) b=     0.96*Math.sin(M_PI*(l-377.0)/133.0);
  else              b=     0.00;
  var rgb = Math.floor(r*256)*65536+Math.floor(g*256)*256 + Math.floor(b*256);
  rgb = '000000' + rgb.toString(16);
  rgb = '#' + rgb.substr(-6).toUpperCase();

  $('#color').html([r,g,b,rgb,l]);
  $('body').css('background-color', rgb);
}

</script>
</head><body>
<div id='color'></div>
<script>
spectral_color(get_value('l'));
</script>
</body>
</html>
Kissner answered 3/8, 2014 at 19:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.