Freigeben über


Implementieren von benutzerdefinierten Effekten

Win2D stellt mehrere APIs zur Darstellung von Objekten bereit, die gezeichnet werden können, die in zwei Kategorien unterteilt sind: Bilder und Effekte. Bilder, dargestellt durch die ICanvasImage Schnittstelle, haben keine Eingaben und können direkt auf einer bestimmten Oberfläche gezeichnet werden. Beispiel CanvasBitmapVirtualizedCanvasBitmapCanvasRenderTarget für Bildtypen. Effekte hingegen werden durch die ICanvasEffect Schnittstelle dargestellt. Sie können Eingaben sowie zusätzliche Ressourcen haben und beliebige Logikoperationen anwenden, um ihre Ausgaben zu erzeugen, wobei ein Effekt auch ein Bild sein kann. Win2D enthält Effekte, die die meisten D2D-Effekte umfassen, wie GaussianBlurEffect, TintEffect und LuminanceToAlphaEffect.

Bilder und Effekte können auch miteinander verkettet werden, um beliebige Diagramme zu erstellen, die dann in Ihrer Anwendung angezeigt werden können (siehe auch die D2D-Dokumente zu Direct2D-Effekten). Gemeinsam bieten sie ein extrem flexibles System, um komplexe Grafiken effizient zu erstellen. Es gibt jedoch Fälle, in denen die integrierten Effekte nicht ausreichen, und Sie können Ihren eigenen Win2D-Effekt erstellen. Um dies zu unterstützen, enthält Win2D eine Reihe leistungsstarker Interop-APIs, die das Definieren von benutzerdefinierten Bildern und Effekten ermöglichen, die nahtlos in Win2D integriert werden können.

Tipp

Wenn Sie C# verwenden und ein benutzerdefiniertes Effekt- oder Effektdiagramm implementieren möchten, empfiehlt es sich, ComputeSharp zu verwenden, anstatt einen Effekt von Grund auf neu zu implementieren. Im folgenden Absatz finden Sie eine ausführliche Erläuterung der Verwendung dieser Bibliothek zum Implementieren von benutzerdefinierten Effekten, die nahtlos in Win2D integriert werden.

Plattform-APIs:ICanvasImage, CanvasBitmap, VirtualizedCanvasBitmap, CanvasRenderTarget, CanvasEffect, GaussianBlurEffect, TintEffect, ICanvasLuminanceToAlphaEffectImage, IGraphicsEffectSource, ID2D21Image, ID2D1Factory1, ID2D1Effect

Implementieren einer benutzerdefinierten ICanvasImage

Das einfachste unterstützbare Szenario ist das Erstellen eines benutzerdefinierten ICanvasImage. Wie bereits erwähnt, ist dies die von Win2D definierte WinRT-Schnittstelle, die alle Arten von Bildern darstellt, mit denen Win2D interopieren kann. Diese Schnittstelle macht nur zwei GetBounds Methoden verfügbar und erweitert IGraphicsEffectSource, was eine Markerschnittstelle ist, die "einige Effektquelle" darstellt.

Wie Sie sehen können, gibt es keine "funktionalen" APIs, die von dieser Schnittstelle verfügbar gemacht werden, um tatsächlich eine Zeichnung auszuführen. Um Ihr eigenes ICanvasImage Objekt zu implementieren, müssen Sie auch die ICanvasImageInterop Schnittstelle implementieren, die alle erforderlichen Logik für Win2D zum Zeichnen des Bilds verfügbar macht. Dies ist eine COM-Schnittstelle, die in der öffentlichen Microsoft.Graphics.Canvas.native.h Kopfzeile definiert ist, die mit Win2D ausgeliefert wird.

Die Schnittstelle wird wie folgt definiert:

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

Und es basiert auch auf diesen beiden Aufzählungstypen, aus demselben 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
}

Die beiden Methoden GetDevice und GetD2DImage sind alles, was benötigt wird, um benutzerdefinierte Bilder (oder Effekte) zu implementieren. Sie bieten Win2D die Erweiterungspunkte, um sie auf einem bestimmten Gerät zu initialisieren und das zugrunde liegende D2D-Bild zum Rendern abzurufen. Die ordnungsgemäße Implementierung dieser Methoden ist wichtig, um sicherzustellen, dass die Dinge in allen unterstützten Szenarien ordnungsgemäß funktionieren.

Sehen wir uns an, wie die einzelnen Methoden funktionieren.

Implementierung von GetDevice

Die GetDevice Methode ist die einfachste der beiden. Was es tut, ruft das Canvasgerät ab, das dem Effekt zugeordnet ist, damit Win2D es bei Bedarf prüfen kann (z. B. um sicherzustellen, dass es mit dem verwendeten Gerät übereinstimmt). Der type Parameter gibt den "Zuordnungstyp" für das zurückgegebene Gerät an.

Es gibt zwei haupt mögliche Fälle:

  • Wenn das Bild ein Effekt ist, sollte es unterstützen, dass es auf mehreren Geräten "realisiert" und "nicht realisiert" wird. Was dies bedeutet: Ein bestimmter Effekt wird in einem nicht initialisierten Zustand erstellt, kann dann erkannt werden, wenn ein Gerät während der Zeichnung übergeben wird, und danach kann er weiterhin mit diesem Gerät verwendet werden oder auf ein anderes Gerät verschoben werden. In diesem Fall setzt der Effekt seinen internen Zustand zurück und erkennt sich dann wieder auf dem neuen Gerät. Dies bedeutet, dass sich das zugeordnete Canvas-Gerät im Laufe der Zeit ändern kann, und es kann auch null sein. Sollte type auf WIN2D_GET_DEVICE_ASSOCIATION_TYPE_REALIZATION_DEVICE festgelegt werden, und das zurückgegebene Gerät sollte auf das aktuelle Realisierungsgerät festgelegt werden, falls verfügbar.
  • Einige Bilder verfügen über ein einzelnes "eigenes Gerät", das zur Erstellungszeit zugewiesen ist und sich nie ändern kann. Dies wäre z. B. der Fall für ein Bild, das eine Textur darstellt, da dies auf einem bestimmten Gerät zugewiesen ist und nicht verschoben werden kann. Wenn GetDevice aufgerufen wird, sollte es das Erstellungsgerät zurückgeben und type auf WIN2D_GET_DEVICE_ASSOCIATION_TYPE_CREATION_DEVICE setzen. Beachten Sie, dass beim Angeben dieses Typs das zurückgegebene Gerät nicht sein nullsollte.

Hinweis

Win2D kann GetDevice aufrufen, während ein Effektgraph rekursiv durchlaufen wird, was bedeutet, dass mehrere aktive Aufrufe GetD2DImage im Stapel stattfinden können. Aus diesem GetDevice Gründen sollte keine Blockierungssperre für das aktuelle Bild verwendet werden, da dies potenziell zu einem Deadlock führen könnte. Stattdessen sollte sie eine erneute Sperrung auf nicht blockierende Weise verwenden und einen Fehler zurückgeben, wenn sie nicht erworben werden kann. Dadurch wird sichergestellt, dass derselbe Thread, der es rekursiv aufruft, es erfolgreich erwirbt, während gleichzeitig laufende Threads, die dasselbe tun, ordnungsgemäß fehlschlagen.

Implementierung GetD2DImage

GetD2DImage ist der Ort, an dem die meisten Arbeiten stattfinden. Diese Methode ist für das Abrufen des ID2D1Image Objekts verantwortlich, das Win2D zeichnen kann, optional die aktuelle Wirkung bei Bedarf zu erkennen. Dies umfasst auch rekursives Durchlaufen und Erkennen des Effektdiagramms für alle Quellen, falls vorhanden, sowie initialisieren eines Zustands, den das Bild möglicherweise benötigt (z. B. Konstantenpuffer und andere Eigenschaften, Ressourcentexturen usw.).

Die genaue Implementierung dieser Methode hängt stark vom Bildtyp ab und kann erheblich variieren, aber im Allgemeinen können Sie erwarten, dass die Methode für einen beliebigen Effekt die folgenden Schritte ausführt:

  • Überprüfen Sie, ob der Aufruf auf derselben Instanz rekursiv war, und brechen Sie den Prozess ab, falls dies der Fall ist. Dies ist erforderlich, um Zyklen in einem Effektdiagramm zu erkennen (z. B. hat Effekt A Effekt B als Quelle, und Effekt B hat Effekt A als Quelle).
  • Erwerben Sie eine Sperre für die Bildinstanz, um vor gleichzeitigem Zugriff zu schützen.
  • Behandeln Sie die Ziel-DPIs gemäß den Eingabe-Flags
  • Überprüfen Sie, ob das Eingabegerät dem verwendeten Gerät entspricht, falls vorhanden. Falls es nicht übereinstimmt und der aktuelle Effekt die Realisierung unterstützt, heben Sie den Effekt auf.
  • Erkennen Sie den Effekt auf dem Eingabegerät. Dies kann bei Bedarf die Registrierung des D2D-Effekts auf das ID2D1Factory1 Objekt umfassen, das vom Eingabegerät oder Gerätekontext abgerufen wurde. Darüber hinaus sollte der gesamte erforderliche Zustand für die erstellung der D2D-Effektinstanz festgelegt werden.
  • Durchlaufen Sie rekursiv alle Quellen und binden Sie sie an den D2D-Effekt.

In Bezug auf die Eingabekennzeichnungen gibt es mehrere mögliche Fälle, in denen benutzerdefinierte Effekte ordnungsgemäß behandelt werden sollten, um die Kompatibilität mit allen anderen Win2D-Effekten sicherzustellen. Mit Ausnahme von WIN2D_GET_D2D_IMAGE_FLAGS_NONE sind die folgenden Flags zu behandeln:

  • WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT: In diesem Fall ist device garantiert nicht null. Der Effekt sollte überprüfen, ob das Gerätekontextziel ein ID2D1CommandList ist, und falls ja, das WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION-Flag hinzufügen. Andernfalls sollte targetDpi (wobei garantiert ist, dass es nicht null ist) auf die aus dem Eingabekontext abgerufenen DPIs festgelegt werden. Dann sollte WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT aus den Flags entfernt werden.
  • WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION und WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION: wird beim Festlegen von Effektquellen verwendet (siehe Hinweise unten).
  • WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION: wenn festgelegt, überspringt rekursiv die Quellen des Effekts und gibt einfach den realisierten Effekt ohne andere Änderungen zurück.
  • WIN2D_GET_D2D_IMAGE_FLAGS_ALLOW_NULL_EFFECT_INPUTS: Wenn festgelegt, dürfen nullEffektquellen, die realisiert werden, sein, wenn der Benutzer sie noch nicht auf eine vorhandene Quelle festgelegt hat.
  • WIN2D_GET_D2D_IMAGE_FLAGS_UNREALIZE_ON_FAILURE: Wenn festgelegt ist und eine Effektquelle, die festgelegt wird, ungültig ist, sollte der Effekt vor dem Fehlschlagen unrealisiert werden. Das heißt, wenn der Fehler beim Auflösen der Effektquellen nach der Realisierung des Effekts aufgetreten ist, sollte der Effekt sich selbst rückgängig machen, bevor der Fehler an den Aufrufer zurückgegeben wird.

Im Hinblick auf die DPI-bezogenen Flags steuern diese, wie Effektquellen festgelegt werden. Um die Kompatibilität mit Win2D sicherzustellen, sollten Effekte bei Bedarf automatisch DPI-Kompensationseffekte zu ihren Eingaben hinzufügen. Sie können steuern, ob dies der Fall ist, auf folgende Weise:

  • Wenn WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION festgelegt ist, wird ein DPI-Ausgleichseffekt benötigt, wenn der inputDpi Parameter nicht 0 ist.
  • Andernfalls ist eine DPI-Kompensation erforderlich, wenn inputDpi nicht 0, WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION nicht festgelegt ist und entweder WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION festgelegt wird, oder die Eingabe-DPI-Werte und die Ziel-DPI-Werte stimmen nicht überein.

Diese Logik sollte angewendet werden, wenn eine Quelle realisiert und an eine Eingabe des aktuellen Effekts gebunden wird. Beachten Sie, dass beim Hinzufügen eines DPI-Kompensationseffekts die Eingabe auf das zugrunde liegende D2D-Bild festgelegt sein sollte. Wenn der Benutzer jedoch versucht, den WinRT-Wrapper für diese Quelle abzurufen, sollte der Effekt darauf achten, zu erkennen, ob ein DPI-Effekt verwendet wurde, und stattdessen einen Wrapper für das ursprüngliche Quellobjekt zurückzugeben. Dies bedeutet, dass DPI-Kompensationseffekte für Benutzer des Effekts transparent sein sollten.

Nachdem alle Initialisierungslogik ausgeführt wurde, sollte das resultierende ID2D1Image (ähnlich wie bei Win2D-Objekten, ist auch ein D2D-Effekt ein Bild) bereit sein, von Win2D im Zielkontext gezeichnet zu werden. Derzeit ist dieser Zielkontext dem Aufrufer jedoch noch nicht bekannt.

Hinweis

Die ordnungsgemäße Implementierung dieser Methode (und ICanvasImageInterop im Allgemeinen) ist äußerst kompliziert, und es ist nur für fortgeschrittene Benutzer gedacht, die unbedingt die zusätzliche Flexibilität benötigen. Ein solides Verständnis von D2D, Win2D, COM, WinRT und C++ wird empfohlen, bevor Sie versuchen, eine ICanvasImageInterop Implementierung zu schreiben. Wenn Ihr benutzerdefinierter Win2D-Effekt auch einen benutzerdefinierten D2D-Effekt umschließen muss, müssen Sie auch Ihr eigenes ID2D1Effect Objekt implementieren (weitere Informationen hierzu finden Sie in den D2D-Dokumenten zu benutzerdefinierten Effekten ). Diese Dokumente sind keine vollständige Beschreibung aller erforderlichen Logik (z. B. behandeln sie nicht, wie Effektquellen über die D2D/Win2D-Grenze ge marshallt und verwaltet werden sollen), daher wird empfohlen, die CanvasEffect Implementierung in der Codebasis von Win2D als Referenzpunkt für einen benutzerdefinierten Effekt zu verwenden und nach Bedarf zu ändern.

Implementieren GetBounds

Die letzte fehlende Komponente zum vollständigen Implementieren eines benutzerdefinierten ICanvasImage Effekts besteht darin, die beiden GetBounds Überladungen zu unterstützen. Um dies einfach zu machen, stellt Win2D einen C-Export bereit, der verwendet werden kann, um die vorhandene Logik von Win2D auf jedes benutzerdefinierte Bild anzuwenden. Der Export lautet wie folgt:

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

Benutzerdefinierte Bilder können diese API aufrufen und sich als image Parameter übergeben und dann einfach das Ergebnis an ihre Aufrufer zurückgeben. Der transform Parameter kann sein null, wenn keine Transformation verfügbar ist.

Optimieren von Gerätekontextzugriffen

Der Parameter deviceContext in ICanvasImageInterop::GetD2DImage kann manchmal null sein, wenn ein Kontext vor dem Aufruf nicht sofort verfügbar ist. Dies geschieht absichtlich, sodass ein Kontext nur träge erstellt wird, wenn er tatsächlich benötigt wird. Wenn ein Kontext verfügbar ist, übergibt Win2D diesen an den GetD2DImage Aufruf, andernfalls können die aufgerufenen Funktionen bei Bedarf einen eigenen Kontext erstellen.

Das Erstellen eines Gerätekontexts ist relativ teuer, daher stellt Win2D APIs bereit, um den Zugriff auf den internen Gerätekontextpool zu beschleunigen. Auf diese Weise können benutzerdefinierte Effekte Gerätekontexte ausleihen und zurückgeben, die einem bestimmten Canvasgerät auf effiziente Weise zugeordnet sind.

Die APIs für Gerätekontext-Leasing sind wie folgt definiert:

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

Die ID2D1DeviceContextPool Schnittstelle wird durch CanvasDeviceden Win2D-Typ implementiert, der die ICanvasDevice Schnittstelle implementiert. Um den Pool zu verwenden, verwenden Sie die Geräteschnittstelle QueryInterface, um einen ID2D1DeviceContextPool Verweis abzurufen, und rufen Sie dann ID2D1DeviceContextPool::GetDeviceContextLease auf, um ein ID2D1DeviceContextLease Objekt abzurufen, damit Sie auf den Gerätekontext zugreifen können. Sobald das nicht mehr benötigt wird, geben Sie die Lease frei. Stellen Sie sicher, dass Sie den Gerätekontext nicht berühren, nachdem die Lease freigegeben wurde, da sie möglicherweise gleichzeitig von anderen Threads verwendet wird.

Aktivierung der Suche nach WinRT-Wrappern

Wie in den Win2D-Interopdokumenten zu sehen, macht der öffentliche Win2D-Header auch eine GetOrCreate Methode verfügbar (auf die über die ICanvasFactoryNative Aktivierungsfactory oder über die GetOrCreate im selben Header definierten C++/CX-Hilfsprogramme zugegriffen werden kann). Dadurch kann ein WinRT-Wrapper aus einer bestimmten systemeigenen Ressource abgerufen werden. So können Sie z. B. eine CanvasDevice Instanz aus einem ID2D1Device1 Objekt, einer CanvasBitmap aus einem ID2D1BitmapUsw. abrufen oder erstellen.

Diese Methode funktioniert auch für alle integrierten Win2D-Effekte: Wenn Sie die systemeigene Ressource für einen bestimmten Effekt abrufen und diese dann zum Abrufen des entsprechenden Win2D-Wrappers verwenden, wird der eigene Win2D-Effekt korrekt zurückgegeben. Damit benutzerdefinierte Effekte auch vom gleichen Zuordnungssystem profitieren können, stellt Win2D mehrere APIs über die Interop-Schnittstelle der Aktivierungsfabrik zur VerfügungCanvasDevice, die der TypICanvasFactoryNative ist, sowie eine zusätzliche Effekt-FactoryICanvasEffectFactoryNative-Schnittstelle.

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

Es gibt mehrere APIs, die hier berücksichtigt werden müssen, da sie erforderlich sind, um alle verschiedenen Szenarien zu unterstützen, in denen Win2D-Effekte verwendet werden können, sowie wie Entwickler mit der D2D-Ebene interopieren können, und dann versuchen, Wrapper für sie aufzulösen. Lassen Sie uns die einzelnen APIs übergehen.

Die RegisterWrapper-Methoden und die UnregisterWrapper-Methoden sollen von benutzerdefinierten Effekten aufgerufen werden, um sich dem internen Win2D-Cache hinzuzufügen.

  • RegisterWrapper: registriert eine systemeigene Ressource und einen eigenen WinRT-Wrapper. Der Parameter wrapper muss auch IWeakReferenceSource implementiert sein, sodass dieser korrekt zwischengespeichert werden kann, ohne Referenzzyklen zu verursachen, die zu Speicherlecks führen. Die Methode gibt S_OK zurück, wenn die systemeigene Ressource dem Cache hinzugefügt werden konnte, S_FALSE, wenn bereits ein registrierter Wrapper für resource vorhanden war, und einen Fehlercode, wenn ein Fehler auftritt.
  • UnregisterWrapper: hebt die Registrierung einer systemeigenen Ressource und des zugehörigen Wrappers auf. Gibt S_OK zurück, wenn die Ressource entfernt werden konnte, S_FALSE, wenn resource noch nicht registriert war, und einen Fehlercode, wenn ein anderer Fehler auftritt.

Benutzerdefinierte Effekte sollten RegisterWrapper und UnregisterWrapper aufrufen, wann immer sie aktiviert oder deaktiviert werden, d. h. wenn eine neue native Ressource erstellt und ihnen zugeordnet wird. Benutzerdefinierte Effekte, die die Realisierung nicht unterstützen (z. B. solche, die ein fest zugeordnetes Gerät haben), können RegisterWrapper und UnregisterWrapper aufrufen, wenn sie erstellt und zerstört werden. Benutzerdefinierte Effekte sollten sicherstellen, dass sie sich von allen möglichen Codepfaden korrekt abmelden, die dazu führen würden, dass der Wrapper ungültig wird (z. B. einschließlich, wenn das Objekt finalisiert wird, falls es in einer verwalteten Sprache implementiert ist).

Die RegisterEffectFactory- und UnregisterEffectFactory-Methoden sind auch zur Verwendung durch benutzerdefinierte Effekte vorgesehen, damit diese einen Rückruf registrieren können, um einen neuen Wrapper zu erstellen, falls ein Entwickler versucht, einen für eine "verwaiste" D2D-Ressource freizugeben.

  • RegisterEffectFactory: Registrieren Sie einen Rückruf, der dieselben Parameter übernimmt, die ein Entwickler an GetOrCreate übergeben hat, und erstellt einen neuen inspizierbaren Wrapper für den Eingabeffekt. Die Effekt-ID wird als Schlüssel verwendet, so dass jede benutzerdefinierte Wirkung beim ersten Laden eine Fabrik dafür registrieren kann. Natürlich sollte dies nur einmal pro Effekttyp erfolgen, und nicht jedes Mal, wenn der Effekt realisiert wird. Die Parameter device, resource und wrapper werden von Win2D überprüft, bevor eine registrierte Rückruffunktion aufgerufen wird, sodass sie garantiert nicht null sind, wenn CreateWrapper aufgerufen wird. Dies dpi gilt als optional und kann ignoriert werden, falls der Effekttyp keine bestimmte Verwendung dafür hat. Beachten Sie, dass beim Erstellen eines neuen Wrappers aus einer registrierten Factory auch sichergestellt werden sollte, dass der neue Wrapper im Cache registriert ist (Win2D fügt dem Cache nicht automatisch Wrapper hinzu, die von externen Fabriken erstellt werden).
  • UnregisterEffectFactory: entfernt einen zuvor registrierten Rückruf. Dieser Ansatz kann beispielsweise verwendet werden, wenn ein Effekt-Wrapper in einer verwalteten Assembly implementiert wird, die gerade entladen wird.

Hinweis

ICanvasFactoryNative wird von der Aktivierungsfactory für CanvasDevice implementiert, die Sie entweder manuell durch Aufruf von RoGetActivationFactory abrufen können, oder durch die Verwendung von Hilfs-APIs aus den Spracherweiterungen, die Sie verwenden (z. B. winrt::get_activation_factory in C++/WinRT). Weitere Informationen zur Funktionsweise finden Sie im WinRT-Typsystem.

Ein praktisches Beispiel dafür, wo diese Zuordnung ins Spiel kommt, sollten Sie berücksichtigen, wie integrierte Win2D-Effekte funktionieren. Wenn sie nicht realisiert werden, werden alle Zustände (z. B. Eigenschaften, Quellen usw.) in einem internen Cache in jeder Effektinstanz gespeichert. Wenn sie realisiert sind, wird der gesamte Zustand auf die native Ressource übertragen (z. B. werden Eigenschaften auf den D2D-Effekt gesetzt, alle Quellen aufgelöst und den Effekteingaben zugeordnet usw.), und solange der Effekt realisiert ist, fungiert er als Autorität für den Zustand des Wrappers. Das heißt, wenn der Wert einer Eigenschaft aus dem Wrapper abgerufen wird, ruft er den aktualisierten Wert für sie aus der systemeigenen D2D-Ressource ab, die ihr zugeordnet ist.

Dadurch wird sichergestellt, dass die Änderungen, die direkt an der D2D-Ressource vorgenommen werden, auch für den äußeren Wrapper sichtbar sind und die beiden niemals "nicht synchronisiert" sind. Wenn der Effekt nicht realisiert wird, wird der gesamte Zustand von der nativen Ressource zurück in den Wrapperzustand übertragen, bevor die Ressource freigegeben wird. Sie wird dort aufbewahrt und aktualisiert, bis der Effekt das nächste Mal realisiert wird. Betrachten Sie nun diese Abfolge von Ereignissen:

  • Sie verfügen über einen Win2D-Effekt, der entweder integriert oder benutzerdefiniert ist.
  • Sie erhalten die ID2D1Image von ihr (was ein ID2D1Effect ist).
  • Sie erstellen eine Instanz eines benutzerdefinierten Effekts.
  • Sie erhalten auch das ID2D1Image von dort.
  • Sie legen dieses Bild manuell als Eingabe für den vorherigen Effekt fest (über ID2D1Effect::SetInput).
  • Dann fragen Sie nach dem ersten Effekt für den WinRT-Wrapper für diese Eingabe.

Da der Effekt realisiert wird (er wurde erkannt, als die native Ressource angefordert wurde), wird sie die native Ressource als Quelle der Wahrheit verwenden. Daher wird die ID2D1Image entsprechende Quelle abgerufen und versucht, den WinRT-Wrapper dafür abzurufen. Wenn der Effekt, aus dem diese Eingabe abgerufen wurde, ordnungsgemäß ein eigenes Paar systemeigener Ressourcen und WinRT-Wrapper zum Win2D-Cache hinzugefügt hat, wird der Wrapper aufgelöst und an Aufrufer zurückgegeben. Wenn nicht, schlägt dieser Eigenschaftszugriff fehl, da Win2D WinRT-Wrapper für Effekte, die es nicht besitzt, nicht auflösen kann, da es nicht weiß, wie diese instanziiert werden.

Hierbei greifen RegisterWrapper und UnregisterWrapper ein, da sie es benutzerdefinierten Effekten ermöglichen, nahtlos an der Wrapper-Entschlüsselungslogik von Win2D teilzunehmen, sodass der richtige Wrapper immer für jede Effektquelle abgerufen werden kann, unabhängig davon, ob sie von WinRT-APIs oder direkt aus der zugrunde liegenden D2D-Ebene festgelegt wurde.

Berücksichtigen Sie dieses Szenario, um zu erläutern, wie die Effektfabriken ebenfalls ins Spiel kommen:

  • Ein Benutzer erstellt eine Instanz eines benutzerdefinierten Wrappers und realisiert ihn.
  • Anschließend holen sie einen Verweis auf den zugrunde liegenden D2D-Effekt ein und behalten ihn bei.
  • Anschließend wird der Effekt auf einem anderen Gerät realisiert. Der Effekt wird unrealisiert und wieder realisiert, und dadurch entsteht ein neuer D2D-Effekt. Der vorherige D2D-Effekt hat jetzt keinen zugeordneten inspizierbaren Wrapper mehr.
  • Der Benutzer ruft GetOrCreate dann den ersten D2D-Effekt auf.

Ohne ein Callback könnte Win2D einen Wrapper nicht auflösen, da kein registrierter Wrapper dafür vorhanden ist. Wenn stattdessen eine Factory registriert ist, kann ein neuer Wrapper für diesen D2D-Effekt erstellt und zurückgegeben werden, sodass das Szenario nahtlos für den Benutzer weiter funktioniert.

Implementieren einer benutzerdefinierten ICanvasEffect

Die Win2D-Schnittstelle ICanvasEffect erweitert ICanvasImage, sodass auch alle vorherigen Punkte auf benutzerdefinierte Effekte angewendet werden. Der einzige Unterschied besteht darin, dass ICanvasEffect auch zusätzliche Methoden implementiert, die spezifisch für Effekte sind, wie z. B. das Ungültigmachen eines Quellrechtecks, das Abrufen der benötigten Rechtecke usw.

Um dies zu unterstützen, macht Win2D C-Exporte verfügbar, die Autoren von benutzerdefinierten Effekten verwenden können, damit sie nicht alle diese zusätzliche Logik von Grund auf neu anwenden müssen. Dies funktioniert auf die gleiche Weise wie der C-Export für GetBounds. Hier sind die verfügbaren Exporte für Effekte:

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

Schauen wir uns an, wie sie verwendet werden können:

  • InvalidateSourceRectangleForICanvasImageInterop soll InvalidateSourceRectangle unterstützen. Stellen Sie einfach die Eingabeparameter zusammen und rufen Sie die Funktion direkt auf, dann kümmert sie sich um alle erforderlichen Aufgaben. Beachten Sie, dass der image Parameter die aktuelle Effektinstanz ist, die implementiert wird.
  • GetInvalidRectanglesForICanvasImageInterop unterstützt GetInvalidRectangles. Dies erfordert auch keine besondere Berücksichtigung, außer das Löschen des zurückgegebenen COM-Arrays, sobald es nicht mehr benötigt wird.
  • GetRequiredSourceRectanglesForICanvasImageInterop ist eine freigegebene Methode, die sowohl GetRequiredSourceRectangle als auch GetRequiredSourceRectangles unterstützen kann. Das heißt, es benötigt einen Zeiger auf ein vorhandenes Array von Werten, um aufzufüllen, sodass Aufrufer entweder einen Zeiger an einen einzelnen Wert übergeben können (der sich auch im Stapel befinden kann, um eine Zuordnung zu vermeiden) oder an ein Array von Werten. Die Implementierung ist in beiden Fällen identisch, sodass ein einzelner C-Export ausreicht, um beide zu aktivieren.

Benutzerdefinierte Effekte in C# mit ComputeSharp

Wie bereits erwähnt, empfiehlt es sich, die ComputeSharp-Bibliothek zu verwenden, wenn Sie C# verwenden und einen benutzerdefinierten Effekt implementieren möchten. Sie können sowohl benutzerdefinierte D2D1-Pixelshader vollständig in C# implementieren als auch benutzerdefinierte Effektdiagramme definieren, die mit Win2D kompatibel sind. Dieselbe Bibliothek wird auch im Microsoft Store verwendet, um mehrere Grafikkomponenten in der Anwendung zu nutzen.

Sie können in Ihrem Projekt über NuGet einen Verweis auf ComputeSharp hinzufügen:

Hinweis

Viele APIs in ComputeSharp.D2D1.* sind in den UWP- und WinUI-Zielen identisch, wobei der einzige Unterschied der Namespace ist (endend entweder .Uwp oder .WinUI). Das UWP-Ziel wird jedoch dauerhaft gewartet und erhält keine neuen Funktionen. Daher können einige Codeänderungen im Vergleich zu den hier gezeigten Beispielen für WinUI erforderlich sein. Die Codeausschnitte in diesem Dokument spiegeln die API-Oberfläche ab ComputeSharp.D2D1.WinUI.0.0 wider (die letzte Version für das UWP-Ziel ist 2.1.0).

Es gibt zwei Hauptkomponenten in ComputeSharp für die Interoperabilität mit Win2D:

  • PixelShaderEffect<T>: ein Win2D-Effekt, der von einem D2D1-Pixelshader unterstützt wird. Der Shader selbst wird in C# mithilfe der von ComputeSharp bereitgestellten APIs geschrieben. Diese Klasse bietet auch Eigenschaften zum Festlegen von Effektquellen, Konstantenwerten und mehr.
  • CanvasEffect: eine Basisklasse für benutzerdefinierte Win2D-Effekte, die ein beliebiges Effektdiagramm umschließen. Es kann verwendet werden, um komplexe Effekte in einem einfach zu verwendenden Objekt zu "packen", das in mehreren Teilen einer Anwendung wiederverwendet werden kann.

Nachfolgend sehen Sie ein Beispiel für einen benutzerdefinierten Pixel-Shader (portiert von diesem Shadertoy-Shader), der mit PixelShaderEffect<T> verwendet und dann auf ein Win2D-Objekt CanvasControl gezeichnet wird (beachten Sie, dass PixelShaderEffect<T>ICanvasImage implementiert):

Ein Beispiel-Pixelshader, der unendlich farbige Sechsecke anzeigt, die auf ein Win2D-Steuerelement gezeichnet und in einem App-Fenster in Betrieb angezeigt werden

In nur zwei Codezeilen können Sie einen Effekt erstellen und über Win2D zeichnen. ComputeSharp kümmert sich um alle Erforderlichen, um den Shader zu kompilieren, zu registrieren und die komplexe Lebensdauer eines Win2D-kompatiblen Effekts zu verwalten.

Als Nächstes sehen wir uns eine schrittweise Anleitung zum Erstellen eines benutzerdefinierten Win2D-Effekts an, der auch einen benutzerdefinierten D2D1-Pixelshader verwendet. Es wird erläutert, wie Sie einen Shader mit ComputeSharp erstellen und seine Eigenschaften einrichten und dann ein benutzerdefiniertes Effektdiagramm erstellen, das in einem CanvasEffect Typ verpackt ist, der in Ihrer Anwendung problemlos wiederverwendet werden kann.

Entwerfen des Effekts

Für diese Demo wollen wir einen einfachen Frostglaseffekt schaffen.

Dazu gehören die folgenden Komponenten:

  • Gaussischer Weichzeichner
  • Farbtoneffekt
  • Rauschen (das wir mit einem Shader prozedural generieren können)

Außerdem möchten wir Eigenschaften bereitstellen, um die Menge an Unschärfe und Rauschen zu steuern. Der endgültige Effekt enthält eine "verpackte" Version dieses Effektdiagramms und ist einfach zu verwenden, indem sie einfach eine Instanz erstellen, diese Eigenschaften festlegen, ein Quellbild verbinden und dann zeichnen. Legen wir los.

Erstellen eines benutzerdefinierten D2D1-Pixelshadrs

Für das Rauschen über dem Effekt können wir einen einfachen D2D1-Pixelshader verwenden. Der Shader berechnet einen Zufallswert basierend auf seinen Koordinaten (die als "Seed" für die Zufallszahl fungieren) und verwendet dann diesen Rauschwert, um den RGB-Wert für dieses Pixel zu berechnen. Wir können dieses Rauschen dann über das resultierende Bild überlagern.

Um den Shader mit ComputeSharp zu schreiben, müssen wir nur einen partial struct Typ definieren, der die ID2D1PixelShader Schnittstelle implementiert, und dann die Logik in der Execute Methode schreiben. Für diesen Noise-Shader können wir etwas wie folgt schreiben:

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

Hinweis

Während der Shader vollständig in C# geschrieben ist, werden grundlegende Kenntnisse in HLSL (die Programmiersprache für DirectX-Shader, in die ComputeSharp C# transpiliert) empfohlen.

Lassen Sie uns diesen Shader im Detail durchgehen:

  • Der Shader hat keine Eingaben, es erzeugt nur ein unendliches Bild mit zufälligen Graustufen-Rauschen.
  • Der Shader erfordert Zugriff auf die aktuelle Pixelkoordinate.
  • Der Shader wird zur Buildzeit vorkompiliert (mit dem PixelShader40 Profil, das garantiert auf jeder GPU verfügbar ist, in der die Anwendung ausgeführt werden kann).
  • Das [D2DGeneratedPixelShaderDescriptor] Attribut wird benötigt, um den Mit ComputeSharp gebündelten Quellgenerator auszulösen, der den C#-Code analysiert, in HLSL transpiliert, den Shader in Bytecode kompiliert usw.
  • Der Shader erfasst einen float amount Parameter über den primären Konstruktor. Der Quellgenerator in ComputeSharp kümmert sich automatisch darum, alle erfassten Werte in einem Shader zu extrahieren und den Konstantenpuffer vorzubereiten, den D2D zum Initialisieren des Shaderzustands benötigt.

Und dieser Teil ist erledigt! Dieser Shader generiert die benutzerdefinierte Rauschtextur, sobald sie benötigt wird. Als Nächstes müssen wir unseren verpackten Effekt mit dem Effektdiagramm erstellen, das alle unsere Effekte miteinander verbindet.

Erstellen eines benutzerdefinierten Effekts

Für unseren benutzerfreundlichen, integrierten Effekt können wir den CanvasEffect Typ von ComputeSharp verwenden. Dieser Typ bietet eine einfache Möglichkeit, alle erforderlichen Logik zum Erstellen eines Effektdiagramms einzurichten und über öffentliche Eigenschaften zu aktualisieren, mit denen Benutzer des Effekts interagieren können. Es gibt zwei Hauptmethoden, die wir implementieren müssen:

  • BuildEffectGraph: Diese Methode ist für das Erstellen des Effektdiagramms verantwortlich, das wir zeichnen möchten. Das heißt, es muss alle benötigten Effekte erstellen und den Ausgabeknoten für das Diagramm registrieren. Bei Effekten, die zu einem späteren Zeitpunkt aktualisiert werden können, erfolgt die Registrierung mit einem zugeordneten CanvasEffectNode<T> Wert, der als Nachschlageschlüssel fungiert, um die Effekte bei Bedarf aus dem Diagramm abzurufen.
  • ConfigureEffectGraph: Mit dieser Methode wird das Effektdiagramm aktualisiert, indem die Vom Benutzer konfigurierten Einstellungen angewendet werden. Diese Methode wird bei Bedarf automatisch aufgerufen, direkt vor dem Zeichnen des Effekts und nur, wenn seit der letzten Verwendung des Effekts mindestens eine Effekteigenschaft geändert wurde.

Unser benutzerdefinierter Effekt kann wie folgt definiert werden:

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

Sie können sehen, dass in dieser Klasse vier Abschnitte vorhanden sind:

  • Zunächst haben wir Felder, um den gesamten veränderbaren Zustand nachzuverfolgen, z. B. die Effekte, die aktualisiert werden können, sowie die Sicherungsfelder für alle Effekteigenschaften, die benutzern des Effekts verfügbar gemacht werden sollen.
  • Als Nächstes haben wir Eigenschaften zum Konfigurieren des Effekts. Der Setter jeder Eigenschaft verwendet die SetAndInvalidateEffectGraph Methode, die von CanvasEffect verfügbar gemacht wird, wodurch der Effekt automatisch ungültig wird, wenn der gesetzte Wert anders als der aktuelle ist. Dadurch wird sichergestellt, dass der Effekt nur bei Bedarf wieder konfiguriert wird.
  • Schließlich haben wir die oben genannten Methoden BuildEffectGraph und ConfigureEffectGraph.

Hinweis

Der PremultiplyEffect Knoten nach dem Rauscheffekt ist sehr wichtig: Dies liegt daran, dass Win2D-Effekte annehmen, dass die Ausgabe prämultipliziert ist, während Pixel-Shader in der Regel mit nicht prämultiplizierten Pixeln arbeiten. Denken Sie daran, manuell vor und nach benutzerdefinierten Shadern Premultiply/Unpremultiply-Knoten einzufügen, um sicherzustellen, dass Farben ordnungsgemäß beibehalten werden.

Hinweis

Dieser Beispieleffekt verwendet WinUI-Namespaces, aber derselbe Code kann auch für UWP verwendet werden. In diesem Fall wird der Namespace für ComputeSharp mit dem Paketnamen übereinstimmen.

Bereit zum Zeichnen!

Und damit ist unser benutzerdefinierter Frostglaseffekt fertig! Wir können es ganz einfach wie folgt zeichnen:

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

    args.DrawingSession.DrawImage(effect);
}

In diesem Beispiel nutzen wir den Effekt des Draw Handlers eines CanvasControl, indem wir das CanvasBitmap, das wir zuvor als Quelle geladen haben, verwenden. Dies ist das Eingabebild, das zum Testen des Effekts verwendet wird:

Ein Bild von einigen Bergen unter einem bewölkten Himmel

Und hier ist das Ergebnis:

eine verschwommene Version des Bilds oben

Hinweis

Dank an Dominic Lange für das Bild.

Zusätzliche Ressourcen