Title: Scanline & Alpha Channel Bitmaps - Basic Overview
Question: Overview of the use of Scanline property to draw bitmaps using alpha-channel translucency
Answer:
Esentially, the important part of drawing bitmaps with alpha-channel transparency is the way you access the bitmap. You need to do some maths with the individual colour values of the pixels that make up the image, but for some reason (known, I suspect, only to whoever wrote the damned property in the first place), the 'Pixels' property - the array of TColors that make up the image - is amazingly slow. Slower than, say, an anaesthetised sloth. If you try and do any serious graphics work but you don't want to write your own bitmap class from scratch, then the best way to achieve your goals is by using the 'Scanline' property of TBitmap.
Scanline is an 'array of PByteArray' - Scanline indexes to an individual line of pixels in your bitmap. The PByteArray that is returned when you index into Scanline is the data that makes up that whole line, as a sequence of bytes. The trick to understanding Scanline is realising that the length of this sequence of bytes is dependent not only upon the width of your image, but also on the colour depth. If it is a 256-colour image (8 bits, or 1 byte, per pixel) then the length of the PByteArray will be 1*width. If it is a High-colour, 24bpp image (24 bits = 3 bytes) then the length will be 3*width, and every pixel will have 3 bytes that make it up.
These 24 bpp images are the easiest to understand, since the three bytes are simply the values for red, green and blue for that pixel. For the rest of the tutorial, I assume 24bpp colour - you can, for instance, achieve this by couching your graphics code in:
var
OldBPP: TPixelFormat;
Picture: TBitmap;
...
...
OldBPP := Picture.PixelFormat;
Picture.PixelFormat := pf24bit;
...
// Code goes here ...
...
Picture.PixelFormat := OldBPP;
and Delphi will convert the image from whatever colour depth it is to 24bpp before doing any of your code, then back again afterwards.
Just as an example, here is one way that the Get method for the Pixels property could have been implemented:
function GetPixel(Source: TBitmap; X, Y: Integer): TColor;
var
Result: TColor; // To store working result value
OldBPP: TPixelFormat;
begin
OldBPP := Source.PixelFormat;
Source.PixelFormat := pf24bit;
// Get Red value - stored in lowest order byte in a TColor -
Result := PByteArray(Source.Scanline[y])^[(x*3)+0];
// Get Green value - second lowest byte in TColor -
Result := Result +
(PByteArray(Source.Scanline[y])^[(x*3)+1] * 256);
// Get Blue value - third lowest byte in TColor -
Result := Result +
(PByteArray(Source.Scanline[y])^[(x*3)+2] * 65536);
GetPixel := Result;
end;
Notice how the data we are after is found by choosing the line simply from the heightwise coordinate, and the start of the pixel data is found in that line by multiplying the widthwise coordinate with the bytes-per-pixel depth (*not* the bits-per-pixel depth!). Individual colour bytes are then found by stepping 0, 1 or 2 bytes from that position.
For translucent drawing of bitmaps, you need to have a sprite and a mask of the same dimensions. Don't confuse these with the sprite and mask used in the more common, raster-ops method of drawing 'transparent' sprites - in that method, the transparency can be either on or off, but nowhere in between. In alpha-mask transparency, you can blend images on top of each other.
The sprite can be whatever colours you like - and if any pixels are going to be fully transparent, it makes no difference at all what colour they are - but we will assume it is already 24bpp.
The mask we will assume for the moment is in shades of grey - the lighter the shade (nearer to white) the less transparent it is - or rather, the more opaque the sprite is. We will also assume this is 24bpp, although of course if it is grey, the RGB values will be equal so all three bytes for a pixel will have the same value.
Obviously, you will need to apply the following process to every pixel in the image to draw the whole image transparently, but for the moment I am assuming that we are drawing just one arbitrary (x, y) pixel from the TBitmap 'Source' to the TBitmap 'Target', using the alpha mask TBitmap 'Mask'.
The method we use is to discover the opacity of the mask at the given pixel, then multiply each of the colour channels by this opacity to get the 'contribution' to the final pixel colour that the source image makes, and then multiply each of the colour channels of the target by the inverse of this opacity, to discover what 'contribution' the existing colour makes to the final pixel colour. Notice that if we are talking about a floating point multiplier for opacity, then the opacity of the source + the opacity of the target should be equal to 1.
var
PixelAlpha, newcolour: double;
x: integer;
bpp: integer;
...
bpp := 3; // pf24bit, 24bpp
...
// Work out the opacity
PixelAlpha := (PByteArray(Mask.scanline[y])^[((x)*bpp)])/255;
for z := 0 to bpp -1 do
begin;
// Start with a black pixel
newcolour := 0;
// Add the source * opacity for source contribution
newcolour := newcolour +
(PByteArray(Source.scanline[y])^[((x)*bpp)+z])*PixelAlpha;
// Add the target * (1-opacity) for target contribution
newcolour := newcolour +
((PByteArray(Target.scanline[y+YOffset])^[((x+XOffset)*bpp)+z]) *
(1-PixelAlpha));
// Finally draw result colour to target
PByteArray(Target.scanline[y+YOffset])^[((x+XOffset)*bpp)+z] :=
round(newcolour);
end;
(XOffset and YOffset are integer values that specify where in the target image to draw the top-left corner of the sprite - useful if this were part of a alpha sprite-drawing routine... ;-)
There are a couple of important things to note about the above example -
First of all is that we loop through once for each colour channel. If you were using different colour depths then this would be done differently - 32bpp, for instance, has one byte spare after the three RGB bytes, so we would set 'bpp' to 4 and loop through 0 to bpp-2. Note that the 'bpp' variable stands for 'bytes-per-pixel', not the usual 'bits-per-pixel'.
Second is that we wait until the last minute to round off newcolour. This is because - as newcolour is a double and not bounded at 255 - rounding errors may result in the value of newcolour becoming 256, which we won't be able to assign to a byte.
Thirdly, we assume that the mask is greyscale for speed, mainly. If we only check one of the three pixels, then we only have to perform the one lookup for it per pixel, as opposed to three, and more to the point, only the one float division, which is probably the slowest single operation above.
The above presents the basis for drawing alpha-mask sprites. I've purposefully not literally given you a full routine to do this, since I'm of the firm opinion that people should learn how to work out how to do things for themselves, rather than learning how to use other people's code - but it's easy enough - if you understand the concepts presented - to write your own routine to do this. I may well present a simple graphics library using the ScanLine function on my website sometime in the future (www.bichatse.co.uk).
Of course, this is not the end of the concept, either. There are several ways in which this code and idea can be extended. For instance, try moving the opacity calculation inside the loop, so you can make your sprites transparent to particular colour channels.
Speed is also something to point out. This routine is *faaaaar* quicker than it would have been had I written it using the 'Pixels' property, but it's still slower than it needs to be - several optimisations can be made to the given code. The first that springs to mind is taking advantage of the fact that a 'divide by 256' operation in binary is the same as a 'shift right by 8 bits' operation - if we instead multiply the source/target pixels by the raw 0-255 mask value, we can then shift that result right by 8 bits to get our 'contribution', and then add the two together - much faster than a floatng point divide!
Still, I hope you learned something, and get some use of this article. Please, post your comments or mail me at Jake@bichatse.co.uk
--
Jake Staines