Поделиться через


Реализация пользовательских эффектов

Win2D предоставляет несколько API для представления объектов, которые можно нарисовать, которые делятся на две категории: изображения и эффекты. Изображения, представленные ICanvasImage интерфейсом, не имеют входных данных и могут быть непосредственно нарисованы на данной поверхности. Например, CanvasBitmapVirtualizedCanvasBitmap и CanvasRenderTarget являются примерами типов изображений. С другой стороны, эффекты представлены интерфейсом ICanvasEffect . Они могут иметь входные данные, а также дополнительные ресурсы и могут применять произвольные логики для создания выходных данных (как эффект также является изображением). Win2D включает эффекты упаковки большинства эффектов D2D, таких как GaussianBlurEffect, TintEffect и LuminanceToAlphaEffect.

Изображения и эффекты также можно объединить, чтобы создать произвольные графы, которые затем можно отобразить в приложении (также см. документы D2D в эффектах Direct2D). Вместе они обеспечивают чрезвычайно гибкую систему для создания сложной графики эффективным образом. Однако существуют случаи, когда встроенные эффекты недостаточно, и вы можете создать собственный эффект Win2D. Для поддержки этого Win2D включает набор мощных API взаимодействия, который позволяет определять пользовательские изображения и эффекты, которые могут легко интегрироваться с Win2D.

Совет

Если вы используете C# и хотите реализовать настраиваемый график эффектов или эффектов, рекомендуется использовать ComputeSharp , а не пытаться реализовать эффект с нуля. В приведенном ниже абзаце подробно описано, как использовать эту библиотеку для реализации пользовательских эффектов, которые легко интегрируются с Win2D.

API платформы: ICanvasImage, CanvasBitmapGaussianBlurEffectICanvasLuminanceToAlphaEffectImageTintEffectIGraphicsEffectSourceCanvasRenderTargetCanvasEffectVirtualizedCanvasBitmap, , 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 вызове оно должно возвращать устройство создания и задать для 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:

Примечание.

Многие 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 и отображаемый в окне приложения

Вы можете увидеть, как всего в двух строках кода можно создать эффект и нарисовать его с помощью 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 см. в примерах приложений и модульных тестах.