Реализация пользовательских эффектов
Win2D предоставляет несколько API для представления объектов, которые можно нарисовать, которые делятся на две категории: изображения и эффекты. Изображения, представленные ICanvasImage
интерфейсом, не имеют входных данных и могут быть непосредственно нарисованы на данной поверхности. Например, CanvasBitmap
VirtualizedCanvasBitmap
и CanvasRenderTarget
являются примерами типов изображений. С другой стороны, эффекты представлены интерфейсом ICanvasEffect
. Они могут иметь входные данные, а также дополнительные ресурсы и могут применять произвольные логики для создания выходных данных (как эффект также является изображением). Win2D включает эффекты упаковки большинства эффектов D2D, таких как GaussianBlurEffect
, TintEffect
и LuminanceToAlphaEffect
.
Изображения и эффекты также можно объединить, чтобы создать произвольные графы, которые затем можно отобразить в приложении (также см. документы D2D в эффектах Direct2D). Вместе они обеспечивают чрезвычайно гибкую систему для создания сложной графики эффективным образом. Однако существуют случаи, когда встроенные эффекты недостаточно, и вы можете создать собственный эффект Win2D. Для поддержки этого Win2D включает набор мощных API взаимодействия, который позволяет определять пользовательские изображения и эффекты, которые могут легко интегрироваться с Win2D.
Совет
Если вы используете C# и хотите реализовать настраиваемый график эффектов или эффектов, рекомендуется использовать ComputeSharp , а не пытаться реализовать эффект с нуля. В приведенном ниже абзаце подробно описано, как использовать эту библиотеку для реализации пользовательских эффектов, которые легко интегрируются с Win2D.
API платформы:
ICanvasImage
,CanvasBitmap
GaussianBlurEffect
ICanvasLuminanceToAlphaEffectImage
TintEffect
IGraphicsEffectSource
CanvasRenderTarget
CanvasEffect
VirtualizedCanvasBitmap
, ,ID2D21Image
ID2D1Factory1
ID2D1Effect
Реализация пользовательского 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
вызове оно должно возвращать устройство создания и задать дляWIN2D_GET_DEVICE_ASSOCIATION_TYPE_CREATION_DEVICE
него значениеtype
. Обратите внимание, что при указании этого типа возвращаемое устройство не должно бытьnull
.
Примечание.
Win2D может вызываться GetDevice
при рекурсивном обходе графа эффектов, что означает, что в стеке может быть несколько активных вызовов GetD2DImage
. Из-за этого GetDevice
не следует блокировать текущий образ, так как это может привести к взаимоблокировки. Скорее, он должен использовать блокировку повторного входа в неблокирующий способ и возвращать ошибку, если она не может быть получена. Это гарантирует, что тот же поток рекурсивно вызывает его, а одновременные потоки выполняются сбоем.
Осуществляющий GetD2DImage
GetD2DImage
где происходит большая часть работы. Этот метод отвечает за получение ID2D1Image
объекта, который Win2D может рисовать, при необходимости реализуя текущий эффект при необходимости. Это также включает рекурсивное обход и реализацию графа эффектов для всех источников, если таковые имеются, а также инициализацию любого состояния, которое может потребоваться изображение (например, буферы констант и другие свойства, текстуры ресурсов и т. д.).
Точную реализацию этого метода сильно зависит от типа изображения, и она может сильно отличаться, но, как правило, для произвольного эффекта можно ожидать, что метод будет выполнять следующие действия:
- Проверьте, был ли вызов рекурсивным в одном экземпляре, и завершится ли это ошибкой. Это необходимо для обнаружения циклов в графе эффектов (например, эффект
A
имеет эффектB
в качестве источника и эффектB
имеет эффектA
в качестве источника). - Получите блокировку экземпляра образа для защиты от параллельного доступа.
- Обработка целевых DPIs в соответствии с входными флагами
- Проверьте, соответствует ли входное устройство используемому. Если он не соответствует, а текущий эффект поддерживает реализацию, нереализировать эффект.
- Реализуйте влияние на входное устройство. При необходимости можно зарегистрировать эффект 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, WinRT, 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.
UnregisterWrapper
Методы RegisterWrapper
должны вызываться пользовательскими эффектами для добавления себя в внутренний кэш Win2D:
RegisterWrapper
: регистрирует собственный ресурс и собственную оболочку WinRT. Параметрwrapper
требуется также для тогоIWeakReferenceSource
, чтобы он был кэширован правильно, не вызывая циклы ссылок, что приведет к утечкам памяти. Метод возвращает,S_OK
если собственный ресурс может быть добавлен в кэш,S_FALSE
если в кэше уже зарегистрирована оболочкаresource
, и код ошибки при возникновении ошибки.UnregisterWrapper
: отменяет регистрацию собственного ресурса и его оболочки. Возвращает значениеS_OK
, если ресурс можно удалить,S_FALSE
еслиresource
он еще не зарегистрирован, и код erro, если произошла другая ошибка.
Пользовательские эффекты должны вызываться RegisterWrapper
и UnregisterWrapper
всякий раз, когда они реализуются и нереализуются, т. е. при создании и сопоставлении с ними нового собственного ресурса. Пользовательские эффекты, не поддерживающие реализацию (например, имеющие фиксированное связанное устройство), могут вызываться RegisterWrapper
и UnregisterWrapper
при создании и уничтожении. Пользовательские эффекты должны правильно отменять регистрацию из всех возможных путей кода, что приведет к тому, что оболочка станет недопустимой (например, когда объект завершен, в случае его реализации на управляемом языке).
UnregisterEffectFactory
Методы RegisterEffectFactory
также предназначены для использования пользовательскими эффектами, чтобы они также могли регистрировать обратный вызов для создания новой оболочки в случае, если разработчик пытается разрешить один для "потерянных" ресурсов D2D:
RegisterEffectFactory
: зарегистрируйте обратный вызов, который принимает входные параметры, переданныеGetOrCreate
разработчику, и создает новую проверяемую оболочку для входного эффекта. Идентификатор эффекта используется в качестве ключа, чтобы каждый настраиваемый эффект смог зарегистрировать фабрику при первой загрузке. Конечно, это должно быть сделано только один раз на тип эффекта, и не каждый раз, когда эффект реализуется.wrapper
resource
Параметрыdevice
проверяются 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 больше не в качестве связанной проверяемой оболочки на этом этапе.
- Затем пользователь вызывает
GetOrCreate
первый эффект D2D.
Без обратного вызова 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:
- В UWP выберите пакет ComputeSharp.D2D1.Uwp.
- В WinAppSDK выберите пакет ComputeSharp.D2D1.WinUI.
Примечание.
Многие API-интерфейсы в ComputeSharp.D2D1.* идентичны для целевых объектов UWP и WinAppSDK, единственное различие заключается в пространстве имен (заканчивается на любом .Uwp
или .WinUI
). Однако целевой объект UWP находится в постоянном обслуживании и не получает новые функции. Таким образом, некоторые изменения кода могут потребоваться по сравнению с примерами, показанными здесь для WinUI. Фрагменты кода в этом документе отражают поверхность API в качестве объекта ComputeSharp.D2D1.WinUI 3.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 предполагают, что выходные данные предварительно премулируются, в то время как шейдеры пикселей обычно работают с нерекомендируемыми пикселями. Таким образом, не забудьте вручную вставить узлы предварительной или нерекомендируемой работы до и после пользовательских шейдеров, чтобы обеспечить правильное сохранение цветов.
Примечание.
Этот пример эффекта использует пространства имен WinUI 3, но тот же код также можно использовать в UWP. В этом случае пространство имен для ComputeSharp будет ComputeSharp.Uwp
совпадать с именем пакета.
Готово к рисованию!
И с этим наш пользовательский эффект заморозки стекла готов! Мы можем легко нарисовать его следующим образом:
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