Freigeben über


Benutzerdefinierte Effekte

Direct2D wird mit einer Bibliothek von Effekten ausgeliefert, die eine Vielzahl allgemeiner Bildvorgänge ausführen. Die vollständige Liste der Effekte finden Sie im Thema integrierte Effekte . Für Funktionen, die mit den integrierten Effekten nicht erreicht werden können, können Sie mit Direct2D Ihre eigenen benutzerdefinierten Effekte mit standard HLSL schreiben. Sie können diese benutzerdefinierten Effekte zusammen mit den integrierten Effekten verwenden, die mit Direct2D ausgeliefert werden.

Beispiele für einen vollständigen Pixel-, Vertex- und Compute-Shadereffekt finden Sie im D2DCustomEffects SDK-Beispiel.

In diesem Thema zeigen wir Ihnen die Schritte und Konzepte, die Sie zum Entwerfen und Erstellen eines vollständig ausgestatteten benutzerdefinierten Effekts benötigen.

Einführung: Was ist in einem Effekt?

Fallschatteneffektdiagramm.

Konzeptionell führt ein Direct2D-Effekt eine Bilderstellungsaufgabe aus, z. B. die Änderung der Helligkeit, das Entsättigen eines Bilds oder, wie oben gezeigt, das Erstellen eines Schlagschattens. Für die App sind sie einfach. Sie können null oder mehr Eingabebilder akzeptieren, mehrere Eigenschaften verfügbar machen, die ihren Vorgang steuern, und ein einzelnes Ausgabeimage generieren.

Es gibt vier verschiedene Teile eines benutzerdefinierten Effekts, für die ein Effektautor verantwortlich ist:

  1. Effektschnittstelle: Die Effektschnittstelle definiert konzeptionell, wie eine App mit einem benutzerdefinierten Effekt interagiert (z. B. wie viele Eingaben der Effekt akzeptiert und welche Eigenschaften verfügbar sind). Die Effektschnittstelle verwaltet ein Transformationsdiagramm, das die tatsächlichen Bilderstellungsvorgänge enthält.
  2. Transformationsdiagramm: Jeder Effekt erstellt ein internes Transformationsdiagramm, das aus einzelnen Transformationen besteht. Jede Transformation stellt einen einzelnen Imagevorgang dar. Der Effekt ist dafür verantwortlich, diese Transformationen in einem Graphen zu verknüpfen, um den beabsichtigten bildgebenden Effekt zu erzielen. Ein Effekt kann Transformationen als Reaktion auf Änderungen an den externen Eigenschaften des Effekts hinzufügen, entfernen, ändern und neu anordnen.
  3. Transformation: Eine Transformation stellt einen Vorgang mit einem einzelnen Bild dar. Sein Standard Zweck besteht darin, die Shader zu beherbergen, die für jedes Ausgabepixel ausgeführt werden. Zu diesem Zweck ist es für die Berechnung der neuen Größe des Ausgabebilds auf Der Grundlage der Logik in seinen Shadern verantwortlich. Außerdem muss berechnet werden, aus welchem Bereich des Eingabebilds die Shader lesen müssen, um den angeforderten Ausgabebereich zu rendern.
  4. Shader: Ein Shader wird für die Eingabe der Transformation auf der GPU (oder CPU) ausgeführt, wenn beim Erstellen des Direct3D-Geräts das Softwarerendering angegeben wird. Effektshader werden in High Level Shading Language (HLSL) geschrieben und während der Kompilierung des Effekts in Bytecode kompiliert, der dann während der Laufzeit vom Effekt geladen wird. In diesem Referenzdokument wird beschrieben, wie Direct2D-kompatible HLSL geschrieben wird. Die Direct3D-Dokumentation enthält eine grundlegende HLSL-Übersicht.

Erstellen einer Effektschnittstelle

Die Effektschnittstelle definiert, wie eine App mit dem benutzerdefinierten Effekt interagiert. Um eine Effektschnittstelle zu erstellen, muss eine Klasse ID2D1EffectImpl implementieren, Metadaten definieren, die den Effekt beschreiben (z. B. Name, Eingabeanzahl und Eigenschaften), und Methoden erstellen, mit denen der benutzerdefinierte Effekt für die Verwendung mit Direct2D registriert wird.

Nachdem alle Komponenten für eine Effektschnittstelle implementiert wurden, wird der Header der Klasse wie folgt angezeigt:

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

Implementieren von ID2D1EffectImpl

Die ID2D1EffectImpl-Schnittstelle enthält drei Methoden, die Sie implementieren müssen:

Initialize(ID2D1EffectContext *pContextInternal, ID2D1TransformGraph *pTransformGraph)

Direct2D ruft die Initialize-Methode auf, nachdem die ID2D1DeviceContext::CreateEffect-Methode von der App aufgerufen wurde. Sie können diese Methode verwenden, um eine interne Initialisierung oder andere Vorgänge auszuführen, die für den Effekt erforderlich sind. Darüber hinaus können Sie damit das erste Transformationsdiagramm des Effekts erstellen.

SetGraph(ID2D1TransformGraph *pTransformGraph)

Direct2D ruft die SetGraph-Methode auf, wenn die Anzahl der Eingaben für den Effekt geändert wird. Während die meisten Effekte eine konstante Anzahl von Eingaben aufweisen, unterstützen andere wie der Zusammengesetzte Effekt eine variable Anzahl von Eingaben. Diese Methode ermöglicht es diesen Effekten, ihr Transformationsdiagramm als Reaktion auf eine sich ändernde Eingabeanzahl zu aktualisieren. Wenn ein Effekt keine Variableneingabeanzahl unterstützt, kann diese Methode einfach E_NOTIMPL zurückgeben.

PrepareForRender (D2D1_CHANGE_TYPE changeType)

Die PrepareForRender-Methode bietet eine Möglichkeit für Effekte, vorgänge als Reaktion auf externe Änderungen auszuführen. Direct2D ruft diese Methode kurz vor dem Rendern eines Effekts auf, wenn mindestens einer der folgenden Werte zutrifft:

  • Der Effekt wurde zuvor initialisiert, aber noch nicht gezeichnet.
  • Eine Effekteigenschaft wurde seit dem letzten Zeichnungsaufruf geändert.
  • Der Zustand des aufrufenden Direct2D-Kontexts (z. B. DPI) hat sich seit dem letzten Zeichnungsaufruf geändert.

Implementieren der Effektregistrierungs- und Rückrufmethoden

Apps müssen Effekte bei Direct2D registrieren, bevor sie instanziiert werden. Diese Registrierung ist auf eine instance einer Direct2D-Factory ausgerichtet und muss bei jeder Ausführung der App wiederholt werden. Um diese Registrierung zu aktivieren, definiert ein benutzerdefinierter Effekt eine eindeutige GUID, eine öffentliche Methode, die den Effekt registriert, und eine private Rückrufmethode, die eine instance des Effekts zurückgibt.

Definieren einer GUID

Sie müssen eine GUID definieren, die den Effekt für die Registrierung bei Direct2D eindeutig identifiziert. Die App verwendet dasselbe, um den Effekt zu identifizieren, wenn ID2D1DeviceContext::CreateEffect aufgerufen wird.

Dieser Code veranschaulicht das Definieren einer solchen GUID für einen Effekt. Sie müssen eine eigene eindeutige GUID mithilfe eines GUID-Generierungstools wie guidgen.exe erstellen.

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

Definieren einer öffentlichen Registrierungsmethode

Definieren Sie als Nächstes eine öffentliche Methode, mit der die App aufgerufen werden soll, um den Effekt bei Direct2D zu registrieren. Da die Effektregistrierung für eine instance einer Direct2D-Factory spezifisch ist, akzeptiert die Methode eine ID2D1Factory1-Schnittstelle als Parameter. Um den Effekt zu registrieren, ruft die Methode dann die ID2D1Factory1::RegisterEffectFromString-API für den PARAMETER ID2D1Factory1 auf .

Diese API akzeptiert eine XML-Zeichenfolge, die die Metadaten, Eingaben und Eigenschaften des Effekts beschreibt. Die Metadaten für einen Effekt dienen nur zu Informationszwecken und können von der App über die ID2D1Properties-Schnittstelle abgefragt werden. Die Eingabe- und Eigenschaftsdaten hingegen werden von Direct2D verwendet und stellen die Funktionalität des Effekts dar.

Eine XML-Zeichenfolge für einen minimalen Beispieleffekt wird hier gezeigt. Das Hinzufügen benutzerdefinierter Eigenschaften zum XML-Code wird im Abschnitt Hinzufügen benutzerdefinierter Eigenschaften zu einem Effekt behandelt.

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

Definieren einer Rückrufmethode für die Effektfactory

Der Effekt muss auch eine private Rückrufmethode bereitstellen, die eine instance des Effekts über einen einzelnen IUnknown**-Parameter zurückgibt. Ein Zeiger auf diese Methode wird in Direct2D bereitgestellt, wenn der Effekt über die ID2D1Factory1::RegisterEffectFromString-API über den Parameter PD2D1_EFFECT_FACTORY\ registriert wird.

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

Implementieren der IUnknown-Schnittstelle

Schließlich muss der Effekt die IUnknown-Schnittstelle implementieren, um die Kompatibilität mit COM zu gewährleisten.

Erstellen des Transformationsdiagramms des Effekts

Ein Effekt kann mehrere verschiedene Transformationen (einzelne Bildvorgänge) verwenden, um den gewünschten Bildeffekt zu erzeugen. Um die Reihenfolge zu steuern, in der diese Transformationen auf das Eingabebild angewendet werden, werden sie durch den Effekt in einem Transformationsdiagramm angeordnet. Ein Transformationsdiagramm kann die in Direct2D enthaltenen Effekte und Transformationen sowie benutzerdefinierte Transformationen nutzen, die vom Effektautor erstellt wurden.

Verwenden von In Direct2D enthaltenen Transformationen

Dies sind die am häufigsten verwendeten Transformationen, die mit Direct2D bereitgestellt werden.

Erstellen eines Transformationsdiagramms mit einem einzelnen Knoten

Nachdem Sie eine Transformation erstellt haben, muss die Eingabe des Effekts mit der Eingabe der Transformation verbunden sein, und die Ausgabe der Transformation muss mit der Ausgabe des Effekts verbunden werden. Wenn ein Effekt nur eine einzelne Transformation enthält, können Sie die ID2D1TransformGraph::SetSingleTransformNode-Methode verwenden, um dies problemlos zu erreichen.

Sie können eine Transformation in den Initialize - oder SetGraph-Methoden des Effekts mithilfe des angegebenen ID2D1TransformGraph-Parameters erstellen oder ändern. Wenn ein Effekt Änderungen am Transformationsdiagramm in einer anderen Methode vornehmen muss, in der dieser Parameter nicht verfügbar ist, kann der Effekt den ID2D1TransformGraph-Parameter als Membervariable der -Klasse speichern und an anderer Stelle darauf zugreifen, z. B. PrepareForRender oder eine Rückrufmethode für benutzerdefinierte Eigenschaften.

Ein Beispiel für die Initialize-Methode wird hier gezeigt. Diese Methode erstellt ein Transformationsdiagramm mit einem Knoten, das das Bild um hundert Pixel auf jeder Achse versetzt.

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

Erstellen eines Transformationsdiagramms mit mehreren Knoten

Das Hinzufügen mehrerer Transformationen zum Transformationsdiagramm eines Effekts ermöglicht es Effekten, intern mehrere Bildvorgänge auszuführen, die einer App als einzelner einheitlicher Effekt angezeigt werden.

Wie oben erwähnt, kann das Transformationsdiagramm des Effekts in jeder Effektmethode mithilfe des ID2D1TransformGraph-Parameters bearbeitet werden, der in der Initialize-Methode des Effekts empfangen wurde. Die folgenden APIs für diese Schnittstelle können verwendet werden, um das Transformationsdiagramm eines Effekts zu erstellen oder zu ändern:

AddNode(ID2D1TransformNode *pNode)

Die AddNode-Methode "registriert" die Transformation mit dem Effekt und muss aufgerufen werden, bevor die Transformation mit einer der anderen Transformationsdiagrammmethoden verwendet werden kann.

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

Die ConnectToEffectInput-Methode verbindet die Bildeingabe des Effekts mit der Eingabe einer Transformation. Die gleiche Effekteingabe kann mit mehreren Transformationen verbunden werden.

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

Die ConnectNode-Methode verbindet die Ausgabe einer Transformation mit der Eingabe einer anderen Transformation. Eine Transformationsausgabe kann mit mehreren Transformationen verbunden werden.

SetOutputNode(ID2D1TransformNode *pNode)

Die SetOutputNode-Methode verbindet die Ausgabe einer Transformation mit der Ausgabe des Effekts. Da ein Effekt nur eine Ausgabe aufweist, kann nur eine einzelne Transformation als "Ausgabeknoten" bezeichnet werden.

Dieser Code verwendet zwei separate Transformationen, um einen einheitlichen Effekt zu erstellen. In diesem Fall ist der Effekt ein übersetzter Schlagschatten.

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

Hinzufügen benutzerdefinierter Eigenschaften zu einem Effekt

Effekte können benutzerdefinierte Eigenschaften definieren, die es einer App ermöglichen, das Verhalten des Effekts während der Laufzeit zu ändern. Es gibt drei Schritte, um eine Eigenschaft für einen benutzerdefinierten Effekt zu definieren:

Hinzufügen der Eigenschaftsmetadaten zu den Registrierungsdaten des Effekts

Hinzufügen einer Eigenschaft zum Registrierungs-XML

Sie müssen die Eigenschaften eines benutzerdefinierten Effekts während der ersten Registrierung des Effekts bei Direct2D definieren. Zunächst müssen Sie die Registrierungs-XML des Effekts in der öffentlichen Registrierungsmethode mit der neuen Eigenschaft aktualisieren:

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

Wenn Sie eine Effekteigenschaft in XML definieren, benötigt sie einen Namen, einen Typ und einen Anzeigenamen. Der Anzeigename einer Eigenschaft sowie die Werte für Kategorie, Autor und Beschreibung des Gesamteffekts können und sollten lokalisiert werden.

Für jede Eigenschaft kann ein Effekt optional standard-, min- und max-Werte angeben. Diese Werte dienen nur der Informationsverwendung. Sie werden nicht von Direct2D erzwungen. Es liegt an Ihnen, eine angegebene Standard-/Min/Max-Logik in der Effektklasse selbst zu implementieren.

Der im XML-Code für die -Eigenschaft aufgeführte Typwert muss mit dem entsprechenden Datentyp übereinstimmen, der von den Getter- und Settermethoden der Eigenschaft verwendet wird. Die entsprechenden XML-Werte für jeden Datentyp werden in dieser Tabelle angezeigt:

Datentyp Entsprechender XML-Wert
PWSTR Zeichenfolge
BOOL bool
UINT uint32
INT int32
GLEITKOMMAZAHL 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
Enumeration (D2D1_INTERPOLATION_MODE usw.) enum

 

Zuordnen der neuen Eigenschaft zu Getter- und Settermethoden

Als Nächstes muss der Effekt diese neue Eigenschaft den Getter- und Settermethoden zuordnen. Dies geschieht über das D2D1_PROPERTY_BINDING Array, das an die ID2D1Factory1::RegisterEffectFromString-Methode übergeben wird.

Das D2D1_PROPERTY_BINDING Arrays sieht wie folgt aus:

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

Nachdem Sie das XML- und bindungsarray erstellt haben, übergeben Sie sie an die RegisterEffectFromString-Methode :

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

Das D2D1_VALUE_TYPE_BINDING Makro erfordert, dass die Effektklasse von ID2D1EffectImpl vor jeder anderen Schnittstelle erbt.

Benutzerdefinierte Eigenschaften für einen Effekt werden in der Reihenfolge indiziert, in der sie im XML deklariert werden, und nach der Erstellung kann die App mithilfe der Methoden ID2D1Properties::SetValue und ID2D1Properties::GetValue auf zugreifen. Zur Vereinfachung können Sie eine öffentliche Enumeration erstellen, die jede Eigenschaft in der Headerdatei des Effekts auflistet:

typedef enum SAMPLEEFFECT_PROP
{
    SAMPLEFFECT_PROP_OFFSET = 0
};

Erstellen der Getter- und Settermethoden für die Eigenschaft

Der nächste Schritt besteht darin, die Getter- und Settermethoden für die neue Eigenschaft zu erstellen. Die Namen der Methoden müssen mit denen übereinstimmen, die im D2D1_PROPERTY_BINDING Array angegeben sind. Darüber hinaus muss der im XML-Code des Effekts angegebene Eigenschaftentyp mit dem Typ des Parameters der Settermethode und dem Rückgabewert der Getter-Methode übereinstimmen.

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

Aktualisieren der Transformationen des Effekts als Reaktion auf Eigenschaftsänderungen

Um die Bildausgabe eines Effekts als Reaktion auf eine Eigenschaftsänderung tatsächlich zu aktualisieren, muss der Effekt seine zugrunde liegenden Transformationen ändern. Dies geschieht in der Regel in der PrepareForRender-Methode des Effekts, die Direct2D automatisch aufruft, wenn eine der Eigenschaften eines Effekts geändert wurde. Transformationen können jedoch in allen Methoden des Effekts aktualisiert werden: z. B. Initialize oder die Eigenschaftensettermethoden des Effekts.

Wenn ein Effekt beispielsweise einen ID2D1OffsetTransform enthält und seinen Offsetwert als Reaktion auf die Änderung der Offset-Eigenschaft des Effekts ändern möchte, wird der folgende Code in PrepareForRender hinzugefügt:

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

Erstellen einer benutzerdefinierten Transformation

Zum Implementieren von Imagevorgängen, die über die in Direct2D bereitgestellten Hinausgehen hinausgehen, müssen Sie benutzerdefinierte Transformationen implementieren. Benutzerdefinierte Transformationen können ein Eingabebild mithilfe von benutzerdefinierten HLSL-Shadern beliebig ändern.

Transformationen implementieren eine von zwei verschiedenen Schnittstellen, abhängig von den Von ihnen verwendeten Shadertypen. Transformationen mit Pixel- und/oder Vertex-Shadern müssen ID2D1DrawTransform implementieren, während Transformationen mithilfe von Compute-Shadern ID2D1ComputeTransform implementieren müssen. Diese Schnittstellen erben beide von ID2D1Transform. Dieser Abschnitt konzentriert sich auf die Implementierung der Funktionen, die für beide gemeinsam sind.

Die ID2D1Transform-Schnittstelle verfügt über vier Methoden zur Implementierung:

GetInputCount

Diese Methode gibt eine ganze Zahl zurück, die die Eingabeanzahl für die Transformation darstellt.

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

MapInputRectsToOutputRect

Direct2D ruft die MapInputRectsToOutputRect-Methode jedes Mal auf, wenn die Transformation gerendert wird. Direct2D übergibt ein Rechteck, das die Grenzen der einzelnen Eingaben darstellt, an die Transformation. Die Transformation ist dann für die Berechnung der Grenzen des Ausgabebilds verantwortlich. Die Größe der Rechtecke für alle Methoden auf dieser Schnittstelle (ID2D1Transform) wird in Pixeln und nicht in DIPs definiert.

Diese Methode ist auch für die Berechnung des Bereichs der Ausgabe verantwortlich, der auf der Logik des Shaders und der undurchsichtigen Regionen jeder Eingabe basiert. Ein undurchsichtiger Bereich eines Bilds ist definiert als der Bereich, in dem der Alphakanal für die gesamte Größe des Rechtecks "1" ist. Wenn unklar ist, ob die Ausgabe einer Transformation undurchsichtig ist, sollte das undurchsichtige Ausgaberechteck als sicherer Wert auf (0, 0, 0, 0) festgelegt werden. Direct2D verwendet diese Informationen, um Renderingoptimierungen mit "garantiert undurchsichtigem" Inhalt durchzuführen. Wenn dieser Wert ungenau ist, kann dies zu einem falschen Rendering führen.

Sie können das Renderingverhalten der Transformation (wie in den Abschnitten 6 bis 8 definiert) während dieser Methode ändern. Sie können jedoch keine anderen Transformationen im Transformationsdiagramm oder das Diagrammlayout selbst ändern.

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

Für ein komplexeres Beispiel sollten Sie überlegen, wie ein einfacher Weichzeichnervorgang dargestellt wird:

Wenn für einen Weichzeichnervorgang ein Radius von 5 Pixeln verwendet wird, muss die Größe des Ausgaberechtecks wie unten dargestellt um 5 Pixel erweitert werden. Beim Ändern von Rechteckkoordinaten muss eine Transformation sicherstellen, dass ihre Logik keine Über-/Unterläufe in den Rechteckkoordinaten verursacht.

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

Da das Bild verschwommen ist, kann ein Bereich des Bilds, der undurchsichtig war, jetzt teilweise transparent sein. Dies liegt daran, dass der Bereich außerhalb des Bilds standardmäßig transparent schwarz ist und diese Transparenz in das Bild um die Ränder gemischt wird. Die Transformation muss dies in ihren Berechnungen für undurchsichtige Ausgaberechtecke widerspiegeln:

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

Diese Berechnungen werden hier visualisiert:

Abbildung der Rechteckberechnung.

Weitere Informationen zu dieser Methode finden Sie auf der MapInputRectsToOutputRect-Referenzseite .

MapOutputRectToInputRects

Direct2D ruft die MapOutputRectToInputRects-Methode nach MapInputRectsToOutputRect auf. Die Transformation muss berechnen, aus welchem Teil des Bilds sie lesen muss, um den angeforderten Ausgabebereich ordnungsgemäß zu rendern.

Wenn ein Effekt die Pixel 1 bis 1 streng zuordnet, kann das Ausgaberechteck wie zuvor an das Eingaberechteck übergeben werden:

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

Wenn eine Transformation ein Bild verkleinert oder erweitert (wie hier das Unschärfebeispiel), verwenden Pixel häufig umgebende Pixel, um ihren Wert zu berechnen. Bei einer Unschärfe wird ein Pixel mit seinen umgebenden Pixeln gemittelt, auch wenn sie sich außerhalb der Grenzen des Eingabebilds befinden. Dieses Verhalten spiegelt sich in der Berechnung wider. Wie zuvor überprüft die Transformation beim Erweitern der Koordinaten eines Rechtecks auf Überläufe.

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

In dieser Abbildung wird die Berechnung visualisiert. Direct2D erfasst automatisch transparente schwarze Pixel, bei denen das Eingabebild nicht vorhanden ist, sodass die Weichzeichner schrittweise mit dem vorhandenen Inhalt auf dem Bildschirm gemischt werden können.

Abbildung eines Effekts zum Sampling transparenter schwarzer Pixel außerhalb eines Rechtecks.

Wenn die Zuordnung nicht trivial ist, sollte diese Methode das Eingaberechteck auf den maximalen Bereich festlegen, um korrekte Ergebnisse zu gewährleisten. Legen Sie hierzu den linken und oberen Rand auf INT_MIN und den rechten und unteren Rand auf INT_MAX fest.

Weitere Informationen zu dieser Methode finden Sie im Thema MapOutputRectToInputRects .

MapInvalidRect

Direct2D ruft auch die MapInvalidRect-Methode auf . Im Gegensatz zu den Methoden MapInputRectsToOutputRect und MapOutputRectToInputRects ist es jedoch nicht garantiert, dass sie zu einem bestimmten Zeitpunkt aufgerufen wird. Diese Methode entscheidet konzeptionell, welcher Teil der Ausgabe einer Transformation als Reaktion darauf, dass sich ein Teil oder die gesamte Eingabe ändert, neu gerendert werden muss. Es gibt drei verschiedene Szenarien, für die das ungültige Rechteck einer Transformation berechnet werden kann.

Transformationen mit 1:1-Pixelzuordnung

Bei Transformationen, die Pixel 1 bis 1 zuordnen, übergeben Sie einfach das ungültige Eingaberechteck an das ungültige Ausgaberechteck:

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

Transformationen mit m:n-Pixelzuordnung

Wenn die Ausgabepixel einer Transformation von ihrer Umgebung abhängen, muss das ungültige Eingaberechteck entsprechend erweitert werden. Dies spiegelt wider, dass Pixel, die das ungültige Eingaberechteck umgeben, ebenfalls betroffen sind und ungültig werden. Beispielsweise wird für eine Unschärfe von fünf Pixeln die folgende Berechnung verwendet:

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

Transformationen mit komplexer Pixelzuordnung

Bei Transformationen, bei denen Eingabe- und Ausgabepixel keine einfache Zuordnung aufweisen, kann die gesamte Ausgabe als ungültig markiert werden. Wenn eine Transformation beispielsweise einfach die durchschnittliche Farbe der Eingabe ausgibt, ändert sich die gesamte Ausgabe der Transformation, wenn auch nur ein kleiner Teil der Eingabe geändert wird. In diesem Fall sollte das ungültige Ausgaberechteck auf ein logisch unendliches Rechteck festgelegt werden (siehe unten). Direct2D klemmt dies automatisch an die Grenzen der Ausgabe.

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

Weitere Informationen zu dieser Methode finden Sie im Thema MapInvalidRect .

Nachdem diese Methoden implementiert wurden, enthält der Header Ihrer Transformation Folgendes:

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

Hinzufügen eines Pixelshaders zu einer benutzerdefinierten Transformation

Nachdem eine Transformation erstellt wurde, muss ein Shader bereitgestellt werden, der die Bildpixel bearbeitet. In diesem Abschnitt werden die Schritte zur Verwendung eines Pixelshaders mit einer benutzerdefinierten Transformation behandelt.

Implementieren von ID2D1DrawTransform

Um einen Pixelshader zu verwenden, muss die Transformation die ID2D1DrawTransform-Schnittstelle implementieren, die von der id2D1Transform-Schnittstelle erbt, die in Abschnitt 5 beschrieben wird. Diese Schnittstelle enthält eine neue Methode, die implementiert werden soll:

SetDrawInfo(ID2D1DrawInfo *pDrawInfo)

Direct2D ruft die SetDrawInfo-Methode auf, wenn die Transformation zum ersten Mal dem Transformationsdiagramm eines Effekts hinzugefügt wird. Diese Methode stellt einen ID2D1DrawInfo-Parameter bereit, der steuert, wie die Transformation gerendert wird. Die hier verfügbaren Methoden finden Sie im Thema ID2D1DrawInfo .

Wenn die Transformation diesen Parameter als Klassenmembervariable speichert, kann von anderen Methoden wie Eigenschaftensettern oder MapInputRectsToOutputRect aus auf das drawInfo-Objekt zugegriffen und geändert werden. Insbesondere kann es nicht von den MapOutputRectToInputRects - oder MapInvalidRect-Methoden auf ID2D1Transform aufgerufen werden.

Erstellen einer GUID für den Pixelshader

Als Nächstes muss die Transformation eine eindeutige GUID für den Pixelshader selbst definieren. Dies wird verwendet, wenn Direct2D den Shader in den Arbeitsspeicher lädt, und wenn die Transformation auswäht, welcher Pixelshader für die Ausführung verwendet werden soll. Tools wie guidgen.exe, die in Visual Studio enthalten sind, können verwendet werden, um eine zufällige GUID zu generieren.

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

Laden des Pixelshaders mit Direct2D

Ein Pixelshader muss in den Arbeitsspeicher geladen werden, bevor er von der Transformation verwendet werden kann.

Um den Pixelshader in den Arbeitsspeicher zu laden, sollte die Transformation den kompilierten Shader-Bytecode aus dem lesen. Von Visual Studio generierte CSO-Datei (ausführliche Informationen finden Sie in der Direct3D-Dokumentation ) in einem Bytearray. Diese Technik wird im D2DCustomEffects SDK-Beispiel ausführlich veranschaulicht.

Nachdem die Shaderdaten in ein Bytearray geladen wurden, rufen Sie die LoadPixelShader-Methode für das ID2D1EffectContext-Objekt des Effekts auf. Direct2D ignoriert Aufrufe von LoadPixelShader , wenn bereits ein Shader mit derselben GUID geladen wurde.

Nachdem ein Pixelshader in den Arbeitsspeicher geladen wurde, muss die Transformation ihn für die Ausführung auswählen, indem die GUID für den ID2D1DrawInfo-Parameter, der während der SetDrawInfo-Methode bereitgestellt wird, an die SetPixelShader-Methode übergeben wird. Der Pixelshader muss bereits in den Arbeitsspeicher geladen werden, bevor er für die Ausführung ausgewählt wird.

Ändern des Shadervorgangs mit Konstantenpuffern

Um die Ausführung eines Shaders zu ändern, kann eine Transformation einen Konstantenpuffer an den Pixelshader übergeben. Dazu definiert eine Transformation eine Struktur, die die gewünschten Variablen im Klassenheader enthält:

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

Die Transformation ruft dann die ID2D1DrawInfo::SetPixelShaderConstantBuffer-Methode für den ID2D1DrawInfo-Parameter auf, der in der SetDrawInfo-Methode bereitgestellt wird, um diesen Puffer an den Shader zu übergeben.

Die HLSL muss auch eine entsprechende Struktur definieren, die den Konstantenpuffer darstellt. Die in der Struktur des Shaders enthaltenen Variablen müssen mit denen in der Struktur der Transformation übereinstimmen.

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

Nachdem der Puffer definiert wurde, können die darin enthaltenen Werte von überall innerhalb des Pixelshaders gelesen werden.

Schreiben eines Pixelshaders für Direct2D

Direct2D-Transformationen verwenden Shader, die mithilfe von HLSL-Standard erstellt wurden. Es gibt jedoch einige wichtige Konzepte zum Schreiben eines Pixelshaders, der aus dem Kontext einer Transformation ausgeführt wird. Ein vollständiges Beispiel für einen voll funktionsfähigen Pixelshader finden Sie im D2DCustomEffects SDK-Beispiel.

Direct2D ordnet die Eingaben einer Transformation automatisch den Texture2D - und SamplerState-Objekten im HLSL zu. Die erste Textur2D befindet sich im Register t0, und der erste SamplerState befindet sich im Register s0. Jede zusätzliche Eingabe befindet sich bei den nächsten entsprechenden Registern (z. B. t1 und s1). Pixeldaten für eine bestimmte Eingabe können durch Aufrufen von Sample für das Texture2D-Objekt und Übergeben des entsprechenden SamplerState-Objekts und der Texelkoordinaten erfasst werden.

Für jedes gerenderte Pixel wird einmal ein benutzerdefinierter Pixelshader ausgeführt. Bei jeder Ausführung des Shaders stellt Direct2D automatisch drei Parameter bereit, die die aktuelle Ausführungsposition identifizieren:

  • Szenenraumausgabe: Dieser Parameter stellt die aktuelle Ausführungsposition in Bezug auf die Gesamtzieloberfläche dar. Es ist in Pixeln definiert, und seine Min/Max-Werte entsprechen den Begrenzungen des Rechtecks, das von MapInputRectsToOutputRect zurückgegeben wird.
  • Clipspace-Ausgabe: Dieser Parameter wird von Direct3D verwendet und darf nicht im Pixel-Shader einer Transformation verwendet werden.
  • Texel-Space-Eingabe: Dieser Parameter stellt die aktuelle Ausführungsposition in einer bestimmten Eingabetextur dar. Ein Shader sollte keine Abhängigkeiten davon annehmen, wie dieser Wert berechnet wird. Es sollte nur verwendet werden, um die Eingabe des Pixelshaders zu erfassen, wie im folgenden Code gezeigt:
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;
}

Hinzufügen eines Vertexshaders zu einer benutzerdefinierten Transformation

Sie können Vertex-Shader verwenden, um andere Imageszenarien als Pixelshader zu erreichen. Insbesondere Vertexshader können geometriebasierte Bildeffekte ausführen, indem Sie Scheitelpunkte transformieren, die ein Bild bilden. Vertexshader können unabhängig von oder in Verbindung mit transformationsspezifischen Pixelshadern verwendet werden. Wenn kein Vertex-Shader angegeben ist, ersetzt Direct2D einen Standardvertexshader für die Verwendung mit dem benutzerdefinierten Pixelshader.

Der Prozess zum Hinzufügen eines Vertexshaders zu einer benutzerdefinierten Transformation ähnelt dem eines Pixelshaders. Die Transformation implementiert die ID2D1DrawTransform-Schnittstelle , erstellt eine GUID und übergibt (optional) Konstantenpuffer an den Shader. Es gibt jedoch einige wichtige zusätzliche Schritte, die für Vertexshader eindeutig sind:

Erstellen eines Vertexpuffers

Ein Vertexshader wird definitionsgemäß für scheitelpunkte ausgeführt, die an ihn übergeben werden, nicht für einzelne Pixel. Um die Scheitelpunkte für den Shader anzugeben, auf dem ausgeführt werden soll, erstellt eine Transformation einen Vertexpuffer, der an den Shader übergeben werden soll. Das Layout von Vertexpuffern liegt außerhalb des Rahmens dieses Dokuments. Weitere Informationen finden Sie in der Direct3D-Referenz oder im D2DCustomEffects SDK-Beispiel für eine Beispielimplementierung.

Nach dem Erstellen eines Vertexpuffers im Arbeitsspeicher verwendet die Transformation die CreateVertexBuffer-Methode für das ID2D1EffectContext-Objekt des enthaltenden Effekts, um diese Daten an die GPU zu übergeben. Eine Beispielimplementierung finden Sie im D2DCustomEffects SDK-Beispiel .

Wenn von der Transformation kein Vertexpuffer angegeben wird, übergibt Direct2D einen Standardvertexpuffer, der die rechteckige Bildposition darstellt.

Ändern von SetDrawInfo zur Verwendung eines Vertex-Shaders

Wie bei Pixelshadern muss die Transformation einen Vertexshader für die Ausführung laden und auswählen. Um den Vertex-Shader zu laden, ruft er die LoadVertexShader-Methode für die ID2D1EffectContext-Methode auf, die in der Initialize-Methode des Effekts empfangen wurde. Um den Vertexshader für die Ausführung auszuwählen, ruft er SetVertexProcessing für den ID2D1DrawInfo-Parameter auf, der in der SetDrawInfo-Methode der Transformation empfangen wird. Diese Methode akzeptiert eine GUID für einen zuvor geladenen Vertexshader sowie (optional) einen zuvor erstellten Vertexpuffer, auf dem der Shader ausgeführt werden soll.

Implementieren eines Direct2D-Vertexshaders

Eine Zeichnungstransformation kann sowohl einen Pixelshader als auch einen Vertexshader enthalten. Wenn eine Transformation sowohl einen Pixel-Shader als auch einen Vertex-Shader definiert, wird die Ausgabe des Vertex-Shaders direkt an den Pixelshader übergeben: Die App kann die Rückgabesignatur des Vertexshaders bzw. der Parameter des Pixelshaders anpassen, solange sie konsistent sind.

Wenn eine Transformation hingegen nur einen Vertex-Shader enthält und auf dem standardmäßigen Passthrough-Pixelshader von Direct2D basiert, muss sie die folgende Standardausgabe zurückgeben:

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

Ein Vertexshader speichert das Ergebnis seiner Vertextransformationen in der Ausgabevariable Scene-space des Shaders. Um die Clipspace-Ausgabe und die Texel-space-Eingabevariablen zu berechnen, stellt Direct2D automatisch Konvertierungsmatrizen in einem konstanten Puffer bereit:

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

Unten finden Sie einen Beispielcode für Vertexshader, der die Konvertierungsmatrizen verwendet, um die richtigen Clip- und Texelräume zu berechnen, die von Direct2D erwartet werden:

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

Der obige Code kann als Ausgangspunkt für einen Vertexshader verwendet werden. Es durchläuft lediglich das Eingabebild, ohne Transformationen auszuführen. Eine vollständig implementierte Vertexshader-basierte Transformation finden Sie im D2DCustomEffects SDK-Beispiel .

Wenn von der Transformation kein Vertexpuffer angegeben wird, ersetzt Direct2D einen Standardvertexpuffer, der die rechteckige Bildposition darstellt. Die Parameter für den Vertex-Shader werden in die Parameter der Standard-Shaderausgabe geändert:

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

Der Vertex-Shader darf seine SceneSpaceOutput- und clipSpaceOutput-Parameter nicht ändern. Sie müssen sie unverändert zurückgeben. Es kann jedoch die texelSpaceInput-Parameter für jedes Eingabebild ändern. Wenn die Transformation auch einen benutzerdefinierten Pixelshader enthält, kann der Vertex-Shader weiterhin zusätzliche benutzerdefinierte Parameter direkt an den Pixelshader übergeben. Darüber hinaus wird der benutzerdefinierte Puffer (b0) der SceneSpace-Konvertierungsmatrizen nicht mehr bereitgestellt.

Hinzufügen eines Compute-Shaders zu einer benutzerdefinierten Transformation

Schließlich können benutzerdefinierte Transformationen Compute-Shader für bestimmte Zielszenarien verwenden. Compute-Shader können verwendet werden, um komplexe Bildeffekte zu implementieren, die beliebigen Zugriff auf Eingabe- und Ausgabebildpuffer erfordern. Beispielsweise kann ein einfacher Histogrammalgorithmus aufgrund von Einschränkungen des Speicherzugriffs nicht mit einem Pixelshader implementiert werden.

Da Compute-Shader höhere Anforderungen auf Hardwarefeatureebene haben als Pixelshader, sollten Pixelshader verwendet werden, wenn möglich, um einen bestimmten Effekt zu implementieren. Insbesondere werden Compute-Shader nur auf den meisten DirectX 10-Levelkarten und höher ausgeführt. Wenn eine Transformation einen Compute-Shader verwendet, muss sie während der Instanziierung zusätzlich zur Implementierung der ID2D1ComputeTransform-Schnittstelle nach der entsprechenden Hardwareunterstützung suchen.

Überprüfen auf Compute-Shader-Unterstützung

Wenn ein Effekt einen Compute-Shader verwendet, muss er während der Erstellung mithilfe der ID2D1EffectContext::CheckFeatureSupport-Methode auf Compute-Shaderunterstützung überprüfen. Wenn die GPU keine Compute-Shader unterstützt, muss der Effekt D2DERR_INSUFFICIENT_DEVICE_CAPABILITIES zurückgeben.

Es gibt zwei verschiedene Arten von Compute-Shadern, die eine Transformation verwenden kann: Shadermodell 4 (DirectX 10) und Shadermodell 5 (DirectX 11). Es gibt bestimmte Einschränkungen für Shader Model 4-Shader. Weitere Informationen finden Sie in der Direct3D-Dokumentation . Transformationen können beide Arten von Shadern enthalten und bei Bedarf auf Shadermodell 4 zurückgreifen: Eine Implementierung finden Sie im D2DCustomEffects SDK-Beispiel .

Implementieren von ID2D1ComputeTransform

Diese Schnittstelle enthält zwei neue Methoden, die zusätzlich zu den methoden in ID2D1Transform implementiert werden sollen:

SetComputeInfo(ID2D1ComputeInfo *pComputeInfo)

Wie bei Pixel- und Vertexshadern ruft Direct2D die SetComputeInfo-Methode auf, wenn die Transformation zum ersten Mal dem Transformationsdiagramm eines Effekts hinzugefügt wird. Diese Methode stellt einen ID2D1ComputeInfo-Parameter bereit, der steuert, wie die Transformation gerendert wird. Dazu gehört die Auswahl des Compute-Shaders, der über die ID2D1ComputeInfo::SetComputeShader-Methode ausgeführt werden soll. Wenn die Transformation diesen Parameter als Klassenmembervariablen speichert, kann mit Ausnahme der MapOutputRectToInputRects - und MapInvalidRect-Methoden von jeder Transformations- oder Effektmethode darauf zugegriffen und geändert werden. Weitere Methoden finden Sie hier im Thema ID2D1ComputeInfo .

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

Während Pixelshader auf Pixelbasis und Vertexshader auf Vertexbasis ausgeführt werden, werden Compute-Shader pro Threadgruppe ausgeführt. Eine Threadgruppe stellt eine Reihe von Threads dar, die gleichzeitig auf der GPU ausgeführt werden. Der HLSL-Code des Computeshaders bestimmt, wie viele Threads pro Threadgruppe ausgeführt werden sollen. Der Effekt skaliert die Anzahl der Threadgruppen, sodass der Shader je nach Logik des Shaders die gewünschte Anzahl von Ausführungen ausführt.

Die CalculateThreadgroups-Methode ermöglicht es der Transformation, Direct2D zu informieren, wie viele Threadgruppen erforderlich sind, basierend auf der Größe des Bilds und den eigenen Kenntnissen der Transformation über den Shader.

Die Häufigkeit der Ausführung des Compute-Shaders ist ein Produkt der hier angegebenen Threadgruppenanzahl und der Anmerkung "Numthreads" im Compute-Shader HLSL. Wenn die Transformation beispielsweise die Threadgruppendimensionen auf (2,2,1) festlegt, gibt der Shader (3,3,1) Threads pro Threadgruppe an, dann werden 4 Threadgruppen mit jeweils 9 Threads für insgesamt 36 Threadinstanzen ausgeführt.

Ein häufiges Szenario ist die Verarbeitung eines Ausgabepixels für jede instance des Compute-Shaders. Um die Anzahl der Threadgruppen für dieses Szenario zu berechnen, dividiert die Transformation die Breite und Höhe des Bilds durch die jeweiligen x- und y-Dimensionen der Numthreads-Anmerkung im Compute-Shader HLSL.

Wichtig: Wenn diese Division durchgeführt wird, muss die Anzahl der angeforderten Threadgruppen immer auf die nächste ganze Zahl aufgerundet werden, da andernfalls die Restlichen Pixel nicht ausgeführt werden. Wenn ein Shader (z. B.) ein einzelnes Pixel mit jedem Thread berechnet, wird der Code der Methode wie folgt angezeigt.

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

Die HLSL verwendet den folgenden Code, um die Anzahl der Threads in jeder Threadgruppe anzugeben:

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

Während der Ausführung werden die aktuelle Threadgruppe und der aktuelle Threadindex als Parameter an die Shadermethode übergeben:

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

Lesen von Bilddaten

Compute-Shader greifen als einzelne zweidimensionale Textur auf das Eingabebild der Transformation zu:

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

Wie Bei Pixelshadern ist es jedoch nicht garantiert, dass die Daten des Bilds in der Textur bei (0, 0) beginnen. Stattdessen stellt Direct2D Systemkonstanten bereit, mit denen Shader alle Offsets kompensieren können:

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

Nachdem der obige Konstantenpuffer und die Hilfsmethode definiert wurden, kann der Shader Bilddaten mit folgendem Beispiel erfassen:

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

Schreiben von Bilddaten

Direct2D erwartet, dass ein Shader einen Ausgabepuffer definiert, damit das resultierende Bild platziert wird. Im Shadermodell 4 (DirectX 10) muss dies aufgrund von Featureeinschränkungen ein eindimensionaler Puffer sein:

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

Die Ausgabetextur wird zeilenweise indiziert, damit das gesamte Bild gespeichert werden kann.

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

Andererseits können Shader model 5 (DirectX 11)-Shader zweidimensionale Ausgabetexturen verwenden:

RWTexture2D<float4> OutputTexture : register(t1);

Mit Shader Model 5-Shadern stellt Direct2D einen zusätzlichen OutputOffset-Parameter im Konstantenpuffer bereit. Die Ausgabe des Shaders sollte um diesen Betrag versetzt werden:

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

Ein fertiger Pass-Through-Shader Model 5 Compute-Shader ist unten dargestellt. Darin liest und schreibt jeder Compute-Shaderthread ein einzelnes Pixel des Eingabebilds.

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

Der folgende Code zeigt die entsprechende Shadermodell 4-Version des Shaders. Beachten Sie, dass der Shader jetzt in einem eindimensionalen Ausgabepuffer gerendert wird.

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

D2DCustomEffects SDK-Beispiel