Delen via


Aangepaste effecten implementeren

Win2D biedt verschillende API's voor objecten die kunnen worden getekend, die zijn onderverdeeld in twee categorieën: afbeeldingen en effecten. Afbeeldingen, vertegenwoordigd door de ICanvasImage interface, hebben geen invoer en kunnen rechtstreeks op een bepaald oppervlak worden getekend. Dit zijn bijvoorbeeld CanvasBitmapVirtualizedCanvasBitmapCanvasRenderTarget voorbeelden van afbeeldingstypen. Effecten worden daarentegen vertegenwoordigd door de ICanvasEffect interface. Ze kunnen invoer en aanvullende resources hebben en kunnen willekeurige logica toepassen om hun uitvoer te produceren (omdat een effect ook een afbeelding is). Win2D bevat effecten die de meeste D2D-effectengebruiken, zoals GaussianBlurEffect, TintEffect en LuminanceToAlphaEffect.

Afbeeldingen en effecten kunnen ook worden gekoppeld om willekeurige grafieken te maken die vervolgens in uw toepassing kunnen worden weergegeven (raadpleeg ook de D2D-documenten op Direct2D-effecten). Samen bieden ze een uiterst flexibel systeem om complexe afbeeldingen op een efficiënte manier te ontwerpen. Er zijn echter gevallen waarin de ingebouwde effecten niet voldoende zijn en u misschien uw eigen Win2D-effect wilt bouwen. Ter ondersteuning hiervan bevat Win2D een set krachtige interop-API's waarmee aangepaste afbeeldingen en effecten kunnen worden gedefinieerd die naadloos kunnen worden geïntegreerd met Win2D.

Aanbeveling

Als u C# gebruikt en een aangepaste effect- of effectgrafiek wilt implementeren, wordt u aangeraden ComputeSharp te gebruiken in plaats van een volledig nieuw effect te implementeren. Zie de onderstaande alinea voor een gedetailleerde uitleg over het gebruik van deze bibliotheek om aangepaste effecten te implementeren die naadloos kunnen worden geïntegreerd met Win2D.

Platform-API's:ICanvasImage, CanvasBitmap, VirtualizedCanvasBitmap, CanvasRenderTarget, CanvasEffect, GaussianBlurEffect, TintEffect, ICanvasLuminanceToAlphaEffectImage, IGraphicsEffectSource, ID2D21Image, ID2D1Factory1, ID2D1Effect

Het implementeren van een aangepaste ICanvasImage

Het eenvoudigste scenario ter ondersteuning is het maken van een aangepaste ICanvasImage. Zoals we al zeiden, is dit de WinRT-interface die is gedefinieerd door Win2D, die alle soorten afbeeldingen vertegenwoordigt waarmee Win2D kan samenwerken. Deze interface maakt alleen twee GetBounds methoden beschikbaar en breidt uit IGraphicsEffectSource. Dit is een markeringsinterface die 'een effectbron' vertegenwoordigt.

Zoals u kunt zien, zijn er geen 'functionele' API's beschikbaar die door deze interface worden weergegeven om daadwerkelijk een tekening uit te voeren. Als u uw eigen ICanvasImage object wilt implementeren, moet u ook de ICanvasImageInterop interface implementeren, waarmee alle benodigde logica voor Win2D wordt weergegeven om de afbeelding te tekenen. Dit is een COM-interface die is gedefinieerd in de openbare Microsoft.Graphics.Canvas.native.h header, die wordt geleverd met Win2D.

De interface wordt als volgt gedefinieerd:

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

En het is ook afhankelijk van deze twee opsommingstypen, van dezelfde header:

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 twee methoden GetDevice en GetD2DImage zijn alles wat nodig is om aangepaste afbeeldingen (of effecten) te implementeren, omdat ze Win2D uitbreidbaarheidspunten bieden om ze op een bepaald apparaat te initialiseren en de onderliggende D2D-afbeelding op te halen om te tekenen. Het correct implementeren van deze methoden is essentieel om ervoor te zorgen dat alles goed werkt in alle ondersteunde scenario's.

Laten we deze bekijken om te zien hoe elke methode werkt.

Implementeren GetDevice

De GetDevice methode is de eenvoudigste van de twee. Het haalt het canvasapparaat op dat is gekoppeld aan het effect, zodat Win2D het indien nodig kan controleren (bijvoorbeeld om te zorgen dat het apparaat in gebruik is). De type parameter geeft het 'koppelingstype' aan voor het geretourneerde apparaat.

Er zijn twee hoofdscenario's:

  • Als de afbeelding een effect is, moet deze de ondersteuning bieden om "gerealiseerd" en "niet-gerealiseerd" te zijn op meerdere apparaten. Wat dit betekent, is: een bepaald effect wordt gemaakt in een niet-geïnitialiseerde status, dan kan het worden gerealiseerd wanneer een apparaat wordt doorgegeven tijdens het tekenen, waarna het kan blijven worden gebruikt met dat apparaat, of kan worden verplaatst naar een ander apparaat. In dat geval stelt het effect de interne status opnieuw in en realiseert het zich vervolgens opnieuw op het nieuwe apparaat. Dit betekent dat het bijbehorende canvasapparaat na verloop van tijd kan veranderen en ook nullkan zijn. Daardoor moet type worden ingesteld op WIN2D_GET_DEVICE_ASSOCIATION_TYPE_REALIZATION_DEVICE, en indien beschikbaar moet het geretourneerde apparaat worden ingesteld op het huidige uitvoerapparaat.
  • Sommige afbeeldingen hebben één apparaat als eigenaar dat wordt toegewezen tijdens het aanmaken en nooit kan worden gewijzigd. Dit is bijvoorbeeld het geval voor een afbeelding die een patroon vertegenwoordigt, omdat deze is toegewezen op een specifiek apparaat en niet kan worden verplaatst. Wanneer GetDevice wordt aangeroepen, moet het het apparaat retourneren en type instellen op WIN2D_GET_DEVICE_ASSOCIATION_TYPE_CREATION_DEVICE. Houd er rekening mee dat wanneer dit type is opgegeven, het geretourneerde apparaat niet mag zijn null.

Opmerking

Win2D kan GetDevice aanroepen terwijl het recursief door een effectgrafiek loopt, wat betekent dat er mogelijk meerdere actieve roepingen van GetD2DImage in de stack zijn. Hierdoor mag GetDevice geen blokkerende vergrendeling op de huidige afbeelding nemen, omdat dat mogelijk een deadlock kan veroorzaken. In plaats daarvan moet het een nieuwe vergrendeling op een niet-blokkerende manier gebruiken en een fout retourneren als deze niet kan worden verkregen. Dit zorgt ervoor dat dezelfde thread die het recursief aanroept, het met succes kan verkrijgen, terwijl gelijktijdige threads die hetzelfde proberen, op een nette manier falen.

Implementeren GetD2DImage

GetD2DImage is waar het grootste deel van het werk plaatsvindt. Deze methode is verantwoordelijk voor het ophalen van het ID2D1Image object dat Win2D kan tekenen, eventueel het huidige effect realiseren indien nodig. Dit omvat ook recursief doorkruisen en het realiseren van de effectgrafiek voor alle bronnen, indien van toepassing, en het initialiseren van een toestand die de afbeelding mogelijk nodig heeft (bijvoorbeeld constante buffers en andere eigenschappen, resourcetextuur, enzovoort).

De exacte implementatie van deze methode is sterk afhankelijk van het type afbeelding en kan sterk variëren, maar over het algemeen kunt u verwachten dat de methode voor een willekeurig effect de volgende stappen uitvoert:

  • Controleer of de aanroep recursief was op hetzelfde exemplaar, en faal als dat het geval is. Dit is nodig om cycli in een effectgrafiek te detecteren (bijvoorbeeld effect A heeft effect B als bron en effect B heeft effect A als bron).
  • Verkrijg een vergrendeling op de afbeelding om tegen gelijktijdige toegang te beschermen.
  • De doel-DPO's afhandelen op basis van de invoervlagmen
  • Controleer of het invoerapparaat overeenkomt met het apparaat dat wordt gebruikt, indien van toepassing. Als het niet overeenkomt en het huidige effect de realisatie ondersteunt, maakt u het effect ongedaan.
  • Realiseer het effect op het invoerapparaat. Dit kan bijvoorbeeld het registreren van het D2D-effect op het ID2D1Factory1-object dat is opgehaald uit de invoerapparaat- of apparaatcontext, indien nodig. Daarnaast moet alle benodigde toestand worden ingesteld op de D2D-effectinstantie die wordt gemaakt.
  • Recursief alle bronnen doorlopen en ze koppelen aan het D2D-effect.

Met betrekking tot de invoervlagmen zijn er verschillende mogelijke gevallen die aangepaste effecten correct moeten verwerken, om compatibiliteit met alle andere Win2D-effecten te garanderen. Exclusief WIN2D_GET_D2D_IMAGE_FLAGS_NONEzijn de te verwerken vlaggen het volgende:

  • WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT: in dit geval is device gegarandeerd geen null. Het effect moet controleren of het doel van de apparaatcontext een ID2D1CommandListis en, als dat het geval is, de vlag WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION toevoegen. Anders moet targetDpi (dat ook gegarandeerd niet nullis) worden ingesteld op de DPI's die zijn opgehaald uit de invoercontext. Verwijder dan WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT uit de vlaggen.
  • WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION en WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION: wordt gebruikt bij het instellen van effectbronnen (zie de opmerkingen hieronder).
  • WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION: indien ingesteld, slaat recursief het realiseren van de bronnen van het effect over en retourneert alleen het gerealiseerde effect zonder andere wijzigingen.
  • WIN2D_GET_D2D_IMAGE_FLAGS_ALLOW_NULL_EFFECT_INPUTS: indien ingesteld, mogen effectbronnen die worden gerealiseerd, worden null, als de gebruiker deze nog niet heeft ingesteld op een bestaande bron.
  • WIN2D_GET_D2D_IMAGE_FLAGS_UNREALIZE_ON_FAILURE: indien ingesteld en een effectbron die wordt ingesteld, ongeldig is, moet het effect ongedaan worden gemaakt voordat het mislukt. Als de fout is opgetreden tijdens het oplossen van de effectbronnen na het realiseren van het effect, moet het effect zichzelf niet meer genereren voordat de fout wordt geretourneerd aan de beller.

Met betrekking tot de DPI-gerelateerde vlaggen bepalen deze hoe effectbronnen worden ingesteld. Om compatibiliteit met Win2D te garanderen, moeten effecten automatisch DPI-compensatieeffecten toevoegen aan hun invoer wanneer dat nodig is. Ze kunnen bepalen of dat het geval is:

  • Als WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION is ingesteld, is een DPI-compensatie-effect nodig wanneer de inputDpi-parameter niet 0 is.
  • Anders is DPI-compensatie nodig als inputDpi niet 0is, WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION niet is ingesteld en WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION wel is ingesteld, of als de invoer-DPI en de doel-DPI niet overeenkomen.

Deze logica moet worden toegepast wanneer een bron wordt gerealiseerd en gebonden aan een invoer van het huidige effect. Houd er rekening mee dat als er een DPI-compensatie-effect wordt toegevoegd, dit de invoer moet zijn die is ingesteld op de onderliggende D2D-afbeelding. Maar, als de gebruiker de WinRT-wrapper voor die bron probeert op te halen, moet het effect ervoor zorgen dat wordt gedetecteerd of er een DPI-effect is gebruikt, en in dat geval een wrapper voor het oorspronkelijke bronobject retourneren. Dat wil gezegd: DPI-compensatie-effecten moeten transparant zijn voor gebruikers van het effect.

Nadat alle initialisatielogica zijn uitgevoerd, zou de resulterende ID2D1Image (net als bij Win2D-objecten, is een D2D-effect ook een afbeelding) klaar moeten zijn om door Win2D te worden getekend op de doelcontext, die op dit moment nog niet bekend is bij de callee.

Opmerking

Het correct implementeren van deze methode (en ICanvasImageInterop in het algemeen) is uiterst ingewikkeld en is alleen bedoeld voor geavanceerde gebruikers die absoluut de extra flexibiliteit nodig hebben. Een solide kennis van D2D, Win2D, COM, WinRT en C++ wordt aanbevolen voordat u een ICanvasImageInterop implementatie schrijft. Als uw aangepaste Win2D-effect ook een aangepast D2D-effect moet verpakken, moet u ook uw eigen ID2D1Effect object implementeren (raadpleeg de D2D-documenten over aangepaste effecten voor meer informatie hierover). Deze documenten zijn geen volledige beschrijving van alle benodigde logica (ze hebben bijvoorbeeld geen betrekking op hoe effectbronnen moeten worden ge marshalld en beheerd over de grens D2D/Win2D), dus het wordt aanbevolen om ook de CanvasEffect implementatie in de codebasis van Win2D te gebruiken als referentiepunt voor een aangepast effect en deze indien nodig te wijzigen.

Implementeren GetBounds

Het laatste ontbrekende onderdeel voor het volledig implementeren van een aangepast ICanvasImage effect is het ondersteunen van de twee GetBounds overbelastingen. Om dit eenvoudig te maken, stelt Win2D een C-export beschikbaar waarmee je de bestaande logica van Win2D kunt benutten op elke aangepaste afbeelding. De export is als volgt:

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

Aangepaste afbeeldingen kunnen deze API aanroepen en zichzelf doorgeven als de image-parameter, en vervolgens het resultaat eenvoudigweg retourneren aan hun aanroepers. De transform parameter kan zijn null, als er geen transformatie beschikbaar is.

Apparaatcontexttoegang optimaliseren

De deviceContext parameter in ICanvasImageInterop::GetD2DImage kan soms zijn null, als een context niet direct beschikbaar is vóór de aanroep. Dit gebeurt met opzet, zodat een context alleen op een "lazy" manier wordt aangemaakt wanneer deze daadwerkelijk nodig is. Dat wil zeggen, als er een context beschikbaar is, zal Win2D deze doorgeven aan de GetD2DImage aanroep, anders kunnen gebruikers er zo nodig zelf een ophalen.

Het maken van een apparaatcontext is relatief duur, dus om het ophalen ervan te versnellen, biedt Win2D API's aan voor toegang tot de interne apparaatcontextpool. Dit staat op maat gemaakte effecten toe om apparaatcontexten die zijn gekoppeld aan een bepaald canvasapparaat op een efficiënte manier te huren en terug te geven.

De lease-API's voor apparaatcontext worden als volgt gedefinieerd:

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

De ID2D1DeviceContextPool interface wordt geïmplementeerd door CanvasDevice, wat het Win2D-type is dat de ICanvasDevice interface implementeert. Om de pool te gebruiken, gebruikt u QueryInterface op de apparaatinterface om een ID2D1DeviceContextPool-verwijzing te verkrijgen, en roept u vervolgens ID2D1DeviceContextPool::GetDeviceContextLease aan om een ID2D1DeviceContextLease-object te verkrijgen voor toegang tot de apparaatcontext. Zodra dat niet meer nodig is, laat u de lease los. Zorg ervoor dat u de apparaatcontext niet aanraakt nadat de lease is vrijgegeven, omdat deze gelijktijdig kan worden gebruikt door andere threads.

Opzoeken van WinRT-wrappers inschakelen

Zoals te zien is uit de Win2D-interopdocumenten, stelt de openbare Win2D-header ook een GetOrCreate-methode bloot (toegankelijk via de activeringsfactory ICanvasFactoryNative, of door gebruik te maken van de GetOrCreate C++/CX-helpers die in dezelfde header zijn gedefinieerd). Hierdoor kan een WinRT-wrapper worden opgehaald uit een bepaalde systeemeigen resource. Hiermee kunt u bijvoorbeeld een instantie ophalen of maken van een CanvasDeviceID2D1Device1 object, een CanvasBitmap instantie van een ID2D1Bitmap object, enzovoort.

Deze methode werkt ook voor alle ingebouwde Win2D-effecten: het ophalen van de natuurlijke hulpbron voor een bepaald effect en deze vervolgens gebruiken om de bijbehorende Win2D-wrapper op te halen, geeft het bijbehorende Win2D-effect ervan correct terug. Om ervoor te zorgen dat maatwerk-effecten ook profiteren van hetzelfde mappingsysteem, stelt Win2D meerdere API's bloot in de interop-interface voor de activeringsfabriek voor CanvasDevice, wat het ICanvasFactoryNative-type is, evenals een aanvullende effectfabriek-interface 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);
};

Er zijn hier verschillende API's om rekening mee te houden, omdat ze nodig zijn om alle verschillende scenario's te ondersteunen waarin Win2D-effecten kunnen worden gebruikt, evenals hoe ontwikkelaars interop kunnen uitvoeren met de D2D-laag en vervolgens proberen wrappers voor hen op te lossen. Laten we elk van deze API's eens bekijken.

De RegisterWrapper- en UnregisterWrapper-methoden zijn bedoeld om door eigen effecten aangeroepen te worden, zodat deze zichzelf kunnen toevoegen aan de interne Win2D-cache.

  • RegisterWrapper: registreert een natuurlijke bron en de bijbehorende WinRT-wrapper. De wrapper-parameter is vereist om ook IWeakReferenceSource te implementeren, zodat het correct in de cache kan worden opgeslagen zonder verwijzingscycli te veroorzaken die tot geheugenlekken zouden leiden. De methode retourneert S_OK als de systeemeigen resource kan worden toegevoegd aan de cache, S_FALSE als er al een geregistreerde wrapper voor resourceis en een foutcode als er een fout optreedt.
  • UnregisterWrapper: maakt een systeemeigen resource en de bijbehorende wrapper ongeregistreerd. Retourneert S_OK of de resource kan worden verwijderd, S_FALSE als resource deze nog niet is geregistreerd en errocode als er een andere fout optreedt.

Aangepaste effecten moeten RegisterWrapper en UnregisterWrapper aanroepen wanneer ze worden gerealiseerd en niet gerealiseerd, dwz. wanneer er een nieuwe systeemeigen resource wordt gemaakt en met hen geassocieerd. Aangepaste effecten die geen ondersteuning bieden voor realisatie (bijvoorbeeld die met een vast gekoppeld apparaat) kunnen RegisterWrapper en UnregisterWrapper aanroepen wanneer ze worden gemaakt en vernietigd. Aangepaste effecten moeten ervoor zorgen dat ze zichzelf correct afmelden van alle mogelijke codepaden die ertoe kunnen leiden dat de wrapper ongeldig wordt (bijvoorbeeld wanneer het object is afgerond, in het geval dat het is geïmplementeerd in een beheerde taal).

De RegisterEffectFactory- en UnregisterEffectFactory-methoden zijn ook bedoeld om te worden gebruikt door aangepaste effecten, zodat ze ook een callback kunnen registreren om een nieuwe wrapper te maken voor het geval een ontwikkelaar probeert er een op te lossen voor een 'zwevende' D2D-resource:

  • RegisterEffectFactory: registreer een callback die dezelfde parameters gebruikt als een ontwikkelaar aan GetOrCreate heeft doorgegeven en maak een nieuwe inspecteerbare wrapper voor het effect van de invoer. De effect-id wordt gebruikt als sleutel, zodat elk aangepast effect een factory kan registreren wanneer deze voor het eerst wordt geladen. Dit moet natuurlijk slechts eenmaal per effecttype worden gedaan en niet telkens wanneer het effect wordt gerealiseerd. De parameters device, resource en wrapper worden gecontroleerd door Win2D voordat een geregistreerde callback wordt aangeroepen door hen, zodat gegarandeerd is dat ze niet null zijn wanneer CreateWrapper wordt aangeroepen. De dpi wordt als optioneel beschouwd en kan worden genegeerd in het geval dat het effecttype er geen specifiek gebruik voor heeft. Houd er rekening mee dat wanneer een nieuwe wrapper wordt gemaakt op basis van een geregistreerde factory, die fabriek er ook voor moet zorgen dat de nieuwe wrapper in de cache is geregistreerd (Win2D voegt niet automatisch wrappers toe die door externe factory's worden geproduceerd in de cache).
  • UnregisterEffectFactory: verwijdert een eerder geregistreerde callback. Dit kan bijvoorbeeld worden gebruikt als een effect-wrapper wordt geïmplementeerd in een beheerde assembly die wordt ontladen.

Opmerking

ICanvasFactoryNative wordt geïmplementeerd door de activeringsfactory voor CanvasDevice, die u kunt ophalen door handmatig RoGetActivationFactory aan te roepen, of door gebruik te maken van helper-API's vanuit de taalextensies die u gebruikt (bijvoorbeeld winrt::get_activation_factory in C++/WinRT). Zie het WinRT-typesysteem voor meer informatie over hoe dit werkt.

Voor een praktisch voorbeeld van waar deze toewijzing van toepassing wordt, kijk hoe ingebouwde Win2D-effecten werken. Als ze niet worden gerealiseerd, wordt alle staat (bijvoorbeeld eigenschappen, bronnen, enzovoort) opgeslagen in de interne cache van elk effectexemplaar. Wanneer ze worden gerealiseerd, wordt alle status overgebracht naar de systeemeigen resource (bijvoorbeeld eigenschappen worden ingesteld op het D2D-effect, worden alle bronnen omgezet en toegewezen aan effectinvoer, enzovoort), en zolang het effect wordt gerealiseerd, fungeert het als de autoriteit op de status van de wrapper. Als de waarde van een eigenschap wordt opgehaald uit de wrapper, wordt de bijgewerkte waarde voor deze eigenschap opgehaald uit de systeemeigen D2D-resource die eraan is gekoppeld.

Dit zorgt ervoor dat als er wijzigingen rechtstreeks in de D2D-resource worden aangebracht, deze ook zichtbaar zijn op de buitenste wrapper en de twee nooit 'niet synchroon' zijn. Wanneer het effect niet is gerealiseerd, wordt alle status teruggezet van de systeemeigen resource naar de wrapperstatus voordat de resource wordt vrijgegeven. Het wordt daar bewaard en bijgewerkt totdat het effect de volgende keer wordt gerealiseerd. Houd nu rekening met deze reeks gebeurtenissen:

  • U hebt een Win2D-effect (ingebouwd of aangepast).
  • U krijgt de ID2D1Image ervan (een ID2D1Effect).
  • U maakt een exemplaar van een aangepast effect.
  • Je krijgt daar ook de ID2D1Image van.
  • U stelt deze afbeelding handmatig in als invoer voor het vorige effect (via ID2D1Effect::SetInput).
  • Vervolgens vraagt u om het eerste effect voor de WinRT-wrapper van die invoer.

Omdat het effect wordt gerealiseerd (het werd gerealiseerd toen de systeemeigen resource werd aangevraagd), wordt de systeemeigen resource gebruikt als de bron van waarheid. Als zodanig krijgt het de ID2D1Image die overeenkomt met de aangevraagde bron en probeert de WinRT-wrapper ervoor op te halen. Als het effect waaruit deze invoer is verkregen correct is toegevoegd met zijn eigen paar systeemeigen resources en WinRT-wrapper aan de cache van Win2D, wordt de wrapper omgezet en geretourneerd aan aanroepers. Als dat niet het geval is, zal de toegang tot deze eigenschap mislukken, omdat Win2D geen WinRT-wrappers kan oplossen voor effecten die het niet bezit, aangezien het niet weet hoe het ze moet instantiëren.

Dit is waar RegisterWrapper en UnregisterWrapper helpen, omdat aangepaste effecten naadloos kunnen deelnemen aan de Win2D-oplossingslogica, zodat de juiste wrapper altijd kan worden verkregen voor elke effectbron, ongeacht of deze is ingesteld vanuit WinRT API's of rechtstreeks vanuit de onderliggende D2D-laag.

Als u wilt uitleggen hoe de effect factory's ook in het spel komen, kunt u dit scenario overwegen:

  • Een gebruiker maakt een exemplaar van een aangepaste wrapper en realiseert dit
  • Hij of zij krijgt vervolgens een verwijzing naar het onderliggende D2D-effect en houdt deze bij zich.
  • Vervolgens wordt het effect gerealiseerd op een ander apparaat. Het effect zal zich ont-realiseren en weer realiseren, waardoor er een nieuw D2D-effect ontstaat. Het vorige D2D-effect heeft op dit moment geen geassocieerde inspecteerbare wrapper meer.
  • De gebruiker roept vervolgens GetOrCreate aan op het eerste D2D-effect.

Zonder een callback zou Win2D een wrapper niet oplossen, omdat er geen geregistreerde wrapper voor is. Als er daarentegen een fabriek is geregistreerd, kan er een nieuwe wrapper voor dat D2D-effect worden aangemaakt en geretourneerd, zodat het scenario probleemloos voor de gebruiker blijft werken.

Het implementeren van een aangepaste ICanvasEffect

De Win2D-interface ICanvasEffect breidt zich uit ICanvasImage, zodat ook alle vorige punten van toepassing zijn op aangepaste effecten. Het enige verschil is dat ICanvasEffect ook aanvullende methoden implementeert die specifiek zijn voor effecten, zoals het ongeldig maken van een bronrechthoek, het verkrijgen van de vereiste rechthoeken enzovoort.

Ter ondersteuning hiervan maakt Win2D C-exports beschikbaar die auteurs van aangepaste effecten kunnen gebruiken, zodat ze niet al deze extra logica helemaal opnieuw hoeven uit te voeren. Dit werkt op dezelfde manier als de C-export voor GetBounds. Dit zijn de beschikbare exports voor effecten:

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

Laten we eens bekijken hoe ze kunnen worden gebruikt:

  • InvalidateSourceRectangleForICanvasImageInterop is bedoeld ter ondersteuning van InvalidateSourceRectangle. Verwerk de invoerparameters en roep het direct aan, en het zorgt voor al het benodigde werk. Houd er rekening mee dat de parameter image het huidige effectexemplaar is dat wordt uitgevoerd.
  • GetInvalidRectanglesForICanvasImageInterop ondersteunt GetInvalidRectangles. Dit vereist ook geen speciale overweging, behalve het verwijderen van de geretourneerde COM-matrix zodra deze niet meer nodig is.
  • GetRequiredSourceRectanglesForICanvasImageInterop is een gedeelde methode die zowel GetRequiredSourceRectangle als GetRequiredSourceRectangles kan ondersteunen. Dat wil zeggen, het neemt een pointer naar een bestaande reeks waarden om in te vullen, zodat bellers een pointer kunnen doorgeven naar één waarde (die ook in het stapelgeheugen kan staan, om een toewijzing te vermijden), of naar een reeks van waarden. De implementatie is in beide gevallen hetzelfde, dus één C-export is voldoende om beide aan te kunnen.

Aangepaste effecten in C# met behulp van ComputeSharp

Zoals we al zeiden, als u C# gebruikt en een aangepast effect wilt implementeren, is de aanbevolen methode om de ComputeSharp-bibliotheek te gebruiken. Hiermee kunt u zowel aangepaste D2D1-pixel-shaders in C# implementeren als eenvoudig aangepaste effectengrafieken definiëren die compatibel zijn met Win2D. Dezelfde bibliotheek wordt ook gebruikt in de Microsoft Store om verschillende grafische componenten in de applicatie aan te drijven.

U kunt een verwijzing toevoegen naar ComputeSharp in uw project via NuGet: Selecteer het Pakket ComputeSharp.D2D1.WinUI .

Opmerking

Veel API's in ComputeSharp.D2D1.* zijn identiek voor de UWP- en WinUI-doelen, het enige verschil is de naamruimte (eindigend op of .Uwp.WinUI). Het UWP-doel bevindt zich echter in duurzaam onderhoud en ontvangt geen nieuwe functies. Daarom zijn er mogelijk enkele codewijzigingen nodig in vergelijking met de voorbeelden die hier voor WinUI worden weergegeven. De fragmenten in dit document weerspiegelen het API-oppervlak vanaf ComputeSharp.D2D1.WinUI.0.0 (de laatste release voor het UWP-doel is 2.1.0).

Er zijn twee hoofdonderdelen in ComputeSharp om samen te werken met Win2D.

  • PixelShaderEffect<T>: een Win2D-effect dat wordt aangedreven door een D2D1 pixel shader. De shader zelf wordt geschreven in C# met behulp van de API's van ComputeSharp. Deze klasse biedt ook eigenschappen voor het instellen van effectbronnen, constante waarden en meer.
  • CanvasEffect: een basisklasse voor aangepaste Win2D-effecten die een willekeurige effectgrafiek verpakken. Het kan worden gebruikt om complexe effecten te verpakken in een eenvoudig te gebruiken object dat kan worden hergebruikt in verschillende onderdelen van een toepassing.

Hier is een voorbeeld van een aangepaste pixel-shader (geporteerd vanuit , deze shadertoy-shader), gebruikt met PixelShaderEffect<T> en vervolgens getekend op een Win2D-CanvasControl (let op dat PixelShaderEffect<T>ICanvasImageimplementeert).

een voorbeeld van pixel-shader met oneindige gekleurde zeshoeken, die worden getekend op een Win2D-besturingselement en worden weergegeven in een app-venster

U kunt zien hoe u in slechts twee regels code een effect kunt maken en tekenen via Win2D. ComputeSharp zorgt voor alle benodigde werkzaamheden om de shader te compileren, te registreren en de complexe levensduur van een win2D-compatibel effect te beheren.

Laten we nu een stapsgewijze handleiding bekijken voor het maken van een aangepast Win2D-effect dat ook gebruikmaakt van een aangepaste D2D1 pixel-shader. We bespreken hoe u een shader maakt met ComputeSharp en de eigenschappen ervan instelt en vervolgens hoe u een aangepaste effectgrafiek maakt die is verpakt in een CanvasEffect type dat eenvoudig opnieuw kan worden gebruikt in uw toepassing.

Het effect ontwerpen

Voor deze demo willen we een eenvoudig vorstglaseffect creëren.

Dit omvat de volgende onderdelen:

  • Gaussiaanse vervaging
  • Tinteffect
  • Ruis (die we procedureel kunnen genereren met een shader)

We willen ook eigenschappen zichtbaar maken om de mate van vervaging en ruis te regelen. Het uiteindelijke effect bevat een verpakte versie van deze effectgrafiek en is eenvoudig te gebruiken door alleen een exemplaar te maken, deze eigenschappen in te stellen, een bronafbeelding te verbinden en deze vervolgens te tekenen. Laten we aan de slag gaan.

Een aangepaste D2D1-pixel-shader maken

Voor de ruis bovenop het effect kunnen we een eenvoudige D2D1 pixel shader gebruiken. De shader berekent een willekeurige waarde op basis van de coördinaten (die fungeert als een 'seed' voor het willekeurige getal) en vervolgens gebruikt deze ruiswaarde om de RGB-hoeveelheid voor die pixel te berekenen. We kunnen deze ruis vervolgens op de resulterende afbeelding mengen.

Als u de shader wilt schrijven met ComputeSharp, moeten we alleen een partial struct type definiëren dat de ID2D1PixelShader interface implementeert en vervolgens onze logica in de Execute methode schrijven. Voor deze ruis-shader kunnen we zoiets schrijven:

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

Opmerking

Hoewel de shader volledig in C# is geschreven, wordt basiskennis van HLSL (de programmeertaal voor DirectX-shaders, waarnaar ComputeSharp C# transpileert) aanbevolen.

Laten we deze shader in detail bekijken.

  • De shader heeft geen invoer, het produceert alleen een oneindige afbeelding met willekeurige grijswaardenruis.
  • De shader vereist toegang tot de huidige pixelcoördinaat.
  • De shader wordt vooraf gecompileerd tijdens de build (met behulp van het PixelShader40 profiel, dat gegarandeerd beschikbaar is op elke GPU waarop de toepassing kan worden uitgevoerd).
  • Het [D2DGeneratedPixelShaderDescriptor] kenmerk is nodig om de brongenerator te activeren die is gebundeld met ComputeSharp, waarmee de C#-code wordt geanalyseerd, naar HLSL wordt getranspileerd, de shader wordt gecompileerd naar bytecode, enzovoort.
  • De shader legt een float amount parameter vast via de primaire constructor. De brongenerator in ComputeSharp zorgt automatisch voor het extraheren van alle vastgelegde waarden in een shader en het voorbereiden van de constante buffer die D2D nodig heeft om de shaderstatus te initialiseren.

En dit deel is klaar! Deze shader genereert wanneer nodig onze aangepaste ruisktekstuur. Vervolgens moeten we ons verpakte effect maken met de effectgrafiek die al onze effecten met elkaar verbindt.

Een aangepast effect maken

Voor ons gebruiksvriendelijke, verpakte effect kunnen we het CanvasEffect type van ComputeSharp gebruiken. Dit type biedt een eenvoudige manier om alle benodigde logica in te stellen om een effectgrafiek te maken en bij te werken via openbare eigenschappen waarmee gebruikers van het effect kunnen communiceren. Er zijn twee belangrijke methoden die we moeten implementeren:

  • BuildEffectGraph: deze methode is verantwoordelijk voor het bouwen van de effectgrafiek die we willen tekenen. Dat wil gezegd, het moet alle effecten maken die we nodig hebben en het uitvoerknooppunt voor de grafiek registreren. Voor effecten die op een later tijdstip kunnen worden bijgewerkt, wordt de registratie uitgevoerd met een bijbehorende CanvasEffectNode<T> waarde, die fungeert als opzoeksleutel om de effecten op te halen uit de grafiek wanneer dat nodig is.
  • ConfigureEffectGraph: deze methode vernieuwt de effectgrafiek door de instellingen toe te passen die de gebruiker heeft geconfigureerd. Deze methode wordt automatisch aangeroepen wanneer dat nodig is, direct voordat het effect wordt getekend en alleen als ten minste één effecteigenschap is gewijzigd sinds de laatste keer dat het effect is gebruikt.

Ons aangepaste effect kan als volgt worden gedefinieerd:

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

U kunt zien dat er vier secties in deze klasse zijn:

  • Eerst hebben we velden om alle veranderlijke statussen bij te houden, zoals de effecten die kunnen worden bijgewerkt en de backingvelden voor alle effecteigenschappen die we beschikbaar willen maken voor gebruikers van het effect.
  • Vervolgens hebben we eigenschappen om het effect te configureren. De setter van elke eigenschap maakt gebruik van de SetAndInvalidateEffectGraph methode die wordt weergegeven door CanvasEffect, waardoor het effect automatisch ongeldig wordt als de waarde die wordt ingesteld anders is dan de huidige waarde. Dit zorgt ervoor dat het effect alleen opnieuw wordt geconfigureerd wanneer dat echt nodig is.
  • Tot slot hebben we de hierboven genoemde BuildEffectGraph en ConfigureEffectGraph methoden.

Opmerking

Het PremultiplyEffect-knooppunt na het ruiseffect is erg belangrijk: dit komt doordat Win2D-effecten ervan uitgaan dat de uitvoer voorgemultipliceerd is, terwijl pixel shaders over het algemeen werken met niet-voorgemultipliceerde pixels. Vergeet daarom niet om premultiply/unpremultiply-knooppunten handmatig in te voegen voor en na aangepaste shaders, om ervoor te zorgen dat kleuren correct worden bewaard.

Klaar om te tekenen!

En daarmee is ons aangepaste vorstglaseffect klaar! We kunnen het als volgt tekenen:

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

    args.DrawingSession.DrawImage(effect);
}

In dit voorbeeld tekenen we het effect van de Draw handler van een CanvasControl, waarbij CanvasBitmap we eerder als bron hebben geladen. Dit is de invoerafbeelding die we gebruiken om het effect te testen:

een foto van een paar bergen onder een bewolkte hemel

En dit is het resultaat:

een wazige versie van de bovenstaande afbeelding

Opmerking

Dank aan Dominic Lange voor de foto.

Aanvullende bronnen