Compartir a través de


Implementación de efectos personalizados

Win2D proporciona varias API para representar objetos que se pueden dibujar, que se dividen en dos categorías: imágenes y efectos. Las imágenes, representadas por la ICanvasImage interfaz, no tienen entradas y se pueden dibujar directamente en una superficie determinada. Por ejemplo, CanvasBitmap, VirtualizedCanvasBitmap y CanvasRenderTarget son ejemplos de tipos de imagen. Por otro lado, los efectos se representan mediante la ICanvasEffect interfaz . Pueden tener entradas, así como recursos adicionales, y pueden aplicar lógica arbitraria para generar sus salidas (ya que un efecto también es una imagen). Win2D incluye efectos que encapsulan la mayoría de los efectos D2D, como GaussianBlurEffect, TintEffect y LuminanceToAlphaEffect.

Las imágenes y los efectos también se pueden encadenar para crear gráficos arbitrarios que se pueden mostrar en la aplicación (consulte también los documentos D2D sobre efectos Direct2D). Juntos, proporcionan un sistema extremadamente flexible para crear gráficos complejos de forma eficaz. Sin embargo, hay casos en los que los efectos integrados no son suficientes y es posible que quieras crear tu propio efecto Win2D. Para admitir esto, Win2D incluye un conjunto de potentes API de interoperabilidad que permite definir imágenes y efectos personalizados que se pueden integrar sin problemas con Win2D.

Sugerencia

Si usa C# y quiere implementar un gráfico de efectos o efectos personalizados, se recomienda usar ComputeSharp en lugar de intentar implementar un efecto desde cero. Consulta el párrafo siguiente para obtener una explicación detallada de cómo usar esta biblioteca para implementar efectos personalizados que se integran perfectamente con Win2D.

API de plataforma: ICanvasImage, CanvasBitmap, , CanvasRenderTargetVirtualizedCanvasBitmap, CanvasEffectTintEffectICanvasLuminanceToAlphaEffectImageGaussianBlurEffect, IGraphicsEffectSource, , , ID2D1Factory1ID2D21ImageID2D1Effect

Implementación de un personalizado ICanvasImage

El escenario más sencillo para admitir es crear un personalizado ICanvasImage. Como hemos mencionado, esta es la interfaz de WinRT definida por Win2D, que representa todo tipo de imágenes con las que Win2D puede interopar. Esta interfaz solo expone dos GetBounds métodos y extiende IGraphicsEffectSource, que es una interfaz de marcador que representa "algún origen de efecto".

Como puede ver, no hay ninguna API "funcional" expuesta por esta interfaz para realizar realmente ningún dibujo. Para implementar su propio ICanvasImage objeto, también deberá implementar la ICanvasImageInterop interfaz , que expone toda la lógica necesaria para que Win2D dibuje la imagen. Se trata de una interfaz COM definida en el encabezado público Microsoft.Graphics.Canvas.native.h , que se incluye con Win2D.

La interfaz se define de la siguiente manera:

[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);
}

Y también se basa en estos dos tipos de enumeración, desde el mismo encabezado:

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
}

Los dos GetDevice métodos y GetD2DImage son todos necesarios para implementar imágenes personalizadas (o efectos), ya que proporcionan Win2D con los puntos de extensibilidad para inicializarlos en un dispositivo determinado y recuperar la imagen D2D subyacente que se va a dibujar. La implementación correcta de estos métodos es fundamental para asegurarse de que las cosas funcionarán correctamente en todos los escenarios admitidos.

Vamos a repasarlos para ver cómo funciona cada método.

Implementación GetDevice

El GetDevice método es el más sencillo de los dos. Lo que hace es recuperar el dispositivo de lienzo asociado al efecto, de modo que Win2D pueda inspeccionarlo si es necesario (por ejemplo, para asegurarse de que coincide con el dispositivo en uso). El type parámetro indica el "tipo de asociación" para el dispositivo devuelto.

Hay dos casos posibles principales:

  • Si la imagen es un efecto, debe ser "realizado" y "no realizado" en varios dispositivos. Lo que significa esto es: se crea un efecto determinado en un estado no inicializado, se puede realizar cuando se pasa un dispositivo mientras se dibuja y, después, se puede seguir usando con ese dispositivo o se puede mover a otro dispositivo. En ese caso, el efecto restablecerá su estado interno y, a continuación, se dará cuenta de nuevo en el nuevo dispositivo. Esto significa que el dispositivo de lienzo asociado puede cambiar con el tiempo y también puede ser null. Debido a esto, type debe establecerse WIN2D_GET_DEVICE_ASSOCIATION_TYPE_REALIZATION_DEVICEen y el dispositivo devuelto debe establecerse en el dispositivo de realización actual, si hay uno disponible.
  • Algunas imágenes tienen un único "dispositivo propietario" que se asigna en el momento de la creación y nunca puede cambiar. Por ejemplo, esto sería el caso de una imagen que representa una textura, ya que se asigna en un dispositivo específico y no se puede mover. Cuando GetDevice se llama a , debe devolver el dispositivo de creación y establecer en type WIN2D_GET_DEVICE_ASSOCIATION_TYPE_CREATION_DEVICE. Tenga en cuenta que cuando se especifica este tipo, el dispositivo devuelto no debe ser null.

Nota:

Win2D puede llamar GetDevice a mientras recorre recursivamente un gráfico de efectos, lo que significa que puede haber varias llamadas activas a GetD2DImage en la pila. Debido a esto, GetDevice no debe tomar un bloqueo de bloqueo en la imagen actual, ya que podría bloquearse. En su lugar, debe usar un bloqueo de reintento de manera no de bloqueo y devolver un error si no se puede adquirir. Esto garantiza que el mismo subproceso que llama recursivamente lo adquirirá correctamente, mientras que los subprocesos simultáneos que hacen lo mismo producirán un error correctamente.

Implementación GetD2DImage

GetD2DImage es donde se lleva a cabo la mayor parte del trabajo. Este método es responsable de recuperar el ID2D1Image objeto que Win2D puede dibujar, si es necesario, realizando opcionalmente el efecto actual. Esto también incluye el recorrido recursivo y la realización del gráfico de efectos para todos los orígenes, si existe, así como la inicialización de cualquier estado que pueda necesitar la imagen (por ejemplo, búferes de constantes y otras propiedades, texturas de recursos, etc.).

La implementación exacta de este método depende en gran medida del tipo de imagen y puede variar mucho, pero generalmente hablando por un efecto arbitrario, puede esperar que el método realice los pasos siguientes:

  • Compruebe si la llamada era recursiva en la misma instancia y produce un error si es así. Esto es necesario para detectar ciclos en un gráfico de efectos (por ejemplo, el efecto A tiene efecto B como origen y el efecto B tiene efecto A como origen).
  • Adquiera un bloqueo en la instancia de imagen para protegerse contra el acceso simultáneo.
  • Controlar los DPIs de destino según las marcas de entrada
  • Compruebe si el dispositivo de entrada coincide con el que está en uso, si existe. Si no coincide y el efecto actual admite la realización, no se realiza el efecto.
  • Tenga en cuenta el efecto en el dispositivo de entrada. Esto puede incluir el registro del efecto D2D en el ID2D1Factory1 objeto recuperado del dispositivo de entrada o el contexto del dispositivo, si es necesario. Además, se debe establecer todo el estado necesario en la instancia de efecto D2D que se va a crear.
  • Atraviesa recursivamente los orígenes y los enlaza al efecto D2D.

Con respecto a las marcas de entrada, hay varios casos posibles que los efectos personalizados deben controlar correctamente, para garantizar la compatibilidad con todos los demás efectos Win2D. Excluyendo WIN2D_GET_D2D_IMAGE_FLAGS_NONE, las marcas que se van a controlar son las siguientes:

  • WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT: en este caso, device se garantiza que no sea null. El efecto debe comprobar si el destino de contexto del dispositivo es un ID2D1CommandListy, si es así, agregue la WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION marca . De lo contrario, se debe establecer targetDpi (que también se garantiza que no sea null) en los DPIs recuperados del contexto de entrada. A continuación, debe quitarse WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT de las marcas.
  • WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION y WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION: se usan al establecer orígenes de efectos (vea las notas siguientes).
  • WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION: si se establece, omite la realización recursiva de los orígenes del efecto y simplemente devuelve el efecto realizado sin otros cambios.
  • WIN2D_GET_D2D_IMAGE_FLAGS_ALLOW_NULL_EFFECT_INPUTS: si se establece, los orígenes de efecto que se realizan pueden ser null, si el usuario aún no los ha establecido en un origen existente.
  • WIN2D_GET_D2D_IMAGE_FLAGS_UNREALIZE_ON_FAILURE: si se establece y un origen de efecto que se establece no es válido, el efecto se debe realizar antes de que se produzca un error. Es decir, si se produjo el error al resolver los orígenes del efecto después de darse cuenta del efecto, el efecto se debe realizar antes de devolver el error al autor de la llamada.

Con respecto a las marcas relacionadas con PPP, estos controlan cómo se establecen los orígenes de efecto. Para garantizar la compatibilidad con Win2D, los efectos deben agregar automáticamente efectos de compensación de PPP a sus entradas cuando sea necesario. Pueden controlar si ese es el caso así:

  • Si WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION se establece, se necesita un efecto de compensación de PPP siempre que el inputDpi parámetro no 0sea .
  • De lo contrario, se necesita una compensación de PPP si inputDpi no es , WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION no se establece y se establece, o bien WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION se establece el valor de PPP de entrada y los valores de PPP de 0destino no coinciden.

Esta lógica se debe aplicar cada vez que se realiza un origen y se enlaza a una entrada del efecto actual. Tenga en cuenta que si se agrega un efecto de compensación de PPP, debe ser el conjunto de entrada en la imagen D2D subyacente. Pero si el usuario intenta recuperar el contenedor de WinRT para ese origen, el efecto debe tener cuidado para detectar si se usó un efecto de PPP y devolver un contenedor para el objeto de origen original en su lugar. Es decir, los efectos de compensación de PPP deben ser transparentes para los usuarios del efecto.

Una vez finalizada toda la lógica de inicialización, el resultado ID2D1Image (al igual que con los objetos Win2D, un efecto D2D también es una imagen) debe estar listo para dibujarse con Win2D en el contexto de destino, que aún no conoce el destinatario en este momento.

Nota:

La implementación correcta de este método (y ICanvasImageInterop en general) es extremadamente complicada y solo está pensada para ser realizada por usuarios avanzados que necesitan absolutamente la flexibilidad adicional. Se recomienda una comprensión sólida de D2D, Win2D, COM, WinRT y C++ antes de intentar escribir una ICanvasImageInterop implementación. Si el efecto Win2D personalizado también tiene que encapsular un efecto D2D personalizado, también tendrás que implementar tu propio ID2D1Effect objeto (consulta los documentos D2D sobre efectos personalizados para obtener más información sobre esto). Estos documentos no son una descripción exhaustiva de toda la lógica necesaria (por ejemplo, no cubren cómo se deben serializar y administrar los orígenes de efecto en el límite D2D/Win2D), por lo que se recomienda usar también la implementación en el CanvasEffect código base de Win2D como punto de referencia para un efecto personalizado y modificarlo según sea necesario.

Implementación GetBounds

El último componente que falta para implementar completamente un efecto personalizado ICanvasImage es admitir las dos GetBounds sobrecargas. Para facilitar esto, Win2D expone una exportación de C que se puede usar para aprovechar la lógica existente para esto desde Win2D en cualquier imagen personalizada. La exportación es la siguiente:

HRESULT GetBoundsForICanvasImageInterop(
    ICanvasResourceCreator* resourceCreator,
    ICanvasImageInterop* image,
    Numerics::Matrix3x2 const* transform,
    Rect* rect);

Las imágenes personalizadas pueden invocar esta API y pasarse como parámetro image y, a continuación, simplemente devolver el resultado a sus autores de llamada. El transform parámetro puede ser null, si no hay ninguna transformación disponible.

Optimización del acceso al contexto del dispositivo

El deviceContext parámetro de a ICanvasImageInterop::GetD2DImage veces puede ser null, si un contexto no está disponible inmediatamente antes de la invocación. Esto se hace a propósito, de modo que un contexto solo se crea de forma diferir cuando realmente es necesario. Es decir, si hay un contexto disponible, Win2D lo pasará a la GetD2DImage invocación; de lo contrario, permitirá que las llamadas recuperen una por sí sola si es necesario.

La creación de un contexto de dispositivo es relativamente costosa, por lo que, para que la recuperación de una win2D más rápida exponga las API para acceder a su grupo de contexto de dispositivos interno. Esto permite a los efectos personalizados alquilar y devolver contextos de dispositivo asociados a un dispositivo de lienzo determinado de forma eficaz.

Las API de concesión de contexto de dispositivo se definen de la siguiente manera:

[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);
}

La ID2D1DeviceContextPool interfaz se implementa mediante CanvasDevice, que es el tipo Win2D que implementa la ICanvasDevice interfaz. Para usar el grupo, use QueryInterface en la interfaz del dispositivo para obtener una ID2D1DeviceContextPool referencia y, a continuación, llame ID2D1DeviceContextPool::GetDeviceContextLease a para obtener un ID2D1DeviceContextLease objeto para acceder al contexto del dispositivo. Una vez que ya no sea necesario, libere la concesión. Asegúrese de no tocar el contexto del dispositivo después de que se haya liberado la concesión, ya que otros subprocesos podrían usarse simultáneamente.

Habilitación de la búsqueda de contenedores de WinRT

Como se ve en los documentos de interoperabilidad de Win2D, el encabezado público de Win2D también expone un GetOrCreate método (accesible desde el ICanvasFactoryNative generador de activación o a través de los GetOrCreate asistentes de C++/CX definidos en el mismo encabezado). Esto permite recuperar un contenedor de WinRT de un recurso nativo determinado. Por ejemplo, permite recuperar o crear una CanvasDevice instancia de a partir de un ID2D1Device1 objeto , un CanvasBitmap objeto de un ID2D1Bitmap, etc.

Este método también funciona para todos los efectos win2D integrados: recuperar el recurso nativo para un efecto determinado y, a continuación, usarlo para recuperar el contenedor Win2D correspondiente devolverá correctamente el efecto Win2D propietario para él. Para que los efectos personalizados también se beneficien del mismo sistema de asignación, Win2D expone varias API en la interfaz de interoperabilidad para la factoría de activación para CanvasDevice, que es el ICanvasFactoryNative tipo, así como una interfaz de factoría de efectos adicional, 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);
};

Hay varias API que se deben tener en cuenta aquí, ya que son necesarias para admitir todos los distintos escenarios en los que se pueden usar los efectos win2D, así como cómo los desarrolladores podrían interopar con la capa D2D e intentar resolver contenedores para ellos. Vamos a repasar cada una de estas API.

Los RegisterWrapper métodos y UnregisterWrapper están diseñados para invocarse mediante efectos personalizados para agregarse a sí mismos en la memoria caché interna de Win2D:

  • RegisterWrapper: registra un recurso nativo y su contenedor winRT propietario. El wrapper parámetro es necesario para también implemement IWeakReferenceSource, de modo que se pueda almacenar en caché correctamente sin causar ciclos de referencia que provocarían pérdidas de memoria. El método devuelve S_OK si el recurso nativo se podría agregar a la memoria caché, S_FALSE si ya había un contenedor registrado para resourcey un código de error si se produce un error.
  • UnregisterWrapper: anula el registro de un recurso nativo y su contenedor. Devuelve S_OK si el recurso se podría quitar, S_FALSE si resource aún no estaba registrado y un código de erro si se produce otro error.

Los efectos personalizados deben llamar a RegisterWrapper y UnregisterWrapper cada vez que se realizan y no se realizan, es decir, cuando se crea un nuevo recurso nativo y se asocia con ellos. Los efectos personalizados que no admiten la realización (por ejemplo, aquellos que tienen un dispositivo asociado fijo) pueden llamar RegisterWrapper a y UnregisterWrapper cuándo se crean y destruyen. Los efectos personalizados deben asegurarse de anular correctamente el registro de todas las rutas de acceso de código posibles que harían que el contenedor no fuera válido (por ejemplo, cuando se finalice el objeto, en caso de que se implemente en un lenguaje administrado).

Los RegisterEffectFactory métodos y UnregisterEffectFactory también están diseñados para ser utilizados por efectos personalizados, de modo que también puedan registrar una devolución de llamada para crear un nuevo contenedor en caso de que un desarrollador intente resolver uno para un recurso D2D "huérfano":

  • RegisterEffectFactory: registra una devolución de llamada que toma los mismos parámetros que un desarrollador pasó a GetOrCreatey crea un nuevo contenedor inspeccionable para el efecto de entrada. El identificador de efecto se usa como clave, de modo que cada efecto personalizado pueda registrar un generador para él cuando se cargue por primera vez. Por supuesto, esto solo debe hacerse una vez por tipo de efecto y no cada vez que se realiza el efecto. Win2D comprueba los deviceparámetros , resource y wrapper antes de invocar cualquier devolución de llamada registrada, por lo que se garantiza que no sean cuando CreateWrapper se null invoca. dpi se considera opcional y se puede omitir en caso de que el tipo de efecto no tenga un uso específico para él. Tenga en cuenta que cuando se crea un nuevo contenedor a partir de una factoría registrada, esa factoría también debe asegurarse de que el nuevo contenedor está registrado en la memoria caché (Win2D no agregará automáticamente contenedores generados por fábricas externas a la memoria caché).
  • UnregisterEffectFactory: quita una devolución de llamada de registro anterior. Por ejemplo, esto podría usarse si se implementa un contenedor de efectos en un ensamblado administrado que se descarga.

Nota:

ICanvasFactoryNative se implementa mediante la factoría de activación para CanvasDevice, que puede recuperar mediante una llamada RoGetActivationFactorymanual a o mediante api auxiliares de las extensiones de lenguaje que está usando (por ejemplo winrt::get_activation_factory , en C++/WinRT). Para obtener más información, consulta Sistema de tipos winRT para obtener más información sobre cómo funciona.

Para obtener un ejemplo práctico de dónde entra en juego esta asignación, considere cómo funcionan los efectos win2D integrados. Si no se realizan, todo el estado (por ejemplo, propiedades, orígenes, etc.) se almacena en una caché interna en cada instancia de efecto. Cuando se realizan, todo el estado se transfiere al recurso nativo (por ejemplo, las propiedades se establecen en el efecto D2D, todos los orígenes se resuelven y se asignan a las entradas de efecto, etc.), y siempre y cuando se realice el efecto, actuará como autoridad en el estado del contenedor. Es decir, si el valor de cualquier propiedad se captura desde el contenedor, recuperará el valor actualizado del recurso D2D nativo asociado.

Esto garantiza que, si se realizan cambios directamente en el recurso D2D, también estarán visibles en el contenedor externo, y los dos nunca serán "sin sincronización". Cuando el efecto no se realiza, todo el estado se transfiere de nuevo del recurso nativo al estado contenedor, antes de que se libere el recurso. Se mantendrá y actualizará allí hasta la próxima vez que se realice el efecto. Ahora, tenga en cuenta esta secuencia de eventos:

  • Tienes algún efecto Win2D (integrado o personalizado).
  • Obtiene el ID2D1Image objeto de él (que es un ID2D1Effect).
  • Se crea una instancia de un efecto personalizado.
  • También obtienes de ID2D1Image eso.
  • Esta imagen se establece manualmente como entrada para el efecto anterior (a través de ID2D1Effect::SetInput).
  • A continuación, pide ese primer efecto para el contenedor de WinRT para esa entrada.

Dado que se realiza el efecto (se realizó cuando se solicitó el recurso nativo), usará el recurso nativo como origen de verdad. Por lo tanto, obtendrá el ID2D1Image correspondiente al origen solicitado e intentará recuperar el contenedor de WinRT para él. Si el efecto de que esta entrada se recuperó de ha agregado correctamente su propio par de recursos nativos y contenedor de WinRT a la memoria caché de Win2D, el contenedor se resolverá y devolverá a los autores de llamadas. Si no es así, se producirá un error en el acceso a la propiedad, ya que Win2D no puede resolver los contenedores de WinRT para efectos que no posee, ya que no sabe cómo crear instancias de ellos.

Aquí es donde RegisterWrapper y UnregisterWrapper ayudan, ya que permiten que los efectos personalizados participen sin problemas en la lógica de resolución del contenedor de Win2D, de modo que el contenedor correcto siempre se pueda recuperar para cualquier origen de efecto, independientemente de si se estableció desde las API de WinRT o directamente desde la capa D2D subyacente.

Para explicar cómo también entran en juego los generadores de efectos, tenga en cuenta este escenario:

  • Un usuario crea una instancia de un contenedor personalizado y se da cuenta de ello.
  • A continuación, obtiene una referencia al efecto D2D subyacente y lo mantiene.
  • A continuación, el efecto se realiza en un dispositivo diferente. El efecto no se realizará y volverá a darse cuenta, y al hacerlo creará un nuevo efecto D2D. El efecto D2D anterior ya no es un contenedor inspeccionable asociado en este momento.
  • A continuación, el usuario llama GetOrCreate al primer efecto D2D.

Sin una devolución de llamada, Win2D simplemente no resolvería un contenedor, ya que no hay ningún contenedor registrado para él. Si se registra un generador en su lugar, se puede crear y devolver un nuevo contenedor para ese efecto D2D, por lo que el escenario solo funciona sin problemas para el usuario.

Implementación de un personalizado ICanvasEffect

La interfaz Win2D ICanvasEffect extiende , por lo que todos los puntos anteriores también se aplican a efectos personalizados ICanvasImage. La única diferencia es el hecho de que ICanvasEffect también implementa métodos adicionales específicos de los efectos, como invalidar un rectángulo de origen, obtener los rectángulos necesarios, etc.

Para admitir esto, Win2D expone las exportaciones de C que los autores de efectos personalizados pueden usar, por lo que no tendrán que volver a implementar toda esta lógica adicional desde cero. Esto funciona de la misma manera que la exportación de C para GetBounds. Estas son las exportaciones disponibles para efectos:

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);

Veamos cómo se pueden usar:

  • InvalidateSourceRectangleForICanvasImageInterop está diseñado para admitir InvalidateSourceRectangle. Simplemente serializa los parámetros de entrada e invoque directamente, y se encargará de todo el trabajo necesario. Tenga en cuenta que el image parámetro es la instancia de efecto actual que se está implementando.
  • GetInvalidRectanglesForICanvasImageInterop admite GetInvalidRectangles. Esto tampoco requiere consideración especial, aparte de tener que eliminar la matriz COM devuelta una vez que ya no sea necesaria.
  • GetRequiredSourceRectanglesForICanvasImageInterop es un método compartido que puede admitir y GetRequiredSourceRectangle GetRequiredSourceRectangles. Es decir, toma un puntero a una matriz de valores existente para rellenar, por lo que los llamadores pueden pasar un puntero a un único valor (que también puede estar en la pila, para evitar una asignación) o a una matriz de valores. La implementación es la misma en ambos casos, por lo que una única exportación de C es suficiente para alimentar ambos.

Efectos personalizados en C# mediante ComputeSharp

Como hemos mencionado, si usa C# y quiere implementar un efecto personalizado, el enfoque recomendado es usar la biblioteca ComputeSharp . Permite implementar sombreadores de píxeles D2D1 personalizados completamente en C#, así como para definir fácilmente gráficos de efectos personalizados compatibles con Win2D. La misma biblioteca también se usa en Microsoft Store para alimentar varios componentes gráficos en la aplicación.

Puede agregar una referencia a ComputeSharp en el proyecto a través de NuGet:

Nota:

Muchas API de ComputeSharp.D2D1.* son idénticas en los destinos de UWP y WinAppSDK, la única diferencia es el espacio de nombres (que termina en .Uwp o .WinUI). Sin embargo, el objetivo de UWP está en mantenimiento sostenido y no recibe nuevas características. Por lo tanto, es posible que se necesiten algunos cambios de código en comparación con los ejemplos que se muestran aquí para WinUI. Los fragmentos de código de este documento reflejan la superficie de API a partir de ComputeSharp.D2D1.WinUI 3.0.0 (la última versión del destino de UWP es 2.1.0).

Hay dos componentes principales en ComputeSharp para interoperabilidad con Win2D:

  • PixelShaderEffect<T>: un efecto Win2D que funciona con un sombreador de píxeles D2D1. El propio sombreador se escribe en C# mediante las API proporcionadas por ComputeSharp. Esta clase también proporciona propiedades para establecer orígenes de efecto, valores constantes, etc.
  • CanvasEffect: una clase base para efectos Win2D personalizados que encapsula un gráfico de efectos arbitrarios. Se puede usar para "empaquetar" efectos complejos en un objeto fácil de usar que se puede reutilizar en varias partes de una aplicación.

Este es un ejemplo de un sombreador de píxeles personalizado (migrado desde este sombreador de sombreador), que se usa con PixelShaderEffect<T> y, a continuación, dibuja en un Win2D CanvasControl (tenga en cuenta que PixelShaderEffect<T> implementa ICanvasImage):

un sombreador de píxeles de ejemplo que muestra hexágonos de color infinito, que se dibuja en un control Win2D y se muestra en ejecución en una ventana de la aplicación

Puedes ver cómo en solo dos líneas de código puedes crear un efecto y dibujarlo a través de Win2D. ComputeSharp se encarga del trabajo necesario para compilar el sombreador, registrarlo y administrar la duración compleja de un efecto compatible con Win2D.

A continuación, veamos una guía paso a paso sobre cómo crear un efecto Win2D personalizado que también usa un sombreador de píxeles D2D1 personalizado. Veremos cómo crear un sombreador con ComputeSharp y configurar sus propiedades y, a continuación, cómo crear un gráfico de efectos personalizado empaquetado en un CanvasEffect tipo que se pueda reutilizar fácilmente en la aplicación.

Diseño del efecto

Para esta demostración, queremos crear un efecto simple de vidrio escarchado.

Esto incluirá los siguientes componentes:

  • Desenfoque gaussiano
  • Efecto de tono
  • Ruido (que podemos generar con un sombreador)

También queremos exponer propiedades para controlar el desenfoque y la cantidad de ruido. El efecto final contendrá una versión "empaquetada" de este gráfico de efectos y será fácil de usar mediante la creación de una instancia, la configuración de esas propiedades, la conexión de una imagen de origen y el dibujo. Comencemos.

Creación de un sombreador de píxeles D2D1 personalizado

Para el ruido sobre el efecto, podemos usar un sombreador de píxeles D2D1 simple. El sombreador calculará un valor aleatorio en función de sus coordenadas (que actuará como "inicialización" para el número aleatorio) y, a continuación, usará ese valor de ruido para calcular la cantidad RGB de ese píxel. A continuación, podemos mezclar este ruido sobre la imagen resultante.

Para escribir el sombreador con ComputeSharp, solo tenemos que definir un partial struct tipo que implemente la ID2D1PixelShader interfaz y, a continuación, escribir nuestra lógica en el Execute método . Para este sombreador de ruido, podemos escribir algo parecido a esto:

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);
    }
}

Nota:

Aunque el sombreador se escribe completamente en C#, se recomienda el conocimiento básico de HLSL (el lenguaje de programación para sombreadores DirectX, al que ComputeSharp transpila C#).

Vamos a repasar este sombreador con detalle:

  • El sombreador no tiene entradas, solo genera una imagen infinita con ruido aleatorio de escala de grises.
  • El sombreador requiere acceso a la coordenada de píxel actual.
  • El sombreador se precompila en tiempo de compilación (mediante el PixelShader40 perfil, que se garantiza que esté disponible en cualquier GPU en la que se pueda ejecutar la aplicación).
  • El [D2DGeneratedPixelShaderDescriptor] atributo es necesario para desencadenar el generador de origen agrupado con ComputeSharp, que analizará el código de C#, lo transpile a HLSL, compilará el sombreador en código de bytes, etc.
  • El sombreador captura un float amount parámetro a través de su constructor principal. El generador de origen de ComputeSharp se encargará automáticamente de extraer todos los valores capturados en un sombreador y preparar el búfer de constantes que D2D necesita para inicializar el estado del sombreador.

¡Y esta parte se hace! Este sombreador generará nuestra textura de ruido personalizada siempre que sea necesario. A continuación, necesitamos crear nuestro efecto empaquetado con el gráfico de efectos que conecta todos nuestros efectos juntos.

Creación de un efecto personalizado

Para que sea fácil de usar, efecto empaquetado, podemos usar el CanvasEffect tipo de ComputeSharp. Este tipo proporciona una manera sencilla de configurar toda la lógica necesaria para crear un gráfico de efectos y actualizarlo a través de propiedades públicas con las que los usuarios del efecto pueden interactuar. Hay dos métodos principales que necesitaremos implementar:

  • BuildEffectGraph: este método es responsable de crear el gráfico de efectos que queremos dibujar. Es decir, debe crear todos los efectos que necesitamos y registrar el nodo de salida del grafo. Para los efectos que se pueden actualizar más adelante, el registro se realiza con un valor asociado CanvasEffectNode<T> , que actúa como clave de búsqueda para recuperar los efectos del gráfico cuando sea necesario.
  • ConfigureEffectGraph: este método actualiza el gráfico de efectos aplicando la configuración que el usuario ha configurado. Este método se invoca automáticamente cuando es necesario, justo antes de dibujar el efecto, y solo si se ha modificado al menos una propiedad de efecto desde la última vez que se usó el efecto.

Nuestro efecto personalizado se puede definir de la siguiente manera:

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);
    }
}

Puede ver que hay cuatro secciones en esta clase:

  • En primer lugar, tenemos campos para realizar un seguimiento de todo el estado mutable, como los efectos que se pueden actualizar, así como los campos de respaldo de todas las propiedades de efecto que queremos exponer a los usuarios del efecto.
  • A continuación, tenemos propiedades para configurar el efecto. El establecedor de cada propiedad usa el SetAndInvalidateEffectGraph método expuesto por CanvasEffect, que invalidará automáticamente el efecto si el valor que se establece es diferente al actual. Esto garantiza que el efecto solo se configure de nuevo cuando sea realmente necesario.
  • Por último, tenemos los BuildEffectGraph métodos y ConfigureEffectGraph mencionados anteriormente.

Nota:

El PremultiplyEffect nodo después del efecto de ruido es muy importante: esto se debe a que los efectos win2D suponen que la salida está premultipada, mientras que los sombreadores de píxeles suelen funcionar con píxeles no premultiplicados. Por lo tanto, recuerde insertar manualmente nodos premultiply/unpremultiply antes y después de los sombreadores personalizados, para asegurarse de que los colores se conservan correctamente.

Nota:

Este efecto de ejemplo usa espacios de nombres winUI 3, pero también se puede usar el mismo código en UWP. En ese caso, el espacio de nombres de ComputeSharp será ComputeSharp.Uwp, que coincide con el nombre del paquete.

¡Listo para dibujar!

Y con esto, nuestro efecto personalizado de vidrio escarchado está listo! Podemos dibujarlo fácilmente de la siguiente manera:

private void CanvasControl_Draw(CanvasControl sender, CanvasDrawEventArgs args)
{
    FrostedGlassEffect effect = new()
    {
        Source = _canvasBitmap,
        BlurAmount = 12,
        NoiseAmount = 0.1
    };

    args.DrawingSession.DrawImage(effect);
}

En este ejemplo, estamos dibujando el efecto del Draw controlador de , CanvasControlmediante un CanvasBitmap que se cargó previamente como origen. Esta es la imagen de entrada que usaremos para probar el efecto:

una imagen de algunas montañas bajo un cielo nublado

Y este es el resultado:

una versión borrosa de la imagen anterior

Nota:

Créditos para Dominic Lange para la imagen.

Recursos adicionales

  • Consulte el código fuente de Win2D para obtener más información.
  • Para obtener más información sobre ComputeSharp, consulte las aplicaciones de ejemplo y las pruebas unitarias.