Udostępnij za pośrednictwem


Implementowanie efektów niestandardowych

Win2D udostępnia kilka interfejsów API do reprezentowania obiektów, które można narysować, które są podzielone na dwie kategorie: obrazy i efekty. Obrazy reprezentowane przez interfejs ICanvasImage nie mają żadnych danych wejściowych i mogą być bezpośrednio rysowane na danej powierzchni. Na przykład CanvasBitmap, VirtualizedCanvasBitmap i CanvasRenderTarget są przykładami typów obrazów. Z drugiej strony efekty są reprezentowane przez interfejs ICanvasEffect. Mogą mieć dane wejściowe, a także dodatkowe zasoby i zastosować dowolną logikę w celu wygenerowania danych wyjściowych (ponieważ efekt jest również obrazem). Win2D zawiera efekty opakowujące większość efektów D2D, takich jak GaussianBlurEffect, TintEffect i LuminanceToAlphaEffect.

Obrazy i efekty mogą być również połączone w łańcuch, aby utworzyć dowolne wykresy, które następnie można wyświetlić w aplikacji (zapoznaj się również z dokumentacją D2D w efektów Direct2D). Razem zapewniają one niezwykle elastyczny system do tworzenia złożonych grafik w wydajny sposób. Jednak istnieją przypadki, w których wbudowane efekty nie są wystarczające i możesz chcieć utworzyć własny efekt Win2D. Aby to wesprzeć, Win2D zawiera zestaw zaawansowanych interfejsów API międzyoperacyjności, które pozwalają na definiowanie niestandardowych obrazów i efektów, mogących płynnie integrować się z Win2D.

Wskazówka

Jeśli używasz języka C# i chcesz zaimplementować niestandardowy efekt lub graf efektu, zaleca się użycie ComputeSharp zamiast próbować tworzyć efekt od podstaw. Zobacz poniższy akapit, aby uzyskać szczegółowe wyjaśnienie sposobu używania tej biblioteki do implementowania efektów niestandardowych, które bezproblemowo integrują się z win2D.

API platformy :ICanvasImage, CanvasBitmap, VirtualizedCanvasBitmap, CanvasRenderTarget, CanvasEffect, GaussianBlurEffect, TintEffect, ICanvasLuminanceToAlphaEffectImage, IGraphicsEffectSource, ID2D21Image, ID2D1Factory1, ID2D1Effect

Implementowanie niestandardowej ICanvasImage

Najprostszym scenariuszem do obsługi jest utworzenie niestandardowego ICanvasImage. Jak wspomnieliśmy, jest to interfejs WinRT zdefiniowany przez Win2D, który reprezentuje wszystkie rodzaje obrazów, z którymi win2D może współdziałać. Ten interfejs uwidacznia tylko dwie metody GetBounds i rozszerza IGraphicsEffectSource, który jest interfejsem znacznika reprezentującym "jakieś źródło efektu".

Jak widać, ten interfejs nie ujawnia żadnych "funkcjonalnych" API, które faktycznie wykonują rysowanie. Aby zaimplementować własny obiekt ICanvasImage, należy również zaimplementować interfejs ICanvasImageInterop, który uwidacznia całą niezbędną logikę do rysowania obrazu w systemie Win2D. Jest to interfejs COM zdefiniowany w publicznym nagłówku Microsoft.Graphics.Canvas.native.h dostarczanym przez Win2D.

Interfejs jest definiowany w następujący sposób:

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

Opiera się również na tych dwóch typach wyliczenia z tego samego nagłówka:

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
}

Dwie metody GetDevice i GetD2DImage są wszystkim, co jest potrzebne do zaimplementowania niestandardowych obrazów (lub efektów), ponieważ umożliwiają one Win2D dostarczanie punktów rozszerzalności do inicjalizacji ich na danym urządzeniu oraz pobierania źródłowego obrazu D2D do rysowania. Prawidłowe zaimplementowanie tych metod ma kluczowe znaczenie, aby upewnić się, że wszystko będzie działać prawidłowo we wszystkich obsługiwanych scenariuszach.

Przyjrzyjmy się im, aby zobaczyć, jak działa każda metoda.

Implementowanie GetDevice

Metoda GetDevice jest najprostsza z tych dwóch. To, co robi, to pobieranie urządzenia kanwy powiązanego z efektem, dzięki czemu Win2D może je sprawdzić w razie potrzeby (na przykład, aby upewnić się, że pasuje do używanego urządzenia). Parametr type wskazuje "typ skojarzenia" dla zwróconego urządzenia.

Istnieją dwa główne możliwe przypadki:

  • Jeśli grafika jest efektem, powinna mieć możliwość zmiany stanu na "zrealizowany" i "niezrealizowany" na wielu urządzeniach. Oznacza to, że dany efekt powstaje w stanie niezainicjowanym, po czym można go aktywować, gdy urządzenie jest przekazywane podczas rysowania; może być następnie używane z tym urządzeniem lub przeniesione do innego. W takim przypadku efekt zresetuje swój stan wewnętrzny, a następnie ponownie zrealizuje się na nowym urządzeniu. Oznacza to, że skojarzone urządzenie kanwy może się zmieniać w czasie i może również być null. W związku z tym należy ustawić type na WIN2D_GET_DEVICE_ASSOCIATION_TYPE_REALIZATION_DEVICE, a zwrócone urządzenie powinno być ustawione na bieżące urządzenie realizacji, jeśli jest dostępne.
  • Niektóre obrazy mają jedno "urządzenie będące właścicielem", które jest przypisane podczas tworzenia i nigdy nie może ulec zmianie. Na przykład w przypadku obrazu reprezentującego teksturę, ponieważ jest to przydzielone na określonym urządzeniu i nie można go przenieść. Po wywołaniu GetDevice powinien zwrócić utworzone urządzenie i ustawić type na WIN2D_GET_DEVICE_ASSOCIATION_TYPE_CREATION_DEVICE. Należy pamiętać, że po określeniu tego typu zwrócone urządzenie nie powinno być null.

Uwaga / Notatka

Win2D może wywoływać GetDevice podczas rekursywnego przechodzenia grafu efektów, co oznacza, że w stosie może istnieć wiele aktywnych wywołań do GetD2DImage. W związku z tym GetDevice nie powinien przyjmować blokującej blokady na obecnym obrazie, ponieważ może to spowodować zakleszczenie. Zamiast tego powinno się użyć blokady ponownego wchodzenia w sposób nieblokujący i zwrócić błąd, jeśli nie można jej uzyskać. Gwarantuje to, że ten sam wątek rekursywnie wywołujący go pomyślnie go uzyska, podczas gdy równoległe wątki wykonujące te samo działanie przegrają bez problemu.

Implementowanie GetD2DImage

GetD2DImage jest miejscem, w którym odbywa się większość pracy. Ta metoda jest odpowiedzialna za pobieranie obiektu ID2D1Image, który Win2D może rysować, opcjonalnie realizując bieżący efekt, jeśli jest to konieczne. Obejmuje to również rekurencyjne przechodzenie i realizację grafu efektów dla wszystkich źródeł, jeśli istnieją, a także inicjalizację dowolnego stanu, którego może potrzebować obraz (np. bufory stałe i inne właściwości, tekstury zasobów itp.).

Dokładna implementacja tej metody jest bardzo zależna od typu obrazu i może się znacznie różnić, ale ogólnie rzecz biorąc dla dowolnego efektu można oczekiwać, że metoda wykona następujące kroki:

  • Sprawdź, czy wywołanie było rekurencyjne w tym samym wystąpieniu, i w takim przypadku zakończ działanie. Jest to potrzebne do wykrywania cykli na wykresie efektów (np. efekt A ma wpływ B jako źródło, a efekt B ma wpływ A jako źródło).
  • Uzyskaj blokadę na konkretne wystąpienie obrazu, aby chronić przed równoczesnym dostępem.
  • Zarządzaj docelowymi wartościami DPI zgodnie z flagami wejściowymi
  • Sprawdź, czy urządzenie wejściowe jest zgodne z używanym urządzeniem, jeśli istnieje. Jeśli nie pasuje, a bieżący efekt wspiera realizację, nierealizuj efektu.
  • Zdaj sobie sprawę z wpływu na urządzenie wejściowe. Może to obejmować rejestrowanie wpływu D2D na obiekt ID2D1Factory1 pobrany z urządzenia wejściowego lub kontekstu urządzenia, w razie potrzeby. Ponadto należy ustawić wszystkie niezbędne stany na tworzonym wystąpieniu efektu D2D.
  • Rekursywnie przeszukuje dowolne źródła i wiąże je z efektem D2D.

W odniesieniu do flag wejściowych istnieje kilka możliwych przypadków, w których efekty niestandardowe powinny być prawidłowo obsługiwane, aby zapewnić zgodność ze wszystkimi innymi efektami Win2D. Z wyłączeniem WIN2D_GET_D2D_IMAGE_FLAGS_NONEflagi do obsługi są następujące:

  • WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT: w tym przypadku gwarantuje się, że device nie będzie null. Efekt powinien sprawdzić, czy element docelowy kontekstu urządzenia jest ID2D1CommandList, a jeśli tak, dodaj flagę WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION. W przeciwnym razie należy ustawić targetDpi (co jest również gwarantowane, że nie będzie null) na rozdzielczości DPI pobrane z kontekstu danych wejściowych. Następnie należy usunąć WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT z flag.
  • WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION i WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION: używane podczas ustawiania źródeł efektów (zobacz uwagi poniżej).
  • WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION: jeśli ustawiono, pomija rekursyjną realizację źródeł efektu i zwraca zrealizowany efekt bez innych zmian.
  • WIN2D_GET_D2D_IMAGE_FLAGS_ALLOW_NULL_EFFECT_INPUTS: jeśli ustawiono, źródła efektów, które są uruchamiane, mogą być null, jeśli użytkownik nie ustawił jeszcze istniejącego źródła.
  • WIN2D_GET_D2D_IMAGE_FLAGS_UNREALIZE_ON_FAILURE: jeśli jest ustawione, a ustawione źródło efektu jest nieprawidłowe, efekt powinien się anulować, zanim nastąpi niepowodzenie. Oznacza to, że jeśli wystąpił błąd podczas rozpoznawania źródeł efektu po zrealizowaniu efektu, efekt powinien nierealizować się przed zwróceniem błędu do obiektu wywołującego.

W odniesieniu do flag związanych z DPI, te kontrolują, jak ustawiane są źródła efektów. Aby zapewnić zgodność z Win2D, efekty powinny automatycznie dodawać efekty kompensacji DPI do swoich danych wejściowych, gdy jest to potrzebne. Mogą kontrolować, czy tak się dzieje:

  • Jeśli WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION jest ustawiona, efekt kompensacji DPI jest wymagany zawsze, gdy parametr inputDpi nie jest 0.
  • W przeciwnym razie kompensacja DPI jest wymagana, jeśli inputDpi nie jest 0, WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION nie jest ustawiona, a WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION jest ustawiona lub wejściowe dpi i docelowe wartości DPI nie są zgodne.

Ta logika powinna być stosowana za każdym razem, gdy źródło jest realizowane i powiązane z danymi wejściowymi bieżącego efektu. Należy pamiętać, że jeśli zostanie dodany efekt kompensacji DPI, powinien to być wejście ustawione na bazowy obraz D2D. Jeśli jednak użytkownik spróbuje pobrać opakowanie WinRT dla tego źródła, należy zadbać o to, aby wykryć, czy użyto efektu DPI, i zwrócić opakowanie oryginalnego obiektu źródłowego. Oznacza to, że efekty kompensacji DPI powinny być przejrzyste dla użytkowników efektu.

Po zakończeniu całej logiki inicjalizacji, wynikowy ID2D1Image (podobnie jak w przypadku obiektów Win2D, efekt D2D jest również obrazem) powinien być gotowy do rysowania przez Win2D w docelowym kontekście, który nie jest jeszcze znany wywoływanemu w tym momencie.

Uwaga / Notatka

Poprawne zaimplementowanie tej metody (i ogólnie ICanvasImageInterop) jest bardzo skomplikowane i ma być wykonywane tylko przez zaawansowanych użytkowników, którzy absolutnie potrzebują dodatkowej elastyczności. Przed podjęciem próby napisania implementacji ICanvasImageInterop zaleca się solidne zrozumienie D2D, Win2D, COM, WinRT i C++. Jeśli Twój niestandardowy efekt Win2D musi również obejmować niestandardowy efekt D2D, musisz również zaimplementować własny obiekt ID2D1Effect (zapoznaj się z dokumentacją D2D na temat efektów niestandardowych, aby uzyskać więcej informacji na ten temat). Te dokumenty nie są wyczerpującym opisem całej niezbędnej logiki (na przykład nie obejmują sposobu, w jaki źródła efektu powinny być rozdzielane i zarządzane przez granicę D2D/Win2D), dlatego zaleca się również użycie implementacji CanvasEffect w bazie kodu Win2D jako punktu odniesienia dla niestandardowego efektu i zmodyfikować je zgodnie z potrzebami.

Implementowanie GetBounds

Ostatni brakujący składnik do pełnego zaimplementowania niestandardowego efektu ICanvasImage polega na obsłudze dwóch przeciążeń GetBounds. Aby to ułatwić, Win2D uwidacznia eksport języka C, który może służyć do wykorzystania istniejącej logiki z win2D na dowolnym obrazie niestandardowym. Eksport jest następujący:

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

Obrazy niestandardowe mogą wywoływać ten interfejs API i przekazywać się jako parametr image, a następnie po prostu zwracać wynik do obiektów wywołujących. Parametr transform może być null, jeśli nie jest dostępna żadna transformacja.

Optymalizowanie dostępu do kontekstu urządzenia

Parametr deviceContext w ICanvasImageInterop::GetD2DImage czasami może być null, jeśli kontekst nie jest natychmiast dostępny przed wywołaniem. Odbywa się to celowo, aby kontekst był tworzony leniwie tylko wtedy, gdy jest rzeczywiście potrzebny. Oznacza to, że jeśli odpowiedni kontekst jest dostępny, Win2D przekaże go do wywołania GetD2DImage, w przeciwnym razie umożliwi wywołującym pobranie go samodzielnie, jeśli będzie to konieczne.

Tworzenie kontekstu urządzenia jest stosunkowo kosztowne, dlatego w celu szybszego pobierania danych Win2D uwidacznia interfejsy API w celu uzyskania dostępu do wewnętrznej puli kontekstów urządzeń. Dzięki temu efekty niestandardowe umożliwiają wydajne wypożyczanie i zwracanie kontekstów urządzeń skojarzonych z danym urządzeniem kanwy.

Interfejsy API dzierżawy kontekstu urządzenia są zdefiniowane w następujący sposób:

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

Interfejs ID2D1DeviceContextPool jest implementowany przez CanvasDevice, który jest typem Win2D implementowania interfejsu ICanvasDevice. Aby użyć puli, użyj QueryInterface w interfejsie urządzenia, aby uzyskać odwołanie do ID2D1DeviceContextPool, a następnie wywołaj ID2D1DeviceContextPool::GetDeviceContextLease, aby uzyskać obiekt ID2D1DeviceContextLease w celu uzyskania dostępu do kontekstu urządzenia. Gdy dzierżawa nie będzie już potrzebna, uwolnij dzierżawę. Upewnij się, aby nie zmieniać kontekstu urządzenia po zwolnieniu dzierżawy, ponieważ może być jednocześnie używany przez inne wątki.

Włączanie wyszukiwania obiektów WinRT opakowujących

Jak widać w dokumentacji międzyoperacyjnej Win2D, publiczny nagłówek Win2D uwidacznia również metodę GetOrCreate (dostępną z fabryki aktywacji ICanvasFactoryNative lub za pośrednictwem pomocników GetOrCreate języka C++/CX zdefiniowanych w tym samym nagłówku). Dzięki temu można pobrać opakowanie WinRT z danego zasobu natywnego. Na przykład umożliwia pobieranie lub tworzenie wystąpienia CanvasDevice z obiektu ID2D1Device1, CanvasBitmap z ID2D1Bitmapitp.

Ta metoda działa również dla wszystkich wbudowanych efektów Win2D: pobieranie zasobu natywnego dla danego efektu, a następnie użycie tej otoki Win2D w celu uzyskania odpowiedniego efektu Win2D, poprawnie zwróci efekt Win2D będący jego właścicielem. Aby efekty niestandardowe również korzystały z tego samego systemu mapowania, Win2D udostępnia kilka API w interfejsie międzyoperacyjnym dla fabryki aktywacji, dotyczącej CanvasDevice, który jest typu ICanvasFactoryNative, a także dodatkowy interfejs fabryki efektów, 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);
};

Istnieje kilka interfejsów API, które należy wziąć pod uwagę, ponieważ są one potrzebne do obsługi różnych scenariuszy, w których mogą być używane efekty Win2D. Dodatkowo, ważne jest, jak deweloperzy mogą współpracować z warstwą D2D i następnie próbować rozwiązywać otoki dla nich. Przyjrzyjmy się każdemu z tych interfejsów API.

Metody RegisterWrapper i UnregisterWrapper mają być wywoływane przez efekty niestandardowe, aby dodać je do wewnętrznej pamięci podręcznej Win2D.

  • RegisterWrapper: rejestruje natywny zasób i jego macierzysty wrapper WinRT. Parametr wrapper jest wymagany również do zaimplementowania IWeakReferenceSource, aby można było go poprawnie buforować, nie powodując cykli referencyjnych, co mogłoby prowadzić do przecieków pamięci. Metoda zwraca S_OK, jeśli zasób macierzysty można dodać do pamięci podręcznej, S_FALSE, jeśli zarejestrowano już opakowanie dla resource, oraz kod błędu, jeśli wystąpi błąd.
  • UnregisterWrapper: wyrejestrowuje zasób macierzysty i jego opakowanie. Zwraca S_OK, jeśli można usunąć zasób, S_FALSE jeśli resource nie został jeszcze zarejestrowany, i kod błędu, jeśli wystąpi inny błąd.

Efekty niestandardowe powinny wywoływać RegisterWrapper i UnregisterWrapper za każdym razem, gdy są one realizowane i niezrealizowane, tj. gdy zostanie utworzony nowy zasób macierzysty i skojarzony z nim. Efekty niestandardowe, które nie obsługują realizacji (np. te, które mają stałe skojarzone urządzenie), mogą wywoływać RegisterWrapper i UnregisterWrapper podczas ich tworzenia i niszczenia. Efekty własne powinny zadbać o to, aby poprawnie wyrejestrować się ze wszystkich możliwych ścieżek kodu, które mogą spowodować, że opakowanie stanie się nieprawidłowe (np. gdy obiekt zostaje sfinalizowany, w przypadku implementacji w języku zarządzanym).

Metody RegisterEffectFactory i UnregisterEffectFactory mają być także używane przez efekty niestandardowe, aby mogły zarejestrować wywołanie zwrotne do utworzenia nowego opakowania w przypadku, gdy deweloper próbuje rozwiązać zasób dla "osieroconego" zasobu D2D.

  • RegisterEffectFactory: zarejestruj wywołanie zwrotne, które przyjmuje w danych wejściowych te same parametry, które deweloper przekazał do GetOrCreate, i tworzy nową, sprawdzalną otokę dla efektu wejściowego. Identyfikator efektu jest używany jako klucz, dzięki czemu każdy efekt niestandardowy może zarejestrować fabrykę dla niego przy pierwszym załadowaniu efektu. Oczywiście należy to zrobić tylko raz na typ efektu, a nie za każdym razem, gdy efekt zostanie zrealizowany. Parametry device, resource i wrapper są sprawdzane przez Win2D przed wywołaniem dowolnego zarejestrowanego wywołania zwrotnego, więc są gwarantowane, że nie będą null, gdy wywoływane jest CreateWrapper. dpi jest uważana za opcjonalną i można ją zignorować w przypadku, gdy typ efektu nie ma dla niego określonego zastosowania. Należy pamiętać, że po utworzeniu nowego opakowania z zarejestrowanej fabryki, ta fabryka powinna również upewnić się, że nowe opakowanie jest zarejestrowane w cache (Win2D nie dodaje automatycznie opakowań produkowanych przez zewnętrzne fabryki do cache).
  • UnregisterEffectFactory: usuwa wcześniej zarejestrowane wywołanie zwrotne. Na przykład, może to być używane, jeśli obudowa efektu jest implementowana w zarządzanym zestawie, który jest rozładowywany.

Uwaga / Notatka

ICanvasFactoryNative jest implementowane z użyciem fabryki aktywacji dla CanvasDevice, którą można uzyskać, ręcznie wywołując RoGetActivationFactorylub korzystając z pomocniczych interfejsów API dostępnych w używanych rozszerzeniach językowych (np. winrt::get_activation_factory w języku C++/WinRT). Aby uzyskać więcej informacji, zobacz system typu WinRT, aby uzyskać więcej informacji na temat tego, jak to działa.

W praktycznym przykładzie, w którym to mapowanie wchodzi w grę, rozważ, jak działają wbudowane efekty Win2D. Jeśli nie zostaną one zrealizowane, cały stan (np. właściwości, źródła itp.) jest przechowywany w wewnętrznej pamięci podręcznej w każdym wystąpieniu efektu. Kiedy są zrealizowane, cały stan jest przenoszony do zasobu natywnego (np. właściwości są ustawiane na efekt D2D, wszystkie źródła są rozpoznawane i mapowane na dane wejściowe itp.), i tak długo, jak efekt jest zrealizowany, będzie działać jako autorytet w kwestii stanu opakowania. Oznacza to, że jeśli wartość dowolnej właściwości zostanie pobrana z opakowania, to ona pobierze zaktualizowaną wartość z natywnego zasobu D2D skojarzonego z tą właściwością.

Gwarantuje to, że jeśli jakiekolwiek zmiany zostaną wprowadzone bezpośrednio do zasobu D2D, będą one widoczne również w zewnętrznej powłoce, a te dwa elementy nigdy nie będą niesynchroniczne. Gdy działanie nie jest zrealizowane, cały stan jest przenoszony z powrotem do stanu otoczki z zasobu natywnego, zanim zasób zostanie zwolniony. Zostanie on zachowany i zaktualizowany tam do następnego momentu realizacji efektu. Teraz rozważmy następującą sekwencję zdarzeń:

  • Masz efekt Win2D (wbudowany lub niestandardowy).
  • Uzyskasz z niego ID2D1Image (czyli ID2D1Effect).
  • Utworzysz wystąpienie efektu niestandardowego.
  • Otrzymasz również ID2D1Image z tego.
  • Ten obraz należy ustawić ręcznie jako dane wejściowe dla poprzedniego efektu (za pośrednictwem ID2D1Effect::SetInput).
  • Następnie należy poprosić o pierwszy wynik dla otoki WinRT w odniesieniu do tego wejścia.

Ponieważ efekt jest zrealizowany (został zrealizowany, gdy zasób natywny został zażądany), użyje zasobu natywnego jako źródła prawdy. W związku z tym uzyska ID2D1Image odpowiadające żądanemu źródłu i spróbuje pobrać opakowanie WinRT. Jeśli efekt, z którego pobrano te dane wejściowe, poprawnie dodał własną parę zasobów natywnych i otoki WinRT do pamięci podręcznej Win2D, otoka zostanie rozpoznana i zwrócona do osób wywołujących. Jeśli nie, dostęp do tej właściwości zakończy się niepowodzeniem, ponieważ win2D nie może rozpoznać otoki WinRT dla efektów, których nie jest właścicielem, ponieważ nie wie, jak je utworzyć.

W tym miejscu RegisterWrapper i UnregisterWrapper pomagają, ponieważ umożliwiają płynne włączenie efektów niestandardowych w logikę rozpoznawania osłon Win2D, dzięki czemu prawidłowa osłona może być zawsze znajdowana dla dowolnego źródła efektu, niezależnie od tego, czy została ustawiona z interfejsów API WinRT, czy bezpośrednio z bazowej warstwy D2D.

Aby wyjaśnić, jak działają również fabryki efektu, rozważmy następujący scenariusz:

  • Użytkownik tworzy wystąpienie niestandardowej otoki i realizuje je
  • Następnie pobierają odwołanie do bazowego efektu D2D i przechowują je.
  • Następnie efekt jest realizowany na innym urządzeniu. Efekt zostanie cofnięty i zrealizowany ponownie, co spowoduje stworzenie nowego efektu D2D. W tym momencie poprzedni efekt D2D nie ma już powiązanego opakowania inspekcyjnego.
  • Następnie użytkownik wywołuje GetOrCreate na pierwszym efekcie D2D.

Bez wywołania zwrotnego, Win2D nie byłby w stanie rozpoznać otoki, ponieważ nie ma dla niej zarejestrowanej otoki. Jeśli fabryka jest zarejestrowana, można utworzyć i zwrócić nową otoczkę dla tego efektu D2D, tak aby scenariusz działał bezproblemowo dla użytkownika.

Implementowanie niestandardowej ICanvasEffect

Interfejs ICanvasEffect Win2D rozszerza ICanvasImage, więc wszystkie poprzednie punkty mają zastosowanie również do efektów niestandardowych. Jedyną różnicą jest fakt, że ICanvasEffect również implementuje dodatkowe metody specyficzne dla efektów, takich jak unieważnienie prostokąta źródłowego, uzyskanie wymaganych prostokątów itd.

Aby to umożliwić, Win2D uwidacznia eksporty języka C, których mogą używać autorzy efektów niestandardowych, dzięki czemu nie będą musieli ponownie zaimplementować tej dodatkowej logiki od podstaw. Działa to w taki sam sposób, jak eksport języka C dla GetBounds. Poniżej przedstawiono dostępne eksporty dla efektów:

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

Przyjrzyjmy się sposobom ich użycia:

  • InvalidateSourceRectangleForICanvasImageInterop ma obsługiwać InvalidateSourceRectangle. Wystarczy uporządkować parametry wejściowe i wywołać je bezpośrednio, a zajmie się całą niezbędną pracą. Należy pamiętać, że parametr image jest bieżącym wystąpieniem efektu, które jest wdrażane.
  • GetInvalidRectanglesForICanvasImageInterop obsługuje GetInvalidRectangles. Nie wymaga to również specjalnej uwagi, poza koniecznością usunięcia zwróconej tablicy COM, gdy nie jest już potrzebna.
  • GetRequiredSourceRectanglesForICanvasImageInterop to współdzielona metoda, która może obsługiwać zarówno GetRequiredSourceRectangle, jak i GetRequiredSourceRectangles. Oznacza to, że funkcja przyjmuje wskaźnik do istniejącej tablicy wartości, którą należy wypełnić. Wywołujący mogą przekazać wskaźnik do pojedynczej wartości (który może być również na stosie, aby uniknąć jednej alokacji) lub do tablicy wartości. Implementacja jest taka sama w obu przypadkach, więc jeden eksport C wystarczy, aby zasilić oba.

Efekty niestandardowe w języku C# przy użyciu funkcji ComputeSharp

Jak wspomnieliśmy, jeśli używasz języka C# i chcesz zaimplementować efekt niestandardowy, zalecaną metodą jest użycie biblioteki ComputeSharp. Umożliwia ona zarówno zaimplementowanie niestandardowych cieniowania pikseli D2D1 w całości w języku C#, jak i łatwe definiowanie grafów efektów niestandardowych, które są zgodne z Win2D. Ta sama biblioteka jest również używana w Sklepie Microsoft do obsługi kilku składników graficznych w aplikacji.

Możesz dodać odwołanie do obiektu ComputeSharp w projekcie za pomocą narzędzia NuGet:

Uwaga / Notatka

Wiele interfejsów API w ComputeSharp.D2D1.* jest identycznych dla platform docelowych UWP i WinUI, a jedyną różnicą jest przestrzeń nazw (kończąca się na .Uwp lub .WinUI). Jednak platforma UWP jest w stałym utrzymaniu i nie otrzymuje nowych funkcji. W związku z tym niektóre zmiany kodu mogą być potrzebne w porównaniu z przykładami przedstawionymi tutaj dla interfejsu WinUI. Fragmenty kodu w tym dokumencie odzwierciedlają zakres interfejsu API wersji ComputeSharp.D2D1.WinUI.0.0 (ostatnia wersja dla docelowej platformy UWP to 2.1.0).

Istnieją dwa główne składniki w ComputeSharp do interakcji z Win2D:

  • PixelShaderEffect<T>: efekt Win2D oparty na shaderze pikseli D2D1. Sam moduł cieniowania jest zapisywany w języku C# przy użyciu interfejsów API udostępnianych przez usługę ComputeSharp. Ta klasa udostępnia również właściwości ustawiania źródeł efektów, wartości stałych i nie tylko.
  • CanvasEffect: klasa bazowa dla niestandardowych efektów Win2D, które opakowuje dowolny graf efektu. Może służyć do "tworzenia pakietów" złożonych efektów w łatwy w użyciu obiekt, który można użyć ponownie w kilku częściach aplikacji.

Oto przykład niestandardowego cieniowania pikselowego (przeniesiony z tego szadera shadertoy), używanego z PixelShaderEffect<T> i następnie rysowany na Win2D CanvasControl (zwróć uwagę, że PixelShaderEffect<T> implementuje ICanvasImage):

Próbka cieniowania pikseli wyświetlająca nieskończone kolorowe sześciokąty, rysowane na kontrolce Win2D i wyświetlająca się w oknie aplikacji

Możesz zobaczyć, jak w dwóch wierszach kodu można utworzyć efekt i narysować go za pomocą win2D. ComputeSharp zajmuje się całą pracą niezbędną do skompilowania cieniowania, zarejestrowania go i zarządzania złożonym okresem istnienia efektu zgodnego z win2D.

Następnie zapoznajmy się z przewodnikiem krok po kroku dotyczącym tworzenia niestandardowego efektu Win2D, który używa również niestandardowego cieniowania pikseli D2D1. Omówimy sposób tworzenia cieniowania za pomocą funkcji ComputeSharp i konfigurowania jego właściwości, a następnie tworzenia wykresu efektu niestandardowego spakowanego w typ CanvasEffect, który można łatwo użyć ponownie w aplikacji.

Projektowanie efektu

Na potrzeby tego pokazu chcemy stworzyć prosty efekt szkła matowego.

Obejmuje to następujące składniki:

  • Rozmycie Gaussowskie
  • Efekt odcienia
  • Szum (który możemy proceduralnie wygenerować za pomocą cieniowania)

Chcemy również uwidocznić właściwości w celu kontrolowania rozmycia i natężenia szumu. Końcowy efekt będzie zawierał "spakowaną" wersję tego grafu efektu i będzie łatwy do użycia, poprzez utworzenie instancji, ustawienie tych właściwości, połączenie obrazu źródłowego, następnie narysowanie go. Zaczynamy!

Tworzenie niestandardowego shadera pikseli D2D1

W przypadku szumu nakładającego się na efekt, możemy użyć prostego cieniowania pikseli D2D1. Shader obliczy losową wartość na podstawie jego współrzędnych (które będą działać jako "ziarno" dla liczby losowej), a następnie użyje tej wartości szumu do obliczenia wartości RGB dla danego piksela. Następnie możemy wymieszać ten szum na podstawie wynikowego obrazu.

Aby napisać cieniowanie za pomocą klasy ComputeSharp, wystarczy zdefiniować typ partial struct implementującego interfejs ID2D1PixelShader, a następnie napisać logikę w metodzie Execute. W przypadku tego cieniowania szumów możemy napisać coś takiego:

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

Uwaga / Notatka

Chociaż shader jest napisany w całości w języku C#, zalecana jest podstawowa wiedza na temat HLSL (języku programowania shaderów DirectX, do którego ComputeSharp transpiluje język C#).

Przyjrzyjmy się szczegółowo temu shaderowi.

  • Shader nie ma żadnych danych wejściowych, po prostu tworzy nieskończony obraz z losowym szumem w skali szarości.
  • Shader wymaga dostępu do bieżącej współrzędnej piksela.
  • Cieniowanie jest wstępnie skompilowane w czasie kompilacji (przy użyciu profilu PixelShader40, który ma gwarancję dostępności na dowolnym procesorze GPU, w którym aplikacja może być uruchomiona).
  • Atrybut [D2DGeneratedPixelShaderDescriptor] jest potrzebny do wyzwolenia generatora źródłowego powiązanego z elementem ComputeSharp, który przeanalizuje kod języka C#, transpiluje go w języku HLSL, kompiluje cieniowanie do kodu bajtowego itp.
  • Shader przechwytuje float amount parametr za pomocą swojego podstawowego konstruktora. Generator źródłowy w narzędziu ComputeSharp automatycznie zajmie się wyodrębnieniem wszystkich przechwyconych wartości w shaderze i przygotowaniem bufora stałego, którego D2D potrzebuje do zainicjowania stanu shadera.

Ta część jest gotowa! Ten shader wygeneruje naszą niestandardową teksturę szumu w razie potrzeby. Następnie musimy utworzyć nasz efekt spakowany z grafem efektu łączącym wszystkie nasze efekty razem.

Tworzenie efektu niestandardowego

W celu łatwego w użyciu, pakietowego efektu, możemy użyć typu CanvasEffect z ComputeSharp. Ten typ zapewnia prosty sposób konfigurowania całej logiki niezbędnej do utworzenia grafu efektu i zaktualizowania go za pośrednictwem właściwości publicznych, z którymi użytkownicy efektu mogą wchodzić w interakcje. Istnieją dwie główne metody, które należy zaimplementować:

  • BuildEffectGraph: ta metoda jest odpowiedzialna za tworzenie wykresu efektów, który chcemy narysować. Oznacza to, że musi utworzyć wszystkie potrzebne efekty i zarejestrować węzeł wyjściowy dla grafu. W przypadku efektów, które można zaktualizować w późniejszym czasie, rejestracja odbywa się przy użyciu skojarzonej wartości CanvasEffectNode<T>, która służy jako klucz wyszukiwania do pobierania efektów z wykresu w razie potrzeby.
  • ConfigureEffectGraph: ta metoda odświeża graf efektów, stosując ustawienia skonfigurowane przez użytkownika. Ta metoda jest wywoływana automatycznie, gdy jest to konieczne, bezpośrednio przed rysunkiem efektu i tylko wtedy, gdy od ostatniego użycia efektu została zmodyfikowana co najmniej jedna właściwość efektu.

Nasz efekt niestandardowy można zdefiniować w następujący sposób:

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

W tej klasie można zobaczyć cztery sekcje:

  • Najpierw mamy pola do śledzenia całego modyfikowalnego stanu, takich jak efekty, które można zaktualizować, a także pola wspierające dla wszystkich właściwości efektu, które chcemy uwidocznić użytkownikom efektu.
  • Następnie mamy właściwości do skonfigurowania efektu. Moduł ustawiający każdej właściwości używa metody SetAndInvalidateEffectGraph uwidocznionej przez CanvasEffect, co spowoduje automatyczne unieważnienie efektu, jeśli ustawiona wartość jest inna niż bieżąca. Zapewnia to ponowne skonfigurowanie efektu tylko wtedy, gdy jest to naprawdę konieczne.
  • Na koniec mamy metody BuildEffectGraph i ConfigureEffectGraph wymienione powyżej.

Uwaga / Notatka

Węzeł PremultiplyEffect po efektie szumu jest bardzo ważny: jest to spowodowane tym, że efekty Win2D zakładają, że dane wyjściowe są wstępnie ułożone, podczas gdy cieniowania pikseli zazwyczaj działają z pikselami niezauważanymi. W związku z tym należy pamiętać, aby ręcznie wstawiać węzły premultiply/unpremultiply przed i po niestandardowych shaderach, aby upewnić się, że kolory są poprawnie zachowywane.

Uwaga / Notatka

Ten przykładowy efekt używa przestrzeni nazw WinUI, ale ten sam kod może być również używany na platformie UWP. W takim przypadku przestrzeń nazw dla obiektu ComputeSharp będzie ComputeSharp.Uwp, zgodna z nazwą pakietu.

Wszystko gotowe do losowania!

A dzięki temu nasz niestandardowy efekt szkła matowego jest gotowy! Możemy go łatwo narysować w następujący sposób:

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

    args.DrawingSession.DrawImage(effect);
}

W tym przykładzie stosujemy efekt z programu obsługi Draw dla CanvasControl, używając CanvasBitmap, który został wcześniej załadowany jako źródło. Jest to obraz wejściowy, który użyjemy do przetestowania efektu:

zdjęcie niektórych gór pod pochmurnym niebo

A oto wynik:

rozmytą wersję obrazu powyżej

Uwaga / Notatka

Podziękowania dla Dominica Lange za zdjęcie.

Dodatkowe zasoby

  • Aby uzyskać więcej informacji, zobacz kod źródłowy Win2D.
  • Aby uzyskać więcej informacji na temat computeSharp, zapoznaj się z przykładowymi aplikacjami i testami jednostkowymi .