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

Мыльные пузыри в Delphi #2

Опубликовано 20.08.2011 г. 03:06

В продолжение предыдущей статьи про разработку аналога заставки "Мыльные пузыри" с использованием Direct2D. Тема данной статьи - сделать пузыри движущимися. Для этих целей я использовал Windows Animation Manager, о котором так же уже рассказывалось немного ранее.

Скажу сразу, результатом я не очень удовлетворен. При большом количество шаров все действо перестает двигаться плавно. Однако, у меня есть мысль по оптимизации. Связана она с тем, что при прорисовки пузыря, как это было реализовано в прошлый раз, активно используются слои. Между тем особо слои использовать не рекомендуется, из за снижения производительности, так что возможно в этом вся причина. Так что думаю, надо уйти от использования слоев, и попробовать создать "кисть-шар" - т.е маску прозрачности в виде растра. Посмотрим, что из этого получится, но в последующих статьях. А пока что об анимации. Сначала можно удалить все действа связанные с таймером, которые были сделаны в качестве заглушки в прошлый раз. Все объекты, касающиеся анимации, я решил сгруппировать в один класс - TBubbleAnimation, и не перегружать этим функционалом сам класс менеджера пузырей.
    TBubbleAnimation = class(TObject)
      strict private
        FTimer : IUIAnimationTimer;
        FManager : IUIAnimationManager;
        FTransitionLibrary : IUIAnimationTransitionLibrary;
      public
        constructor Create();
        procedure UpdateTimer();
        property Timer : IUIAnimationTimer read FTimer;
        property Manager : IUIAnimationManager read FManager;
        property TransitionLibrary : IUIAnimationTransitionLibrary read FTransitionLibrary;
    end;
В общем, класс является по сути контейнером, так что код его реализации весьма прост:
constructor TBubbleAnimation.Create;
begin
    inherited;

    FTimer := CreateComObject(CLSID_UIAnimationTimer) as IUIAnimationTimer;
    FManager  := CreateComObject(CLSID_UIAnimationManager) as IUIAnimationManager;
    FTransitionLibrary := CreateComObject(CLSID_UIAnimationTransitionLibrary) as IUIAnimationTransitionLibrary;
end;

procedure TBubbleAnimation.UpdateTimer;
begin
    FManager.Update(FTimer.GetTime());
end;
При инициализации создаются все нужные COM объекты - менеджер, таймер и библиотека методов анимации переменных. Для удобства введен метод UpdateTimer. Класс менеджер пузырей - TBubbleManager слегка видоизменился. Основное - он изменил предка - теперь это TInterfacedObject. Связанно данное действие с тем, что необходимо поддерживать интерфейс IUIAnimationManagerEventHandler - для перерисовки сцены, когда менеджер меняет состояние. Его метод OnManagerStatusChanged является обратным вызовом для менеджера анимации. Так что основной класс обзавелся парой новых методов:
procedure TBubbleManager.OnManagerStatusChanged(const NewStatus, PreviousStatus: TUIAnimationManagerStatus);
begin
    if NewStatus = UIAnimationManagerBusy then
        UpdateWindow();
end;

procedure TBubbleManager.UpdateWindow;
begin
    InvalidateRect(FRenderTarget.GetHwnd, nil, false);
end;
В оригинальной заставке в начале пузыри появляются не сразу, а по очереди. Для реализации последовательного появления пузырей я ввел два новых члена класса - состояние работы FStarting, и время вылета последнего шара - FStartTime. Конечно же первое необходимо установить в true в конструкторе. А шары будут добавляться при проведении операции отрисовки Render(), а не в конструкторе.
    if FStarting then AddBubble();
при этом сам метод добавления шаров изменен для учета пауз между запусками.
procedure TBubbleManager.AddBubble();
const bubble_wait = 2;
var t : TDateTime;
begin
    t := now();
    if SecondsBetween(t, FStartTime) < bubble_wait then exit;
    FStartTime := t;

    FBubbles.Add( TBubble.Create(FAnimation) );

    FStarting := (FBubbles.Count <= MAX_BUBBLE_COUNT )
end;
Т.е если пауза меньше двух секунд, то ничего не происходит. Иначе добавляется новый пузырь и запоминается время. Когда количество шаров достигает максимального, то состояние FStarting устанавливается в false. Конечно же объект анимации шаров также включен в состав менеджера и инициализируется в конструкторе, здесь же наш менеджер пузырей назначается обработчиком событий смены состояний менеджера анимации (для этого мы ввели поддержку соответствующего интерфейса выше)
    FAnimation := TBubbleAnimation.Create();
    FAnimation.Manager.SetManagerEventHandler(self);
Чтобы закончить с описанием изменений в менеджере пузырей, скажу еще о двух вещах. Если пузырь не двигается, то его надо запустить дальше. Т.е изначально пузырь запускается из левого нижнего угла, и летит куда попало (ну или почти куда (: ), долетая до края экрана он останавливается, и вот если он вдруг остановился, то надо его подтолкнуть дальше. За это будет отвечать небольшой код в начале метода Render:
    FAnimation.UpdateTimer();
    for b in FBubbles do begin
        if not b.Moving then b.MoveNext();
    end;
А в конце рисования всей сцены необходимо проверить статус менеджера анимации, и если он все еще работает, то вновь обновить окно:
    if FAnimation.Manager.Status = UIAnimationManagerBusy then
        UpdateWindow();
Самые большие изменения произошли в классе-пузыре TBubble, ведь именно здесь реализуется весь функционал для отправления пузырей в полет.
    TBubble = class(TObject)
      strict private
        FAnimation : TBubbleAnimation;
        FStoryBoard : IUIAnimationStoryboard;
        FMovement : IUIAnimationVariable;
        FSpeed : integer;

        FStartPos : TPoint;

        FAngle : real;
        FNextAngle : real;

        FColor : TColor;
        function getPoint() : TPoint;
        function isMoving():boolean;
      public
        constructor Create(Animation : TBubbleAnimation);
        procedure   MoveNext();

        property Color : TColor read FColor;
        property Point : TPoint read getPoint;
        property Moving : boolean read isMoving;
    end;
наружу доступны несколько свойств, впрочем новое из них только одно isMoving. Цвет кстати до сих пор меняется случайным образом при долете до края экрана. MoveNext как я уже говорил отправляет шар к следующей грани. А параметром конструктора теперь является объект-контейнер для анимации, что вы могли уже заметить в процедуре добавления пузырей AddBubble. Также появились новые члены класса. FAngle & FNextAngle указывают текущий угол полета, и угол, под которым пузырь полетит, после встречи с краем экрана. FSpeed указыает скорость полета, на начальной стадии (запуска шаров) она выше. FStartPos определяет начальную точку полета - т.е координату от которой пузырь каждый раз стартует, т.е "отскакивает". Вообще если вспомнить, то для создания анимации используются три составных объекта. Во первых нужна сама переменная анимации - IUIAnimationVariable, у нас это FMovement. Далее требуется "задание" анимации, в которое добавляются когда, как долго и как переменная должна меняться - FAnimation : IUIAnimationStoryboard. Ну и третье - закон изменения данной переменной. Мы будем использовать линейное изменение. Для анимации нам требуется передвигать шар из одной точки (х0,у0) в другую (х1,у1). Для этих целей можно анимировать обе переменные х и у. Но можно пойти другим путем - для расчета координат мы можем использовать формулу расстояния между двумя этим точками. Т.е анимированная переменная в нашем случае будет представлять расстояние от точки (х0,у0) до текущей, а координаты вычислим используя синус и косинус нашего угла FAngle. Конструктор класса задает начальные значения переменных. В т.ч начальную точку, угол полета и скорость. Угол будет произвольным от 30 до 60 градусов. Так же было бы неплохо провести все вычисления в экранной системе координат, а то у меня сейчас вычисления происходят исходя из того что точка (0,0) в левом нижнем углу. Так что потом происходит переворот по Оу, что немного запутывает код. Весь алгоритм сводится к тому, что мы имеем начальную точку и угол полета. С помощью этого мы рассчитываем конечную точку - пересечение с краем экрана. После чего рассчитываем расстояние между двумя этими точками. Исходя из расстояния и скорости полета вычисляем время анимации. После чего уже создаем переменную анимации, устанавливаем ей границы, создаем StoryBoard, задаем линейную зависимость изменения и отправляем менеджер на выполнение:
    FMovement := FAnimation.Manager.CreateAnimationVariable(0);
    FMovement.SetLowerBound(0);
    FMovement.SetUpperBound( Hypot(screen.Width, screen.Height) );

    FStoryBoard := FAnimation.Manager.CreateStoryboard();
    ltr := FAnimation.TransitionLibrary.CreateLinearTransition(duration, distance);
    FStoryBoard.AddTransition(FMovement, ltr);

    FStoryBoard.Schedule( FAnimation.Timer.GetTime() );
Тут надо заметить, что для каждого передвижения пузыря от начальной к конечной точке создается новая переменная и StoryBoard. Т.е менеджер анимации владеет таким количеством StoryBoard'ов по количеству самих пузырей. Это связано с тем, что после того как мы отправили "задание анимации" на выполнение мы уже не можем его изменить, и добавить туда анимирование новой переменной/передвижения пузыря. Для расчета конечных точек столкновения с границами экрана необходимо применить простенькие знания математики и геометрии. Имея начальную точку (х0,у0) и угол FAngle, мы знаем, что соответствующее уравнение прямой будет y(x) = tan(FAngle)*(x - x0) + y0. Допустим угол лежит в первой четверти. Следовательно прямая пересекает границу экрана либо на правой либо на верхней грани. Рассчитываем значение у(х), при х = ширине экрана. Если полученное значение у меньше чем высота экрана, то пересечение с правой гранью и координата пересечения (width, y(width)). Если y(width) > height следовательно пересечение с верхней гранью. В таком случае для поиска точки надо решить уравнение - y(x) = height, и найти х при котором выполняется тождество. Код приводить не буду, ибо вроде просто, но место занимает. Кому интересно может посмотреть в приложенном коде. В общем говоря, большой сложности в реализации движения не было, хотя надо признать, что на момент написания прошлой статьи я как то не представлял как можно реализовать анимацию, ибо в предыдущие мои общения с менеджером анимаций я никогда не использовал несколько заданий анимации одновременно, и конечно же не помнил, что это возможно. Если вы вдруг решите попробовать запустить приложенный код на выполнение, то не удивляйтесь что при закрытии приложения (alt+f4) появляется Invalid pointer operation. Я честно сказать пока что не понял в чем тут причина. Для меня весьма странно, что при числе пузырей равном 15 уже наблюдаются подтормаживания. Тут может две причины, либо отрисовка, либо менеджер анимации. Третья - мои руки (: По идее с анимацией проблем не должно быть, так что возможно отрисовка. Надо попробовать отказаться от использования слоев при рисовании пузырей. На самом деле это и действительно кажется слишком сложно, ведь каждый шар рисуется отдельно. Когда фактически они все одинаковы, только цвета разные. Попробую реализовать с использованием растровой кисти в качестве маски прозрачности, по идее должно работать и в таком случае существенно повысить скорость. Так же может быть есть смысл обратить внимание на то, что мои расчеты по передвижению шаров проводятся в целых числах, хотя отрисовку можно проводить во float. Тем не менее это кажется ни при чем, поскольку когда летает 1 пузырь, то подтормаживаний нет, когда их 15, то они начинают быть заметны. Еще беспокоит меня затрата ресурсов. При 15 шарах загрузка процессора около 40%. Такое ни в какие ворота не лезет. Есть предположение что сей факт связан с тем что используется слишком много "задач анимирования". Т.е когда в одной задаче анимируются несколько переменных, как на пример в примере Grid Layout

то все нормально. А если несколько задач, то это начинает загружать процессор, в связи с тем, что чаще происходит перерисовка, ибо задачи между собой не синхронизированы. Также следует добавить изменение цвета шаров по ходу движения но это не сложно, потом сделаю. Вот так выглядят мыльные пузыри настоящий момент:
 
Исходный код доступен .тут (скачать), в архиве также находится скомпилированный файл, если вы хотите просто посмотреть на пузыри (: Для работы очевидно требуется Windows 7. Если д2д также доступен и в Vista то менеджер анимаций нет. Хотя с другой стороны это всего лишь COM библиотека, так что может и перенесен в Vista и другие версии. Если у вас есть идеи по поводу повышения производительности пузырей, то пишите комментарии, буду рад услышать любые замечания и предложения (:

Метки:  Direct2D  |  animation 

Комментарии

denis
08.11.2012 в 04:17
хороший формат изложения мыслей) я тоже заморочился с аналогичным примером и наткнулся на тормоза, делал я не надо direct2d а на обычном gdi, когда оптимизировать было нечего я понял что трабла все таки в прорисовка хотя у меня ваще частицы были хоть их и много. в итоге забрел на этот сайт) читаю узнаю свои сомнения) графика или вычисления?? хехе. вот. д2д особо не помогает как оказалось но я тут копаю варик с opencl - крутая штука, апартное ускорение при вычислениях. dll идет для nvidea и ati там засада все это качать у них там какаято своя студия, вощем я не особо сдвинулся с точки). судя по дате твоего поста мой не очень актуален для тебя но я все же не удержался, так как долго искал инфу про чертов д2д) ладно пойду почитаю че у тебя тут еще есть) респект!
teran
13.11.2012 в 20:38
Да у меня и пузыри эти подтормаживали (: не то что когда много частиц (:
А если частицы рендерить, то лучше двигаться в сторону OpenCL и Direct3D наверное.
В случае когда схожие частицы рисуются стоит обратить внимание на вот этот пример http://msdn.microsoft.com/en-us/library/dd756659%28v=vs.85%29.aspx
я что то хотел переписать его на Delphi, но руки так и не дошли.

Что касается систем частиц, то я хотел было переисать вот этот http://habrahabr.ru/post/149933/ пример на Delphi, но что то не вышло ничего. Не знаком я с Direct3D. Когда речь идет о десятках и сотнях тысяч частиц движения которых можно обрабатывать параллельно, то тут определнно стоит думать об OpenCL
denis
07.01.2013 в 10:27
я вот все никак не решусь взяться за d3d )) особенно меня сводят сума их длинные названия типов и т.п.!! а opengl, как то не то, сложилось стереотипное мышление что опенсорсный движ рано или поздно подведет) линки ща гляну, спасибо за ответы)
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно