DirectComposition: преобразования и анимация

OSzone.net » Microsoft » Разработка приложений » Windows (до Windows 10) » DirectComposition: преобразования и анимация
Автор: Кенни Керр
Иcточник: msdn.microsoft.com
Опубликована: 18.09.2015
Визуалы DirectComposition предоставляют куда больше простых свойств offset и content, о которых я так или иначе говорил в своих последних нескольких статьях. По-настоящему визуалы оживают, когда вы начинаете воздействовать на них с помощью преобразований и анимаций. В обоих случаях Windows-механизм композиции является своего рода процессором, и на вас возлагается ответственность за вычисление или конструирование матриц преобразований, а также анимационных кривых с кубическими функциями и синусоидальными волнами. К счастью, Windows API обеспечивает необходимую поддержку за счет пары взаимодополняющих API-функций. Direct2D имеет превосходную поддержку для определения матриц преобразования, упрощая описание вращения, масштабирования, создания перспективы, трансляции и многого другого. Аналогично Windows Animation Manager освобождает вас от необходимости быть экспертом в области математики, позволяя описывать анимации с помощью богатой библиотеки анимационных переходов (animation transitions), раскадровки (storyboard) с ключевыми кадрами и т. д. Комбинируя Windows Animation Manager, Direct2D и DirectComposition в одном приложении, вы по-настоящему прочувствуете мощь этих компонентов.

В прошлой статье (msdn.microsoft.com/magazine/dn759437) я показал, как DirectComposition API можно использовать наряду с Direct2D, чтобы получить лучшее из двух миров — графики режима сохранения (retained mode) и прямого режима (immediate mode). Проект-пример, включенный в ту статью, иллюстрировал эту концепцию на простом приложении, которое позволяет создавать окружности, двигать их по окну и довольно легко управлять их Z-порядком. В этой статье я хочу показать, насколько легко добавить некоторые впечатляющие эффекты с помощью преобразований и анимации. Прежде всего вы должны осознать, что DirectComposition предоставляет перегруженные версии многих скалярных свойств. Вероятно, вы заметили это, если следовали за мной в течение последних нескольких месяцев, когда я исследовал этот API. Например, смещение визуала можно задать с помощью методов SetOffsetX и SetOffsetY:

ComPtr<IDCompositionVisual2> visual = ...
HR(visual->SetOffsetX(10.0f));
HR(visual->SetOffsetY(20.0f));

Но интерфейс IDCompositionVisual, от которого наследует IDCompositionVisual2, также предоставляет перегрузки этих методов, которые принимают объект анимации, а не значение с плавающей точкой. Этот объект анимации материализуется как интерфейс IDCompositionAnimation. Например, я мог бы установить смещение визуала с помощью одного или двух объектов анимации в зависимости от того, нужна мне анимация по одной оси или по обеим:

ComPtr<IDCompositionAnimation> animateX = ...
ComPtr<IDCompositionAnimation> animateY = ...
HR(visual->SetOffsetX(animateX.Get()));
HR(visual->SetOffsetY(animateY.Get()));

Но механизм композиции обладает куда большими возможностями в анимации. Визуалы также поддерживают двух- и трехмерные преобразования. Метод SetTransform визуала можно использовать для применения двухмерного преобразования при заданном скалярном значении:

D2D1_MATRIX_3X2_F matrix = ...
HR(visual->SetTransform(matrix));

Здесь DirectComposition на самом деле опирается на матрицу 3×2, определенную в Direct2D API. С ее помощью вы можете выполнять разнообразные операции вроде вращения, трансляции, масштабирования и наклона визуала. Это влияет на координатное пространство, на которое проецируется контент визуала, но ограничено двухмерной графикой с осями X и Y.

Естественно, интерфейс IDCompositionVisual предоставляет перегруженную версию метода SetTransform, но она не принимает объект анимации напрямую. Видите ли, объект анимации отвечает за анимацию только одного значения в течение некоего времени. Матрица, по определению, состоит из набора значений. Возможно, вы захотите анимировать какое-то количество ее членов в зависимости от нужного вам эффекта. Поэтому перегруженная версия SetTransform принимает объект transform:

ComPtr<IDCompositionTransform> transform = ...
HR(visual->SetTransform(transform.Get()));

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

ComPtr<IDCompositionRotateTransform> transform = ...
HR(transform->SetCenterX(width / 2.0f));
HR(transform->SetCenterY(height / 2.0f));
HR(transform->SetAngle(animation.Get()));
HR(visual->SetTransform(transform.Get()));

Интерфейс IDCompositionRotateTransform наследует от IDCompositionTransform и представляет двухмерное преобразование, которое влияет на поворот визуала вокруг оси Z. Здесь я присваиваю центральной точке фиксированное значение, исходя из неких значений ширины и высоты, и использую объект анимации для управления углом.

Таков базовый шаблон. Я описал лишь двухмерные преобразования, но трехмерные преобразования во многом работают так же. Теперь позвольте мне проиллюстрировать более практический пример, показав, как применить в проекте из моей прошлой статьи преобразование и анимацию разнообразными способами с помощью Direct2D и Windows Animation Manager.

Иллюстрация преобразований и анимации в печатном издании невозможна без выхода за границы того, что можно легко ухватить, глядя на статическую страницу. Если вы хотите увидеть их в действии, просмотрите мой онлайновый учебный курс, где можно увидеть, как все это происходит (bit.ly/WhKQZT). Чтобы сделать концепции немного понятнее в печатном виде, я решил заменить окружности в своем проекте из прошлой статьи на квадраты. Это должно несколько прояснить различные преобразования на бумаге. Сначала я заменю переменную-член m_geometry в SampleWindow геометрией прямоугольника:

ComPtr<ID2D1RectangleGeometry> m_geometry;

Затем в методе SampleWindow CreateFactoryAndGeometry я буду получать Direct2D-фабрику, чтобы создавать геометрию прямоугольника вместо геометрии эллипса:

D2D1_RECT_F const rectangle =
  RectF(0.0f, 0.0f, 100.0f, 100.0f);
HR(m_factory->CreateRectangleGeometry(
  rectangle,
  m_geometry.GetAddressOf()));

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

*
Рис. 1. Иллюстрация преобразований и анимации с помощью квадратов вместо окружностей

Далее я намерен добавить простой обработчик для реагирования на сообщение WM_KEYDOWN. В метод MessageHandler из SampleWindow я включаю для этого выражение if:

else if (WM_KEYDOWN == message)
{
  KeyDownHandler(wparam);
}

Как обычно, в нем понадобится обработка, необходимая для восстановления после потери устройства. На рис. 2 представлен стандартный шаблон освобождения ресурсов устройства и объявления окна недействительным, чтобы обработчик сообщений WM_PAINT мог перестроить стек устройства. Кроме того, я ограничиваю обработчик клавишей Enter, чтобы избежать путаницы с клавишей Ctrl, используемой при добавлении фигур.

Рис. 2. Обвязка для восстановления после потери устройства

void KeyDownHandler(WPARAM const wparam)
{
  try
  {
    if (wparam != VK_RETURN)
    {
      return;
    }
    // Выполняем работу!
  }
  catch (ComException const & e)
  {
    TRACE(L"KeyDownHandler failed 0x%X\n",
      e.result);
    ReleaseDeviceResources();
    VERIFY(InvalidateRect(m_window,
                          nullptr,
                          false));
  }
}

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

RECT bounds {};
VERIFY(GetClientRect(m_window, &bounds));

Затем выводим центральную точку клиентской области окна:

D2D1_POINT_2F center
{
  bounds.right / 2.0f,
  bounds.bottom / 2.0f
};

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

D2D1_MATRIX_3X2_F const matrix =
  Matrix3x2F::Rotation(30.0f, center);

А затем просто задаем свойство transform визуала и передаем изменение в дерево визуальных объектов. Для упрощения я применю это изменение к корневому визуалу:

HR(m_rootVisual->SetTransform(matrix));
HR(m_device->Commit());

Конечно, вы можете применять любое количество изменений к любому числу визуалов, и механизм композиции сам позаботится о координации этих изменений. Результаты этого простого двухмерного преобразования показаны на рис. 3. Вероятно, вы заметили некоторую ступенчатость (aliasing). Хотя Direct2D по умолчанию осуществляет сглаживание (anti-aliasing), он предполагает, что картинка появится в координатном пространстве, в котором выполнялся ее рендеринг. Механизм композиции ничего не знает о геометрии, с использованием которой был выполнен рендеринг поверхности композиции, поэтому у него нет возможности поправить это. В любом случае, как только в смесь добавляется анимация, ступенчатость будет видна на крайне малое время и, следовательно, почти незаметна.

*
Рис. 3. Простое двухмерное преобразование

Чтобы добавить анимацию к этому преобразованию, нужно исключить структуру матрицы для преобразования композиции. Я заменю структуры D2D1_POINT_2F и D2D1_MATRIX_3X2_F одним преобразованием поворота. Сначала нужно создать это преобразование, используя устройство композиции:

ComPtr<IDCompositionRotateTransform> transform;
HR(m_device->CreateRotateTransform(transform.GetAddressOf()));

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

HR(transform->SetCenterX(bounds.right / 2.0f));
HR(transform->SetCenterY(bounds.bottom / 2.0f));
HR(transform->SetAngle(30.0f));

И компилятор выбирает подходящую перегруженную версию для обработки преобразования композиции:

HR(m_rootVisual->SetTransform(transform.Get()));

Вы полнение этого кода даст тот же эффект, что и на рис. 3, так как я еще не добавил анимацию. Создать объект animation достаточно просто:

ComPtr<IDCompositionAnimation> animation;
HR(m_device->CreateAnimation(animation.GetAddressOf()));

Затем я могу использовать этот объект animation вместо константного значения при задании угла:

HR(transform->SetAngle(animation.Get()));

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

float duration = 1.0f;
HR(animation->AddCubic(0.0,
                       0.0f,
                       360.0f / длительность,
                       0.0f,
                       0.0f));

Третий параметр метода AddCubic указывает коэффициент линейного увеличения, так что это имеет смысл. Если бы я оставил все в таком виде, визуал вечно вращался бы на 360 градусов. Анимацию можно остановить, как только угол станет равным 360 градусам:

HR(animation->End(duration, 360.0f));

Первый параметр метода End указывает смещение от начала анимации независимо от значения функции анимации. Второй параметр — это конечное значение анимации. Учитывайте это, так как анимация будет «перескакивать» к этому значению при неправильно выбранном шаге, что приведет к визуальному эффекту рывка.

В таких линейных анимациях достаточно легко разобраться, но более сложные анимации могут оказаться чрезмерно запутанными. Здесь и вступает в игру Windows Animation Manager. Вместо того, чтобы вызывать различные методы IDCompositionAnimation, чтобы добавить синусоидальные сегменты, кубический полином, повторяющиеся сегменты и т. д., можно сконструировать раскадровку анимации с помощью Windows Animation Manager и его богатой библиотеки переходов (transitions). После этого полученная переменная анимации используется для заполнения анимации композиции. Это требует написания несколько большего количества кода, но преимущество — гораздо большие возможности и контроль над анимацией в приложении. Для начала я должен создать сам диспетчер анимации:

ComPtr<IUIAnimationManager2> manager;
HR(CoCreateInstance(__uuidof(UIAnimationManager2),
  nullptr, CLSCTX_INPROC, __uuidof(manager),
  reinterpret_cast<void **>(manager.GetAddressOf())));

Да, Windows Animation Manager полагается на COM-активацию, поэтому не забудьте вызвать CoInitializeEx или RoInitialize для инициализации исполняющей среды. Кроме того, нужно создать библиотеку переходов:

ComPtr<IUIAnimationTransitionLibrary2> library;
HR(CoCreateInstance(__uuidof(UIAnimationTransitionLibrary2),
  nullptr, CLSCTX_INPROC, __uuidof(library),
  reinterpret_cast<void **>(library.GetAddressOf())));

Как правило, приложения будут сохранять эти два объекта в течение всего жизненного цикла, поскольку они нужны для непрерывной анимации и особенно для синхронизации скорости (velocity matching). Затем я должен создать раскадровку анимации (animation storyboard):

ComPtr<IUIAnimationStoryboard2> storyboard;
HR(manager->CreateStoryboard(storyboard.GetAddressOf()));

Раскадровка — то, что связывает переходы с переменными анимации и определяет их относительные расписания в течение некоего времени. Раскадровка способна агрегировать различные переходы, применяемые к разным переменным анимации; она гарантирует их синхронизацию и планируется как единое целое. (Конечно, вы можете создавать несколько раскадровок для планирования независимых анимаций.) Теперь мне нужно попросить диспетчер анимации создать переменную анимации:

ComPtr<IUIAnimationVariable2> variable;
  HR(manager->CreateAnimationVariable(
    0.0, // начальное значение
    variable.GetAddressOf()));

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

ComPtr<IUIAnimationTransition2> transition;
HR(library->CreateAccelerateDecelerateTransition(
  1.0,   // длительность
  360.0, // конечное значение
  0.7,   // коэффициент ускорения
  0.3,   // коэффициент замедления
  transition.GetAddressOf()));

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

HR(storyboard->AddTransition(variable.Get(),
                             transition.Get()));

Теперь раскадровка готова к планированию:

HR(storyboard->Schedule(0.0));

Единственный параметр метода Schedule сообщает планировщику текущее время анимации. Это больше полезно для координации анимации и ее синхронизации с частотой обновления, используемой механизмом композиции, но пригодится и сейчас. К этому моменту переменная анимации «заряжена», и я могу попросить ее заполнить кривыми анимацию композиции:

HR(variable->GetCurve(animation.Get()));

В данном случае это равноценно тому, как если бы я вызвал такую анимацию композиции:

HR(animation->AddCubic(0.0, 0.0f, 0.0f, 514.2f, 0.0f));
HR(animation->AddCubic(0.7, 252.0f, 720.0f, -1200.0f, 0.0f));
HR(animation->End(1.0, 360.0f));

Здесь определенно требуется намного меньше кода, чем при работе с Windows Animation Manager, но разобраться во всей этой математике совсем не просто. Windows Animation Manager также позволяет координировать и плавно выполнять переходы между анимациями, что крайне трудно сделать вручную.


Ссылка: http://www.oszone.net/28033/