Programming: Статьи


Вывод графики на рабочий стол Windows с использованием оверлеев DirectX



  Автор: Колесников Александр

  Источник:

О чём речь

  Объяснить это очень просто - запускаем WinAmp версии 2.80 (возможно 2.7x) и далее; включаем плагин AVS (Advanced Visualization Studio) и в его настройках (Settings/Display) - функцию Overlay Mode и Set Desktop to color.

  При этом AVS, кроме обычного окошка, начинает выписывать свои узоры на месте обоев рабочего стола, т.е. "под" значками и окнами. Можно перетаскивать эти самые значки и окна как угодно - никаких артефактов, такое ощущение, что это и впрямь обои… но динамические, и обновляются гораздо быстрее, чем это можно сделать стандартными средствами Windows. И вообще, всё крутится довольно быстро, особенно по сравнению с тормозами в полноэкранном режиме. А здесь ведь тоже полный экран… хотя нет, не полный - масштабируется исходное окошко… но опять же, быстро масштабируется и с фильтрацией… Неужели после долгой и упорной приверженности программным методам гении из NullSoft задействовали-таки аппаратное ускорение? Но что это за блиттинг (рендеринг?) такой, что значки не затирает? В общем, КАК ОНИ ЭТО СДЕЛАЛИ?

  Оставайтесь с нами, и вы узнаете это :). Кроме того, будет обсуждаться, хотя и только теоретически, возможность использования оверлеев для вывода скринмейтов (screenmate). Вообще, о скринмейтах есть [1], где описаны различные методы их вывода, но относительно оверлеев имеется только упоминание и отсылка к примеру Mosquito из состава DX7 SDK (именно 7, в 8-м имеется только почти нигде не работающий OverlayAnimate). Конечно, можно обратиться к SDK, благо там есть вся (почти) необходимая информация, но куда проще использовать пример, прилагаемый к статье, которую вы читаете, и переделать его под свои цели, или использовать как базу для оверлейных экспериментов.

  В какой-то степени статья будет полезна и тем, кто занимается или собирается заняться работой с видео (воспроизведением видео, т.е. разного рода медиаплеерами), поскольку там тоже часто используются оверлеи. Хотя полезность скорее теоретическая, т.к. пример на работу с видео не рассчитан.

Разного рода требования

  Предполагается, что читатель знаком с основами DirectDraw (это не проблема - в Сети много статей на эту тему, см., например, раздел "Hello, World!" Королевства Дельфи). Желательно иметь некоторое представление о WinAPI (тогда, кстати, и DirectX будет легче изучать), ну и собственно Delphi.

  Так что вопросы вроде "что такое поверхность" или "а где у примера форма" приниматься не будут.

  Аппаратные требования. Нужна видеокарта с поддержкой оверлеев - т.е. практически любой 3D-ускоритель, и даже некоторые, гм, 3D-тормоза. Если у вас именно тормоз, можно попробовать найти его в [2], и посмотреть, поддерживает ли он форматы UYVY или YUY2; можно также запустить DirectX Caps Viewer из комплекта DX8 SDK и посмотреть DirectDraw Devices\<название девайса>\Caps\Overlay Caps; можно просто скачать пример и попробовать запустить, но лучше всего - проапгрейдиться :). Программа протестирована (успешно) на следующих видеокартах: GeForce2MX400 (64 Mb); Rage 3D Pro (8 Mb); Trident 3D (4 Mb); i752 (интегрированная на i810); i82815 (интегрированная на i815).

  Из программных компонентов требуется DirectX7 runtime.
Для компиляции примера - Delphi, предположительно начиная с 4-го (проверено на 5 и 7). Программа использует заголовки DirectX7 и библиотеку FastLIB, хотя качать всё это необязательно - нужные модули имеются в комплекте.

Немного теории

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

  Применительно к DirectX7 оверлей - это поверхность DirectDraw, для вывода которой, в отличие от обычных поверхностей, не нужно выполнять блиттинг или перенос данных непосредственным доступом. Достаточно просто указать положение на экране и размеры оверлея, и он будет постоянно "висеть" перед первичной поверхностью, добавляясь к каждому кадру по описанному принципу (своего рода дополнительная первичная поверхность произвольного размера и положения). Т.к. добавление происходит непосредственно в RAMDAC, оверлей не изменяет содержимое первичной поверхности (не "портит" фон), и если убрать его - изображение восстановится само собой.

  В DX SDK оверлеи сравниваются с "листом прозрачного пластика, который накладывается на экран". Не очень-то прозрачный, добавлю я от себя. Но общий принцип именно такой - можно накладывать этот лист на экран, потом убирать, и это нисколько не повлияет на содержимое экрана. Можете лично проверить и убедиться :).

  Также легко догадаться логически и убедиться натурально в невозможности получить скриншот этого "листа" (скриншот - это копия памяти первичной поверхности, а оверлей в память не копируется). Многие сталкиваются с этим при написании "грабилок" экрана или просто при попытке получить кадр любимого фильма. Единственное, что здесь можно посоветовать - отключить (попросить отключить пользователя) использование оверлеев в медиаплеере (в "Универсальном проигрывателе" - Файл\Свойства\Дополнительно\Video Renderer\Свойства\DirectDraw, "переключения" и "перекрытия").

  Но, за исключением этого случая, "неуловимость" оверлеев - их большое достоинство. Представьте, что в спрайтовом движке не нужно восстанавливать фон за каждым спрайтом… прибавьте к этому практически "бесплатный" в плане производительности вывод… и ругните нехорошим словом производителей видеокарт, которые встраивают в свои детища поддержку только 1 (одного) оверлея одновременно. Поэтому (потому, что?) оверлеи используются в основном для вывода видео и фокусов наподобие AVS'овского.

  Другое весьма полезное свойство оверлеев - поддержка цветового ключа приёмника (destination color key, далее DCK). Вообще это понятие применимо не только к оверлейному выводу, но найти видеокарту c аппаратной поддержкой DCK при блиттинге практически невозможно, тогда как для оверлеев это своего рода бесплатное приложение.

  Итак, что такое DCK. При использовании широко известного цветового ключа источника (source color key) мы задаём цвет в поверхности-источнике, который НЕ БУДЕТ выводиться на экран, например, чёрный фон у спрайта. При DCK мы задаём цвет в поверхности-приёмнике, который БУДЕТ заменяться поверхностью-источником при выводе. (Вообще спецификация цветовых ключей DirectDraw подразумевает возможность использования диапазона цветов вместо единственного цвета, но тут опять всё упирается в аппаратную поддержку, вернее, её отсутствие).

  Пример? Возвращаемся к AVS, включаем только overlay mode (не включаем set desktop to color). Щелкаем на цвет чуть ниже (вот попробуй догадайся, что на него можно щёлкать), в диалоге выбираем, например, белый. Смотрим на результат и понимаем, что вывод на рабочий стол является всего лишь частным случаем, и AVS благодаря использованию DCK может заменять ЛЮБОЙ цвет на экране. Для того, чтобы рисовать на рабочий стол, нужно присвоить ему некоторый цвет (желательно уникальный), и установить этот же цвет в качестве DCK. Задача решена. Ура, товарищи? :)

  На самом деле не совсем "ура" - имеются ещё некоторые проблемы, относящиеся к ограниченности аппаратной поддержки оверлеев. В DX SDK, например, указываются следующие возможные (в зависимости от видеокарты) ограничения: размер оверлея и его положение на экране должно быть кратно какому-то числу, а масштабирование - укладываться в определённый диапазон. В качестве примера приводится минимальное масштабирование в 1.3 раза, но такое представляется маловероятным. На практике наименьший диапазон масштабирования продемонстрировал Trident 3D (1 -16), у GeForce, для сравнения, 0,001 - 20. Параметры выравнивания размеров и положения на всех протестированных видеокартах равны нулю или единице (т.е. ограничений нет). Так что "официальные" ограничения на самом деле не слишком важны.

  Далее, как следствие ориентации на вывод видео, большинство видеокарт поддерживают только оверлеи с форматом пикселей YUV вместо простого и знакомого RGB. Формат YUV соответствует видео (телевизионному) сигналу: яркость (Y) и цветовые разности синий-яркость (U) и красный-яркость (V), причём последние два параметра являются общими для двух соседних (по горизонтали) пикселей, т.е. формат фактически 16-битный. Но перевод изображения в этот формат, несмотря на внешнюю сложность - не главная проблема. Главная - это то, что блиттинг с ним не работает, или работает неправильно. Поэтому данные в оверлей приходится запихивать блокировкой (за исключением случаев, когда требуется вывод нескольких кадров анимации, как в примере Mosquito). А блокировка поверхности в видеопамяти на каждом кадре (в системной оверлей не создать) - это неизбежные тормоза, о чём достаточно хорошо написано в [3]. Размер оверлея приходится ограничивать, и хотя его можно вывести на весь экран благодаря аппаратному масштабированию с фильтрацией, этот метод хорошо работает только в случае размытых абстракционистских картинок наподобие тех, что выдаёт AVS.

  Однако ориентация на видео приносит и некоторые дополнительные возможности, например, поддержку цветового контроля (Color Control). Этот контроль имеет примерно то же назначение, что и экранное меню вашего монитора, т.е. позволяет "бесплатно" для производительности изменять базовые параметры изображения (оверлея в нашем случае): яркость, контрастность, цветовой тон, насыщенность, чёткость и гамму. Некоторые из этих параметров могут не поддерживаться (например, GeForce не знает о последних двух); на старых видеокартах цветовой контроль может вообще не функционировать. Но всё-таки его поддержка у оверлеев встречается гораздо чаще цветового контроля первичной поверхности, который тоже теоретически должен существовать (обычно имеется только менее функциональный гамма-контроль).

  Подведём итоги теоретической части. Выяснено, что рисование на рабочий стол или любой произвольный цвет (например, фон окон) с помощью оверлеев жить и работать может, с ограничениями по размеру оверлея. Пример такого рода будет рассмотрен в следующем разделе.

  Со скринмейтами ситуация сложнее. Несмотря на то, что пример Mosquito изображает как раз что-то в этом роде, по нему хорошо видны ограничения оверлеев в области скринмейтостроения: на GeForce отсутствует source color key (вот так вот, DCK есть, а о такой банальной вещи не позаботились), т.е. комар рисуется вместе с фоном, ну и летает он в гордом одиночестве. Первое можно лечить, используя DCK для вывода на одноцветный фон (например, на фоновый цвет окон). А что? Летающий по окнам дух БГ ("The Spirit of Gates"), например. Опять же, хорошее оправдание, что один… (к счастью, всего один! :). Сделать множественное число можно, только используя оверлей в качестве заднего буфера. Но здесь возникают трудности с YUV - блиттинг нужно эмулировать, т.е. блокировать буфер, для приемлемой скорости уменьшать его размер и получать на экране размазню. Впрочем, в некоторых случаях, если требуется изобразить жизнь существ мутных и нечётких (амёб? "Жизнь под микроскопом", ага), такой подход может быть использован. В частности, в рассматриваемом далее примере этим способом реализована система частиц.

  Конечно, всё зависит от видеокарты - на старичке Rage 3D (т.е. скорее всего на всей продукции ATI) и i82815 присутствует цветовой ключ источника; возможно, у вас будут работать и RGB-оверлеи (во множественном числе - да не в этой жизни! Впрочем, на какой-то старой платформе, то ли Atari, то ли Amiga, говорят, было…). Моё дело - предупредить, что на едва ли не самых массовых GeForce имеется именно такой набор поддерживаемых возможностей. И вряд ли в старших моделях он будет расширяться - в NVidia заняты больше шейдерами, чем какими-то там "наложениями" и "перекрытиями" (так перевели "overlays" в драйверах Detonator и "Универсальном проигрывателе" соответственно). Предполагается, что "перекрывать" нужно только видео - и MS в DirectX8 выносит поддержку оверлеев из помершего DirectDraw в DirectShow (вот почему мы рассматриваем DX7). Поэтому, если требуется именно видео, лучше использовать DirectShow8-9, интерфейс IOverlay, прошу любить и жаловать. А если вы нежно ненавидите MS с их DirectX - среди флагов инициализации OpenGL присутствует константа PFD_OVERLAY_PLANE…

Практика

  Рассмотрим написание программы, выводящей некий спецэффект на рабочий стол либо на место произвольного цвета (основным режимом считаем рисование на рабочем столе). Относительно выбора спецэффекта особо затрудняться не будем, не в этом суть, и AVS всё равно не догнать, поэтому позаимствуем из примеров к библиотеке FastLIB алгоритм рисования огня (к сожалению, следы автора в сети потерялись, так что могу только выразить ему свою благодарность прямо здесь и сейчас и привести ссылку на [4] в архиве DelphiGFX). Добавим простенькую систему частиц, то бишь файерболов, и назовём полученный результат DesktopOnFire… хотя подождите, мы ведь его, результат, ещё не получили.
Общий алгоритм будет следующим:

  1. Инициализировать DirectDraw (только первичную поверхность).
  2. Проверить возможности по выводу оверлеев и создать (если можно) оверлей.
  3. Убрать с рабочего стола картинку, если есть, и присвоить ему цвет DCK.
  4. Включить оверлей с DCK.
  5. Выполнить системные задачи, как-то создание окна (невидимого), иконки в трее, меню и.т.д.
  6. В основном цикле либо по таймеру обновлять содержимое оверлея.
  7. При выходе из программы восстановить состояние рабочего стола.

  Рассмотрим пункты, непосредственно связанные с рабочим столом, DirectX и оверлеями, т.е. фактически все, кроме 5-го. Эта часть программы вынесена в модули DX_Overlay (класс TDXOverlay) и DX_Utils (вспомогательные процедуры DirectDraw).

Инициализация DirectDraw

  Выполняется в методе TDXOverlay.InitDX, который вызывается из конструктора:

  If not DDCheck('DirectDrawCreate:',
                DirectDrawCreateEx(nil,FDD7,IID_IDirectDraw7,nil))
				then Exit;
// получение интерфейса DirectDraw
If not DDCheck('SetCooperativeLevel:',
                FDD7.SetCooperativelevel(GetDesktopWindow,DDSCL_NORMAL))
				then Exit;

// установка уровня кооперации, поскольку
// окна на экране нет,
// можно взять handle desktop'а
If not (CreateSurface(FDD7, 0, 0, [scIsPrimary], 0, nil, FPrimarySurface)=0)
then Exit;
// создание первичной поверхности
GetSurfPixelFormat(FPrimarySurface,FScrFormat);
// получение её формата

  CreateSurface в данном случае - это не метод IDirectDrawSurface, а его "обёртка" в DX_Utils, позволяющая создавать одной строчкой кода практически любые типы поверхностей, вплоть до текстур с мип-уровнями. К сожалению, громоздкость DX часто требует создания таких вот "обёрток" для приведения кода к более-менее читабельному виду ("But why bother, when there is this order nifty procedural API already there" - хмыкает некто Кармак, но мы его слушать не будем… по крайней мере, сейчас :).

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

Создание оверлея

  Метод TDXOverlay.InitOverlay, вызывается также из конструктора в случае успешного выполнения предыдущего пункта.

GetOverlayCaps(FDD7,FCaps,'OverlayCaps.txt');
// проверка поддержки оверлеев по флагам DD и вывод отчёта в файл
If not ((FCaps.Enabled) and (FCaps.Stretch) and (FCaps.DestCKey)) then Exit;
// нам требуется масштабирование и цветовой ключ приёмника
WScale:=FScrSize.x / FSize.x;
If WScale>FCaps.MaxStretch then FSize.x:=Round(FScrSize.x/FCaps.MaxStretch);
// подправляем размер оверлея до такого,
// с которого он может масштабироваться на полный экран
If (FCaps.AlignSrcSize) and (FCaps.ASrcSizeValue<>0) then
  FSize.x:=(FSize.x div FCaps.ASrcSizeValue) * FCaps.ASrcSizeValue;
// подправляем размер оверлея для соответствия требованиям
If FSize.x>FScrSize.x then FSize.x:=FScrSize.x;
With FFormat do
  YUVFormatNum:=CreateOverlay(FDD7, FSize.x, FSize.y, FOverlay, @DDPFormat);
// создание оверлея
If FFormat.YUVFormatNum=-1 then Exit;
If (FCaps.ColorControl) then begin
  FOverlay.QueryInterface(IID_IDirectDrawColorControl,FColorControl);
  If (FColorControl<>nil) then FCCSupported:=ColorControl(FColorControl,0,0);
end; // получение интерфейса и поддерживаемых типов цветового контроля

  В GetOverlayCaps ничего интересного и сложного нет - это просто перенос из DDCaps всех связанных с оверлеями битов/значений в более компактную структуру TOverlayCaps. Разве что получение названия видеокарты для отчёта может оказаться для кого-то в новость - см. .

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

  Вообще, вся "проверка" сделана больше для получения отчёта, т.к. само создание оверлея, как выяснилось, делается по методу полного перебора. MS, слегка покраснев, поясняет, что это не шутка, и что никакой функции вроде EnumOverlayFormats до сих пор не придумано, поэтому нужно тыкать во все вообразимые форматы, надеясь, что повезёт. (Справедливости ради, существует функция IDirectDraw7.GetFourCCCodes для получения кодов не-RGB форматов (FourCC), но они включают не только нужные YUV, но и форматы сжатых текстур и пр. Копаться в данном списке и гадать относительно каждого формата, YUV это или не YUV, не представляется рациональным). В примере Mosquito перебираются 4 формата (2 RGB и 2 YUV). Насчёт RGB - это, конечно, излишний оптимизм, поэтому будем проверять только YUV (функция CreateOverlay):

Const FormatsYUV : array [0..1] of String = ('UYVY','YUY2');
Var pf : TDDPixelFormat;
ZeroMemory(@pf,SizeOf(TDDPixelFormat));
pf.dwSize:=SizeOf(TDDPixelFormat);
pf.dwFlags:=DDPF_FOURCC; // требуется формат FourCC
For n:=0 to 1 do begin
  pf.dwFourCC:=MAKEFOURCC(FormatsYUV[n,1], FormatsYUV[n,2],
                          FormatsYUV[n,3], FormatsYUV[n,4]);
  hr:=CreateSurface(DD7,w,h,[scIsOverlay, scClearSurface],0,@pf,surf);
    // пытаемся создать оверлей
  If hr=DD_OK then begin
    If DestPFormat<>nil then DestPFormat^:=pf;
    Result:=n; Break;
  end;
end;

  Кстати, эти форматы практически одинаковы и отличаются только порядком записи компонент. Тем не менее, Trident 3D не признаёт первый и готов работать только со вторым, поэтому даже такой ограниченный перебор оказывается полезным. GeForce поддерживает оба формата и ещё некоторые - так что, если интересно, можно почитать описания YUV [5], сделать-таки GetFourCCCodes или запустить DirectX Caps Viewer из комплекта DX8 SDK, выбрать нужные форматы и… вам правда интересно? :)

  После создания оверлея единственная опциональная задача - инициализация цветового контроля. Для вывода на рабочий стол оно нам, строго говоря, не нужно, но вообще - пригодится, поэтому запрашиваем соответствующий интерфейс у оверлея и получаем поддерживаемые типы очередной "обёрткой" ColorControl.

Подготовка рабочего стола

  Если включён режим вывода на рабочий стол (основной), следует убрать картинку и присвоить desktop'у цвет DCK; при выводе на произвольный цвет и при завершении работы программы нужно, наоборот, восстановить его состояние. Эти (и ещё некоторые) действия выполняет метод TDXOverlay.SetOverlayCKey:

procedure TDXOverlay.SetOverlayCKey(Color: DWord; Draw2Desktop: Boolean);
Const DsTempColor98 = $0F001F;
      DsTempColorNT = $100020;
Var pb : PByte;
    CKey : DWord;
    KeyQuad : TRGBQuad;
begin
If Draw2Desktop then
  If FIsWinNT then Color:=DsTempColorNT else Color:=DsTempColor98;
If Draw2Desktop<>FDraw2Desktop then begin
  If Draw2Desktop then begin
    FDsColor:=Color; FDsWallPaper:='';
    SetDesktopState(FDsColor,FDsWallpaper,True);
  end else SetDesktopState(FDsColor,FDsWallpaper,False);
  // устанавливаем на рабочий стол нужный цвет и убираем картинку
  FDraw2Desktop:=Draw2Desktop;
end;
KeyQuad.rgbRed  :=(Color and $FF);
KeyQuad.rgbGreen:=(Color and $FF00) shr 8;
KeyQuad.rgbBlue :=(Color and $FF0000) shr 16;
CKey:=0; pb:=PByte(@CKey);
SetImagePixel(pb,KeyQuad,FScrFormat);
// преобразуем цветовой ключ к формату первичной поверхности
ShowOverlay(CKey);
end;

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

function SetDesktopState(Var Color : DWord; Var Wallpaper: String;
                         GetOldValues : Boolean): Boolean;
Var Key : HKey;
    NewColor, ColorType : DWord;
    NewWallpaper : String;
begin
Result:=False;
NewColor:=Color; NewWallpaper:=WallPaper; ColorType:=COLOR_BACKGROUND;
If RegOpenKeyEx(hKey_Current_User, PChar('Control Panel\Desktop'), 0,
                KEY_ALL_ACCESS, Key) = ERROR_SUCCESS then
  begin
    If GetOldValues then Wallpaper:=ReadRegistryString(Key,'Wallpaper');
// получаем текущее имя файла обоев
    Result:=WriteRegistryString(Key,'Wallpaper',NewWallpaper);
// записываем новое
    RegCloseKey(Key);
  end;
If Result then begin
  If GetOldValues then Color:=GetSysColor(ColorType);
  // получаем текущее значение цвета рабочего стола
  SetSysColors(1,ColorType,NewColor);
  // записываем новое
  SystemParametersInfo(SPI_SETDESKWALLPAPER, 0, nil, SPIF_SENDWININICHANGE);
  // применяем изменения
end;
end;

  Как легко заметить, картинка на рабочем столе изменяется через реестр (функции ReadRegistryString и WriteRegistryString упрощают запись/чтение строк из реестра), а цвет - через функции GetSysColor/SetSysColors. Изменение параметров требует некоторого времени - около 1 с, вроде немного, но при постоянных запусках (при отладке) подобные тормоза раздражают - в таком случае можно установить по умолчанию рисование на произвольный цвет. Опять же, при вылете с ошибкой не придётся вручную всё восстанавливать.

  Возможно, именно с задержкой при смене состояния desktop'а (или с "инерциальностью" оверлея на медленных видеокартах) связан следующий эффект - для нормальной работы перед вызовом SetOverlayCKey следует слегка "притормозить" - поставить Sleep(1). Почему именно перед - неясно, тем не менее, на Trident 3D работает только так и никак иначе.

  В качестве цветового ключа для вывода на рабочий стол в AVS устанавливается (в формате RGB) цвет 31-0-15. Для Win98 можно взять тот же, но для WinNT (главным образом Win2000), как было выяснено опытным путём, лучше подходит 32-0-16 - он более корректно переводится в 16-битный цвет (цветовой ключ нужно задавать в формате первичной поверхности). Собственно перевод выполняет функция SetImagePixel, используя для этого полученные ранее (в GetSurfPixelFormat) параметры.

  Поддержка 8-битного формата рабочего стола не предусмотрена, но её при необходимости легко прикрутить - требуется только правильно задать цветовой ключ. Оверлей от формата первичной поверхности не зависит и отображает 65536 цветов даже в 8-битном режиме - возможность, которая была весьма ценной в недалёком прошлом, но малоприменимая сейчас.

"Включение" оверлея

  Данный пункт, реализуемый методом TDXOverlay.ShowOverlay, для простоты выполняется в той же SetOverlayCKey, сразу после изменения параметров рабочего стола. Сам метод тоже не слишком сложен:

procedure TDXOverlay.ShowOverlay(ColorKey : DWord);
Var OverFX : TDDOverlayFX;
begin
ZeroMemory(@OverFX,SizeOf(TDDOverlayFX));
OverFX.dwSize:=SizeOf(TDDOverlayFX);
OverFX.dckDestColorkey.dwColorSpaceLowValue:=ColorKey;
OverFX.dckDestColorkey.dwColorSpaceHighValue:=ColorKey;
DDCheck('UpdateOverlay: ', FOverlay.UpdateOverlay
  (nil,FPrimarySurface,nil,DDOVER_KEYDESTOVERRIDE or DDOVER_SHOW,@OverFX));
end;

  В данном случае выполняется установка оверлея с DCK на полный экран. Если требуется вывод в произвольном месте с произвольным масштабированием (при создании оверлейного скринмейта), функцию придётся доработать, передавая ей соответствующие параметры и преобразуя их в прямоугольники приёмника (destination rectangle, положение/размер на экране) и источника (source rectangle, можно задавать им текущий кадр анимации, если хранить эти анимации в виде "полоски"), которые затем скармливаются UpdateOverlay. Если требуется изменить только положение оверлея - можно использовать IDirectDrawSurface7.SetOverlayPosition.

Обновление содержимого оверлея

  После "включения" оверлей ведёт себя так же, как и первичная поверхность - т.е. постоянно находится на экране (точнее, висит "перед" экраном), и все изменения его содержимого (памяти) мгновенно отображаются. Продолжая аналогию с первичной поверхностью, можно предположить, что писать прямо на экран - не есть хорошо, требуется задний буфер, который можно было бы переключать (Flip). В принципе, это возможно (в примере Mosquito таким образом сделана анимация комара, благо кадров анимации всего три), но в нашем случае оказалось не нужно - никаких визуальных артефактов из-за отсутствия буферизации не возникает. Поэтому для экономии памяти дополнительные буферы не используется; обновление оверлея делается просто блокировкой и заполнением.

  Точнее, сначала изображение генерируется - рисуется огонь и частицы (вынесено в модуль EffectSource_unit, см. исходники), а затем копируется в оверлей (метод TDXOverlay.DrawImage). Рассмотрим второй этап, и для начала - параметры, передаваемые методу DrawImage.

  Изображение-источник для пущей универсальности передаётся не в виде объекта-контейнера, а в виде набора параметров, задающих его положение в памяти - указатель на начало первой скан-линии, шаг (pitch) изображения, указатель на палитру. Так что метод воспримет и используемый в примере TFastDIB, и VCL'овский TBitmap, и IDirectDrawSurface после Lock, хотя в последнем случае желательна доработка, т.к. порядок следования цветовых компонент у surface может быть произвольным (в данном случае он считается соответствующим стандарту Windows, т.е. bbggrr). Размер изображения должен быть равен размеру оверлея; поддерживаются 8 и 24-битные битмапы.

procedure TDXOverlay.DrawImage(Src : PByte; SrcPitch : Integer; 
  Pal : PRGBQuad = nil;
                               Reversed : Boolean = False);
Var x,y,w,h,Pitch : Integer;
    p, pb, spb : PByte;
    c : array[0..1] of PRGBQuad;
begin
w:=FSize.x div 2; 
// за один шаг обрабатывается 2 пикселя, поэтому берём половину ширины
h:=FSize.y;
p:=LockSurface(FOverlay,Pitch);
If p<>nil then begin
  For y:=0 to h-1 do begin
    spb:=Src; pb:=p;
    For x:=0 to w-1 do begin
      If Pal=nil then begin
        c[0]:=PRGBQuad(spb); Inc(spb,3);
        c[1]:=PRGBQuad(spb); Inc(spb,3);
  // если палитры нет (24 бит), получаем цвет непосредственно 
  // из текущего положения указателя
      end else begin 
        c[0]:=Pal; Inc(c[0],spb^); Inc(spb);
        c[1]:=Pal; Inc(c[1],spb^); Inc(spb);
      end; // иначе высчитываем смещение в палитре и берём цвет оттуда
      SetYUVPixel(PDWord(pb),c[0],c[1],FFormat.YUVFormatNum); Inc(pb,4);
	// преобразуем 2 полученных RGB в YUV
    end;
    Inc(p,Pitch);
    If Reversed then Dec(Src,SrcPitch) else Inc(Src,SrcPitch);
  end;
  FOverlay.Unlock(nil);
end;
end;

  Достаточно стандартная процедура заполнения поверхности при непосредственном доступе, за исключением разве что преобразования RGB в YUV. Напоминаю, в формате YUV цветность задаётся (усредняется) для двух соседних пикселей, поэтому и преобразовывать удобнее по два пикселя. Последовательность расположения цветовых компонент для первого из используемых форматов (UYVY) в соответствии с его названием будет U-Y1-V-Y2, где U и V - цветовые разности, а Y1,Y2 - яркости первого и второго пикселя (каждый компонент занимает 1 байт). Для второго формата - примерно то же самое: Y1-U-Y2-V.

  Данное преобразование выполняет функция SetYUVPixel, хотя и не совсем корректно - цветность на самом деле не усредняется, а берётся от одного из пикселей, не проверяется выход за пределы ("and $FF" не в счёт) - но всё-таки лучше, чем соответствующая функция из примера Mosquito.

procedure SetYUVPixel
            (pd : PDWord; p1, p2 : PRGBQuad; YUVFormatNum : Integer);
Var y0, y1, u, v : DWord;
begin
y0:=263*p1.rgbRed + 516*p1.rgbGreen + 100*p1.rgbBlue + $4000;
u:= - 152*p1.rgbRed - 298*p1.rgbGreen + 450*p1.rgbBlue + $20000;
y1:=263*p2.rgbRed + 516*p2.rgbGreen + 100*p2.rgbBlue + $4000;
v:= 450*p2.rgbRed - 377*p2.rgbGreen - 73*p2.rgbBlue + $20000;
If YUVFormatNum=0 then
  pd^:=((y1 shl 14) and $FF000000) or ((v shl 6) and $FF0000) or
       ((y0 shr 2) and $FF00) or ((u shr 10) and $FF)
else
  pd^:=((v shl 14) and $FF000000) or ((y1 shl 6) and $FF0000) or
       ((u shr 2) and $FF00) or ((y0 shr 10) and $FF);
end;

  Коэффициенты для получения яркости и цветовых разностей взяты не помню уж откуда, наверное, на сайте по форматам FourCC [5] или где-то рядом. В данном случае они домножены на 1024, чтобы обойтись целыми числами. Можно преобразовывать и в соответствии с определением YUV, т.е. получать яркость и вычитать её из красного и синего цветов, как это описано [6].

  Если говорить о обновлении оверлея - можно вспомнить ещё цветовой контроль, который мы инициализировали ранее, и реализовать через него, например, изменение цветового тона изображения. Для этого требуется просто вызывать функцию ColorControl с нужными параметрами:

function ColorControl(cc : IDirectDrawColorControl; ccType : DWord;
                      ChangeInPercents : Integer): DWord;
Var DDCControl : TDDColorControl;
begin
ZeroMemory(@DDCControl, SizeOf(TDDColorControl));
DDCControl.dwSize:=SizeOf(TDDColorControl);
cc.GetColorControls(DDCControl);
Result:=DDCControl.dwFlags;
If (ccType=0) or (ChangeInPercents=0) then Exit;
With DDCControl do
  Case ccType of
//  ...
    DDCOLOR_HUE : begin
      lHue:=lHue + Round(ChangeInPercents*1.8);
    end;
//  ...
// по поводу других параметров - см. исходники
  end;
DDCControl.dwFlags:=ccType;
cc.SetColorControls(DDCControl);
end;

  HUE (произносится как "хью", а не по буквам :) - это как раз и есть цветовой тон. Вообще метод IDirectDrawColorControl.SetColorControls подразумевает установку абсолютного значения этого и других параметров, но мне показалось более логичным передавать в функцию изменение в процентах - вроде как крутим соответствующую ручку или нажимаем кнопки "больше"/"меньше". При этом цветовой тон изменяется циклически, а вот остальные параметры нужно ограничивать, что и реализовано в полной версии функции.

  Поскольку цветовой контроль - возможность более редкая, чем собственно оверлеи, следовало бы сделать его эмуляцию (через палитру, например). Но в данном случае изменение цветового тона - функция не первостепенной важности, и поэтому написание эмуляции оставлено на совесть читателя… конечно, надо быть очень совестливым, чтобы помнить о пользователях древних Trident 3D и Rage 3D :).

Заключение. Итоги и нерешённые проблемы

  Собственно, это всё, что необходимо знать для применения оверлеев вообще и вывода на рабочий стол в частности; какие-то детали в тексте не описаны - см. прилагаемые исходники. Эти исходники также можно считать примером работы с WinAPI (модуль winstuff_light) - создание иконки в трее, всплывающего меню к ней, диалога выбора цвета… вообще-то, если бы я писал программу для себя, а не на публику, то просто воспользовался бы [7] (VCL в данном случае - это, как понимаете, муху слоном :). А так получилась хоть очень маленькая, но своя WinAPI-библиотечка.

  Однако, при всех достоинствах примера, в нём имеется несколько ошибок и некорректностей, претензии по которым я вряд ли буду принимать (отмахиваясь фразами "не относится к основной теме", "не фатально", "проявляется не везде", "ну это же всего лишь пример" и т.п.), но о которых следовало бы упомянуть - во избежание.

  Первое - при запуске под WinXP окно выбора произвольного цвета может подвешивать систему на некоторое время - хотя огонь исправно полыхает, Explorer на действия пользователя не реагирует (придётся подождать где-то с минуту). Точнее, подвешивает не само по себе окно (вызываемое функцией ColorDialog), а в сочетании с функцией SetSysColors из SetDesktopState. Примирить эти функции у меня так и не получилось; можно отказаться от использования какой-нибудь (зачем выбирать произвольный цвет, если используется обычно только чёрный или белый), попробовать VCL'ный TColorDialog, или найти способ заменять цвет без SetSysColors. Или можно решить, что вовсе не программа виновата, а языковая панель WinXP - если её развернуть из трея на экран или полностью убрать, всё работает нормально…

  Другая проблема связана опять-таки с WinXP и с заданием определённой скорости обновления оверлея. Вообще обновление выполняется в основном цикле, и чтобы программа не загружала процессор полностью, делаются паузы функцией Sleep. В Win98 эта функция работает более-менее стабильно, сколько поставите - примерно такая пауза и будет. А вот жадная XP, едва почуяв, что программа готова пожертвовать часть процессорного времени на общее благо, сразу откусывает столько, что огонь начинает конвульсивно дёргаться даже если просто подвигать мышью (справедливости ради - XP зато не тормозит при появлении Hint'ов). Поэтому при запуске под ОС семейства NT пауза просто отключается, что, конечно, не есть хорошо - предполагается, что программа такого рода должна по возможности меньше тормозить работу других. Можно попробовать таймеры, но однозначно не стандартные, т.к. данный алгоритм рисования огня требует достаточно высокой скорости обновления для получения реалистичного эффекта, с интервалом не более 20 мс. Мультимедиа таймер (timeSetEvent в MMSystem) лучше работает на таких интервалах, но при этом подтормаживает систему. Остаётся waitable таймер (см. CreateWaitableTimer), хотя он по сути то же самое (отдельный поток), но за счёт ручной настройки (выставления приоритетов и пр.), может, получится лучше…

  Последнее - при некоторых размерах оверлея (например, 640*480) вылетает с ошибкой функция рисования огня. Выдать корректную ошибку в данном случае трудно - функция написана очень "профессионально", т.е. непонятно. Понятно только, что указатель вылезает за отведённые границы, но где и как… Со стандартным размером оверлея (300*300) всё работает нормально.

  Ну а в остальном, как говорится, enjoy.

Список ссылок.

  1. Статья по скринмейтам:
  2. Информация по поддержке оверлеев старыми видеокартами:
  3. Статья по непосредственному доступу к поверхностям DirectDraw:
  4. Библиотека FastLIB:
  5. Описания форматов YUV:
  6. Ещё одна статья по YUV:
  7. Библиотека KOL:

(c) Sapersky 2003-2004







При перепечатке любого материала с сайта, видимая ссылка на источник www.warayg.narod.ru и все имена, ссылки авторов обязательны.

© 2005