Share via


Effetti personalizzati

Direct2D viene fornito con una libreria di effetti che eseguono un'ampia gamma di operazioni comuni sulle immagini. Per l'elenco completo degli effetti, vedere l'argomento effetti predefiniti . Per le funzionalità che non possono essere ottenute con gli effetti predefiniti, Direct2D consente di scrivere effetti personalizzati usando HLSL standard. È possibile usare questi effetti personalizzati insieme agli effetti predefiniti forniti con Direct2D.

Per visualizzare esempi di effetto pixel, vertice e shader di calcolo completi, vedere l'esempio D2DCustomEffects SDK.

In questo argomento vengono illustrati i passaggi e i concetti necessari per progettare e creare un effetto personalizzato completo.

Introduzione: Che cos'è all'interno di un effetto?

diagramma dell'effetto ombreggiatura.

Concettualmente, un effetto Direct2D esegue un'attività di creazione dell'immagine, ad esempio la modifica della luminosità, la saturazione di un'immagine o, come illustrato in precedenza, la creazione di un'ombreggiatura. Per l'app, sono semplici. Possono accettare zero o più immagini di input, esporre più proprietà che controllano l'operazione e generare una singola immagine di output.

Esistono quattro parti diverse di un effetto personalizzato che un autore dell'effetto è responsabile di:

  1. Interfaccia effetto: l'interfaccia dell'effetto definisce concettualmente il modo in cui un'app interagisce con un effetto personalizzato , ad esempio il numero di input accettati dall'effetto e le proprietà disponibili. L'interfaccia dell'effetto gestisce un grafico di trasformazione, che contiene le operazioni di creazione dell'immagine effettive.
  2. Trasforma grafico: ogni effetto crea un grafico di trasformazione interno costituito da singole trasformazioni. Ogni trasformazione rappresenta una singola operazione di immagine. L'effetto è responsabile del collegamento di queste trasformazioni in un grafico per eseguire l'effetto di creazione dell'immagine desiderato. Un effetto può aggiungere, rimuovere, modificare e riordinare le trasformazioni in risposta alle modifiche alle proprietà esterne dell'effetto.
  3. Transform: una trasformazione rappresenta una singola operazione di immagine. Lo scopo principale è quello di ospitare gli shader eseguiti per ogni pixel di output. A tale scopo, è responsabile del calcolo delle nuove dimensioni dell'immagine di output in base alla logica negli shader. Deve inoltre calcolare l'area dell'immagine di input da cui gli shader devono leggere per eseguire il rendering dell'area di output richiesta.
  4. Shader: uno shader viene eseguito sull'input della trasformazione nella GPU o sulla CPU se il rendering software viene specificato quando l'app crea il dispositivo Direct3D. Gli shader degli effetti vengono scritti in High Level Shading Language (HLSL) e vengono compilati in codice byte durante la compilazione dell'effetto, che viene quindi caricato dall'effetto durante la fase di esecuzione. Questo documento di riferimento descrive come scrivere HLSL conforme a Direct2D. La documentazione di Direct3D contiene una panoramica HLSL di base.

Creazione di un'interfaccia di effetto

L'interfaccia dell'effetto definisce il modo in cui un'app interagisce con l'effetto personalizzato. Per creare un'interfaccia di effetto, una classe deve implementare ID2D1EffectImpl, definire i metadati che descrivono l'effetto (ad esempio il nome, il numero di input e le proprietà) e creare metodi che registrano l'effetto personalizzato da usare con Direct2D.

Dopo l'implementazione di tutti i componenti per un'interfaccia di effetto, l'intestazione della classe sarà simile alla seguente:

#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.
};

Implementare ID2D1EffectImpl

L'interfaccia ID2D1EffectImpl contiene tre metodi che è necessario implementare:

Initialize(ID2D1EffectContext *pContextInternal, ID2D1TransformGraph *pTransformGraph)

Direct2D chiama il metodo Initialize dopo che il metodo ID2D1DeviceContext::CreateEffect è stato chiamato dall'app. È possibile utilizzare questo metodo per eseguire l'inizializzazione interna o qualsiasi altra operazione necessaria per l'effetto. È anche possibile usarlo per creare il grafico di trasformazione iniziale dell'effetto.

SetGraph(ID2D1TransformGraph *pTransformGraph)

Direct2D chiama il metodo SetGraph quando viene modificato il numero di input all'effetto. Anche se la maggior parte degli effetti ha un numero costante di input, altri come l'effetto Composito supportano un numero variabile di input. Questo metodo consente a questi effetti di aggiornare il grafico di trasformazione in risposta a un numero di input modificato. Se un effetto non supporta un conteggio di input di variabili, questo metodo può semplicemente restituire E_NOTIMPL.

PrepareForRender (D2D1_CHANGE_TYPE changeType)

Il metodo PrepareForRender offre un'opportunità per gli effetti di eseguire qualsiasi operazione in risposta a modifiche esterne. Direct2D chiama questo metodo subito prima di eseguire il rendering di un effetto se almeno uno di questi è vero:

  • L'effetto è stato inizializzato in precedenza ma non ancora disegnato.
  • Una proprietà dell'effetto è stata modificata dall'ultima chiamata di disegno.
  • Lo stato del contesto Direct2D chiamante (come DPI) è cambiato dall'ultima chiamata di disegno.

Implementare i metodi di registrazione e callback dell'effetto

Le app devono registrare gli effetti con Direct2D prima di crearne un'istanza. Questa registrazione ha come ambito un'istanza di una factory Direct2D e deve essere ripetuta ogni volta che viene eseguita l'app. Per abilitare questa registrazione, un effetto personalizzato definisce un GUID univoco, un metodo pubblico che registra l'effetto e un metodo di callback privato che restituisce un'istanza dell'effetto.

Definire un GUID

È necessario definire un GUID che identifichi in modo univoco l'effetto per la registrazione con Direct2D. L'app usa lo stesso per identificare l'effetto quando chiama ID2D1DeviceContext::CreateEffect.

Questo codice illustra la definizione di tale GUID per un effetto. È necessario creare un GUID univoco usando uno strumento di generazione GUID, ad esempio 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);

Definire un metodo di registrazione pubblico

Definire quindi un metodo pubblico per l'app da chiamare per registrare l'effetto con Direct2D. Poiché la registrazione dell'effetto è specifica per un'istanza di una factory Direct2D, il metodo accetta un'interfaccia ID2D1Factory1 come parametro. Per registrare l'effetto, il metodo chiama quindi l'API ID2D1Factory1::RegisterEffectFromString nel parametro ID2D1Factory1 .

Questa API accetta una stringa XML che descrive i metadati, gli input e le proprietà dell'effetto. I metadati per un effetto sono solo a scopo informativo e possono essere sottoposti a query dall'app tramite l'interfaccia ID2D1Properties . I dati di input e proprietà, d'altra parte, vengono usati da Direct2D e rappresentano la funzionalità dell'effetto.

Di seguito è illustrata una stringa XML per un effetto di esempio minimo. L'aggiunta di proprietà personalizzate al codice XML è descritta nella sezione Aggiunta di proprietà personalizzate a un effetto.

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

Definire un metodo di callback della factory degli effetti

L'effetto deve inoltre fornire un metodo di callback privato che restituisce un'istanza dell'effetto tramite un singolo parametro IUnknown**. Un puntatore a questo metodo viene fornito a Direct2D quando l'effetto viene registrato tramite l'API ID2D1Factory1::RegisterEffectFromString tramite il parametro 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;
}

Implementare l'interfaccia IUnknown

Infine, l'effetto deve implementare l'interfaccia IUnknown per la compatibilità con COM.

Creazione del grafico di trasformazione dell'effetto

Un effetto può usare diverse trasformazioni (singole operazioni di immagine) per creare l'effetto di creazione dell'immagine desiderato. Per controllare l'ordine in cui queste trasformazioni vengono applicate all'immagine di input, l'effetto li dispone in un grafico di trasformazione. Un grafico di trasformazione può usare gli effetti e le trasformazioni inclusi in Direct2D , nonché trasformazioni personalizzate create dall'autore dell'effetto.

Uso delle trasformazioni incluse in Direct2D

Si tratta delle trasformazioni usate più comunemente fornite con Direct2D.

Creazione di un grafico di trasformazione a nodo singolo

Dopo aver creato una trasformazione, l'input dell'effetto deve essere connesso all'input della trasformazione e l'output della trasformazione deve essere connesso all'output dell'effetto. Quando un effetto contiene solo una singola trasformazione, è possibile usare il metodo ID2D1TransformGraph::SetSingleTransformNode per eseguire facilmente questa operazione.

È possibile creare o modificare una trasformazione nei metodi Initialize o SetGraph dell'effetto usando il parametro ID2D1TransformGraph specificato. Se un effetto deve apportare modifiche al grafico di trasformazione in un altro metodo in cui questo parametro non è disponibile, l'effetto può salvare il parametro ID2D1TransformGraph come variabile membro della classe e accedervi altrove, ad esempio PrepareForRender o un metodo di callback di proprietà personalizzata.

Di seguito è riportato un esempio di metodo Initialize . Questo metodo crea un grafico di trasformazione a nodo singolo che sfalsa l'immagine di un centinaio di pixel in ogni asse.

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

Creazione di un grafo di trasformazione multinodo

L'aggiunta di più trasformazioni al grafico di trasformazione di un effetto consente agli effetti di eseguire internamente più operazioni di immagine presentate a un'app come un unico effetto unificato.

Come indicato in precedenza, il grafico di trasformazione dell'effetto può essere modificato in qualsiasi metodo di effetto usando il parametro ID2D1TransformGraph ricevuto nel metodo Initialize dell'effetto. Le API seguenti su tale interfaccia possono essere usate per creare o modificare il grafico di trasformazione di un effetto:

AddNode(ID2D1TransformNode *pNode)

Il metodo AddNode , in effetti, "registra" la trasformazione con l'effetto e deve essere chiamato prima che la trasformazione possa essere usata con qualsiasi altro metodo del grafo di trasformazione.

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

Il metodo ConnectToEffectInput connette l'input dell'immagine dell'effetto all'input di una trasformazione. Lo stesso input dell'effetto può essere connesso a più trasformazioni.

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

Il metodo ConnectNode connette l'output di una trasformazione all'input di un'altra trasformazione. Un output di trasformazione può essere connesso a più trasformazioni.

SetOutputNode(ID2D1TransformNode *pNode)

Il metodo SetOutputNode connette l'output di una trasformazione all'output dell'effetto. Poiché un effetto ha un solo output, è possibile designare solo una singola trasformazione come "nodo di output".

Questo codice usa due trasformazioni separate per creare un effetto unificato. In questo caso, l'effetto è un'ombreggiatura di rilascio tradotta.

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

Aggiunta di proprietà personalizzate a un effetto

Gli effetti possono definire proprietà personalizzate che consentono a un'app di modificare il comportamento dell'effetto durante il runtime. Esistono tre passaggi per definire una proprietà per un effetto personalizzato:

Aggiungere i metadati della proprietà ai dati di registrazione dell'effetto

Aggiungere la proprietà al codice XML di registrazione

È necessario definire le proprietà di un effetto personalizzato durante la registrazione iniziale dell'effetto con Direct2D. Prima di tutto, è necessario aggiornare il codice XML di registrazione dell'effetto nel metodo di registrazione pubblico con la nuova proprietà:

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

Quando si definisce una proprietà dell'effetto in XML, è necessario un nome, un tipo e un nome visualizzato. Il nome visualizzato di una proprietà, nonché i valori di categoria, autore e descrizione dell'effetto complessivo possono e devono essere localizzati.

Per ogni proprietà, un effetto può facoltativamente specificare valori predefiniti, min e max. Questi valori sono solo per uso informativo. Non vengono applicati da Direct2D. È possibile implementare manualmente qualsiasi logica predefinita/min/max specificata nella classe dell'effetto.

Il valore del tipo elencato nel codice XML per la proprietà deve corrispondere al tipo di dati corrispondente utilizzato dai metodi getter e setter della proprietà. I valori XML corrispondenti per ogni tipo di dati sono visualizzati in questa tabella:

Tipo di dati Valore XML corrispondente
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[] blob
IUnknown* Iunknown
ID2D1ColorContext* Colorcontext
CLSID clsid
Enumerazione (D2D1_INTERPOLATION_MODE e così via) enum

 

Eseguire il mapping della nuova proprietà ai metodi getter e setter

Successivamente, l'effetto deve eseguire il mapping di questa nuova proprietà ai metodi getter e setter. Questa operazione viene eseguita tramite la matrice di D2D1_PROPERTY_BINDING passata al metodo ID2D1Factory1::RegisterEffectFromString .

La matrice di D2D1_PROPERTY_BINDING è simile alla seguente:

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

Dopo aver creato la matrice XML e binding, passarle al metodo 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 richiede che la classe dell'effetto erediti da ID2D1EffectImpl prima di qualsiasi altra interfaccia.

Le proprietà personalizzate per un effetto vengono indicizzate nell'ordine in cui vengono dichiarate nel codice XML e, una volta create, è possibile accedervi tramite i metodi ID2D1Properties::SetValue e ID2D1Properties::GetValue . Per praticità, è possibile creare un'enumerazione pubblica che elenca ogni proprietà nel file di intestazione dell'effetto:

typedef enum SAMPLEEFFECT_PROP
{
    SAMPLEFFECT_PROP_OFFSET = 0
};

Creare i metodi getter e setter per la proprietà

Il passaggio successivo consiste nel creare i metodi getter e setter per la nuova proprietà. I nomi dei metodi devono corrispondere a quelli specificati nella matrice D2D1_PROPERTY_BINDING . Inoltre, il tipo di proprietà specificato nel codice XML dell'effetto deve corrispondere al tipo del parametro del metodo setter e al valore restituito del metodo 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;
}

Trasformazioni dell'effetto di aggiornamento in risposta alla modifica della proprietà

Per aggiornare effettivamente l'output dell'immagine di un effetto in risposta a una modifica di proprietà, l'effetto deve modificare le trasformazioni sottostanti. Questa operazione viene in genere eseguita nel metodo PrepareForRender dell'effetto che Direct2D chiama automaticamente quando una delle proprietà di un effetto è stata modificata. Tuttavia, le trasformazioni possono essere aggiornate in uno dei metodi dell'effetto, ad esempio Initialize o i metodi setter della proprietà dell'effetto.

Ad esempio, se un effetto conteneva un OGGETTO ID2D1OffsetTransform e voleva modificare il valore di offset in risposta alla proprietà Offset dell'effetto da modificare, aggiungerebbe il codice seguente in 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;
}

Creazione di una trasformazione personalizzata

Per implementare operazioni di immagine oltre a quanto fornito in Direct2D, è necessario implementare trasformazioni personalizzate. Le trasformazioni personalizzate possono modificare arbitrariamente un'immagine di input tramite l'uso di shader HLSL personalizzati.

Le trasformazioni implementano una di due interfacce diverse a seconda dei tipi di shader usati. Le trasformazioni che usano pixel e/o vertex shader devono implementare ID2D1DrawTransform, mentre le trasformazioni che usano gli shader di calcolo devono implementare ID2D1ComputeTransform. Queste interfacce ereditano entrambe da ID2D1Transform. Questa sezione è incentrata sull'implementazione delle funzionalità comuni a entrambe.

L'interfaccia ID2D1Transform include quattro metodi da implementare:

GetInputCount

Questo metodo restituisce un numero intero che rappresenta il numero di input per la trasformazione.

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

MapInputRectsToOutputRect

Direct2D chiama il metodo MapInputRectsToOutputRect ogni volta che viene eseguito il rendering della trasformazione. Direct2D passa un rettangolo che rappresenta i limiti di ogni input alla trasformazione. La trasformazione è quindi responsabile del calcolo dei limiti dell'immagine di output. Le dimensioni dei rettangoli per tutti i metodi in questa interfaccia (ID2D1Transform) sono definite in pixel, non DIP.

Questo metodo è anche responsabile del calcolo dell'area dell'output opaca in base alla logica del relativo shader e alle aree opache di ogni input. Un'area opaca di un'immagine è definita come quella in cui il canale alfa è '1' per l'intero rettangolo. Se non è chiaro se l'output di una trasformazione è opaco, il rettangolo opaco di output deve essere impostato su (0, 0, 0, 0) come valore sicuro. Direct2D usa queste informazioni per eseguire ottimizzazioni del rendering con contenuto opaco garantito. Se questo valore non è accurato, può comportare un rendering non corretto.

È possibile modificare il comportamento di rendering della trasformazione (come definito nelle sezioni da 6 a 8) durante questo metodo. Tuttavia, non è possibile modificare altre trasformazioni nel grafico di trasformazione o il layout del grafico stesso qui.

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

Per un esempio più complesso, considerare la rappresentazione di una semplice operazione di sfocatura:

Se un'operazione di sfocatura usa un raggio di 5 pixel, le dimensioni del rettangolo di output devono essere espanse di 5 pixel, come illustrato di seguito. Quando si modificano le coordinate del rettangolo, una trasformazione deve garantire che la logica non causi over/underflow nelle coordinate del rettangolo.

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

Poiché l'immagine è sfocata, un'area dell'immagine che era opaca potrebbe ora essere parzialmente trasparente. Ciò è dovuto al fatto che per impostazione predefinita l'area esterna all'immagine è trasparente nero e questa trasparenza verrà combinata nell'immagine intorno ai bordi. La trasformazione deve riflettere questo valore nei calcoli del rettangolo opaco di output:

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

Questi calcoli vengono visualizzati qui:

Illustrazione del calcolo rettangolo.

Per altre info su questo metodo, vedi la pagina di riferimento MapInputRectsToOutputRect .

MapOutputRectToInputRects

Direct2D chiama il metodo MapOutputRectToInputRects dopo MapInputRectsToOutputRect. La trasformazione deve calcolare la parte dell'immagine da cui deve essere letta per eseguire correttamente il rendering dell'area di output richiesta.

Come in precedenza, se un effetto mappa rigorosamente pixel 1-1, può passare il rettangolo di output attraverso al rettangolo di input:

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

Analogamente, se una trasformazione riduce o espande un'immagine (come l'esempio di sfocatura qui), i pixel spesso usano i pixel circostanti per calcolarne il valore. Con una sfocatura, un pixel viene mediato con i pixel circostanti, anche se si trovano al di fuori dei limiti dell'immagine di input. Questo comportamento si riflette nel calcolo. Come in precedenza, la trasformazione verifica la presenza di overflow durante l'espansione delle coordinate di un rettangolo.

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

Questa figura visualizza il calcolo. Direct2D campiona automaticamente pixel neri trasparenti in cui l'immagine di input non esiste, consentendo di fondere gradualmente la sfocatura con il contenuto esistente sullo schermo.

illustrazione di un effetto campionamento di pixel neri trasparenti all'esterno di un rettangolo.

Se il mapping non è semplice, questo metodo deve impostare il rettangolo di input sull'area massima per garantire risultati corretti. A tale scopo, impostare i bordi sinistro e superiore su INT_MIN e i bordi destro e inferiore su INT_MAX.

Per altre info su questo metodo, vedi l'argomento MapOutputRectToInputRects .

MapInvalidRect

Direct2D chiama anche il metodo MapInvalidRect . Tuttavia, a differenza dei metodi MapInputRectsToOutputRect e MapOutputRectRectToInputRects Direct2D, non è garantito chiamarlo in un determinato momento. Questo metodo decide concettualmente quale parte dell'output di una trasformazione deve essere sottoposta di nuovo a rendering in risposta a parte o a tutte le modifiche di input. Esistono tre diversi scenari per cui calcolare la correzione non valida di una trasformazione.

Trasformazioni con mapping di pixel uno-a-uno

Per le trasformazioni che eseguono il mapping dei pixel 1-1, è sufficiente passare il rettangolo di input non valido al rettangolo di output non valido:

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

Trasformazioni con mapping pixel molti-a-molti

Quando i pixel di output di una trasformazione dipendono dall'area circostante, il rettangolo di input non valido deve essere espanso in modo corrispondente. Ciò significa che anche i pixel che circondano il rettangolo di input non valido saranno interessati e diventeranno non validi. Ad esempio, una sfocatura di cinque pixel usa il calcolo seguente:

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

Trasformazioni con mapping pixel complesso

Per le trasformazioni in cui i pixel di input e output non hanno un mapping semplice, l'intero output può essere contrassegnato come non valido. Ad esempio, se una trasformazione restituisce semplicemente il colore medio dell'input, l'intero output della trasformazione cambia se viene modificata anche una piccola parte dell'input. In questo caso, il rettangolo di output non valido deve essere impostato su un rettangolo logicamente infinito (illustrato di seguito). Direct2D blocca automaticamente questa impostazione ai limiti dell'output.

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

Per altre info su questo metodo, vedi l'argomento MapInvalidRect .

Dopo aver implementato questi metodi, l'intestazione della trasformazione conterrà quanto segue:

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

Aggiunta di un pixel shader a una trasformazione personalizzata

Dopo aver creato una trasformazione, deve fornire uno shader che modificherà i pixel dell'immagine. Questa sezione illustra i passaggi per l'uso di un pixel shader con una trasformazione personalizzata.

Implementazione di ID2D1DrawTransform

Per usare un pixel shader, la trasformazione deve implementare l'interfaccia ID2D1DrawTransform , che eredita dall'interfaccia ID2D1Transform descritta nella sezione 5. Questa interfaccia contiene un nuovo metodo da implementare:

SetDrawInfo(ID2D1DrawInfo *pDrawInfo)

Direct2D chiama il metodo SetDrawInfo quando la trasformazione viene aggiunta per la prima volta al grafico di trasformazione di un effetto. Questo metodo fornisce un parametro ID2D1DrawInfo che controlla la modalità di rendering della trasformazione. Vedere l'argomento ID2D1DrawInfo per i metodi disponibili qui.

Se la trasformazione sceglie di archiviare questo parametro come variabile membro della classe, è possibile accedere all'oggetto drawInfo e modificarlo da altri metodi, ad esempio setter di proprietà o MapInputRectsToOutputRect. In particolare, non può essere chiamato dai metodi MapOutputRectToInputRects o MapInvalidRect in ID2D1Transform.

Creazione di un GUID per il pixel shader

Successivamente, la trasformazione deve definire un GUID univoco per il pixel shader stesso. Viene usato quando Direct2D carica lo shader in memoria, nonché quando la trasformazione sceglie quale pixel shader usare per l'esecuzione. È possibile usare strumenti come guidgen.exe, inclusi in Visual Studio, per generare un GUID casuale.

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

Caricamento del pixel shader con Direct2D

Per poter essere usato dalla trasformazione, è necessario caricare un pixel shader in memoria.

Per caricare il pixel shader in memoria, la trasformazione deve leggere il codice di byte dello shader compilato da . File CSO generato da Visual Studio (vedere la documentazione di Direct3D per informazioni dettagliate) in una matrice di byte. Questa tecnica è illustrata in dettaglio nell'esempio D2DCustomEffects SDK.

Dopo aver caricato i dati dello shader in una matrice di byte, chiamare il metodo LoadPixelShader sull'oggetto ID2D1EffectContext dell'effetto. Direct2D ignora le chiamate a LoadPixelShader quando uno shader con lo stesso GUID è già stato caricato.

Dopo che un pixel shader è stato caricato in memoria, la trasformazione deve selezionarla per l'esecuzione passando il GUID al metodo SetPixelShader sul parametro ID2D1DrawInfo fornito durante il metodo SetDrawInfo . Il pixel shader deve essere già caricato in memoria prima di essere selezionato per l'esecuzione.

Modifica dell'operazione shader con buffer costanti

Per modificare la modalità di esecuzione di uno shader, una trasformazione può passare un buffer costante al pixel shader. A tale scopo, una trasformazione definisce uno struct che contiene le variabili desiderate nell'intestazione della classe:

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

La trasformazione chiama quindi il metodo ID2D1DrawInfo::SetPixelShaderConstantBuffer sul parametro ID2D1DrawInfo fornito nel metodo SetDrawInfo per passare questo buffer allo shader.

HLSL deve anche definire uno struct corrispondente che rappresenta il buffer costante. Le variabili contenute nello struct dello shader devono corrispondere a quelle nello struct della trasformazione.

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

Dopo aver definito il buffer, i valori contenuti in possono essere letti da qualsiasi posizione all'interno del pixel shader.

Scrittura di un pixel shader per Direct2D

Le trasformazioni Direct2D usano shader creati usando HLSL standard. Esistono tuttavia alcuni concetti chiave per scrivere un pixel shader eseguito dal contesto di una trasformazione. Per un esempio completo di pixel shader completamente funzionante, vedere l'esempio D2DCustomEffects SDK.

Direct2D esegue automaticamente il mapping degli input di una trasformazione agli oggetti Texture2D e SamplerState in HLSL. Il primo Texture2D si trova in register t0 e il primo SamplerState si trova in register s0. Ogni input aggiuntivo si trova nei registri corrispondenti successivi (ad esempio t1 e s1). I dati pixel per un determinato input possono essere campionati chiamando Sample sull'oggetto Texture2D e passando le coordinate SamplerState corrispondenti e i texel.

Un pixel shader personalizzato viene eseguito una volta per ogni pixel di cui viene eseguito il rendering. Ogni volta che viene eseguito lo shader, Direct2D fornisce automaticamente tre parametri che identificano la posizione di esecuzione corrente:

  • Output dello spazio della scena: questo parametro rappresenta la posizione di esecuzione corrente in termini della superficie di destinazione complessiva. Viene definito in pixel e i relativi valori min/max corrispondono ai limiti del rettangolo restituito da MapInputRectsToOutputRect.
  • Output dello spazio clip: questo parametro viene usato da Direct3D e non deve essere usato nel pixel shader di una trasformazione.
  • Input dello spazio texel: questo parametro rappresenta la posizione di esecuzione corrente in una particolare trama di input. Uno shader non deve accettare alcuna dipendenza dalla modalità di calcolo di questo valore. Deve usarlo solo per campionare l'input del pixel shader, come illustrato nel codice seguente:
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;
}

Aggiunta di un vertex shader a una trasformazione personalizzata

È possibile usare vertex shader per eseguire scenari di creazione di immagini diversi rispetto ai pixel shader. In particolare, i vertex shader possono eseguire effetti immagine basati sulla geometria trasformando i vertici che costituiscono un'immagine. I vertex shader possono essere usati indipendentemente o in combinazione con gli shader pixel specificati per la trasformazione. Se non si specifica un vertex shader, Direct2D sostituisce in un vertex shader predefinito per l'uso con il pixel shader personalizzato.

Il processo di aggiunta di un vertex shader a una trasformazione personalizzata è simile a quello di un pixel shader. La trasformazione implementa l'interfaccia ID2D1DrawTransform , crea un GUID e (facoltativamente) passa buffer costanti allo shader. Esistono tuttavia alcuni passaggi aggiuntivi chiave univoci per i vertex shader:

Creazione di un buffer dei vertici

Un vertex shader per definizione viene eseguito sui vertici passati, non sui singoli pixel. Per specificare i vertici per l'esecuzione dello shader, una trasformazione crea un buffer dei vertici da passare allo shader. Il layout dei vertex buffer non rientra nell'ambito di questo documento. Per informazioni dettagliate, vedere le informazioni di riferimento su Direct3D o l'esempio D2DCustomEffects SDK per un'implementazione di esempio.

Dopo aver creato un vertex buffer in memoria, la trasformazione usa il metodo CreateVertexBuffer nell'oggetto ID2D1EffectContext dell'effetto contenitore per passare tali dati alla GPU. Anche in questo caso, vedere l'esempio D2DCustomEffects SDK per un'implementazione di esempio.

Se non viene specificato alcun vertex buffer dalla trasformazione, Direct2D passa un buffer di vertici predefinito che rappresenta la posizione dell'immagine rettangolare.

Modifica di SetDrawInfo per l'utilizzo di un vertex shader

Analogamente ai pixel shader, la trasformazione deve caricare e selezionare un vertex shader per l'esecuzione. Per caricare il vertex shader, chiama il metodo LoadVertexShader nel metodo ID2D1EffectContext ricevuto nel metodo Initialize dell'effetto. Per selezionare il vertex shader per l'esecuzione, chiama SetVertexProcessing sul parametro ID2D1DrawInfo ricevuto nel metodo SetDrawInfo della trasformazione. Questo metodo accetta un GUID per un vertex shader caricato in precedenza e (facoltativamente) un buffer dei vertici creato in precedenza per l'esecuzione dello shader.

Implementazione di un vertex shader Direct2D

Una trasformazione di disegno può contenere sia un pixel shader che un vertex shader. Se una trasformazione definisce sia un pixel shader che un vertex shader, l'output del vertex shader viene assegnato direttamente al pixel shader: l'app può personalizzare la firma restituita del vertex shader/i parametri del pixel shader purché siano coerenti.

D'altra parte, se una trasformazione contiene solo un vertex shader e si basa sul pixel shader predefinito di Direct2D, deve restituire l'output predefinito seguente:

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

Un vertex shader archivia il risultato delle trasformazioni dei vertici nella variabile di output dello spazio della scena dello shader. Per calcolare l'output clip-space e le variabili di input dello spazio texel, Direct2D fornisce automaticamente matrici di conversione in un buffer costante:

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

Di seguito è riportato il codice del vertex shader di esempio che usa le matrici di conversione per calcolare gli spazi di ritaglio e texel corretti previsti da 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;  
}

Il codice precedente può essere usato come punto di partenza per un vertex shader. Passa semplicemente attraverso l'immagine di input senza eseguire alcuna trasformazione. Anche in questo caso, vedere l'esempio D2DCustomEffects SDK per una trasformazione basata su vertex shader completamente implementata.

Se non viene specificato alcun vertex buffer dalla trasformazione, Direct2D sostituisce in un buffer di vertici predefinito che rappresenta la posizione dell'immagine rettangolare. I parametri del vertex shader vengono modificati in quelli dell'output dello shader predefinito:

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

Il vertex shader potrebbe non modificare i parametri sceneSpaceOutput e clipSpaceOutput . Deve restituirli invariati. Può tuttavia modificare i parametri texelSpaceInput per ogni immagine di input. Se la trasformazione contiene anche un pixel shader personalizzato, il vertex shader è comunque in grado di passare parametri personalizzati aggiuntivi direttamente al pixel shader. Inoltre, il buffer personalizzato (b0) delle matrici di conversione sceneSpace non viene più fornito.

Aggiunta di uno shader di calcolo a una trasformazione personalizzata

Infine, le trasformazioni personalizzate possono usare gli shader di calcolo per determinati scenari di destinazione. Gli shader di calcolo possono essere usati per implementare effetti di immagine complessi che richiedono l'accesso arbitrario ai buffer di input e di output delle immagini. Ad esempio, un algoritmo istogramma di base non può essere implementato con un pixel shader a causa di limitazioni sull'accesso alla memoria.

Poiché gli shader di calcolo hanno requisiti di livello di funzionalità hardware più elevati rispetto ai pixel shader, gli shader pixel devono essere usati quando possibile per implementare un determinato effetto. In particolare, gli shader di calcolo vengono eseguiti solo nella maggior parte delle schede di livello DirectX 10 e versioni successive. Se una trasformazione sceglie di usare uno shader di calcolo, deve verificare il supporto hardware appropriato durante la creazione di istanze oltre all'implementazione dell'interfaccia ID2D1ComputeTransform .

Verifica del supporto di Compute Shader

Se un effetto usa uno shader di calcolo, deve verificare la presenza del supporto dello shader di calcolo durante la creazione usando il metodo ID2D1EffectContext::CheckFeatureSupport . Se la GPU non supporta gli shader di calcolo, l'effetto deve restituire D2DERR_INSUFFICIENT_DEVICE_CAPABILITIES.

Esistono due diversi tipi di compute shader che possono essere usati da una trasformazione: Shader Model 4 (DirectX 10) e Shader Model 5 (DirectX 11). Esistono alcune limitazioni per gli shader del modello shader 4. Per informazioni dettagliate, vedere la documentazione di Direct3D . Le trasformazioni possono contenere entrambi i tipi di shader e possono eseguire il fallback al modello shader 4 quando necessario: vedere l'esempio SDK D2DCustomEffects per un'implementazione di questo tipo.

Implementare ID2D1ComputeTransform

Questa interfaccia contiene due nuovi metodi da implementare oltre a quelli in ID2D1Transform:

SetComputeInfo(ID2D1ComputeInfo *pComputeInfo)

Analogamente ai pixel e ai vertex shader, Direct2D chiama il metodo SetComputeInfo quando la trasformazione viene aggiunta per la prima volta al grafico di trasformazione di un effetto. Questo metodo fornisce un parametro ID2D1ComputeInfo che controlla la modalità di rendering della trasformazione. Ciò include la scelta dello shader di calcolo da eseguire tramite il metodo ID2D1ComputeInfo::SetComputeShader . Se la trasformazione sceglie di archiviare questo parametro come variabile membro della classe, è possibile accedervi e modificarlo da qualsiasi metodo di trasformazione o effetto, ad eccezione dei metodi MapOutputRectToInputRects e MapInvalidRect . Vedere l'argomento ID2D1ComputeInfo per altri metodi disponibili qui.

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

Mentre i pixel shader vengono eseguiti su base per pixel e i vertex shader vengono eseguiti in base al vertice, gli shader di calcolo vengono eseguiti in base al threadgroup. Un threadgroup rappresenta un numero di thread eseguiti simultaneamente nella GPU. Il codice HLSL del compute shader determina il numero di thread da eseguire per ogni threadgroup. L'effetto ridimensiona il numero di threadgroup in modo che lo shader eseguo il numero di volte desiderato, a seconda della logica dello shader.

Il metodo CalculateThreadgroups consente alla trasformazione di informare Direct2D il numero di gruppi di thread necessari, in base alle dimensioni dell'immagine e alla conoscenza dello shader della trasformazione.

Il numero di esecuzioni dello shader di calcolo è un prodotto dei conteggi del threadgroup specificati qui e l'annotazione "numthreads" nell'HLSL del compute shader. Ad esempio, se la trasformazione imposta le dimensioni del threadgroup su (2,2,1) che lo shader specifica (3,3,1) thread per ogni threadgroup, verranno eseguiti 4 threadgroup, ognuno con 9 thread in essi contenuti, per un totale di 36 istanze di thread.

Uno scenario comune consiste nell'elaborare un pixel di output per ogni istanza del compute shader. Per calcolare il numero di gruppi di thread per questo scenario, la trasformazione divide la larghezza e l'altezza dell'immagine in base alle rispettive dimensioni x e y dell'annotazione 'numthreads' nell'HLSL del compute shader.

È importante notare che, se questa divisione viene eseguita, il numero di gruppi di thread richiesti deve essere sempre arrotondato al numero intero più vicino. In caso contrario, i pixel di "resto" non verranno eseguiti su . Se uno shader ,ad esempio, calcola un singolo pixel con ogni thread, il codice del metodo verrà visualizzato come segue.

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 usa il codice seguente per specificare il numero di thread in ogni gruppo di thread:

// 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(
...

Durante l'esecuzione, il threadgroup corrente e l'indice del thread corrente vengono passati come parametri al metodo shader:

#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
    )
{
...

Lettura dei dati dell'immagine

Gli shader di calcolo accedono all'immagine di input della trasformazione come trama bidimensionale:

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

Tuttavia, come i pixel shader, i dati dell'immagine non sono garantiti a partire da (0, 0) sulla trama. Direct2D fornisce invece costanti di sistema che consentono agli shader di compensare qualsiasi offset:

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

Dopo aver definito il buffer costante e il metodo helper precedenti, lo shader può campionare i dati dell'immagine usando quanto segue:

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

Scrittura di dati immagine

Direct2D prevede che uno shader definiscano un buffer di output per l'immagine risultante da posizionare. Nel modello shader 4 (DirectX 10), questo deve essere un buffer unidimensionale a causa di vincoli di funzionalità:

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

La trama di output viene indicizzata per prima riga per consentire l'archiviazione dell'intera immagine.

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

D'altra parte, gli shader Model 5 (DirectX 11) possono usare trame di output bidimensionali:

RWTexture2D<float4> OutputTexture : register(t1);

Con shader Model 5, Direct2D fornisce un parametro 'outputOffset' aggiuntivo nel buffer costante. L'output dello shader deve essere sfalsato da questa quantità:

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

Di seguito è riportato un shader di calcolo pass-through modello 5 completato. In esso, ognuno dei thread dello shader di calcolo legge e scrive un singolo pixel dell'immagine di input.

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

Il codice seguente mostra la versione equivalente del modello shader 4 dello shader. Si noti che lo shader esegue ora il rendering in un buffer di output unidimensionale.

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

Esempio D2DCustomEffects SDK