Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Win2D предоставляет несколько API для представления объектов, которые можно нарисовать, которые делятся на две категории: изображения и эффекты. Изображения, представленные ICanvasImage интерфейсом, не имеют входных данных и могут быть непосредственно нарисованы на данной поверхности. Например, CanvasBitmapVirtualizedCanvasBitmap и CanvasRenderTarget являются примерами типов изображений. С другой стороны, эффекты представлены интерфейсом ICanvasEffect . Они могут иметь входные данные, а также дополнительные ресурсы, и могут применять произвольную логику для создания выходных данных, поскольку эффект также представляет собой изображение. Win2D включает эффекты упаковки большинства эффектов D2D, таких как GaussianBlurEffect, TintEffect и LuminanceToAlphaEffect.
Изображения и эффекты также можно объединить, чтобы создать произвольные графы, которые затем можно отобразить в приложении (также см. документы D2D в эффектах Direct2D). Вместе они обеспечивают чрезвычайно гибкую систему для создания сложной графики эффективным образом. Однако существуют случаи, когда встроенные эффекты недостаточно, и вы можете создать собственный эффект Win2D. Для поддержки этого Win2D включает набор мощных API взаимодействия, который позволяет определять пользовательские изображения и эффекты, которые могут легко интегрироваться с Win2D.
Совет
Если вы используете C# и хотите реализовать настраиваемый график эффектов или эффектов, рекомендуется использовать ComputeSharp , а не пытаться реализовать эффект с нуля. В приведенном ниже абзаце подробно описано, как использовать эту библиотеку для реализации пользовательских эффектов, которые легко интегрируются с Win2D.
API платформы:
ICanvasImage,CanvasBitmapVirtualizedCanvasBitmapCanvasRenderTargetCanvasEffectGaussianBlurEffectTintEffectICanvasLuminanceToAlphaEffectImageIGraphicsEffectSource, ,ID2D21ImageID2D1Factory1ID2D1Effect
Реализация пользовательского ICanvasImage
Самый простой поддерживаемый сценарий — создание пользовательского ICanvasImage. Как уже упоминалось, это интерфейс WinRT, определенный Win2D, который представляет все виды изображений, с которыми может взаимодействовать Win2D. Этот интерфейс предоставляет только два GetBounds метода и расширяет IGraphicsEffectSourceинтерфейс маркера, представляющий "некоторый источник эффекта".
Как вы видите, в этом интерфейсе отсутствуют "функциональные" API для реального выполнения любого рисования. Чтобы реализовать собственный ICanvasImage объект, необходимо также реализовать ICanvasImageInterop интерфейс, который предоставляет всю необходимую логику для Win2D для рисования изображения. Это COM-интерфейс, определенный в общедоступном Microsoft.Graphics.Canvas.native.h заголовке, который поставляется с Win2D.
Интерфейс определяется следующим образом:
[uuid("E042D1F7-F9AD-4479-A713-67627EA31863")]
class ICanvasImageInterop : IUnknown
{
HRESULT GetDevice(
ICanvasDevice** device,
WIN2D_GET_DEVICE_ASSOCIATION_TYPE* type);
HRESULT GetD2DImage(
ICanvasDevice* device,
ID2D1DeviceContext* deviceContext,
WIN2D_GET_D2D_IMAGE_FLAGS flags,
float targetDpi,
float* realizeDpi,
ID2D1Image** ppImage);
}
Кроме того, он зависит от этих двух типов перечисления из одного заголовка:
enum WIN2D_GET_DEVICE_ASSOCIATION_TYPE
{
WIN2D_GET_DEVICE_ASSOCIATION_TYPE_UNSPECIFIED,
WIN2D_GET_DEVICE_ASSOCIATION_TYPE_REALIZATION_DEVICE,
WIN2D_GET_DEVICE_ASSOCIATION_TYPE_CREATION_DEVICE
}
enum WIN2D_GET_D2D_IMAGE_FLAGS
{
WIN2D_GET_D2D_IMAGE_FLAGS_NONE,
WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT,
WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION,
WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION,
WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION,
WIN2D_GET_D2D_IMAGE_FLAGS_ALLOW_NULL_EFFECT_INPUTS,
WIN2D_GET_D2D_IMAGE_FLAGS_UNREALIZE_ON_FAILURE
}
Два метода GetDevice и GetD2DImage — это всё, что нужно для реализации пользовательских изображений (или эффектов), так как они предоставляют Win2D точки расширения для инициализации их на заданном устройстве и получения базового изображения D2D для отрисовки. Реализация этих методов очень важна для правильной работы в всех поддерживаемых сценариях.
Давайте рассмотрим их, чтобы узнать, как работает каждый метод.
Реализация GetDevice
Метод GetDevice является самым простым из двух. Это действие извлекает устройство холста, связанное с эффектом, чтобы Win2D смог проверить устройство при необходимости (например, чтобы убедиться, что оно соответствует устройству, которое используется). Параметр type указывает тип сопоставления для возвращаемого устройства.
Существует два основных возможных случая:
- Если изображение является эффектом, оно должно поддерживать состояния "активировано" и "неактивировано" на различных устройствах. Это означает, что эффект создается в неинициализированном состоянии и затем может быть активирован, когда устройство подключается во время рисования. После этого эффект может продолжать использоваться с этим устройством или перенесен на другое. В этом случае эффект сначала сбросит свое внутреннее состояние, а затем снова активируется на новом устройстве. Это означает, что связанное устройство холста может изменяться со временем, и это также может быть
null. Из-за этогоtypeследует задать значениеWIN2D_GET_DEVICE_ASSOCIATION_TYPE_REALIZATION_DEVICE, и возвращаемое устройство должно быть установлено на текущее устройство реализации, если оно доступно. - Некоторые образы имеют одно "устройство владения", которое назначается во время создания и никогда не может изменяться. Например, это будет для изображения, представляющего текстуру, так как она выделяется на определенном устройстве и не может быть перемещена. Когда вызывается
GetDevice, оно должно возвращать устройство создания и установитьtypeвWIN2D_GET_DEVICE_ASSOCIATION_TYPE_CREATION_DEVICE. Обратите внимание, что при указании этого типа возвращаемое устройство не должно бытьnull.
Примечание.
Win2D может вызывать GetDevice во время рекурсивного обхода графа эффектов, что означает, что в стеке может одновременно быть несколько активных вызовов GetD2DImage. Из-за этого GetDevice не следует устанавливать блокировку на текущий образ, так как это может привести к взаимоблокировке. Скорее, он должен использовать блокировку повторного входа в неблокирующем режиме и возвращать ошибку, если захватить её не удалось. Это гарантирует, что тот же поток, вызывающий его рекурсивно, успешно его захватит, в то время как одновременные потоки, пытающиеся сделать то же самое, завершатся неудачей без сбоев.
Реализация GetD2DImage
GetD2DImage — это место, где происходит большая часть работы. Этот метод отвечает за получение объекта ID2D1Image, который Win2D может рисовать, реализуя текущий эффект при необходимости. Это также включает рекурсивный обход и реализацию графа эффектов для всех источников, если таковые имеются, а также инициализацию любого состояния, которое может потребоваться изображению (например, буферы констант и другие свойства, ресурсные текстуры и т. д.).
Точную реализацию этого метода сильно зависит от типа изображения, и она может сильно отличаться, но, как правило, для произвольного эффекта можно ожидать, что метод будет выполнять следующие действия:
- Проверьте, не был ли вызов рекурсивным на том же экземпляре, и завершите выполнение с ошибкой, если это так. Это необходимо для обнаружения циклов в графе эффектов (например, эффект
Aимеет эффектBв качестве источника и эффектBимеет эффектAв качестве источника). - Установите блокировку на изображение для защиты от параллельного доступа.
- Обработайте целевые DPI в соответствии с входными параметрами
- Проверьте, соответствует ли входное устройство используемому, если таковое имеется. Если он не соответствует, а текущий эффект поддерживает реализацию, отменить реализацию эффекта.
- Осознайте эффект на входное устройство. При необходимости эффект D2D можно зарегистрировать на объекте
ID2D1Factory1, полученном из входного устройства или контекста устройства. Кроме того, необходимо задать все необходимое состояние для создаваемого экземпляра эффекта D2D. - Рекурсивно пересекает все источники и привязывает их к эффекту D2D.
Что касается флагов ввода, существует несколько возможных случаев, которые пользовательские эффекты должны правильно обрабатывать, чтобы обеспечить совместимость со всеми другими эффектами Win2D. За исключением WIN2D_GET_D2D_IMAGE_FLAGS_NONE, флаги для обработки следующие:
-
WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT: в этом случаеdeviceгарантированно не будетnull. Эффект должен проверить, является ли целевой объект контекста устройстваID2D1CommandList, и, если да, добавлять флагWIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION. В противном случае он должен задатьtargetDpi(который также гарантированно не должен бытьnull) для DPIs, полученных из входного контекста. Затем он должен удалитьWIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXTиз флагов. -
WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATIONиWIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION: используется при установке источников эффектов (см. примечания ниже). -
WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION: если задано, пропускает процесс рекурсивной реализации источников эффекта и просто возвращает реализованный эффект без других изменений. -
WIN2D_GET_D2D_IMAGE_FLAGS_ALLOW_NULL_EFFECT_INPUTS: если установлено, источникам эффектов, которые реализуются, разрешается бытьnull, если пользователь еще не присвоил им существующий источник. -
WIN2D_GET_D2D_IMAGE_FLAGS_UNREALIZE_ON_FAILURE: если установлено и источник эффекта является недействительным, эффект должен быть отменен перед возникновением сбоя. То есть, если произошла ошибка при разрешении источников эффекта после реализации эффекта, эффект должен быть отменён перед возвратом ошибки вызывающему объекту.
В отношении флагов, связанных с DPI, эти элементы определяют, как задаются источники эффектов. Чтобы обеспечить совместимость с Win2D, эффекты должны автоматически добавлять эффекты компенсации DPI в входные данные при необходимости. Они могут контролировать, так ли это:
- Если
WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATIONзадан, эффект компенсации DPI необходим всякий раз, когда параметрinputDpiне является0. - В противном случае требуется компенсация DPI, если
inputDpiне0,WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATIONне задана, иWIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATIONзадана, или входной DPI и целевые значения DPI не совпадают.
Эта логика должна применяться всякий раз, когда источник реализуется и привязан к входным данным текущего эффекта. Обратите внимание, что если добавлен эффект компенсации DPI, то это должен быть входной набор для базового образа D2D. Но если пользователь пытается получить обёртку WinRT для данного источника, эффект должен учитывать, был ли включен эффект DPI, и вернуть обёртку для исходного объекта вместо этого. То есть эффекты компенсации DPI должны быть прозрачными для пользователей эффекта.
После завершения логики инициализации результирующий ID2D1Image результат (как и с объектами Win2D, эффект D2D также является изображением) должен быть готов к рисованию Win2D в целевом контексте, который еще не известен вызывающим объектом в настоящее время.
Примечание.
Правильная реализация этого метода (и ICanvasImageInterop в целом) очень сложна, и это предназначено только для выполнения расширенными пользователями, которые абсолютно нуждаются в дополнительной гибкости. Перед попыткой написания реализации ICanvasImageInterop рекомендуется твердое понимание D2D, Win2D, COM, WinRT и C++. Если пользовательский эффект Win2D также должен упаковать настраиваемый эффект D2D, необходимо также реализовать собственный ID2D1Effect объект (дополнительные сведения об этом см. в документации D2D по пользовательским эффектам). Эта документация не является исчерпывающим описанием всей необходимой логики (например, она не охватывает, как следует выполнять маршалинг и управление источниками эффектов на границе между D2D и Win2D), поэтому рекомендуется также использовать реализацию CanvasEffect в кодовой базе Win2D в качестве ориентировочной точки для пользовательского эффекта и изменять ее по мере необходимости.
Реализация GetBounds
Последний недостающий элемент для полной реализации пользовательского ICanvasImage эффекта — поддержка двух GetBounds перегрузок. Чтобы упростить эту задачу, Win2D предоставляет экспорт C, который можно использовать для использования существующей логики из Win2D на любом пользовательском образе. Экспорт выглядит следующим образом:
HRESULT GetBoundsForICanvasImageInterop(
ICanvasResourceCreator* resourceCreator,
ICanvasImageInterop* image,
Numerics::Matrix3x2 const* transform,
Rect* rect);
Пользовательские образы могут вызывать этот API, передавая себя в качестве параметра image, а затем просто возвращать результат вызывающим сторонам. Параметр transform может быть null, если преобразование недоступно.
Оптимизация доступа к контексту устройства
В deviceContext параметр ICanvasImageInterop::GetD2DImage иногда может быть в состоянии null, если контекст недоступен сразу перед вызовом. Это делается с целью, чтобы контекст создавался только лениво, когда он действительно необходим. То есть, если контекст доступен, Win2D передает его вызову GetD2DImage, а в противном случае вызывающие функции смогут получить его самостоятельно, если необходимо.
Создание контекста устройства является относительно затратным, поэтому, чтобы ускорить его получение, Win2D предоставляет API для доступа к внутреннему пулу контекстов устройств. Это позволяет пользовательским эффектам арендовать и возвращать контексты устройств холста, связанные с заданным устройством, эффективным образом.
Интерфейсы API аренды контекста устройства определяются следующим образом:
[uuid("A0928F38-F7D5-44DD-A5C9-E23D94734BBB")]
interface ID2D1DeviceContextLease : IUnknown
{
HRESULT GetD2DDeviceContext(ID2D1DeviceContext** deviceContext);
}
[uuid("454A82A1-F024-40DB-BD5B-8F527FD58AD0")]
interface ID2D1DeviceContextPool : IUnknown
{
HRESULT GetDeviceContextLease(ID2D1DeviceContextLease** lease);
}
Интерфейс ID2D1DeviceContextPool реализуется с помощью CanvasDeviceтипа Win2D, реализующего ICanvasDevice интерфейс. Чтобы использовать этот пул, используйте интерфейс устройства QueryInterface, чтобы получить ссылку ID2D1DeviceContextPool, а затем вызовите ID2D1DeviceContextPool::GetDeviceContextLease, чтобы получить объект ID2D1DeviceContextLease для доступа к контексту устройства. После этого выпустите аренду. Не касайтесь контекста устройства после освобождения аренды, так как он может использоваться параллельно другими потоками.
Включение поиска оболочек WinRT
Как указано в Win2D документации по взаимодействию, общедоступный заголовок Win2D также предоставляет доступ к GetOrCreate методу (доступному из ICanvasFactoryNative фабрики активации или с использованием GetOrCreate помощников C++/CX, определённых в том же заголовке). Это позволяет получить оболочку WinRT из заданного собственного ресурса. Например, он позволяет извлекать или создавать CanvasDevice экземпляр из ID2D1Device1 объекта, CanvasBitmap из ID2D1Bitmapобъекта и т. д.
Этот метод также работает для всех встроенных эффектов Win2D: получение нативного ресурса для заданного эффекта, а затем использование этого для получения соответствующей обёртки Win2D будет правильно возвращать соответствующий эффект Win2D для него. Чтобы пользовательские эффекты также могли воспользоваться той же системой сопоставления, Win2D предоставляет несколько API в интерфейсе взаимодействия для фабрики активации CanvasDevice, которая является типом ICanvasFactoryNative, и также дополнительный интерфейс фабрики эффектов: ICanvasEffectFactoryNative
[uuid("29BA1A1F-1CFE-44C3-984D-426D61B51427")]
class ICanvasEffectFactoryNative : IUnknown
{
HRESULT CreateWrapper(
ICanvasDevice* device,
ID2D1Effect* resource,
float dpi,
IInspectable** wrapper);
};
[uuid("695C440D-04B3-4EDD-BFD9-63E51E9F7202")]
class ICanvasFactoryNative : IInspectable
{
HRESULT GetOrCreate(
ICanvasDevice* device,
IUnknown* resource,
float dpi,
IInspectable** wrapper);
HRESULT RegisterWrapper(IUnknown* resource, IInspectable* wrapper);
HRESULT UnregisterWrapper(IUnknown* resource);
HRESULT RegisterEffectFactory(
REFIID effectId,
ICanvasEffectFactoryNative* factory);
HRESULT UnregisterEffectFactory(REFIID effectId);
};
Здесь есть несколько API, которые необходимо учитывать, так как они необходимы для поддержки всех различных сценариев, в которых можно использовать эффекты Win2D, а также способов взаимодействия разработчиков с уровнем D2D, а затем попытаться устранить оболочки для них. Давайте рассмотрим каждый из этих API.
Методы RegisterWrapper и UnregisterWrapper предназначены для вызова пользовательскими эффектами, чтобы добавить их в внутренний кэш Win2D.
-
RegisterWrapper: регистрирует собственный ресурс и собственную оболочку WinRT. Необходима реализация параметраwrapperиIWeakReferenceSource, чтобы их можно было правильно кэшировать и избежать циклов ссылок, которые могут привести к утечкам памяти. Метод возвращаетS_OK, если собственный ресурс может быть добавлен в кэш;S_FALSE, если уже зарегистрирована оболочка дляresource, и код ошибки в случае ошибки. -
UnregisterWrapper: отменяет регистрацию нативного ресурса и его обертки. Возвращает значениеS_OK, если ресурс можно удалить,S_FALSEеслиresourceон еще не зарегистрирован, и код erro, если произошла другая ошибка.
Пользовательские эффекты должны вызывать RegisterWrapper и UnregisterWrapper всякий раз, когда они реализуются и теряют актуальность, то есть когда создаётся и ассоциируется с ними новый собственный ресурс. Пользовательские эффекты, которые не поддерживают реализацию (например, имеющие фиксированное связанное устройство), могут вызывать RegisterWrapper и UnregisterWrapper при создании и уничтожении. Пользовательские эффекты должны правильно отменять регистрацию из всех возможных путей кода, чтобы избежать ситуации, когда оболочка становится недопустимой, включая случай финализации объекта в случае его реализации на управляемом языке.
RegisterEffectFactory и UnregisterEffectFactory методы также предназначены для использования в пользовательских эффектах, чтобы они могли регистрировать функцию обратного вызова для создания новой оболочки в случае, если разработчик пытается разрешить один для "сиротских" ресурсов D2D.
-
RegisterEffectFactory: зарегистрируйте обратный вызов, который принимает те же параметры, что разработчик передал вGetOrCreate, и создает новую проверяемую оболочку для входного эффекта. Идентификатор эффекта используется в качестве ключа, чтобы каждый настраиваемый эффект смог зарегистрировать свою фабрику при его первой загрузке. Конечно, это должно быть сделано только один раз на тип эффекта, и не каждый раз, когда эффект реализуется.device,resourceиwrapperпроверяются Win2D перед вызовом любого зарегистрированного обратного вызова, поэтому они гарантированно не равныnull, когда вызываетсяCreateWrapper.dpiсчитается необязательным и можно проигнорировать в случае, если для типа эффекта нет конкретного применения. Обратите внимание, что при создании новой оболочки из зарегистрированной фабрики эта фабрика также должна убедиться, что новая оболочка зарегистрирована в кэше (Win2D не будет автоматически добавлять оболочки, созданные внешними фабриками в кэш). -
UnregisterEffectFactory: удаляет ранее зарегистрированный обратный вызов. Например, это можно использовать, если оболочка эффектов реализована в управляемой сборке, которая выгружается.
Примечание.
ICanvasFactoryNative реализуется фабрикой активации, CanvasDeviceдля которой можно получить, вызывая RoGetActivationFactoryвручную или используя вспомогательные API из расширений языка, которые вы используете (например winrt::get_activation_factory , в C++/WinRT). Дополнительные сведения см. в разделе "Система типов WinRT" для получения дополнительных сведений о том, как это работает.
Например, чтобы продемонстрировать, как это сопоставление проявляется на практике, рассмотрим, как работают встроенные эффекты Win2D. Если они не реализованы, все состояния (например, свойства, источники и т. д.) хранятся во внутреннем кэше в каждом экземпляре эффекта. Когда они реализуются, все состояния передаются в собственный ресурс (например, свойства устанавливаются на эффекте D2D, все источники разрешаются и сопоставляются с его входными данными и т. д.), и пока эффект находится в реализованном состоянии, он будет выступать в качестве основного источника информации о состоянии оболочки. То есть, если значение любого свойства извлекается из оболочки, оно получит обновленное значение из родного ресурса D2D, связанного с ним.
Это гарантирует, что если любые изменения вносятся непосредственно в ресурс D2D, они будут отображаться на внешней оболочке, и они никогда не будут синхронизированы. Когда эффект не реализован, все состояние возвращается из нативного ресурса в состояние оболочки перед освобождением ресурса. Он будет храниться и обновляться там до следующего момента реализации эффекта. Теперь рассмотрим эту последовательность событий:
- У вас есть некоторый эффект Win2D (встроенный или настраиваемый).
- Вы получаете
ID2D1Imageиз этого (который являетсяID2D1Effect). - Вы создаёте экземпляр пользовательского эффекта.
- Вы также получаете
ID2D1Imageот этого. - Вы вручную задали это изображение в качестве входных данных для предыдущего эффекта (через
ID2D1Effect::SetInput). - Затем запросите первый эффект для оболочки WinRT в контексте этого ввода.
Так как эффект реализован (он был реализован при запросе собственного ресурса), он будет использовать собственный ресурс в качестве источника истины. Таким образом, он получит ID2D1Image, соответствующий запросу ресурс, и попытается получить для него оболочку WinRT. Если эффект, из которого был извлечен этот вход, правильно добавил свою пару ресурсов и оболочку WinRT в кэш Win2D, оболочка будет разрешена и возвращена вызывающим объектам. Если нет, доступ к свойствам завершится ошибкой, так как Win2D не может обработать оболочки WinRT для эффектов, которые ему не принадлежат, так как он не знает, как их инициализировать.
Здесь RegisterWrapper и UnregisterWrapper оказывают помощь, так как они позволяют пользовательским эффектам беспрепятственно интегрироваться в логику разрешения оболочек Win2D. Благодаря этому всегда можно получить корректную оболочку для любого источника эффектов, независимо от того, задан ли он через API WinRT или непосредственно на базовом уровне D2D.
Чтобы объяснить, как фабрики эффектов также вступают в игру, рассмотрим этот сценарий:
- Пользователь создает экземпляр пользовательской оболочки и реализует его.
- Затем они получают ссылку на базовый эффект D2D и сохраняют его.
- Затем эффект реализуется на другом устройстве. Эффект будет отменять реализацию и затем повторно её реализовать, и при этом создаст новый эффект D2D. На этом этапе предыдущий эффект D2D больше не связан с проверяемой оболочкой.
- Затем пользователь вызывает на первом эффекте D2D
GetOrCreate.
Без обратного вызова Win2D просто не смог бы разрешить обертку, так как для нее нет зарегистрированной обертки. Если фабрика зарегистрирована, можно создать и вернуть новую обертку для этого эффекта D2D, так что сценарий будет продолжать работать плавно для пользователя.
Реализация пользовательского ICanvasEffect
Интерфейс Win2D ICanvasEffect расширяет ICanvasImage, поэтому все предыдущие пункты также применяются к пользовательским эффектам. Единственное различие заключается в том, что ICanvasEffect также реализует дополнительные методы, специфические для эффектов, такие как аннулирование исходного прямоугольника, получение необходимых прямоугольников и т. д.
Для поддержки этого Win2D предоставляет экспорт C, который авторы пользовательских эффектов могут использовать, чтобы им не придется повторно выполнять всю эту дополнительную логику с нуля. Это работает так же, как экспорт C для GetBounds. Ниже приведены доступные экспорты для эффектов:
HRESULT InvalidateSourceRectangleForICanvasImageInterop(
ICanvasResourceCreatorWithDpi* resourceCreator,
ICanvasImageInterop* image,
uint32_t sourceIndex,
Rect const* invalidRectangle);
HRESULT GetInvalidRectanglesForICanvasImageInterop(
ICanvasResourceCreatorWithDpi* resourceCreator,
ICanvasImageInterop* image,
uint32_t* valueCount,
Rect** valueElements);
HRESULT GetRequiredSourceRectanglesForICanvasImageInterop(
ICanvasResourceCreatorWithDpi* resourceCreator,
ICanvasImageInterop* image,
Rect const* outputRectangle,
uint32_t sourceEffectCount,
ICanvasEffect* const* sourceEffects,
uint32_t sourceIndexCount,
uint32_t const* sourceIndices,
uint32_t sourceBoundsCount,
Rect const* sourceBounds,
uint32_t valueCount,
Rect* valueElements);
Давайте рассмотрим, как их можно использовать:
-
InvalidateSourceRectangleForICanvasImageInteropпредназначен для поддержкиInvalidateSourceRectangle. Просто сгруппируйте входные параметры и вызывайте его напрямую, и он позаботится обо всей необходимой работе. Обратите внимание, чтоimageпараметр является текущим экземпляром эффекта, реализованным. -
GetInvalidRectanglesForICanvasImageInteropподдерживаетGetInvalidRectangles. Это также не требует особого внимания, кроме необходимости удаления возвращаемого COM-массива после того, как он больше не нужен. -
GetRequiredSourceRectanglesForICanvasImageInterop— это общий метод, который может поддерживать обаGetRequiredSourceRectangleиGetRequiredSourceRectangles. То есть он принимает указатель на существующий массив значений для заполнения, поэтому вызывающие элементы могут передавать указатель на одно значение (которое может также находиться в стеке, чтобы избежать выделения памяти) или на массив значений. Реализация одинакова в обоих случаях, поэтому для работы обоих из них достаточно одного экспорта C.
Пользовательские эффекты в C# с помощью ComputeSharp
Как уже упоминалось, если вы используете C# и хотите реализовать настраиваемый эффект, рекомендуемый подход — использовать библиотеку ComputeSharp . Он позволяет реализовать пользовательские шейдеры пикселей D2D1 полностью в C#, а также легко определять пользовательские графы эффектов, совместимые с Win2D. Эта же библиотека также используется в Microsoft Store для питания нескольких графических компонентов в приложении.
Вы можете добавить ссылку на ComputeSharp в проекте через NuGet: выберите пакет ComputeSharp.D2D1.WinUI .
Примечание.
Многие API-интерфейсы в ComputeSharp.D2D1.* идентичны для целевых платформ UWP и WinUI, единственное различие заключается в пространстве имен (заканчивается либо на .Uwp, либо на .WinUI). Однако целевой объект UWP находится в постоянном обслуживании и не получает новые функции. Таким образом, некоторые изменения кода могут потребоваться по сравнению с примерами, показанными здесь для WinUI. Фрагменты кода в этом документе отражают интерфейс API на момент ComputeSharp.D2D1.WinUI.0.0 (последняя версия для целевой платформы UWP — 2.1.0).
Существует два основных компонента в ComputeSharp для взаимодействия с Win2D:
-
PixelShaderEffect<T>: эффект Win2D, который используется шейдером пикселей D2D1. Сам шейдер написан на C# с помощью API, предоставляемых ComputeSharp. Этот класс также предоставляет свойства для задания источников эффектов, константных значений и т. д. -
CanvasEffect: базовый класс для пользовательских эффектов Win2D, который упаковывает произвольный граф эффектов. Его можно использовать для "упаковки" сложных эффектов в простой объект, который можно повторно использовать в нескольких частях приложения.
Ниже приведен пример пользовательского шейдера пикселей (перенесенного из этого шейдера), используемого с PixelShaderEffect<T> и затем отображаемого на Win2D CanvasControl (обратите внимание, что PixelShaderEffect<T> реализует ICanvasImage).
Вы можете увидеть, как всего в двух строках кода можно создать эффект и нарисовать его с помощью Win2D. ComputeSharp заботится обо всех работах, необходимых для компиляции шейдера, регистрации его и управления сложным временем существования эффекта, совместимого с Win2D.
Далее давайте рассмотрим пошаговое руководство по созданию пользовательского эффекта Win2D, который также использует шейдер пикселей D2D1. Мы рассмотрим, как создать шейдер с помощью ComputeSharp и настроить его свойства, а затем как создать настраиваемый граф эффектов, упакованный в CanvasEffect тип, который можно легко использовать в приложении.
Проектирование эффекта
Для этой демонстрации мы хотим создать простой эффект заморозки стекла.
К ним относятся следующие компоненты:
- Размытие Гауссиана
- Эффект тонов
- Шум (который можно процедурно создать с помощью шейдера)
Мы также хотим предоставить доступ к свойствам для управления степенью размытия и уровнем шума. Окончательный эффект будет содержать "упакованую" версию этого графа эффектов и легко использовать, просто создавая экземпляр, устанавливая эти свойства, подключая исходный образ, а затем рисуя его. Приступим.
Создание пользовательского шейдера пикселей D2D1
Для шума поверх эффекта мы можем использовать простой шейдер пикселей D2D1. Шейдер вычисляет случайное значение на основе его координат (который будет выступать в качестве начального значения для случайного числа), а затем будет использовать это шумовое значение для вычисления суммы RGB для этого пикселя. Затем мы можем смешать этот шум поверх полученного изображения.
Чтобы написать шейдер с помощью ComputeSharp, необходимо просто определить partial struct тип, реализующий ID2D1PixelShader интерфейс, а затем написать логику в методе Execute . Для этого шумового шейдера можно написать примерно следующее:
using ComputeSharp;
using ComputeSharp.D2D1;
[D2DInputCount(0)]
[D2DRequiresScenePosition]
[D2DShaderProfile(D2D1ShaderProfile.PixelShader40)]
[D2DGeneratedPixelShaderDescriptor]
public readonly partial struct NoiseShader(float amount) : ID2D1PixelShader
{
/// <inheritdoc/>
public float4 Execute()
{
// Get the current pixel coordinate (in pixels)
int2 position = (int2)D2D.GetScenePosition().XY;
// Compute a random value in the [0, 1] range for each target pixel. This line just
// calculates a hash from the current position and maps it into the [0, 1] range.
// This effectively provides a "random looking" value for each pixel.
float hash = Hlsl.Frac(Hlsl.Sin(Hlsl.Dot(position, new float2(41, 289))) * 45758.5453f);
// Map the random value in the [0, amount] range, to control the strength of the noise
float alpha = Hlsl.Lerp(0, amount, hash);
// Return a white pixel with the random value modulating the opacity
return new(1, 1, 1, alpha);
}
}
Примечание.
Хотя шейдер полностью написан на C#, рекомендуется использовать базовые знания о HLSL (языке программирования для шейдеров DirectX, для которых ComputeSharp транспилирует C#).
Давайте подробно рассмотрим этот шейдер:
- Шейдер не имеет входных данных, он просто создает бесконечное изображение со случайным серым шумом.
- Для шейдера требуется доступ к текущей координате пикселя.
- Шейдер предварительно компилируется во время сборки (с помощью
PixelShader40профиля, который гарантированно будет доступен на любом GPU, где может работать приложение). - Атрибут
[D2DGeneratedPixelShaderDescriptor]необходим для активации исходного генератора, упаковавшегося в ComputeSharp, который будет анализировать код C#, транспилировать его в HLSL, компилировать шейдер в байт-код и т. д. - Шейдер захватывает
float amountпараметр через основной конструктор. Генератор исходного кода в ComputeSharp автоматически будет заботиться об извлечении всех захваченных значений в шейдере и подготовке буфера констант, необходимого D2D для инициализации состояния шейдера.
И эта часть выполнена! Этот шейдер создаст нашу пользовательскую текстуру шума всякий раз, когда это необходимо. Далее необходимо создать упакованный эффект с графом эффектов, соединяющим все наши эффекты вместе.
Создание настраиваемого эффекта
Чтобы легко использовать упакованный эффект, можно использовать CanvasEffect тип из ComputeSharp. Этот тип предоставляет простой способ настройки всей необходимой логики для создания графа эффектов и обновления его с помощью общедоступных свойств, с которыми пользователи эффекта могут взаимодействовать. Существует два основных метода, которые необходимо реализовать:
-
BuildEffectGraph: этот метод отвечает за построение графа эффектов, который мы хотим нарисовать. То есть необходимо создать все необходимые эффекты и зарегистрировать выходной узел для графа. Для эффектов, которые можно обновить позже, регистрация выполняется с соответствующимCanvasEffectNode<T>значением, которое служит ключом для поиска эффектов в графе при необходимости. -
ConfigureEffectGraph: этот метод обновляет граф эффектов, применяя параметры, настроенные пользователем. Этот метод автоматически вызывается при необходимости, прямо перед рисованием эффекта, и только если с момента последнего использования эффекта было изменено по крайней мере одно свойство эффекта.
Настраиваемый эффект можно определить следующим образом:
using ComputeSharp.D2D1.WinUI;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;
public sealed class FrostedGlassEffect : CanvasEffect
{
private static readonly CanvasEffectNode<GaussianBlurEffect> BlurNode = new();
private static readonly CanvasEffectNode<PixelShaderEffect<NoiseShader>> NoiseNode = new();
private ICanvasImage? _source;
private double _blurAmount;
private double _noiseAmount;
public ICanvasImage? Source
{
get => _source;
set => SetAndInvalidateEffectGraph(ref _source, value);
}
public double BlurAmount
{
get => _blurAmount;
set => SetAndInvalidateEffectGraph(ref _blurAmount, value);
}
public double NoiseAmount
{
get => _noiseAmount;
set => SetAndInvalidateEffectGraph(ref _noiseAmount, value);
}
/// <inheritdoc/>
protected override void BuildEffectGraph(CanvasEffectGraph effectGraph)
{
// Create the effect graph as follows:
//
// ┌────────┐ ┌──────┐
// │ source ├──►│ blur ├─────┐
// └────────┘ └──────┘ ▼
// ┌───────┐ ┌────────┐
// │ blend ├──►│ output │
// └───────┘ └────────┘
// ┌───────┐ ▲
// │ noise ├──────────────┘
// └───────┘
//
GaussianBlurEffect gaussianBlurEffect = new();
BlendEffect blendEffect = new() { Mode = BlendEffectMode.Overlay };
PixelShaderEffect<NoiseShader> noiseEffect = new();
PremultiplyEffect premultiplyEffect = new();
// Connect the effect graph
premultiplyEffect.Source = noiseEffect;
blendEffect.Background = gaussianBlurEffect;
blendEffect.Foreground = premultiplyEffect;
// Register all effects. For those that need to be referenced later (ie. the ones with
// properties that can change), we use a node as a key, so we can perform lookup on
// them later. For others, we register them anonymously. This allows the effect
// to autommatically and correctly handle disposal for all effects in the graph.
effectGraph.RegisterNode(BlurNode, gaussianBlurEffect);
effectGraph.RegisterNode(NoiseNode, noiseEffect);
effectGraph.RegisterNode(premultiplyEffect);
effectGraph.RegisterOutputNode(blendEffect);
}
/// <inheritdoc/>
protected override void ConfigureEffectGraph(CanvasEffectGraph effectGraph)
{
// Set the effect source
effectGraph.GetNode(BlurNode).Source = Source;
// Configure the blur amount
effectGraph.GetNode(BlurNode).BlurAmount = (float)BlurAmount;
// Set the constant buffer of the shader
effectGraph.GetNode(NoiseNode).ConstantBuffer = new NoiseShader((float)NoiseAmount);
}
}
В этом классе можно увидеть четыре раздела:
- Во-первых, у нас есть поля для отслеживания всех изменяемых состояний, таких как эффекты, которые можно обновить, а также резервные поля для всех свойств эффекта, которые мы хотим предоставить пользователям эффекта.
- Далее у нас есть свойства для настройки эффекта. Для задания каждого свойства используется
SetAndInvalidateEffectGraphметод, предоставляемый методомCanvasEffect, который автоматически отменяет эффект, если заданное значение отличается от текущего. Это гарантирует, что эффект настраивается повторно только тогда, когда это действительно необходимо. - Наконец, у нас есть методы
BuildEffectGraphиConfigureEffectGraph, которые мы упомянули выше.
Примечание.
Узел PremultiplyEffect после эффекта шума очень важен: это связано с тем, что эффекты Win2D предполагают, что выходные данные предварительно премулируются, в то время как шейдеры пикселей обычно работают с нерекомендируемыми пикселями. Таким образом, не забудьте вручную вставить узлы предварительного умножения/отмены умножения перед и после кастомных шейдеров, чтобы обеспечить правильное сохранение цветов.
Готово к рисованию!
И с этим наш пользовательский эффект заморозки стекла готов! Мы можем легко нарисовать его следующим образом:
private void CanvasControl_Draw(CanvasControl sender, CanvasDrawEventArgs args)
{
FrostedGlassEffect effect = new()
{
Source = _canvasBitmap,
BlurAmount = 12,
NoiseAmount = 0.1
};
args.DrawingSession.DrawImage(effect);
}
В этом примере мы рисуем эффект от Draw обработчика объекта CanvasControl, используя CanvasBitmap который мы ранее загружали в качестве источника. Это входной образ, который мы будем использовать для проверки эффекта:
И вот результат:
Примечание.
Благодарность Доминику Ланге за изображение.
Дополнительные ресурсы
- Дополнительные сведения см. в исходном коде Win2D.
- Дополнительные сведения о ComputeSharp см. в примерах приложений и модульных тестах.
Windows developer