Benutzerdefinierte Effekte
Direct2D enthält eine Bibliothek mit Effekten, die eine Vielzahl gängiger Imagevorgänge ausführen. Eine 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 eigene benutzerdefinierte Effekte mit hlsl-Standardeffekten schreiben. Sie können diese benutzerdefinierten Effekte zusammen mit den integrierten Effekten verwenden, die in Direct2D integriert sind.
Beispiele für einen vollständigen Pixel-, Scheitelpunkt- 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ändigen benutzerdefinierten Effekts benötigen.
Einführung: Was ist innerhalb eines Effekts?

Konzeptionell führt ein Direct2D-Effekt eine Bildverarbeitungsaufgabe aus, z. B. das Ändern 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 Betrieb steuern, und ein einzelnes Ausgabebild generieren.
Es gibt vier verschiedene Teile eines benutzerdefinierten Effekts, für die ein Effektautor verantwortlich ist:
- Effect Interface: 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 eigentlichen Bildbearbeitungsvorgänge enthält.
- Transformationsdiagramm: Jeder Effekt erstellt ein internes Transformationsdiagramm, das aus einzelnen Transformationen besteht. Jede Transformation stellt einen einzelnen Bildvorgang dar. Der Effekt ist dafür verantwortlich, diese Transformationen in einem Diagramm zu verknüpfen, um den beabsichtigten Bildverarbeitungseffekt zu erzielen. Ein Effekt kann Transformationen als Reaktion auf Änderungen an den externen Eigenschaften des Effekts hinzufügen, entfernen, ändern und neu anordnen.
- Transformieren: Eine Transformation stellt einen einzelnen Bildvorgang dar. Der Hauptzweck besteht in der Verwendung der Shader, die für jedes Ausgabepixel ausgeführt werden. Zu diesem Zweck ist er für die Berechnung der neuen Größe des Ausgabebilds basierend auf der Logik in den Shadern verantwortlich. Außerdem muss berechnet werden, aus welchem Bereich des Eingabebilds die Shader lesen müssen, um den angeforderten Ausgabebereich zu rendern.
- Shader: Ein Shader wird für die Eingabe der Transformation auf der GPU ausgeführt (oder CPU, wenn Softwarerendering angegeben wird, wenn die App das Direct3D-Gerät erstellt). Effekt-Shader werden in der High Level Shading Language(HLSL)geschrieben und während der Kompilierung des Effekts in Bytecode kompiliert, der dann während der Laufzeit durch den Effekt geladen wird. In diesem Referenzdokument wird beschrieben, wie Sie Mit Direct2Dkompatibles HLSL schreiben. 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, die den benutzerdefinierten Effekt für die Verwendung mit Direct2Dregistrieren.
Nachdem alle Komponenten für eine Effektschnittstelle implementiert wurden, wird der Header der Klasse wie im folgenden Beispiel 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 durchzufü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 haben, unterstützen andere Wie der Composite-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 variable Eingabeanzahl 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, um vorgänge als Reaktion auf externe Änderungen durchzuführen. Direct2D ruft diese Methode unmittelbar vor dem Rendern eines Effekts auf, wenn mindestens eine der folgenden Bedingungen zutrifft:
- Der Effekt wurde zuvor initialisiert, aber noch nicht gezeichnet.
- Eine Effect-Eigenschaft wurde seit dem letzten Zeichnen-Aufruf geändert.
- Der Zustand des aufrufenden Direct2D-Kontexts (wie DPI) hat sich seit dem letzten Zeichnen-Aufruf geändert.
Implementieren der Effektregistrierungs- und Rückrufmethoden
Apps müssen Effekte bei Direct2D registrieren, bevor sie instanziiert werden. Diese Registrierung gilt für eine Instanz einer Direct2D-Factory 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 Instanz 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 denselben , um den Effekt zu identifizieren, wenn sie ID2D1DeviceContext::CreateEffect aufruft.
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.
// 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, damit die App aufruft, um den Effekt bei Direct2D zu registrieren. Da die Effect-Registrierung für eine Instanz 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 ID2D1Factory1-Parameter 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 werden dagegen 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 Effect Factory-Rückrufmethode
Der Effekt muss auch eine private Rückrufmethode bereitstellen, die eine Instanz des Effekts über einen einzelnen IUnknown-Parameter * * zurückgibt. Ein Zeiger auf diese Methode wird für Direct2D bereitgestellt, wenn der Effekt über die ID2D1Factory1::RegisterEffectFromString-API über den PD2D1 _ EFFECT _ FACTORY-Parameter 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 für die Kompatibilität mit COM implementieren.
Erstellen des Transformationsdiagramms des Effekts
Ein Effekt kann mehrere verschiedene Transformationen (einzelne Bildvorgänge) verwenden, um den gewünschten Bildverarbeitungseffekt zu erzielen. Um die Reihenfolge zu steuern, in der diese Transformationen auf das Eingabebild angewendet werden, ordnet der Effekt sie in einem Transformationsdiagramm an. Ein Transformationsdiagramm kann die Effekte und Transformationen, die in Direct2D enthalten sind, sowie benutzerdefinierte Transformationen nutzen, die vom Effektautor erstellt wurden.
Verwenden von Transformationen, die in Direct2D enthalten sind
Dies sind die am häufigsten verwendeten Transformationen, die mit Direct2D bereitgestellt werden.
- ID2D1BlendTransform:Diese Transformation kombiniert eine beliebige Anzahl von Eingaben basierend auf einer Blendbeschreibung. Weitere Informationen finden Sie im Thema D2D1 _ BLEND _ DESCRIPTION und im Beispiel für zusammengesetzte Direct2D-Effektmodi. Diese Transformation wird von der ID2D1EffectContext::CreateBlendTransform-Methode zurückgegeben.
- ID2D1BorderTransform:Diese Transformation erweitert die Ausgabegröße eines Bilds entsprechend dem angegebenen D2D1 _ EXTEND _ MODE. Diese Transformation wird von der ID2D1EffectContext::CreateBorderTransform-Methode zurückgegeben.
- ID2D1OffsetTransform:Diese Transformation versetzt (übersetzt) ihre Eingabe um eine beliebige Anzahl von Pixeln. Diese Transformation wird von der ID2D1EffectContext::CreateOffsetTransform-Methode zurückgegeben.
- Jeder vorhandene Effekt kann mithilfe der ID2D1EffectContext::CreateTransformNodeFromEffect-Methode als Transformation für die Erstellung eines Transformationsdiagramms behandelt 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 sein. Wenn ein Effekt nur eine einzelne Transformation enthält, können Sie die ID2D1TransformGraph::SetSingleTransformNode-Methode verwenden, um dies einfach 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, bei 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 benutzerdefinierte Eigenschaftsrückrufmethode.
Hier wird eine Initialize-Beispielmethode gezeigt. Diese Methode erstellt ein Einzelknoten-Transformationsdiagramm, das das Bild auf jeder Achse um 100 Pixel 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
Durch das Hinzufügen mehrerer Transformationen zum Transformationsdiagramm eines Effekts können Effekte intern mehrere Bildvorgänge ausführen, die einer App als einzelner einheitlicher Effekt präsentiert werden.
Wie bereits erwähnt, kann das Transformationsdiagramm des Effekts in jeder Effect-Methode 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 hat, kann nur eine einzelne Transformation als "Ausgabeknoten" festgelegt werden.
Dieser Code verwendet zwei separate Transformationen, um einen einheitlichen Effekt zu erstellen. In diesem Fall ist der Effekt ein übersetzter Schatten.
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 zum Definieren einer Eigenschaft für einen benutzerdefinierten Effekt:
Hinzufügen der Eigenschaftsmetadaten zu den Registrierungsdaten des Effekts
Hinzufügen einer Eigenschaft zu Registrierungs-XML
Sie müssen die Eigenschaften eines benutzerdefinierten Effekts während der ersten Registrierung des Effekts bei Direct2Ddefinieren. 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 Effect-Eigenschaft in XML definieren, benötigt sie einen Namen, einen Typ und einen Anzeigenamen. Der Anzeigename einer Eigenschaft sowie die Kategorie-, Autor- und Beschreibungswerte des Gesamteffekts können und sollten lokalisiert werden.
Für jede Eigenschaft kann ein Effekt optional Standardwerte, Mindest- und Höchstwerte angeben. Diese Werte dienen nur zur Informationsverwendung. Sie werden nicht von Direct2Derzwungen. Es liegt an Ihnen, jede angegebene Standard-/Min/Max-Logik in der Effect-Klasse 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. Entsprechende 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 _ MODEusw.) | enum |
Zuordnen der neuen Eigenschaft zu Getter- und Settermethoden
Als Nächstes muss der Effekt diese neue Eigenschaft getter- und setter-Methoden zuordnen. Dies erfolgt über das D2D1 _ PROPERTY _ BINDING-Array, das an die ID2D1Factory1::RegisterEffectFromString-Methode übergeben wird.
Das D2D1 _ PROPERTY _ BINDING-Array 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 es 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 Effect-Klasse von ID2D1EffectImpl vor jeder anderen Schnittstelle erbt.
Benutzerdefinierte Eigenschaften für einen Effekt werden in der Reihenfolge indiziert, in der sie im XML-Code deklariert sind. Nach der Erstellung kann die App mithilfe der Methoden ID2D1Properties::SetValue und ID2D1Properties::GetValue darauf zugreifen. Der Einfachheit halber 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 Gettermethode ü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 eine Eigenschaftsänderung
Um die Bildausgabe eines Effekts als Reaktion auf eine Eigenschaftsänderung tatsächlich zu aktualisieren, muss der Effekt seine zugrunde liegenden Transformationen ändern. Dies erfolgt 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 jeder der Methoden des Effekts aktualisiert werden, z. B. Initialize oder den Eigenschaftensettermethoden des Effekts.
Wenn ein Effekt beispielsweise eine ID2D1OffsetTransform enthielt und seinen Offsetwert als Reaktion auf die Änderung der Offset-Eigenschaft des Effekts ändern möchte, würde er den folgenden Code in PrepareForRenderhinzufügen:
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
Um Imagevorgänge zu implementieren, die über die in Direct2Dbereitgestellten Hinausgehenden hinausgehen, müssen Sie benutzerdefinierte Transformationen implementieren. Benutzerdefinierte Transformationen können ein Eingabebild beliebig ändern, indem benutzerdefinierte HLSL-Shader verwendet werden.
Transformationen implementieren abhängig von den verwendeten Shadertypen eine von zwei verschiedenen Schnittstellen. Transformationen mit Pixel- und/oder Vertex-Shadern müssen ID2D1DrawTransformimplementieren, während Transformationen mit Compute-Shadern ID2D1ComputeTransformimplementieren 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 zum Implementieren:
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 an die Transformation darstellt. Die Transformation ist dann für die Berechnung der Begrenzungen des Ausgabebilds verantwortlich. Die Größe der Rechtecke für alle Methoden auf dieser Schnittstelle (ID2D1Transform) wird in Pixel und nicht in DIPs definiert.
Diese Methode ist auch für die Berechnung des Ausgabebereichs verantwortlich, der nicht transparent ist, basierend auf der Logik des Shaders und den nicht transparenten Bereichen der einzelnen Eingaben. Ein nicht transparenter Bereich eines Bilds ist so definiert, dass der Alphakanal für das gesamte Rechteck "1" ist. Wenn unklar ist, ob die Ausgabe einer Transformation nicht transparent ist, sollte das nicht transparente Ausgaberechteck als sicherer Wert auf (0, 0, 0, 0) festgelegt werden. Direct2D verwendet diese Informationen, um Renderingoptimierungen mit garantiert nicht transparentem 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 Graphlayout selbst hier ä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;
}
Betrachten Sie für ein komplexeres Beispiel, wie ein einfacher Weichdevorgang dargestellt wird:
Wenn ein Weichdevorgang einen Radius von 5 Pixeln verwendet, 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-/Unterflüsse 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 unscharf ist, ist ein nicht transparenter Bereich des Bilds jetzt möglicherweise teilweise transparent. 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 opaken Ausgaberechteckberechnungen 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:

Weitere Informationen zu dieser Methode finden Sie auf der Referenzseite zu MapInputRectsToOutputRect.
MapOutputRectToInputRects
Direct2D ruft die MapOutputRectToInputRects-Methode nach MapInputRectsToOutputRect auf. Die Transformation muss berechnen, aus welchem Teil des Bilds gelesen werden muss, um den angeforderten Ausgabebereich ordnungsgemäß zu rendern.
Wenn ein Effekt wie zuvor die Pixel 1 bis 1 genau zupasst, kann er das Ausgaberechteck an das Eingaberechteck übergeben:
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;
}
Ebenso verwenden Pixel, wenn eine Transformation ein Bild verkleinert oder erweitert (wie im Beispiel hier weicher), häufig umgebende Pixel, um ihren Wert zu berechnen. Bei einem Weichbild wird ein Pixel mit seinen umgebenden Pixeln gemittelt, auch wenn sie sich außerhalb der Grenzen des Eingabebilds befinden. Dieses Verhalten wird in der Berechnung widergespiegelt. Wie zuvor sucht die Transformation beim Erweitern der Koordinaten eines Rechtecks nach Überläufen.
// 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 nimmt automatisch Stichproben von transparenten schwarzen Pixeln, wenn das Eingabebild nicht vorhanden ist, sodass der Weichblender schrittweise mit dem vorhandenen Inhalt auf dem Bildschirm kombiniert werden kann.

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 Direct2D es zu einem bestimmten Zeitpunkt aufruft. Diese Methode entscheidet konzeptionell, welcher Teil der Ausgabe einer Transformation erneut gerendert werden muss, wenn sich ein Teil oder die ganze Eingabe ändert. Es gibt drei verschiedene Szenarien, für die das ungültige Rect einer Transformation berechnet werden soll.
Transformationen mit 1:1-Pixelzuordnung
Übergeben Sie für Transformationen, die Pixel 1 bis 1 zuordnen, 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 n:n-Pixelzuordnung
Wenn die Ausgabepixel einer Transformation von ihrer Umgebung abhängig sind, muss das ungültige Eingaberechteck entsprechend erweitert werden. Dies soll widerspiegeln, dass Pixel, die das ungültige Eingaberechteck umgeben, ebenfalls betroffen sind und ungültig werden. Beispielsweise wird für eine Weichansicht mit 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 aus gibt, ä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 klammert 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 Pixel-Shaders zu einer benutzerdefinierten Transformation
Nachdem eine Transformation erstellt wurde, muss sie einen Shader bereitstellen, der die Bildpixel bearbeitet. In diesem Abschnitt werden die Schritte zur Verwendung eines Pixel-Shaders mit einer benutzerdefinierten Transformation beschrieben.
Implementieren von ID2D1DrawTransform
Um einen Pixel-Shader zu verwenden, muss die Transformation die ID2D1DrawTransform-Schnittstelle implementieren, die von der in Abschnitt 5 beschriebenen ID2D1Transform-Schnittstelle erbt. Diese Schnittstelle enthält eine neue Methode, die implementiert werden soll:
SetDrawInfo(ID2D1DrawInfo * pDrawInfo)
Direct2D ruft die SetDrawInfo-Methode auf, wenn die Transformation dem Transformationsdiagramm eines Effekts zum ersten Mal hinzugefügt wird. Diese Methode bietet einen ID2D1DrawInfo-Parameter, der steuert, wie die Transformation gerendert wird. Die hier verfügbaren Methoden finden Sie im Thema ID2D1DrawInfo.
Wenn die Transformation diesen Parameter als Klassen membervariable speichern möchte, kann auf das drawInfo-Objekt von anderen Methoden wie Eigenschaftensettern oder MapInputRectsToOutputRectzugegriffen und geändert werden. Insbesondere kann sie nicht von den MapOutputRectToInputRects- oder MapInvalidRect-Methoden in ID2D1Transform aufgerufen werden.
Erstellen einer GUID für den Pixel-Shader
Als Nächstes muss die Transformation eine eindeutige GUID für den Pixel-Shader selbst definieren. Dies wird verwendet, wenn Direct2D den Shader in den Arbeitsspeicher lädt und wenn die Transformation auswählt, welcher Pixel-Shader 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 Pixel-Shaders mit Direct2D
Ein Pixel-Shader muss in den Arbeitsspeicher geladen werden, bevor er von der Transformation verwendet werden kann.
Um den Pixel-Shader in den Arbeitsspeicher zu laden, sollte die Transformation den kompilierten Shader-Bytecode aus dem lesen. Die von Visual Studio generierte CSO-Datei (Details finden Sie in der Direct3D-Dokumentation) in einem Bytearray. Diese Technik wird im D2DCustomEffects SDK-Beispiel ausführlich erläutert.
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 der gleichen GUID geladen wurde.
Nachdem ein Pixelshader in den Arbeitsspeicher geladen wurde, muss die Transformation ihn für die Ausführung auswählen, indem er seine GUID an die SetPixelShader-Methode für den PARAMETER ID2D1DrawInfo übergibt, der während der SetDrawInfo-Methode bereitgestellt wird. Der Pixel-Shader muss bereits in den Arbeitsspeicher geladen werden, bevor er für die Ausführung ausgewählt wird.
Ändern des Shader-Vorgangs mit konstanten Puffern
Um die Ausführung eines Shaders zu ändern, kann eine Transformation einen konstanten Puffer an den Pixel-Shader übergeben. Zu diesem Ziel 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.
HlSL muss auch eine entsprechende Struktur definieren, die den konstanten Puffer 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 in enthaltenen Werte von überall innerhalb des Pixel-Shaders gelesen werden.
Schreiben eines Pixel-Shaders für Direct2D
Direct2D-Transformationen verwenden Shader, die mit hlsl-Standard-. Es gibt jedoch einige wichtige Konzepte zum Schreiben eines Pixel-Shaders, der aus dem Kontext einer Transformation ausgeführt wird. Ein vollständiges Beispiel für einen voll funktionsfähigen Pixel-Shader finden Sie im D2DCustomEffects SDK-Beispiel.
Direct2D ordnet die Eingaben einer Transformation automatisch Texture2D- und SamplerState-Objekten im HLSL zu. Das erste Texture2D befindet sich im Register t0, und der erste SamplerState befindet sich unter register s0. Jede zusätzliche Eingabe befindet sich in 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 entnommen werden.
Ein benutzerdefinierter Pixel-Shader wird einmal für jedes gerenderte Pixel ausgeführt. Jedes Mal, wenn der Shader ausgeführt wird, stellt Direct2D automatisch drei Parameter zur Identifizierung seiner aktuellen Ausführungsposition zur Lage:
- Szenenbereichsausgabe: Dieser Parameter stellt die aktuelle Ausführungsposition in Bezug auf die gesamte Zieloberfläche dar. Sie wird in Pixel definiert, und ihre Min/Max-Werte entsprechen den Grenzen des Rechtecks, das von MapInputRectsToOutputRect zurückgegeben wird.
- Clip-Space-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 übernehmen, wie dieser Wert berechnet wird. Es sollte nur verwendet werden, um die Eingabe des Pixels shaders zu verwenden, 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 Vertex-Shaders zu einer benutzerdefinierten Transformation
Sie können Vertex-Shader verwenden, um andere Bildverarbeitungsszenarien als Pixel-Shader zu erreichen. Insbesondere vertex-Shader können geometriebasierte Bildeffekte ausführen, indem Scheitelpunkte transformiert werden, aus denen ein Bild besteht. Vertex-Shader können unabhängig von oder in Verbindung mit transformationsspezifischen Pixel-Shadern verwendet werden. Wenn kein Vertex-Shader angegeben ist, ersetzt Direct2D in einem Standard-Vertex-Shader die Verwendung mit dem benutzerdefinierten Pixel-Shader.
Der Prozess zum Hinzufügen eines Vertex-Shaders zu einer benutzerdefinierten Transformation ähnelt dem eines Pixel-Shaders. Die Transformation implementiert die ID2D1DrawTransform-Schnittstelle, erstellt eine GUID und übergibt (optional) konstante Puffer an den Shader. Es gibt jedoch einige wichtige zusätzliche Schritte, die für Vertex-Shader eindeutig sind:
Erstellen eines Scheitelpunktpuffers
Ein Vertex-Shader wird definitionsgemäß auf Scheitelpunkten ausgeführt, die ihm übergeben werden, nicht auf einzelnen Pixeln. Um die Scheitelpunkte anzugeben, auf die der Shader ausgeführt werden soll, erstellt eine Transformation einen Scheitelpunktpuffer, der an den Shader übergeben werden soll. Das Layout von Scheitelpunktpuffern geht über den Rahmen dieses Dokuments hinaus. Weitere Informationen finden Sie in der Direct3D-Referenz oder im D2DCustomEffects SDK-Beispiel für eine Beispielimplementierung.
Nach dem Erstellen eines Scheitelpunktpuffers 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 Beispielimplementierungen finden Sie auch hier im D2DCustomEffects SDK-Beispiel.
Wenn von der Transformation kein Scheitelpunktpuffer angegeben wird, übergibt Direct2D einen Standardvertexpuffer, der die rechteckige Bildposition darstellt.
Ändern von SetDrawInfo zur Verwendung eines Vertex-Shaders
Wie bei Pixel-Shadern muss die Transformation geladen werden und einen Vertex-Shader für die Ausführung auswählen. Um den Vertexshader zu laden, ruft er die LoadVertexShader-Methode für die ID2D1EffectContext-Methode auf, die in der Initialize-Methode des Effekts empfangen wurde. Um den Vertex-Shader für die Ausführung auszuwählen, ruft er SetVertexProcessing für den ID2D1DrawInfo-Parameter auf, der in der SetDrawInfo-Methode der Transformation empfangen wurde. Diese Methode akzeptiert eine GUID für einen zuvor geladenen Vertex-Shader sowie (optional) einen zuvor erstellten Scheitelpunktpuffer für die Ausführung des Shaders.
Implementieren eines Direct2D-Vertex-Shaders
Eine Draw-Transformation kann sowohl einen Pixelshader als auch einen Vertex-Shader enthalten. Wenn eine Transformation sowohl einen Pixelshader als auch einen Vertex-Shader definiert, wird die Ausgabe des Vertex-Shaders direkt an den Pixelshader übergeben: Die App kann die Rückgabesignatur des Vertex-Shaders bzw. die Parameter des Pixelshader anpassen, solange sie konsistent sind.
Wenn eine Transformation dagegen nur einen Vertex-Shader enthält und auf dem Standardmäßigen Pass-Through-Pixelshader von Direct2Dbasiert, muss die folgende Standardausgabe zurückgegeben werden:
struct VSOut
{
float4 clipSpaceOutput : SV_POSITION;
float4 sceneSpaceOutput : SCENE_POSITION;
float4 texelSpaceInput0 : TEXCOORD0;
};
Ein Vertex-Shader speichert das Ergebnis seiner Scheitelpunkttransformationen in der Ausgabevariable Scene-space des Shaders. Um die Clip-Space-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;
};
Ein Beispielcode für vertex-Shader finden Sie unten, der die Konvertierungsmatrizen verwendet, um die richtigen Clip- und Texelräume zu berechnen, die von Direct2Derwartet 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 Vertex-Shader verwendet werden. Es durchläuft lediglich das Eingabebild, ohne Transformationen durchzuführen. Eine vollständig implementierte Vertex-Shader-basierte Transformation finden Sie auch hier im D2DCustomEffects SDK-Beispiel.
Wenn von der Transformation kein Scheitelpunktpuffer 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 ändert die Parameter sceneSpaceOutput und clipSpaceOutput möglicherweise nicht. Sie müssen unverändert zurückgegeben werden. Sie kann jedoch die texelSpaceInput-Parameter für jedes Eingabebild ändern. Wenn die Transformation auch einen benutzerdefinierten Pixelshader enthält, kann der Vertex-Shader weitere 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 willkürlichen Zugriff auf Eingabe- und Ausgabebildpuffer erfordern. Beispielsweise kann ein grundlegender Histogrammalgorithmus aufgrund von Einschränkungen beim Speicherzugriff nicht mit einem Pixel-Shader implementiert werden.
Da Compute-Shader höhere Hardwarefeatureanforderungen als Pixel-Shader haben, sollten Pixel-Shader nach Möglichkeit verwendet werden, um einen bestimmten Effekt zu implementieren. Insbesondere werden Compute-Shader nur auf den meisten DirectX 10-Karten und höher ausgeführt. Wenn sich eine Transformation für die Verwendung eines Compute-Shaders entscheidet, muss sie während der Instanziierung zusätzlich zur Implementierung der ID2D1ComputeTransform-Schnittstelle die entsprechende Hardwareunterstützung überprüfen.
Ü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 prüfen. Wenn die GPU keine Compute-Shader unterstützt, muss der Effekt D2DERR _ INSUFFICIENT _ DEVICE _ CAPABILITIESzurü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. Ausführliche Informationen finden Sie in der Direct3D-Dokumentation. Transformationen können beide Shadertypen enthalten und bei Bedarf auf Shadermodell 4 zurückgreifen. Eine Entsprechende Implementierung finden Sie im D2DCustomEffects SDK-Beispiel.
Implementieren von ID2D1ComputeTransform
Diese Schnittstelle enthält zwei neue Methoden, die zusätzlich zu den methoden in ID2D1Transformimplementiert werden müssen:
SetComputeInfo(ID2D1ComputeInfo * pComputeInfo)
Wie bei Pixel- und Vertex-Shadern 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. Dies schließt die Auswahl des Computeshaders ein, der über die ID2D1ComputeInfo::SetComputeShader-Methode ausgeführt werden soll. Wenn die Transformation diesen Parameter als Klassenmembervariable speichert, kann mit Ausnahme der Methoden MapOutputRectToInputRects und MapInvalidRect von jeder Transformations- oder Effektmethode darauf zugegriffen und geändert werden. Weitere Methoden finden Sie hier im Thema ID2D1ComputeInfo.
CalculateThreadgroups(ID2D1ComputeInfo * pOutputRect, UINT32 * pDimensionX, UINT32 * pDimensionY, UINT32 * pDimensionZ)
Während Pixel-Shader pro Pixel und Vertex-Shader pro Scheitelpunkt 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 Compute-Shaders entscheidet, wie viele Threads pro Threadgruppe ausgeführt werden sollen. Der Effekt skaliert die Anzahl der Threadgruppen so, dass der Shader die gewünschte Anzahl von Ausführungen ausführt, abhängig von der Logik des Shaders.
Mit der CalculateThreadgroups-Methode kann die Transformation Direct2D darüber informieren, wie viele Threadgruppen erforderlich sind, basierend auf der Größe des Bilds und dem eigenen Wissen der Transformation über den Shader.
Die Anzahl der Ausführungen 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, die der Shader (3,3,1) Threads pro Threadgruppe angibt, werden vier Threadgruppen mit jeweils 9 Threads für insgesamt 36 Threadinstanzen ausgeführt.
Ein gängiges Szenario ist die Verarbeitung eines Ausgabepixels für jede Instanz 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 Anmerkung "numthreads" 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.) mit jedem Thread ein einzelnes Pixel berechnet, würde der Code der Methode wie folgt aussehen.
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 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 Pixel-Shadern ist jedoch nicht garantiert, dass die Daten des Bilds bei (0, 0) in der Textur beginnen. Stattdessen stellt Direct2D Systemkonstanten bereit, mit denen Shader offsetkompensiert werden 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 oben genannte Konstantenpuffer und die Hilfsmethode definiert wurden, kann der Shader Bilddaten wie folgt abtasten:
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 für das resultierende Bild definiert, das platziert werden soll. In Shader Model 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 nacheinander 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;
Auf der anderen Seite können ShaderModell 5-Shader (DirectX 11) zweidimensionale Ausgabetexturen verwenden:
RWTexture2D<float4> OutputTexture : register(t1);
Mit ShaderModell 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 abgeschlossener Pass-Through-Shadermodell 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 einen 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;
}