« Animating Worley Noise | Main | Mac OpenGL contexts »

May 10, 2005

Straight Alpha and Bilinear Filtering

There's a subtle problem using graphics hardware to do bilinear filtering on straight alpha images.

Take an image with a single pixel of full white, completely opaque, surrounded by fully transparent pixels, with black in their RGB channels

if you use bilinear filtering to get color and alpha values for a point halfway between the white pixel and a black one, this is what you end up with:

If you render this texture scaled up using the standard blending equation for straight alpha, glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA) then you'll see black fringing around the white pixel.

In games, this is not a problem, because you just make sure any zero alpha pixels in your artwork have the correct straight color values.

Where this does become an issue is when the image is generated as an intermediate stage of an image processing pipeline. For example, if you read in images that are pre-multiplied with black alpha, and convert them to straight alpha for the pipeline, then you have no way of telling what the correct color values for fully transparent pixels are.

Another example is drawing a polygon into a pbuffer or render texture. Nothing's drawn into the fully transparent areas, and there's no definitive way of picking a color to put in those pixels.

We looked at several solutions to this:

- Do a processing pass that averages the rgb values of the non-transparent neighbors of each zero alpha pixel, and replaces the existing color with that, or some other way of 'growing' good rgb values into the zero alpha areas.

Though this would probably do a decent job of removing the fringing in real world images, I could still construct cases where this would fail, such as a zero alpha pixel between black and white opaque pixels:

This would mean an extra pass over all of our intermediate textures, which would hit performance.

- Ignore any pixels with zero alpha when doing the bilinear filtering

Before each lerp in the bilinear process, do a check to see if one of the pixels to be lerped between has a zero alpha, and replace it with the other non-zero alpha pixel rather than lerping between them if so.

In pseudo-code, here's the original bilinear process

topColor = lerp(topLeftNeighbor,topRightNeighbor, fractionalX);
bottomColor = lerp(bottomLeftNeighbour,bottomRightNeighbor, fractionalX);
result = lerp(topColor, bottomColor, fractionalY);

and here's the version that checks for zero alpha

if (topLeftNeighbor.alpha==0)
topColor = topRightNeighbor;
else if (topRightNeighbor.alpha==0)
topColor = topLeftNeighbor;
else
topColor = lerp(topLeftNeighbor,topRightNeighbor, fractionalX);

if (bottomLeftNeighbor.alpha==0)
bottomColor = bottomRightNeighbor;
else if (bottomRightNeighbor.alpha==0)
bottomColor = bottomLeftNeighbor;
else
bottomColor = lerp(bottomLeftNeighbour,bottomRightNeighbor, fractionalX);

if (topColor.alpha==0)
result = bottomColor;
else if (bottomColor.alpha==0)
result = topColor;
else
result = lerp(topColor, bottomColor, fractionalY);

This would fix the problem, but requires 4 texture access's if implemented using OpenGL fragment programs, as well as being rather inelegant.

- Change the pipeline to use premultiplied with black

Using glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA) lets you render premultiplied with black alpha textures correctly, and bilinear filtering does the right thing. In the original case, the neighborhood of the white pixel evaluates to a full white (premultiplied by the alpha which drops away), removing the dark fringing.

This is what we did, even though it seems to go against the grain of normally using straight alpha with graphics cards, unlike the other solutions it didn't hurt performance.

Posted by petewarden at May 10, 2005 05:00 PM

Comments

Post a comment




Remember Me?