Condividi tramite


Implementazione di effetti personalizzati

Win2D offre diverse API per rappresentare gli oggetti che possono essere disegnati, suddivisi in due categorie: immagini ed effetti. Le immagini, rappresentate ICanvasImage dall'interfaccia, non hanno input e possono essere disegnate direttamente su una determinata superficie. Ad esempio, CanvasBitmap, VirtualizedCanvasBitmap e CanvasRenderTarget sono esempi di tipi di immagine. Gli effetti, d'altra parte, sono rappresentati ICanvasEffect dall'interfaccia. Possono avere input e risorse aggiuntive e possono applicare logica arbitraria per produrre i relativi output (come effetto è anche un'immagine). Win2D include effetti che includono la maggior parte degli effetti D2D, ad esempio GaussianBlurEffect, TintEffect e LuminanceToAlphaEffect.

Le immagini e gli effetti possono anche essere concatenati per creare grafici arbitrari che possono quindi essere visualizzati nell'applicazione (vedere anche la documentazione D2D sugli effetti Direct2D). Insieme, forniscono un sistema estremamente flessibile per creare grafica complessa in modo efficiente. Tuttavia, ci sono casi in cui gli effetti predefiniti non sono sufficienti e potresti voler creare il tuo effetto Win2D molto personale. Per supportare questo problema, Win2D include un set di potenti API di interoperabilità che consentono di definire immagini e effetti personalizzati che possono integrarsi facilmente con Win2D.

Suggerimento

Se si usa C# e si vuole implementare un grafico di effetto o effetto personalizzato, è consigliabile usare ComputeSharp anziché provare a implementare un effetto da zero. Vedere il paragrafo seguente per una spiegazione dettagliata di come usare questa libreria per implementare effetti personalizzati che si integrano perfettamente con Win2D.

API della piattaforma: ICanvasImage, CanvasBitmap, VirtualizedCanvasBitmapCanvasRenderTarget, CanvasEffect, , IGraphicsEffectSource, GaussianBlurEffectID2D1Factory1TintEffectICanvasLuminanceToAlphaEffectImageID2D21ImageID2D1Effect

Implementazione di un personalizzato ICanvasImage

Lo scenario più semplice da supportare consiste nel creare un oggetto personalizzato ICanvasImage. Come accennato, questa è l'interfaccia WinRT definita da Win2D che rappresenta tutti i tipi di immagini con cui Win2D può interagire. Questa interfaccia espone solo due GetBounds metodi ed estende IGraphicsEffectSource, che è un'interfaccia marcatore che rappresenta "un'origine dell'effetto".

Come si può notare, non ci sono API "funzionali" esposte da questa interfaccia per eseguire effettivamente qualsiasi disegno. Per implementare il proprio ICanvasImage oggetto, è necessario implementare anche ICanvasImageInterop l'interfaccia, che espone tutta la logica necessaria per Win2D per disegnare l'immagine. Si tratta di un'interfaccia COM Microsoft.Graphics.Canvas.native.h definita nell'intestazione pubblica, fornita con Win2D.

L'interfaccia viene definita come segue:

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

E si basa anche su questi due tipi di enumerazione, dalla stessa intestazione:

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
}

I due GetDevice metodi e GetD2DImage sono tutti necessari per implementare immagini personalizzate (o effetti), poiché forniscono a Win2D i punti di estendibilità per inizializzarli in un determinato dispositivo e recuperare l'immagine D2D sottostante da disegnare. L'implementazione di questi metodi correttamente è fondamentale per garantire che le operazioni funzionino correttamente in tutti gli scenari supportati.

Esaminiamoli per vedere come funziona ogni metodo.

Implementazione GetDevice

Il GetDevice metodo è il più semplice dei due. Ciò che fa è che recupera il dispositivo canvas associato all'effetto, in modo che Win2D possa esaminarlo se necessario (ad esempio, per assicurarsi che corrisponda al dispositivo in uso). Il type parametro indica il "tipo di associazione" per il dispositivo restituito.

Esistono due casi principali:

  • Se l'immagine è un effetto, deve supportare l'essere "realizzati" e "non realizzati" su più dispositivi. Ciò significa che un determinato effetto viene creato in uno stato non inizializzato, quindi può essere realizzato quando un dispositivo viene passato durante il disegno e dopo che può continuare a essere usato con tale dispositivo oppure può essere spostato in un dispositivo diverso. In tal caso, l'effetto reimposta lo stato interno e quindi si rende nuovamente conto sul nuovo dispositivo. Ciò significa che il dispositivo canvas associato può cambiare nel tempo e può anche essere null. Per questo motivo, type deve essere impostato su WIN2D_GET_DEVICE_ASSOCIATION_TYPE_REALIZATION_DEVICEe il dispositivo restituito deve essere impostato sul dispositivo di realizzazione corrente, se disponibile.
  • Alcune immagini hanno un singolo "dispositivo proprietario" assegnato in fase di creazione e non può mai cambiare. Ad esempio, questo è il caso di un'immagine che rappresenta una trama, in quanto allocata in un dispositivo specifico e non può essere spostata. Quando GetDevice viene chiamato, deve restituire il dispositivo di creazione e impostare type su WIN2D_GET_DEVICE_ASSOCIATION_TYPE_CREATION_DEVICE. Si noti che quando si specifica questo tipo, il dispositivo restituito non deve essere null.

Nota

Win2D può chiamare GetDevice durante l'attraversamento ricorsivo di un grafico degli effetti, ovvero potrebbero esserci più chiamate attive a GetD2DImage nello stack. Per questo motivo, GetDevice non dovrebbe accettare un blocco sull'immagine corrente, in quanto ciò potrebbe potenzialmente causare un deadlock. Deve invece usare un blocco di nuovo partecipante in modo non bloccante e restituire un errore se non può essere acquisito. In questo modo si garantisce che lo stesso thread che lo chiamerà in modo ricorsivo lo acquisirà correttamente, mentre i thread simultanei che eseguono la stessa operazione avranno esito negativo normalmente.

Implementazione GetD2DImage

GetD2DImage è dove si svolge la maggior parte del lavoro. Questo metodo è responsabile del recupero dell'oggetto ID2D1Image che Win2D può disegnare, rendendo facoltativamente l'effetto corrente, se necessario. Ciò include anche l'attraversamento ricorsivo e la realizzazione del grafico degli effetti per tutte le origini, se presenti, nonché l'inizializzazione di qualsiasi stato che potrebbe essere necessaria per l'immagine (ad esempio buffer costanti e altre proprietà, trame di risorse e così via).

L'implementazione esatta di questo metodo dipende molto dal tipo di immagine e può variare molto, ma in genere per un effetto arbitrario è possibile prevedere che il metodo esegua i passaggi seguenti:

  • Controllare se la chiamata è stata ricorsiva nella stessa istanza e, in caso affermativo, ha esito negativo. Ciò è necessario per rilevare i cicli in un grafico degli effetti (ad esempio, A l'effetto B ha effetto come origine e l'effetto B ha effetto A come origine).
  • Acquisire un blocco sull'istanza dell'immagine per proteggersi dall'accesso simultaneo.
  • Gestire i DPI di destinazione in base ai flag di input
  • Verificare se il dispositivo di input corrisponde a quello in uso, se presente. Se non corrisponde e l'effetto corrente supporta la realizzazione, l'effetto non è stato realizzato.
  • Realizzare l'effetto sul dispositivo di input. Ciò può includere la registrazione dell'effetto D2D sull'oggetto ID2D1Factory1 recuperato dal dispositivo di input o dal contesto di dispositivo, se necessario. Inoltre, tutto lo stato necessario deve essere impostato sull'istanza dell'effetto D2D in fase di creazione.
  • Attraversa in modo ricorsivo tutte le origini e le associa all'effetto D2D.

Per quanto riguarda i flag di input, esistono diversi casi possibili che gli effetti personalizzati debbano gestire correttamente, per garantire la compatibilità con tutti gli altri effetti Win2D. Escludendo WIN2D_GET_D2D_IMAGE_FLAGS_NONE, ti flag da gestire sono i seguenti:

  • WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT: in questo caso, device non è garantito che sia null. L'effetto deve verificare se la destinazione del contesto di dispositivo è un oggetto ID2D1CommandListe, in tal caso, aggiungere il WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION flag. In caso contrario, deve essere impostato targetDpi (che è garantito anche non essere null) sui DPI recuperati dal contesto di input. Quindi, dovrebbe essere rimosso WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT dai flag.
  • WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION e WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION: usato durante l'impostazione delle origini degli effetti (vedere le note seguenti).
  • WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION: se impostato, ignora in modo ricorsivo la realizzazione delle origini dell'effetto e restituisce solo l'effetto realizzato senza altre modifiche.
  • WIN2D_GET_D2D_IMAGE_FLAGS_ALLOW_NULL_EFFECT_INPUTS: se impostata, le origini degli effetti rese effettive possono essere null, se l'utente non le ha ancora impostate su un'origine esistente.
  • WIN2D_GET_D2D_IMAGE_FLAGS_UNREALIZE_ON_FAILURE: se impostato e un'origine dell'effetto impostata non è valida, l'effetto deve essere annullato prima dell'esito negativo. Ovvero, se l'errore si è verificato durante la risoluzione delle origini dell'effetto dopo aver realizzato l'effetto, l'effetto deve essere annullato prima di restituire l'errore al chiamante.

Per quanto riguarda i flag correlati a DPI, questi controllano il modo in cui vengono impostate le origini degli effetti. Per garantire la compatibilità con Win2D, gli effetti devono aggiungere automaticamente effetti di compensazione DPI agli input, quando necessario. ossono controllare se questo è il caso in questo modo:

  • Se WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION è impostato, è necessario un effetto di compensazione DPI ogni volta che il inputDpi parametro non è 0.
  • In caso contrario, la compensazione DPI è necessaria se inputDpi non è 0, WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION non è impostata e WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION viene impostata o i valori DPI di input e i valori DPI di destinazione 0non corrispondono.

Questa logica deve essere applicata ogni volta che viene realizzata un'origine e associata a un input dell'effetto corrente. Si noti che se viene aggiunto un effetto di compensazione DPI, deve essere l'input impostato sull'immagine D2D sottostante. Tuttavia, se l'utente tenta di recuperare il wrapper WinRT per tale origine, l'effetto deve prestare attenzione a rilevare se è stato usato un effetto DPI e restituire invece un wrapper per l'oggetto di origine originale. Vale a dire, gli effetti di compensazione DPI devono essere trasparenti agli utenti dell'effetto.

Dopo aver completato tutta la logica di inizializzazione, il risultato ID2D1Image (proprio come con gli oggetti Win2D, un effetto D2D è anche un'immagine) deve essere pronto per essere disegnato da Win2D nel contesto di destinazione, che non è ancora noto dal chiamato in questo momento.

Nota

L'implementazione corretta di questo metodo (e ICanvasImageInterop in generale) è estremamente complessa ed è destinata solo agli utenti avanzati che necessitano assolutamente di maggiore flessibilità. È consigliabile una conoscenza approfondita di D2D, Win2D, COM, WinRT e C++ prima di provare a scrivere ICanvasImageInterop un'implementazione. Se anche l'effetto Win2D personalizzato deve eseguire il wrapping di un effetto D2D personalizzato, dovrai implementare anche il tuo ID2D1Effect oggetto (vedi la documentazione D2D sugli effetti personalizzati per altre info su questo). Questi documenti non sono una descrizione completa di tutta la logica necessaria (ad esempio, non illustrano il modo in cui le origini degli effetti devono essere gestite e gestite attraverso il limite D2D/Win2D), quindi è consigliabile usare anche CanvasEffect l'implementazione nella codebase di Win2D come punto di riferimento per un effetto personalizzato e modificarla in base alle esigenze.

Implementazione GetBounds

L'ultimo componente mancante per implementare completamente un effetto personalizzato ICanvasImage consiste nel supportare i due GetBounds overload. Per semplificare questa operazione, Win2D espone un'esportazione C che può essere usata per sfruttare la logica esistente per questo da Win2D in qualsiasi immagine personalizzata. L'esportazione è la seguente:

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

Le immagini personalizzate possono richiamare questa API e passarsi come image parametro e quindi restituire semplicemente il risultato ai chiamanti. Il transform parametro può essere null, non è disponibile alcuna trasformazione.

Ottimizzazione degli accessi al contesto di dispositivo

Il deviceContext parametro in ICanvasImageInterop::GetD2DImage può talvolta essere null, se un contesto non è immediatamente disponibile prima della chiamata. Questa operazione viene eseguita a scopo, in modo che un contesto venga creato in modo da differire solo quando è effettivamente necessario. Ovvero, se è disponibile un contesto, Win2D lo passerà alla GetD2DImage chiamata, altrimenti lascerà che i chiamati recupereranno uno autonomamente, se necessario..

La creazione di un contesto di dispositivo è relativamente costosa, quindi per rendere più veloce il recupero di una win2D espone le API per accedere al pool di contesto di dispositivo interno. Ciò consente agli effetti personalizzati di noleggiare e restituire contesti di dispositivo associati a un determinato dispositivo canvas in modo efficiente.

Le API di lease del contesto di dispositivo sono definite come segue:

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

L'interfaccia ID2D1DeviceContextPool viene implementata da CanvasDevice, ovvero il tipo Win2D che implementa ICanvasDevice l'interfaccia. Per usare il pool, usare QueryInterface nell'interfaccia del dispositivo per ottenere un ID2D1DeviceContextPool riferimento e quindi chiamare ID2D1DeviceContextPool::GetDeviceContextLease per ottenere un ID2D1DeviceContextLease oggetto per accedere al contesto di dispositivo. Una volta che non è più necessario, rilasciare il lease. Assicurarsi di non toccare il contesto del dispositivo dopo il rilascio del lease, perché potrebbe essere usato simultaneamente da altri thread.

Abilitazione della ricerca dei wrapper WinRT

Come illustrato nella documentazione di interoperabilità Win2D, l'intestazione pubblica Win2D espone anche un GetOrCreate metodo (accessibile dalla ICanvasFactoryNative factory di attivazione o tramite gli GetOrCreate Chelper C++/CX definiti nella stessa intestazione). In questo modo è possibile recuperare un wrapper WinRT da una determinata risorsa nativa. Ad esempio, consente di recuperare o creare un'istanza CanvasDevice da un ID2D1Device1 oggetto, da CanvasBitmap un oggetto e ID2D1Bitmapcosì via.

Questo metodo funziona anche per tutti gli effetti Win2D predefiniti: il recupero della risorsa nativa per un determinato effetto e quindi l'uso di tale per recuperare il wrapper Win2D corrispondente restituirà correttamente l'effetto Win2D proprietario per esso. Affinché gli effetti personalizzati traggano vantaggio anche dallo stesso sistema di mapping, Win2D espone diverse API nell'interfaccia di interoperabilità per la factory di attivazione per CanvasDevice, ovvero il ICanvasFactoryNative tipo, nonché un'interfaccia di factory per effetti aggiuntiva, 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);
};

Esistono diverse API da considerare qui, perché sono necessarie per supportare tutti i vari scenari in cui è possibile usare effetti Win2D, nonché come gli sviluppatori potrebbero eseguire l'interoperabilità con il livello D2D e quindi provare a risolvere i wrapper per loro. Esaminiamo ognuna di queste API.

I RegisterWrapper metodi e UnregisterWrapper devono essere richiamati da effetti personalizzati per aggiungersi alla cache Win2D interna:

  • RegisterWrapper: registra una risorsa nativa e il wrapper WinRT proprietario. Il wrapper parametro è necessario anche per implemementare IWeakReferenceSource, in modo che possa essere memorizzato correttamente nella cache senza causare cicli di riferimento che causano perdite di memoria. Il metodo restituisce S_OK se la risorsa nativa può essere aggiunta alla cache, S_FALSE se è già presente un wrapper registrato per resourcee un codice di errore se si verifica un errore.
  • UnregisterWrapper: annulla la registrazione di una risorsa nativa e del relativo wrapper. Restituisce S_OK se la risorsa può essere rimossa, S_FALSE se resource non è già stata registrata e un codice erro se si verifica un altro errore.

Gli effetti personalizzati devono chiamare RegisterWrapper e UnregisterWrapper ogni volta che vengono realizzati e non realizzati, ovvero quando viene creata e associata una nuova risorsa nativa. Gli effetti personalizzati che non supportano la realizzazione (ad esempio quelli con un dispositivo associato fisso) possono chiamare RegisterWrapper e UnregisterWrapper quando vengono creati e distrutti. Gli effetti personalizzati devono assicurarsi di annullare correttamente la registrazione da tutti i possibili percorsi di codice che causano la mancata invalidità del wrapper, ad esempio quando l'oggetto viene finalizzato, nel caso in cui venga implementato in un linguaggio gestito).

I RegisterEffectFactory metodi e UnregisterEffectFactory devono essere usati anche da effetti personalizzati, in modo che possano anche registrare un callback per creare un nuovo wrapper nel caso in cui uno sviluppatore tenti di risolverlo per una risorsa D2D "orfana":

  • RegisterEffectFactory: registra un callback che accetta gli stessi parametri passati da uno sviluppatore a GetOrCreatee crea un nuovo wrapper ispezionabile per l'effetto di input. L'id effetto viene usato come chiave, in modo che ogni effetto personalizzato possa registrare una factory al primo caricamento. Naturalmente, questo dovrebbe essere fatto solo una volta per tipo di effetto, e non ogni volta che l'effetto viene realizzato. I device, resource parametri e wrapper vengono controllati da Win2D prima di richiamare qualsiasi callback registrato, quindi è garantito che non siano null quando CreateWrapper viene richiamato. È dpi considerato facoltativo e può essere ignorato nel caso in cui il tipo di effetto non abbia un uso specifico per esso. Si noti che quando viene creato un nuovo wrapper da una factory registrata, tale factory deve anche assicurarsi che il nuovo wrapper sia registrato nella cache (Win2D non aggiungerà automaticamente wrapper prodotti da factory esterne alla cache).
  • UnregisterEffectFactory: rimuove un callback del registro in precedenza. Ad esempio, questa operazione può essere usata se un wrapper di effetto viene implementato in un assembly gestito che viene scaricato.

Nota

ICanvasFactoryNative viene implementato dalla factory di attivazione per CanvasDevice, che è possibile recuperare chiamando manualmente RoGetActivationFactoryo usando le API helper dalle estensioni del linguaggio in uso (ad esempio winrt::get_activation_factory in C++/WinRT). Per altre info, vedi Sistema di tipi WinRT per altre informazioni sul funzionamento.

Per un esempio pratico di dove entra in gioco questo mapping, considerare il funzionamento degli effetti Win2D predefiniti. Se non vengono realizzati, tutti gli stati ,ad esempio proprietà, origini e così via, vengono archiviati in una cache interna in ogni istanza dell'effetto. Quando vengono realizzati, tutti gli stati vengono trasferiti alla risorsa nativa (ad esempio, le proprietà vengono impostate sull'effetto D2D, tutte le origini vengono risolte e mappate agli input degli effetti e così via) e, purché l'effetto venga realizzato, fungerà da autorità sullo stato del wrapper. Ovvero, se il valore di una proprietà viene recuperato dal wrapper, recupererà il valore aggiornato dalla risorsa D2D nativa associata.

In questo modo, se vengono apportate modifiche direttamente alla risorsa D2D, queste saranno visibili anche nel wrapper esterno e le due non saranno mai "sincronizzate". Quando l'effetto non viene realizzato, tutto lo stato viene trasferito di nuovo dalla risorsa nativa allo stato wrapper, prima del rilascio della risorsa. Verrà mantenuto e aggiornato lì fino alla successiva realizzazione dell'effetto. Si consideri ora questa sequenza di eventi:

  • Hai un effetto Win2D (predefinito o personalizzato).
  • Si ottiene da ID2D1Image esso (che è un ID2D1Effect).
  • Si crea un'istanza di un effetto personalizzato.
  • Si ottiene anche da ID2D1Image questo.
  • Questa immagine viene impostata manualmente come input per l'effetto precedente (tramite ID2D1Effect::SetInput).
  • Chiedi quindi il primo effetto per il wrapper WinRT per l'input.

Poiché l'effetto viene realizzato (è stato realizzato quando è stata richiesta la risorsa nativa), userà la risorsa nativa come origine della verità. Di conseguenza, otterrà il ID2D1Image corrispondente all'origine richiesta e tenterà di recuperare il wrapper WinRT per esso. Se l'effetto da cui è stato recuperato questo input ha aggiunto correttamente la propria coppia di risorse native e il wrapper WinRT alla cache di Win2D, il wrapper verrà risolto e restituito ai chiamanti. In caso contrario, l'accesso alla proprietà avrà esito negativo, perché Win2D non riesce a risolvere i wrapper WinRT per gli effetti di cui non è proprietario, perché non sa come crearne un'istanza.

Questo è il punto RegisterWrapper e UnregisterWrapper l'aiuto, in quanto consentono agli effetti personalizzati di partecipare senza problemi alla logica di risoluzione del wrapper win2D, in modo che il wrapper corretto possa sempre essere recuperato per qualsiasi origine dell'effetto, indipendentemente dal fatto che sia stato impostato dalle API WinRT o direttamente dal livello D2D sottostante.

Per spiegare anche come entra in gioco le factory degli effetti, considerare questo scenario:

  • Un utente crea un'istanza di un wrapper personalizzato e si rende conto
  • Ottiene quindi un riferimento all'effetto D2D sottostante e lo mantiene.
  • L'effetto viene quindi realizzato su un dispositivo diverso. L'effetto verrà realizzato e realizzato di nuovo, e in questo modo creerà un nuovo effetto D2D. L'effetto D2D precedente non è più un wrapper ispezionabile associato a questo punto.
  • L'utente chiama GetOrCreate quindi il primo effetto D2D.

Senza un callback, Win2D non riesce a risolvere un wrapper, perché non è presente alcun wrapper registrato. Se invece una factory è registrata, è possibile creare e restituire un nuovo wrapper per tale effetto D2D, quindi lo scenario continua a funzionare senza problemi per l'utente.

Implementazione di un personalizzato ICanvasEffect

L'interfaccia Win2D ICanvasEffect estende ICanvasImage, quindi tutti i punti precedenti si applicano anche agli effetti personalizzati. L'unica differenza è il fatto che ICanvasEffect implementa anche metodi aggiuntivi specifici per gli effetti, ad esempio invalidando un rettangolo di origine, ottenendo i rettangoli necessari e così via.

Per supportare questo problema, Win2D espone le esportazioni C che gli autori di effetti personalizzati possono usare, in modo che non dovranno riapplicare tutta questa logica aggiuntiva da zero. Questa operazione funziona allo stesso modo dell'esportazione C per GetBounds. Ecco le esportazioni disponibili per gli effetti:

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

Esaminiamo come possono essere usati:

  • InvalidateSourceRectangleForICanvasImageInterop è progettato per supportare InvalidateSourceRectangle. È sufficiente effettuare il marshalling dei parametri di input e richiamarlo direttamente e si occuperà di tutto il lavoro necessario. Si noti che il image parametro è l'istanza dell'effetto corrente implementata.
  • GetInvalidRectanglesForICanvasImageInterop supporta GetInvalidRectangles. Questo non richiede inoltre considerazioni particolari, oltre alla necessità di eliminare la matrice COM restituita, una volta che non è più necessaria.
  • GetRequiredSourceRectanglesForICanvasImageInterop è un metodo condiviso che può supportare sia GetRequiredSourceRectangle che GetRequiredSourceRectangles. Ovvero, richiede un puntatore a una matrice di valori esistente da popolare, in modo che i chiamanti possano passare un puntatore a un singolo valore (che può anche trovarsi nello stack, per evitare un'allocazione) o a una matrice di valori. L'implementazione è la stessa in entrambi i casi, quindi una singola esportazione C è sufficiente per supportare entrambe.

Effetti personalizzati in C# con ComputeSharp

Come accennato, se si usa C# e si vuole implementare un effetto personalizzato, l'approccio consigliato consiste nell'usare la libreria ComputeSharp. Consente di implementare i pixel shader D2D1 personalizzati interamente in C#, nonché di definire facilmente grafici di effetti personalizzati compatibili con Win2D. La stessa libreria viene usata anche in Microsoft Store per alimentare diversi componenti grafici nell'applicazione.

È possibile aggiungere un riferimento a ComputeSharp nel progetto tramite NuGet:

  • Nella piattaforma UWP selezionare il pacchetto ComputeSharp.D2D1.Uwp.
  • In WinAppSDK selezionare il pacchetto ComputeSharp.D2D1.WinUI.

Nota

Molte API in ComputeSharp.D2D1.* sono identiche nelle destinazioni UWP e WinAppSDK, l'unica differenza è lo spazio dei nomi (che termina con .Uwp o .WinUI). Tuttavia, la destinazione UWP è in manutenzione sostenuta e non riceve nuove funzionalità. Di conseguenza, alcune modifiche al codice potrebbero essere necessarie rispetto agli esempi illustrati qui per WinUI. I frammenti di codice in questo documento riflettono la superficie dell'API a partire da ComputeSharp.D2D1.WinUI 3.0.0 (l'ultima versione per la destinazione UWP è invece 2.1.0).

Esistono due componenti principali in ComputeSharp per l'interoperabilità con Win2D:

  • PixelShaderEffect<T>: un effetto Win2D alimentato da uno shader pixel D2D1. Lo shader stesso viene scritto in C# usando le API fornite da ComputeSharp.. Questa classe fornisce anche proprietà per impostare origini degli effetti, valori costanti e altro ancora.
  • CanvasEffect: una classe base per effetti Win2D personalizzati che esegue il wrapping di un grafico di effetto arbitrario. Può essere usato per "creare pacchetti" effetti complessi in un oggetto facile da usare che può essere riutilizzato in diverse parti di un'applicazione.

Di seguito è riportato un esempio di pixel shader personalizzato (convertito da questo shadertoy shader), usato con PixelShaderEffect<T> e quindi disegnare in un win2D CanvasControl (si noti che PixelShaderEffect<T> implementa ICanvasImage):

un pixel shader di esempio che visualizza esagoni colorati infiniti, disegnati su un controllo Win2D e visualizzati in esecuzione in una finestra dell'app

Puoi vedere come in due righe di codice puoi creare un effetto e disegnarlo tramite Win2D. ComputeSharp si occupa di tutto il lavoro necessario per compilare lo shader, registrarlo e gestire la durata complessa di un effetto compatibile con Win2D.

Verrà ora illustrata una guida dettagliata su come creare un effetto Win2D personalizzato che usa anche un pixel shader D2D1 personalizzato. Verrà illustrato come creare uno shader con ComputeSharp e configurarne le proprietà e quindi come creare un grafo dell'effetto personalizzato incluso in un CanvasEffect tipo che può essere facilmente riutilizzato nell'applicazione.

Progettazione dell'effetto

Per questa demo, vogliamo creare un semplice effetto vetro gelato.

Sono inclusi i componenti seguenti:

  • Sfocatura gaussiana
  • Effetto tinta
  • Rumore (che è possibile generare proceduralmente con uno shader)

Si vuole anche esporre le proprietà per controllare la sfocatura e la quantità di rumore. L'effetto finale conterrà una versione "in pacchetto" di questo grafico degli effetti e sarà facile da usare semplicemente creando un'istanza, impostando tali proprietà, collegando un'immagine di origine e quindi disegnandola. È ora di iniziare.

Creazione di un pixel shader D2D1 personalizzato

Per il rumore sopra l'effetto, possiamo usare un semplice pixel shader D2D1. Lo shader calcola un valore casuale in base alle coordinate (che fungerà da "valore di inizializzazione" per il numero casuale) e quindi userà tale valore di rumore per calcolare la quantità RGB per tale pixel. È quindi possibile fondere questo rumore sopra l'immagine risultante.

Per scrivere lo shader con ComputeSharp, è sufficiente definire un partial struct tipo che implementa l'interfaccia ID2D1PixelShader e quindi scrivere la logica nel Execute metodo. Per questo noise shader, è possibile scrivere qualcosa di simile al seguente:

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

Anche se lo shader è scritto interamente in C#, è consigliabile conoscere la conoscenza di base di HLSL (il linguaggio di programmazione per gli shader DirectX, a cui ComputeSharp esegue la transpile C#).

Esaminiamo questo shader in dettaglio:

  • Lo shader non ha input, ma produce solo un'immagine infinita con rumore casuale in scala di grigi.
  • Lo shader richiede l'accesso alla coordinata pixel corrente.
  • Lo shader è precompilato in fase di compilazione (usando il PixelShader40 profilo, che è garantito che sia disponibile in qualsiasi GPU in cui l'applicazione potrebbe essere in esecuzione).
  • L'attributo [D2DGeneratedPixelShaderDescriptor] è necessario per attivare il generatore di origine in bundle con ComputeSharp, che analizzerà il codice C#, lo traspilerà in HLSL, compilerà lo shader in bytecode e così via.
  • Lo shader acquisisce un float amount parametro tramite il relativo costruttore primario. Il generatore di origine in ComputeSharp eseguirà automaticamente l'estrazione di tutti i valori acquisiti in uno shader e la preparazione del buffer costante che D2D deve inizializzare lo stato dello shader.

E questa parte è fatta! Questo shader genererà la trama del rumore personalizzata ogni volta che è necessario. Successivamente, è necessario creare l'effetto in pacchetto con il grafico degli effetti che collega tutti gli effetti insieme.

Creazione di un effetto personalizzato

Per usare facilmente l'effetto in pacchetto, è possibile usare il CanvasEffect tipo da ComputeSharp. Questo tipo offre un modo semplice per configurare tutta la logica necessaria per creare un grafico degli effetti e aggiornarlo tramite proprietà pubbliche con cui gli utenti dell'effetto possono interagire. È necessario implementare due metodi principali:

  • BuildEffectGraph: questo metodo è responsabile della compilazione del grafico degli effetti da disegnare. È quindi necessario creare tutti gli effetti necessari e registrare il nodo di output per il grafico. Per gli effetti che possono essere aggiornati in un secondo momento, la registrazione viene eseguita con un valore associato, CanvasEffectNode<T> che funge da chiave di ricerca per recuperare gli effetti dal grafico quando necessario.
  • ConfigureEffectGraph: questo metodo aggiorna il grafico degli effetti applicando le impostazioni configurate dall'utente. Questo metodo viene richiamato automaticamente quando necessario, subito prima di disegnare l'effetto e solo se almeno una proprietà di effetto è stata modificata dall'ultima volta che è stato utilizzato l'effetto.

L'effetto personalizzato può essere definito come segue:

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

È possibile visualizzare quattro sezioni in questa classe:

  • In primo luogo, sono disponibili campi per tenere traccia di tutti gli stati modificabili, ad esempio gli effetti che possono essere aggiornati, nonché i campi di supporto per tutte le proprietà dell'effetto che si desidera esporre agli utenti dell'effetto.
  • Successivamente, sono disponibili proprietà per configurare l'effetto. Il setter di ogni proprietà usa il SetAndInvalidateEffectGraph metodo esposto da CanvasEffect, che invaliderà automaticamente l'effetto se il valore impostato è diverso da quello corrente. In questo modo, l'effetto viene configurato di nuovo solo quando è effettivamente necessario.
  • Infine, abbiamo i BuildEffectGraph metodi e ConfigureEffectGraph menzionati in precedenza.

Nota

Il PremultiplyEffect nodo dopo l'effetto rumore è molto importante: questo perché gli effetti Win2D presuppongono che l'output sia premoltiplicato, mentre gli shader pixel in genere funzionano con pixel non premoltiplicati. Di conseguenza, ricordarsi di inserire manualmente nodi premultiply/unpremultiply prima e dopo shader personalizzati, per garantire che i colori vengano mantenuti correttamente.

Nota

Questo effetto di esempio usa spazi dei nomi WinUI 3, ma lo stesso codice può essere usato anche in UWP. In tal caso, lo spazio dei nomi per ComputeSharp sarà ComputeSharp.Uwp, corrispondente al nome del pacchetto.

Pronto a disegnare!

E con questo, il nostro effetto vetro gelato personalizzato è pronto! Possiamo disegnare facilmente come segue:

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

    args.DrawingSession.DrawImage(effect);
}

In questo esempio viene disegnato l'effetto dal Draw gestore di un CanvasControl, usando un CanvasBitmap oggetto caricato in precedenza come origine. Questa è l'immagine di input che verrà usata per testare l'effetto:

una foto di alcune montagne sotto un cielo nuvoloso

Ed ecco il risultato:

una versione sfocata dell'immagine precedente

Nota

Crediti a Dominic Lange per l'immagine.

Risorse aggiuntive