| |
|
|
||||
![]() |
||||||
| |
|
|||||
|
Blending in Alpha Channels Alpha channels come in handy mainly for 3D rendering. The alpha channel is actually an opacity value for each pixel of a bitmap. In 3D graphics, the alpha channel is stored with each pixel’s color information. On a disk, however, the situation is different. Very few graphics file formats are capable of storing an alpha channel in the same file as the RGB data. A common solution is to store alpha channels as separate bitmaps, using the lightness of pixels as an alpha channel into another RGB bitmap. 3D Studio Max and several other rendering systems support this approach. Now that we’ve used our converter to convert RGB data, we can try assigning it to other tasks. Heck, it’s dealing with components’ bit masks, positions, and all that stuff, so why not put it to further good use? Let’s declare the following function in the class: bool AlphaBlend(void *src_data, void *dst_data, size_t sx, size_t sy, size_t src_pitch, size_t dst_pitch, S_pixelformat *src_format, dword flags) const; Now let’s assume that the destination buffer already contains properly converted RGB data in the same pixel format, for which the converter is currently initialized. The source data is an opacity bitmap that we want to mix into the alpha channel of the destination bitmap. Our implementation uses a different approach than the Convert method - the speed is less important here, so all possible conversions are done in one loop. For each pixel, we must extract a single RGB component from the opacity bitmap and compute the overall lightness of each pixel. The formula is lightness = red * 0.3 + green * 0.6 + blue * 0.1, based on the sensitivity of the human eye to single components of the spectrum. The computed lightness value is in the range from 0 to 255. Note that it isn’t necessary to use floating-point math for this computation; rather, we can use integer math: lightness = ( (int)red * 77 + (int)green * 154 + blue * 25 ) / 256 where the compiler may replace the division by a shift. We might also decide to extract alpha information from only one component. In this phase, it’s trivial to add the option to invert the alpha channel by performing the following calculation: lightness = 255 - lightness We could specify this option with the function's input parameter flags. 3D Studio Max offers us the ability to use inverted alpha channels, so why not aim for maximum compatibility? After this step, we can add a few lines to enable dithering. Alpha channels are often narrow in depth; the common mode is 4444, where only 4 bits are reserved for an alpha channel. Dithering an alpha channel really improves image quality, especially if the RGB bitmap has very low contrast. Our final step is to encode the alpha component into a pixel. The code now branches based on the destination pixel depth. For each case, we first need to mask off the previous alpha value and add a new alpha value, which must be shifted to its proper position and masked by an alpha bit mask of the current pixel format. That’s all there is to blending alpha-channels. It’s that simple! Figures 6 through 9 show an example of alpha mixing
You may wonder why I suggested setting the alpha value of the pixel during RGB conversion to a maximum value. If the loading or mixing of an opacity map fails for whatever reason, its better to see the RGB bitmap in full opacity, rather than in zero opacity (which you can’t see at all). Color-Key Substitution Color-keying is a technique wherein a certain color in a bitmap (or a range of colors) is assumed to be transparent and is not copied to the destination surface during blitting or rendering. Many developers use color-keying in 2D graphics for sprites, for example, as well as for textures in 3D, where one could render a fence merely by defining what pixels in a texture are transparent. Most hardware supports color-keying in some form. But color-keying presents some problems - the edges of color-keyed textures are tricky. Most 3D cards use color-keying to blend textures to the edge of the transparent color. So, for example, the blended edges of a green leaf texture might look black or white (or whatever other color is supposed to be transparent). We can mitigate this effect by choosing a color-key that is similar to other colors contained in the texture. Achieving exactly the right transparent color is difficult due to fact that color information is sometimes lost during certain conversions. When color information is lost during conversion, some pixels may become identical, which can cause unexpected parts of texture to become transparent (Figure 10). A better approach is to replace color-keying by alpha channels. All decent 3D hardware offers texture formats with a 1-bit alpha channel (for example 1555 high-color mode). The edges of alpha-blended textures fade out nicely to translucency (Figure 11).
We can define transparent pixel information in a bitmap in the same way as with color-keying - a special color defines transparent parts of an image. All we need is a function that will scan an entire bitmap and set a pixel’s alpha value to translucent if the pixel equals the color-key value and to opaque if otherwise. We don’t need to limit this function to 1-bit alpha channels - we’re better off making it more flexible - so we’ll set the alpha value to its maximum intensity. We can declare the new method that we’re adding into our converter as bool CKeyToAlpha(void *dest_data, size_t sx, size_t sy, size_t pitch, unsigned long color_key) const; This function only operates on an RGBA surface that is already initialized in the same pixel format for which the converter is currently initialized. We need to scan every single pixel of the surface and compare it with the color_key parameter. Note that color_key must be in the surface’s pixel format. If the pixel matches the color-key, its alpha value is set to zero; otherwise it’s set to maximum. Because we’re only dealing with minimal and maximal alpha values, all we need to do is either clear or set all of the alpha bits in a pixel. MIP-maps Generating MIP-maps is another task suitable for our converter. Though we could generate MIP-maps in one of several various ways, I’ll describe one simple method. This implementation is not particularly fast, however, because it uses conversions/memory transfers that could be avoided. Anyhow, using our existing conversion function, we can generate a MIP-map chain by writing only a few lines of code. We need to filter down each MIP-map level to one-quarter of the size of the original bitmap. We average every neighboring 2x2 pixels of source bitmap; the result is a single pixel of a new MIP-map level. The most comfortable way to generate MIP-maps is to use the 32-bit pixel format containing ARGB values in 8-bit depth each. Our first task is to allocate memory in which to hold the original bitmap in the 32-bit format. Then we call the Convert function to convert the source bitmap from whatever format it may be to the 32-bit ARGB format. We’ll need to add a function to filter the pixels. To avoid allocating more memory, we can put the results of our filtering operation into the same buffer that holds our source data. Listing 2 shows an example of such function. We can create MIP-maps up to the smallest possible bitmap resolution that current hardware supports. Most hardware supports MIP-map surfaces in which the smaller side is one pixel wide. After filtering one level, we’ll need to convert the data back to the original bitmap format. We can use our converter’s Convert() function, which gives the memory buffer as the source bitmap and a Locked surface of a MIP-map level as a destination buffer. It should be noted that combining MIP-map generation and color-keying results in aggregate effects - the color-keys in the bitmap shrink after each filtering. The smaller the MIP-map level, the less transparent it usually appears. Also, formats with a 1-bit alpha channel can produce unwanted artifacts. The average alpha value of four pixels may result in only two values, 0 or 1. If we’re going to have to generate MIP-map levels from color-keyed textures, we’d be better off choosing a texture format with a wider alpha channel. Filters The previous paragraph hints at some additional tasks that we could implement in our converter class. Let’s call all the other operations on a bitmap "filtering." We can define filtering as any operation over existing bitmap data (or part of it). Photoshop filters offer an easy-to-visualize example - adjusting the brightness level of image or its color saturation, converting bitmaps to black and white, or combine two bitmaps together. Another example of filtering is shrinking a bitmap to one-quarter its original size to generate the next MIP-map level, as described in the previous paragraph. We can create whatever filter we need, but more filters require more lines of code, and good performance requires even more code. For this article, however, let’s simply create a filter that converts textures to grayscale at run time. Furthermore, let’s define an expandable interface so that we can add more effects in the future, rather than writing a specialized function that only makes a bitmap black and white. The filtering function takes arguments such as a pointer to bitmap memory, its resolution and pitch, the filter ID (a value identifying the requested filter), and additional data that depends on the kind of filter we’re creating (brightness level, for example, or a pointer to a palette). First, we need to initialize the converter to the pixel format of the bitmap in question. The implementations of individual filters vary. For the grayscale filter, we need to extract RGB components from each pixel, average them, and write the result back in the proper pixel format. The principle is similar to those that apply to MIP-map generation: extract RGB components from the source pixel, perform the operation on them, and encode the modified components back to pixel’s format. Most filters (such as those manipulating brightness, grayscale, and so on) will have a single code path, traversing all pixels of the bitmap in a loop, extracting RGB components, performing a required operation, and encoding the data back. For palletized modes, most filters can simply perform their operations on the palette rather than the pixels of the bitmap. We can consider the palette to be an array of values in RGB format. Thus, we simply assign the palette to be the buffer on which we want to do the operation. We assign the palette a width of 256 and a height of 1 and then jump to the branch where we deal with true-color pixel formats. |
|
|