Partager via


Implémentation d’effets personnalisés

Win2D fournit plusieurs API pour représenter des objets qui peuvent être dessinés, divisés en deux catégories : images et effets. Les images, représentées par l’interface ICanvasImage , n’ont aucune entrée et peuvent être dessinées directement sur une surface donnée. Par exemple, CanvasBitmapVirtualizedCanvasBitmap et CanvasRenderTarget sont des exemples de types d’images. Les effets, en revanche, sont représentés par l’interface ICanvasEffect . Ils peuvent avoir des entrées ainsi que des ressources supplémentaires et appliquer une logique arbitraire pour produire leurs sorties (en tant qu’effet est également une image). Win2D inclut des effets encapsulant la plupart des effets D2D, tels que GaussianBlurEffect, TintEffect et LuminanceToAlphaEffect.

Les images et les effets peuvent également être chaînés pour créer des graphiques arbitraires qui peuvent ensuite être affichés dans votre application (reportez-vous également aux documents D2D sur les effets Direct2D). Ensemble, ils fournissent un système extrêmement flexible pour créer des graphiques complexes de manière efficace. Toutefois, il existe des cas où les effets intégrés ne sont pas suffisants et que vous souhaiterez peut-être créer votre propre effet Win2D. Pour prendre en charge cela, Win2D inclut un ensemble d’API d’interopérabilité puissantes qui permettent de définir des images et des effets personnalisés qui peuvent s’intégrer en toute transparence à Win2D.

Conseil

Si vous utilisez C# et que vous souhaitez implémenter un graphique d’effet ou d’effet personnalisé, il est recommandé d’utiliser ComputeSharp plutôt que d’essayer d’implémenter un effet à partir de zéro. Consultez le paragraphe ci-dessous pour obtenir une explication détaillée de l’utilisation de cette bibliothèque pour implémenter des effets personnalisés qui s’intègrent en toute transparence à Win2D.

API de plateforme : , , CanvasBitmap, VirtualizedCanvasBitmap, CanvasEffectTintEffectID2D21ImageCanvasRenderTargetGaussianBlurEffectICanvasLuminanceToAlphaEffectImageIGraphicsEffectSource, , ID2D1Factory1ICanvasImageID2D1Effect

Implémentation d’une application personnalisée ICanvasImage

Le scénario le plus simple à prendre en charge est la création d’un modèle personnalisé ICanvasImage. Comme nous l’avons mentionné, il s’agit de l’interface WinRT définie par Win2D qui représente toutes sortes d’images avec lesquelles Win2D peut interagir. Cette interface expose uniquement deux GetBounds méthodes et s’étend IGraphicsEffectSource, qui est une interface de marqueur représentant « une source d’effet ».

Comme vous pouvez le voir, il n’existe aucune API « fonctionnelle » exposée par cette interface pour effectuer réellement n’importe quel dessin. Pour implémenter votre propre ICanvasImage objet, vous devez également implémenter l’interface ICanvasImageInterop , qui expose toute la logique nécessaire pour Win2D afin de dessiner l’image. Il s’agit d’une interface COM définie dans l’en-tête public Microsoft.Graphics.Canvas.native.h , fournie avec Win2D.

L’interface se définit comme suit :

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

Il s’appuie également sur ces deux types d’énumération, à partir du même en-tête :

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
}

Les deux GetDevice et GetD2DImage méthodes sont toutes nécessaires pour implémenter des images personnalisées (ou des effets), car elles fournissent à Win2D les points d’extensibilité pour les initialiser sur un appareil donné et récupérer l’image D2D sous-jacente à dessiner. L’implémentation correcte de ces méthodes est essentielle pour garantir que les choses fonctionneront correctement dans tous les scénarios pris en charge.

Voyons comment fonctionne chaque méthode.

Application GetDevice

La GetDevice méthode est la plus simple des deux. Ce qu’il fait, il récupère l’appareil de canevas associé à l’effet, afin que Win2D puisse l’inspecter si nécessaire (par exemple, pour s’assurer qu’il correspond à l’appareil en cours d’utilisation). Le type paramètre indique le « type d’association » pour l’appareil retourné.

Il existe deux cas principaux possibles :

  • Si l’image est un effet, elle doit prendre en charge l’être « réalisé » et « non réalisé » sur plusieurs appareils. Cela signifie qu’un effet donné est créé dans un état non initialisé, puis il peut être réalisé lorsqu’un appareil est passé pendant le dessin, et après cela il peut continuer à être utilisé avec cet appareil, ou il peut être déplacé vers un autre appareil. Dans ce cas, l’effet réinitialise son état interne, puis se rend à nouveau compte sur le nouvel appareil. Cela signifie que l’appareil de canevas associé peut changer au fil du temps et qu’il peut également être null. En raison de cela, type doit être défini WIN2D_GET_DEVICE_ASSOCIATION_TYPE_REALIZATION_DEVICEsur , et l’appareil retourné doit être défini sur l’appareil de réalisation actuel, si celui-ci est disponible.
  • Certaines images ont un seul « appareil propriétaire » qui est affecté au moment de la création et ne peut jamais changer. Par exemple, il s’agit du cas d’une image représentant une texture, car elle est allouée sur un appareil spécifique et ne peut pas être déplacée. Quand GetDevice il est appelé, il doit retourner l’appareil de création et défini sur type WIN2D_GET_DEVICE_ASSOCIATION_TYPE_CREATION_DEVICE. Notez que lorsque ce type est spécifié, l’appareil retourné ne doit pas être null.

Remarque

Win2D peut appeler GetDevice tout en parcourant de manière récursive un graphique d’effet, ce qui signifie qu’il peut y avoir plusieurs appels actifs dans GetD2DImage la pile. En raison de cela, GetDevice ne doit pas prendre un verrou bloquant sur l’image actuelle, car cela pourrait potentiellement interblocage. Au lieu de cela, il doit utiliser un verrou re-entrant de manière non bloquante et retourner une erreur s’il ne peut pas être acquis. Cela garantit que le même thread récursive l’appellera avec succès, tandis que les threads simultanés qui effectuent la même opération échouent correctement.

Application GetD2DImage

GetD2DImage est l’endroit où la plupart du travail a lieu. Cette méthode est chargée de récupérer l’objet ID2D1Image que Win2D peut dessiner, éventuellement en réalisant l’effet actuel si nécessaire. Cela inclut également la traversée récursive et la réalisation du graphique d’effet pour toutes les sources, le cas échéant, ainsi que l’initialisation d’un état dont l’image peut avoir besoin (par exemple, des mémoires tampons constantes et d’autres propriétés, des textures de ressources, etc.).

L’implémentation exacte de cette méthode dépend fortement du type d’image et elle peut varier beaucoup, mais en général en parlant d’un effet arbitraire, vous pouvez vous attendre à ce que la méthode effectue les étapes suivantes :

  • Vérifiez si l’appel a été récursif sur la même instance et échouez le cas échéant. Cela est nécessaire pour détecter les cycles d’un graphique d’effet (par exemple, l’effet A a un effet B en tant que source et l’effet B a un effet A comme source).
  • Acquérir un verrou sur l’instance d’image pour vous protéger contre l’accès simultané.
  • Gérer les API cibles en fonction des indicateurs d’entrée
  • Vérifiez si l’appareil d’entrée correspond à celui utilisé, le cas échéant. S’il ne correspond pas et que l’effet actuel prend en charge la réalisation, ne réalisez pas l’effet.
  • Réalisez l’effet sur l’appareil d’entrée. Cela peut inclure l’inscription de l’effet D2D sur l’objet ID2D1Factory1 récupéré à partir de l’appareil d’entrée ou du contexte de l’appareil, si nécessaire. En outre, l’état nécessaire doit être défini sur l’instance d’effet D2D en cours de création.
  • Parcourez de manière récursive toutes les sources et liez-les à l’effet D2D.

En ce qui concerne les indicateurs d’entrée, il existe plusieurs cas possibles que les effets personnalisés doivent gérer correctement, afin de garantir la compatibilité avec tous les autres effets Win2D. À l’exclusion WIN2D_GET_D2D_IMAGE_FLAGS_NONE, les indicateurs à gérer sont les suivants :

  • WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT: dans ce cas, device il est garanti de ne pas être null. L’effet doit vérifier si la cible de contexte de l’appareil est un ID2D1CommandList, et si c’est le cas, ajoutez l’indicateur WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION . Dans le cas contraire, il doit définir targetDpi (qui est également garanti qu’il ne doit pas être null) sur les DPIs récupérés à partir du contexte d’entrée. Ensuite, il doit être supprimé WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT des indicateurs.
  • WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION et WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION: utilisé lors de la définition de sources d’effet (voir les notes ci-dessous).
  • WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION: si elle est définie, ignore de façon récursive la réalisation des sources de l’effet, et retourne simplement l’effet réalisé sans aucune autre modification.
  • WIN2D_GET_D2D_IMAGE_FLAGS_ALLOW_NULL_EFFECT_INPUTS: si l’ensemble, les sources d’effet en cours de réalisation sont autorisées à être null, si l’utilisateur ne les a pas encore définies sur une source existante.
  • WIN2D_GET_D2D_IMAGE_FLAGS_UNREALIZE_ON_FAILURE: si la valeur est définie et qu’une source d’effet définie n’est pas valide, l’effet doit se réaliser avant d’échouer. Autrement dit, si l’erreur s’est produite lors de la résolution des sources d’effet après la réalisation de l’effet, l’effet doit se réaliser avant de renvoyer l’erreur à l’appelant.

En ce qui concerne les indicateurs liés à l’ppp, ces indicateurs contrôlent la façon dont les sources d’effet sont définies. Pour garantir la compatibilité avec Win2D, les effets doivent ajouter automatiquement des effets de compensation DPI à leurs entrées si nécessaire. Ils peuvent contrôler si c’est le cas comme suit :

  • Si WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION elle est définie, un effet de compensation DPI est nécessaire chaque fois que le inputDpi paramètre n’est pas 0.
  • Sinon, la compensation ppp est nécessaire si inputDpi ce n’est pas le cas 0, WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION n’est pas définie et WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION est définie, ou l’ppp d’entrée et les valeurs DPI cibles ne correspondent pas.

Cette logique doit être appliquée chaque fois qu’une source est réalisée et liée à une entrée de l’effet actuel. Notez que si un effet de compensation DPI est ajouté, il doit s’agir de l’entrée définie sur l’image D2D sous-jacente. Toutefois, si l’utilisateur tente de récupérer le wrapper WinRT pour cette source, l’effet doit prendre soin de détecter si un effet DPI a été utilisé et retourner un wrapper pour l’objet source d’origine à la place. Autrement dit, les effets de compensation des PPP doivent être transparents pour les utilisateurs de l’effet.

Une fois la logique d’initialisation terminée, le résultat ID2D1Image (comme avec les objets Win2D, un effet D2D est également une image) doit être dessiné par Win2D sur le contexte cible, qui n’est pas encore connu par l’appelé pour l’instant.

Remarque

L’implémentation correcte de cette méthode (et ICanvasImageInterop en général) est extrêmement compliquée, et elle est destinée uniquement à être effectuée par les utilisateurs avancés qui ont absolument besoin de la flexibilité supplémentaire. Une bonne compréhension de D2D, Win2D, COM, WinRT et C++ est recommandée avant de tenter d’écrire une ICanvasImageInterop implémentation. Si votre effet Win2D personnalisé doit également encapsuler un effet D2D personnalisé, vous devez également implémenter votre propre ID2D1Effect objet (reportez-vous à la documentation D2D sur les effets personnalisés pour plus d’informations sur ce sujet). Ces documents ne sont pas une description exhaustive de toutes les logiques nécessaires (par exemple, ils ne couvrent pas la façon dont les sources d’effet doivent être marshalées et gérées sur la limite D2D/Win2D), il est donc recommandé d’utiliser également l’implémentation dans la CanvasEffect base de code de Win2D comme point de référence pour un effet personnalisé et de la modifier si nécessaire.

Application GetBounds

Le dernier composant manquant pour implémenter entièrement un effet personnalisé ICanvasImage consiste à prendre en charge les deux GetBounds surcharges. Pour faciliter cette opération, Win2D expose une exportation C qui peut être utilisée pour tirer parti de la logique existante pour cela à partir de Win2D sur n’importe quelle image personnalisée. L’exportation est la suivante :

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

Les images personnalisées peuvent appeler cette API et se passer en tant que image paramètre, puis renvoyer simplement le résultat à leurs appelants. Le transform paramètre peut être null, si aucune transformation n’est disponible.

Optimisation des accès au contexte de l’appareil

Le deviceContext paramètre peut ICanvasImageInterop::GetD2DImage parfois être null, si un contexte n’est pas immédiatement disponible avant l’appel. Cela est effectué à des fins, afin qu’un contexte soit créé de manière différée lorsqu’il est réellement nécessaire. Autrement dit, si un contexte est disponible, Win2D le transmet à l’appel GetD2DImage  ; sinon, il permet aux appelants de récupérer un par eux-mêmes si nécessaire.

La création d’un contexte d’appareil est relativement coûteuse, de sorte que la récupération d’une api Win2D plus rapide expose les API pour accéder à son pool de contextes d’appareil interne. Cela permet aux effets personnalisés de louer et de retourner des contextes d’appareil associés à un appareil de canevas donné de manière efficace.

Les API de bail de contexte d’appareil sont définies comme suit :

[uuid("A0928F38-F7D5-44DD-A5C9-E23D94734BBB")]
interface ID2D1DeviceContextLease : IUnknown
{
    HRESULT GetD2DDeviceContext(ID2D1DeviceContext** deviceContext);
}

[uuid("454A82A1-F024-40DB-BD5B-8F527FD58AD0")]
interface ID2D1DeviceContextPool : IUnknown
{
    HRESULT GetDeviceContextLease(ID2D1DeviceContextLease** lease);
}

L’interface ID2D1DeviceContextPool est implémentée par CanvasDevicele type Win2D implémentant l’interface ICanvasDevice . Pour utiliser le pool, utilisez QueryInterface sur l’interface de l’appareil pour obtenir une ID2D1DeviceContextPool référence, puis appelez ID2D1DeviceContextPool::GetDeviceContextLease pour obtenir un ID2D1DeviceContextLease objet pour accéder au contexte de l’appareil. Une fois que cela n’est plus nécessaire, relâchez le bail. Veillez à ne pas toucher le contexte de l’appareil une fois le bail libéré, car il peut être utilisé simultanément par d’autres threads.

Activation de la recherche de wrappers WinRT

Comme indiqué dans les documents d’interopérabilité Win2D, l’en-tête public Win2D expose également une GetOrCreate méthode (accessible à partir de la ICanvasFactoryNative fabrique d’activation ou via les GetOrCreate helpers C++/CX définis dans le même en-tête). Cela permet de récupérer un wrapper WinRT à partir d’une ressource native donnée. Par exemple, il vous permet de récupérer ou de créer une CanvasDevice instance à partir d’un ID2D1Device1 objet, d’un CanvasBitmap ID2D1Bitmap, etc.

Cette méthode fonctionne également pour tous les effets Win2D intégrés : la récupération de la ressource native pour un effet donné, puis l’utilisation de celui-ci pour récupérer le wrapper Win2D correspondant retourne correctement l’effet Win2D propriétaire pour celui-ci. Pour que les effets personnalisés bénéficient également du même système de mappage, Win2D expose plusieurs API dans l’interface d’interopérabilité pour la fabrique d’activation pour CanvasDevice, qui est le ICanvasFactoryNative type, ainsi qu’une interface de fabrique d’effet supplémentaire, 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);
};

Il existe plusieurs API à prendre en compte ici, car elles sont nécessaires pour prendre en charge tous les différents scénarios où les effets Win2D peuvent être utilisés, ainsi que la façon dont les développeurs peuvent effectuer une interopérabilité avec la couche D2D, puis essayer de résoudre les wrappers pour eux. Examinons chacune de ces API.

Les RegisterWrapper méthodes et UnregisterWrapper les méthodes sont destinées à être appelées par des effets personnalisés pour s’ajouter dans le cache Win2D interne :

  • RegisterWrapper: inscrit une ressource native et son propre wrapper WinRT. Le wrapper paramètre est nécessaire pour également implémenter IWeakReferenceSource, afin qu’il puisse être mis en cache correctement sans provoquer de cycles de référence qui entraîneraient des fuites de mémoire. La méthode retourne S_OK si la ressource native peut être ajoutée au cache, S_FALSE s’il existe déjà un wrapper inscrit pour resource, et un code d’erreur si une erreur se produit.
  • UnregisterWrapper: annule l’inscription d’une ressource native et de son wrapper. Retourne S_OK si la ressource peut être supprimée, S_FALSE si resource elle n’a pas déjà été inscrite, et un code erro si une autre erreur se produit.

Les effets personnalisés doivent appeler RegisterWrapper et UnregisterWrapper chaque fois qu’ils sont réalisés et non réalisés, c’est-à-dire lorsqu’une nouvelle ressource native est créée et associée. Les effets personnalisés qui ne prennent pas en charge la réalisation (par exemple, ceux ayant un appareil associé fixe) peuvent appeler RegisterWrapper et UnregisterWrapper quand ils sont créés et détruits. Les effets personnalisés doivent s’assurer de se désinscrire correctement de tous les chemins de code possibles qui entraîneraient l’invalidation du wrapper (par exemple, lorsque l’objet est finalisé, dans le cas où il est implémenté dans un langage managé).

Les RegisterEffectFactory méthodes et UnregisterEffectFactory les méthodes sont également destinées à être utilisées par des effets personnalisés, afin qu’elles puissent également inscrire un rappel pour créer un wrapper au cas où un développeur tente de résoudre un pour une ressource D2D « orpheline » :

  • RegisterEffectFactory: inscrivez un rappel qui prend en entrée les mêmes paramètres qu’un développeur passé à GetOrCreate, et crée un wrapper inspectable pour l’effet d’entrée. L’ID d’effet est utilisé comme clé, afin que chaque effet personnalisé puisse inscrire une fabrique pour celle-ci lors du premier chargement. Bien sûr, cela ne devrait être effectué qu’une seule fois par type d’effet, et pas chaque fois que l’effet est réalisé. Les deviceparamètres et wrapper les paramètres sont vérifiés par Win2D avant d’appeler un rappel inscrit, de sorte qu’ils ne sont pas CreateWrapper null garantis lorsqu’ils sont resource appelés. Il dpi est considéré comme facultatif et peut être ignoré si le type d’effet n’a pas d’utilisation spécifique pour celui-ci. Notez que lorsqu’un nouveau wrapper est créé à partir d’une fabrique inscrite, cette fabrique doit également s’assurer que le nouveau wrapper est inscrit dans le cache (Win2D n’ajoute pas automatiquement des wrappers produits par des fabriques externes au cache).
  • UnregisterEffectFactory: supprime un rappel précédemment inscrit. Par exemple, cela peut être utilisé si un wrapper d’effet est implémenté dans un assembly managé qui est déchargé.

Remarque

ICanvasFactoryNative est implémenté par la fabrique d’activation pour CanvasDevicelaquelle vous pouvez récupérer en appelant RoGetActivationFactorymanuellement ou en utilisant des API d’assistance à partir des extensions de langage que vous utilisez (par exemple, winrt::get_activation_factory en C++/WinRT). Pour plus d’informations, consultez le système de type WinRT pour plus d’informations sur le fonctionnement de ce système.

Pour obtenir un exemple pratique de l’endroit où ce mappage entre en jeu, réfléchissez au fonctionnement des effets Win2D intégrés. S’ils ne sont pas réalisés, tous les états (par exemple, propriétés, sources, etc.) sont stockés dans un cache interne dans chaque instance d’effet. Lorsqu’elles sont réalisées, tous les états sont transférés vers la ressource native (par exemple, les propriétés sont définies sur l’effet D2D, toutes les sources sont résolues et mappées aux entrées d’effet, etc.), et tant que l’effet est réalisé, il agit comme autorité sur l’état du wrapper. Autrement dit, si la valeur d’une propriété est extraite du wrapper, elle récupère la valeur mise à jour pour celle-ci à partir de la ressource D2D native associée.

Cela garantit que si des modifications sont apportées directement à la ressource D2D, celles-ci seront également visibles sur le wrapper externe et les deux ne seront jamais « hors synchronisation ». Lorsque l’effet n’est pas réalisé, tout l’état est transféré de la ressource native à l’état wrapper, avant la libération de la ressource. Elle sera conservée et mise à jour jusqu’à la prochaine réalisation de l’effet. À présent, tenez compte de cette séquence d’événements :

  • Vous avez un effet Win2D (intégré ou personnalisé).
  • Vous obtenez de lui ID2D1Image (qui est un ID2D1Effect).
  • Vous créez une instance d’un effet personnalisé.
  • Vous obtenez aussi ça ID2D1Image .
  • Vous définissez manuellement cette image comme entrée pour l’effet précédent (via ID2D1Effect::SetInput).
  • Vous demandez ensuite ce premier effet pour le wrapper WinRT pour cette entrée.

Étant donné que l’effet est réalisé (il a été réalisé lorsque la ressource native a été demandée), il utilisera la ressource native comme source de vérité. Par conséquent, il obtient le ID2D1Image correspondant à la source demandée et tente de récupérer le wrapper WinRT pour celui-ci. Si l’effet à partir duquel cette entrée a été récupérée a correctement ajouté sa propre paire de ressources natives et de wrapper WinRT au cache de Win2D, le wrapper est résolu et retourné aux appelants. Si ce n’est pas le cas, cet accès aux propriétés échoue, car Win2D ne peut pas résoudre les wrappers WinRT pour les effets qu’il ne possède pas, car il ne sait pas comment les instancier.

C’est RegisterWrapper là où et UnregisterWrapper aide, car ils permettent aux effets personnalisés de participer en toute transparence à la logique de résolution du wrapper de Win2D, afin que le wrapper correct puisse toujours être récupéré pour n’importe quelle source d’effet, qu’il ait été défini à partir des API WinRT ou directement à partir de la couche D2D sous-jacente.

Pour expliquer comment les fabriques d’effet entrent également en jeu, envisagez ce scénario :

  • Un utilisateur crée une instance d’un wrapper personnalisé et le réalise
  • Ils obtiennent ensuite une référence à l’effet D2D sous-jacent et le conservent.
  • Ensuite, l’effet est réalisé sur un autre appareil. L’effet n’est pas réalisé et se réalise à nouveau, et dans ce cas, il crée un nouvel effet D2D. L’effet D2D précédent n’est plus un wrapper inspectable associé à ce stade.
  • L’utilisateur appelle GetOrCreate ensuite le premier effet D2D.

Sans rappel, Win2D ne parvient pas à résoudre un wrapper, car il n’y a pas de wrapper inscrit pour lui. Si une fabrique est inscrite à la place, un nouveau wrapper pour cet effet D2D peut être créé et retourné, de sorte que le scénario continue de fonctionner en toute transparence pour l’utilisateur.

Implémentation d’une application personnalisée ICanvasEffect

L’interface Win2D ICanvasEffect s’étend , de sorte que tous les points précédents ICanvasImages’appliquent également aux effets personnalisés. La seule différence est le fait qu’implémente également des méthodes supplémentaires spécifiques aux effets, telles que ICanvasEffect l’invalidation d’un rectangle source, l’obtention des rectangles requis et ainsi de suite.

Pour prendre en charge cela, Win2D expose les exportations C que les auteurs d’effets personnalisés peuvent utiliser, de sorte qu’ils n’auront pas à réexéplier toute cette logique supplémentaire à partir de zéro. Cela fonctionne de la même façon que l’exportation C pour GetBounds. Voici les exportations disponibles pour les effets :

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

Voyons comment ils peuvent être utilisés :

  • InvalidateSourceRectangleForICanvasImageInterop est destiné à prendre en charge InvalidateSourceRectangle. Il vous suffit de marshaler les paramètres d’entrée et de l’appeler directement, et il s’occupera de tout le travail nécessaire. Notez que le image paramètre est l’instance d’effet actuelle implémentée.
  • GetInvalidRectanglesForICanvasImageInterop prend en charge GetInvalidRectangles. Cela ne nécessite pas non plus de considérations particulières, à part la nécessité de supprimer le tableau COM retourné une fois qu’il n’est plus nécessaire.
  • GetRequiredSourceRectanglesForICanvasImageInterop est une méthode partagée qui peut prendre en charge les deux GetRequiredSourceRectangle et GetRequiredSourceRectangles. Autrement dit, il faut un pointeur vers un tableau de valeurs existant pour remplir, afin que les appelants puissent passer un pointeur à une seule valeur (qui peut également se trouver sur la pile, pour éviter une allocation) ou à un tableau de valeurs. L’implémentation est la même dans les deux cas, donc une seule exportation C est suffisante pour alimenter les deux.

Effets personnalisés en C# à l’aide de ComputeSharp

Comme nous l’avons mentionné, si vous utilisez C# et que vous souhaitez implémenter un effet personnalisé, l’approche recommandée consiste à utiliser la bibliothèque ComputeSharp . Il vous permet d’implémenter des nuanceurs de pixels D2D1 personnalisés entièrement en C#, ainsi que de définir facilement des graphiques d’effets personnalisés compatibles avec Win2D. La même bibliothèque est également utilisée dans le Microsoft Store pour alimenter plusieurs composants graphiques dans l’application.

Vous pouvez ajouter une référence à ComputeSharp dans votre projet via NuGet :

Remarque

De nombreuses API dans ComputeSharp.D2D1.* sont identiques sur les cibles UWP et WinAppSDK, la seule différence étant l’espace de noms (se terminant par l’un ou l’autre.Uwp)..WinUI Toutefois, la cible UWP est en maintenance soutenue et ne reçoit pas de nouvelles fonctionnalités. Par conséquent, certaines modifications de code peuvent être nécessaires par rapport aux exemples présentés ici pour WinUI. Les extraits de code de ce document reflètent la surface de l’API à partir de ComputeSharp.D2D1.WinUI 3.0.0 (la dernière version de la cible UWP est à la place 2.1.0).

Il existe deux composants principaux dans ComputeSharp pour interagir avec Win2D :

  • PixelShaderEffect<T>: effet Win2D alimenté par un nuanceur de pixels D2D1. Le nuanceur lui-même est écrit en C# à l’aide des API fournies par ComputeSharp. Cette classe fournit également des propriétés pour définir des sources d’effet, des valeurs constantes, etc.
  • CanvasEffect: classe de base pour les effets Win2D personnalisés qui encapsule un graphique d’effet arbitraire. Il peut être utilisé pour « empaqueter » des effets complexes dans un objet facile à utiliser qui peut être réutilisé dans plusieurs parties d’une application.

Voici un exemple de nuanceur de pixels personnalisé (porté à partir de ce nuanceur de nuanceur), utilisé avec PixelShaderEffect<T> , puis dessinez sur un Win2D CanvasControl (notez que l’implémente PixelShaderEffect<T> ICanvasImage) :

un exemple de nuanceur de pixels affichant des hexagones colorés infinis, dessinés sur un contrôle Win2D et affichés en cours d’exécution dans une fenêtre d’application

Vous pouvez voir comment en deux lignes de code vous pouvez créer un effet et le dessiner via Win2D. ComputeSharp s’occupe de tout le travail nécessaire pour compiler le nuanceur, l’inscrire et gérer la durée de vie complexe d’un effet compatible Win2D.

Ensuite, voyons un guide pas à pas sur la création d’un effet Win2D personnalisé qui utilise également un nuanceur de pixels D2D1 personnalisé. Nous allons découvrir comment créer un nuanceur avec ComputeSharp et configurer ses propriétés, puis comment créer un graphique d’effet personnalisé empaqueté dans un CanvasEffect type qui peut facilement être réutilisé dans votre application.

Conception de l’effet

Pour cette démonstration, nous voulons créer un effet de verre givré simple.

Cela inclut les composants suivants :

  • Flou gaussien
  • Effet teinte
  • Bruit (que nous pouvons générer de façon procédurale avec un nuanceur)

Nous voulons également exposer des propriétés pour contrôler le flou et la quantité de bruit. L’effet final contient une version « empaquetée » de ce graphique d’effet et être facile à utiliser en créant simplement une instance, en définissant ces propriétés, en connectant une image source, puis en la dessinant. C’est parti !

Création d’un nuanceur de pixels D2D1 personnalisé

Pour le bruit au-dessus de l’effet, nous pouvons utiliser un nuanceur de pixels D2D1 simple. Le nuanceur calcule une valeur aléatoire en fonction de ses coordonnées (qui agit comme une « valeur initiale » pour le nombre aléatoire), puis utilise cette valeur de bruit pour calculer la quantité RVB pour ce pixel. Nous pouvons ensuite mélanger ce bruit au-dessus de l’image résultante.

Pour écrire le nuanceur avec ComputeSharp, nous devons simplement définir un partial struct type implémentant l’interface ID2D1PixelShader , puis écrire notre logique dans la Execute méthode. Pour ce nuanceur de bruit, nous pouvons écrire quelque chose comme ceci :

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

Remarque

Bien que le nuanceur soit entièrement écrit en C#, les connaissances de base de HLSL (langage de programmation pour les nuanceurs DirectX, vers lesquels ComputeSharp transpile C# ) sont recommandées.

Passons en détail à ce nuanceur :

  • Le nuanceur n’a pas d’entrées, il produit simplement une image infinie avec un bruit aléatoire de nuances de gris.
  • Le nuanceur nécessite l’accès à la coordonnée de pixel actuelle.
  • Le nuanceur est précompilé au moment de la génération (à l’aide du PixelShader40 profil, qui est garanti pour être disponible sur n’importe quel GPU où l’application peut être en cours d’exécution).
  • L’attribut [D2DGeneratedPixelShaderDescriptor] est nécessaire pour déclencher le générateur source groupé avec ComputeSharp, qui analyse le code C#, le transpile en HLSL, compile le nuanceur en bytecode, etc.
  • Le nuanceur capture un float amount paramètre, via son constructeur principal. Le générateur source dans ComputeSharp s’occupe automatiquement de l’extraction de toutes les valeurs capturées dans un nuanceur et de la préparation de la mémoire tampon constante dont D2D a besoin pour initialiser l’état du nuanceur.

Et cette partie est faite ! Ce nuanceur génère notre texture de bruit personnalisée chaque fois que nécessaire. Ensuite, nous devons créer notre effet empaqueté avec le graphique d’effet connectant tous nos effets ensemble.

Création d’un effet personnalisé

Pour notre effet empaqueté facile à utiliser, nous pouvons utiliser le CanvasEffect type de ComputeSharp. Ce type fournit un moyen simple de configurer toute la logique nécessaire pour créer un graphique d’effet et de le mettre à jour via des propriétés publiques avec lesquelles les utilisateurs de l’effet peuvent interagir. Il existe deux méthodes principales que nous devons implémenter :

  • BuildEffectGraph: cette méthode est chargée de créer le graphique d’effet que nous voulons dessiner. Autrement dit, il doit créer tous les effets dont nous avons besoin et inscrire le nœud de sortie pour le graphique. Pour les effets qui peuvent être mis à jour ultérieurement, l’inscription est effectuée avec une valeur associée CanvasEffectNode<T> , qui agit comme clé de recherche pour récupérer les effets à partir du graphique si nécessaire.
  • ConfigureEffectGraph: cette méthode actualise le graphique d’effet en appliquant les paramètres que l’utilisateur a configurés. Cette méthode est appelée automatiquement si nécessaire, juste avant de dessiner l’effet, et uniquement si au moins une propriété d’effet a été modifiée depuis la dernière utilisation de l’effet.

Notre effet personnalisé peut être défini comme suit :

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

Vous pouvez voir qu’il existe quatre sections dans cette classe :

  • Tout d’abord, nous avons des champs pour suivre tous les états mutables, tels que les effets qui peuvent être mis à jour ainsi que les champs de stockage pour toutes les propriétés d’effet que nous voulons exposer aux utilisateurs de l’effet.
  • Ensuite, nous avons des propriétés pour configurer l’effet. Le setter de chaque propriété utilise la SetAndInvalidateEffectGraph méthode exposée par CanvasEffect, qui invalide automatiquement l’effet si la valeur définie est différente de celle actuelle. Cela garantit que l’effet n’est configuré que lorsque cela est vraiment nécessaire.
  • Enfin, nous avons les BuildEffectGraph ConfigureEffectGraph méthodes mentionnées ci-dessus.

Remarque

Le PremultiplyEffect nœud après l’effet de bruit est très important : c’est parce que les effets Win2D supposent que la sortie est prémultipliée, tandis que les nuanceurs de pixels fonctionnent généralement avec des pixels non prémultipliés. Par conséquent, n’oubliez pas d’insérer manuellement des nœuds prémultiply/non prémultiply avant et après les nuanceurs personnalisés pour vous assurer que les couleurs sont correctement conservées.

Remarque

Cet exemple d’effet utilise également des espaces de noms WinUI 3, mais le même code peut également être utilisé sur UWP. Dans ce cas, l’espace de noms pour ComputeSharp correspond ComputeSharp.Uwpau nom du package.

Prêt à dessiner !

Et avec cela, notre effet de verre gelé personnalisé est prêt ! Nous pouvons facilement le dessiner comme suit :

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

    args.DrawingSession.DrawImage(effect);
}

Dans cet exemple, nous dessinons l’effet à partir du gestionnaire d’un CanvasControl, à l’aide Draw d’un CanvasBitmap que nous avons précédemment chargé en tant que source. Il s’agit de l’image d’entrée que nous allons utiliser pour tester l’effet :

une image de quelques montagnes sous un ciel nuageux

Voici le résultat :

une version floue de l’image ci-dessus

Remarque

Crédits à Dominic Lange pour l’image.

Ressources supplémentaires