OpenGL Indexed Color

John Tsiombikas nuclear@mutantstargoat.com

20 August 2022

Indexed color support in OpenGL is one of those obscure parts of the API from the distant past of its birth in the early 90s on Silicon Graphics (SGI) computers. It's a remnant of an era, when the price of RAM was the main factor in compromises between functionality and system price. Even in the space of ludicrously expensive SGI workstations, such compromises punch through. Some of the early SGI workstations, like the IRIS indigo, had low color options with 8 bit per pixel framebuffers, and the Z-buffer (with its own dedicated memory) was an optional add-on board.

Even though 8bit color framebuffers were still common by the early 90s when OpenGL was born, which necessitated adding something about palettized modes in the design of the API, indexed color support seems very much like a second class citizen; a bolted-on addition, rather than a core part of the design. Part of that was probably a focus on the more capable higher-end rendering systems, and part of it was likely due to the fact that SGIs very capable rasterization hardware performed excellent dithering, and could produce convicing results even in ridiculously low bits-per-pixel RGB visuals, like 16-color RGB121!. No matter the motivation, the decision to focus on RGB for the design of OpenGL proved correct, since by the second part of the 1990s the price of RAM dropped and even lowly PCs usually had true-color framebuffers.

8bpp and 4bpp RGB

In just such an environment we return today, on an SGI Indigo with the XS8 graphics board, running IRIX 5.3, to explore the indexed color parts of the OpenGL API.

Setting up the colormap

OpenGL with its main design pillar of portability, steps lightly around anything which has to do with the window system. Colormap (palette) management is one of those things which are very different from system to system, and very much in the domain of the window system, and therefore left to it. OpenGL does not provide any functionality to alter the palette, leaving it to system-specific APIs instead. In X11 this is done through XCreateColormap, XStoreColor and so on, but for simplicitly let's leave it up to GLUT to handle this. glutSetColor(index, r, g, b) will let us set colors to the palette, while glutGet(GLUT_WINDOW_COLORMAP_SIZE) will tell us how many colors we can use (generally 256 colors for 8bit visuals, 16 colors for 4bit visuals).

If we set a red and a blue color in the palette, we can use them to draw shapes in those colors:

glutSetColor(1, 1, 0, 0);
glutSetColor(2, 0, 0, 1);

glPushMatrix();
glTranslatef(-1.5, 0, 0);
glIndexi(1);              /* use color 1 (red) */
glutSolidSphere(1, 20, 10);
glPopMatrix();

glPushMatrix();
glTranslatef(1.5, 0, 0);
glIndexi(2);              /* use color 2 (blue) */
glutSolidTorus(0.35, 0.9, 12, 24);
glPopMatrix();

red and blue shapes

Of course solid color shapes are not very interesting. To properly visualize 3D objects we need lighting, and for that we need to set up the palette with appropriate color ramps. But before we can set up a palette, we need to understand how lighting works in indexed color mode.

Originally I would go ahead describing indexed color lighting and how it differs from the usual RGB mode lighting calculations, but it occurs to me that many younger readers might not be intimately familiar with that either, since the fixed function pipeline of OpenGL is not as ubiquitous as it once was. So let's go over it quickly first.

OpenGL lighting in RGB mode

OpenGL implements per-vertex Blinn-Phong lighting. We provide a normal vector for each vertex, the position and color of light sources, and material properties of the surface (mainly diffuse color, specular color, and specular exponent), and OpenGL computes the final color of that vertex as (simplified):

simplified lighting equation

Where a is ambient, d is diffuse, s is specular, m is the specular exponent, l is the light color, N is the normal vector, L is the light direction, and H is the half-angle vector between the view direction and the light direction.

Light source properties

Light sources are defined with the glLight family of functions. For instance this snippet defines a red-ish light source at position (-5, 5, 10):

float pos[] = {-5, 5, 10, 1};
float col[] = {1.0, 0.8, 0.4, 1};
glLightfv(GL_LIGHT0, GL_POSITION, pos);
glLightfv(GL_LIGHT0, GL_DIFFUSE, col);
glLightfv(GL_LIGHT0, GL_SPECULAR, col);

Light positions are multiplied by the current modelview matrix, so for this to be a world space position, it needs to be called after setting up the view matrix, and before concatenating the model matrix.

Material properties

Materials are defined with glMaterial calls. For instance this snippet defines a shiny cyan surface:

float dif[] = {0.1, 0.9, 1.0, 1};
float spec[] = {1, 1, 1, 1};
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, dif);
glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, spec);
glMaterialfv(GL_FRONT_AND_BACK, GL_SHININESS, 50);   /* exponent */

OpenGL lighting in indexed color mode

In indexed color mode we're no longer dealing with separate R G and B components, but rather with palette indices. Everything we saw in the previous section goes out the window. Instead OpenGL pre-supposes that we have constructed the colormap in such a way, as to have a range of colors going from unlit (or ambient) to fully lit diffuse, and from fully lit diffuse, to peak specular. OpenGL disregards all material properties other than GL_SHININESS, which still controls the specular exponent, and instead introduces a special material property called GL_COLOR_INDEXES (sic). This new property lets us provide an array of 3 color indices defining our color ramp, corresponding to the unlit/ambient color, the fully lit diffuse color, and the maximum specularity color.

To calculate lighting and assign an appropriate color index to each vertex, OpenGL calculates the specular term (N.H)^m; if it is greater than 0 (so we're in the area of the specular highlight), it interpolates between the second and third index. If the specular term is 0, then it uses the diffuse term (N.L) as the interpolation parameter between the first and the second index.

So let's construct such a colormap with two distinct lighting ramps, one for a red object and one for a blue object:

for(i=0; i<128; i++) {
    if(i < 96) {
        x = i / 95.0;
        glutSetColor(i, x, 0, 0);
        glutSetColor(i + 128, 0, 0, x);
    } else {
        x = (i - 96) / 31.0;
        glutSetColor(i, 1, x, x);
        glutSetColor(i + 128, x, x, 1);
    }
}

The above snippet produces this colormap:

2-ramp colormap

Having set up the palette, we can now render our two objects, setting the color ramp indices before drawing each object.

int redramp[] = {0, 95, 127};
int blueramp[] = {128, 223, 255};

glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 40);

glMaterialiv(GL_FRONT_AND_BACK, GL_COLOR_INDEXES, redramp);
glPushMatrix();
glTranslatef(-1.5, 0, 0);
glutSolidSphere(1, 30, 15);
glPopMatrix();

glMaterialiv(GL_FRONT_AND_BACK, GL_COLOR_INDEXES, blueramp);
glPushMatrix();
glTranslatef(1.5, 0, 0);
glutSolidTorus(0.35, 0.9, 20, 40);
glPopMatrix();

The result looks like this:

indexed color lighting

Texture mapping in indexed color mode

That's not too bad, looks quite nice, now tell us how texture mapping works in indexed color mode.

... it doesn't.



Discuss this post

Back to my blog