Всем здравствуйте! Появилось время, и сразу пишу вам. Сегодня поговорим об анимации.
Важно понимать, что Direct2D — это низкоуровневое API, и готовых инструментов в нём нет. Однако существуют вспомогательные компоненты: Windows Animation Manager (WAM) и DirectComposition. Первый появился в Windows 7, второй — в Windows 8.
Windows Animation Manager создан для того, чтобы избавить разработчика от математических расчётов анимации: линейной интерполяции, кривых ускорения/замедления, эффекта пружины и т. д. Вы просто задаёте системе начальные и конечные координаты и эффект, а WAM самостоятельно вычисляет.
Ключевые понятия WAM:
IUIAnimationManager — главный управляющий объект. Он создаёт анимации, отслеживает их выполнение и уведомляет о необходимости обновления кадра.
IUIAnimationVariable — анимируемая переменная. Это может быть координата X, прозрачность, масштаб или любое другое число с плавающей запятой. Вы просто устанавливаете текущее значение переменной, например: «эта переменная сейчас равна 100».
IUIAnimationTransition — закон изменения переменной во времени. WAM предоставляет богатую библиотеку встроенных переходов: линейный (CreateLinearTransition) , с ускорением и замедлением (CreateAccelerateDecelerateTransition), «пружина» (CreateSpringTransition) и многие другие(IUIAnimationTransitionLibrary и IUIAnimationTransitionLibrary2).
IUIAnimationStoryboard — «раскадровка», контейнер, в который вы собираете один или несколько переходов и запускаете их одновременно или последовательно.
В качестве краткого примера (к 11-й статье вы уже умеете работать с графикой, и нет нужды каждый раз писать полотно текста) приведём фрагмент:
В классе окна или приложения объявим необходимые COM-указатели:
// WAMCComPtr<IUIAnimationManager> pAnimManager;CComPtr<IUIAnimationTransitionLibrary> pTransitionLibrary;CComPtr<IUIAnimationVariable> pAnimVarX;CComPtr<IUIAnimationStoryboard> pStoryboard;// Direct2D (упрощённо)CComPtr<ID2D1Factory> pD2DFactory;CComPtr<ID2D1HwndRenderTarget> pRenderTarget;CComPtr<ID2D1SolidColorBrush> pBrush;
При старте приложения (например, в обработчике WM_CREATE) инициализируется COM и создаются необходимые объекты.
// Инициализация COM (если ещё не сделано)CoInitialize(NULL);// Создаём менеджер анимацийCoCreateInstance(CLSID_UIAnimationManager, NULL, CLSCTX_INPROC_SERVER, IID_IUIAnimationManager, (void**)&pAnimManager);// Создаём библиотеку переходовCoCreateInstance(CLSID_UIAnimationTransitionLibrary, NULL, CLSCTX_INPROC_SERVER, IID_IUIAnimationTransitionLibrary, (void**)&pTransitionLibrary);// Создаём переменную для координаты X. Начальное значение = 0pAnimManager->CreateAnimationVariable(0.0, &pAnimVarX);
Ниже приведены пояснения к аргументам используемых функций.
CoInitialize:
Первый аргумент зарезервирован и должен принимать значение NULL.
CoCreateInstance:
Первый аргумент — идентификатор класса (CLSID).
Второй аргумент — указатель на управляющий IUnknown при агрегации объектов; в нашем случае агрегация не используется, поэтому передаётся NULL.
Третий аргумент — контекст выполнения. Значение CLSCTX_INPROC_SERVER указывает, что объект должен быть загружен как DLL внутри текущего процесса.
Четвёртый аргумент — указатель на требуемый интерфейс (IID).
Пятый аргумент — адрес переменной, в которую будет передан указатель на созданный объект.
CreateAnimationVariable:
Первый аргумент — начальное значение переменной (тип DOUBLE).
Второй аргумент — адрес указателя на объект IUIAnimationVariable, который будет создан.
Теперь предположим, что мы хотим анимировать перемещение прямоугольника из позиции X = 0 в X = 300 за 2 секунды с эффектом ускорения и замедления.
// Создаём переход: ускорение-замедление, длительность 2 сек, конечное значение 300CComPtr<IUIAnimationTransition> pTransition;pTransitionLibrary->CreateAccelerateDecelerateTransition( 2.0, // длительность в секундах 300.0, // конечное значение 0.3, // доля ускорения (0..1) 0.3, // доля замедления (0..1) &pTransition);// Создаём раскадровкуpAnimManager->CreateStoryboard(&pStoryboard);// Добавляем переход к переменной XpStoryboard->AddTransition(pAnimVarX, pTransition);// Запускаем раскадровку (сразу)pStoryboard->Schedule(0); // 0 = запустить немедленно
Пояснение аргументов методов:
CreateAccelerateDecelerateTransition — уже описан в комментариях выше (параметры: ускорение, замедление и продолжительность).
CreateStoryboard (метод IUIAnimationManager):
Единственный аргумент — адрес указателя на создаваемую раскадровку (IUIAnimationStoryboard*). Раскадровка представляет собой контейнер, который может содержать один или несколько переходов, применяемых к разным переменным. Все переходы внутри одной раскадровки запускаются одновременно (если не заданы индивидуальные задержки).
AddTransition:
Первый аргумент — указатель на анимируемую переменную (IUIAnimationVariable*).
Второй аргумент — указатель на ранее созданный переход (IUIAnimationTransition*). Именно этот переход определяет закон изменения переменной во времени.
Schedule:
Первый аргумент — момент времени (в секундах) относительно текущего времени системы, с которого должна запуститься раскадровка. Обычно передаётся 0.0 для немедленного старта.
После вызова Schedule анимация начинает выполняться. WAM автоматически вычисляет промежуточные значения переменной в зависимости от текущего времени.
В каждом кадре (например, в обработчике WM_PAINT или в отдельном потоке рендеринга) необходимо выполнить следующие шаги:
Обновить состояние WAM, передав прошедшее время (с помощью метода Update менеджера).
Получить текущее значение анимируемой переменной (метод GetValue).
void RenderFrame(){ // 1. Получить время, прошедшее с прошлого кадра (в секундах) static double lastTime = 0.0; double currentTime = GetCurrentTimeInSeconds(); // ваша функция получения времени double deltaTime = currentTime - lastTime; lastTime = currentTime; // 2. Обновить WAM. Передаём дельту времени (в секундах) HRESULT hr = pAnimManager->Update(deltaTime); // Если анимация завершилась, можно обработать и, например, запустить обратную // 3. Получить текущее значение X double currentX; pAnimVarX->GetValue(¤tX); // 4. Начать рисование через Direct2D pRenderTarget->BeginDraw(); pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White)); // Рисуем прямоугольник размером 50x50 в точке (currentX, 50) D2D1_RECT_F rect = D2D1::RectF((FLOAT)currentX, 50.0f, (FLOAT)(currentX + 50), 100.0f); pRenderTarget->FillRectangle(&rect, pBrush); // Завершаем рисование hr = pRenderTarget->EndDraw(); if (hr == D2DERR_RECREATE_TARGET) { /* восстановить ресурсы */ }}
Чтобы организовать движение прямоугольника туда-обратно, можно подписаться на событие завершения раскадровки. Однако для простоты реализуем циклическую проверку: в каждом кадре или в отдельном цикле отслеживаем состояние анимации, и если она завершена, запускаем новую раскадровку с обратным направлением движения.
// После pAnimManager->Update(deltaTime) можно проверить статус:UI_ANIMATION_MANAGER_STATUS status;pAnimManager->GetStatus(&status);if (status == UI_ANIMATION_MANAGER_IDLE){ // Анимация завершена - запускаем новую в обратном направлении // Получаем текущее значение X (например, 300) double currentX; pAnimVarX->GetValue(¤tX); // Создаём переход к 0 (или в зависимости от текущего положения) CComPtr<IUIAnimationTransition> pReverseTransition; pTransitionLibrary->CreateAccelerateDecelerateTransition( 2.0, 0.0, 0.3, 0.3, &pReverseTransition ); pAnimManager->CreateStoryboard(&pStoryboard); pStoryboard->AddTransition(pAnimVarX, pReverseTransition); pStoryboard->Schedule(0);}
Пояснение аргументов:
GetStatus принимает один аргумент — выходной указатель, в который записывается текущее состояние менеджера. Возможные значения:
UI_ANIMATION_MANAGER_IDLE — все анимации завершены, менеджер находится в состоянии ожидания;
UI_ANIMATION_MANAGER_BUSY — выполняется как минимум одна раскадровка;
UI_ANIMATION_MANAGER_INSUFFICIENT_PRIORITY — встречается крайне редко и сигнализирует о том, что запланированные анимации не могут быть выполнены из-за конфликтов приоритетов.
Обратите внимание: в вызов Update необходимо передавать актуальное время в секундах. Для его измерения с высокой точностью используйте функцию QueryPerformanceCounter.
double GetCurrentTimeInSeconds(){ static LARGE_INTEGER frequency = {0}; if (frequency.QuadPart == 0) QueryPerformanceFrequency(&frequency); LARGE_INTEGER now; QueryPerformanceCounter(&now); return (double)now.QuadPart / (double)frequency.QuadPart;}
При завершении приложения необходимо освободить все COM-объекты (при использовании умных указателей освобождение произойдёт автоматически при выходе за область видимости). Windows Animation Manager не требует явного вызова завершения или освобождения ресурсов.
DirectComposition. Теперь — о нём.
Если WAM работает на центральном процессоре и лишь вычисляет промежуточные значения, то DirectComposition (доступный начиная с Windows 8) функционирует на стороне видеокарты в отдельном потоке. Он принимает готовые растровые изображения (битмапы), применяет к ним трансформации, эффекты и анимации, после чего выводит результирующую сцену на экран с высокой частотой кадров.
Ключевые понятия DirectComposition:
Визуальное дерево (Visual Tree) — иерархия объектов IDCompositionVisual. Корневой визуальный элемент привязан к окну, дочерние позиционируются относительно родителя. Это позволяет легко строить сложные сцены: например, персонаж представляет собой родительский элемент, а его руки и ноги — дочерние визуалы.
Свойства визуала — положение (OffsetX, OffsetY), трансформации (поворот, масштаб), прозрачность и эффекты.
IDCompositionAnimation — анимационная кривая, которую можно применить к любому свойству. Она задаётся в виде сегментов: линейных, кубических, синусоидальных или повторяющихся.
Commit — после настройки дерева и анимаций вызывается метод Commit, после чего все изменения применяются.
Перейдём к примеру.
// Direct3D 11 (нужен для DirectComposition)CComPtr<ID3D11Device> m_pD3D11Device;// DirectCompositionCComPtr<IDCompositionDevice> m_pDCompDevice;CComPtr<IDCompositionTarget> m_pDCompTarget;CComPtr<IDCompositionVisual> m_pVisual;// Direct2D (для создания контента)CComPtr<ID2D1Factory> m_pD2DFactory;CComPtr<ID2D1Device> m_pD2DDevice;CComPtr<ID2D1DeviceContext> m_pD2DContext;CComPtr<ID2D1Bitmap1> m_pD2DBitmap;CComPtr<ID2D1SolidColorBrush> m_pBrush;// Объекты анимацииCComPtr<IDCompositionAnimation> m_pAnimX;CComPtr<IDCompositionAnimation> m_pAnimOpacity;
Инициализация. Данный шаг выполняется однократно при запуске приложения (например, в обработчике сообщения WM_CREATE).
HRESULT Init(HWND hwnd){ HRESULT hr = S_OK; // 1. Создаём устройство Direct3D 11 с поддержкой BGRA (нужно для D2D) D3D_FEATURE_LEVEL featureLevel; hr = D3D11CreateDevice( nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, D3D11_CREATE_DEVICE_BGRA_SUPPORT, // Важно для Direct2D! nullptr, 0, D3D11_SDK_VERSION, &m_pD3D11Device, &featureLevel, nullptr ); // 2. Получаем интерфейс IDXGIDevice от устройства D3D CComPtr<IDXGIDevice> pDXGIDevice; if (SUCCEEDED(hr)) hr = m_pD3D11Device->QueryInterface(&pDXGIDevice); // 3. Создаём устройство DirectComposition[reference:1] if (SUCCEEDED(hr)) hr = DCompositionCreateDevice(pDXGIDevice, __uuidof(IDCompositionDevice), reinterpret_cast<void**>(&m_pDCompDevice)); // 4. Создаём цель композиции для нашего окна[reference:2] if (SUCCEEDED(hr)) hr = m_pDCompDevice->CreateTargetForHwnd(hwnd, TRUE, &m_pDCompTarget); // 5. Создаём фабрику Direct2D if (SUCCEEDED(hr)) hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pD2DFactory); // 6. Создаём устройство Direct2D из устройства DXGI if (SUCCEEDED(hr)) hr = m_pD2DFactory->CreateDevice(pDXGIDevice, &m_pD2DDevice); // 7. Создаём контекст устройства Direct2D if (SUCCEEDED(hr)) hr = m_pD2DDevice->CreateDeviceContext( D2D1_DEVICE_CONTEXT_OPTIONS_NONE, &m_pD2DContext ); // 8. Создаём битмап с контентом (квадрат 200x200) if (SUCCEEDED(hr)) hr = CreateBitmapContent(); // 9. Создаём визуал и устанавливаем ему контент[reference:3] if (SUCCEEDED(hr)) hr = m_pDCompDevice->CreateVisual(&m_pVisual); if (SUCCEEDED(hr)) hr = m_pVisual->SetContent(m_pD2DBitmap); // 10. Добавляем визуал в корень дерева if (SUCCEEDED(hr)) hr = m_pDCompTarget->SetRoot(m_pVisual); // 11. Создаём анимации if (SUCCEEDED(hr)) hr = CreateAnimations(); // 12. Применяем анимации к визуалу if (SUCCEEDED(hr)) { // Анимация положения по X: перемещаемся от 0 до 500 hr = m_pVisual->SetOffsetX(m_pAnimX); } if (SUCCEEDED(hr)) { // Анимация прозрачности: применяем эффект с анимацией CComPtr<IDCompositionEffect> pEffect; hr = m_pDCompDevice->CreateEffect(&pEffect); if (SUCCEEDED(hr)) { // Устанавливаем анимацию для свойства Opacity эффекта hr = pEffect->SetOpacity(m_pAnimOpacity); if (SUCCEEDED(hr)) hr = m_pVisual->SetEffect(pEffect); } } // 13. Фиксируем все изменения — анимация запускается![reference:4] if (SUCCEEDED(hr)) hr = m_pDCompDevice->Commit(); return hr;}
Создание контента (квадрата). Функция, выполняющая отрисовку квадрата в битмапе Direct2D:
HRESULT CreateBitmapContent(){ HRESULT hr = S_OK; // Размер битмапа D2D1_SIZE_U size = D2D1::SizeU(200, 200); // Свойства битмапа D2D1_BITMAP_PROPERTIES1 props = D2D1::BitmapProperties1( D2D1_BITMAP_OPTIONS_TARGET, // Можно использовать как цель D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, // Формат пикселей D2D1_ALPHA_MODE_PREMULTIPLIED), 96.0f, 96.0f // DPI ); // Создаём битмап в контексте D2D hr = m_pD2DContext->CreateBitmap(size, nullptr, 0, props, &m_pD2DBitmap); // Рисуем на битмапе if (SUCCEEDED(hr)) { // Устанавливаем битмап как цель рисования m_pD2DContext->SetTarget(m_pD2DBitmap); m_pD2DContext->BeginDraw(); m_pD2DContext->Clear(D2D1::ColorF(D2D1::ColorF::CornflowerBlue)); // Создаём кисть и рисуем квадрат с обводкой m_pD2DContext->CreateSolidColorBrush( D2D1::ColorF(D2D1::ColorF::OrangeRed), &m_pBrush ); D2D1_RECT_F rect = D2D1::RectF(20.0f, 20.0f, 180.0f, 180.0f); m_pD2DContext->FillRectangle(rect, m_pBrush); m_pD2DContext->DrawRectangle(rect, m_pBrush, 5.0f); hr = m_pD2DContext->EndDraw(); m_pD2DContext->SetTarget(nullptr); } return hr;}
Создание анимаций. Здесь мы определяем, как именно будут меняться свойства во времени. Вместо использования WAM мы создаём анимационные кривые вручную.
HRESULT CreateAnimations(){ HRESULT hr = S_OK; // --- Анимация движения по X (от 0 до 500 и обратно) --- hr = m_pDCompDevice->CreateAnimation(&m_pAnimX); if (SUCCEEDED(hr)) { // Начинаем с 0 m_pAnimX->SetAbsoluteBeginTime(0); m_pAnimX->SetKeyframes(nullptr, 0, nullptr); // Первый сегмент: за 3 секунды перемещаемся к 500 с замедлением в конце m_pAnimX->AddCubic( 0.0f, // Начальное смещение по времени 0.0f, // Начальное значение 3.0f, // Длительность (сек) 500.0f, // Конечное значение 0.0f, 0.0f, // Контрольные точки (0 = линейная) 1.0f, 1.0f ); // Второй сегмент: за 3 секунды возвращаемся к 0 m_pAnimX->AddCubic( 0.0f, 500.0f, 3.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f ); // Указываем, что анимация должна повторяться бесконечно m_pAnimX->SetRepeatCount(-1); } // --- Анимация прозрачности (от 0.2 до 1.0) --- hr = m_pDCompDevice->CreateAnimation(&m_pAnimOpacity); if (SUCCEEDED(hr)) { m_pAnimOpacity->SetAbsoluteBeginTime(0); // Плавно увеличиваем прозрачность от 0.2 до 1.0 за 2 секунды m_pAnimOpacity->AddCubic( 0.0f, 0.2f, 2.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f ); // Плавно уменьшаем обратно до 0.2 за 2 секунды m_pAnimOpacity->AddCubic( 0.0f, 1.0f, 2.0f, 0.2f, 0.0f, 0.0f, 1.0f, 1.0f ); // Бесконечное повторение m_pAnimOpacity->SetRepeatCount(-1); } return hr;}
Пояснение аргументов методов:
CreateAnimation — единственный аргумент — выходной указатель, в который записывается адрес созданного объекта анимации (IDCompositionAnimation*).
SetAbsoluteBeginTime — устанавливает абсолютное время начала анимации относительно глобальной временной шкалы DirectComposition. Передача 0 означает, что анимация начинается с момента применения к свойству (то есть с момента вызова Commit).
SetKeyframes
Этот метод предназначен для задания ключевых кадров (более сложный способ построения анимации). В приведённом коде переданы значения nullptr, 0, nullptr — это фактически сбрасывает предыдущие ключевые кадры (если они были). Аргументы метода:
первый — массив элементов
второй — количество элементов
третий — отдельный ключевой кадр (в данном случае не используется).
AddCubic (добавление кубического сегмента)
Первый сегмент (движение по оси X от 0 до 500 за 3 секунды):
аргумент 1 — смещение внутри анимации, с которого начинается сегмент (0.0f);
аргумент 2 — значение, соответствующее началу сегмента (0.0f);
аргумент 3 — длительность сегмента в секундах (3.0f);
аргумент 4 — конечное значение сегмента (координата X станет равной 500.0f);
аргументы 5-6 — координаты первой контрольной точки кубической кривой Безье;
аргументы 7-8 — координаты второй контрольной точки.
Второй сегмент (движение от 500 обратно к 0 за 3 секунды):
beginValue = 500.0f — старт с 500;
endValue = 0.0f — возврат к 0;
длительность — снова 3 секунды;
контрольные точки такие же (0,0 и 1,1), что обеспечивает плавный старт и финиш.
SetRepeatCount
Задаёт количество повторений анимации после её первого выполнения. Значение -1 (передаётся как UINT, но в коде используется литерал -1, который интерпретируется как 0xFFFFFFFF) означает бесконечное повторение.
Анимация прозрачности (второй блок) создаётся аналогично — через m_pAnimOpacity.
Важно отметить: после вызова Commit() анимация начинает выполняться автоматически, без какого-либо участия приложения. Вам не нужно организовывать цикл обновления, вызывать BeginDraw/EndDraw в каждом кадре или синхронизироваться с WAM. DirectComposition обрабатывает анимацию в отдельном потоке на видеокарте.
Пока анимация работает, ваше приложение может выполнять любые другие задачи: обрабатывать пользовательский ввод, загружать данные или даже находиться в режиме ожидания — частота кадров при этом сохраняется постоянной.
На этом всё. По сути, вы можете создать свой аналог этих библиотек. Думаю, следующая статья будет посвящена эффектам и слоям, а потом будет создан движок игры, например, как в Stardew Valley, опираясь исключительно на свои же статьи, показав, что не так страшен чёрт, как его рисуют, и что даже такая низкоуровневая API, как Direct2D, вполне подходит инди-разработчикам.
Но, возможно, перед этим я рассмотрю XAudio2 (чтобы в игре был звук), а также сетевую часть (Winsock).
По сути, основная цель — создать игру на тех инструментах, которые рекомендует Microsoft.
При желании материально поддержать перевод и структурирование информации — средства можете отправить через сбор в ЮМани.
ссылка на оригинал статьи https://habr.com/ru/articles/1052652/