自定义效果

Direct2D 附带了执行各种常见图像操作的效果库。 有关效果的完整列表,请参阅 内置 效果主题。 对于无法通过内置效果实现的功能,Direct2D 允许使用标准 HLSL 编写自己的自定义效果。 可以将这些自定义效果与 Direct2D 附带的内置效果一起使用。

若要查看完整的像素、顶点和计算着色器效果的示例,请参阅 D2DCustomEffects SDK 示例

在本主题中,我们将介绍设计和创建功能齐全的自定义效果所需的步骤和概念。

简介:效果内部是什么?

投影效果图。

从概念上讲, Direct2D 效果执行图像任务,如更改亮度、使图像去饱和,或如上所示创建投影。 对应用,它们很简单。 它们可以接受零个或多个输入图像,公开控制其操作的多个属性,并生成单个输出图像。

效果作者负责自定义效果的四个不同部分:

  1. 效果接口:效果接口在概念上定义应用如何与自定义效果交互 (例如效果接受的输入数以及) 可用的属性。 效果接口管理转换图,其中包含实际图像处理操作。
  2. 转换图:每个效果创建由单个转换组成的内部转换图。 每个转换表示单个图像操作。 该效果负责将这些转换链接到图形中,以执行预期的图像效果。 效果可以添加、删除、修改和重新排序转换,以响应对效果的外部属性的更改。
  3. 转换:转换表示单个图像操作。 其main用途是容纳为每个输出像素执行的着色器。 为此,它负责根据着色器中的逻辑计算其输出图像的新大小。 它还必须计算着色器需要从中读取的输入图像的哪个区域来呈现请求的输出区域。
  4. 着色器:如果在应用创建 Direct3D 设备) 时指定了软件呈现,则针对 GPU (或 CPU 上的转换输入执行着色器。 效果着色器以高级着色语言 (HLSL) 编写,并在效果编译期间编译为字节代码,然后在运行时由效果加载。 本参考文档介绍如何编写 符合 Direct2D 的 HLSL。 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 接口包含必须实现的三种方法:

初始化 (ID2D1EffectContext *pContextInternal,ID2D1TransformGraph *pTransformGraph)

在应用调用 ID2D1DeviceContext::CreateEffect 方法后,Direct2D 调用 Initialize 方法。 可以使用此方法执行内部初始化或效果所需的任何其他操作。 此外,还可以使用它来创建效果的初始转换图。

SetGraph (ID2D1TransformGraph *pTransformGraph)

当效果的输入数发生更改时,Direct2D 调用 SetGraph 方法。 虽然大多数效果具有固定数量的输入,但其他效果(如 复合效果 )支持可变数量的输入。 此方法允许这些效果更新其转换图,以响应不断变化的输入计数。 如果效果不支持变量输入计数,则此方法只需返回E_NOTIMPL。

PrepareForRender (D2D1_CHANGE_TYPE changeType)

PrepareForRender 方法提供了效果执行任何操作以响应外部更改的机会。 如果至少有一个为 true,Direct2D 会在呈现效果之前调用此方法:

  • 该效果以前已初始化,但尚未绘制。
  • 自上次绘制调用以来,效果属性已更改。
  • 自上次绘制调用以来,调用 Direct2D 上下文 (的状态(如 DPI) )已更改。

实现效果注册和回调方法

应用必须在实例化效果之前向 Direct2D 注册效果。 此注册的范围限定为 Direct2D 工厂的实例,每次运行应用时都必须重复。 若要启用此注册,自定义效果定义唯一 GUID、注册效果的公共方法和返回效果实例的专用回调方法。

定义 GUID

必须定义唯一标识 Direct2D 注册效果的 GUID。 应用在调用 ID2D1DeviceContext::CreateEffect 时使用相同的 来标识效果。

此代码演示如何为效果定义这样的 GUID。 必须使用 GUID 生成工具(如 guidgen.exe)创建自己的唯一 GUID。

// 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 接口作为参数。 为了注册效果,该方法随后对 ID2D1Factory1 参数调用 ID2D1Factory1::RegisterEffectFromString API。

此 API 接受描述效果的元数据、输入和属性的 XML 字符串。 效果的元数据仅供参考,应用可以通过 ID2D1Properties 接口查询。 另一方面,输入和属性数据由 Direct2D 使用,表示效果的功能。

此处显示了最小示例效果的 XML 字符串。 向 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** 参数返回效果的实例。 通过 PD2D1_EFFECT_FACTORY\ 参数通过 ID2D1Factory1::RegisterEffectFromString API 注册效果时,会向 Direct2D 提供指向此方法的指针。

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 方法轻松完成此操作。

可以使用提供的 ID2D1TransformGraph 参数在效果的 InitializeSetGraph 方法中创建或修改转换。 如果效果需要在此参数不可用的另一种方法中对转换图进行更改,该效果可以将 ID2D1TransformGraph 参数保存为类的成员变量,并在其他位置访问它,例如 PrepareForRender 或自定义属性回调方法。

此处显示了 个示例 Initialize 方法。 此方法创建一个单节点转换图,该图在每个轴中将图像偏移 100 个像素。

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

创建多节点转换图

将多个转换添加到效果的转换图允许效果在内部执行多个图像操作,这些操作作为单个统一效果呈现给应用。

如上所述,可以使用在效果的 Initialize 方法中收到的 ID2D1TransformGraph 参数在任何效果方法中编辑效果的转换图。 该接口上的以下 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 不强制实施它们。 由你自己在效果类中实现任何指定的 default/min/max 逻辑。

属性的 XML 中列出的类型值必须与属性的 getter 和 setter 方法使用的相应数据类型匹配。 下表中显示了每种数据类型的相应 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[] blob
IUnknown* iunknown
ID2D1ColorContext* colorcontext
CLSID clsid
枚举 (D2D1_INTERPOLATION_MODE等 ) 枚举

 

将新属性映射到 getter 和 setter 方法

接下来,效果必须将此新属性映射到 getter 和 setter 方法。 这是通过传递到 ID2D1Factory1::RegisterEffectFromString 方法的D2D1_PROPERTY_BINDING数组完成的。

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 和绑定数组后,将它们传递到 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::SetValueID2D1Properties::GetValue 方法访问。 为方便起见,可以创建一个公共枚举,该枚举在效果的头文件中列出每个属性:

typedef enum SAMPLEEFFECT_PROP
{
    SAMPLEFFECT_PROP_OFFSET = 0
};

为 属性创建 getter 和 setter 方法

下一步是为新属性创建 getter 和 setter 方法。 方法的名称必须与 D2D1_PROPERTY_BINDING 数组中指定的名称匹配。 此外,在效果的 XML 中指定的属性类型必须与 setter 方法的 参数的类型和 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;
}

更新效果的转换以响应属性更改

若要实际更新效果的图像输出以响应属性更改,该效果需要更改其基础转换。 这通常在效果的 PrepareForRender 方法中完成,当效果的某个属性发生更改时 ,Direct2D 会自动调用该方法。 但是,转换可以在任何效果的方法中更新:例如 Initialize 或效果的属性 setter 方法。

例如,如果某个效果包含 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)定义。

此方法还负责根据着色器的逻辑和每个输入的不透明区域计算不透明输出的区域。 图像的不透明区域定义为整个矩形的 alpha 通道为“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

Direct2DMapInputRectsToOutputRect 之后 调用 MapOutputRectToInputRects 方法。 转换必须计算需要从中读取的图像部分,以便正确呈现请求的输出区域。

与以前一样,如果效果严格映射到像素 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 方法。 但是,与 MapInputRectsToOutputRectMapOutputRectToInputRects 方法不同,Direct2D 不能保证在任何特定时间调用它。 此方法在概念上决定转换输出的哪个部分需要重新呈现,以响应部分或全部输入更改。 有三种不同的方案可以计算转换的无效 rect。

使用一对一像素映射进行转换

对于映射像素 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 接口,该接口继承自第 5 节中所述的 ID2D1Transform 接口。 此接口包含一个要实现的新方法:

SetDrawInfo (ID2D1DrawInfo *pDrawInfo)

Direct2D 在首次将转换添加到效果的转换图时调用 SetDrawInfo 方法。 此方法提供一个 ID2D1DrawInfo 参数,用于控制转换的呈现方式。 有关此处提供的方法,请参阅 ID2D1DrawInfo 主题。

如果转换选择将此参数存储为类成员变量,则可以访问 drawInfo 对象,并从其他方法(如属性 setter 或 MapInputRectsToOutputRect)进行更改。 值得注意的是,它不能从 ID2D1Transform 上的 MapOutputRectToInputRectsMapInvalidRect 方法调用。

为像素着色器创建 GUID

接下来,转换必须为像素着色器本身定义唯一 GUID。 当 Direct2D 将着色器加载到内存中时,以及当转换选择要用于执行哪个像素着色器时,使用此方法。 Visual Studio 附带的 guidgen.exe 等工具可用于生成随机 GUID。

// 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 加载像素着色器

必须先将像素着色器加载到内存中,然后转换才能使用它。

若要将像素着色器加载到内存中,转换应从 读取编译的着色器字节代码。Visual Studio 生成的 CSO 文件 (有关) 字节数组的详细信息,请参阅 Direct3D 文档。 D2DCustomEffects SDK 示例中详细介绍了此方法。

将着色器数据加载到字节数组中后,对效果的 ID2D1EffectContext 对象调用 LoadPixelShader 方法。 当已加载具有相同 GUID 的着色器时,Direct2D 将忽略对 LoadPixelShader 的调用。

将像素着色器加载到内存中后,转换需要将其 GUID 传递到 SetDrawInfo 方法期间提供的 ID2D1DrawInfo 参数上的 SetPixelShader方法,以将其选中执行。 在选择执行之前,像素着色器必须已加载到内存中。

使用常量缓冲区更改着色器操作

若要更改着色器的执行方式,转换可将常量缓冲区传递给像素着色器。 为此,转换定义一个结构,该结构在类标头中包含所需的变量:

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

然后,转换调用 SetDrawInfo 方法中提供的 ID2D1DrawInfo 参数上的 ID2D1DrawInfo::SetPixelShaderConstantBuffer 方法,以将此缓冲区传递给着色器。

HLSL 还需要定义表示常量缓冲区的相应结构。 着色器结构中包含的变量必须与转换结构中的变量匹配。

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

定义缓冲区后,可以从像素着色器内的任意位置读取中包含的值。

为 Direct2D 编写像素着色器

Direct2D 转换使用使用标准 HLSL 创作的着色器。 但是,编写从转换上下文执行的像素着色器有几个关键概念。 有关功能完备的像素着色器示例,请参阅 D2DCustomEffects SDK 示例

Direct2D 自动将转换的输入映射到 HLSL 中的 Texture2DSamplerState 对象。 第一个 Texture2D 位于寄存器 t0,第一个 SamplerState 位于 register s0。 每个附加输入位于下一个相应的寄存器 (t1 和 s1,例如) 。 可以通过对 Texture2D 对象调用 Sample 并传入相应的 SamplerState 对象和纹素坐标来采样特定输入的像素数据。

自定义像素着色器将针对呈现的每个像素运行一次。 每次运行着色器时, Direct2D 都会自动提供三个用于标识其当前执行位置的参数:

  • 场景空间输出:此参数表示整体目标图面的当前执行位置。 它以像素为单位定义,其最小值/最大值对应于 MapInputRectsToOutputRect 返回的矩形的边界。
  • 剪辑空间输出:此参数由 Direct3D 使用,不得在转换的像素着色器中使用。
  • 纹素空间输入:此参数表示特定输入纹理中的当前执行位置。 着色器不应依赖于此值的计算方式。 它应仅使用它来采样像素着色器的输入,如以下代码所示:
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 参考 ;有关示例实现,请参阅 D2DCustomEffects SDK 示例

在内存中创建顶点缓冲区后,转换在包含效果的 ID2D1EffectContext 对象上使用 CreateVertexBuffer 方法将该数据传递到 GPU。 同样,请参阅 D2DCustomEffects SDK 示例 以获取示例实现。

如果转换未指定顶点缓冲区, Direct2D 将传递表示矩形图像位置的默认顶点缓冲区。

更改 SetDrawInfo 以利用顶点着色器

与像素着色器一样,转换必须加载并选择顶点着色器以供执行。 为了加载顶点着色器,它在效果的 Initialize 方法中接收的 ID2D1EffectContext 方法上调用 LoadVertexShader 方法。 若要选择要执行的顶点着色器,它会在转换的 SetDrawInfo 方法中接收的 ID2D1DrawInfo 参数上调用 SetVertexProcessing 此方法接受以前加载的顶点着色器的 GUID,并可以选择 () 以前创建的顶点缓冲区,以便着色器执行。

实现 Direct2D 顶点着色器

绘制转换可以同时包含像素着色器和顶点着色器。 如果转换同时定义像素着色器和顶点着色器,则顶点着色器的输出将直接提供给像素着色器:只要顶点着色器一致,应用可以自定义顶点着色器的返回签名/像素着色器的参数。

另一方面,如果转换仅包含顶点着色器,并且依赖于 Direct2D 的默认直通像素着色器,则必须返回以下默认输出:

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

顶点着色器将其顶点转换的结果存储在着色器的场景空间输出变量中。 为了计算剪辑空间输出和纹素空间输入变量, 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;  
}

上述代码可用作顶点着色器的起点。 它只是通过输入图像而不执行任何转换。 同样,请参阅 D2DCustomEffects SDK 示例 ,了解完全实现的基于顶点着色器的转换。

如果转换未指定顶点缓冲区, Direct2D 将替换表示矩形图像位置的默认顶点缓冲区。 顶点着色器的参数将更改为默认着色器输出的参数:

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

顶点着色器不能修改其 sceneSpaceOutputclipSpaceOutput 参数。 它必须返回未更改的它们。 但是,它可能会修改每个输入图像 () 的 texelSpaceInput 参数。 如果转换还包含自定义像素着色器,则顶点着色器仍能够将其他自定义参数直接传递给像素着色器。 此外,不再提供 sceneSpace 转换矩阵自定义缓冲区 (b0) 。

将计算着色器添加到自定义转换

最后,自定义转换可能会将计算着色器用于某些目标方案。 计算着色器可用于实现需要任意访问输入和输出图像缓冲区的复杂图像效果。 例如,由于内存访问限制,无法使用像素着色器实现基本直方图算法。

由于计算着色器的硬件功能级别要求高于像素着色器,因此应尽可能使用像素着色器来实现给定效果。 具体而言,计算着色器仅在大多数 DirectX 10 级别卡及更高级别上运行。 如果转换选择使用计算着色器,则除了实现 ID2D1ComputeTransform 接口外,它还必须在实例化期间检查相应的硬件支持。

检查计算着色器支持

如果效果使用计算着色器,则必须在创建期间使用 ID2D1EffectContext::CheckFeatureSupport 方法检查计算着色器支持。 如果 GPU 不支持计算着色器,则效果必须返回 D2DERR_INSUFFICIENT_DEVICE_CAPABILITIES

转换可以使用两种不同类型的计算着色器:着色器模型 4 (DirectX 10) 和着色器模型 5 (DirectX 11) 。 着色器模型 4 着色器存在某些限制。 有关详细信息,请参阅 Direct3D 文档。 转换可以包含这两种类型的着色器,并且可以在需要时回退到着色器模型 4:请参阅 D2DCustomEffects SDK 示例 了解此实现。

实现 ID2D1ComputeTransform

除了 ID2D1Transform 中的方法外,此接口还包含要实现的两个新方法:

SetComputeInfo (ID2D1ComputeInfo *pComputeInfo)

与像素和顶点着色器一样, Direct2D 在首次将转换添加到效果的转换图时调用 SetComputeInfo 方法。 此方法提供一个 ID2D1ComputeInfo 参数,用于控制转换的呈现方式。 这包括选择要通过 ID2D1ComputeInfo::SetComputeShader 方法执行的计算着色器。 如果转换选择将此参数存储为类成员变量,则可以从任何转换或效果方法( MapOutputRectToInputRectsMapInvalidRect 方法除外)对其进行访问和更改。 有关此处提供的其他方法,请参阅 ID2D1ComputeInfo 主题。

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

像素着色器按像素执行,顶点着色器按顶点执行,计算着色器按线程组执行。 线程组表示在 GPU 上并发执行的多个线程。 计算着色器 HLSL 代码决定每个线程组应执行的线程数。 该效果缩放线程组的数量,以便着色器执行所需的次数,具体取决于着色器的逻辑。

CalculateThreadgroups 方法允许转换根据图像的大小和转换自己对着色器的了解,通知 Direct2D 需要多少个线程组。

计算着色器的执行次数是此处指定的线程组计数和计算着色器 HLSL 中“numthreads”注释的乘积。 例如,如果转换将线程组维度设置为 (2,2,1,) 着色器为每个线程组指定 (3,3,1) 线程,则将执行 4 个线程组,每个线程组包含 9 个线程,总共 36 个线程实例。

常见方案是处理计算着色器的每个实例的一个输出像素。 为了计算此方案的线程组数,转换将图像的宽度和高度除以计算着色器 HLSL 中“numthreads”注释的相应 x 和 y 维度。

重要的是,如果执行此除法,则请求的线程组数必须始终舍入到最接近的整数,否则不会对“剩余”像素执行。 例如,如果着色器 () 计算每个线程的单个像素,则方法的代码如下所示。

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

D2DCustomEffects SDK 示例