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
, ,CanvasRenderTarget
VirtualizedCanvasBitmap
,CanvasEffect
TintEffect
ICanvasLuminanceToAlphaEffectImage
GaussianBlurEffect
,IGraphicsEffectSource
, , ,ID2D1Factory1
ID2D21Image
ID2D1Effect
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 establecerseWIN2D_GET_DEVICE_ASSOCIATION_TYPE_REALIZATION_DEVICE
en 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 entype
WIN2D_GET_DEVICE_ASSOCIATION_TYPE_CREATION_DEVICE
. Tenga en cuenta que cuando se especifica este tipo, el dispositivo devuelto no debe sernull
.
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 efectoB
como origen y el efectoB
tiene efectoA
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 seanull
. El efecto debe comprobar si el destino de contexto del dispositivo es unID2D1CommandList
y, si es así, agregue laWIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION
marca . De lo contrario, se debe establecertargetDpi
(que también se garantiza que no seanull
) en los DPIs recuperados del contexto de entrada. A continuación, debe quitarseWIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT
de las marcas.WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION
yWIN2D_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 sernull
, 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 elinputDpi
parámetro no0
sea . - 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 bienWIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION
se establece el valor de PPP de entrada y los valores de PPP de0
destino 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. Elwrapper
parámetro es necesario para también implemementIWeakReferenceSource
, de modo que se pueda almacenar en caché correctamente sin causar ciclos de referencia que provocarían pérdidas de memoria. El método devuelveS_OK
si el recurso nativo se podría agregar a la memoria caché,S_FALSE
si ya había un contenedor registrado pararesource
y un código de error si se produce un error.UnregisterWrapper
: anula el registro de un recurso nativo y su contenedor. DevuelveS_OK
si el recurso se podría quitar,S_FALSE
siresource
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ó aGetOrCreate
y 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 losdevice
parámetros ,resource
ywrapper
antes de invocar cualquier devolución de llamada registrada, por lo que se garantiza que no sean cuandoCreateWrapper
senull
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 RoGetActivationFactory
manual 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 unID2D1Effect
). - 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 admitirInvalidateSourceRectangle
. Simplemente serializa los parámetros de entrada e invoque directamente, y se encargará de todo el trabajo necesario. Tenga en cuenta que elimage
parámetro es la instancia de efecto actual que se está implementando.GetInvalidRectanglesForICanvasImageInterop
admiteGetInvalidRectangles
. 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 yGetRequiredSourceRectangle
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:
- En UWP, seleccione el paquete ComputeSharp.D2D1.Uwp.
- En WinAppSDK, seleccione el paquete ComputeSharp.D2D1.WinUI.
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
):
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 asociadoCanvasEffectNode<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 porCanvasEffect
, 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 yConfigureEffectGraph
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 , CanvasControl
mediante un CanvasBitmap
que se cargó previamente como origen. Esta es la imagen de entrada que usaremos para probar el efecto:
Y este es el resultado:
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.