Partilhar via


Efeitos personalizados

Direct2D vem com uma biblioteca de efeitos que executam uma variedade de operações de imagem comuns. Consulte o tópico efeitos internos para obter a lista completa de efeitos. Para funcionalidades que não podem ser alcançadas com os efeitos internos, o Direct2D permite que você escreva seus próprios efeitos personalizados usando o padrão HLSL. Você pode usar esses efeitos personalizados juntamente com os efeitos internos fornecidos com o Direct2D.

Para ver exemplos de um pixel completo, vértice e efeito de sombreador de computação, consulte o exemplo de SDK D2DCustomEffects.

Neste tópico, mostramos as etapas e os conceitos necessários para projetar e criar um efeito personalizado completo.

Introdução: O que está dentro de um efeito?

diagrama de efeitos de sombra projetada.

Conceitualmente, um efeito de Direct2D executa uma tarefa de criação de imagens, como alterar o brilho, dessaturar uma imagem ou, como mostrado acima, criar uma sombra. Para o aplicativo, eles são simples. Eles podem aceitar zero ou mais imagens de entrada, expor várias propriedades que controlam sua operação e gerar uma única imagem de saída.

Há quatro partes diferentes de um efeito personalizado pelas quais um autor de efeito é responsável:

  1. Interface de efeito: a interface de efeitos define conceitualmente como um aplicativo interage com um efeito personalizado (como quantas entradas o efeito aceita e quais propriedades estão disponíveis). A interface de efeitos gerencia um gráfico de transformação, que contém as operações reais de criação de imagens.
  2. Gráfico de transformação: Cada efeito cria um gráfico de transformação interno composto por transformações individuais. Cada transformação representa uma única operação de imagem. O efeito é responsável por ligar essas transformações em um gráfico para realizar o efeito de imagem pretendido. Um efeito pode adicionar, remover, modificar e reordenar transformações em resposta a alterações nas propriedades externas do efeito.
  3. Transformar: uma transformação representa uma única operação de imagem. Seu principal objetivo é abrigar os sombreadores que são executados para cada pixel de saída. Para esse fim, ele é responsável por calcular o novo tamanho de sua imagem de saída com base na lógica em seus sombreadores. Ele também deve calcular qual área de sua imagem de entrada os sombreadores precisam ler para renderizar a região de saída solicitada.
  4. Shader: um sombreador é executado na entrada da transformação na GPU (ou CPU se a renderização de software for especificada quando o aplicativo cria o dispositivo Direct3D). Os sombreadores de efeito são escritos em High Level Shading Language (HLSL) e são compilados em código de byte durante a compilação do efeito, que é então carregado pelo efeito durante o tempo de execução. Este documento de referência descreve como escrever HLSL compatível comDirect2D. A documentação do Direct3D contém uma visão geral básica do HLSL.

Criando uma interface de efeito

A interface de efeito define como um aplicativo interage com o efeito personalizado. Para criar uma interface de efeito, uma classe deve implementar ID2D1EffectImpl, definir metadados que descrevem o efeito (como seu nome, contagem de entrada e propriedades) e criar métodos que registrem o efeito personalizado para uso com Direct2D.

Depois que todos os componentes de uma interface de efeito tiverem sido implementados, o cabeçalho da classe aparecerá assim:

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

Implementar ID2D1EffectImpl

A interfaceID2D1EffectImpl docontém três métodos que você deve implementar:

Inicializar(ID2D1EffectContext *pContextInternal, ID2D1TransformGraph *pTransformGraph)

Direct2D chama o métodoInitialize depois que o ID2D1DeviceContext::CreateEffect método foi chamado pelo aplicativo. Você pode usar esse método para executar a inicialização interna ou quaisquer outras operações necessárias para o efeito. Além disso, você pode usá-lo para criar o gráfico de transformação inicial do efeito.

SetGraph(ID2D1TransformGraph *pTransformGraph)

Direct2D chama o método SetGraph quando o número de entradas para o efeito é alterado. Enquanto a maioria dos efeitos tem um número constante de entradas, outros como o efeito composto suportam um número variável de entradas. Esse método permite que esses efeitos atualizem seu gráfico de transformação em resposta a uma contagem de entrada em mudança. Se um efeito não suportar uma contagem de entrada variável, este método pode simplesmente retornar E_NOTIMPL.

PrepareForRender (D2D1_CHANGE_TYPE changeType)

O métodoPrepareForRender oferece uma oportunidade para que os efeitos executem quaisquer operações em resposta a alterações externas. Direct2D chama esse método imediatamente antes de renderizar um efeito se pelo menos uma destas opções for verdadeira:

  • O efeito foi previamente inicializado, mas ainda não foi desenhado.
  • Uma propriedade effect foi alterada desde a última chamada de sorteio.
  • O estado da chamada contexto de Direct2D (como DPI) mudou desde a última chamada de sorteio.

Implementar os métodos de registro de efeito e retorno de chamada

Os aplicativos devem registrar efeitos com Direct2D antes de instanciá-los. Esse registro tem como escopo uma instância de uma fábrica Direct2D e deve ser repetido sempre que o aplicativo for executado. Para habilitar esse registro, um efeito personalizado define um GUID exclusivo, um método público que registra o efeito e um método de retorno de chamada privado que retorna uma instância do efeito.

Definir um GUID

Você deve definir um GUID que identifique exclusivamente o efeito para o registro com Direct2D. O aplicativo usa o mesmo para identificar o efeito quando chama ID2D1DeviceContext::CreateEffect.

Este código demonstra a definição de tal GUID para um efeito. Você deve criar seu próprio GUID exclusivo usando uma ferramenta de geração de GUID, como 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);

Definir um método de registo público

Em seguida, defina um método público para o aplicativo chamar para registrar o efeito com Direct2D. Como o registro de efeito é específico para uma instância de uma fábrica Direct2D, o método aceita um ID2D1Factory1 interface como parâmetro. Para registrar o efeito, o método chama a ID2D1Factory1::RegisterEffectFromString API no parâmetro ID2D1Factory1.

Essa API aceita uma cadeia de caracteres XML que descreve os metadados, entradas e propriedades do efeito. Os metadados para um efeito são apenas para fins informativos e podem ser consultados pelo aplicativo por meio da interfaceID2D1Properties. Os dados de entrada e propriedade, por outro lado, são usados por Direct2D e representam a funcionalidade do efeito.

Uma cadeia de caracteres XML para um efeito de amostra mínimo é mostrada aqui. A adição de propriedades personalizadas ao XML é abordada na seção Adicionando propriedades personalizadas a um efeito.

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

Definir um método de retorno de chamada de fábrica de efeitos

O efeito também deve fornecer um método de retorno de chamada privado que retorna uma instância do efeito por meio de um único parâmetro IUnknown**. Um ponteiro para esse método é fornecido para Direct2D quando o efeito é registrado por meio do ID2D1Factory1::RegisterEffectFromString API através do parâmetro 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;
}

Implementar a interface IUnknown

Finalmente, o efeito deve implementar a interface IUnknown para compatibilidade com COM.

Criando o gráfico de transformação do efeito

Um efeito pode usar várias transformações diferentes (operações de imagem individuais) para criar o efeito de imagem desejado. Para controlar a ordem em que essas transformações são aplicadas à imagem de entrada, o efeito as organiza em um gráfico de transformação. Um gráfico de transformação pode usar os efeitos e transformações incluídos em Direct2D, bem como transformações personalizadas criadas pelo autor do efeito.

Usando transformações incluídas no Direct2D

Estas são as transformações mais usadas fornecidas com Direct2D.

Criando um gráfico de transformação de nó único

Depois de criar uma transformação, a entrada do efeito precisa ser conectada à entrada da transformação, e a saída da transformação precisa ser conectada à saída do efeito. Quando um efeito contém apenas uma única transformação, você pode usar o método ID2D1TransformGraph::SetSingleTransformNode para fazer isso facilmente.

Você pode criar ou modificar uma transformação no do efeito ou métodos de SetGraph usando o parâmetro ID2D1TransformGraph fornecido. Se um efeito precisar fazer alterações no gráfico de transformação em outro método onde esse parâmetro não estiver disponível, o efeito poderá salvar o parâmetro ID2D1TransformGraphcomo uma variável membro da classe e acessá-lo em outro lugar, como PrepareForRender ou um método de retorno de chamada de propriedade personalizada.

Um exemplo método Initialize é mostrado aqui. Esse método cria um gráfico de transformação de nó único que desloca a imagem em cem pixels em cada eixo.

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

Criando um gráfico de transformação de vários nós

Adicionar várias transformações ao gráfico de transformação de um efeito permite que os efeitos executem internamente várias operações de imagem que são apresentadas a um aplicativo como um único efeito unificado.

Como observado acima, o gráfico de transformação do efeito pode ser editado em qualquer método de efeito usando o parâmetro ID2D1TransformGraph recebido no método Initialize do efeito. As APIs a seguir nessa interface podem ser usadas para criar ou modificar o gráfico de transformação de um efeito:

AddNode(ID2D1TransformNode *pNode)

O método AddNode, na verdade, 'registra' a transformação com o efeito, e deve ser chamado antes que a transformação possa ser usada com qualquer um dos outros métodos de gráfico de transformação.

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

O método ConnectToEffectInput conecta a entrada de imagem do efeito à entrada de uma transformação. A mesma entrada de efeito pode ser conectada a várias transformações.

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

O método ConnectNode conecta a saída de uma transformação à entrada de outra transformação. Uma saída de transformação pode ser conectada a várias transformações.

SetOutputNode(ID2D1TransformNode *pNode)

O método SetOutputNode conecta a saída de uma transformação à saída do efeito. Como um efeito tem apenas uma saída, apenas uma única transformação pode ser designada como o 'nó de saída'.

Esse código usa duas transformações separadas para criar um efeito unificado. Neste caso, o efeito é uma sombra traduzida.

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

Adicionando propriedades personalizadas a um efeito

Os efeitos podem definir propriedades personalizadas que permitem que um aplicativo altere o comportamento do efeito durante o tempo de execução. Há três etapas para definir uma propriedade para um efeito personalizado:

Adicionar os metadados da propriedade aos dados de registro do efeito

Adicionar propriedade ao XML de registro

Você deve definir as propriedades de um efeito personalizado durante o registro inicial do efeito com Direct2D. Primeiro, você deve atualizar o XML de registro do efeito em seu método de registro público com a nova propriedade:

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

Quando você define uma propriedade effect em XML, ela precisa de um nome, um tipo e um nome para exibição. O nome de exibição de uma propriedade, bem como os valores gerais de categoria, autor e descrição do efeito geral podem e devem ser localizados.

Para cada propriedade, um efeito pode, opcionalmente, especificar valores padrão, min e max. Estes valores são apenas para uso informativo. Eles não são impostos pelo Direct2D. Cabe a você mesmo implementar qualquer lógica default/min/max especificada na classe effect.

O valor do tipo listado no XML para a propriedade deve corresponder ao tipo de dados correspondente usado pelos métodos getter e setter da propriedade. Os valores XML correspondentes para cada tipo de dados são mostrados nesta tabela:

Tipo de dados Valor XML correspondente
PWSTR string
BOOL Bool
UINT UINT32
INT Int32
FLUTUAR flutuar
D2D_VECTOR_2F vetor2
D2D_VECTOR_3F vetor3
D2D_VECTOR_4F vetor4
D2D_MATRIX_3X2_F matriz3x2
D2D_MATRIX_4X3_F matriz4x3
D2D_MATRIX_4X4_F matriz4x4
D2D_MATRIX_5X4_F matriz5x4
BYTE[] bolha
IUnconhecido* idesconhecido
* ID2D1ColorContext colorcontext
CLSID CLSID
Enumeração (D2D1_INTERPOLATION_MODE, etc.) enum

 

Mapeie a nova propriedade para os métodos getter e setter

Em seguida, o efeito deve mapear essa nova propriedade para os métodos getter e setter. Isso é feito por meio da matriz D2D1_PROPERTY_BINDING que é passada para o ID2D1Factory1::RegisterEffectFromString método.

A matriz D2D1_PROPERTY_BINDING tem esta aparência:

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

Depois de criar a matriz XML e bindings, passe-os para o RegisterEffectFromString método:

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

A macro D2D1_VALUE_TYPE_BINDING requer que a classe effect herde de ID2D1EffectImpl antes de qualquer outra interface.

As propriedades personalizadas de um efeito são indexadas na ordem em que são declaradas no XML e, uma vez criadas, podem ser acessadas pelo aplicativo usando os métodos ID2D1Properties::SetValue e ID2D1Properties::GetValue. Por conveniência, você pode criar uma enumeração pública que lista cada propriedade no arquivo de cabeçalho do efeito:

typedef enum SAMPLEEFFECT_PROP
{
    SAMPLEFFECT_PROP_OFFSET = 0
};

Criar os métodos getter e setter para a propriedade

A próxima etapa é criar os métodos getter e setter para a nova propriedade. Os nomes dos métodos devem corresponder aos especificados na matriz D2D1_PROPERTY_BINDING. Além disso, o tipo de propriedade especificado no XML do efeito deve corresponder ao tipo do parâmetro do método setter e ao valor de retorno do método getter.

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

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

    m_offset = offset;

    return S_OK;
}

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

Transformações do efeito de atualização em resposta à alteração de propriedade

Para realmente atualizar a saída de imagem de um efeito em resposta a uma alteração de propriedade, o efeito precisa alterar suas transformações subjacentes. Isso geralmente é feito no método PrepareForRender do efeito, que Direct2D chama automaticamente quando uma das propriedades de um efeito é alterada. No entanto, as transformações podem ser atualizadas em qualquer um dos métodos do efeito: como Initialize ou os métodos setter de propriedades do efeito.

Por exemplo, se um efeito contivesse umID2D1OffsetTransforme quisesse modificar seu valor de deslocamento em resposta à alteração da propriedade Offset do efeito, ele adicionaria o seguinte código em 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;
}

Criando uma transformação personalizada

Para implementar operações de imagem além do que é fornecido em Direct2D, você deve implementar transformações personalizadas. As transformações personalizadas podem alterar arbitrariamente uma imagem de entrada através do uso de sombreadores HLSL personalizados.

As transformações implementam uma das duas interfaces diferentes, dependendo dos tipos de sombreadores que usam. As transformações que usam sombreadores de pixel e/ou vértice devem implementar ID2D1DrawTransform, enquanto as transformações que usam sombreadores de computação devem implementar ID2D1ComputeTransform. Essas interfaces herdam de ID2D1Transform. Esta seção se concentra na implementação da funcionalidade que é comum a ambos.

O interface ID2D1Transform tem quatro métodos para implementar:

GetInputCount

Esse método retorna um inteiro que representa a contagem de entrada para a transformação.

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

MapInputRectsToOutputRect

Direct2D chama o método MapInputRectsToOutputRect cada vez que a transformação é renderizada. O Direct2D passa um retângulo que representa os limites de cada uma das entradas para a transformação. A transformação é então responsável por calcular os limites da imagem de saída. O tamanho dos retângulos para todos os métodos nesta interface (ID2D1Transform) são definidos em pixels, não DIPs.

Este método também é responsável por calcular a região da saída que é opaca com base na lógica de seu sombreador e as regiões opacas de cada entrada. Uma região opaca de uma imagem é definida como aquela em que o canal alfa é «1» para a totalidade do retângulo. Se não estiver claro se a saída de uma transformação é opaca, o retângulo opaco de saída deve ser definido como (0, 0, 0, 0) como um valor seguro. Direct2D usa essas informações para executar otimizações de renderização com conteúdo "opaco garantido". Se esse valor for impreciso, pode resultar em renderização incorreta.

Você pode modificar o comportamento de renderização da transformação (conforme definido nas seções 6 a 8) durante esse método. No entanto, não é possível modificar outras transformações no gráfico de transformação ou no layout do gráfico em si aqui.

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

Para um exemplo mais complexo, considere como uma simples operação de desfoque seria representada:

Se uma operação de desfoque usar um raio de 5 pixels, o tamanho do retângulo de saída deve se expandir em 5 pixels, como mostrado abaixo. Ao modificar as coordenadas do retângulo, uma transformação deve garantir que sua lógica não cause sobre/subfluxo nas coordenadas do retângulo.

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

Como a imagem está desfocada, uma região da imagem que era opaca pode agora ser parcialmente transparente. Isso ocorre porque a área fora da imagem tem como padrão preto transparente e essa transparência será misturada à imagem ao redor das bordas. A transformação deve refletir isso em seus cálculos de retângulo opaco de saída:

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

Estes cálculos são visualizados aqui:

ilustração do cálculo do retângulo.

Para obter mais informações sobre esse método, consulte a página de referênciaMapInputRectsToOutputRect.

MapOutputRectToInputRects

Direct2D chama o MapOutputRectToInputRects método após MapInputRectsToOutputRect. A transformação deve calcular qual parte da imagem precisa ler para renderizar corretamente a região de saída solicitada.

Como antes, se um efeito mapeia estritamente os pixels 1-1, ele pode passar o retângulo de saída para o retângulo de entrada:

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

Da mesma forma, se uma transformação encolhe ou expande uma imagem (como o exemplo de desfoque aqui), os pixels geralmente usam pixels ao redor para calcular seu valor. Com um desfoque, um pixel é calculado em média com seus pixels ao redor, mesmo que eles estejam fora dos limites da imagem de entrada. Esse comportamento é refletido no cálculo. Como antes, a transformação verifica se há estouros ao expandir as coordenadas de um retângulo.

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

Esta figura visualiza o cálculo. Direct2D obtém automaticamente amostras de pixels pretos transparentes onde a imagem de entrada não existe, permitindo que o desfoque seja misturado gradualmente com o conteúdo existente na tela.

ilustração de um efeito de amostragem de pixels pretos transparentes fora de um retângulo.

Se o mapeamento não for trivial, esse método deve definir o retângulo de entrada para a área máxima para garantir resultados corretos. Para fazer isso, defina as bordas esquerda e superior para INT_MIN e as bordas direita e inferior para INT_MAX.

Para obter mais informações sobre esse método, consulte o MapOutputRectToInputRects tópico.

MapInvalidRect

Direct2D também chama o MapInvalidRect método. No entanto, ao contrário do MapInputRectsToOutputRect e MapOutputRectToInputRects métodos Direct2D não é garantido para chamá-lo em qualquer momento específico. Esse método decide conceitualmente qual parte da saída de uma transformação precisa ser reprocessada em resposta à alteração de parte ou de toda a sua entrada. Há três cenários diferentes para os quais calcular a correção inválida de uma transformação.

Transforma-se com mapeamento de pixel um-para-um

Para transformações que mapeiam pixels 1-1, basta passar o retângulo de entrada inválido para o retângulo de saída inválido:

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

Transforma-se com mapeamento de muitos para muitos pixels

Quando os pixels de saída de uma transformação dependem de sua área circundante, o retângulo de entrada inválido deve ser expandido correspondentemente. Isso é para refletir que os pixels ao redor do retângulo de entrada inválido também serão afetados e se tornarão inválidos. Por exemplo, um desfoque de cinco pixels usa o seguinte cálculo:

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

Transforma-se com mapeamento de pixel complexo

Para transformações em que os pixels de entrada e saída não têm um mapeamento simples, toda a saída pode ser marcada como inválida. Por exemplo, se uma transformação simplesmente produz a cor média da entrada, toda a saída da transformação muda se até mesmo uma pequena parte da entrada for alterada. Neste caso, o retângulo de saída inválido deve ser definido como um retângulo logicamente infinito (mostrado abaixo). Direct2D fixa automaticamente isso aos limites da saída.

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

Para obter mais informações sobre esse método, consulte o MapInvalidRect tópico.

Depois que esses métodos forem implementados, o cabeçalho da transformação conterá o seguinte:

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

Adicionando um sombreador de pixel a uma transformação personalizada

Uma vez que uma transformação tenha sido criada, ela precisa fornecer um sombreador que manipulará os pixels da imagem. Esta seção aborda as etapas para usar um sombreador de pixel com uma transformação personalizada.

Implementando ID2D1DrawTransform

Para usar um sombreador de pixel, a transformação deve implementar a interfaceID2D1DrawTransform, que herda da interfaceID2D1Transformdescrita na seção 5. Esta interface contém um novo método para implementar:

SetDrawInfo(ID2D1DrawInfo *pDrawInfo)

Direct2D chama o método SetDrawInfo quando a transformação é adicionada pela primeira vez ao gráfico de transformação de um efeito. Esse método fornece um ID2D1DrawInfo parâmetro que controla como a transformação é renderizada. Consulte o tópico ID2D1DrawInfo para obter os métodos disponíveis aqui.

Se a transformação optar por armazenar esse parâmetro como uma variável de membro de classe, o objeto drawInfo poderá ser acessado e alterado de outros métodos, como setters de propriedade ou MapInputRectsToOutputRect. Notavelmente, ele não pode ser chamado a partir do MapOutputRectToInputRects ou métodos MapInvalidRect em ID2D1Transform.

Criando um GUID para o sombreador de pixel

Em seguida, a transformação deve definir um GUID exclusivo para o próprio sombreador de pixel. Isso é usado quando Direct2D carrega o sombreador na memória, bem como quando a transformação escolhe qual sombreador de pixel usar para execução. Ferramentas como guidgen.exe, que está incluído no Visual Studio, podem ser usadas para gerar um GUID aleatório.

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

Carregando o sombreador de pixel com Direct2D

Um sombreador de pixel deve ser carregado na memória antes de poder ser usado pela transformação.

Para carregar o sombreador de pixel na memória, a transformação deve ler o código de byte do sombreador compilado do . CSO gerado pelo Visual Studio (consulte documentação do Direct3D para obter detalhes) em uma matriz de bytes. Essa técnica é demonstrada em detalhes no D2DCustomEffects SDK exemplo.

Depois que os dados do sombreador tiverem sido carregados em uma matriz de bytes, chame o método LoadPixelShader no objeto ID2D1EffectContext do efeito. Direct2D ignora chamadas para LoadPixelShader quando um sombreador com o mesmo GUID já foi carregado.

Depois que um sombreador de pixel tiver sido carregado na memória, a transformação precisa selecioná-lo para execução passando seu GUID para o métodoSetPixelShaderno parâmetro ID2D1DrawInfo fornecido durante o método SetDrawInfo. O sombreador de pixel já deve estar carregado na memória antes de ser selecionado para execução.

Alterando a operação do sombreador com buffers constantes

Para alterar a forma como um sombreador é executado, uma transformação pode passar um buffer constante para o sombreador de pixel. Para fazer isso, uma transformação define uma struct que contém as variáveis desejadas no cabeçalho da classe:

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

Em seguida, a transformação chama o método de ID2D1DrawInfo::SetPixelShaderConstantBuffer no parâmetro ID2D1DrawInfo fornecido no método SetDrawInfo para passar esse buffer para o sombreador.

O HLSL também precisa definir uma estrutura correspondente que represente o buffer constante. As variáveis contidas na struct do sombreador devem corresponder àquelas na struct da transformação.

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

Uma vez definido o buffer, os valores contidos nele podem ser lidos de qualquer lugar dentro do sombreador de pixel.

Escrevendo um sombreador de pixel para Direct2D

transformações de Direct2D usam sombreadores criados usando o padrão HLSL. No entanto, existem alguns conceitos-chave para escrever um sombreador de pixel que é executado a partir do contexto de uma transformação. Para obter um exemplo completo de um sombreador de pixel totalmente funcional, consulte o exemplo de SDK D2DCustomEffects.

Direct2D mapeia automaticamente as entradas de uma transformação para Texture2D e objetos SamplerState no HLSL. O primeiro Texture2D está localizado no registro t0 e o primeiro SamplerState está localizado no registro s0. Cada entrada adicional está localizada nos próximos registos correspondentes (t1 e s1, por exemplo). Os dados de pixel para uma entrada específica podem ser amostrados chamando Sample no objeto Texture2D e passando o objeto SamplerState correspondente e as coordenadas texel.

Um sombreador de pixel personalizado é executado uma vez para cada pixel renderizado. Cada vez que o sombreador é executado, Direct2D fornece automaticamente três parâmetros que identificam sua posição de execução atual:

  • Saída cena-espaço: Este parâmetro representa a posição de execução atual em termos da superfície de destino geral. Ele é definido em pixels e seus valores min/max correspondem aos limites do retângulo retornado por MapInputRectsToOutputRect.
  • Saída de espaço de clipe: esse parâmetro é usado pelo Direct3D e não deve ser usado no sombreador de pixel de uma transformação.
  • Entrada Texel-space: Este parâmetro representa a posição de execução atual em uma textura de entrada específica. Um sombreador não deve ter nenhuma dependência de como esse valor é calculado. Ele só deve usá-lo para obter amostras da entrada do sombreador de pixel, conforme mostrado no código abaixo:
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;
}

Adicionando um sombreador de vértice a uma transformação personalizada

Você pode usar sombreadores de vértice para realizar cenários de imagem diferentes dos sombreadores de pixel. Em particular, os sombreadores de vértice podem executar efeitos de imagem baseados em geometria transformando vértices que compõem uma imagem. Os sombreadores de vértice podem ser usados independentemente ou em conjunto com sombreadores de pixel especificados por transformação. Se um sombreador de vértice não for especificado, Direct2D substituirá um sombreador de vértice padrão para uso com o sombreador de pixel personalizado.

O processo para adicionar um sombreador de vértice a uma transformação personalizada é semelhante ao de um sombreador de pixel – a transformação implementa a interfaceID2D1DrawTransform, cria um GUID e (opcionalmente) passa buffers constantes para o sombreador. No entanto, existem algumas etapas adicionais importantes que são exclusivas para sombreadores de vértice:

Criando um buffer de vértice

Um sombreador de vértice por definição é executado em vértices passados para ele, não pixels individuais. Para especificar os vértices para o sombreador executar, uma transformação cria um buffer de vértice para passar para o sombreador. O layout dos buffers de vértice está além do escopo deste documento. Consulte o de referência do Direct3D para obter detalhes ou o de exemplo do SDK D2DCustomEffects para obter uma implementação de exemplo.

Depois de criar um buffer de vértice na memória, a transformação usa o método CreateVertexBuffer no objeto ID2D1EffectContext do efeito que contém para passar esses dados para a GPU. Novamente, consulte o de exemplo do SDK D2DCustomEffects para obter uma implementação de exemplo.

Se nenhum buffer de vértice for especificado pela transformação, Direct2D passa por um buffer de vértice padrão que representa o local da imagem retangular.

Alterando SetDrawInfo para utilizar um sombreador de vértice

Como com sombreadores de pixel, a transformação deve carregar e selecionar um sombreador de vértice para execução. Para carregar o sombreador de vértice, ele chama o métodoLoadVertexShader no ID2D1EffectContext método recebido no método Initialize do efeito. Para selecionar o sombreador de vértice para execução, ele chama SetVertexProcessing no parâmetro ID2D1DrawInfo recebido no métodoSetDrawInfoda transformação. Este método aceita um GUID para um sombreador de vértice carregado anteriormente, bem como (opcionalmente) um buffer de vértice criado anteriormente para o sombreador executar.

Implementando um sombreador de vértice Direct2D

Uma transformação de desenho pode conter um sombreador de pixel e um sombreador de vértice. Se uma transformação define um sombreador de pixel e um sombreador de vértice, a saída do sombreador de vértice é dada diretamente ao sombreador de pixel: o aplicativo pode personalizar a assinatura de retorno do sombreador de vértice / os parâmetros do sombreador de pixel, desde que sejam consistentes.

Por outro lado, se uma transformação contiver apenas um sombreador de vértice e depender do sombreador de pixel de passagem padrão doDirect2D, ela deverá retornar a seguinte saída padrão:

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

Um sombreador de vértice armazena o resultado de suas transformações de vértice na variável de saída Scene-space do sombreador. Para calcular a saída Clip-space e as variáveis de entrada Texel-space, Direct2D fornece automaticamente matrizes de conversão em um buffer constante:

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

Exemplo de código de sombreador de vértice pode ser encontrado abaixo que usa as matrizes de conversão para calcular os espaços corretos de clipe e texel esperados por 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;  
}

O código acima pode ser usado como um ponto de partida para um sombreador de vértice. Ele simplesmente passa pela imagem de entrada sem realizar nenhuma transformação. Novamente, consulte o de exemplo do SDK D2DCustomEffects para obter uma transformação baseada em sombreador de vértice totalmente implementada.

Se nenhum buffer de vértice for especificado pela transformação, Direct2D substituirá em um buffer de vértice padrão representando o local da imagem retangular. Os parâmetros para o sombreador de vértice são alterados para os da saída de sombreador padrão:

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

O sombreador de vértice não pode modificar seus sceneSpaceOutput e clipSpaceOutput parâmetros. Deve devolvê-los inalterados. Pode, no entanto, modificar o(s) parâmetro(s) texelSpaceInput para cada imagem de entrada. Se a transformação também contiver um sombreador de pixel personalizado, o sombreador de vértice ainda poderá passar parâmetros personalizados adicionais diretamente para o sombreador de pixel. Além disso, o sceneSpace matrizes de conversão de buffer personalizado (b0) não é mais fornecido.

Adicionando um sombreador de computação a uma transformação personalizada

Finalmente, as transformações personalizadas podem utilizar sombreadores de computação para determinados cenários de destino. Os sombreadores de computação podem ser usados para implementar efeitos de imagem complexos que exigem acesso arbitrário a buffers de imagem de entrada e saída. Por exemplo, um algoritmo de histograma básico não pode ser implementado com um sombreador de pixel devido a limitações no acesso à memória.

Como os sombreadores de computação têm requisitos de nível de recurso de hardware mais altos do que os sombreadores de pixel, os sombreadores de pixel devem ser usados quando possível para implementar um determinado efeito. Especificamente, os sombreadores de computação só são executados na maioria das placas de nível DirectX 10 e superiores. Se uma transformação optar por usar um sombreador de computação, ela deverá verificar o suporte de hardware apropriado durante a instanciação, além de implementar a interfaceID2D1ComputeTransform.

Verificando o suporte ao Compute Shader

Se um efeito usa um sombreador de computação, ele deve verificar se há suporte a sombreador de computação durante sua criação usando o método ID2D1EffectContext::CheckFeatureSupport. Se a GPU não suportar sombreadores de computação, o efeito deverá retornar D2DERR_INSUFFICIENT_DEVICE_CAPABILITIES.

Há dois tipos diferentes de sombreadores de computação que uma transformação pode usar: Shader Model 4 (DirectX 10) e Shader Model 5 (DirectX 11). Existem certas limitações para sombreadores Shader Model 4. Consulte a documentação do Direct3D para obter detalhes. As transformações podem conter ambos os tipos de sombreadores e podem retornar ao Shader Model 4 quando necessário: consulte o de exemplo do SDK D2DCustomEffects para obter uma implementação disso.

Implementar ID2D1ComputeTransform

Esta interface contém dois novos métodos para implementar, além dos ID2D1Transform:

SetComputeInfo(ID2D1ComputeInfo *pComputeInfo)

Como acontece com sombreadores de pixel e vértice, Direct2D chama o método SetComputeInfo quando a transformação é adicionada pela primeira vez ao gráfico de transformação de um efeito. Esse método fornece um ID2D1ComputeInfo parâmetro que controla como a transformação é renderizada. Isso inclui escolher o sombreador de computação a ser executado por meio do método ID2D1ComputeInfo::SetComputeShader. Se a transformação optar por armazenar esse parâmetro como uma variável de membro de classe, ela poderá ser acessada e alterada de qualquer método de transformação ou efeito, com exceção dosMapOutputRectToInputRectse métodos de MapInvalidRect. Consulte o tópico ID2D1ComputeInfo para obter outros métodos disponíveis aqui.

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

Enquanto os sombreadores de pixel são executados por pixel e os sombreadores de vértice são executados por vértice, os sombreadores de computação são executados por 'threadgroup'. Um threadgroup representa vários threads que são executados simultaneamente na GPU. O código HLSL do sombreador de computação decide quantos threads devem ser executados por threadgroup. O efeito dimensiona o número de threadgroups para que o sombreador execute o número desejado de vezes, dependendo da lógica do sombreador.

O métodoCalculateThreadgroups permite que a transformação informe Direct2D quantos grupos de threads são necessários, com base no tamanho da imagem e no próprio conhecimento da transformação sobre o sombreador.

O número de vezes que o sombreador de computação é executado é um produto das contagens de threadgroup especificadas aqui e da anotação 'numthreads' no sombreador de computação HLSL. Por exemplo, se a transformação definir as dimensões do grupo de threads como (2,2,1) o sombreador especificar (3,3,1) threads por threadgroup, então 4 threadgroups serão executados, cada um com 9 threads neles, para um total de 36 instâncias de thread.

Um cenário comum é processar um pixel de saída para cada instância do sombreador de computação. Para calcular o número de grupos de threads para este cenário, a transformação divide a largura e a altura da imagem pelas respetivas dimensões x e y da anotação 'numthreads' no sombreador de computação HLSL.

É importante ressaltar que, se essa divisão for executada, o número de grupos de threads solicitados deve sempre ser arredondado para o número inteiro mais próximo, caso contrário, os pixels 'restantes' não serão executados. Se um sombreador (por exemplo) calcular um único pixel com cada thread, o código do método aparecerá da seguinte maneira.

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

O HLSL usa o código a seguir para especificar o número de threads em cada grupo de threads:

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

Durante a execução, o threadgroup atual e o índice de thread atual são passados como parâmetros para o método shader:

#define NUMTHREADS_X 24
#define NUMTHREADS_Y 24

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

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

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

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

Leitura de dados de imagem

Os sombreadores de computação acessam a imagem de entrada da transformação como uma única textura bidimensional:

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

No entanto, como sombreadores de pixel, não é garantido que os dados da imagem comecem em (0, 0) na textura. Em vez disso, Direct2D fornece constantes do sistema que permitem que os sombreadores compensem qualquer deslocamento:

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

Uma vez que o buffer constante acima e o método auxiliar tenham sido definidos, o sombreador pode obter amostras de dados de imagem usando o seguinte:

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

Gravando dados de imagem

Direct2D espera que um sombreador defina um buffer de saída para a imagem resultante a ser colocada. No Shader Model 4 (DirectX 10), este deve ser um buffer unidimensional devido a restrições de recursos:

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

A textura de saída é indexada linha primeiro para permitir que toda a imagem seja armazenada.

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

Por outro lado, os sombreadores Shader Model 5 (DirectX 11) podem usar texturas de saída bidimensionais:

RWTexture2D<float4> OutputTexture : register(t1);

Com sombreadores Shader Model 5, Direct2D fornece um parâmetro 'outputOffset' adicional no buffer constante. A saída do sombreador deve ser compensada por este montante:

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

Um sombreador de computação Shader Model 5 de passagem concluído é mostrado abaixo. Nele, cada um dos threads do sombreador de computação lê e grava um único pixel da imagem de entrada.

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

O código abaixo mostra a versão equivalente Shader Model 4 do sombreador. Observe que o sombreador agora é renderizado em um buffer de saída unidimensional.

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

de exemplo do SDK D2DCustomEffects