Поделиться через


Пользовательские эффекты

Direct2D поставляется с библиотекой эффектов, которые выполняют различные распространенные операции с образами. Полный список эффектов см. в разделе о встроенных эффектах . Для функциональных возможностей, которые не могут быть достигнуты с помощью встроенных эффектов, Direct2D позволяет создавать собственные настраиваемые эффекты с помощью стандартного HLSL. Эти настраиваемые эффекты можно использовать вместе со встроенными эффектами, поставляемыми с Direct2D.

Примеры полного пикселя, вершины и эффекта шейдера вычислений см. в примере пакета SDK D2DCustomEffects.

В этом разделе приведены шаги и основные понятия, необходимые для разработки и создания полнофункционированного пользовательского эффекта.

Введение: Что внутри эффекта?

схема эффекта тени перетаскивания.

Концептуально эффект Direct2D выполняет задачу создания образа, например изменение яркости, денасыщение изображения или, как показано выше, создание тени. Для приложения они просты. Они могут принимать ноль или несколько входных изображений, предоставлять несколько свойств, управляющих их операцией, и создавать одно выходное изображение.

Существует четыре различных части настраиваемого эффекта, за которые отвечает автор эффектов:

  1. Интерфейс эффекта. Интерфейс эффекта концептуально определяет, как приложение взаимодействует с пользовательским эффектом (например, сколько входных данных принимает эффект и какие свойства доступны). Интерфейс эффектов управляет графом преобразования, который содержит фактические операции создания образов.
  2. Граф преобразования. Каждый эффект создает внутренний граф преобразования, состоящий из отдельных преобразований. Каждое преобразование представляет одну операцию изображения. Эффект отвечает за связывание этих преобразований в граф для выполнения предполагаемого эффекта визуализации. Эффект может добавлять, удалять, изменять и изменять порядок преобразований в ответ на изменения внешних свойств эффекта.
  3. Преобразование. Преобразование представляет одну операцию изображения. Его main целью является размещения шейдеров, которые выполняются для каждого выходного пикселя. С этой целью он отвечает за вычисление нового размера выходного изображения на основе логики в своих шейдерах. Он также должен вычислить, в какой области входного изображения шейдеры должны считывать, чтобы отобразить запрошенную выходную область.
  4. Шейдер. Шейдер выполняется на основе входных данных преобразования на GPU (или ЦП, если при создании приложением устройства Direct3D указано программное отрисовка). Шейдеры эффектов написаны на языке HLSL и компилируются в байтовый код во время компиляции эффекта, который затем загружается эффектом во время выполнения. В этом справочном документе описывается написание HLSL, совместимого с Direct2D. Документация по Direct3D содержит базовый обзор HLSL.

Создание интерфейса эффекта

Интерфейс эффекта определяет, как приложение взаимодействует с пользовательским эффектом. Чтобы создать интерфейс эффектов, класс должен реализовать ID2D1EffectImpl, определить метаданные, описывающие эффект (например, его имя, количество входных данных и свойства), а также создать методы, которые регистрируют пользовательский эффект для использования с Direct2D.

После реализации всех компонентов интерфейса эффекта заголовок класса будет выглядеть следующим образом:

#include <d2d1_1.h>
#include <d2d1effectauthor.h>  
#include <d2d1effecthelpers.h>

// Example GUID used to uniquely identify the effect. It is passed to Direct2D during
// effect registration, and used by the developer to identify the effect for any
// ID2D1DeviceContext::CreateEffect calls in the app. The app should create
// a unique name for the effect, as well as a unique GUID using a generation tool.
DEFINE_GUID(CLSID_SampleEffect, 0x00000000, 0x0000, 0x0000, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);

class SampleEffect : public ID2D1EffectImpl
{
public:
    // 2.1 Declare ID2D1EffectImpl implementation methods.
    IFACEMETHODIMP Initialize(
        _In_ ID2D1EffectContext* pContextInternal,
        _In_ ID2D1TransformGraph* pTransformGraph
        );

    IFACEMETHODIMP PrepareForRender(D2D1_CHANGE_TYPE changeType);
    IFACEMETHODIMP SetGraph(_In_ ID2D1TransformGraph* pGraph);

    // 2.2 Declare effect registration methods.
    static HRESULT Register(_In_ ID2D1Factory1* pFactory);
    static HRESULT CreateEffect(_Outptr_ IUnknown** ppEffectImpl);

    // 2.3 Declare IUnknown implementation methods.
    IFACEMETHODIMP_(ULONG) AddRef();
    IFACEMETHODIMP_(ULONG) Release();
    IFACEMETHODIMP QueryInterface(_In_ REFIID riid, _Outptr_ void** ppOutput);

private:
    // Constructor should be private since it should never be called externally.
    SampleEffect();

    LONG m_refCount; // Internal ref count used by AddRef() and Release() methods.
};

Реализация ID2D1EffectImpl

Интерфейс ID2D1EffectImpl содержит три метода, которые необходимо реализовать:

Initialize(ID2D1EffectContext *pContextInternal, ID2D1TransformGraph *pTransformGraph)

Direct2D вызывает метод Initialize после вызова приложением метода ID2D1DeviceContext::CreateEffect . Этот метод можно использовать для выполнения внутренней инициализации или любых других операций, необходимых для эффекта. Кроме того, его можно использовать для создания начального графа преобразования эффекта.

SetGraph(ID2D1TransformGraph *pTransformGraph)

Direct2D вызывает метод SetGraph при изменении количества входных данных для эффекта. Хотя большинство эффектов имеют постоянное количество входов, другие, такие как составной эффект , поддерживают переменное число входов. Этот метод позволяет этим эффектам обновлять граф преобразования в ответ на изменение количества входных данных. Если эффект не поддерживает число переменных входных данных, этот метод может просто вернуть E_NOTIMPL.

PrepareForRender (D2D1_CHANGE_TYPE changeType)

Метод PrepareForRender позволяет эффектам выполнять любые операции в ответ на внешние изменения. Direct2D вызывает этот метод непосредственно перед отображением эффекта, если хотя бы одно из них верно:

  • Эффект был ранее инициализирован, но еще не нарисован.
  • Свойство эффекта изменилось с момента последнего вызова draw.
  • Состояние вызывающего контекста Direct2D (например, DPI) изменилось с момента последнего вызова draw.

Реализация методов регистрации эффектов и обратного вызова

Приложения должны регистрировать эффекты в Direct2D , прежде чем создавать их экземпляры. Эта регистрация ограничена экземпляром фабрики Direct2D и должна повторяться при каждом запуске приложения. Чтобы включить эту регистрацию, настраиваемый эффект определяет уникальный GUID, открытый метод, регистрирующий эффект, и метод частного обратного вызова, возвращающий экземпляр эффекта.

Определение GUID

Необходимо определить GUID, который однозначно определяет эффект для регистрации с помощью Direct2D. Приложение использует то же самое для определения эффекта при вызове ID2D1DeviceContext::CreateEffect.

Этот код демонстрирует определение такого GUID для эффекта. Необходимо создать собственный уникальный GUID с помощью средства создания GUID, например 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);

Определение метода общедоступной регистрации

Затем определите открытый метод для вызова приложения для регистрации эффекта в Direct2D. Так как регистрация эффекта зависит от экземпляра фабрики Direct2D, метод принимает интерфейс ID2D1Factory1 в качестве параметра. Чтобы зарегистрировать эффект, метод вызывает API ID2D1Factory1::RegisterEffectFromString для параметра ID2D1Factory1 .

Этот API принимает XML-строку, описывающую метаданные, входные данные и свойства эффекта. Метаданные для эффекта доступны только в информационных целях и могут быть запрошены приложением через интерфейс ID2D1Properties . Входные данные и данные свойств, с другой стороны, используются Direct2D и представляют функциональные возможности эффекта.

Здесь показана XML-строка для минимального примера эффекта. Добавление настраиваемых свойств в XML рассматривается в разделе Добавление настраиваемых свойств в эффект.

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

Определение метода обратного вызова фабрики эффектов

Эффект также должен предоставлять частный метод обратного вызова, который возвращает экземпляр эффекта с помощью одного параметра IUnknown**. Указатель на этот метод предоставляется на Direct2D , когда эффект регистрируется с помощью API ID2D1Factory1::RegisterEffectFromString с помощью параметра 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;
}

Реализация интерфейса IUnknown

Наконец, эффект должен реализовать интерфейс IUnknown для совместимости с COM.

Создание графа преобразования эффекта

Эффект может использовать несколько различных преобразований (отдельных операций с изображением) для создания требуемого эффекта создания образа. Чтобы управлять порядком применения этих преобразований к входным изображениям, эффект упорядочивает их в граф преобразования. Граф преобразования может использовать эффекты и преобразования, включенные в Direct2D , а также пользовательские преобразования, созданные автором эффекта.

Использование преобразований, включенных в Direct2D

Это наиболее часто используемые преобразования, предоставляемые Direct2D.

Создание графа преобразования с одним узлом

После создания преобразования входные данные эффекта должны быть подключены к входным данным преобразования, а выходные данные преобразования должны быть подключены к выходным данным эффекта. Если эффект содержит только одно преобразование, для этого можно использовать метод ID2D1TransformGraph::SetSingleTransformNode .

Вы можете создать или изменить преобразование в методах Initialize или SetGraph эффекта с помощью предоставленного параметра ID2D1TransformGraph. Если эффекту необходимо внести изменения в граф преобразования в другом методе, где этот параметр недоступен, эффект может сохранить параметр ID2D1TransformGraph в качестве переменной-члена класса и получить к нему доступ в другом месте, например PrepareForRender или метод обратного вызова настраиваемого свойства.

Здесь показан пример метода Initialize . Этот метод создает граф преобразования с одним узлом, который смещает изображение на сто пикселей по каждой оси.

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

Создание графа преобразования с несколькими узлами

Добавление нескольких преобразований в граф преобразования эффекта позволяет эффектам выполнять несколько операций с изображениями, которые представляются приложению в виде единого единого эффекта.

Как отмечалось выше, граф преобразования эффекта можно изменить в любом методе эффекта с помощью параметра ID2D1TransformGraph , полученного в методе Initialize эффекта. Для создания или изменения графа преобразования эффекта можно использовать следующие API в этом интерфейсе:

AddNode(ID2D1TransformNode *pNode)

Метод AddNode , по сути, "регистрирует" преобразование с эффектом и должен быть вызван, прежде чем преобразование можно будет использовать с любым из других методов графа преобразования.

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

Метод ConnectToEffectInput соединяет входные данные изображения эффекта с входными данными преобразования. Один и тот же входный эффект может быть подключен к нескольким преобразованиям.

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

Метод ConnectNode подключает выходные данные преобразования к входным данным другого преобразования. Выходные данные преобразования можно подключить к нескольким преобразованиям.

SetOutputNode(ID2D1TransformNode *pNode)

Метод SetOutputNode подключает выходные данные преобразования к выходным данным эффекта. Так как эффект имеет только один выход, только одно преобразование может быть назначено в качестве выходного узла.

Этот код использует два отдельных преобразования для создания единого эффекта. В этом случае эффектом является преобразованная тень.

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

Добавление настраиваемых свойств в эффект

Эффекты могут определять пользовательские свойства, позволяющие приложению изменять поведение эффекта во время выполнения. Существует три шага для определения свойства для настраиваемого эффекта:

Добавление метаданных свойства в данные регистрации эффекта

Добавление свойства в XML регистрации

Необходимо определить свойства настраиваемого эффекта во время первоначальной регистрации эффекта в Direct2D. Во-первых, необходимо обновить XML регистрации эффекта в методе общедоступной регистрации с помощью нового свойства :

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

При определении свойства эффекта в XML ему требуется имя, тип и отображаемое имя. Отображаемое имя свойства, а также значения категории, автора и описания общего эффекта могут и должны быть локализованы.

Для каждого свойства эффект может при необходимости указать значения по умолчанию, минимальное и максимальное значение. Эти значения предназначены только для информационного использования. Они не применяются Direct2D. Вы можете самостоятельно реализовать любую указанную логику по умолчанию, мин или максимум в классе эффектов.

Значение типа, указанное в XML для свойства, должно соответствовать соответствующему типу данных, используемому методами получения и задания свойства. Соответствующие ЗНАЧЕНИЯ XML для каждого типа данных приведены в следующей таблице:

Тип данных Соответствующее значение XML
PWSTR строка
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[] большой двоичный объект
Iunknown* Iunknown
ID2D1ColorContext* colorcontext
CLSID clsid
Перечисление (D2D1_INTERPOLATION_MODE и т. д.) enum

 

Сопоставление нового свойства с методами получения и задания

Затем эффект должен сопоставить это новое свойство с методами getter и setter. Это делается с помощью массива D2D1_PROPERTY_BINDING , который передается в метод ID2D1Factory1::RegisterEffectFromString .

Массив D2D1_PROPERTY_BINDING выглядит следующим образом:

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

После создания массива XML и bindings передайте их в метод 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.
    );

Макрос D2D1_VALUE_TYPE_BINDING требует, чтобы класс эффектов наследовал от ID2D1EffectImpl перед любым другим интерфейсом.

Пользовательские свойства для эффекта индексируются в том порядке, в который они объявляются в XML, и после создания приложение может получить доступ с помощью методов ID2D1Properties::SetValue и ID2D1Properties::GetValue . Для удобства можно создать общедоступное перечисление, которое перечисляет каждое свойство в файле заголовка эффекта:

typedef enum SAMPLEEFFECT_PROP
{
    SAMPLEFFECT_PROP_OFFSET = 0
};

Создание методов получения и задания для свойства

Следующим шагом является создание методов получения и задания для нового свойства. Имена методов должны совпадать с именами, указанными в массиве D2D1_PROPERTY_BINDING . Кроме того, тип свойства, указанный в XML-коде эффекта, должен соответствовать типу параметра метода задания и возвращаемого значения метода получения.

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

Преобразования эффекта обновления в ответ на изменение свойства

Чтобы фактически обновить выходные данные изображения эффекта в ответ на изменение свойства, эффекту необходимо изменить его базовые преобразования. Обычно это делается в методе PrepareForRender эффекта, который Direct2D автоматически вызывает при изменении одного из свойств эффекта. Однако преобразования можно обновить в любом из методов эффекта, например Initialize или методах задания свойств эффекта.

Например, если эффект содержит ID2D1OffsetTransform и хочет изменить его значение смещения в ответ на изменение свойства Offset эффекта, он добавит следующий код в 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;
}

Создание пользовательского преобразования

Для реализации операций с образами, помимо операций, предоставляемых в Direct2D, необходимо реализовать пользовательские преобразования. Пользовательские преобразования могут произвольно изменять входное изображение с помощью пользовательских шейдеров HLSL.

Преобразования реализуют один из двух разных интерфейсов в зависимости от типов используемых ими шейдеров. Преобразования, использующие шейдеры пикселей и (или) вершин, должны реализовывать ID2D1DrawTransform, а преобразования, использующие шейдеры вычислений, должны реализовывать ID2D1ComputeTransform. Эти интерфейсы наследуются от ID2D1Transform. В этом разделе основное внимание уделяется реализации функций, общих для обоих.

Интерфейс ID2D1Transform имеет четыре метода для реализации:

GetInputCount

Этот метод возвращает целое число, представляющее количество входных данных для преобразования.

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

MapInputRectsToOutputRect

Direct2D вызывает метод MapInputRectsToOutputRect каждый раз при отрисовке преобразования. Direct2D передает в преобразование прямоугольник, представляющий границы каждого из входных данных. Затем преобразование отвечает за вычисление границ выходного изображения. Размер прямоугольников для всех методов в этом интерфейсе (ID2D1Transform) определяется в пикселях, а не в DIP.

Этот метод также отвечает за вычисление непрозрачной области выходных данных на основе логики его шейдера и непрозрачных областей каждого входного значения. Непрозрачная область изображения определяется как область, где альфа-канал имеет значение "1" для всей прямоугольника. Если неясно, являются ли выходные данные преобразования непрозрачными, для непрозрачного прямоугольника вывода должно быть задано значение (0, 0, 0, 0) в качестве безопасного значения. Direct2D использует эти сведения для оптимизации отрисовки с "гарантированно непрозрачным" содержимым. Если это значение является неточным, это может привести к неправильной отрисовке.

Можно изменить поведение отрисовки преобразования (как определено в разделах 6–8) во время этого метода. Однако вы не можете изменить другие преобразования в графе преобразования или самом макете графа.

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

В более сложном примере рассмотрим, как будет представлена простая операция размытия:

Если операция размытия использует радиус 5 пикселей, размер выходного прямоугольника должен увеличиться на 5 пикселей, как показано ниже. При изменении координат прямоугольника преобразование должно гарантировать, что его логика не приводит к переполнению или недополучению координат прямоугольника.

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

Так как изображение размыто, область изображения, которая была непрозрачной, теперь может быть частично прозрачной. Это связано с тем, что область за пределами изображения по умолчанию является прозрачной черной, и эта прозрачность будет смешиваться с изображением по краям. Преобразование должно отражать это в выходных вычислениях непрозрачного прямоугольника:

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

Эти вычисления визуализированы здесь:

Иллюстрация вычисления прямоугольника.

Дополнительные сведения об этом методе см. на странице справочника MapInputRectsToOutputRect .

MapOutputRectToInputRects

Direct2D вызывает метод MapOutputRectToInputRects после MapInputRectsToOutputRects. Преобразование должно вычислить, из какой части изображения необходимо считывать, чтобы правильно отобразить запрошенную выходную область.

Как и раньше, если эффект строго сопоставляет пиксели 1–1, он может передать выходной прямоугольник через входной прямоугольник:

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

Аналогичным образом, если преобразование сжимает или расширяет изображение (как в примере размытия), пиксели часто используют окружающие пиксели для вычисления их значения. При размытии пиксель усреднен по окружающим пикселям, даже если они находятся за пределами входного изображения. Это поведение отражается в вычислении. Как и раньше, преобразование проверяет переполнения при расширении координат прямоугольника.

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

На этом рисунке показана визуализация вычисления. Direct2D автоматически выполняет выборку прозрачных черных пикселей там, где входное изображение не существует, что позволяет постепенно смешивать размытие с существующим содержимым на экране.

иллюстрация выборки прозрачных черных пикселей за пределами прямоугольника.

Если сопоставление нетривиальное, этот метод должен задать для входного прямоугольника максимальную область, чтобы гарантировать правильные результаты. Для этого задайте левый и верхний края INT_MIN, а правый и нижний края — INT_MAX.

Дополнительные сведения об этом методе см. в разделе MapOutputRectToInputRects .

MapInvalidRect

Direct2D также вызывает метод MapInvalidRect . Однако, в отличие от методов MapInputRectsToOutputRect и MapOutputRectToInputRects , Direct2D не гарантирует вызов его в определенный момент времени. Этот метод концептуально определяет, какая часть выходных данных преобразования должна быть повторно отображена в ответ на изменение части или всей его входной части. Существует три разных сценария, в которых вычисляется недопустимый прямоугольник преобразования.

Преобразования с сопоставлением пикселей "один к одному"

Для преобразований, которые сопоставляют пиксели 1–1, просто передайте недопустимый входной прямоугольник в недопустимый выходной прямоугольник:

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

Преобразования с сопоставлением пикселей "многие ко многим"

Если выходные пиксели преобразования зависят от окружающей области, необходимо соответствующим образом развернуть недопустимый входной прямоугольник. Это отражает, что пиксели, окружающие недопустимый входной прямоугольник, также будут затронуты и становятся недействительными. Например, для размытия пяти пикселей используется следующее вычисление:

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

Преобразования со сложным сопоставлением пикселей

Для преобразований, в которых входные и выходные пиксели не имеют простого сопоставления, все выходные данные можно пометить как недопустимые. Например, если преобразование просто выводит средний цвет входных данных, все выходные данные преобразования изменяются, если изменяется даже небольшая часть входных данных. В этом случае для недопустимого выходного прямоугольника следует задать логически бесконечный прямоугольник (показано ниже). Direct2D автоматически зажимает его в границах выходных данных.

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

Дополнительные сведения об этом методе см. в разделе MapInvalidRect .

После реализации этих методов заголовок преобразования будет содержать следующее:

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

Добавление пиксельного шейдера в пользовательское преобразование

После создания преобразования необходимо предоставить шейдер, который будет управлять пикселями изображения. В этом разделе рассматриваются шаги по использованию пиксельного шейдера с пользовательским преобразованием.

Реализация ID2D1DrawTransform

Чтобы использовать пиксельный шейдер, преобразование должно реализовывать интерфейс ID2D1DrawTransform , который наследуется от интерфейса ID2D1Transform , описанного в разделе 5. Этот интерфейс содержит один новый метод для реализации:

SetDrawInfo(ID2D1DrawInfo *pDrawInfo)

Direct2D вызывает метод SetDrawInfo при первом добавлении преобразования в граф преобразования эффекта. Этот метод предоставляет параметр ID2D1DrawInfo , который управляет способом отрисовки преобразования. Доступные здесь методы см. в разделе ID2D1DrawInfo .

Если преобразование решит сохранить этот параметр в качестве переменной-члена класса, доступ к объекту drawInfo можно получить и изменить из других методов, таких как методы задания свойств или MapInputRectsToOutputRect. В частности, его нельзя вызвать из методов MapOutputRectToInputRects или MapInvalidRect в ID2D1Transform.

Создание GUID для пиксельного шейдера

Затем преобразование должно определить уникальный идентификатор GUID для самого пиксельного шейдера. Это используется, когда Direct2D загружает шейдер в память, а также когда преобразование выбирает, какой пиксельный шейдер будет использоваться для выполнения. Для создания случайного GUID можно использовать такие средства, как guidgen.exe, которые входят в состав Visual Studio.

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

Загрузка пиксельного шейдера с помощью Direct2D

Пиксельный шейдер должен быть загружен в память, прежде чем его можно будет использовать преобразованием.

Чтобы загрузить пиксельный шейдер в память, преобразование должно считывать скомпилированный байтовый код шейдера из . CsO-файл, созданный Visual Studio (дополнительные сведения см. в документации по Direct3D ) в массиве байтов. Этот метод подробно демонстрируется в примере пакета SDK D2DCustomEffects.

После загрузки данных шейдера в массив байтов вызовите метод LoadPixelShader для объекта ID2D1EffectContext эффекта. Direct2D игнорирует вызовы LoadPixelShader , когда уже загружен шейдер с тем же GUID.

После загрузки пиксельного шейдера в память преобразование должно выбрать его для выполнения, передав его GUID методу SetPixelShader с параметром ID2D1DrawInfo , указанным во время метода SetDrawInfo . Пиксельный шейдер должен быть уже загружен в память перед тем, как его выбрать для выполнения.

Изменение операции шейдера с помощью буферов констант

Чтобы изменить способ выполнения шейдера, преобразование может передать буфер констант в пиксельный шейдер. Для этого преобразование определяет структуру, содержащую нужные переменные в заголовке класса:

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

Затем преобразование вызывает метод ID2D1DrawInfo::SetPixelShaderConstantBuffer для параметра ID2D1DrawInfo , предоставленного в методе SetDrawInfo , чтобы передать этот буфер шейдеру.

HLSL также должен определить соответствующую структуру, представляющую буфер констант. Переменные, содержащиеся в структуре шейдера, должны соответствовать переменным в структуре преобразования.

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

После определения буфера значения, содержащиеся в нем, можно считывать из любого места в шейдере пикселей.

Создание пиксельного шейдера для Direct2D

Преобразования Direct2D используют шейдеры, созданные с помощью стандартного HLSL. Однако существует несколько ключевых понятий для написания пиксельного шейдера, который выполняется из контекста преобразования. Полный пример полнофункциональной текстуры шейдера см. в примере пакета SDK D2DCustomEffects.

Direct2D автоматически сопоставляет входные данные преобразования с объектами Texture2D и SamplerState в HLSL. Первый Texture2D находится в регистре t0, а первый SamplerState — в регистре s0. Каждый дополнительный вход находится в следующих соответствующих регистрах (например, t1 и s1). Пиксельные данные для определенных входных данных можно использовать для выборки путем вызова Sample в объекте Texture2D и передачи соответствующего объекта SamplerState и координат текселя.

Пользовательский шейдер пикселей запускается один раз для каждого отображаемого пикселя. При каждом запуске шейдера Direct2D автоматически предоставляет три параметра, определяющие его текущую позицию выполнения:

  • Выходные данные сцены: этот параметр представляет текущую позицию выполнения с точки зрения общей целевой поверхности. Он определяется в пикселях, а его значения min/max соответствуют границам прямоугольника, возвращаемого mapInputRectsToOutputRect.
  • Выходные данные клипов: этот параметр используется Direct3D и не должен использоваться в пиксельном шейдере преобразования.
  • Входные данные texel-space: этот параметр представляет текущую позицию выполнения в определенной входной текстуре. Шейдер не должен принимать никаких зависимостей от того, как вычисляется это значение. Он должен использовать его только для выборки входных данных пиксельного шейдера, как показано в следующем коде:
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;
}

Добавление вершинного шейдера в пользовательское преобразование

Вы можете использовать вершинные шейдеры для выполнения различных сценариев создания изображений, чем пиксельные шейдеры. В частности, вершинные шейдеры могут выполнять эффекты изображений на основе геометрии путем преобразования вершин, составляющих изображение. Вершинные шейдеры можно использовать независимо от или в сочетании с шейдерами пикселей, заданными преобразованием. Если вершинный шейдер не указан, Direct2D заменяет шейдер вершин по умолчанию для использования с пользовательским шейдером пикселей.

Процесс добавления вершинного шейдера в пользовательское преобразование аналогичен процессу пиксельного шейдера — преобразование реализует интерфейс ID2D1DrawTransform , создает GUID и (необязательно) передает буферы констант в шейдер. Однако существует несколько ключевых дополнительных шагов, уникальных для вершинных шейдеров:

Создание буфера вершин

Вершинный шейдер по определению выполняется на переданных ему вершинах, а не на отдельных пикселях. Чтобы указать вершины для выполнения шейдера, преобразование создает буфер вершин для передачи шейдеру. Структура буферов вершин выходит за рамки область этого документа. Дополнительные сведения см. в справочнике по Direct3D или в примере пакета SDK D2DCustomEffects для примера реализации.

После создания буфера вершин в памяти преобразование использует метод CreateVertexBuffer для объекта ID2D1EffectContext содержащего эффекта, чтобы передать эти данные в GPU. Опять же, ознакомьтесь с примером реализации пакета SDK D2DCustomEffects .

Если в преобразовании не указан буфер вершин, Direct2D передает буфер вершин по умолчанию, представляющий расположение прямоугольного изображения.

Изменение SetDrawInfo для использования вершинного шейдера

Как и в случае с пиксельными шейдерами, преобразование должно загружаться и выбирать вершинный шейдер для выполнения. Чтобы загрузить вершинный шейдер, он вызывает метод LoadVertexShader в методе ID2D1EffectContext , полученном в методе Initialize эффекта. Чтобы выбрать вершинный шейдер для выполнения, он вызывает SetVertexProcessing для параметра ID2D1DrawInfo , полученного в методе SetDrawInfo преобразования. Этот метод принимает GUID для ранее загруженного вершинного шейдера, а также (необязательно) ранее созданный буфер вершин для выполнения шейдера.

Реализация вершинного шейдера Direct2D

Преобразование рисования может содержать как пиксельный шейдер, так и вершинный шейдер. Если преобразование определяет как пиксельный шейдер, так и вершинный шейдер, то выходные данные вершинного шейдера передаются непосредственно шейдеру пикселей: приложение может настроить возвращаемую сигнатуру вершинного шейдера или параметры шейдера пикселей, если они согласованы.

С другой стороны, если преобразование содержит только вершинный шейдер и использует сквозной шейдер пикселей Direct2D по умолчанию, оно должно возвращать следующие выходные данные по умолчанию:

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

Вершинный шейдер сохраняет результат преобразований вершин в выходной переменной scene-space шейдера. Чтобы вычислить выходные данные clip-space и входные переменные Texel-space, 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;
};

Ниже приведен пример кода шейдера вершин, который использует матрицы преобразования для вычисления правильных интервалов клипа и текселя, ожидаемых 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;  
}

Приведенный выше код можно использовать в качестве отправной точки для вершинного шейдера. Он просто проходит через входное изображение, не выполняя никаких преобразований. Опять же, ознакомьтесь с примером пакета SDK D2DCustomEffects для полного преобразования на основе вершинного шейдера.

Если в преобразовании не указан буфер вершин, Direct2D заменяет буфер вершин по умолчанию, представляющий расположение прямоугольного изображения. Параметры вершинного шейдера изменяются на параметры выходных данных шейдера по умолчанию:

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

Вершинный шейдер не может изменять параметры sceneSpaceOutput и clipSpaceOutput . Он должен возвращать их без изменений. Однако он может изменять параметры texelSpaceInput для каждого входного изображения. Если преобразование также содержит пользовательский шейдер пикселей, вершинный шейдер по-прежнему может передавать дополнительные настраиваемые параметры непосредственно в шейдер пикселей. Кроме того, пользовательский буфер матриц преобразования sceneSpace (b0) больше не предоставляется.

Добавление вычислительного шейдера в пользовательское преобразование

Наконец, пользовательские преобразования могут использовать вычислительные шейдеры для определенных целевых сценариев. Вычислительные шейдеры можно использовать для реализации сложных эффектов изображений, требующих произвольного доступа к буферам входных и выходных изображений. Например, базовый алгоритм гистограммы не может быть реализован с помощью пиксельного шейдера из-за ограничений на доступ к памяти.

Так как вычислительные шейдеры имеют более высокие требования к аппаратному уровню функций, чем шейдеры пикселей, по возможности следует использовать пиксельные шейдеры для реализации заданного эффекта. В частности, вычислительные шейдеры выполняются только на большинстве карт уровня DirectX 10 и более поздних версий. Если преобразование решает использовать вычислительный шейдер, оно должно проверка для соответствующей аппаратной поддержки во время создания экземпляра в дополнение к реализации интерфейса ID2D1ComputeTransform.

Проверка поддержки вычислительного шейдера

Если эффект использует вычислительный шейдер, он должен проверка для поддержки вычислительного шейдера во время создания с помощью метода ID2D1EffectContext::CheckFeatureSupport. Если GPU не поддерживает вычислительные шейдеры, эффект должен возвращать D2DERR_INSUFFICIENT_DEVICE_CAPABILITIES.

Существует два разных типа вычислительных шейдеров, которые может использовать преобразование: модель шейдера 4 (DirectX 10) и модель шейдера 5 (DirectX 11). Существуют определенные ограничения для шейдеров модели 4 шейдеров. Дополнительные сведения см. в документации по Direct3D . Преобразования могут содержать оба типа шейдеров и при необходимости могут возвращаться к модели шейдера 4: см. пример пакета SDK D2DCustomEffects .

Реализация ID2D1ComputeTransform

Этот интерфейс содержит два новых метода для реализации в дополнение к методам в ID2D1Transform:

SetComputeInfo(ID2D1ComputeInfo *pComputeInfo)

Как и в случае с шейдерами пикселей и вершин, Direct2D вызывает метод SetComputeInfo при первом добавлении преобразования в граф преобразования эффекта. Этот метод предоставляет параметр ID2D1ComputeInfo , который управляет способом отрисовки преобразования. Сюда входит выбор вычислительного шейдера для выполнения с помощью метода ID2D1ComputeInfo::SetComputeShader . Если преобразование решит сохранить этот параметр в качестве переменной-члена класса, к нему можно получить доступ и изменить из любого метода преобразования или эффекта, за исключением методов MapOutputRectToInputRects и MapInvalidRect. Другие доступные здесь методы см. в разделе ID2D1ComputeInfo .

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

В то время как пиксельные шейдеры выполняются по пикселям, а вершинные шейдеры — для каждой вершины, вычислительные шейдеры выполняются на основе группы потоков. Группа потоков представляет собой несколько потоков, которые выполняются одновременно на GPU. Код HLSL шейдера вычислений определяет, сколько потоков должно быть выполнено на каждую группу потоков. Эффект масштабирует количество групп потоков, чтобы шейдер выполнял требуемое количество раз в зависимости от логики шейдера.

Метод CalculateThreadgroups позволяет преобразованию сообщать Direct2D , сколько групп потоков необходимо, в зависимости от размера изображения и собственных знаний преобразования о шейдере.

Количество выполнений шейдера вычислений является результатом указанного здесь количества групп потоков и заметки numthreads в HLSL вычислительного шейдера. Например, если преобразование задает для размеров группы потоков значение (2,2,1), шейдер задает (3,3,1) потоков на группу потоков, то будут выполнены 4 группы потоков, каждая из которых содержит 9 потоков, в общей сложности 36 экземпляров потоков.

Распространенным сценарием является обработка одного выходного пикселя для каждого экземпляра вычислительного шейдера. Чтобы вычислить количество групп потоков для этого сценария, преобразование делит ширину и высоту изображения на соответствующие размеры x и y заметки numthreads в HLSL вычислительного шейдера.

Важно отметить, что если выполняется это деление, количество запрошенных групп потоков всегда должно округлить до ближайшего целого числа, в противном случае остальные пиксели не будут выполняться. Если шейдер (например) вычисляет один пиксель с каждым потоком, код метода будет выглядеть следующим образом.

IFACEMETHODIMP SampleTransform::CalculateThreadgroups(
    _In_ const D2D1_RECT_L* pOutputRect,
    _Out_ UINT32* pDimensionX,
    _Out_ UINT32* pDimensionY,
    _Out_ UINT32* pDimensionZ
    )
{    
    // The input image's dimensions are divided by the corresponding number of threads in each
    // threadgroup. This is specified in the HLSL, and in this example is 24 for both the x and y
    // dimensions. Dividing the image dimensions by these values calculates the number of
    // thread groups that need to be executed.

    *pDimensionX = static_cast<UINT32>(
         ceil((m_inputRect.right - m_inputRect.left) / 24.0f);

    *pDimensionY = static_cast<UINT32>(
         ceil((m_inputRect.bottom - m_inputRect.top) / 24.0f);

    // The z dimension is set to '1' in this example because the shader will
    // only be executed once for each pixel in the two-dimensional input image.
    // This value can be increased to perform additional executions for a given
    // input position.
    *pDimensionZ = 1;

    return S_OK;
}

HLSL использует следующий код, чтобы указать количество потоков в каждой группе потоков:

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

Во время выполнения текущая группа потоков и индекс текущего потока передаются в качестве параметров методу шейдера:

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

Чтение данных изображения

Вычислительные шейдеры обращаются к входным изображениям преобразования как к одной двумерной текстуре:

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

Однако, как и пиксельные шейдеры, данные изображения не гарантированно начинаются с (0, 0) на текстуре. Вместо этого Direct2D предоставляет системные константы, которые позволяют шейдерам компенсировать любое смещение:

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

После определения указанного выше буфера констант и вспомогательного метода шейдер может выполнить выборку данных изображения с помощью следующего:

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

Запись данных изображения

Direct2D ожидает, что шейдер определит выходной буфер для размещения результирующего изображения. В модели шейдера 4 (DirectX 10) это должен быть одномерный буфер из-за ограничений функций:

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

Текстура выходных данных индексируется по строке, чтобы сохранить все изображение.

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

С другой стороны, шейдеры модели 5 (DirectX 11) могут использовать двухмерные выходные текстуры:

RWTexture2D<float4> OutputTexture : register(t1);

При использовании шейдеров модели 5 шейдеров Direct2D предоставляет дополнительный параметр outputOffset в буфере констант. Выходные данные шейдера должны быть смещены на следующую величину:

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

Ниже показан завершенный шейдер вычислений модели 5 сквозного шейдера. В нем каждый поток вычислительного шейдера считывает и записывает один пиксель входного изображения.

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

В приведенном ниже коде показана эквивалентная версия шейдера модели 4 шейдера. Обратите внимание, что теперь шейдер преобразуется в одномерный выходной буфер.

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

Пример пакета SDK D2DCustomEffects