Delphi programming blog
Источник: http://teran.karelia.pro/articles/item_4419.html
 

Direct2D: Layers Overview

Опубликовано 16.05.2010 г. 13:03

В продолжение начатой в предыдущий раз темы изучения Direct2D, разберем статью MSDN Layers Overview. Поэтому далее вы можете прочитать перевод(пересказ) данной статьи с примечаниями и выкладками аналогов исходного кода с использованием Delphi. Данный обзор описывает основы использование слоев в Direct2D и состоит из следующих разделов:

  • Что такое слои?
  • Создание слоев
  • Границы содержимого слоев
  • Маски прозрачности
  • Геометрические маски
  • Альтернативы к использованию слоев.

Что такое слои?

Слои в Direct2D позволяют приложению управлять группой операций рисования и предсталвены объектами типа ID2D1Layer [не имеют аналогов в модуле Direct2d.pas] Вы можете использовать слои добавляя их на "поверхность" рисования (renderTarget). Последующие операции рисования на данной поверхности относятся к добавленному слою. После того как вы выполнили необходимые операции со слоем, вы можете снять его с вашей поверхности, после чего содержимое будет перенесено на поверхность. Как кисти, слои создаются с помощью самой поверхности и могут использоваться с любым видом поверхностей [есть несколько типов поверхностей, например hwndRenderTarget, bitmapRenderTarget и обычная renderTarget], но при этом только с одной поверхностью в один момент времени. Слои предоставляют мощный инструмент для создания различных эффектов, но при этом требуют дополнительных ресурсов, поэтому для повышения производительности требуется максимально задействовать их повторное использование, а не многократное создание.

Создание слоев

Использование слоев требует знания методов CreateLayer, PushLayer и PopLayer [методы RenderTarget]а также структуры D2D1_LAYER_PARAMETERS [TD2D1LayerParameters], которая содержит набор параметров, определяющих как слой будет использоваться. Далее приведен порядок использования и описание методов и структуры.

  1. Вызовите метод CreateLayer чтобы создать новый слой [метод вызывается у renderTarget и имеет один выходной параметр - слой]
  2. После начала рисования на поверхности (т.е после того как был вызван метод BeginDraw) вы можете использовать метод PushLayer(). Данный метод добавляет указанный слой на поверхность, и все последующий операции рисования применяются к слою, до тех пор пока не вызван метод PopLayer(). Метод pushLayer получает два параметры: объект слоя (ID2D1Layer) созданный при вызове CreateLayer и структуру параметров TD2D1LayerParameters. Про свойства структуры будет рассказано позже, на примерах.
  3. Чтобы объединить содержимое слоя и поверхности по окончании рисования, следует вызвать метод PopLayer, при этом следует учесть, что метод PopLayer следует вызывать до завершения рисования и вызова EndDraw.

При использовании данных методов следует убедиться что для каждого вызова PushLayer существует соответствующий вызов PopLayer.

Поскольку далее оригинал статьи содержит примеры использования с исходным кодом и его объяснение, то мы отступим от первоисточника, и займемся адаптацией приведенных примеров с использованием Delphi. Приведенный далее исходный код будет расширять пример из прошлой статьи Creating a Simple Direct2D Application, напомню что в тогда мы переопределили свойство canvas главной формы приложения для использования TDirect2DCanvas. Чтобы не загромождать исходный код, удалим из процедуры рисования вывод двух квадратов, и оставим рисование только сетки, которое впрочем заключим в {$REGION}. Сразу проведем некоторую модификацию кода: при перерисовке изображения формы происходит некоторое мерцание (источник и первоисточник), поэтому требуется самостоятельно обработать сообщение WM_ERASEBKGND, видимо чтобы форма не очищалась перед рисованием. Поэтому добавим добавим данный метод:

  private
    procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND;
.....
procedure TMainForm.WMEraseBkgnd(var Message: TWMEraseBkgnd);
begin
    message.result := 1;
end;

Далее поскольку мы будем рисовать некоторую картинку, то ее следует загрузить и хранить, так что опять же расширим описание формы добавив туда img : TImage (можно конечно добавить ее как компонент на форму, только прийдется отключить ее показ используя свойство visible). Далее в процедуре создания формы инициализируем данный объект, и загрузим требуемое изображение.

procedure TMainForm.FormCreate(Sender: TObject);
begin
    .......
    img := TImage.Create(self);
    img.Picture.LoadFromFile('img.jpg');
    ClientHeight := img.Picture.Height;
end;

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

procedure TMainForm.FormPaint(Sender: TObject);
var w,h : integer;
    x,y : integer;
begin
    h := ClientHeight;
    w := ClientWidth;

    with Canvas do begin
        BeginDraw();
        renderTarget.Clear(D2D1ColorF(1,1,1,1));
        {$REGION '> Draw background grid'}
        ......................
        {$ENDREGION}

        Draw(0,0,img.picture.graphic);
        //тут будет рисование слоев
        EndDraw();
    end;
end;

Все приведенные далее действия будут проводится между выводом полной картинки с использованием метода Draw() и до завершения рисования с вызовом EndDraw(). Говоря о методе Draw скажем, что параметрами метода являются координаты левого верхнего угла для рисования картинки, а также само изображение. Также мы можем убедиться что для рисования используется действительно Direct2D просмотрев исходный код функции Draw, который через цепочку вызовов других функций приводит нас к renderTarget.DrawBitMap(). Однако сложно не заметить при просмотре этого кода, что передав, как этого требует описание функции Draw, параметр TGraphic, в итоге опять же будет происходить создание TBitmap. Спрашивается зачем? Вывод на мой вгляд может быть только один: рисовать изображение надо напрямую используя renderTarget.DrawBitmap() чтобы избежать лишнего создания объектов, поскольку процедура рисования должна быть настолько быстрой, насколько это возможно. Таким образом, загрузив изображение в TImage, мы должны создать соответствующий объект ID2D1Bitmap и использовать его. Впрочем, это у меня не получилось (: что требует дальнейшего изучения. Казалось бы все просто: добавляем в private секцию описание картинки bitmap : ID2D1Bitmap; после загрузки изображения в TImage инициализируем наш bitmap. Для чего используется вызов

bitmap := Canvas.CreateBitmap(img.picture.bitmap);

Все бы должно быть хорошо, но после этого вызова img становится равен nil, а на форме вообще больше ничего не рисуется. Поэтому оставим этот вопрос до лучших времен. Сразу представим результат работы (рисунок ниже), на рисунке располагается 1) изначальное изображение, 2) отрисовка изображения с помощью границ 3) использование маски прозрачности 4) использование геометрической маски.
 

Границы содержимого

Границы содержимого (contentBounds) ограничивают ту область, которая после окончания рисования на слое будет перенесена на поверхность. Таким образом мы можем отрисовывать только часть изображения. Алгоритм таков: 1. Используем метод setTransform чтобы установить исходные координаты для начала рисования (скажем так при рисовании начало координат наше перенесется в точку (300,100)). 2. Создадим слой 3. Определим границы для отрисовки изображения на слое в координатах слоя (прямоугольник topleft - (50,50), bottomright- (150,200)) 4. установим прозрачность равную 0.45 5. добавим слой на поврехность 6. отрисуем изображение 7. извлечем слой.

var  layer : ID2D1Layer;
    lp : TD2D1LayerParameters;
......
        //------ bounds rect
        renderTarget.SetTransform(TD2DMatrix3x2F.Translation(300, 100));
        renderTarget.CreateLayer(nil,layer);

        zeroMemory(@lp,sizeof(lp));
        lp.contentBounds := D2D1RectF(50,50,150,200);
        lp.opacity := 0.45;
        renderTarget.PushLayer(lp,layer);

        Draw(0,0,img.Picture.Graphic);
        //renderTarget.DrawBitmap(bitmap);

        renderTarget.PopLayer();

В результате чего получим часть изображения ограниченную нашим определенным прямоугольником.

Маска прозрачности

Маска прозрачности позволяет делать изображение частично или полностью прозрачным, создавать маску можно с помощью кисти или также изображения. При использовании кисти маска задается с помощью альфа канала. Скажем так реализация данного примера не была такой простой (: Началось все с того что надо было убрать границы содержимого установленные нами ранее. Для этого используется по сути просто прямоугольник с "бесконечными границами", и вспомогательная функция D2D1InfiniteRect, которую почему то забыли описать в модуле Direct2D.pas. Код ее однако должен быть примерно таков, хотя можно придумать другие вариации:

            function D2D1InfiniteRect() : TD2D1RectF;
            begin
                result.top := - MINSHORT;
                result.left := - MINSHORT;
                result.right :=   MAXSHORT;
                result.bottom :=   MAXSHORT;
            end;

Вообще для формирования дефолтной структуры TD2D1LayerParameters предназначена функция D2D1LayerParameters, заголовок которой присутствует в Direct2D.pas, однако он закомментирован, а тело функции вообще отсутствует, в заголовке при этом пропущен один из параметров. Далее выяснилось что стандартным методом canvas.createBrush невозможно создать кисть использующую прозрачность, т.к цвета градиента кисти в параметрах функции имеют тип TColor, а цвета используемые в Direct2D кроме каналов RGB имеют еще и прозрачность, которая в данному случае устанавливается по умолчанию в единицу. Алгоритм действий: 1. С помощью SetTransform сдвинемся вправо. 2. заполняем структуру LayerParameters, убираем границы. 3. Создаем массив описывающий градиент кисти. цвет в позиции 0 должен иметь прозрачность 1, в позиции 1 прозрачность 0. что даст нам исчезновение изображения к краям из центра (кисть - круговой градиент) 4. создаем кисть и определяем ее в LayerParameters 5. рисуем слой.

var   gc : ID2D1GradientStopCollection;
    gs : array[0..1] of TD2D1GradientStop;
    gb : ID2D1RadialGradientBrush;
.........
        //---- opacity mask test
        renderTarget.SetTransform(TD2DMatrix3x2F.Translation(450,20 ));

        zeroMemory(@lp,sizeof(lp));
        lp.contentBounds := D2D1InfiniteRect();
        lp.maskAntialiasMode := D2D1_ANTIALIAS_MODE_PER_PRIMITIVE;
        lp.maskTransform := TD2DMatrix3x2F.Identity();
        lp.opacity := 1;
        gs[0].position := 0;
        gs[0].color :=  D2D1ColorF(clWhite,1);
        gs[1].position := 1;
        gs[1].color := D2D1ColorF(clWhite,0);
        renderTarget.CreateGradientStopCollection(@gs,2,D2D1_GAMMA_2_2,D2D1_EXTEND_MODE_CLAMP,gc);

        renderTarget.CreateRadialGradientBrush(
                         D2D1RadialGradientBrushProperties(D2D1PointF(90,210),D2D1PointF(0,0),90,90),
                         nil,
                         gc,
                         gb
                     );
        lp.opacityBrush := gb;
        //canvas.CreateBrush([clBlack,clWhite],D2D1PointF(50,50),D2D1PointF(0,0),50,50);
        lp.layerOptions := D2D1_LAYER_OPTIONS_NONE;

        renderTarget.PushLayer(lp,layer);
        Draw(0,0,img.Picture.Graphic);
        renderTarget.PopLayer();

Градиентная кисть имеет несколько определяющих свойств. Во первых это координаты ее использования (в системе координат поверхности), во вторых это точка задающая центра градиента (относительно центра кисти), два радиуса (вобще кисть эллиптическая, круг - частный случай), ко всему этому набор цветов градиента. Вообще если требуется применять маску прозрачности только к одному объекту, то рекомендуется это делать не через слой, а с помощью метода FillOpacityMask поверхности.

Геометрическая маска

С помощью использования геометрической маски можно ограничить изображение произвольной геометрической фигурой, фигура задается набором точек, и кривых их соединяющих. Поэтому для использования геометрической маски требуется эту маску создать, т.е описать саму геометрическую фигуру с помощью объекта ID2D1PathGeometry. В нашем случае геометрической фигурой будет четырехугольник, точки которого исключая первую определим в константном массиве. Объект ID2D1PathGeometry создается с помощью фабрики, указатель на которую мы можем получить у нашей поверхности. Непосредственно работа по созданию геометрической фигуры проводится с помощью объекта ID2D1GeometrySink.

const     gpoints  : array[0..2] of TD2D1Point2F = ( (x:150;y:50), (x:280;y:150), (x:150;y:350));
var  pathGeometry : ID2D1PathGeometry;
    factory : ID2D1Factory;
    sink : ID2D1GeometrySink;
.......
        //----geometry mask
        renderTarget.SetTransform(TD2DMatrix3x2F.Translation(650,50 ));

        renderTarget.GetFactory(factory);
        factory.CreatePathGeometry(pathGeometry);

        pathGeometry.Open(sink);
        sink.SetFillMode(D2D1_FILL_MODE_WINDING);
        sink.BeginFigure(D2D1PointF(0,250),D2D1_FIGURE_BEGIN_FILLED);
        sink.AddLines(@gpoints,3);
        sink.EndFigure(D2D1_FIGURE_END_CLOSED);
        sink.Close();

        zeroMemory(@lp,sizeof(lp));
        lp := D2D1LayerParameters(D2D1InfiniteRect);
        lp.geometricMask := pathGeometry;

        renderTarget.PushLayer(lp,layer);
        draw(0,0,img.Picture.Graphic);
        renderTarget.PopLayer;

После того как мы закончили рисовать восстановим исходное состояние поверхности:

        renderTarget.SetTransform(TD2DMatrix3x2F.Identity());

Хотя наверное будет более верным делать это непосредственно перед рисованием.

Альтернативы использования слоев

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

Метки:  Direct2D  |  layers 

Комментарии

ALLIGATOR
16.05.2010 в 20:16
Всё доступно, разжевано, получается отличный курс обучению Direct2D.
Не знаю "болеете" ли Вы кросплатформенностью, и другими модными словами, поинтересуюсь - не изучали ли вы библиотеку SDL ?
ter
16.05.2010 в 20:41
знакомство с SDL ограничилось рисованием спирали Архимеда по точками из файла (: кроссплатформенные вещи меня мало как то интересуют (:
Сирожа
26.05.2010 в 14:45
Долгое время рылся в интернете в поисках доступного и понятно написанного материала. И вот наконец-то наткнулся на ваши статьи. Большое спасибо! Буду изучать всё сначала.
До этого работал с VB6 (пока не перешёл на windows 7, и всё, написанное под директ перестало работать), там единственное что я понял, так это как посылать точку напрямую в память :-) в принципе мне этого хватило чтоб написать нужные процедурки для рисования, и даже написать игру.
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно