- Alpha Blending -
O Alpha Blending consiste em juntar duas imagens levando em conta o canal alpha, ou seja, a transparência em cada pixel. Aqui eu vou abordar apenas uma forma simples e eficiente para desenhar imagens semi-transparentes (mas sem transparência por pixel). Os códigos estão em Object Pascal (Delphi/Lazarus), mas creio que será bem fácil adaptar para outras linguagens.
- Matemática -
Para combinar as imagens nós combinamos cada pixel de uma imagem com os pixels da outra imagem. Como exemplo, suponha que vamos desenhar uma imagem totalmente verde limão com 50% de transparência (R: 0, G: 255, B: 0) em cima de uma imagem vermelha (R: 255, G: 0, B: 0). Observe o resultado na imagem abaixo:

Combinando os pixels
A matemática para essa combinação é muito simples: pegamos as cores principais do pixel a ser desenhado e multiplicamos cada uma pela porcentagem de transparência da imagem (nesse caso 50%, o que seria a mesma coisa que dividir os valores por 2) e somamos com o valor das cores principais do pixel base (da imagem onde iremos desenhar) multiplicado por 100% – a transparência da imagem a ser desenhada (que nesse caso também seria 50%). Assim nós chegamos à esta equação:
cor final = (cor do pixel 2 * transparência da imagem) + (cor do pixel 1 * 100% - transparência da imagem))
Levando em conta o exemplo acima, usaríamos esta equação três vezes (uma para cada cor principal):
vermelho = (0 * 50%) + (255 * (100% - 50%)) = 127,5
verde = (255 * 50%) + (0 * (100% - 50%)) = 127,5
azul = (0 * 50%) + (0 * (100% - 50%)) = 0
- Optimização x Precisão -
Um dos maiores problemas no processamento de imagens é que precisamos utilizar muita matemática (no exemplo acima, por exemplo, utilizamos diversas vezes multiplicações e divisões com números decimais! Isto leva muito tempo), por isso que muitas vezes é melhor utilizar algo veloz, mas que chegue perto da precisão (não precisa ser 100%). No caso doalpha blending essa situação é razoável, já que o valor das cores varia de 0 a 255 (byte).
Não entendeu o porquê do fato do valor das cores ser compatível com o formato byte (0..255) influenciar na performance? Simples! A base de todos os dados no computador são números binários (0 e 1), certo? Então quando temos valores divisíveis por 2 podemos, ao invés da divisão e multiplicação, utilizar simples (e rápidas) operações com bits.
E como utilizar estas operações?!
Em Object Pascal nós temos os operadores shr e shl (">>" e "<<", respectivamente em C/C++) que podem ser utilizados em uma operação aritmética (assim como os operadores not, and, or e xor). Esses operadores servem para alterar diretamente os bits dos dados. A operação x shr y ou x shl y, por exemplo, moveria x para a direita ou para a esquerda por y bits. Isso significa que se x for o número binário 1000 (8, em decimais) executarmos a operação x shr 2 retornaria o número binário 10 (2, em decimais) e uma operação x shl 2 retornaria o número binário 100000 (32, em decimais). Note que no primeiro caso nós tiramos os dois últimos bits de x, e no segundo caso nós colocamos 2 bist no final do número!
Vamos então partir da nossa equação anterior, mas agora ao invés de números decimais vamos utilizar números inteiros. Note que a transparência aqui também deve ser um byte, ou seja, deve estar entre 0 e 255:
cor final = ((cor 2 * transparência)/255) + (cor 1 * (255 - transparência)) / 255)
Não parece muito bom, não é. Acabamos de adicionar mais multiplicações e divisões. Mas essa equação ainda é 100% precisa. Vamos mexer nela um pouquinho.
cor final = ((cor 2 * transparência) shr 8) + ((cor 1 * (256 - transparência)) shr 9) shl 1
Trocamos as divisões por simples mudanças de bits, mas a precisão caiu um pouquinho. Se executarmos essa equação para as cores do primeiro exemplo a cor retornada seria (R: 126, G: 128, B: 0).
- Tabela de Transparência -
Alternativamente você pode usar tabelas de transparências. Essas nada mais são do que valores de transparência pré-calculados e armazenados na memória. Como esses valores já estarão no formato byte, você não terá que fazer conversões ou testes, portanto a execução será mais rápida. Claro que tudo isso ao custo de um pouco de espaço na memória (vamos alocar uma matriz bi-dimensional de bytes).
Bem, vamos começar definindo nossa matriz global:
var
AlphaTable: array [0..255,0..255] of Byte;
Em seguida temos que inicializá-la, então vamos criar uma função para isso:
procedure InitAlphaTable;
var x, y: Integer;
begin
for y:= 0 to 255 do
begin
for x:= 0 to 255 do
begin
AlphaTable[y,x]:= Trunc(y * (x/255));
end;
end;
end;
Agora, antes de qualquer referência à tabela de transparência nós precisamos chamar a função para inicializá-la. Isso pode ser feito no início do programa.
- Exemplo -
Segue um pequeno exemplo de como combinar dois bitmaps utilizando a técnica de Alpha Blending. O código está em Object Pascal.
const
{ Máximo de pixels em uma array de cores }
MaxPixels = 32768;
type
{ Array de cores para Bitmap }
TRGBTripleArray = array [0..MaxPixels-1] of TRGBTriple;
pRGBTripleArray = ^TRGBTripleArray;
function MergeBMP(bmp1, bmp2: TBitmap; alpha: Byte; x, y: Integer): TBitmap;
var p, p2, p3: pRGBTripleArray;
w, h: Integer;
c, l: Integer;
tmp: TBitmap;
ai: Byte;
begin
//Cria o bitmap de destino...
tmp:= TBitmap.Create;
//Todos os bitmaps devem ser 24bit
tmp.pixelFormat:= pf24bit;
if bmp1.PixelFormat pf24bit then
bmp1.PixelFormat:= pf24bit;
if bmp2.PixelFormat pf24bit then
bmp2.PixelFormat:= pf24bit;
//Assigna ao bitmap base (bmp1)
tmp.Assign(bmp1);
w:= bmp2.Width;
h:= bmp2.Height;
//verificação de posição e tamanho
if x < 0 then
x:= 0;
if y < 0 then
y:= 0;
if x + w > bmp1.width then
w:= bmp1.width;
if y + h > bmp1.height then
h:= bmp1.height;
//Inverso da transparência
ai:= 255 - alpha;
//percorre os pixels e seta a transparência...
for l:= y to h-1 do
begin
//pega a scanline dos bitmaps
p:= tmp.ScanLine[l];
p2:= bmp1.ScanLine[l];
p3:= bmp2.ScanLine[l];
for c:= x to w-1 do
begin
//azul...
p^[c].rgbtBlue := AlphaTable[p3^[c].rgbtBlue, alpha] + AlphaTable[p2^[c].rgbtBlue, ai];
//verde
p^[c].rgbtGreen := AlphaTable[p3^[c].rgbtGreen, alpha] + AlphaTable[p2^[c].rgbtGreen, ai];
//vermelho
p^[c].rgbtRed := AlphaTable[p3^[c].rgbtRed, alpha] + AlphaTable[p2^[c].rgbtRed, ai];
end;
end;
//Retorna o bitmap temporário
Result:= tmp;
end;