Compartilhar via


Efeitos personalizados

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

Para ver exemplos de um efeito de sombreador de computação, vértice e pixel completo, consulte o exemplo do 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 efeito de sombra.

Conceitualmente, um efeito Direct2D executa uma tarefa de geração de imagens, como alterar o brilho, desassaturar uma imagem ou, conforme 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 efeito 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 efeito gerencia um grafo de transformação, que contém as operações reais de geração de imagens.
  2. Transformar grafo: cada efeito cria um grafo de transformação interno composto por transformações individuais. Cada transformação representa uma única operação de imagem. O efeito é responsável por vincular essas transformações em um grafo para executar 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. Transformação: uma transformação representa uma única operação de imagem. Sua finalidade main é hospedar os sombreadores 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 de qual área de sua imagem de entrada os sombreadores precisam ler para renderizar a região de saída solicitada.
  4. Sombreador: 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). Sombreadores de efeito são escritos em HLSL (Linguagem de Sombreamento de Alto Nível) e são compilados em código de byte durante a compilação do efeito, que é carregado pelo efeito durante o tempo de execução. Este documento de referência descreve como escrever HLSL compatível com Direct2D. 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 descrevam o efeito (como seu nome, contagem de entrada e propriedades) e criar métodos que registram 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 será exibido da seguinte maneira:

#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 interface ID2D1EffectImpl contém três métodos que você deve implementar:

Initialize(ID2D1EffectContext *pContextInternal, ID2D1TransformGraph *pTransformGraph)

O Direct2D chama o método Initialize depois que o método ID2D1DeviceContext::CreateEffect 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 grafo de transformação inicial do efeito.

SetGraph(ID2D1TransformGraph *pTransformGraph)

O Direct2D chama o método SetGraph quando o número de entradas para o efeito é alterado. Embora a maioria dos efeitos tenha um número constante de entradas, outros como o efeito Composto dão suporte a um número variável de entradas. Esse método permite que esses efeitos atualizem seu grafo de transformação em resposta a uma alteração na contagem de entradas. Se um efeito não der suporte a uma contagem de entrada variável, esse método poderá simplesmente retornar E_NOTIMPL.

PrepareForRender (D2D1_CHANGE_TYPE changeType)

O método PrepareForRender fornece uma oportunidade para que os efeitos executem quaisquer operações em resposta a alterações externas. O Direct2D chama esse método pouco antes de renderizar um efeito se pelo menos um deles for verdadeiro:

  • O efeito foi inicializado anteriormente, mas ainda não foi desenhado.
  • Uma propriedade de efeito foi alterada desde a última chamada de desenho.
  • O estado do contexto direct2D de chamada (como DPI) foi alterado desde a última chamada de desenho.

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

Os aplicativos devem registrar efeitos com o 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 é 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 registro com Direct2D. O aplicativo usa o mesmo para identificar o efeito quando chama ID2D1DeviceContext::CreateEffect.

Esse código demonstra a definição desse 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 registro 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 uma interface ID2D1Factory1 como um parâmetro. Para registrar o efeito, o método chama a API ID2D1Factory1::RegisterEffectFromString no parâmetro ID2D1Factory1 .

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

Uma cadeia de caracteres XML para um efeito de exemplo mínimo é mostrada aqui. A adição de propriedades personalizadas ao XML é abordada na seção Adicionar 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 ao Direct2D quando o efeito é registrado por meio da API ID2D1Factory1::RegisterEffectFromString por meio 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

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

Criando o grafo 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 na qual essas transformações são aplicadas à imagem de entrada, o efeito organiza-as em um grafo de transformação. Um grafo de transformação pode usar os efeitos e transformações incluídos no Direct2D , bem como transformações personalizadas criadas pelo autor do efeito.

Usando transformações incluídas no Direct2D

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

Criando um grafo 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 nos métodos Initialize ou SetGraph do efeito usando o parâmetro ID2D1TransformGraph fornecido. Se um efeito precisar fazer alterações no grafo de transformação em outro método em que esse parâmetro não está disponível, o efeito poderá salvar o parâmetro ID2D1TransformGraph como 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 método Initialize de exemplo é mostrado aqui. Esse método cria um grafo 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 grafo de transformação de vários nós

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

Conforme observado acima, o grafo 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 seguintes APIs nessa interface podem ser usadas para criar ou modificar o grafo 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 grafo 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. Nesse caso, o efeito é uma sombra de soltar 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;
}

Adicionar propriedades personalizadas a um efeito

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

Adicionar os metadados de 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 o 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 de efeito em XML, ela precisa de um nome, um tipo e um nome de exibição. O nome de exibição de uma propriedade, bem como os valores 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, mínimo e máximo. Esses valores são apenas para uso informativo. Elas não são impostas pelo Direct2D. Cabe a você implementar qualquer lógica padrão/min/max especificada na classe de efeito por conta própria.

O valor de tipo listado no XML da 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
FLOAT FLOAT
D2D_VECTOR_2F vector2
D2D_VECTOR_3F vector3
D2D_VECTOR_4F vector4
D2D_MATRIX_3X2_F matrix3x2
D2D_MATRIX_4X3_F matrix4x3
D2D_MATRIX_4X4_F matrix4x4
D2D_MATRIX_5X4_F matrix5x4
BYTE[] blob
Iunknown* Iunknown
ID2D1ColorContext* Colorcontext
CLSID clsid
Enumeração (D2D1_INTERPOLATION_MODE etc.) enum

 

Mapear a nova propriedade para métodos getter e setter

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

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 associações, passe-as para o método 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.
    );

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

As propriedades personalizadas para 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 . Para 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 retornado 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 da 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 normalmente é feito no método PrepareForRender do efeito que o 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 Métodos de inicialização ou setter de propriedade do efeito.

Por exemplo, se um efeito contiver um ID2D1OffsetTransform e quisesse modificar seu valor de deslocamento em resposta à alteração da propriedade Offset do efeito, ela 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 no Direct2D, você deve implementar transformações personalizadas. Transformações personalizadas podem alterar arbitrariamente uma imagem de entrada por meio 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 usando sombreadores de pixel e/ou vértice devem implementar ID2D1DrawTransform, enquanto as transformações usando 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.

A 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

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

Esse método também é responsável por calcular a região da saída opaca com base na lógica de seu sombreador e nas 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. O Direct2D usa essas informações para executar otimizações de renderização com conteúdo 'opaco garantido'. Se esse valor for impreciso, ele poderá resultar em renderização incorreta.

O que 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 grafo de transformação ou o layout do grafo 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 obter um exemplo mais complexo, considere como uma operação de desfoque simples seria representada:

Se uma operação de desfoque usar um raio de 5 pixels, o tamanho do retângulo de saída deverá ser expandido em 5 pixels, conforme mostrado abaixo. Ao modificar coordenadas de retângulo, uma transformação deve garantir que sua lógica não cause nenhum 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 agora pode ser parcialmente transparente. Isso ocorre porque a área fora da imagem usa como padrão preto transparente e essa transparência será combinada à 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;

Esses cálculos são visualizados aqui:

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

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

MapOutputRectToInputRects

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

Como antes, se um efeito mapear estritamente pixels 1-1, ele poderá 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 reduzir ou expandir uma imagem (como o exemplo de desfoque aqui), os pixels geralmente usam pixels ao redor para calcular seu valor. Com um desfoque, um pixel é médio com seus pixels ao redor, mesmo que 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;

Essa figura visualiza o cálculo. O Direct2D amostra automaticamente pixels pretos transparentes em que a imagem de entrada não existe, permitindo que o desfoque seja mesclado gradualmente com o conteúdo existente na tela.

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

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

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

MapInvalidRect

Direct2D também chama o método MapInvalidRect . No entanto, ao contrário dos métodos MapInputRectsToOutputRect e MapOutputRectToInputRects , não há garantia de que o Direct2D o chame em nenhum momento específico. Esse método decide conceitualmente qual parte da saída de uma transformação precisa ser renderizada novamente em resposta a parte ou a toda a sua alteração de entrada. Há três cenários diferentes para os quais calcular o rect inválido de uma transformação.

Transformações com mapeamento de pixel um para um

Para transformações que mapeiam pixels de 1 a 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ções com mapeamento de pixel muitos para muitos

Quando os pixels de saída de uma transformação dependem da área ao redor, o retângulo de entrada inválido deve ser expandido correspondentemente. Isso é para refletir que 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ções com mapeamento de pixel complexo

Para transformações em que 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 gerar a cor média da entrada, toda a saída da transformação será alterada se até mesmo uma pequena parte da entrada for alterada. Nesse caso, o retângulo de saída inválido deve ser definido como um retângulo logicamente infinito (mostrado abaixo). O Direct2D fixa automaticamente isso nos 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 tópico MapInvalidRect .

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

Depois que uma transformação for criada, ela precisará 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 interface ID2D1DrawTransform , que herda da interface ID2D1Transform descrita na seção 5. Essa interface contém um novo método a ser implementado:

SetDrawInfo(ID2D1DrawInfo *pDrawInfo)

O Direct2D chama o método SetDrawInfo quando a transformação é adicionada pela primeira vez ao grafo de transformação de um efeito. Esse método fornece um parâmetro ID2D1DrawInfo 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 dos métodos MapOutputRectToInputRects ou MapInvalidRect em ID2D1Transform.

Criando um GUID para o sombreador de pixel

Em seguida, a transformação deve definir um GUID exclusivo para o sombreador de pixel em si. Isso é usado quando o 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ída 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 . Arquivo CSO gerado pelo Visual Studio (consulte a documentação do Direct3D para obter detalhes) em uma matriz de bytes. Essa técnica é demonstrada em detalhes no exemplo do SDK D2DCustomEffects.

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

Depois que um sombreador de pixel for carregado na memória, a transformação precisará selecioná-lo para execução passando seu GUID para o método SetPixelShader no 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 como um sombreador é executado, uma transformação pode passar um buffer constante para o sombreador de pixel. Para fazer isso, uma transformação define um 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 ID2D1DrawInfo::SetPixelShaderConstantBuffer no parâmetro ID2D1DrawInfo fornecido no método SetDrawInfo para passar esse buffer para o sombreador.

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

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

Depois que o buffer tiver sido definido, os valores contidos em poderão ser lidos de qualquer lugar dentro do sombreador de pixel.

Gravando um sombreador de pixel para Direct2D

As transformações direct2D usam sombreadores criados usando HLSL padrão. No entanto, há alguns conceitos importantes 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 do SDK D2DCustomEffects.

O Direct2D mapeia automaticamente as entradas de uma transformação para objetos Texture2D e 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 registros correspondentes (t1 e s1, por exemplo). 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, o Direct2D fornece automaticamente três parâmetros que identificam sua posição de execução atual:

  • Saída de espaço de cena: esse 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 mínimo/máximo 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 de espaço texel: esse parâmetro representa a posição de execução atual em uma textura de entrada específica. Um sombreador não deve assumir nenhuma dependência de como esse valor é calculado. Ele só deve usá-lo para amostrar a 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 geração de imagens diferentes dos sombreadores de pixel. Em particular, sombreadores de vértice podem executar efeitos de imagem baseados em geometria transformando vértices que compõem uma imagem. Sombreadores de vértice podem ser usados independentemente de ou em conjunto com sombreadores de pixel especificados pela transformação. Se um sombreador de vértice não for especificado, o Direct2D substituirá em 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 interface ID2D1DrawTransform , cria um GUID e (opcionalmente) passa buffers constantes para o sombreador. No entanto, há 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 em pixels individuais. Para especificar os vértices para o sombreador a ser executado, 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 a referência do Direct3D para obter detalhes ou o 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 contido para passar esses dados para a GPU. Novamente, consulte o exemplo do SDK D2DCustomEffects para obter uma implementação de exemplo.

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

Alterando SetDrawInfo para utilizar um sombreador de vértice

Assim como acontece 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étodo LoadVertexShader no método ID2D1EffectContext 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étodo SetDrawInfo da transformação. Esse 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 definir um sombreador de pixel e um sombreador de vértice, a saída do sombreador de vértice será fornecida diretamente ao sombreador de pixel: o aplicativo poderá 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 do Direct2D, 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 Espaço de cena do sombreador. Para calcular a saída de espaço de clipe e as variáveis de entrada texel-space, o Direct2D fornece matrizes de conversão automaticamente 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;
};

O código de sombreador de vértice de exemplo pode ser encontrado abaixo que usa as matrizes de conversão para calcular os espaços de recorte e texel corretos esperados pelo 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 ponto de partida para um sombreador de vértice. Ele simplesmente passa pela imagem de entrada sem executar nenhuma transformação. Novamente, consulte o 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, o Direct2D substituirá em um buffer de vértice padrão que representa o local da imagem retangular. Os parâmetros para o sombreador de vértice são alterados para aqueles da saída do sombreador padrão:

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

O sombreador de vértice pode não modificar seus parâmetros sceneSpaceOutput e clipSpaceOutput . Ele deve retorná-los inalterados. No entanto, ele pode modificar os parâmetros 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 buffer personalizado das matrizes de conversão sceneSpace (b0) não é mais fornecido.

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

Por fim, as transformações personalizadas podem utilizar sombreadores de computação para determinados cenários de destino. 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ão executados apenas na maioria dos cartões de nível DirectX 10 e superiores. Se uma transformação optar por usar um sombreador de computação, ela deverá marcar para o suporte de hardware apropriado durante a instanciação, além de implementar a interface ID2D1ComputeTransform.

Verificando o suporte ao sombreador de computação

Se um efeito usar um sombreador de computação, ele deverá marcar para suporte ao sombreador de computação durante sua criação usando o método ID2D1EffectContext::CheckFeatureSupport. Se a GPU não der suporte a 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: Modelo de Sombreador 4 (DirectX 10) e Modelo de Sombreador 5 (DirectX 11). Há certas limitações para sombreadores do Modelo de Sombreador 4. Consulte a documentação do Direct3D para obter detalhes. As transformações podem conter ambos os tipos de sombreadores e podem voltar para o Modelo de Sombreador 4 quando necessário: consulte o exemplo do SDK D2DCustomEffects para obter uma implementação disso.

Implementar ID2D1ComputeTransform

Essa interface contém dois novos métodos a serem implementados além dos que estão em ID2D1Transform:

SetComputeInfo(ID2D1ComputeInfo *pComputeInfo)

Assim como acontece com sombreadores de pixel e vértice, o Direct2D chama o método SetComputeInfo quando a transformação é adicionada pela primeira vez ao grafo de transformação de um efeito. Esse método fornece um parâmetro ID2D1ComputeInfo 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 dos métodos MapOutputRectToInputRects e 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 sombreadores de pixel são executados por pixel e 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étodo CalculateThreadgroups permite que a transformação informe ao 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 a 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, 4 threadgroups serão executados, cada um com 9 threads, 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 esse cenário, a transformação divide a largura e a altura da imagem pelas respectivas 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 sempre deverá ser arredondado para cima até o 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 seguinte código 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 de sombreador:

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

Lendo dados de imagem

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 há garantia de que os dados da imagem comecem em (0, 0) na textura. Em vez disso, o Direct2D fornece constantes do sistema que permitem que 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;
}

Depois que o buffer constante acima e o método auxiliar tiverem sido definidos, o sombreador poderá amostrar 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

O Direct2D espera que um sombreador defina um buffer de saída para a imagem resultante a ser colocada. No Modelo de Sombreador 4 (DirectX 10), esse 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 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, sombreadores do Modelo de Sombreador 5 (DirectX 11) podem usar texturas de saída bidimensionais:

RWTexture2D<float4> OutputTexture : register(t1);

Com sombreadores do Modelo de Sombreador 5, o Direct2D fornece um parâmetro 'outputOffset' adicional no buffer constante. A saída do sombreador deve ser compensada por esse valor:

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

Um sombreador de computação do Modelo de Sombreador de Passagem 5 concluído é mostrado abaixo. Nele, cada um dos threads de 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 a seguir mostra a versão equivalente do Modelo de Sombreador 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;
}

Exemplo do SDK D2DCustomEffects