Replacing Colors with WebGL

This is a prototype demonstrating how to manipulate specific colors in an image using a WebGL fragment shader. Click on the color chips to the right of the game controller to replace a color.

Note: for simplicity I'm using using basic HTML color input to pick colors in this demo. It should work for most browsers, but the actual color picker implementation is left up to the browser. I've found it works best with the desktop version of Chrome.

Click a color chip

8-bit RGB is typically represented by a color cube with dimensions from 0-255. Because vectors, WebGL uses a color cube with dimensions from 0-1, so we have to scale our color math proportionally.

As always, keep in mind that you cannot perform equality operations on floating point values, like the ones in our vectors. Instead, check for "close enough" matches based on your application's required tolerance. Depending on your usecase, you may find a broad tolerance is useful so that you can match a range of close-ish colors.

Colors to Vectors

We need to rescale our colors from 0-255 per dimension to 0-1. There's a lot of ways to do this but if you already have your color as a number, you can do it quite simply by using a binary AND to grab the digits for each color and then scaling them proportionally. We've added a 1.0 to the alpha channel to keep our color vectors the same size as texture2D's output.


return [
  (color & 0xFF0000) / 0xFF0000,
  (color & 0xFF00)   / 0xFF00,
  (color & 0xFF)     / 0xFF,
  1.0
]
      

Keep in mind that WebGL expects you to pass arrays of vectors as one large flat array of values, so don't nest your values in a parent array if you're working with multiple colors.

Fragment Shader

Swapping colors is really just a matter of looking for a close enough match and using your new color if you find one.

Notice that we use the dot product of our vector's difference to find our distance squared. Square roots are expensive operations and in this case we don't necessarily need the true distance to estimate that we have a color match.


#define MAX_COLORS 24
#define COLOR_TOLERANCE 0.001

precision highp float;
varying vec2 vTex;
uniform sampler2D sampler0;

uniform vec4 u_new_colors[MAX_COLORS];
uniform vec4 u_original_colors[MAX_COLORS];
uniform int u_colors_count;

vec4 colorSwap(vec4 oldColor[MAX_COLORS], vec4 newColor[MAX_COLORS], vec4 imageColor) {
  for (int i = 0; i < MAX_COLORS; i++) {
    if (i < u_colors_count) {
      // Calculate distance^2 to avoid using an expensive square root operation
      vec4 delta = oldColor[i] - imageColor;
      float distSquared = dot(delta, delta);

      if (distSquared < COLOR_TOLERANCE) return newColor[i];
    }
  }
  return imageColor; // No Match, keep the original color
}

void main(void){
  gl_FragColor = colorSwap(u_original_colors, u_new_colors, texture2D(sampler0, vTex));
}
      

There's no reason that you need to swap to one one specific other color as well. You could use this same technique to replace a certain color with the colors from another texture in the style of a television studio green screen.

With a few minor mondifications, you could also swap colors from another source like a video.

The source for his page is available here.