Effets personnalisés

Direct2D est fourni avec une bibliothèque d’effets qui effectuent diverses opérations d’image courantes. Consultez la rubrique effets intégrés pour obtenir la liste complète des effets. Pour les fonctionnalités qui ne peuvent pas être obtenues avec les effets intégrés, Direct2D vous permet d’écrire vos propres effets personnalisés à l’aide de HLSL standard. Vous pouvez utiliser ces effets personnalisés avec les effets intégrés fournis avec Direct2D.

Pour afficher des exemples d’effet de pixel, de vertex et de nuanceur de calcul complets, consultez l’exemple du Kit de développement logiciel (SDK) D2DCustomEffects.

Dans cette rubrique, nous vous montrons les étapes et les concepts dont vous avez besoin pour concevoir et créer un effet personnalisé complet.

Introduction : Qu’est-ce qu’un effet ?

drop shadow effect diagram.

D’un point de vue conceptuel, un effet Direct2D effectue une tâche d’imagerie, telle que la modification de la luminosité, la désaturation d’une image ou, comme indiqué ci-dessus, la création d’une ombre portée. Pour l’application, ils sont simples. Ils peuvent accepter zéro ou plusieurs images d’entrée, exposer plusieurs propriétés qui contrôlent leur fonctionnement et générer une seule image de sortie.

Un auteur d’effet personnalisé est responsable de quatre parties différentes :

  1. Interface d’effet : l’interface d’effet définit conceptuellement la façon dont une application interagit avec un effet personnalisé (par exemple, le nombre d’entrées que l’effet accepte et les propriétés disponibles). L’interface d’effet gère un graphique de transformation, qui contient les opérations d’imagerie réelles.
  2. Graphe de transformation : chaque effet crée un graphe de transformation interne composé de transformations individuelles. Chaque transformation représente une opération d’image unique. L’effet est chargé de lier ces transformations ensemble en un graphe pour effectuer l’effet d’imagerie prévu. Un effet peut ajouter, supprimer, modifier et réorganiser des transformations en réponse aux modifications apportées aux propriétés externes de l’effet.
  3. Transformation : une transformation représente une seule opération d’image. Son main objectif est d’héberger les nuanceurs exécutés pour chaque pixel de sortie. À cette fin, il est chargé de calculer la nouvelle taille de son image de sortie en fonction de la logique dans ses nuanceurs. Il doit également calculer la zone de son image d’entrée à partir de laquelle les nuanceurs doivent lire pour afficher la région de sortie demandée.
  4. Nuanceur : un nuanceur est exécuté sur l’entrée de la transformation sur le GPU (ou l’UC si le rendu logiciel est spécifié lorsque l’application crée l’appareil Direct3D). Les nuanceurs d’effets sont écrits en langage HLSL (High Level Shading Language) et sont compilés en code d’octets pendant la compilation de l’effet, qui est ensuite chargé par l’effet pendant l’exécution. Ce document de référence explique comment écrire un HLSL compatible Direct2D. La documentation Direct3D contient une vue d’ensemble hlSL de base.

Création d’une interface d’effet

L’interface d’effet définit la façon dont une application interagit avec l’effet personnalisé. Pour créer une interface d’effet, une classe doit implémenter ID2D1EffectImpl, définir des métadonnées qui décrivent l’effet (telles que son nom, son nombre d’entrées et ses propriétés) et créer des méthodes qui inscrivent l’effet personnalisé pour une utilisation avec Direct2D.

Une fois que tous les composants d’une interface d’effet ont été implémentés, l’en-tête de la classe s’affiche comme suit :

#include <d2d1_1.h>
#include <d2d1effectauthor.h>  
#include <d2d1effecthelpers.h>

// Example GUID used to uniquely identify the effect. It is passed to Direct2D during
// effect registration, and used by the developer to identify the effect for any
// ID2D1DeviceContext::CreateEffect calls in the app. The app should create
// a unique name for the effect, as well as a unique GUID using a generation tool.
DEFINE_GUID(CLSID_SampleEffect, 0x00000000, 0x0000, 0x0000, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);

class SampleEffect : public ID2D1EffectImpl
{
public:
    // 2.1 Declare ID2D1EffectImpl implementation methods.
    IFACEMETHODIMP Initialize(
        _In_ ID2D1EffectContext* pContextInternal,
        _In_ ID2D1TransformGraph* pTransformGraph
        );

    IFACEMETHODIMP PrepareForRender(D2D1_CHANGE_TYPE changeType);
    IFACEMETHODIMP SetGraph(_In_ ID2D1TransformGraph* pGraph);

    // 2.2 Declare effect registration methods.
    static HRESULT Register(_In_ ID2D1Factory1* pFactory);
    static HRESULT CreateEffect(_Outptr_ IUnknown** ppEffectImpl);

    // 2.3 Declare IUnknown implementation methods.
    IFACEMETHODIMP_(ULONG) AddRef();
    IFACEMETHODIMP_(ULONG) Release();
    IFACEMETHODIMP QueryInterface(_In_ REFIID riid, _Outptr_ void** ppOutput);

private:
    // Constructor should be private since it should never be called externally.
    SampleEffect();

    LONG m_refCount; // Internal ref count used by AddRef() and Release() methods.
};

Implémenter ID2D1EffectImpl

L’interface ID2D1EffectImpl contient trois méthodes que vous devez implémenter :

Initialize(ID2D1EffectContext *pContextInternal, ID2D1TransformGraph *pTransformGraph)

Direct2D appelle la méthode Initialize une fois que la méthode ID2D1DeviceContext::CreateEffect a été appelée par l’application. Vous pouvez utiliser cette méthode pour effectuer une initialisation interne ou toute autre opération nécessaire à l’effet. En outre, vous pouvez l’utiliser pour créer le graphique de transformation initiale de l’effet.

SetGraph(ID2D1TransformGraph *pTransformGraph)

Direct2D appelle la méthode SetGraph lorsque le nombre d’entrées de l’effet est modifié. Alors que la plupart des effets ont un nombre constant d’entrées, d’autres comme l’effet Composite prennent en charge un nombre variable d’entrées. Cette méthode permet à ces effets de mettre à jour leur graphique de transformation en réponse à un nombre d’entrées changeant. Si un effet ne prend pas en charge un nombre d’entrées de variable, cette méthode peut simplement retourner E_NOTIMPL.

PrepareForRender (D2D1_CHANGE_TYPE changeType)

La méthode PrepareForRender permet aux effets d’effectuer des opérations en réponse à des modifications externes. Direct2D appelle cette méthode juste avant qu’elle ne restitue un effet si au moins l’un de ces éléments est vrai :

  • L’effet a été initialisé précédemment, mais pas encore dessiné.
  • Une propriété d’effet a changé depuis le dernier appel de dessin.
  • L’état du contexte Direct2D appelant (comme ppp) a changé depuis le dernier appel de dessin.

Implémenter les méthodes d’inscription et de rappel d’effet

Les applications doivent inscrire des effets auprès de Direct2D avant de les instancier. Cette inscription est limitée à un instance d’une fabrique Direct2D et doit être répétée chaque fois que l’application est exécutée. Pour activer cette inscription, un effet personnalisé définit un GUID unique, une méthode publique qui enregistre l’effet et une méthode de rappel privée qui retourne une instance de l’effet.

Définir un GUID

Vous devez définir un GUID qui identifie de manière unique l’effet de l’inscription auprès de Direct2D. L’application utilise le même pour identifier l’effet lorsqu’elle appelle ID2D1DeviceContext::CreateEffect.

Ce code illustre la définition d’un tel GUID pour un effet. Vous devez créer son propre GUID unique à l’aide d’un outil de génération de GUID tel que guidgen.exe.

// Example GUID used to uniquely identify the effect. It is passed to Direct2D during
// effect registration, and used by the developer to identify the effect for any
// ID2D1DeviceContext::CreateEffect calls in the app. The app should create
// a unique name for the effect, as well as a unique GUID using a generation tool.
DEFINE_GUID(CLSID_SampleEffect, 0x00000000, 0x0000, 0x0000, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);

Définir une méthode d’inscription publique

Ensuite, définissez une méthode publique que l’application appelle pour enregistrer l’effet auprès de Direct2D. Étant donné que l’inscription d’effet est spécifique à une instance d’une fabrique Direct2D, la méthode accepte une interface ID2D1Factory1 en tant que paramètre. Pour inscrire l’effet, la méthode appelle ensuite l’API ID2D1Factory1::RegisterEffectFromString sur le paramètre ID2D1Factory1 .

Cette API accepte une chaîne XML qui décrit les métadonnées, les entrées et les propriétés de l’effet. Les métadonnées d’un effet sont fournies à titre d’information uniquement et peuvent être interrogées par l’application via l’interface ID2D1Properties . Les données d’entrée et de propriété, en revanche, sont utilisées par Direct2D et représentent les fonctionnalités de l’effet.

Une chaîne XML pour un exemple minimal d’effet est affichée ici. L’ajout de propriétés personnalisées au code XML est abordé dans la section Ajout de propriétés personnalisées à un effet.

#define XML(X) TEXT(#X) // This macro creates a single string from multiple lines of text.

PCWSTR pszXml =
    XML(
        <?xml version='1.0'?>
        <Effect>
            <!-- System Properties -->
            <Property name='DisplayName' type='string' value='SampleEffect'/>
            <Property name='Author' type='string' value='Contoso'/>
            <Property name='Category' type='string' value='Sample'/>
            <Property name='Description' type='string' value='This is a demo effect.'/>
            <Inputs>
                <Input name='SourceOne'/>
                <!-- <Input name='SourceTwo'/> -->
                <!-- Additional inputs go here. -->
            </Inputs>
            <!-- Custom Properties go here. -->
        </Effect>
        );

Définir une méthode de rappel de fabrique d’effets

L’effet doit également fournir une méthode de rappel privée qui retourne une instance de l’effet via un seul paramètre IUnknown**. Un pointeur vers cette méthode est fourni vers Direct2D lorsque l’effet est inscrit via l’API ID2D1Factory1::RegisterEffectFromString via le paramètre PD2D1_EFFECT_FACTORY\.

HRESULT __stdcall SampleEffect::CreateEffect(_Outptr_ IUnknown** ppEffectImpl)
{
    // This code assumes that the effect class initializes its reference count to 1.
    *ppEffectImpl = static_cast<ID2D1EffectImpl*>(new SampleEffect());

    if (*ppEffectImpl == nullptr)
    {
        return E_OUTOFMEMORY;
    }

    return S_OK;
}

Implémenter l’interface IUnknown

Enfin, l’effet doit implémenter l’interface IUnknown pour la compatibilité avec COM.

Création du graphe de transformation de l’effet

Un effet peut utiliser plusieurs transformations différentes (opérations d’image individuelles) pour créer son effet d’image souhaité. Pour contrôler l’ordre dans lequel ces transformations sont appliquées à l’image d’entrée, l’effet les organise dans un graphique de transformation. Un graphe de transformation peut utiliser les effets et les transformations inclus dans Direct2D , ainsi que les transformations personnalisées créées par l’auteur de l’effet.

Utilisation des transformations incluses avec Direct2D

Il s’agit des transformations les plus couramment utilisées fournies avec Direct2D.

Création d’un graphe de transformation à nœud unique

Une fois que vous avez créé une transformation, l’entrée de l’effet doit être connectée à l’entrée de la transformation, et la sortie de la transformation doit être connectée à la sortie de l’effet. Lorsqu’un effet ne contient qu’une seule transformation, vous pouvez utiliser la méthode ID2D1TransformGraph::SetSingleTransformNode pour y parvenir facilement.

Vous pouvez créer ou modifier une transformation dans les méthodes Initialize ou SetGraph de l’effet à l’aide du paramètre ID2D1TransformGraph fourni. Si un effet doit apporter des modifications au graphique de transformation dans une autre méthode où ce paramètre n’est pas disponible, l’effet peut enregistrer le paramètre ID2D1TransformGraph en tant que variable membre de la classe et y accéder ailleurs, comme PrepareForRender ou une méthode de rappel de propriété personnalisée.

Un exemple de méthode Initialize est présenté ici. Cette méthode crée un graphique de transformation à nœud unique qui décalera l’image de cent pixels dans chaque axe.

IFACEMETHODIMP SampleEffect::Initialize(
    _In_ ID2D1EffectContext* pEffectContext,
    _In_ ID2D1TransformGraph* pTransformGraph
    )
{
    HRESULT hr = pEffectContext->CreateOffsetTransform(
        D2D1::Point2L(100,100),  // Offsets the input by 100px in each axis.
        &m_pOffsetTransform
        );

    if (SUCCEEDED(hr))
    {
        // Connects the effect's input to the transform's input, and connects
        // the transform's output to the effect's output.
        hr = pTransformGraph->SetSingleTransformNode(m_pOffsetTransform);
    }

    return hr;
}

Création d’un graphique de transformation à plusieurs nœuds

L’ajout de plusieurs transformations au graphe de transformation d’un effet permet aux effets d’effectuer en interne plusieurs opérations d’image qui sont présentées à une application sous la forme d’un seul effet unifié.

Comme indiqué ci-dessus, le graphe de transformation de l’effet peut être modifié dans n’importe quelle méthode d’effet à l’aide du paramètre ID2D1TransformGraph reçu dans la méthode Initialize de l’effet. Les API suivantes sur cette interface peuvent être utilisées pour créer ou modifier le graphe de transformation d’un effet :

AddNode(ID2D1TransformNode *pNode)

La méthode AddNode , en fait, « inscrit » la transformation avec l’effet et doit être appelée avant que la transformation puisse être utilisée avec l’une des autres méthodes de graphe de transformation.

ConnectToEffectInput(UINT32 toEffectInputIndex, ID2D1TransformNode *pNode, UINT32 toNodeInputIndex)

La méthode ConnectToEffectInput connecte l’entrée d’image de l’effet à l’entrée d’une transformation. La même entrée d’effet peut être connectée à plusieurs transformations.

ConnectNode(ID2D1TransformNode *pFromNode, ID2D1TransformNode *pToNode, UINT32 toNodeInputIndex)

La méthode ConnectNode connecte la sortie d’une transformation à l’entrée d’une autre transformation. Une sortie de transformation peut être connectée à plusieurs transformations.

SetOutputNode(ID2D1TransformNode *pNode)

La méthode SetOutputNode connecte la sortie d’une transformation à la sortie de l’effet. Étant donné qu’un effet n’a qu’une seule sortie, une seule transformation peut être désignée comme « nœud de sortie ».

Ce code utilise deux transformations distinctes pour créer un effet unifié. Dans ce cas, l’effet est une ombre portée traduite.

IFACEMETHODIMP SampleEffect::Initialize(
    _In_ ID2D1EffectContext* pEffectContext, 
    _In_ ID2D1TransformGraph* pTransformGraph
    )
{   
    // Create the shadow effect.
    HRESULT hr = pEffectContext->CreateEffect(CLSID_D2D1Shadow, &m_pShadowEffect);

    // Create the shadow transform from the shadow effect.
    if (SUCCEEDED(hr))
    {
        hr = pEffectContext->CreateTransformNodeFromEffect(m_pShadowEffect, &m_pShadowTransform);
    }

    // Create the offset transform.
    if (SUCCEEDED(hr))
    {
        hr = pEffectContext->CreateOffsetTransform(
            D2D1::Point2L(0,0),
            &m_pOffsetTransform
            );
    }

    // Register both transforms with the effect graph.
    if (SUCCEEDED(hr))
    {
        hr = pTransformGraph->AddNode(m_pShadowTransform);
    }

    if (SUCCEEDED(hr))
    {
        hr = pTransformGraph->AddNode(m_pOffsetTransform);
    }

    // Connect the custom effect's input to the shadow transform's input.
    if (SUCCEEDED(hr))
    {
        hr = pTransformGraph->ConnectToEffectInput(
            0,                  // Input index of the effect.
            m_pShadowTransform, // The receiving transform.
            0                   // Input index of the receiving transform.
            );
    }

    // Connect the shadow transform's output to the offset transform's input.
    if (SUCCEEDED(hr))
    {
        hr = pTransformGraph->ConnectNode(
            m_pShadowTransform, // 'From' node.
            m_pOffsetTransform, // 'To' node.
            0                   // Input index of the 'to' node. There is only one output for the 'From' node.
            );
    }

    // Connect the offset transform's output to the custom effect's output.
    if (SUCCEEDED(hr))
    {
        hr = pTransformGraph->SetOutputNode(
            m_pOffsetTransform
            );
    }

    return hr;
}

Ajout de propriétés personnalisées à un effet

Les effets peuvent définir des propriétés personnalisées qui permettent à une application de modifier le comportement de l’effet pendant l’exécution. Il existe trois étapes pour définir une propriété pour un effet personnalisé :

Ajouter les métadonnées de propriété aux données d’inscription de l’effet

Ajouter une propriété au xml d’inscription

Vous devez définir les propriétés d’un effet personnalisé lors de l’inscription initiale de l’effet auprès de Direct2D. Tout d’abord, vous devez mettre à jour le code XML d’inscription de l’effet dans sa méthode d’inscription publique avec la nouvelle propriété :

PCWSTR pszXml =
    TEXT(
        <?xml version='1.0'?>
        <Effect>
            <!-- System Properties -->
            <Property name='DisplayName' type='string' value='SampleEffect'/>
            <Property name='Author' type='string' value='Contoso'/>
            <Property name='Category' type='string' value='Sample'/>
            <Property name='Description'
                type='string'
                value='Translates an image by a user-specifiable amount.'/>
            <Inputs>
                <Input name='Source'/>
                <!-- Additional inputs go here. -->
            </Inputs>
            <!-- Custom Properties go here. -->
            <Property name='Offset' type='vector2'>
                <Property name='DisplayName' type='string' value='Image Offset'/>
                <!— Optional sub-properties -->
                <Property name='Min' type='vector2' value='(-1000.0, -1000.0)' />
                <Property name='Max' type='vector2' value='(1000.0, 1000.0)' />
                <Property name='Default' type='vector2' value='(0.0, 0.0)' />
            </Property>
        </Effect>
        );

Lorsque vous définissez une propriété d’effet en XML, elle a besoin d’un nom, d’un type et d’un nom d’affichage. Le nom d’affichage d’une propriété, ainsi que les valeurs de catégorie, d’auteur et de description de l’effet global peuvent et doivent être localisés.

Pour chaque propriété, un effet peut éventuellement spécifier des valeurs par défaut, min et max. Ces valeurs sont destinées à des fins d’information uniquement. Elles ne sont pas appliquées par Direct2D. Il vous incombe d’implémenter vous-même toute logique par défaut/min/max spécifiée dans la classe d’effet.

La valeur de type répertoriée dans le xml de la propriété doit correspondre au type de données correspondant utilisé par les méthodes getter et setter de la propriété. Les valeurs XML correspondantes pour chaque type de données sont indiquées dans ce tableau :

Type de données Valeur XML correspondante
PWSTR string
BOOL bool
UINT uint32
INT int32
FLOAT float
D2D_VECTOR_2F vector2
D2D_VECTOR_3F vector3
D2D_VECTOR_4F vector4
D2D_MATRIX_3X2_F matrix3x2
D2D_MATRIX_4X3_F matrix4x3
D2D_MATRIX_4X4_F matrix4x4
D2D_MATRIX_5X4_F matrix5x4
BYTE[] objet BLOB
IUnknown * Iunknown
ID2D1ColorContext* colorcontext
CLSID clsid
Énumération (D2D1_INTERPOLATION_MODE, etc.) enum

 

Mapper la nouvelle propriété aux méthodes getter et setter

Ensuite, l’effet doit mapper cette nouvelle propriété aux méthodes getter et setter. Cela s’effectue via le tableau D2D1_PROPERTY_BINDING passé à la méthode ID2D1Factory1::RegisterEffectFromString .

Le tableau D2D1_PROPERTY_BINDING ressemble à ceci :

const D2D1_PROPERTY_BINDING bindings[] =
{
    D2D1_VALUE_TYPE_BINDING(
        L"Offset",      // The name of property. Must match name attribute in XML.
        &SetOffset,     // The setter method that is called on "SetValue".
        &GetOffset      // The getter method that is called on "GetValue".
        )
};

Une fois que vous avez créé le tableau XML et les liaisons, passez-les dans la méthode RegisterEffectFromString :

pFactory->RegisterEffectFromString(
    CLSID_SampleEffect,  // GUID defined in class header file.
    pszXml,              // Previously-defined XML that describes effect.
    bindings,            // The previously-defined property bindings array.
    ARRAYSIZE(bindings), // Number of entries in the property bindings array.    
    CreateEffect         // Static method that returns an instance of the effect's class.
    );

La macro D2D1_VALUE_TYPE_BINDING nécessite que la classe d’effet hérite de ID2D1EffectImpl avant toute autre interface.

Les propriétés personnalisées d’un effet sont indexées dans l’ordre dans lequel elles sont déclarées dans le code XML, et une fois créées, l’application est accessible à l’aide des méthodes ID2D1Properties::SetValue et ID2D1Properties::GetValue . Pour des raisons pratiques, vous pouvez créer une énumération publique qui répertorie chaque propriété dans le fichier d’en-tête de l’effet :

typedef enum SAMPLEEFFECT_PROP
{
    SAMPLEFFECT_PROP_OFFSET = 0
};

Créer les méthodes getter et setter pour la propriété

L’étape suivante consiste à créer les méthodes getter et setter pour la nouvelle propriété. Les noms des méthodes doivent correspondre à ceux spécifiés dans le tableau D2D1_PROPERTY_BINDING . En outre, le type de propriété spécifié dans le xml de l’effet doit correspondre au type du paramètre de la méthode setter et à la valeur de retour de la méthode getter.

HRESULT SampleEffect::SetOffset(D2D_VECTOR_2F offset)
{
    // Method must manually clamp to values defined in XML.
    offset.x = min(offset.x, 1000.0f); 
    offset.x = max(offset.x, -1000.0f); 

    offset.y = min(offset.y, 1000.0f); 
    offset.y = max(offset.y, -1000.0f); 

    m_offset = offset;

    return S_OK;
}

D2D_VECTOR_2F SampleEffect::GetOffset() const
{
    return m_offset;
}

Transformations de l’effet de mise à jour en réponse à une modification de propriété

Pour mettre à jour la sortie d’image d’un effet en réponse à une modification de propriété, l’effet doit modifier ses transformations sous-jacentes. Cela est généralement effectué dans la méthode PrepareForRender de l’effet que Direct2D appelle automatiquement lorsque l’une des propriétés d’un effet a été modifiée. Toutefois, les transformations peuvent être mises à jour dans n’importe quelle méthode de l’effet , comme Initialize ou les méthodes setter de propriété de l’effet.

Par exemple, si un effet contient un ID2D1OffsetTransform et souhaite modifier sa valeur de décalage en réponse à la modification de la propriété Offset de l’effet, il ajoute le code suivant dans PrepareForRender :

IFACEMETHODIMP SampleEffect::PrepareForRender(D2D1_CHANGE_TYPE changeType)
{
    // All effect properties are DPI independent (specified in DIPs). In this offset
    // example, the offset value provided must be scaled from DIPs to pixels to ensure
    // a consistent appearance at different DPIs (excluding minor scaling artifacts).
    // A context's DPI can be retrieved using the ID2D1EffectContext::GetDPI API.
    
    D2D1_POINT_2L pixelOffset;
    pixelOffset.x = static_cast<LONG>(m_offset.x * (m_dpiX / 96.0f));
    pixelOffset.y = static_cast<LONG>(m_offset.y * (m_dpiY / 96.0f));
    
    // Update the effect's offset transform with the new offset value.
    m_pOffsetTransform->SetOffset(pixelOffset);

    return S_OK;
}

Création d’une transformation personnalisée

Pour implémenter des opérations d’image au-delà de ce qui est fourni dans Direct2D, vous devez implémenter des transformations personnalisées. Les transformations personnalisées peuvent modifier arbitrairement une image d’entrée à l’aide de nuanceurs HLSL personnalisés.

Les transformations implémentent l’une des deux interfaces différentes en fonction des types de nuanceurs qu’elles utilisent. Les transformations utilisant des nuanceurs de pixels et/ou de vertex doivent implémenter ID2D1DrawTransform, tandis que les transformations utilisant des nuanceurs de calcul doivent implémenter ID2D1ComputeTransform. Ces interfaces héritent toutes deux d’ID2D1Transform. Cette section se concentre sur l’implémentation des fonctionnalités communes aux deux.

L’interface ID2D1Transform a quatre méthodes à implémenter :

GetInputCount

Cette méthode retourne un entier représentant le nombre d’entrées pour la transformation.

IFACEMETHODIMP_(UINT32) GetInputCount() const
{
    return 1;
}

MapInputRectsToOutputRect

Direct2D appelle la méthode MapInputRectsToOutputRect chaque fois que la transformation est rendue. Direct2D transmet un rectangle représentant les limites de chacune des entrées de la transformation. La transformation est ensuite responsable du calcul des limites de l’image de sortie. La taille des rectangles pour toutes les méthodes de cette interface (ID2D1Transform) est définie en pixels, et non en dips.

Cette méthode est également responsable du calcul de la région de la sortie opaque en fonction de la logique de son nuanceur et des régions opaques de chaque entrée. Une région opaque d’une image est définie comme celle où le canal alpha est « 1 » pour l’intégralité du rectangle. S’il n’est pas clair si la sortie d’une transformation est opaque, le rectangle opaque de sortie doit être défini sur (0, 0, 0, 0) comme valeur sécurisée. Direct2D utilise ces informations pour effectuer des optimisations de rendu avec du contenu « opaque garanti ». Si cette valeur est inexacte, cela peut entraîner un rendu incorrect.

Vous pouvez modifier le comportement de rendu de la transformation (comme défini dans les sections 6 à 8) au cours de cette méthode. Toutefois, vous ne pouvez pas modifier d’autres transformations dans le graphe de transformation ou la disposition de graphe elle-même ici.

IFACEMETHODIMP SampleTransform::MapInputRectsToOutputRect(
    _In_reads_(inputRectCount) const D2D1_RECT_L* pInputRects,
    _In_reads_(inputRectCount) const D2D1_RECT_L* pInputOpaqueSubRects,
    UINT32 inputRectCount,
    _Out_ D2D1_RECT_L* pOutputRect,
    _Out_ D2D1_RECT_L* pOutputOpaqueSubRect
    )
{
    // This transform is designed to only accept one input.
    if (inputRectCount != 1)
    {
        return E_INVALIDARG;
    }

    // The output of the transform will be the same size as the input.
    *pOutputRect = pInputRects[0];
    // Indicate that the image's opacity has not changed.
    *pOutputOpaqueSubRect = pInputOpaqueSubRects[0];
    // The size of the input image can be saved here for subsequent operations.
    m_inputRect = pInputRects[0];

    return S_OK;
}

Pour un exemple plus complexe, réfléchissez à la façon dont une opération de flou simple serait représentée :

Si une opération de flou utilise un rayon de 5 pixels, la taille du rectangle de sortie doit augmenter de 5 pixels, comme indiqué ci-dessous. Lors de la modification des coordonnées de rectangle, une transformation doit s’assurer que sa logique n’entraîne pas de dépassements/sous-flux dans les coordonnées du rectangle.

// Expand output image by 5 pixels.

// Do not expand empty input rectangles.
if (pInputRects[0].right  > pInputRects[0].left &&
    pInputRects[0].bottom > pInputRects[0].top
    )
{
    pOutputRect->left   = ((pInputRects[0].left   - 5) < pInputRects[0].left  ) ? (pInputRects[0].left   - 5) : LONG_MIN;
    pOutputRect->top    = ((pInputRects[0].top    - 5) < pInputRects[0].top   ) ? (pInputRects[0].top    - 5) : LONG_MIN;
    pOutputRect->right  = ((pInputRects[0].right  + 5) > pInputRects[0].right ) ? (pInputRects[0].right  + 5) : LONG_MAX;
    pOutputRect->bottom = ((pInputRects[0].bottom + 5) > pInputRects[0].bottom) ? (pInputRects[0].bottom + 5) : LONG_MAX;
}

Étant donné que l’image est floue, une région de l’image opaque peut maintenant être partiellement transparente. En effet, la zone située à l’extérieur de l’image est par défaut en noir transparent et cette transparence sera fusionnée dans l’image autour des bords. La transformation doit refléter cela dans ses calculs de rectangle opaque de sortie :

// Shrink opaque region by 5 pixels.
pOutputOpaqueSubRect->left   = pInputOpaqueSubRects[0].left   + 5;
pOutputOpaqueSubRect->top    = pInputOpaqueSubRects[0].top    + 5;
pOutputOpaqueSubRect->right  = pInputOpaqueSubRects[0].right  - 5;
pOutputOpaqueSubRect->bottom = pInputOpaqueSubRects[0].bottom - 5;

Ces calculs sont visualisées ici :

illustration de calcul rectangle.

Pour plus d’informations sur cette méthode, consultez la page de référence MapInputRectsToOutputRect .

MapOutputRectToInputRects

Direct2D appelle la méthode MapOutputRectToInputRects après MapInputRectsToOutputRect. La transformation doit calculer la partie de l’image à partir de laquelle elle doit lire afin de restituer correctement la région de sortie demandée.

Comme précédemment, si un effet mappe strictement les pixels 1 à 1, il peut passer le rectangle de sortie au rectangle d’entrée :

IFACEMETHODIMP SampleTransform::MapOutputRectToInputRects(
    _In_ const D2D1_RECT_L* pOutputRect,
    _Out_writes_(inputRectCount) D2D1_RECT_L* pInputRects,
    UINT32 inputRectCount
    ) const
{
    // This transform is designed to only accept one input.
    if (inputRectCount != 1)
    {
        return E_INVALIDARG;
    }

    // The input needed for the transform is the same as the visible output.
    pInputRects[0] = *pOutputRect;
    return S_OK;
}

De même, si une transformation réduit ou développe une image (comme l’exemple de flou ici), les pixels utilisent souvent les pixels environnants pour calculer leur valeur. Avec un flou, un pixel est moyenné avec ses pixels environnants, même s’ils sont en dehors des limites de l’image d’entrée. Ce comportement est reflété dans le calcul. Comme précédemment, la transformation vérifie les dépassements de capacité lors du développement des coordonnées d’un rectangle.

// Expand the input rectangle to reflect that more pixels need to 
// be read from than are necessarily rendered in the effect's output.
pInputRects[0].left   = ((pOutputRect->left   - 5) < pOutputRect->left  ) ? (pOutputRect->left   - 5) : LONG_MIN;
pInputRects[0].top    = ((pOutputRect->top    - 5) < pOutputRect->top   ) ? (pOutputRect->top    - 5) : LONG_MIN;
pInputRects[0].right  = ((pOutputRect->right  + 5) > pOutputRect->right ) ? (pOutputRect->right  + 5) : LONG_MAX;
pInputRects[0].bottom = ((pOutputRect->bottom + 5) > pOutputRect->bottom) ? (pOutputRect->bottom + 5) : LONG_MAX;

Cette figure visualise le calcul. Direct2D échantillonne automatiquement des pixels noirs transparents là où l’image d’entrée n’existe pas, ce qui permet de fusionner progressivement le flou avec le contenu existant à l’écran.

illustration d’un effet échantillonnant des pixels noirs transparents en dehors d’un rectangle.

Si le mappage n’est pas trivial, cette méthode doit définir le rectangle d’entrée sur la zone maximale pour garantir des résultats corrects. Pour ce faire, définissez les bords gauche et supérieur sur INT_MIN, et les bords droit et inférieur sur INT_MAX.

Pour plus d’informations sur cette méthode, consultez la rubrique MapOutputRectToInputRects .

MapInvalidRect

Direct2D appelle également la méthode MapInvalidRect . Toutefois, contrairement aux méthodes MapInputRectsToOutputRect et MapOutputRectToInputRects , Il n’est pas garanti de l’appeler à un moment donné. Cette méthode détermine conceptuellement quelle partie de la sortie d’une transformation doit être restituée en réponse à la modification d’une partie ou de la totalité de son entrée. Il existe trois scénarios différents pour calculer le rect non valide d’une transformation.

Transformations avec mappage de pixels un-à-un

Pour les transformations qui mappent les pixels 1 à 1, il suffit de passer le rectangle d’entrée non valide au rectangle de sortie non valide :

IFACEMETHODIMP SampleTransform::MapInvalidRect(
    UINT32 inputIndex,
    D2D1_RECT_L invalidInputRect,
    _Out_ D2D1_RECT_L* pInvalidOutputRect
    ) const
{
    // This transform is designed to only accept one input.
    if (inputIndex != 0)
    {
        return E_INVALIDARG;
    }

    // If part of the transform's input is invalid, mark the corresponding
    // output region as invalid. 
    *pInvalidOutputRect = invalidInputRect;

    return S_OK;
}

Transformations avec mappage de pixels plusieurs à plusieurs

Lorsque les pixels de sortie d’une transformation dépendent de leur zone environnante, le rectangle d’entrée non valide doit être développé en conséquence. Cela permet de refléter que les pixels entourant le rectangle d’entrée non valide seront également affectés et deviennent non valides. Par exemple, un flou de cinq pixels utilise le calcul suivant :

// Expand the input invalid rectangle by five pixels in each direction. This
// reflects that a change in part of the given input image will cause a change
// in an expanded part of the output image (five pixels in each direction).
pInvalidOutputRect->left   = ((invalidInputRect.left   - 5) < invalidInputRect.left  ) ? (invalidInputRect.left   - 5) : LONG_MIN;
pInvalidOutputRect->top    = ((invalidInputRect.top    - 5) < invalidInputRect.top   ) ? (invalidInputRect.top    - 5) : LONG_MIN;
pInvalidOutputRect->right  = ((invalidInputRect.right  + 5) > invalidInputRect.right ) ? (invalidInputRect.right  + 5) : LONG_MAX;
pInvalidOutputRect->bottom = ((invalidInputRect.bottom + 5) > invalidInputRect.bottom) ? (invalidInputRect.bottom + 5) : LONG_MAX;

Transformations avec mappage de pixels complexes

Pour les transformations où les pixels d’entrée et de sortie n’ont pas de mappage simple, la sortie entière peut être marquée comme non valide. Par exemple, si une transformation génère simplement la couleur moyenne de l’entrée, la sortie entière de la transformation change si même une petite partie de l’entrée est modifiée. Dans ce cas, le rectangle de sortie non valide doit être défini sur un rectangle logiquement infini (illustré ci-dessous). Direct2D l’attache automatiquement aux limites de la sortie.

// If any change in the input image affects the entire output, the
// transform should set pInvalidOutputRect to a logically infinite rect.
*pInvalidOutputRect = D2D1::RectL(LONG_MIN, LONG_MIN, LONG_MAX, LONG_MAX);

Pour plus d’informations sur cette méthode, consultez la rubrique MapInvalidRect .

Une fois ces méthodes implémentées, l’en-tête de votre transformation contient les éléments suivants :

class SampleTransform : public ID2D1Transform 
{
public:
    SampleTransform();

    // ID2D1TransformNode Methods:
    IFACEMETHODIMP_(UINT32) GetInputCount() const;
    
    // ID2D1Transform Methods:
    IFACEMETHODIMP MapInputRectsToOutputRect(
        _In_reads_(inputRectCount) const D2D1_RECT_L* pInputRects,
        _In_reads_(inputRectCount) const D2D1_RECT_L* pInputOpaqueSubRects,
        UINT32 inputRectCount,
        _Out_ D2D1_RECT_L* pOutputRect,
        _Out_ D2D1_RECT_L* pOutputOpaqueSubRect
        );    

    IFACEMETHODIMP MapOutputRectToInputRects(
        _In_ const D2D1_RECT_L* pOutputRect,
        _Out_writes_(inputRectCount) D2D1_RECT_L* pInputRects,
        UINT32 inputRectCount
        ) const;

    IFACEMETHODIMP MapInvalidRect(
        UINT32 inputIndex,
        D2D1_RECT_L invalidInputRect,
        _Out_ D2D1_RECT_L* pInvalidOutputRect 
        ) const;

    // IUnknown Methods:
    IFACEMETHODIMP_(ULONG) AddRef();
    IFACEMETHODIMP_(ULONG) Release();
    IFACEMETHODIMP QueryInterface(REFIID riid, _Outptr_ void** ppOutput);

private:
    LONG m_cRef; // Internal ref count used by AddRef() and Release() methods.
    D2D1_RECT_L m_inputRect; // Stores the size of the input image.
};

Ajout d’un nuanceur de pixels à une transformation personnalisée

Une fois qu’une transformation a été créée, elle doit fournir un nuanceur qui manipulera les pixels de l’image. Cette section décrit les étapes d’utilisation d’un nuanceur de pixels avec une transformation personnalisée.

Implémentation d’ID2D1DrawTransform

Pour utiliser un nuanceur de pixels, la transformation doit implémenter l’interface ID2D1DrawTransform , qui hérite de l’interface ID2D1Transform décrite dans la section 5. Cette interface contient une nouvelle méthode à implémenter :

SetDrawInfo(ID2D1DrawInfo *pDrawInfo)

Direct2D appelle la méthode SetDrawInfo lorsque la transformation est ajoutée pour la première fois au graphe de transformation d’un effet. Cette méthode fournit un paramètre ID2D1DrawInfo qui contrôle le rendu de la transformation. Pour connaître les méthodes disponibles ici, consultez la rubrique ID2D1DrawInfo .

Si la transformation choisit de stocker ce paramètre en tant que variable membre de classe, l’objet drawInfo est accessible et modifié à partir d’autres méthodes telles que les setters de propriétés ou MapInputRectsToOutputRect. Notamment, il ne peut pas être appelé à partir des méthodes MapOutputRectToInputRects ou MapInvalidRect sur ID2D1Transform.

Création d’un GUID pour le nuanceur de pixels

Ensuite, la transformation doit définir un GUID unique pour le nuanceur de pixels lui-même. Cela est utilisé lorsque Direct2D charge le nuanceur en mémoire, ainsi que lorsque la transformation choisit le nuanceur de pixels à utiliser pour l’exécution. Des outils tels que guidgen.exe, inclus dans Visual Studio, peuvent être utilisés pour générer un GUID aléatoire.

// Example GUID used to uniquely identify HLSL shader. Passed to Direct2D during
// shader load, and used by the transform to identify the shader for the
// ID2D1DrawInfo::SetPixelShader method. The effect author should create a
// unique name for the shader as well as a unique GUID using
// a GUID generation tool.
DEFINE_GUID(GUID_SamplePixelShader, 0x00000000, 0x0000, 0x0000, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);

Chargement du nuanceur de pixels avec Direct2D

Un nuanceur de pixels doit être chargé dans la mémoire avant de pouvoir être utilisé par la transformation.

Pour charger le nuanceur de pixels en mémoire, la transformation doit lire le code d’octet du nuanceur compilé à partir de . Fichier CSO généré par Visual Studio (consultez la documentation Direct3D pour plus d’informations) dans un tableau d’octets. Cette technique est illustrée en détail dans l’exemple sdk D2DCustomEffects.

Une fois les données du nuanceur chargées dans un tableau d’octets, appelez la méthode LoadPixelShader sur l’objet ID2D1EffectContext de l’effet. Direct2D ignore les appels à LoadPixelShader lorsqu’un nuanceur avec le même GUID a déjà été chargé.

Une fois qu’un nuanceur de pixels a été chargé dans la mémoire, la transformation doit le sélectionner pour l’exécution en passant son GUID à la méthode SetPixelShader sur le paramètre ID2D1DrawInfo fourni pendant la méthode SetDrawInfo . Le nuanceur de pixels doit déjà être chargé en mémoire avant d’être sélectionné pour exécution.

Modification de l’opération de nuanceur avec des mémoires tampons constantes

Pour modifier l’exécution d’un nuanceur, une transformation peut passer une mémoire tampon constante au nuanceur de pixels. Pour ce faire, une transformation définit un struct qui contient les variables souhaitées dans l’en-tête de classe :

// This struct defines the constant buffer of the pixel shader.
struct
{
    float valueOne;
    float valueTwo;
} m_constantBuffer;

La transformation appelle ensuite la méthode ID2D1DrawInfo::SetPixelShaderConstantBuffer sur le paramètre ID2D1DrawInfo fourni dans la méthode SetDrawInfo pour passer cette mémoire tampon au nuanceur.

Le HLSL doit également définir un struct correspondant qui représente la mémoire tampon constante. Les variables contenues dans le struct du nuanceur doivent correspondre à celles du struct de la transformation.

cbuffer constants : register(b0)
{
    float valueOne : packoffset(c0.x);
    float valueTwo : packoffset(c0.y);
};

Une fois la mémoire tampon définie, les valeurs contenues dans peuvent être lues à partir de n’importe où dans le nuanceur de pixels.

Écriture d’un nuanceur de pixels pour Direct2D

Les transformations Direct2D utilisent des nuanceurs créés à l’aide de HLSL standard. Toutefois, il existe quelques concepts clés pour écrire un nuanceur de pixels qui s’exécute à partir du contexte d’une transformation. Pour obtenir un exemple complet d’un nuanceur de pixels entièrement fonctionnel, consultez l’exemple sdk D2DCustomEffects.

Direct2D mappe automatiquement les entrées d’une transformation aux objets Texture2D et SamplerState dans le HLSL. Le premier Texture2D se trouve au registre t0, et le premier SamplerState se trouve à register s0. Chaque entrée supplémentaire se trouve dans les registres correspondants suivants (t1 et s1 par exemple). Les données de pixel pour une entrée particulière peuvent être échantillonnées en appelant Sample sur l’objet Texture2D et en transmettant l’objet SamplerState correspondant et les coordonnées texel.

Un nuanceur de pixels personnalisé est exécuté une fois pour chaque pixel rendu. Chaque fois que le nuanceur est exécuté, Direct2D fournit automatiquement trois paramètres qui identifient sa position d’exécution actuelle :

  • Sortie de l’espace de scène : ce paramètre représente la position d’exécution actuelle en termes de surface cible globale. Il est défini en pixels et ses valeurs min/max correspondent aux limites du rectangle retourné par MapInputRectsToOutputRect.
  • Sortie espace clip : ce paramètre est utilisé par Direct3D et ne doit pas être utilisé dans le nuanceur de pixels d’une transformation.
  • Entrée espace Texel : ce paramètre représente la position d’exécution actuelle dans une texture d’entrée particulière. Un nuanceur ne doit pas prendre de dépendances sur la façon dont cette valeur est calculée. Il doit uniquement l’utiliser pour échantillonner l’entrée du nuanceur de pixels, comme indiqué dans le code ci-dessous :
Texture2D InputTexture : register(t0);
SamplerState InputSampler : register(s0);

float4 main(
    float4 clipSpaceOutput  : SV_POSITION,
    float4 sceneSpaceOutput : SCENE_POSITION,
    float4 texelSpaceInput0 : TEXCOORD0
    ) : SV_Target
{
    // Samples pixel from ten pixels above current position.

    float2 sampleLocation =
        texelSpaceInput0.xy    // Sample position for the current output pixel.
        + float2(0,-10)        // An offset from which to sample the input, specified in pixels.
        * texelSpaceInput0.zw; // Multiplier that converts pixel offset to sample position offset.

    float4 color = InputTexture.Sample(
        InputSampler,          // Sampler and Texture must match for a given input.
        sampleLocation
        );

    return color;
}

Ajout d’un nuanceur de vertex à une transformation personnalisée

Vous pouvez utiliser des nuanceurs de vertex pour accomplir différents scénarios d’imagerie que les nuanceurs de pixels. En particulier, les nuanceurs de vertex peuvent effectuer des effets d’image basés sur la géométrie en transformant les sommets qui composent une image. Les nuanceurs de vertex peuvent être utilisés indépendamment de ou conjointement avec des nuanceurs de pixels spécifiés par transformation. Si aucun nuanceur de vertex n’est spécifié, Direct2D remplace dans un nuanceur de vertex par défaut à utiliser avec le nuanceur de pixels personnalisé.

Le processus d’ajout d’un nuanceur de vertex à une transformation personnalisée est similaire à celui d’un nuanceur de pixels : la transformation implémente l’interface ID2D1DrawTransform , crée un GUID et (éventuellement) transmet des mémoires tampons constantes au nuanceur. Toutefois, il existe quelques étapes supplémentaires clés qui sont propres aux nuanceurs de vertex :

Création d’une mémoire tampon de vertex

Un nuanceur de vertex par définition s’exécute sur les sommets qui lui sont passés, et non sur des pixels individuels. Pour spécifier les sommets que le nuanceur doit exécuter, une transformation crée une mémoire tampon de vertex à passer au nuanceur. La disposition des tampons de vertex dépasse l’étendue de ce document. Pour plus d’informations, consultez la référence Direct3D ou l’exemple sdk D2DCustomEffects pour obtenir un exemple d’implémentation.

Après avoir créé une mémoire tampon de vertex en mémoire, la transformation utilise la méthode CreateVertexBuffer sur l’objet ID2D1EffectContext de l’effet conteneur pour transmettre ces données au GPU. Encore une fois, consultez l’exemple sdk D2DCustomEffects pour obtenir un exemple d’implémentation.

Si aucune mémoire tampon de vertex n’est spécifiée par la transformation, Direct2D transmet une mémoire tampon de vertex par défaut représentant l’emplacement de l’image rectangulaire.

Modification de SetDrawInfo pour utiliser un nuanceur de vertex

Comme avec les nuanceurs de pixels, la transformation doit charger et sélectionner un nuanceur de vertex pour l’exécution. Pour charger le nuanceur de vertex, il appelle la méthode LoadVertexShader sur la méthode ID2D1EffectContext reçue dans la méthode Initialize de l’effet. Pour sélectionner le nuanceur de vertex à exécuter, il appelle SetVertexProcessing sur le paramètre ID2D1DrawInfo reçu dans la méthode SetDrawInfo de la transformation. Cette méthode accepte un GUID pour un nuanceur de vertex précédemment chargé, ainsi que (éventuellement) une mémoire tampon de vertex créée précédemment sur laquelle le nuanceur doit s’exécuter.

Implémentation d’un nuanceur de vertex Direct2D

Une transformation de dessin peut contenir à la fois un nuanceur de pixels et un nuanceur de vertex. Si une transformation définit à la fois un nuanceur de pixels et un nuanceur de vertex, la sortie du nuanceur de vertex est directement donnée au nuanceur de pixels : l’application peut personnaliser la signature de retour du nuanceur de vertex et des paramètres du nuanceur de pixels tant qu’ils sont cohérents.

En revanche, si une transformation contient uniquement un nuanceur de vertex et s’appuie sur le nuanceur de pixels direct par défaut de Direct2D, elle doit retourner la sortie par défaut suivante :

struct VSOut
{
    float4 clipSpaceOutput  : SV_POSITION; 
    float4 sceneSpaceOutput : SCENE_POSITION;
    float4 texelSpaceInput0 : TEXCOORD0;  
};

Un nuanceur de vertex stocke le résultat de ses transformations de vertex dans la variable de sortie d’espace de scène du nuanceur. Pour calculer la sortie clip-space et les variables d’entrée Texel-space, Direct2D fournit automatiquement des matrices de conversion dans une mémoire tampon constante :

// Constant buffer b0 is used to store the transformation matrices from scene space
// to clip space. Depending on the number of inputs to the vertex shader, there
// may be more or fewer "sceneToInput" matrices.
cbuffer Direct2DTransforms : register(b0)
{
    float2x1 sceneToOutputX;
    float2x1 sceneToOutputY;
    float2x1 sceneToInput0X;
    float2x1 sceneToInput0Y;
};

Vous trouverez ci-dessous un exemple de code de nuanceur de vertex qui utilise les matrices de conversion pour calculer les espaces de découpage et de texel appropriés attendus par Direct2D :

// Constant buffer b0 is used to store the transformation matrices from scene space
// to clip space. Depending on the number of inputs to the vertex shader, there
// may be more or fewer "sceneToInput" matrices.
cbuffer Direct2DTransforms : register(b0)
{
    float2x1 sceneToOutputX;
    float2x1 sceneToOutputY;
    float2x1 sceneToInput0X;
    float2x1 sceneToInput0Y;
};

// Default output structure. This can be customized if transform also contains pixel shader.
struct VSOut
{
    float4 clipSpaceOutput  : SV_POSITION; 
    float4 sceneSpaceOutput : SCENE_POSITION;
    float4 texelSpaceInput0 : TEXCOORD0;  
};

// The parameter(s) passed to the vertex shader are defined by the vertex buffer's layout
// as specified by the transform. If no vertex buffer is specified, Direct2D passes two
// triangles representing the rectangular image with the following layout:
//
//    float4 outputScenePosition : OUTPUT_SCENE_POSITION;
//
//    The x and y coordinates of the outputScenePosition variable represent the image's
//    position on the screen. The z and w coordinates are used for perspective and
//    depth-buffering.

VSOut GeometryVS(float4 outputScenePosition : OUTPUT_SCENE_POSITION) 
{
    VSOut output;

    // Compute Scene-space output (vertex simply passed-through here). 
    output.sceneSpaceOutput.x = outputScenePosition.x;
    output.sceneSpaceOutput.y = outputScenePosition.y;
    output.sceneSpaceOutput.z = outputScenePosition.z;
    output.sceneSpaceOutput.w = outputScenePosition.w;

    // Generate standard Clip-space output coordinates.
    output.clipSpaceOutput.x = (output.sceneSpaceOutput.x * sceneToOutputX[0]) +
        output.sceneSpaceOutput.w * sceneToOutputX[1];

    output.clipSpaceOutput.y = (output.sceneSpaceOutput.y * sceneToOutputY[0]) + 
        output.sceneSpaceOutput.w * sceneToOutputY[1];

    output.clipSpaceOutput.z = output.sceneSpaceOutput.z;
    output.clipSpaceOutput.w = output.sceneSpaceOutput.w;

    // Generate standard Texel-space input coordinates.
    output.texelSpaceInput0.x = (outputScenePosition.x * sceneToInput0X[0]) + sceneToInput0X[1];
    output.texelSpaceInput0.y = (outputScenePosition.y * sceneToInput0Y[0]) + sceneToInput0Y[1];
    output.texelSpaceInput0.z = sceneToInput0X[0];
    output.texelSpaceInput0.w = sceneToInput0Y[0];

    return output;  
}

Le code ci-dessus peut être utilisé comme point de départ pour un nuanceur de vertex. Il passe simplement à travers l’image d’entrée sans effectuer de transformations. Là encore, consultez l’exemple sdk D2DCustomEffects pour une transformation entièrement implémentée basée sur un nuanceur de vertex.

Si aucune mémoire tampon de vertex n’est spécifiée par la transformation, Direct2D remplace dans une mémoire tampon de vertex par défaut représentant l’emplacement de l’image rectangulaire. Les paramètres du nuanceur de vertex sont remplacés par ceux de la sortie du nuanceur par défaut :

struct VSIn
{
    float4 clipSpaceOutput  : SV_POSITION; 
    float4 sceneSpaceOutput : SCENE_POSITION;
    float4 texelSpaceInput0 : TEXCOORD0;  
};

Le nuanceur de vertex ne peut pas modifier ses paramètres sceneSpaceOutput et clipSpaceOutput . Elle doit les retourner inchangées. Il peut toutefois modifier le ou les paramètres texelSpaceInput pour chaque image d’entrée. Si la transformation contient également un nuanceur de pixels personnalisé, le nuanceur de vertex peut toujours passer des paramètres personnalisés supplémentaires directement au nuanceur de pixels. En outre, la mémoire tampon personnalisée des matrices de conversion sceneSpace (b0) n’est plus fournie.

Ajout d’un nuanceur de calcul à une transformation personnalisée

Enfin, les transformations personnalisées peuvent utiliser des nuanceurs de calcul pour certains scénarios ciblés. Les nuanceurs de calcul peuvent être utilisés pour implémenter des effets d’image complexes qui nécessitent un accès arbitraire aux mémoires tampons d’images d’entrée et de sortie. Par exemple, un algorithme d’histogramme de base ne peut pas être implémenté avec un nuanceur de pixels en raison de limitations d’accès à la mémoire.

Étant donné que les nuanceurs de calcul ont des exigences de niveau de fonctionnalité matérielle plus élevées que les nuanceurs de pixels, les nuanceurs de pixels doivent être utilisés lorsque cela est possible pour implémenter un effet donné. Plus précisément, les nuanceurs de calcul s’exécutent uniquement sur la plupart des cartes de niveau DirectX 10 et supérieures. Si une transformation choisit d’utiliser un nuanceur de calcul, elle doit case activée pour la prise en charge matérielle appropriée pendant l’instanciation, en plus d’implémenter l’interface ID2D1ComputeTransform.

Vérification de la prise en charge du nuanceur de calcul

Si un effet utilise un nuanceur de calcul, il doit case activée pour la prise en charge du nuanceur de calcul lors de sa création à l’aide de la méthode ID2D1EffectContext::CheckFeatureSupport. Si le GPU ne prend pas en charge les nuanceurs de calcul, l’effet doit retourner D2DERR_INSUFFICIENT_DEVICE_CAPABILITIES.

Il existe deux types de nuanceurs de calcul différents qu’une transformation peut utiliser : nuanceur modèle 4 (DirectX 10) et nuanceur modèle 5 (DirectX 11). Certaines limitations sont applicables aux nuanceurs du modèle de nuanceur 4. Pour plus d’informations, consultez la documentation Direct3D . Les transformations peuvent contenir les deux types de nuanceurs, et peuvent revenir au modèle de nuanceur 4 si nécessaire : consultez l’exemple de kit SDK D2DCustomEffects pour une implémentation de ce.

Implémenter ID2D1ComputeTransform

Cette interface contient deux nouvelles méthodes à implémenter en plus de celles de ID2D1Transform :

SetComputeInfo(ID2D1ComputeInfo *pComputeInfo)

Comme avec les nuanceurs de pixels et de vertex, Direct2D appelle la méthode SetComputeInfo lorsque la transformation est ajoutée pour la première fois au graphique de transformation d’un effet. Cette méthode fournit un paramètre ID2D1ComputeInfo qui contrôle le rendu de la transformation. Cela inclut le choix du nuanceur de calcul à exécuter via la méthode ID2D1ComputeInfo::SetComputeShader . Si la transformation choisit de stocker ce paramètre en tant que variable membre de classe, il est accessible et modifié à partir de n’importe quelle méthode de transformation ou d’effet, à l’exception des méthodes MapOutputRects et MapInvalidRect . Consultez la rubrique ID2D1ComputeInfo pour connaître les autres méthodes disponibles ici.

CalculateThreadgroups(const D2D1_RECT_L *pOutputRect, UINT32 *pDimensionX, UINT32 *pDimensionY, UINT32 *pDimensionZ)

Alors que les nuanceurs de pixels sont exécutés par pixel et que les nuanceurs de vertex sont exécutés sur une base par sommet, les nuanceurs de calcul sont exécutés par « threadgroup ». Un groupe de threads représente un certain nombre de threads qui s’exécutent simultanément sur le GPU. Le code HLSL du nuanceur de calcul détermine le nombre de threads à exécuter par groupe de threads. L’effet met à l’échelle le nombre de threadgroups afin que le nuanceur exécute le nombre de fois souhaité, en fonction de la logique du nuanceur.

La méthode CalculateThreadgroups permet à la transformation d’informer Direct2D du nombre de groupes de threads requis, en fonction de la taille de l’image et de la propre connaissance du nuanceur par la transformation.

Le nombre de fois où le nuanceur de calcul est exécuté est un produit des nombres de threads spécifiés ici et de l’annotation « numthreads » dans le nuanceur de calcul HLSL. Par exemple, si la transformation définit les dimensions du groupe de threads sur (2,2,1) que le nuanceur spécifie (3,3,1) threads par threadgroup, 4 threadgroups sont exécutés, chacun avec 9 threads, pour un total de 36 instances de thread.

Un scénario courant consiste à traiter un pixel de sortie pour chaque instance du nuanceur de calcul. Pour calculer le nombre de groupes de threads pour ce scénario, la transformation divise la largeur et la hauteur de l’image par les dimensions x et y respectives de l’annotation « numthreads » dans le nuanceur HLSL du nuanceur de calcul.

Plus important encore, si cette division est effectuée, le nombre de groupes de threads demandés doit toujours être arrondi à l’entier le plus proche. Sinon, les pixels « restants » ne seront pas exécutés sur. Si un nuanceur (par exemple) calcule un seul pixel avec chaque thread, le code de la méthode apparaît comme suit.

IFACEMETHODIMP SampleTransform::CalculateThreadgroups(
    _In_ const D2D1_RECT_L* pOutputRect,
    _Out_ UINT32* pDimensionX,
    _Out_ UINT32* pDimensionY,
    _Out_ UINT32* pDimensionZ
    )
{    
    // The input image's dimensions are divided by the corresponding number of threads in each
    // threadgroup. This is specified in the HLSL, and in this example is 24 for both the x and y
    // dimensions. Dividing the image dimensions by these values calculates the number of
    // thread groups that need to be executed.

    *pDimensionX = static_cast<UINT32>(
         ceil((m_inputRect.right - m_inputRect.left) / 24.0f);

    *pDimensionY = static_cast<UINT32>(
         ceil((m_inputRect.bottom - m_inputRect.top) / 24.0f);

    // The z dimension is set to '1' in this example because the shader will
    // only be executed once for each pixel in the two-dimensional input image.
    // This value can be increased to perform additional executions for a given
    // input position.
    *pDimensionZ = 1;

    return S_OK;
}

HlSL utilise le code suivant pour spécifier le nombre de threads dans chaque groupe de threads :

// numthreads(x, y, z)
// This specifies the number of threads in each dispatched threadgroup. 
// For Shader Model 4, z == 1 and x*y*z <= 768. For Shader Model 5, z <= 64 and x*y*z <= 1024.
[numthreads(24, 24, 1)]
void main(
...

Pendant l’exécution, le groupe de threads actuel et l’index de thread actuel sont passés en tant que paramètres à la méthode du nuanceur :

#define NUMTHREADS_X 24
#define NUMTHREADS_Y 24

// numthreads(x, y, z)
// This specifies the number of threads in each dispatched threadgroup.
// For Shader Model 4, z == 1 and x*y*z <= 768. For Shader Model 5, z <= 64 and x*y*z <= 1024.
[numthreads(NUMTHREADS_X, NUMTHREADS_Y, 1)]
void main(
    // dispatchThreadId - Uniquely identifies a given execution of the shader, most commonly used parameter.
    // Definition: (groupId.x * NUM_THREADS_X + groupThreadId.x, groupId.y * NUMTHREADS_Y + groupThreadId.y,
    // groupId.z * NUMTHREADS_Z + groupThreadId.z)
    uint3 dispatchThreadId  : SV_DispatchThreadID,

    // groupThreadId - Identifies an individual thread within a thread group.
    // Range: (0 to NUMTHREADS_X - 1, 0 to NUMTHREADS_Y - 1, 0 to NUMTHREADS_Z - 1)
    uint3 groupThreadId     : SV_GroupThreadID,

    // groupId - Identifies which thread group the individual thread is being executed in.
    // Range defined in ID2D1ComputeTransform::CalculateThreadgroups.
    uint3 groupId           : SV_GroupID, 

    // One dimensional indentifier of a compute shader thread within a thread group.
    // Range: (0 to NUMTHREADS_X * NUMTHREADS_Y * NUMTHREADS_Z - 1)
    uint  groupIndex        : SV_GroupIndex
    )
{
...

Lecture des données d’image

Les nuanceurs de calcul accèdent à l’image d’entrée de la transformation sous la forme d’une texture à deux dimensions unique :

Texture2D<float4> InputTexture : register(t0);
SamplerState InputSampler : register(s0);

Toutefois, comme les nuanceurs de pixels, il n’est pas garanti que les données de l’image commencent à (0, 0) sur la texture. Au lieu de cela, Direct2D fournit des constantes système qui permettent aux nuanceurs de compenser tout décalage :

// These are default constants passed by D2D.
cbuffer systemConstants : register(b0)
{
    int4 resultRect; // Represents the input rectangle to the shader in terms of pixels.
    float2 sceneToInput0X;
    float2 sceneToInput0Y;
};

// The image does not necessarily begin at (0,0) on InputTexture. The shader needs
// to use the coefficients provided by Direct2D to map the requested image data to
// where it resides on the texture.
float2 ConvertInput0SceneToTexelSpace(float2 inputScenePosition)
{
    float2 ret;
    ret.x = inputScenePosition.x * sceneToInput0X[0] + sceneToInput0X[1];
    ret.y = inputScenePosition.y * sceneToInput0Y[0] + sceneToInput0Y[1];
    
    return ret;
}

Une fois la mémoire tampon constante et la méthode d’assistance ci-dessus définies, le nuanceur peut échantillonner des données d’image à l’aide des éléments suivants :

float4 color = InputTexture.SampleLevel(
        InputSampler, 
        ConvertInput0SceneToTexelSpace(
            float2(xIndex + .5, yIndex + .5) + // Add 0.5 to each coordinate to hit the center of the pixel.
            resultRect.xy // Offset sampling location by input image offset.
            ),
        0
        );

Écriture de données d’image

Direct2D s’attend à ce qu’un nuanceur définisse une mémoire tampon de sortie pour l’image résultante à placer. Dans le nuanceur modèle 4 (DirectX 10), il doit s’agir d’une mémoire tampon unidimensionnelle en raison de contraintes de fonctionnalités :

// Shader Model 4 does not support RWTexture2D, must use 1D buffer instead.
RWStructuredBuffer<float4> OutputTexture : register(t1);

La texture de sortie est indexée row-first pour permettre le stockage de l’image entière.

uint imageWidth = resultRect[2] - resultRect[0];
uint imageHeight = resultRect[3] - resultRect[1];
OutputTexture[yIndex * imageWidth + xIndex] = color;

En revanche, les nuanceurs du modèle de nuanceur 5 (DirectX 11) peuvent utiliser des textures de sortie à deux dimensions :

RWTexture2D<float4> OutputTexture : register(t1);

Avec les nuanceurs du modèle de nuanceur 5, Direct2D fournit un paramètre « outputOffset » supplémentaire dans la mémoire tampon constante. La sortie du nuanceur doit être décalée par ce montant :

OutputTexture[uint2(xIndex, yIndex) + outputOffset.xy] = color;

Un nuanceur de calcul pass-through shader model 5 terminé est illustré ci-dessous. Dans celui-ci, chacun des threads du nuanceur de calcul lit et écrit un seul pixel de l’image d’entrée.

#define NUMTHREADS_X 24
#define NUMTHREADS_Y 24

Texture2D<float4> InputTexture : register(t0);
SamplerState InputSampler : register(s0);

RWTexture2D<float4> OutputTexture : register(t1);

// These are default constants passed by D2D.
cbuffer systemConstants : register(b0)
{
    int4 resultRect; // Represents the region of the output image.
    int2 outputOffset;
    float2 sceneToInput0X;
    float2 sceneToInput0Y;
};

// The image does not necessarily begin at (0,0) on InputTexture. The shader needs
// to use the coefficients provided by Direct2D to map the requested image data to
// where it resides on the texture.
float2 ConvertInput0SceneToTexelSpace(float2 inputScenePosition)
{
    float2 ret;
    ret.x = inputScenePosition.x * sceneToInput0X[0] + sceneToInput0X[1];
    ret.y = inputScenePosition.y * sceneToInput0Y[0] + sceneToInput0Y[1];
    
    return ret;
}

// numthreads(x, y, z)
// This specifies the number of threads in each dispatched threadgroup.
// For Shader Model 5, z <= 64 and x*y*z <= 1024
[numthreads(NUMTHREADS_X, NUMTHREADS_Y, 1)]
void main(
    // dispatchThreadId - Uniquely identifies a given execution of the shader, most commonly used parameter.
    // Definition: (groupId.x * NUM_THREADS_X + groupThreadId.x, groupId.y * NUMTHREADS_Y + groupThreadId.y,
    // groupId.z * NUMTHREADS_Z + groupThreadId.z)
    uint3 dispatchThreadId  : SV_DispatchThreadID,

    // groupThreadId - Identifies an individual thread within a thread group.
    // Range: (0 to NUMTHREADS_X - 1, 0 to NUMTHREADS_Y - 1, 0 to NUMTHREADS_Z - 1)
    uint3 groupThreadId     : SV_GroupThreadID,

    // groupId - Identifies which thread group the individual thread is being executed in.
    // Range defined in DFTVerticalTransform::CalculateThreadgroups.
    uint3 groupId           : SV_GroupID, 

    // One dimensional indentifier of a compute shader thread within a thread group.
    // Range: (0 to NUMTHREADS_X * NUMTHREADS_Y * NUMTHREADS_Z - 1)
    uint  groupIndex        : SV_GroupIndex
    )
{
    uint xIndex = dispatchThreadId.x;
    uint yIndex = dispatchThreadId.y;

    uint imageWidth = resultRect.z - resultRect.x;
    uint imageHeight = resultRect.w - resultRect.y;

    // It is likely that the compute shader will execute beyond the bounds of the input image, since the shader is
    // executed in chunks sized by the threadgroup size defined in ID2D1ComputeTransform::CalculateThreadgroups.
    // For this reason each shader should ensure the current dispatchThreadId is within the bounds of the input
    // image before proceeding.
    if (xIndex >= imageWidth || yIndex >= imageHeight)
    {
        return;
    }

    float4 color = InputTexture.SampleLevel(
        InputSampler, 
        ConvertInput0SceneToTexelSpace(
            float2(xIndex + .5, yIndex + .5) + // Add 0.5 to each coordinate to hit the center of the pixel.
            resultRect.xy // Offset sampling location by image offset.
            ),
        0
        );

    OutputTexture[uint2(xIndex, yIndex) + outputOffset.xy] = color;

Le code ci-dessous montre la version de nuanceur model 4 équivalente du nuanceur. Notez que le nuanceur s’affiche désormais dans une mémoire tampon de sortie unidimensionnelle.

#define NUMTHREADS_X 24
#define NUMTHREADS_Y 24

Texture2D<float4> InputTexture : register(t0);
SamplerState InputSampler : register(s0);

// Shader Model 4 does not support RWTexture2D, must use one-dimensional buffer instead.
RWStructuredBuffer<float4> OutputTexture : register(t1);

// These are default constants passed by D2D. See PixelShader and VertexShader
// projects for how to pass custom values into a shader.
cbuffer systemConstants : register(b0)
{
    int4 resultRect; // Represents the region of the output image.
    float2 sceneToInput0X;
    float2 sceneToInput0Y;
};

// The image does not necessarily begin at (0,0) on InputTexture. The shader needs
// to use the coefficients provided by Direct2D to map the requested image data to
// where it resides on the texture.
float2 ConvertInput0SceneToTexelSpace(float2 inputScenePosition)
{
    float2 ret;
    ret.x = inputScenePosition.x * sceneToInput0X[0] + sceneToInput0X[1];
    ret.y = inputScenePosition.y * sceneToInput0Y[0] + sceneToInput0Y[1];
    
    return ret;
}

// numthreads(x, y, z)
// This specifies the number of threads in each dispatched threadgroup.
// For Shader Model 4, z == 1 and x*y*z <= 768
[numthreads(NUMTHREADS_X, NUMTHREADS_Y, 1)]
void main(
    // dispatchThreadId - Uniquely identifies a given execution of the shader, most commonly used parameter.
    // Definition: (groupId.x * NUM_THREADS_X + groupThreadId.x, groupId.y * NUMTHREADS_Y + groupThreadId.y, groupId.z * NUMTHREADS_Z + groupThreadId.z)
    uint3 dispatchThreadId  : SV_DispatchThreadID,

    // groupThreadId - Identifies an individual thread within a thread group.
    // Range: (0 to NUMTHREADS_X - 1, 0 to NUMTHREADS_Y - 1, 0 to NUMTHREADS_Z - 1)
    uint3 groupThreadId     : SV_GroupThreadID,

    // groupId - Identifies which thread group the individual thread is being executed in.
    // Range defined in DFTVerticalTransform::CalculateThreadgroups
    uint3 groupId           : SV_GroupID, 

    // One dimensional indentifier of a compute shader thread within a thread group.
    // Range: (0 to NUMTHREADS_X * NUMTHREADS_Y * NUMTHREADS_Z - 1)
    uint  groupIndex        : SV_GroupIndex
    )
{
    uint imageWidth = resultRect[2] - resultRect[0];
    uint imageHeight = resultRect[3] - resultRect[1];

    uint xIndex = dispatchThreadId.x;
    uint yIndex = dispatchThreadId.y;

    // It is likely that the compute shader will execute beyond the bounds of the input image, since the shader is executed in chunks sized by
    // the threadgroup size defined in ID2D1ComputeTransform::CalculateThreadgroups. For this reason each shader should ensure the current
    // dispatchThreadId is within the bounds of the input image before proceeding.
    if (xIndex >= imageWidth || yIndex >= imageHeight)
    {
        return;
    }

    float4 color = InputTexture.SampleLevel(
        InputSampler, 
        ConvertInput0SceneToTexelSpace(
            float2(xIndex + .5, yIndex + .5) + // Add 0.5 to each coordinate to hit the center of the pixel.
            resultRect.xy // Offset sampling location by image offset.
            ),
        0
        );

    OutputTexture[yIndex * imageWidth + xIndex] = color;
}

Exemple de SDK D2DCustomEffects