16+
ComputerPrice
НА ГЛАВНУЮ СТАТЬИ НОВОСТИ О НАС




Яндекс цитирования


Версия для печати

Модуль поиска не установлен.

Альфа-наложение - спецэффекты почти без усилий!

27.05.2003

Александр Теут <2alex@tut.by>

Начиная с Windows 98, Microsoft предложила разработчикам программного обеспечения новую методику графического вывода - альфа-наложение (alpha blending). Суть её заключается в том, что итоговый пиксел приёмной поверхности представляется суммой пикселов источника и приёмника, которые предварительно умножены на определённые коэффициенты. Особенно наглядно математические формулы альфа-наложения выглядят для графических поверхностей с 24 или 32-кодировкой цвета. В этом случае, как известно, пиксел содержит полную информацию о своём цвете и описывается 3 или 4 байтами соответственно. Первые три байта содержат значения интенсивностей голубого, зелёного и красного цветов. Об информации, содержащейся в четвёртом байте, будет сказано позже.

В общем виде альфа-наложение вычисляется по следующим формулам:

Приём.Blue=alpha* Источн.Blue+(1-alpha)* Приём.Blue
Приём.Green=alpha* Источн.Green+(1-alpha)* Приём.Green
Приём.Red=alpha* Источн.Red+(1-alpha)* Приём.Red
Приём.alpha=alpha* Источн.alpha+(1-alpha)* Приём.alpha

Здесь "Приём." - означает приёмная поверхность, а "Источн." - поверхность с накладываемым изображением.

Значения alpha - 0 и 1 реализуют два крайних случая копирования одного изображения на другое. При alpha = 0 мы имеем прозрачный вывод (transparent), а при alpha = 1 - непрозрачный (opaque).

Для поддержки альфаналожения в своих приложениях программисты могут воспользоваться пока только одной структурой и одной функцией. Импортируются они из файла msimg32.dll, который располагается в папке System, содержащейся в директории с Windows. Кстати, он содержит ещё ряд "продвинутых" графических функций. Для справки приведём фрагмент результатов применения утилиты tdump.exe (поставляется с Builder) к этой динамической библиотеке:

Exports from MSIMG32.dll
   5 exported name(s), 5 export addresse(s). Ordinal base is 1.
   Sorted by Name:
     RVA           Ord.  Hint  Name
     --------      ----  ----     ----
     000010A4     2  0000  AlphaBlend
     00001034     3  0001  DllInitialize
     00001094     4  0002  GradientFill
     00001160     5  0003  TransparentBlt
     00001290     1  0004  vSetDdrawflag

Сразу отметим, что реализация альфа-наложения в программах, запускаемых под Windows 95, сводится лишь к дополнительной поставке вышеупомянутого файла с вашим приложением и не требует особых установок цветовой палитры в настройках экрана компьютера. То есть, вы сможете наблюдать альфа-наложение и при 256 цветах.

Итак, ознакомимся с единственной структурой, которая используется для достижения эффекта альфа-наложения.

typedef struct _BLENDFUNCTION{
BYTE BlendOp;
BYTE BlendFlags;
BYTE SourceConstantAlpha;
BYTE AlphaFormat;
} BLENDFUNCTION;

Поле BlendOp может принимать единственное значение AC_SRC_OVER и поэтому особых пояснений не требует.

Поле BlendFlags должно быть равно 0, оно зарезервировано для будущих версий Windows.

Поле SourceConstantAlpha определяет значение альфа-коэффициента в интервале 0 - 255.

Поле AlphaFormat может принимать только два значения: 0, если для всех пикселов растра-источника предполагается постоянный альфа-коэффициент, и AC_SRC_ALPHA, если пикселы поверхности источника содержат данные альфа-канала.

Непосредственно функция, которая реализует альфа-наложение, объявляется следующим образом:

BOOL AlphaBlend (HDC hdcDest, int iLeftDest, int iTopDest, int iWidthDest, int iHeightDest, HDC hdcSource, int iLeftSource, int iTopSource, int iWidthSource, int iHeightSource, BLENDFUNCTION alphaBlend);

Несмотря на несколько громоздкий вид, её параметры легко интерпретируются.

Через первый параметр передаётся манипулятор приёмного контекста устройства. Следующие четыре параметра описывают геометрические размеры принимающей поверхности или её части, а именно: координаты верхнего левого угла, ширину и высоту. Затем идут в том же порядке точно такие же параметры, но уже относящиеся к поверхности источника. Завершает всё экземпляр структуры BLENDFUNCTION.

Поскольку "те, кто дают советы, не сопровождая их примерами, подобны дорожным столбам, которые указывают, но сами по ней не ходят", проиллюстрируем применение альфа-наложения на нескольких примерах.

В качестве среды программирования будет использоваться Borland C++ Builder v5.0.

В самом начале (можно и перед первой компиляцией), к приложению, в котором вы собираетесь использовать функцию AlphaBlend, следует подключить библиотеку msimg32.lib. Найти её можно в той же директории, где у вас установлен Builder(Delphi), придерживаясь пути: Borland\CBuilder5(6)\Lib\Psdk.

Первый наш пример продемонстрирует, как вывести изображение, представленное на рис.1, на поверхность, фоном которой будет другое изображение (рис.2) с альфа-коэффициентом, равным 128. Итак, создаём новое приложение. Помещаем в форму два компонента TImage, в которые, с помощью "Инспектора Объекта", через свойство Picture загрузим, например, вышеприведённые картинки (для нашего случая, они обязательно должны быть в формате BMP). Ещё нам понадобится компонент TButton. Сам код разместим в его обработчике событий OnClick.

Далее применяем стандартный приём при работе с WinAPI-функциями, которые в качестве параметра содержат экземпляр структуры, а именно: сначала объявляем экземпляр структуры, а затем инициализируем её поля.

BLENDFUNCTION blend;
blend.BlendOp = AC_SRC_OVER;
blend.BlendFlags = 0;
blend.SourceConstantAlpha = 128;
blend.AlphaFormat = 0;

Кстати, приведённый фрагмент кода можно записать одной строкой, но при этом строго соблюдая порядок фактических параметров:

BLENDFUNCTION blend = {AC_SRC_OVER,0,128,0};

Осталось вызвать функцию смешивания, спрятать налагаемое изображение и перерисовать фон.

::AlphaBlend(Image1->Canvas->Handle,Image2->Left,Image2->Top,
Image2->Width,Image2->Height,Image2->Canvas->Handle,0,0,
Image2->Width,Image2->Height,blend);
Image2->Hide();
Image1->Repaint();

Результатом выполнения программы будет изображение, представленное на рис. 3.

Здесь следует отметить, что, произведя очевидную модификацию данного кода, можно реализовать также настройку смешивания изображений, чтобы иметь возможность регулировать степень прозрачности, подобно тому, как это сделано в программе "AT Screen Thief" (http://www.atscreenthief.com/) (рис.4).

Лёгкость, с которой достигаются подобные эффекты, позволяет нам немного усложнить задачу, а именно: будем выводить второе изображение постепенно, то есть добьёмся эффекта "проявления".

Для этого нам понадобится компонента TTimer, ещё одна кнопка и статическая переменная типа BYTE.

Пользуясь "Инспектором Объектов", свойство Enabled для таймера установим равным false, а свойство Interval равным 500 (таймер будет "срабатывать" через каждые полсекунды). В обработчике события OnClick нашей новой кнопки запишем всего одну строчку:

Timer1->Enabled = true;

Это означает, что при нажатии на кнопку будет запускаться таймер.

Компонент TTimer имеет только один обработчик события OnTimer, который активизируется через его свойство Enabled. В тело этого обработчика поместим следующий код:

static BYTE bAlpha = 0;

BLENDFUNCTION blend = {AC_SRC_OVER,0,bAlpha,0};
AlphaBlend(Image1->Canvas->Handle,Image2->Left,Image2->Top,
Image2->Width,Image2->Height,Image2->Canvas->Handle,0,0,
Image2->Width,Image2->Height,blend);
Image1->Repaint();
bAlpha = bAlpha +2;
if (bAlpha >255) Timer1->Enabled=false;

Обратите внимание, остановка таймера происходит в самом обработчике. Естественно, остановить таймер, а следовательно, "закрепить" изображение, можно на любой стадии "проявки".

До сих пор мы использовали постоянный альфа-коэффициент, который применяли к каждому пикселу накладываемого изображения. Но в 32-разрядных изображениях, как было уже указано в начале статьи, каждый пиксел представляется 4 байтами, первые три мы описали, а четвёртый байт хранит информацию об альфа-канале. AlphaBlend - единственная функция, которая использует этот байт. При этом она предполагает, что пикселы источника уже умножены на альфа-коэффициент. Отсюда следует вывод: чтобы иметь возможность работать с альфа-каналами, мы должны получить доступ к отдельным пикселам изображения и вручную произвести операцию умножения.

Для демонстрации подобной техники создадим круглую цветную кисть с переменной прозрачностью, подобную той, которую можно наблюдать в некоторых современных графических редакторах, например, в Photoshop'е. Кстати, пример реализации такой кисти через класс приведён в книге Фень Юаня "Программирование графики для Windows".

Схема наших дальнейших действий будет следующей. Сначала мы создадим объект TBitmap, на котором нарисуем цветной круг. Затем подготовленный таким образом аппаратно-зависимый растр (DDB) преобразуем в аппаратно-независимый (DIB), для того чтобы получить доступ к отдельным пикселам, каждый из которых умножим на альфа-коэффициент. Последний будет определяться в зависимости от условного расположения пиксела по отношению к центру круга. Не забудем одновременно заполнить альфа-канал. И, наконец, сделаем обратное преобразование с растрами: DIB -> DDB. Полученную таким образом кисть мы будем отображать на поверхности формы при движении по ней мышкой с помощью функции AlphaBlend.

Сначала подключим вспомогательные заголовочные файлы:

#include <algorithm>
#include <memory>

В секции private h-файла формы объявим несколько переменных, что позволит нам обращаться к ним из любого обработчика формы:

std::auto_ptr<Graphics::TBitmap>bitmap;
int iCenterX, iCenterY, iWidth,iHeight;
bool bBeginDraw;

Поясним конструкцию std::auto_ptr из первой строки. Здесь мы объявляем автоматический указатель на объект Graphics::TBitmap ("smart" pointer). Вся прелесть работы с подобными указателями заключается в том, что нам в дальнейшем нет нужды заботиться об уничтожении переменных, на которые они ссылаются, как приходится делать в случае с обычными динамически создаваемыми переменными. Объявленные таким образом объекты уничтожаются автоматически при выходе за область их определения.

Чтобы иметь возможность работать с подобными "умными" указателями, мы и объявили заголовочный файл memory из STL (стандартная библиотека шаблонов).

Код, отвечающий за создание изображения кисти, разместим в теле конструктора формы. Здесь стоит акцентировать внимание на том факте, что bitmap должен динамически создаваться в инициализирующей части конструктора, а не в его теле.

__fastcall TForm1::TForm1(TComponent* Owner)

: TForm(Owner),bitmap(new(Graphics::TBitmap))
{
....
}

Затем проинициализируем ряд переменных, они потребуются нам в дальнейшем:

bBeginDraw = false;
iWidth = 30;
iHeight = 30;
iCenterX = iWidth>>1;
iCenterY = iHeight>>1;

Далее инициализируем структуру BITMAPINFOHEADER, которая будет использоваться для организации доступа к отдельным пикселам растра (предполагаем для определённости, что формат пиксела 32-битный):

BITMAPINFOHEADER bmiHeader = {sizeof(BITMAPINFOHEADER),0};
bmiHeader.biWidth = iWidth;
bmiHeader.biHeight = iHeight;
bmiHeader.biPlanes = 1;
bmiHeader.biCompression = BI_RGB;
bmiHeader.biBitCount = 32;

Зададим размеры изображения кисточки и пиксельный формат растра:

bitmap->Width=iWidth;
bitmap->Height=iHeight;
bitmap->PixelFormat=pf32bit;

Создаём, а затем выбираем в памяти последовательно цвет кисти, тип пера (границы нам не нужны!) и рисуем круг, залитый, например, красным цветом:

bitmap->Canvas->Brush->Color=clRed;
bitmap->Canvas->Pen->Style=psClear;
bitmap->Canvas->Ellipse(0,0,iWidth,iHeight);

Как указывалось выше, нам необходимо получить доступ к отдельным пикселам изображения. Для этого требуется манипулятор контекста устройства экрана, который возвращает функция GetDC:

HDC ScreenDC=::GetDC(0);

Начиная с 32-битных версий Windows, растры выводимых изображений выравниваются по двойному слову. Исходя из этого, вычислим, какое количество байт приходится на одну строку развёртки:

int SrcNumberOfBytesPerRow= (((iWidth * bmiHeader.biBitCount)+31)&~31)>>3;

Теперь определим размер растра в байтах. Для этого будем использовать функцию GDI GetDIBits. Описание её можно найти в справочном файле Win32.hlp, поставляемом вместе с Builder(Delphi). Мы же поясним только сам способ получения, с помощью данной функции, размера растра. Для этого её пятый параметр должен иметь значение NULL. Тогда, после вызова функции, поле biSizeImage структуры BITMAPINFOHEADER будет содержать размер растра.

::GetDIBits(ScreenDC,bitmap->Handle,0,iHeight,
NULL,(BITMAPINFO*)&bmiHeader, DIB_RGB_COLORS);

На всякий случай, застрахуем себя от неожиданного результата и вручную вычислим размер растра - это стандартный приём при проведении подобных операций.

if(bmiHeader.biSizeImage==0)
{
bmiHeader.biSizeImage=SrcNumberOfBytes PerRow * iHeight;
}

Настало время объявить и динамически создать массив байтов для аппаратно-независимого растра. И в этом случае, опять будем использовать "умные" указатели.

std::auto_ptr<BYTE>pBits(new BYTE[bmiHeader.biSizeImage]);

Для того чтобы проинициализировать вновь созданный массив, в очередной раз воспользуемся функцией GetDIBits, но теперь в качестве пятого параметра используем указатель на сам массив с методом get.

::GetDIBits(ScreenDC,bitmap->Handle,0,iHeight,
pBits.get(),(BITMAPINFO*)&bmiHeader, DIB_RGB_COLORS);

Далее работаем с массивом пикселов аппаратно-независимого растра. Коэффициент альфа будем вычислять в зависимости от расположения отдельного пиксела растра по отношению к центру круга. Здесь нам понадобятся функции, объявленные в файле algorithm: min и max.

Определим вспомогательный указатель на переменную типа BYTE:

BYTE *pPixel=pBits.get();

Выполним двойной цикл, чтобы обработать все пикселы растра.

for(int y = 0;y<iHeight;y++)

{
   for(int x = 0;x < iWidth; x++, pPixel += 4)
   {
  int distance = (int)(sqrt((x-iCenterX) * (x-iCenterX) + (y-iCenterY) * (y-iCenterY))*255/iCenterX);
    BYTE alpha=(BYTE) max (min(255- distance, 255), 0);
    pPixel[0] = pPixel[0] * alpha/255;
    pPixel[1] = pPixel[1] * alpha/255;
    pPixel[2]=pPixel[2]*alpha/255;
    pPixel[3]=alpha;
   }
}

Таким образом, альфа-каналы и пикселы поверхности источника подготовлены для вывода на приёмную поверхность. Осталось преобразовать аппаратно-независимый растр в аппаратно-зависимый. Для этой цели в WinGDI используется функция SetDIBits:

::SetDIBits(ScreenDC,bitmap->Handle,0,iHeight,
pBits.get(),(BITMAPINFO*) &bmiHeader, DIB_RGB_COLORS);

Не забудем освободить ресурс контекста устройства экрана:

::ReleaseDC(0,ScreenDC);

Таким образом, картинка, с помощью которой мы собираемся имитировать нашу кисточку, создана. Осталось запрограммировать сам процесс рисования. Для этого будем использовать три обработчика события компонента TImage, который у нас служит в качестве фона (для разнообразия картинку фона мы сменим): Image1MouseDown, Image1MouseMove, Image1MouseUp.

В тело обработчика Image1MouseDown запишем следующий код:

if (Button == mbLeft) bBeginDraw = true;

То есть, при нажатии левой кнопки мыши, булевая переменная станет равной истине; в нашем случае это даст старт процессу рисования.

Само рисование будет происходить в обработчике Image1MouseMove:

if (bBeginDraw == true)
{
BLENDFUNCTION blend = {AC_SRC_OVER, 0, 200, AC_SRC_ALPHA};
AlphaBlend (Image1->Canvas->Handle, X-iCenterX,Y-iCenterY, iWidth, iHeight, hMem, 0, 0, iWidth, iHeight, blend);
Image1->Repaint();
}

И, наконец, когда левая кнопка мыши отпускается, следует закончить рисование. Для этого в обработчике Image1MouseUp, достаточно поместить следующую строчку:

bBeginDraw = false;

Результатом вышеописанных действий должно быть что-то, похожее на рис. 5.

Варьируя вид функции для вычисления коэффициента альфа, можно получать самые причудливые формы изображения кисточки. Кстати, аналогично осуществляется размытие границ картинок при наложении их друг на друга.

Начиная с 6 версии Borland C++ Builder, у многих компонент появилось свойство Alpha Blend, что до определённой степени позволяет разнообразить интерфейс программы "малой кровью", например, стало довольно просто запрограммировать процесс постепенной проявки или исчезновения формы, но более изощрённые возможности альфа-наложения несомненно требуют прямого доступа к пикселам растра, с использованием, описанных в статье средств WinAPI GDI.

И в заключение. Вышеизложенная процедура доступа к пикселам применима и во многих других случаях, когда требуется производить быстрые растровые операции с изображениями, к примеру: аффинные преобразования, реализацию какого-нибудь пространственного фильтра или осуществление его цветовой коррекции.



статьи
статьи
 / 
новости
новости
 / 
контакты
контакты