Dela via


Implementera anpassade effekter

Win2D innehåller flera API:er som representerar objekt som kan ritas, som är indelade i två kategorier: bilder och effekter. Bilder, som representeras av ICanvasImage gränssnittet, har inga indata och kan ritas direkt på en viss yta. Till exempel CanvasBitmap, VirtualizedCanvasBitmap och CanvasRenderTarget är exempel på bildtyper. Effekterna representeras å andra sidan av ICanvasEffect gränssnittet. De kan ha indata och ytterligare resurser och kan använda godtycklig logik för att skapa sina utdata (eftersom en effekt också är en bild). Win2D innehåller effekter som omsluter de flesta D2D-effekter, till exempel GaussianBlurEffect, TintEffect och LuminanceToAlphaEffect.

Bilder och effekter kan också länkas samman för att skapa godtyckliga grafer som sedan kan visas i ditt program (se även D2D-dokumenten om Direct2D-effekter). Tillsammans ger de ett extremt flexibelt system för att skapa komplex grafik på ett effektivt sätt. Det finns dock fall där de inbyggda effekterna inte räcker och du kanske vill skapa en egen Win2D-effekt. För att stödja detta innehåller Win2D en uppsättning kraftfulla interop-API:er som gör det möjligt att definiera anpassade bilder och effekter som sömlöst kan integreras med Win2D.

Tips/Råd

Om du använder C# och vill implementera ett anpassat effekt- eller effektdiagram rekommenderar vi att du använder ComputeSharp i stället för att försöka implementera en effekt från grunden. Se stycket nedan för en detaljerad förklaring av hur du använder det här biblioteket för att implementera anpassade effekter som integreras sömlöst med Win2D.

API:er för plattformar:ICanvasImage, CanvasBitmap, VirtualizedCanvasBitmap, CanvasRenderTarget, CanvasEffect, GaussianBlurEffect, TintEffect, ICanvasLuminanceToAlphaEffectImage, IGraphicsEffectSource, ID2D21Image, ID2D1Factory1, ID2D1Effect

Implementera en anpassad ICanvasImage

Det enklaste scenariot att stödja är att skapa en anpassad ICanvasImage. Som vi nämnde är detta WinRT-gränssnittet som definieras av Win2D och som representerar alla typer av bilder som Win2D kan interagera med. Det här gränssnittet exponerar bara två GetBounds metoder och utökar IGraphicsEffectSource, vilket är ett markörgränssnitt som representerar "någon effektkälla".

Som du ser finns det inga "funktionella" API:er som exponeras av det här gränssnittet för att faktiskt utföra någon ritning. För att implementera ditt eget ICanvasImage objekt måste du även implementera ICanvasImageInterop gränssnittet, vilket exponerar all nödvändig logik för att Win2D ska kunna rita avbildningen. Det här är ett COM-gränssnitt som definierats i den publika Microsoft.Graphics.Canvas.native.h headerfilen och som levereras med Win2D.

Gränssnittet definieras på följande sätt:

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

Och den förlitar sig också på dessa två uppräkningstyper, från samma rubrik:

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
}

De två metoderna GetDevice och GetD2DImage är allt som behövs för att implementera anpassade bilder (eller effekter), eftersom de förser Win2D med utökningspunkterna för att initiera dem på en viss enhet och hämta den underliggande D2D-bilden för att ritas. Att implementera dessa metoder på rätt sätt är viktigt för att säkerställa att saker och ting fungerar korrekt i alla scenarier som stöds.

Vi går igenom dem för att se hur varje metod fungerar.

Implementera GetDevice

Metoden GetDevice är den enklaste av de två. Vad den gör är att den hämtar canvas-enheten som är associerad med effekten, så att Win2D kan inspektera den vid behov (till exempel för att säkerställa att den matchar den enhet som används). Parametern type anger "association type" för den returnerade enheten.

Det finns två huvudsakliga möjliga fall:

  • Om bilden är en effekt bör den ha stöd för att "aktiveras" och "avaktiveras" på flera enheter. Vad detta innebär är: en given effekt skapas i ett onitialiserat tillstånd, sedan kan den realiseras när en enhet skickas under ritningen, och därefter kan den fortsätta att användas med den enheten, eller så kan den flyttas till en annan enhet. I så fall återställer effekten dess interna tillstånd och realiseras sedan igen på den nya enheten. Det innebär att den associerade enheten kan ändras med tiden, och den kan också vara null. På grund av detta ska type vara inställt på WIN2D_GET_DEVICE_ASSOCIATION_TYPE_REALIZATION_DEVICE, och den returnerade enheten ska ställas in på den aktuella realiserande enheten, om en är tillgänglig.
  • Vissa avbildningar har en enda "ägande enhet" som tilldelas vid skapandetillfället och som aldrig kan ändras. Detta skulle till exempel vara fallet för en bild som representerar en struktur, eftersom den allokeras på en specifik enhet och inte kan flyttas. När GetDevice anropas ska det returnera skapande enheten och ställa in type till WIN2D_GET_DEVICE_ASSOCIATION_TYPE_CREATION_DEVICE. Observera att när den här typen anges ska den returnerade enheten inte vara null.

Anmärkning

Win2D kan anropa GetDevice när du rekursivt passerar ett effektdiagram, vilket innebär att det kan finnas flera aktiva anrop till GetD2DImage i stacken. På grund av detta bör GetDevice inte ta ett blockeringslås på den nuvarande bilden, eftersom det potentiellt kan orsaka en deadlock. I stället bör den använda ett återinträdeslås på ett icke-blockerande sätt och returnera ett fel om det inte kan förvärvas. Detta säkerställer att samma tråd som rekursivt anropar den kan få tillgång till den, medan samtidiga trådar som gör detsamma misslyckas på ett tillförlitligt sätt.

Implementera GetD2DImage

GetD2DImage är där det mesta av arbetet sker. Den här metoden ansvarar för att hämta det ID2D1Image-objekt som Win2D kan rita, och realisera den aktuella effekten vid behov. Detta inkluderar även rekursivt bläddring och realisering av effektdiagrammet för alla källor, om några, samt initierar alla tillstånd som bilden kan behöva (t.ex. konstanta buffertar och andra egenskaper, resursstrukturer osv.).

Den exakta implementeringen av den här metoden är mycket beroende av bildtypen och kan variera mycket, men generellt sett kan du förvänta dig att metoden utför följande steg för en godtycklig effekt:

  • Kontrollera om anropet var rekursivt på samma instans och misslyckas i så fall. Detta krävs för att identifiera cykler i ett effektdiagram (t.ex. har effekten A effekt B som källa, och effekten B har effekt A som källa).
  • Skaffa ett lås på avbildningsinstansen för att skydda mot samtidig åtkomst.
  • Hantera mål-DPIs enligt indataflaggor
  • Kontrollera om indataenheten matchar den som eventuellt används. Om den inte överensstämmer och den aktuella effekten stöder genomförande, avaktiverar du effekten.
  • Förstå effekten på inmatningsenheten. Detta kan omfatta registrering av D2D-effekten på det ID2D1Factory1-objekt som hämtats från inmatningsenheten eller enhetskontexten, om det behövs. Dessutom bör alla nödvändiga tillstånd anges för D2D-effektinstansen som skapas.
  • Rekursivt traversera eventuella källor och koppla dem till D2D-effekten.

När det gäller indataflaggor finns det flera möjliga fall som anpassade effekter bör hantera korrekt för att säkerställa kompatibilitet med alla andra Win2D-effekter. Exklusive WIN2D_GET_D2D_IMAGE_FLAGS_NONE är de flaggor som ska hanteras följande:

  • WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT: i det här fallet device är garanterat inte null. Effekten bör kontrollera om målet för enhetskontexten är en ID2D1CommandListoch i så fall lägga till flaggan WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION. Annars bör den ange targetDpi (vilket också är garanterat att inte vara null) till de DPI:er som hämtats från indatakontexten. Sedan bör den ta bort WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT från flaggorna.
  • WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION och WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION: används vid inställning av effektkällor (se anteckningar nedan).
  • WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION: Om det är inställt hoppar du över den rekursiva realiseringen av källorna för effekten och returnerar bara den realiserade effekten utan några andra ändringar.
  • WIN2D_GET_D2D_IMAGE_FLAGS_ALLOW_NULL_EFFECT_INPUTS: om det anges tillåts implementerade effektkällor att bli null, om användaren inte har specificerat dem till en befintlig källa ännu.
  • WIN2D_GET_D2D_IMAGE_FLAGS_UNREALIZE_ON_FAILURE: om det anges och en effektkälla som anges inte är giltig bör effekten avaktiveras innan den misslyckas. Det vill säga, om felet inträffade när effektkällorna löstes efter att effekten har realiserats, bör effekten återställas innan felet returneras till anroparen.

När det gäller DPI-relaterade flaggor styr dessa hur effektkällor anges. För att säkerställa kompatibilitet med Win2D bör effekter automatiskt lägga till DPI-kompensationseffekter i sina indata när det behövs. De kan kontrollera om så är fallet:

  • Om WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION anges, behövs en DPI-kompensationseffekt när parametern inputDpi inte är 0.
  • Annars krävs DPI-kompensation om inputDpi inte 0, WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION inte har angetts och antingen WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION har angetts, eller så matchar inte indata-DPI:et och mål-DPI-värdena.

Den här logiken bör tillämpas när en källa realiseras och binds till indata för den aktuella effekten. Observera att om en DPI-kompensationseffekt läggs till bör denna användas som indata för den underliggande D2D-avbildningen. Men om användaren försöker hämta WinRT-omslutningen för den källan bör effekten vara noga med att identifiera om en DPI-effekt användes och returnera en omslutning för det ursprungliga källobjektet i stället. DPI-kompensationseffekter bör alltså vara transparenta för användarna av effekten.

När all initieringslogik är klar bör resultatet ID2D1Image (precis som med Win2D-objekt, eftersom en D2D-effekt också är en bild) vara redo att ritas av Win2D i målkontexten, som ännu inte är känd av mottagaren för tillfället.

Anmärkning

Korrekt implementering av den här metoden (och ICanvasImageInterop i allmänhet) är mycket komplicerad, och den är bara avsedd att utföras av avancerade användare som absolut behöver den extra flexibiliteten. En gedigen förståelse för D2D, Win2D, COM, WinRT och C++ rekommenderas innan du försöker skriva en ICanvasImageInterop implementering. Om din anpassade Win2D-effekt också måste omsluta en anpassad D2D-effekt måste du även implementera ditt eget ID2D1Effect objekt (se D2D-dokumenten om anpassade effekter för mer information om detta). Dessa dokument är inte en fullständig beskrivning av all nödvändig logik (till exempel omfattar de inte hur effektkällor ska ordnas och hanteras över gränsen D2D/Win2D), så vi rekommenderar att du även använder CanvasEffect implementeringen i Win2D:s kodbas som referenspunkt för en anpassad effekt och ändrar den efter behov.

Implementera GetBounds

Den sista saknade komponenten för att fullt ut implementera en anpassad ICanvasImage-effekt är att stödja de två GetBounds-överlagringarna. För att göra detta enkelt exponerar Win2D en C-export som kan användas för att utnyttja den befintliga logiken för detta från Win2D på alla anpassade avbildningar. Exporten är följande:

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

Anpassade avbildningar kan anropa det här API:et och skicka sig själva som parametern image och sedan helt enkelt returnera resultatet till sina anropare. Parametern transform kan vara null, om ingen transformering är tillgänglig.

Optimera åtkomst till enhetskontext

Parametern deviceContext i ICanvasImageInterop::GetD2DImage kan ibland vara null, om en kontext inte är omedelbart tillgänglig före anropet. Detta görs avsiktligt så att en kontext bara skapas lätt när den faktiskt behövs. Det vill säga, om en kontext är tillgänglig kommer Win2D att skicka den till anropet GetD2DImage; annars låter det anropare hämta en på egen hand om det behövs.

Det är relativt dyrt att skapa ett enhetssammanhang, så för att göra hämtningen snabbare gör Win2D API:er tillgängliga för att komma åt dess interna enhetssammanhangspool. Detta gör att anpassade effekter kan hyra och returnera enhetskontexter som är associerade med en given canvasenhet på ett effektivt sätt.

API:erna för leasing av enhetskontext definieras på följande sätt:

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

Gränssnittet ID2D1DeviceContextPool implementeras av CanvasDevice, vilket är win2D-typen som implementerar ICanvasDevice gränssnittet. Om du vill använda poolen använder QueryInterface du i enhetsgränssnittet för att hämta en ID2D1DeviceContextPool referens och anropar ID2D1DeviceContextPool::GetDeviceContextLease sedan för att hämta ett ID2D1DeviceContextLease objekt för att få åtkomst till enhetskontexten. När det inte längre behövs släpper du lånet. Se till att du inte rör enhetens kontext efter att lånet har släppts, eftersom det kan användas samtidigt av andra trådar.

Aktivera WinRT-wrapper-sökning

Som ses i Win2D-interopdokumentenexponerar Win2D-rubriken även en GetOrCreate-metod (tillgänglig från aktiveringsfabriken ICanvasFactoryNative eller via de GetOrCreate C++/CX-hjälpverktyg som definieras i samma header). På så sätt kan du hämta en WinRT-omslutning från en viss intern resurs. Du kan till exempel hämta eller skapa en CanvasDevice instans från ett ID2D1Device1 objekt, en CanvasBitmap från en ID2D1Bitmaposv.

Den här metoden fungerar också för alla inbyggda Win2D-effekter: genom att hämta den ursprungliga resursen för en given effekt och sedan använda den för att hämta den motsvarande Win2D-wrappare returneras korrekt den Win2D-effekt som äger den. För att anpassade effekter också ska kunna dra nytta av samma mappningssystem exponerar Win2D flera API:er genom aktiveringsfabriken CanvasDevice, som är av typen ICanvasFactoryNative, samt ett ytterligare gränssnitt för effektfabriken, 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);
};

Det finns flera API:er att tänka på här, eftersom de behövs för att stödja alla olika scenarier där Win2D-effekter kan användas, samt hur utvecklare kan göra interop med D2D-lagret och sedan försöka lösa omslutningar åt dem. Nu ska vi gå igenom vart och ett av dessa API:er.

Metoderna RegisterWrapper och UnregisterWrapper är avsedda att anropas av anpassade effekter för att lägga till sig själva i den interna Win2D-cachen:

  • RegisterWrapper: registrerar en infödd resurs och dess tillhörande WinRT-omslag. Parametern wrapper är nödvändig för att implementera IWeakReferenceSourceockså, så att den kan cachelagras korrekt utan att orsaka referenscykler vilket skulle leda till minnesläckor. Metoden returnerar S_OK om den naturliga resursen kunde läggas till i cacheminnet, S_FALSE om det redan fanns en registrerad omslag för resource, eller en felkod om ett fel inträffar.
  • UnregisterWrapper: avregistrerar en inbyggd resurs och dess omslag. Returnerar S_OK om resursen kunde tas bort, S_FALSE om resource den inte redan har registrerats, och en erro-kod om ett annat fel inträffar.

Anpassade effekter bör anropa RegisterWrapper och UnregisterWrapper när de realiseras och orealiseras, dvs. när en ny intern resurs skapas och associeras med dem. Anpassade effekter som inte stöder genomförande (t.ex. de som har en fast associerad enhet) kan anropa RegisterWrapper och UnregisterWrapper när de skapas och förstörs. Anpassade effekter bör noga se till att korrekt avregistrera sig från alla möjliga kodsökvägar som skulle kunna göra att omslutningen blir ogiltig, t.ex. när objektet har slutförts, speciellt om det implementeras på ett hanterat språk.

Metoderna RegisterEffectFactory och UnregisterEffectFactory är också avsedda att användas av anpassade effekter, så att de också kan registrera ett återanrop för att skapa en ny omslutning om en utvecklare försöker lösa en för en "överbliven" D2D-resurs:

  • RegisterEffectFactory: registrera ett återanrop som tar in samma parametrar som en utvecklare skickade till GetOrCreateoch skapar en ny kontrollbar omslutning för indataeffekten. Effekt-ID:t används som nyckel, så att varje anpassad effekt kan registrera en fabrik för den när den först laddas. Naturligtvis bör detta bara göras en gång per effekttyp, och inte varje gång effekten realiseras. Parametrarna device, resource och wrapper kontrolleras av Win2D innan några registrerade återanrop anropas, så de är garanterat inte null när CreateWrapper anropas. dpi Anses vara valfritt och kan ignoreras om effekttypen inte har någon specifik användning för den. Observera att när en ny omslutning skapas från en registrerad fabrik bör den fabriken också se till att den nya omslutningen registreras i cachen (Win2D lägger inte automatiskt till omslutningar som produceras av externa fabriker i cacheminnet).
  • UnregisterEffectFactory: tar bort en tidigare registrerad callback-funktion. Detta kan till exempel användas om en effektomslutning implementeras i en hanterad sammansättning som tas bort.

Anmärkning

ICanvasFactoryNative implementeras av aktiveringsfabriken för CanvasDevice, som du kan hämta genom att antingen anropa RoGetActivationFactorymanuellt eller använda hjälp-API:er från de språktillägg som du använder (t.ex. winrt::get_activation_factory i C++/WinRT). För mer information, se WinRT-typsystem för att förstå hur detta fungerar.

Ett praktiskt exempel på var den här mappningen kommer till användning finns i hur inbyggda Win2D-effekter fungerar. Om de inte realiseras lagras alla tillstånd (t.ex. egenskaper, källor osv.) i en intern cache i varje effektinstans. När de realiseras överförs hela tillståndet till den inhemska resursen (t.ex. egenskaper ställs in på D2D-effekten, alla källor löses och mappas till effektens indatakällor, osv.), och så länge effekten är realiserad kommer den att agera som auktoritet över omslagets tillstånd. Om värdet för en egenskap hämtas från omslutningen hämtas det uppdaterade värdet för den från den interna D2D-resursen som är associerad med den.

Detta säkerställer att om några ändringar görs direkt i D2D-resursen visas de även på den yttre omslutningen, och de två kommer aldrig att vara "osynkroniserade". När effekten är orealiserad överförs alla tillstånd tillbaka från den interna resursen till omslutningstillståndet innan resursen släpps. Den kommer att hållas och uppdateras där till nästa gång effekten realiseras. Tänk nu på den här händelsesekvensen:

  • Du har viss Win2D-effekt (antingen inbyggd eller anpassad).
  • Du får ID2D1Image från det (vilket är en ID2D1Effect).
  • Du skapar en instans av en anpassad effekt.
  • Du får också ID2D1Image från det.
  • Du anger den här bilden manuellt som indata för den tidigare effekten (via ID2D1Effect::SetInput).
  • Sedan ber du om den initiala effekten för WinRT-omslutningen för dessa indata.

Eftersom effekten realiseras (den realiserades när den interna resursen begärdes) kommer den att använda den inbyggda resursen som sanningskälla. Först kommer ID2D1Image som motsvarar den begärda källan att hämtas, och sedan försöker det att hämta WinRT-omslutningen för den. Om den effekt som indata hämtades från har korrekt lagt till sitt eget par av ursprungliga resurser och WinRT-omslutning i Win2D:s cache, löses omslutningen och returneras till anroparen. Annars misslyckas den egenskapsåtkomsten eftersom Win2D inte kan lösa WinRT-omslutningar för effekter som den inte äger, eftersom den inte vet hur de ska instansieras.

Det är här RegisterWrapper och UnregisterWrapper hjälper, eftersom de tillåter anpassade effekter att sömlöst delta i Win2D:s logik för omslutningsupplösning, så att rätt omslag alltid kan hämtas för alla effektkällor, oavsett om den ställts in från WinRT-API:er eller direkt från det underliggande D2D-lagret.

Tänk på det här scenariot för att förklara hur effektfabrikerna också spelar in:

  • En användare skapar en instans av en anpassad omslutning och inser vad det innebär.
  • De hämtar sedan en referens till den underliggande D2D-effekten och behåller den.
  • Sedan realiseras effekten på en annan enhet. Effekten kommer att avrealiseras och omrealiseras, och därigenom skapar den en ny D2D-effekt. Den tidigare D2D-effekten har inte längre ett associerat inspektionsbart omslag vid den här tidpunkten.
  • Användaren anropar GetOrCreate sedan den första D2D-effekten.

Utan återanrop skulle Win2D bara misslyckas med att lösa en omslutning, eftersom det inte finns någon registrerad omslutning för den. Om en fabrik registreras i stället kan en ny omslutning för D2D-effekten skapas och returneras, så scenariot fortsätter bara att fungera sömlöst för användaren.

Implementera en anpassad ICanvasEffect

Win2D-gränssnittet ICanvasEffect utökar ICanvasImage, så alla föregående punkter gäller även för anpassade effekter. Den enda skillnaden är det faktum att ICanvasEffect även implementerar ytterligare metoder som är specifika för effekter, till exempel att ogiltigförklara en källrektangel, hämta nödvändiga rektanglar och så vidare.

För att stödja detta exponerar Win2D C-exporter som författare av anpassade effekter kan använda, så att de inte behöver implementera all den här extra logiken från grunden. Detta fungerar på samma sätt som C-exporten för GetBounds. Här är de tillgängliga exporterna för effekter:

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

Nu ska vi gå vidare med hur de kan användas:

  • InvalidateSourceRectangleForICanvasImageInterop är avsett att stödja InvalidateSourceRectangle. Hantera bara indataparametrarna och anropa den direkt, så gör den resten av jobbet. Observera att parametern image är den aktuella effektinstansen som implementeras.
  • GetInvalidRectanglesForICanvasImageInterop stöder GetInvalidRectangles. Detta kräver inte heller något särskilt övervägande, förutom att du behöver ta bort den returnerade COM-matrisen när den inte längre behövs.
  • GetRequiredSourceRectanglesForICanvasImageInterop är en delad metod som kan stödja både GetRequiredSourceRectangle och GetRequiredSourceRectangles. Det vill säga att det tar en pekare till en befintlig matris med värden att fylla i, så anropare kan antingen skicka en pekare till ett enda värde (som också kan finnas på stacken, för att undvika en allokering) eller till en matris med värden. Implementeringen är densamma i båda fallen, så en enda C-export räcker för att driva båda.

Anpassade effekter i C# med ComputeSharp

Som vi nämnde är den rekommenderade metoden att använda ComputeSharp-biblioteket om du använder C# och vill implementera en anpassad effekt. Det gör att du både kan implementera anpassade D2D1-pixelskuggare helt i C#, samt att enkelt definiera anpassade effektdiagram som är kompatibla med Win2D. Samma bibliotek används också i Microsoft Store för att driva flera grafikkomponenter i programmet.

Du kan lägga till en referens till ComputeSharp i projektet via NuGet:

Anmärkning

Många API:er i ComputeSharp.D2D1.* är identiska mellan UWP- och WinAppSDK-målen, den enda skillnaden är namnområdet (slutar antingen .Uwp eller .WinUI). UWP-plattformen är dock under regelbundet underhåll och får inte nya funktioner. Därför kan vissa kodändringar behövas jämfört med exemplen som visas här för WinUI. Kodfragmenten i det här dokumentet återspeglar API-ytan från och med ComputeSharp.D2D1.WinUI 3.0.0 (den senaste versionen för UWP-målet är i stället 2.1.0).

Det finns två huvudkomponenter i ComputeSharp för att samarbeta med Win2D.

  • PixelShaderEffect<T>: en Win2D-effekt som drivs av en D2D1-pixelshader. Själva skuggningen skrivs i C# med hjälp av API:erna som tillhandahålls av ComputeSharp. Den här klassen innehåller också egenskaper för att ange effektkällor, konstanta värden med mera.
  • CanvasEffect: en basklass för anpassade Win2D-effekter som omsluter ett godtyckligt effektdiagram. Det kan användas för att "paketera" komplexa effekter till ett lättanvänt objekt som kan återanvändas i flera delar av ett program.

Här är ett exempel på en anpassad pixelshader (portad från denna shadertoy shader), som används med PixelShaderEffect<T> och sedan rita på en Win2D-yta CanvasControl (observera att PixelShaderEffect<T> implementerar ICanvasImage):

ett exempel på en pixelshader som visar oändliga färgade sexhörningar, ritade på en Win2D-kontroll och visas som körs i ett appfönster

Du kan se hur du på bara två kodrader kan skapa en effekt och rita den via Win2D. ComputeSharp tar hand om allt arbete som krävs för att kompilera skuggningen, registrera den och hantera den komplexa livslängden för en Win2D-kompatibel effekt.

Nu ska vi se en steg för steg-guide om hur du skapar en anpassad Win2D-effekt som också använder en anpassad D2D1-pixelskuggning. Vi går vidare med hur du skapar en skuggning med ComputeSharp och konfigurerar dess egenskaper och sedan hur du skapar ett anpassat effektdiagram som paketeras i en CanvasEffect typ som enkelt kan återanvändas i ditt program.

Att designa effekten

I den här demonstrationen vill vi skapa en enkel frostad glaseffekt.

Detta omfattar följande komponenter:

  • Gaussisk oskärpa
  • Toningseffekt
  • Brus (som vi kan generera proceduralt med en shader)

Vi vill också exponera egenskaper för att styra oskärpa och brusmängd. Den slutliga effekten innehåller en "paketerad" version av den här effektdiagrammet och är lätt att använda genom att bara skapa en instans, ange dessa egenskaper, ansluta en källavbildning och sedan rita den. Nu ska vi komma igång!

Skapa en anpassad D2D1-pixelskuggare

För bruset ovanpå effekten kan vi använda en enkel D2D1 pixelskuggning. Shadern beräknar ett slumpmässigt värde baserat på dess koordinater (som fungerar som ett "frö" för det slumpmässiga talet), och använder sedan brusvärdet för att beräkna RGB-värdet för den pixelen. Vi kan sedan blanda det här bruset ovanpå den resulterande bilden.

För att skriva skuggningen med ComputeSharp behöver vi bara definiera en partial struct typ som implementerar ID2D1PixelShader gränssnittet och sedan skriva vår logik i Execute -metoden. För den här brusskuggaren kan vi skriva ungefär så här:

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

Anmärkning

Även om skuggningen är helt skriven i C#, rekommenderas grundläggande kunskaper om HLSL (programmeringsspråket för DirectX-skuggningar, som ComputeSharp överför C# till).

Låt oss gå över den här skuggningen i detalj:

  • Shadern har inga indata, den producerar bara en ständig bild med slumpmässigt gråskalebrus.
  • Skuggningen kräver åtkomst till den aktuella pixelkoordinaten.
  • Skuggningen är förkompilerad vid byggtiden (med hjälp av profilen PixelShader40 , som garanterat är tillgänglig på alla GPU:ar där programmet kan köras).
  • Attributet [D2DGeneratedPixelShaderDescriptor] behövs för att utlösa källgeneratorn som paketerats med ComputeSharp, som analyserar C#-koden, överför den till HLSL, kompilerar skuggningen till bytekod osv.
  • Shadermodulen samlar in en float amount parameter via dess primära konstruktor. Källgeneratorn i ComputeSharp tar automatiskt hand om att extrahera alla insamlade värden i en skuggning och förbereder den konstanta buffert som D2D behöver för att initiera skuggningstillståndet.

Och den här delen är klar! Den här skuggningen genererar vår anpassade brusstruktur när det behövs. Sedan måste vi skapa vår paketerade effekt med effektdiagrammet som kopplar samman alla våra effekter.

Skapa en anpassad effekt

För vår enkla, paketerade effekt kan vi använda typen CanvasEffect från ComputeSharp. Den här typen är ett enkelt sätt att konfigurera all nödvändig logik för att skapa ett effektdiagram och uppdatera det via offentliga egenskaper som användare av effekten kan interagera med. Det finns två huvudsakliga metoder som vi behöver implementera:

  • BuildEffectGraph: Den här metoden ansvarar för att skapa det effektdiagram som vi vill rita. Den måste alltså skapa alla effekter vi behöver och registrera utdatanoden för diagrammet. För effekter som kan uppdateras vid ett senare tillfälle görs registreringen med ett associerat CanvasEffectNode<T> värde, som fungerar som uppslagsnyckel för att hämta effekterna från diagrammet när det behövs.
  • ConfigureEffectGraph: Den här metoden uppdaterar effektdiagrammet genom att tillämpa de inställningar som användaren har konfigurerat. Den här metoden anropas automatiskt när det behövs, precis innan effekten ritas, och endast om minst en effektegenskap har ändrats sedan den senaste gången effekten användes.

Vår anpassade effekt kan definieras på följande sätt:

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

Du kan se att det finns fyra avsnitt i den här klassen:

  • Först har vi fält för att spåra alla föränderliga tillstånd, till exempel de effekter som kan uppdateras samt bakgrundsfälten för alla effektegenskaper som vi vill exponera för användare av effekten.
  • Sedan har vi egenskaper för att konfigurera effekten. Inställaren för varje egenskap använder metoden SetAndInvalidateEffectGraph som tillhandahålls av CanvasEffect, vilket automatiskt gör effekten ogiltig om det värde som anges skiljer sig från det nuvarande. Detta säkerställer att effekten bara konfigureras igen när det verkligen behövs.
  • Slutligen har vi de BuildEffectGraph metoder och ConfigureEffectGraph metoder som vi nämnde ovan.

Anmärkning

Den PremultiplyEffect-noden efter brus är mycket viktig: detta beror på att Win2D-effekter förutsätter att utdata är förmultiplcerad, medan pixelskuggor vanligtvis fungerar med icke-premultiplcerade pixlar. Kom därför ihåg att manuellt infoga premultiply/unpremultiply-noder före och efter anpassade shaders för att säkerställa att färgerna bevaras korrekt.

Anmärkning

Den här exempeleffekten använder WinUI 3-namnområden, men samma kod kan också användas på UWP. I så fall blir ComputeSharp.Uwp namnområdet för ComputeSharp, som matchar paketnamnet.

Redo att rita!

Och med detta är vår anpassade frostade glaseffekt klar! Vi kan enkelt rita den på följande sätt:

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

    args.DrawingSession.DrawImage(effect);
}

I det här exemplet hämtar vi effekten från Draw-hanterare för en CanvasControl, med hjälp av en CanvasBitmap som vi tidigare laddade in som källa. Det här är den indatabild som vi ska använda för att testa effekten:

en bild av några berg under en molnig himmel

Och här är resultatet:

en suddig version av bilden ovan

Anmärkning

Krediter till Dominic Lange för bilden.

Ytterligare resurser

  • Mer information finns i Win2D-källkoden .
  • Mer information om ComputeSharp finns i exempelapparna och enhetstesterna .