Delen via


Aangepaste effecten

Direct2D wordt geleverd met een bibliotheek met effecten die verschillende algemene afbeeldingsbewerkingen uitvoeren. Zie het ingebouwde effecten onderwerp voor de volledige lijst met effecten. Voor functionaliteit die niet kan worden bereikt met de ingebouwde effecten, kunt u met Direct2D uw eigen aangepaste effecten schrijven met standaard HLSL-. U kunt deze aangepaste effecten naast de ingebouwde effecten gebruiken die met Direct2D worden verzonden.

Zie de D2DCustomEffects SDK-voorbeeld-voor voorbeelden van een volledige pixel-, hoekpunt- en rekenschaduwingseffect.

In dit onderwerp laten we u de stappen en concepten zien die u nodig hebt om een volledig aangepast effect te ontwerpen en te maken.

Inleiding: Wat zit er in een effect?

slagschaduweffectdiagram.

Conceptueel gezien voert een Direct2D--effect een afbeeldingstaak uit, zoals het wijzigen van helderheid, het desatureren van een afbeelding of zoals hierboven wordt weergegeven, waardoor een slagschaduw ontstaat. Voor de app zijn ze eenvoudig. Ze kunnen nul of meer invoerafbeeldingen accepteren, meerdere eigenschappen weergeven die hun bewerking beheren en één uitvoerafbeelding genereren.

Er zijn vier verschillende onderdelen van een aangepast effect waarvoor een effectauteur verantwoordelijk is:

  1. Effectinterface: De effectinterface definieert conceptueel hoe een app communiceert met een aangepast effect (zoals hoeveel invoer het effect accepteert en welke eigenschappen beschikbaar zijn). De effectinterface beheert een transformatiegrafiek, die de werkelijke imaging-bewerkingen bevat.
  2. Transformatiegrafiek: Elk effect maakt een interne transformatiegrafiek die bestaat uit afzonderlijke transformaties. Elke transformatie vertegenwoordigt één afbeeldingsbewerking. Het effect is verantwoordelijk voor het koppelen van deze transformaties aan een grafiek om het beoogde imaging-effect uit te voeren. Een effect kan transformaties toevoegen, verwijderen, wijzigen en opnieuw ordenen als reactie op wijzigingen in de externe eigenschappen van het effect.
  3. Transformeren: Een transformatie vertegenwoordigt één afbeeldingsbewerking. Het belangrijkste doel is het huis van de shaders die worden uitgevoerd voor elke uitvoer pixel. Daartoe is het verantwoordelijk voor het berekenen van de nieuwe grootte van de uitvoerafbeelding op basis van logica in de shaders. Ook moet worden berekend van welk gebied van de invoerafbeelding de shaders moeten lezen om de aangevraagde uitvoerregio weer te geven.
  4. Shader: Er wordt een shader uitgevoerd op basis van de invoer van de transformatie op de GPU (of CPU als softwarerendering wordt opgegeven wanneer de app het Direct3D-apparaat maakt). Effect-shaders worden geschreven in arceringstaal op hoog niveau (HLSL-) en worden gecompileerd in bytecode tijdens de compilatie van het effect, die vervolgens tijdens runtime door het effect wordt geladen. In dit referentiedocument wordt beschreven hoe u Direct2D--compatibele HLSL schrijft. De Direct3D-documentatie bevat een eenvoudig HLSL-overzicht.

Een effectinterface maken

De effectinterface definieert hoe een app communiceert met het aangepaste effect. Als u een effectinterface wilt maken, moet een klasse ID2D1EffectImpl implementeren, metagegevens definiëren die het effect beschrijven (zoals de naam, het aantal invoer en eigenschappen) en methoden maken waarmee het aangepaste effect wordt geregistreerd voor gebruik met Direct2D-.

Zodra alle onderdelen voor een effectinterface zijn geïmplementeerd, wordt de header van de klasse als volgt weergegeven:

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

ID2D1EffectImpl implementeren

De ID2D1EffectImpl-interface bevat drie methoden die u moet implementeren:

Initialize(ID2D1EffectContext *pContextInternal, ID2D1TransformGraph *pTransformGraph)

Direct2D- roept de methode initialiseren aan nadat de methode ID2D1DeviceContext::CreateEffect door de app is aangeroepen. U kunt deze methode gebruiken om interne initialisatie uit te voeren of andere bewerkingen die nodig zijn voor het effect. Daarnaast kunt u deze gebruiken om de eerste transformatiegrafiek van het effect te maken.

SetGraph(ID2D1TransformGraph *pTransformGraph)

Direct2D- roept de methode SetGraph aan wanneer het aantal invoergegevens voor het effect wordt gewijzigd. Hoewel de meeste effecten een constant aantal invoer hebben, ondersteunen anderen, zoals het samengestelde effect een variabel aantal invoerwaarden. Met deze methode kunnen deze effecten hun transformatiegrafiek bijwerken als reactie op een veranderend aantal invoer. Als een effect geen ondersteuning biedt voor het aantal variabeleninvoer, kan deze methode gewoon E_NOTIMPL retourneren.

PrepareForRender (D2D1_CHANGE_TYPE changeType)

De methode PrepareForRender biedt effecten om bewerkingen uit te voeren als reactie op externe wijzigingen. Direct2D- roept deze methode aan net voordat deze een effect weergeeft als ten minste één van deze waar is:

  • Het effect is eerder geïnitialiseerd, maar nog niet getekend.
  • Een effecteigenschap is gewijzigd sinds de laatste tekenaanroep.
  • De status van de aanroepende Direct2D context (zoals DPI) is gewijzigd sinds de laatste tekenoproep.

De effectregistratie- en callback-methoden implementeren

Apps moeten effecten registreren bij Direct2D- voordat ze worden geïnstrigeerd. Deze registratie is gericht op een exemplaar van een Direct2D-factory en moet worden herhaald telkens wanneer de app wordt uitgevoerd. Als u deze registratie wilt inschakelen, definieert een aangepast effect een unieke GUID, een openbare methode waarmee het effect wordt geregistreerd en een private callback-methode die een exemplaar van het effect retourneert.

Een GUID definiëren

U moet een GUID definiëren waarmee het effect voor registratie met Direct2D-uniek wordt geïdentificeerd. De app gebruikt hetzelfde om het effect te identificeren wanneer deze ID2D1DeviceContext::CreateEffectaanroept.

Deze code demonstreert het definiëren van een dergelijke GUID voor een effect. U moet een eigen unieke GUID maken met behulp van een hulpprogramma voor het genereren van guid's, zoals 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);

Een openbare registratiemethode definiëren

Definieer vervolgens een openbare methode voor de app om het effect te registreren bij Direct2D-. Omdat effectregistratie specifiek is voor een exemplaar van een Direct2D-factory, accepteert de methode een ID2D1Factory1 interface als parameter. Als u het effect wilt registreren, roept de methode vervolgens de ID2D1Factory1::RegisterEffectFromString-API aan op de parameter ID2D1Factory1.

Deze API accepteert een XML-tekenreeks die de metagegevens, invoer en eigenschappen van het effect beschrijft. De metagegevens voor een effect zijn alleen bedoeld ter informatie en kunnen door de app worden opgevraagd via de interface ID2D1Properties. De invoer- en eigenschapsgegevens worden daarentegen gebruikt door Direct2D- en vertegenwoordigt de functionaliteit van het effect.

Hier ziet u een XML-tekenreeks voor een minimaal voorbeeldeffect. Het toevoegen van aangepaste eigenschappen aan de XML wordt behandeld in de sectie Aangepaste eigenschappen toevoegen aan een effectsectie.

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

Een callback-methode voor effectfactory definiëren

Het effect moet ook een privé callback-methode bieden die een exemplaar van het effect retourneert via één IUnknown**-parameter. Een aanwijzer naar deze methode wordt verstrekt aan Direct2D- wanneer het effect is geregistreerd via de ID2D1Factory1::RegisterEffectFromString-API via de parameter PD2D1_EFFECT_FACTORY\.

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

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

    return S_OK;
}

De IUnknown-interface implementeren

Ten slotte moet het effect de IUnknown-interface implementeren voor compatibiliteit met COM.

De transformatiegrafiek van het effect maken

Een effect kan verschillende transformaties (afzonderlijke afbeeldingsbewerkingen) gebruiken om het gewenste imaging-effect te maken. Als u de volgorde wilt bepalen waarin deze transformaties worden toegepast op de invoerafbeelding, rangschikt het effect deze in een transformatiegrafiek. Een transformatiegrafiek kan gebruikmaken van de effecten en transformaties die zijn opgenomen in Direct2D- en aangepaste transformaties die zijn gemaakt door de auteur van het effect.

Transformaties gebruiken die zijn opgenomen in Direct2D

Dit zijn de meestgebruikte transformaties die worden geleverd met Direct2D-.

Een transformatiegrafiek met één knooppunt maken

Zodra u een transformatie hebt gemaakt, moet de invoer van het effect zijn verbonden met de invoer van de transformatie en moet de uitvoer van de transformatie worden verbonden met de uitvoer van het effect. Wanneer een effect slechts één transformatie bevat, kunt u de methode ID2D1TransformGraph::SetSingleTransformNode gebruiken om dit eenvoudig te doen.

U kunt een transformatie maken of wijzigen in de Initialiseer of SetGraph methoden met behulp van de opgegeven parameter ID2D1TransformGraph. Als een effect wijzigingen moet aanbrengen in de transformatiegrafiek in een andere methode waarbij deze parameter niet beschikbaar is, kan het effect de parameter ID2D1TransformGraph opslaan als lidvariabele van de klasse en deze ergens anders openen, zoals PrepareForRender of een callback-methode voor aangepaste eigenschappen.

Hier ziet u een voorbeeld van initialiseren methode. Met deze methode maakt u een transformatiegrafiek met één knooppunt die de afbeelding met honderd pixels in elke as verschoven.

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

Een transformatiegrafiek met meerdere knooppunten maken

Door meerdere transformaties toe te voegen aan een transformatiegrafiek van een effect, kunnen effecten intern meerdere afbeeldingsbewerkingen uitvoeren die aan een app worden gepresenteerd als één geïntegreerd effect.

Zoals hierboven vermeld, kan de transformatiegrafiek van het effect worden bewerkt in elke effectmethode met behulp van de parameter ID2D1TransformGraph die is ontvangen in de methode Initialize van het effect. De volgende API's op die interface kunnen worden gebruikt om de transformatiegrafiek van een effect te maken of te wijzigen:

AddNode(ID2D1TransformNode *pNode)

De AddNode methode registreert de transformatie met het effect en moet worden aangeroepen voordat de transformatie kan worden gebruikt met een van de andere transformatiegrafiekmethoden.

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

De methode ConnectToEffectInput verbindt de afbeeldingsinvoer van het effect met de invoer van een transformatie. Dezelfde effectinvoer kan worden verbonden met meerdere transformaties.

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

De methode ConnectNode verbindt de uitvoer van een transformatie met de invoer van een andere transformatie. Een transformatieuitvoer kan worden verbonden met meerdere transformaties.

SetOutputNode(ID2D1TransformNode *pNode)

De methode SetOutputNode verbindt de uitvoer van een transformatie met de uitvoer van het effect. Omdat een effect slechts één uitvoer heeft, kan slechts één transformatie worden aangewezen als het 'uitvoerknooppunt'.

Deze code maakt gebruik van twee afzonderlijke transformaties om een uniform effect te maken. In dit geval is het effect een vertaalde slagschaduw.

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

Aangepaste eigenschappen toevoegen aan een effect

Effecten kunnen aangepaste eigenschappen definiëren waarmee een app het gedrag van het effect tijdens runtime kan wijzigen. Er zijn drie stappen voor het definiëren van een eigenschap voor een aangepast effect:

De metagegevens van de eigenschap toevoegen aan de registratiegegevens van het effect

Eigenschap toevoegen aan registratie-XML

U moet de eigenschappen van een aangepast effect definiëren tijdens de initiële registratie van het effect met Direct2D-. Eerst moet u de registratie-XML van het effect bijwerken in de openbare registratiemethode met de nieuwe eigenschap:

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

Wanneer u een effecteigenschap in XML definieert, heeft deze een naam, een type en een weergavenaam nodig. De weergavenaam van een eigenschap, evenals de algemene waarden voor de categorie, auteur en beschrijving van het effect kunnen en moeten worden gelokaliseerd.

Voor elke eigenschap kan een effect optioneel standaardwaarden, minimumwaarden en maximumwaarden opgeven. Deze waarden zijn alleen bedoeld voor informatief gebruik. Ze worden niet afgedwongen door Direct2D-. Het is aan u om zelf een opgegeven standaard-/min/max-logica in de effectklasse te implementeren.

De typewaarde die in de XML voor de eigenschap wordt vermeld, moet overeenkomen met het bijbehorende gegevenstype dat wordt gebruikt door de getter- en settermethoden van de eigenschap. Overeenkomende XML-waarden voor elk gegevenstype worden weergegeven in deze tabel:

Gegevenstype Overeenkomende XML-waarde
PWSTR snaar
BOOL Bool
UINT uint32
INT int32
DRIJVEN drijven
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
Opsomming (D2D1_INTERPOLATION_MODE, enzovoort) opsomming

 

De nieuwe eigenschap toewijzen aan getter- en settermethoden

Vervolgens moet het effect deze nieuwe eigenschap toewijzen om methoden op te halen en in te stellen. Dit wordt gedaan via de D2D1_PROPERTY_BINDING-matrix die wordt doorgegeven aan de methode ID2D1Factory1::RegisterEffectFromString.

De D2D1_PROPERTY_BINDING matrix ziet er als volgt uit:

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

Nadat u de matrix XML en bindingen hebt gemaakt, geeft u deze door aan de methode RegisterEffectFromString:

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

Voor de D2D1_VALUE_TYPE_BINDING macro moet de effectklasse worden overgenomen van ID2D1EffectImpl vóór een andere interface.

Aangepaste eigenschappen voor een effect worden geïndexeerd in de volgorde waarin ze worden gedeclareerd in de XML en nadat deze zijn gemaakt, toegankelijk zijn voor de app met behulp van de methoden ID2D1Properties::SetValue en ID2D1Properties::GetValue methoden. Voor het gemak kunt u een openbare opsomming maken waarin elke eigenschap wordt vermeld in het headerbestand van het effect:

typedef enum SAMPLEEFFECT_PROP
{
    SAMPLEFFECT_PROP_OFFSET = 0
};

De getter- en setter-methoden voor de eigenschap maken

De volgende stap bestaat uit het maken van de getter- en setter-methoden voor de nieuwe eigenschap. De namen van de methoden moeten overeenkomen met de namen die zijn opgegeven in de D2D1_PROPERTY_BINDING matrix. Daarnaast moet het eigenschapstype dat is opgegeven in de XML van het effect overeenkomen met het type van de parameter van de settermethode en de retourwaarde van de getter-methode.

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

Transformaties van effect bijwerken als reactie op wijziging van eigenschap

Als u de afbeeldingsuitvoer van een effect daadwerkelijk wilt bijwerken als reactie op een wijziging in een eigenschap, moet het effect de onderliggende transformaties wijzigen. Dit wordt meestal gedaan in de PrepareForRender-methode van het effect die automatisch Direct2D aanroept wanneer een van de eigenschappen van een effect is gewijzigd. Transformaties kunnen echter worden bijgewerkt in een van de methoden van het effect: zoals Initialiseren of de methoden voor het instellen van eigenschappen van het effect.

Als een effect bijvoorbeeld een ID2D1OffsetTransform bevat en de offsetwaarde ervan wilde wijzigen als reactie op de offseteigenschap van het effect dat wordt gewijzigd, wordt de volgende code toegevoegd in PrepareForRender-:

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

    return S_OK;
}

Een aangepaste transformatie maken

Als u installatiekopieën wilt implementeren buiten wat wordt geleverd in Direct2D-, moet u aangepaste transformaties implementeren. Aangepaste transformaties kunnen een invoerafbeelding willekeurig wijzigen door gebruik te maken van aangepaste HLSL-shaders.

Transformaties implementeren een van twee verschillende interfaces, afhankelijk van de typen shaders die ze gebruiken. Transformaties met pixel- en/of hoekpunt-shaders moeten ID2D1DrawTransform-implementeren, terwijl transformaties met behulp van compute-shaders ID2D1ComputeTransform-moeten implementeren. Deze interfaces nemen beide over van ID2D1Transform. Deze sectie is gericht op het implementeren van de functionaliteit die gebruikelijk is voor beide.

De interface ID2D1Transform heeft vier methoden om te implementeren:

GetInputCount

Deze methode retourneert een geheel getal dat het invoeraantal voor de transformatie vertegenwoordigt.

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

MapInputRectsToOutputRect

Direct2D- roept de methode MapInputRectsToOutputRect aan telkens wanneer de transformatie wordt weergegeven. Direct2D geeft een rechthoek door die de grenzen van elke invoer aan de transformatie aangeeft. De transformatie is vervolgens verantwoordelijk voor het berekenen van de grenzen van de uitvoerafbeelding. De grootte van de rechthoeken voor alle methoden op deze interface (ID2D1Transform) worden gedefinieerd in pixels, niet in DIPs.

Deze methode is ook verantwoordelijk voor het berekenen van de regio van de uitvoer die ondoorzichtig is op basis van de logica van de arcering en de ondoorzichtige regio's van elke invoer. Een ondoorzichtig gebied van een afbeelding wordt gedefinieerd als het alfakanaal '1' is voor de gehele rechthoek. Als het onduidelijk is of de uitvoer van een transformatie ondoorzichtig is, moet de ondoorzichtige uitvoerrechthoek worden ingesteld op (0, 0, 0, 0) als een veilige waarde. Direct2D- gebruikt deze informatie om renderingoptimalisaties uit te voeren met 'gegarandeerde ondoorzichtige' inhoud. Als deze waarde onnauwkeurig is, kan dit leiden tot onjuiste rendering.

Tijdens deze methode kunt u het renderinggedrag van de transformatie wijzigen (zoals gedefinieerd in sectie 6 tot en met 8). Het is echter niet mogelijk om andere transformaties in de transformatiegrafiek of de grafiekindeling zelf hier te wijzigen.

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

Voor een complexer voorbeeld kunt u overwegen hoe een eenvoudige vervagingsbewerking wordt weergegeven:

Als bij een vervagingsbewerking een straal van 5 pixels wordt gebruikt, moet de grootte van de uitvoerrechthoek met 5 pixels worden uitgebreid, zoals hieronder wordt weergegeven. Wanneer u rechthoekcoördinaten wijzigt, moet een transformatie ervoor zorgen dat de logica geen overloop/onderloop veroorzaakt in de rechthoekcoördinaten.

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

Omdat de afbeelding wazig is, kan een gebied van de afbeelding dat ondoorzichtig was nu gedeeltelijk transparant zijn. Dit komt doordat het gebied buiten de afbeelding standaard transparant zwart is en deze transparantie wordt samengevoegd in de afbeelding rond de randen. De transformatie moet dit weerspiegelen in de uitvoer ondoorzichtige rechthoekberekeningen:

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

Deze berekeningen worden hier gevisualiseerd:

afbeelding van de berekening van rechthoeken.

Zie de referentiepagina MapInputRectsToOutputRect voor meer informatie over deze methode.

MapOutputRectToInputRects

Direct2D- roept de methode MapOutputRectToInputRects aan na MapInputRectsToOutputRect. De transformatie moet berekenen van welk gedeelte van de afbeelding deze moet lezen om de aangevraagde uitvoerregio correct weer te geven.

Net als voorheen, als een effect strikt pixels 1-1 toewijst, kan het de uitvoerrechthoek doorgeven aan de invoerrechthoek:

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

Als een transformatie een afbeelding verkleint of uitbreidt (zoals het onscherpe voorbeeld hier), gebruiken pixels vaak omringende pixels om hun waarde te berekenen. Met een vervaging wordt een pixel gemiddeld berekend met de omringende pixels, zelfs als deze zich buiten de grenzen van de invoerafbeelding bevinden. Dit gedrag wordt weerspiegeld in de berekening. Net als voorheen controleert de transformatie op overloop bij het uitvouwen van de coördinaten van een rechthoek.

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

Met deze afbeelding wordt de berekening gevisualiseerd. Direct2D- automatisch transparante zwarte pixels steekt waar de invoerafbeelding niet bestaat, zodat de vervaging geleidelijk kan worden gecombineerd met de bestaande inhoud op het scherm.

illustratie van een effect dat transparante zwarte pixels buiten een rechthoek steekt.

Als de toewijzing niet triviaal is, moet met deze methode de invoerrechthoek worden ingesteld op het maximale gebied om de juiste resultaten te garanderen. Hiervoor stelt u de linker- en bovenrand in op INT_MIN en de rechter- en onderrand op INT_MAX.

Zie het onderwerp MapOutputRectToInputRects voor meer informatie over deze methode.

MapInvalidRect

Direct2D- roept ook de methode MapInvalidRect aan. In tegenstelling tot de MapInputRectsToOutputRect en MapOutputRectToInputRects methoden direct2D wordt deze echter op een bepaald moment niet gegarandeerd aangeroepen. Met deze methode wordt bepaald welk deel van de uitvoer van een transformatie opnieuw moet worden weergegeven als reactie op een deel of alle wijzigingen in de invoer. Er zijn drie verschillende scenario's voor het berekenen van de ongeldige rect van een transformatie.

Transformaties met een-op-een-pixeltoewijzing

Voor transformaties die pixels 1-1 toewijzen, geeft u de ongeldige invoerrechthoek door aan de ongeldige uitvoerrechthoek:

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

Transformaties met veel-op-veel-pixeltoewijzing

Wanneer de uitvoer pixels van een transformatie afhankelijk zijn van het omringende gebied, moet de ongeldige invoerrechthoek overeenkomstig worden uitgevouwen. Dit is om aan te geven dat pixels rond de ongeldige invoerrechthoek ook worden beïnvloed en ongeldig worden. Voor een vervaging van vijf pixels wordt bijvoorbeeld de volgende berekening gebruikt:

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

Transformaties met complexe pixeltoewijzing

Voor transformaties waarbij invoer- en uitvoer pixels geen eenvoudige toewijzing hebben, kan de volledige uitvoer worden gemarkeerd als ongeldig. Als een transformatie bijvoorbeeld alleen de gemiddelde kleur van de invoer uitvoert, verandert de volledige uitvoer van de transformatie als zelfs een klein deel van de invoer wordt gewijzigd. In dit geval moet de ongeldige uitvoerrechthoek worden ingesteld op een logisch oneindige rechthoek (zie hieronder). Direct2D dit automatisch vastklemt aan de grenzen van de uitvoer.

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

Zie het onderwerp MapInvalidRect voor meer informatie over deze methode.

Zodra deze methoden zijn geïmplementeerd, bevat de header van uw transformatie het volgende:

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

Een pixel-shader toevoegen aan een aangepaste transformatie

Zodra een transformatie is gemaakt, moet deze een shader bieden waarmee de afbeeldings pixels worden bewerkt. In deze sectie worden de stappen beschreven voor het gebruik van een pixel-shader met een aangepaste transformatie.

Id2D1DrawTransform implementeren

Als u een pixel-shader wilt gebruiken, moet de transformatie de interface ID2D1DrawTransform implementeren, die wordt overgenomen van de interface ID2D1Transform die wordt beschreven in sectie 5. Deze interface bevat een nieuwe methode om te implementeren:

SetDrawInfo(ID2D1DrawInfo *pDrawInfo)

Direct2D- roept de methode SetDrawInfo aan wanneer de transformatie voor het eerst wordt toegevoegd aan de transformatiegrafiek van een effect. Deze methode biedt een ID2D1DrawInfo parameter waarmee wordt bepaald hoe de transformatie wordt weergegeven. Zie het onderwerp ID2D1DrawInfo voor de methoden die hier beschikbaar zijn.

Als de transformatie ervoor kiest om deze parameter op te slaan als een klasselidvariabele, kan het drawInfo object worden geopend en gewijzigd van andere methoden, zoals eigenschapssetters of MapInputRectsToOutputRect. Het kan met name niet worden aangeroepen vanuit de MapOutputRectToInputRects of MapInvalidRect methoden op ID2D1Transform-.

Een GUID maken voor de pixel-shader

Vervolgens moet de transformatie een unieke GUID definiëren voor de pixel-shader zelf. Dit wordt gebruikt wanneer Direct2D de shader in het geheugen laadt en wanneer de transformatie kiest welke pixel-shader moet worden gebruikt voor uitvoering. Hulpprogramma's zoals guidgen.exe, die deel uitmaken van Visual Studio, kunnen worden gebruikt om een willekeurige GUID te genereren.

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

De pixel-shader laden met Direct2D

Een pixel-shader moet in het geheugen worden geladen voordat deze kan worden gebruikt door de transformatie.

Als u de pixel-shader in het geheugen wilt laden, moet de transformatie de gecompileerde bytecode van de shader lezen van de . CSO-bestand dat is gegenereerd door Visual Studio (zie Documentatie voor Direct3D voor meer informatie) in een bytematrix. Deze techniek wordt gedetailleerd gedemonstreerd in het D2DCustomEffects SDK-voorbeeld.

Zodra de shader-gegevens in een bytematrix zijn geladen, roept u de Load PixelShader methode aan op het ID2D1EffectContext-object van het effect. Direct2D aanroepen naar Load PixelShader negeert wanneer er al een shader met dezelfde GUID is geladen.

Nadat een pixel-shader in het geheugen is geladen, moet de transformatie deze selecteren voor uitvoering door de GUID door te geven aan de methode Set PixelShader op de parameterID2D1DrawInfo die is opgegeven tijdens de methode SetDrawInfo. De pixel-shader moet al in het geheugen worden geladen voordat deze wordt geselecteerd voor uitvoering.

Arceringsbewerking wijzigen met constante buffers

Als u wilt wijzigen hoe een shader wordt uitgevoerd, kan een transformatie een constante buffer doorgeven aan de pixel-shader. Hiervoor definieert een transformatie een struct die de gewenste variabelen in de klasseheader bevat:

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

De transformatie roept vervolgens de methode ID2D1DrawInfo::Set PixelShaderConstantBuffer methode aan op de parameter ID2D1DrawInfo die is opgegeven in de methode SetDrawInfo om deze buffer door te geven aan de shader.

De HLSL- moet ook een bijbehorende struct definiëren die de constante buffer vertegenwoordigt. De variabelen in de struct van de shader moeten overeenkomen met de variabelen in de struct van de transformatie.

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

Zodra de buffer is gedefinieerd, kunnen de waarden in de pixel-shader overal worden gelezen.

Een pixel-shader schrijven voor Direct2D

Direct2D transformaties maken gebruik van shaders die zijn gemaakt met behulp van standaard HLSL-. Er zijn echter enkele belangrijke concepten voor het schrijven van een pixel-shader die wordt uitgevoerd vanuit de context van een transformatie. Zie het D2DCustomEffects SDK-voorbeeldvoor een voltooid voorbeeld van een volledig functionele pixel-shader.

Direct2D- automatisch de invoer van een transformatie toe wijst aan Texture2D- en SamplerState--objecten in de HLSL. De eerste Texture2D bevindt zich in register t0 en de eerste SamplerState bevindt zich in register s0. Elke extra invoer bevindt zich in de volgende bijbehorende registers (bijvoorbeeld t1 en s1). Pixelgegevens voor een bepaalde invoer kunnen worden bemonsterd door Sample aan te roepen op het object Texture2D en het bijbehorende SamplerState-object en de texelcoördinaten door te geven.

Een aangepaste pixel-shader wordt eenmaal uitgevoerd voor elke pixel die wordt weergegeven. Telkens wanneer de shader wordt uitgevoerd, biedt Direct2D- automatisch drie parameters waarmee de huidige uitvoeringspositie wordt geïdentificeerd:

  • Uitvoer van scèneruimte: deze parameter vertegenwoordigt de huidige uitvoeringspositie in termen van het totale doeloppervlak. Deze wordt gedefinieerd in pixels en de min/max-waarden komen overeen met de grenzen van de rechthoek die wordt geretourneerd door MapInputRectsToOutputRect.
  • Uitvoer van clipruimte: deze parameter wordt gebruikt door Direct3D en mag niet worden gebruikt in de pixel-shader van een transformatie.
  • Texel-ruimte-invoer: Deze parameter vertegenwoordigt de huidige uitvoeringspositie in een bepaald invoerpatroon. Een shader mag geen afhankelijkheden aannemen van de manier waarop deze waarde wordt berekend. Deze mag alleen worden gebruikt om de invoer van de pixel-shader te samplen, zoals wordt weergegeven in de onderstaande code:
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;
}

Een hoekpunt-shader toevoegen aan een aangepaste transformatie

U kunt hoekpunt-shaders gebruiken om verschillende beeldscenario's uit te voeren dan pixel-shaders. Met name hoekpunt-shaders kunnen geometriegebaseerde afbeeldingseffecten uitvoeren door hoekpunten te transformeren die een afbeelding vormen. Hoekpunt-shaders kunnen onafhankelijk van of in combinatie met transformatie-opgegeven pixel-shaders worden gebruikt. Als er geen hoekpunt-shader is opgegeven, Direct2D vervangen in een standaard hoekpunt-shader voor gebruik met de aangepaste pixel-shader.

Het proces voor het toevoegen van een hoekpunt-shader aan een aangepaste transformatie is vergelijkbaar met die van een pixel-shader. De transformatie implementeert de interface ID2D1DrawTransform, maakt een GUID en (optioneel) geeft constante buffers door aan de shader. Er zijn echter enkele belangrijke extra stappen die uniek zijn voor hoekpunt-shaders:

Een hoekpuntbuffer maken

Een hoekpunt-shader per definitie wordt uitgevoerd op hoekpunten die eraan worden doorgegeven, niet afzonderlijke pixels. Als u de hoekpunten wilt opgeven waarop de shader moet worden uitgevoerd, maakt een transformatie een hoekpuntbuffer die moet worden doorgegeven aan de shader. De indeling van hoekpuntbuffers valt buiten het bereik van dit document. Raadpleeg de Direct3D-verwijzing voor meer informatie of het D2DCustomEffects SDK-voorbeeld voor een voorbeeld-implementatie.

Nadat u een hoekpuntbuffer in het geheugen hebt gemaakt, gebruikt de transformatie de methode CreateVertexBuffer op de ID2D1EffectContext-object om die gegevens door te geven aan de GPU. Zie opnieuw het D2DCustomEffects SDK-voorbeeld voor een voorbeeld-implementatie.

Als er geen hoekpuntbuffer wordt opgegeven door de transformatie, geeft Direct2D- een standaard hoekpuntbuffer door die de rechthoekige afbeeldingslocatie vertegenwoordigt.

SetDrawInfo wijzigen om een hoekpunt-shader te gebruiken

Net als bij pixel-shaders moet de transformatie worden geladen en moet er een hoekpunt-shader worden geselecteerd voor uitvoering. Als u de hoekpunt-shader wilt laden, wordt de methode LoadVertexShader aangeroepen op de methode ID2D1EffectContext die is ontvangen in de initialisatiemethode van het effect. Als u de hoekpunt-shader wilt selecteren voor uitvoering, wordt SetVertexProcessing- aangeroepen op de parameter ID2D1DrawInfo die is ontvangen in de methode SetDrawInfo van de transformatie. Deze methode accepteert een GUID voor een eerder geladen hoekpunt-shader en (optioneel) een eerder gemaakte hoekpuntbuffer voor de shader om uit te voeren.

Een Direct2D-hoekpunt-shader implementeren

Een tekentransformatie kan zowel een pixel-shader als een hoekpuntschaduw bevatten. Als een transformatie zowel een pixel-shader als een hoekpuntschaduw definieert, wordt de uitvoer van de hoekpunt-shader rechtstreeks aan de pixel-shader gegeven: de app kan de retourhandtekening van de hoekpunthandtekening aanpassen/ de parameters van de pixel-shader zolang ze consistent zijn.

Als een transformatie echter alleen een hoekpuntschaduw bevat en afhankelijk is van Direct2D-standaardpassthrough pixel arcering, moet deze de volgende standaarduitvoer retourneren:

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

Met een hoekpunt-shader wordt het resultaat van de hoekpunttransformaties opgeslagen in de uitvoervariabele Scèneruimte van de shader. Voor het berekenen van de Clipruimte-uitvoer en de Texel-ruimteinvoervariabelen biedt Direct2D- automatisch conversiematrices in een constante buffer:

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

Hieronder vindt u voorbeeldcode voor hoekpunt-shader die gebruikmaakt van de conversiematrices om de juiste clip- en Texelse ruimten te berekenen die worden verwacht door Direct2D-:

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

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

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

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

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

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

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

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

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

    return output;  
}

De bovenstaande code kan worden gebruikt als uitgangspunt voor een hoekpunt arcering. Het passeert alleen de invoerafbeelding zonder transformaties uit te voeren. Zie opnieuw het D2DCustomEffects SDK-voorbeeld voor een volledig geïmplementeerde transformatie op basis van een hoektex-shader.

Als er geen hoekpuntbuffer wordt opgegeven door de transformatie, Direct2D vervangen in een standaard hoekpuntbuffer die de rechthoekige afbeeldingslocatie vertegenwoordigt. De parameters voor de hoekpunt-shader worden gewijzigd in die van de standaard-shader-uitvoer:

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

De hoekpunt-shader kan de sceneSpaceOutput- en clipSpaceOutput parameters niet wijzigen. Deze moeten ongewijzigd worden geretourneerd. Het kan echter voor elke invoerafbeelding de texelSpaceInput parameter(s) wijzigen. Als de transformatie ook een aangepaste pixel-shader bevat, kan de hoekpunt-shader nog steeds extra aangepaste parameters rechtstreeks doorgeven aan de pixel-shader. Daarnaast is de sceneSpace aangepaste buffer voor conversiematrices (b0) niet meer beschikbaar.

Een compute-shader toevoegen aan een aangepaste transformatie

Ten slotte kunnen aangepaste transformaties gebruikmaken van compute-shaders voor bepaalde doelscenario's. Compute-shaders kunnen worden gebruikt voor het implementeren van complexe afbeeldingseffecten waarvoor willekeurige toegang tot invoer- en uitvoerafbeeldingsbuffers is vereist. Een eenvoudig histogram-algoritme kan bijvoorbeeld niet worden geïmplementeerd met een pixel-shader vanwege beperkingen voor geheugentoegang.

Omdat compute-shaders hogere vereisten op hardwareniveau hebben dan pixel-shaders, moeten pixel-shaders indien mogelijk worden gebruikt om een bepaald effect te implementeren. Compute-shaders worden met name alleen uitgevoerd op de meeste DirectX 10-niveaukaarten en hoger. Als een transformatie ervoor kiest om een compute-shader te gebruiken, moet deze controleren op de juiste hardwareondersteuning tijdens het instantiëring, naast het implementeren van de ID2D1ComputeTransform interface.

Controleren op ondersteuning voor Compute Shader

Als een effect gebruikmaakt van een compute-shader, moet het controleren op ondersteuning voor compute-shader tijdens het maken ervan met behulp van de methode ID2D1EffectContext::CheckFeatureSupport. Als de GPU geen ondersteuning biedt voor compute-shaders, moet het effect D2DERR_INSUFFICIENT_DEVICE_CAPABILITIESretourneren.

Er zijn twee verschillende typen compute-shaders die een transformatie kan gebruiken: Shader Model 4 (DirectX 10) en Shader Model 5 (DirectX 11). Er gelden bepaalde beperkingen voor Shader Model 4-shaders. Zie de documentatie Direct3D voor meer informatie. Transformaties kunnen beide typen shaders bevatten en kunnen zo nodig terugvallen op Shader Model 4: zie het D2DCustomEffects SDK-voorbeeld voor een implementatie hiervan.

Id2D1ComputeTransform implementeren

Deze interface bevat twee nieuwe methoden om naast de methoden in ID2D1Transformte implementeren:

SetComputeInfo(ID2D1ComputeInfo *pComputeInfo)

Net als bij pixel- en hoekpunt-shaders roept Direct2D- de methode SetComputeInfo aan wanneer de transformatie voor het eerst wordt toegevoegd aan de transformatiegrafiek van een effect. Deze methode biedt een ID2D1ComputeInfo parameter waarmee wordt bepaald hoe de transformatie wordt weergegeven. Dit omvat het kiezen van de compute-shader die moet worden uitgevoerd via de methode ID2D1ComputeInfo::SetComputeShader. Als de transformatie ervoor kiest om deze parameter op te slaan als een klasselidvariabele, kan deze worden geopend en gewijzigd van elke transformatie- of effectmethode met uitzondering van de methoden MapOutputRectToInputRects en MapInvalidRect methoden. Zie het onderwerp ID2D1ComputeInfo voor andere methoden die hier beschikbaar zijn.

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

Terwijl pixel-shaders per pixel worden uitgevoerd en hoekpunt-shaders per hoekpunt worden uitgevoerd, worden compute-shaders per threadgroup uitgevoerd. Een threadgroup vertegenwoordigt een aantal threads die gelijktijdig worden uitgevoerd op de GPU. De HLSL-code voor compute-shader bepaalt hoeveel threads per threadgroup moeten worden uitgevoerd. Het effect schaalt het aantal threadgroepen zodat de shader het gewenste aantal keren uitvoert, afhankelijk van de logica van de shader.

Met de methode CalculateThreadgroups kan de transformatie Direct2D hoeveel threadgroepen vereist zijn, op basis van de grootte van de afbeelding en de eigen kennis van de arcering van de transformatie.

Het aantal keren dat de compute-shader wordt uitgevoerd, is een product van de threadgroup-tellingen die hier worden opgegeven en de annotatie 'numthreads' in de compute-shader HLSL-. Als de transformatie bijvoorbeeld de dimensies van de threadgroep instelt op (2,2,1) geeft de shader threads (3,3,1) per threadgroep op, dan worden 4 threadgroepen uitgevoerd, elk met 9 threads erin, voor een totaal van 36 threadexemplaren.

Een veelvoorkomend scenario is het verwerken van één uitvoer pixel voor elk exemplaar van de compute-shader. Als u het aantal threadgroepen voor dit scenario wilt berekenen, verdeelt de transformatie de breedte en hoogte van de afbeelding door de respectieve x- en y-dimensies van de annotatie 'aantalthreads' in de compute-shader HLSL-.

Belangrijk: als deze deling wordt uitgevoerd, moet het aantal aangevraagde threadgroepen altijd worden afgerond op het dichtstbijzijnde gehele getal, anders worden de 'rest' pixels niet uitgevoerd. Als een shader (bijvoorbeeld) één pixel met elke thread berekent, wordt de code van de methode als volgt weergegeven.

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

De HLSL- gebruikt de volgende code om het aantal threads in elke threadgroep op te geven:

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

Tijdens de uitvoering worden de huidige threadgroup en de huidige threadindex doorgegeven als parameters aan de shader-methode:

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

Afbeeldingsgegevens lezen

Compute-shaders krijgen toegang tot de invoerafbeelding van de transformatie als één tweedimensionaal patroon:

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

Net als pixel-shaders beginnen de gegevens van de afbeelding echter niet op (0, 0) op het patroon. In plaats daarvan biedt Direct2D- systeemconstanten waarmee shaders elke offset kunnen compenseren:

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

Zodra de bovenstaande constante buffer en helpermethode zijn gedefinieerd, kan de shader afbeeldingsgegevens als voorbeeld nemen met behulp van het volgende:

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

Afbeeldingsgegevens schrijven

Direct2D- verwacht dat een shader een uitvoerbuffer definieert voor de resulterende afbeelding. In Shader Model 4 (DirectX 10) moet dit een enkelvoudige buffer zijn vanwege functiebeperkingen:

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

Het uitvoerpatroon wordt eerst geïndexeerd, zodat de hele afbeelding kan worden opgeslagen.

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

Arceringsmodel 5 (DirectX 11) arceringen kunnen daarentegen tweedimensionale uitvoertextuur gebruiken:

RWTexture2D<float4> OutputTexture : register(t1);

Met Shader Model 5-shaders biedt Direct2D- een extra outputOffset-parameter in de constante buffer. De uitvoer van de shader moet worden verschoven met deze hoeveelheid:

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

Hieronder ziet u een voltooide arcering voor passthrough model 5. Hierin leest en schrijft elk van de compute-shader-threads één pixel van de invoerafbeelding.

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

De onderstaande code toont de equivalente Shader Model 4-versie van de shader. U ziet dat de shader nu wordt weergegeven in een singledimensionale uitvoerbuffer.

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