사용자 지정 효과

Direct2D 는 다양한 일반적인 이미지 작업을 수행하는 효과 라이브러리와 함께 제공합니다. 효과의 전체 목록은 기본 제공 효과 항목을 참조하세요. 기본 제공 효과로 구현할 수 없는 기능의 경우 Direct2D를 사용하면 표준 HLSL을 사용하여 사용자 지정 효과를 직접 작성할 수 있습니다. Direct2D와 함께 제공되는 기본 제공 효과와 함께 이러한 사용자 지정 효과를 사용할 수 있습니다.

전체 픽셀, 꼭짓점 및 컴퓨팅 셰이더 효과의 예를 보려면 D2DCustomEffects SDK 샘플을 참조하세요.

이 항목에서는 완전한 기능을 갖춘 사용자 지정 효과를 디자인하고 만드는 데 필요한 단계와 개념을 보여 줍니다.

소개: 효과 안에 무엇이 있나요?

그림자 효과 다이어그램

개념적으로 Direct2D 효과는 밝기 변경, 이미지 포화 해제 또는 위와 같이 그림자 만들기와 같은 이미징 작업을 수행합니다. 앱에는 간단합니다. 0개 이상의 입력 이미지를 수락하고, 작업을 제어하는 여러 속성을 노출하고, 단일 출력 이미지를 생성할 수 있습니다.

사용자 지정 효과에는 효과 작성자가 담당하는 네 가지 부분이 있습니다.

  1. 효과 인터페이스: 효과 인터페이스는 앱이 사용자 지정 효과와 상호 작용하는 방식(예: 효과가 허용하는 입력 수 및 사용 가능한 속성)을 개념적으로 정의합니다. 효과 인터페이스는 실제 이미징 작업을 포함하는 변환 그래프를 관리합니다.
  2. 변환 그래프: 각 효과는 개별 변환으로 구성된 내부 변환 그래프를 만듭니다. 각 변환은 단일 이미지 작업을 나타냅니다. 이 효과는 이러한 변환을 그래프로 연결하여 의도한 이미징 효과를 수행합니다. 효과는 효과의 외부 속성에 대한 변경 내용에 따라 변환을 추가, 제거, 수정 및 다시 정렬할 수 있습니다.
  3. 변환: 변환은 단일 이미지 작업을 나타냅니다. 기본 목적은 각 출력 픽셀에 대해 실행되는 셰이더를 보관하는 것입니다. 이를 위해 셰이더의 논리를 기반으로 출력 이미지의 새 크기를 계산해야 합니다. 또한 요청된 출력 영역을 렌더링하기 위해 셰이더가 읽어야 하는 입력 이미지의 영역을 계산해야 합니다.
  4. 셰이더: 셰이더는 GPU에서 변환의 입력에 대해 실행됩니다(또는 앱이 Direct3D 디바이스를 만들 때 소프트웨어 렌더링이 지정된 경우 CPU). 효과 셰이더는 HLSL(High Level Shading Language)으로 작성되며 효과 컴파일 중에 바이트 코드로 컴파일된 다음 런타임 동안 효과에 의해 로드됩니다. 이 참조 문서에서는 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 인터페이스에는 다음 세 가지 메서드를 구현해야 합니다.

Initialize(ID2D1EffectContext *pContextInternal, ID2D1TransformGraph *pTransformGraph)

Direct2D앱에서 ID2D1DeviceContext::CreateEffect 메서드를 호출한 후 Initialize 메서드를 호출합니다. 이 메서드를 사용하여 내부 초기화 또는 효과에 필요한 다른 작업을 수행할 수 있습니다. 또한 이를 사용하여 효과의 초기 변환 그래프를 만들 수 있습니다.

SetGraph(ID2D1TransformGraph *pTransformGraph)

Direct2D 는 효과에 대한 입력 수가 변경되면 SetGraph 메서드를 호출합니다. 대부분의 효과에는 일정한 수의 입력이 있지만 복합 효과 와 같은 다른 효과는 가변적인 수의 입력을 지원합니다. 이 메서드를 사용하면 이러한 효과가 변경된 입력 수에 대한 응답으로 변환 그래프를 업데이트할 수 있습니다. 효과가 변수 입력 수를 지원하지 않는 경우 이 메서드는 단순히 E_NOTIMPL 반환할 수 있습니다.

PrepareForRender(D2D1_CHANGE_TYPE changeType)

PrepareForRender 메서드는 외부 변경에 대한 응답으로 효과를 통해 모든 작업을 수행할 수 있는 기회를 제공합니다. Direct2D 는 다음 중 하나 이상이 true인 경우 효과를 렌더링하기 직전에 이 메서드를 호출합니다.

  • 효과는 이전에 초기화되었지만 아직 그려지지 않았습니다.
  • 마지막 그리기 호출 이후 효과 속성이 변경되었습니다.
  • 호출 하는 Direct2D 컨텍스트(예: DPI)의 상태가 마지막 그리기 호출 이후 변경되었습니다.

효과 등록 및 콜백 메서드 구현

앱은 효과를 인스턴스화하기 전에 Direct2D 에 등록해야 합니다. 이 등록은 Direct2D 팩터리의 instance 범위가 지정되며 앱이 실행될 때마다 반복되어야 합니다. 이 등록을 사용하도록 설정하기 위해 사용자 지정 효과는 고유한 GUID, 효과를 등록하는 public 메서드 및 효과의 instance 반환하는 프라이빗 콜백 메서드를 정의합니다.

GUID 정의

Direct2D를 사용하여 등록하는 효과를 고유하게 식별하는 GUID를 정의해야 합니다. 앱은 ID2D1DeviceContext::CreateEffect를 호출할 때 효과를 식별하기 위해 동일한 를 사용합니다.

이 코드는 효과에 대해 이러한 GUID를 정의하는 방법을 보여 줍니다. guidgen.exe 같은 GUID 생성 도구를 사용하여 고유한 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 팩터리의 instance 관련되므로 메서드는 ID2D1Factory1 인터페이스를 매개 변수로 허용합니다. 효과를 등록하기 위해 메서드는 ID2D1Factory1 매개 변수에서 ID2D1Factory1::RegisterEffectFromString API를 호출합니다.

이 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** 매개 변수를 통해 효과의 instance 반환하는 프라이빗 콜백 메서드를 제공해야 합니다. 이 메서드에 대한 포인터는 효과가 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 인터페이스 구현

마지막으로, 효과는 COM과의 호환성을 위해 IUnknown 인터페이스를 구현해야 합니다.

효과의 변환 그래프 만들기

효과는 여러 변환(개별 이미지 작업)을 사용하여 원하는 이미징 효과를 만들 수 있습니다. 이러한 변환이 입력 이미지에 적용되는 순서를 제어하기 위해 효과는 변환 그래프로 정렬합니다. 변환 그래프는 Direct2D 에 포함된 효과 및 변환뿐만 아니라 효과 작성자가 만든 사용자 지정 변환을 사용할 수 있습니다.

Direct2D에 포함된 변환 사용

Direct2D와 함께 제공되는 가장 일반적으로 사용되는 변환입니다.

단일 노드 변환 그래프 만들기

변환을 만들면 효과의 입력을 변환의 입력에 연결해야 하며 변환의 출력을 효과의 출력에 연결해야 합니다. 효과에 단일 변환만 포함된 경우 ID2D1TransformGraph::SetSingleTransformNode 메서드를 사용하여 쉽게 수행할 수 있습니다.

제공된 ID2D1TransformGraph 매개 변수를 사용하여 효과의 Initialize 또는 SetGraph 메서드에서 변환을 만들거나 수정할 수 있습니다. 이 매개 변수를 사용할 수 없는 다른 메서드에서 변환 그래프를 변경해야 하는 경우 효과는 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에 의해 적용되지 않습니다. 효과 클래스에서 지정된 기본/최소/최대 논리를 직접 구현하는 것은 사용자에게 달려 있습니다.

속성의 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 등) enum

 

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

속성 변경에 대한 응답으로 효과의 변환 업데이트

속성 변경에 대한 응답으로 효과의 이미지 출력을 실제로 업데이트하려면 효과가 기본 변환을 변경해야 합니다. 이 작업은 일반적으로 효과의 속성 중 하나가 변경되었을 때 Direct2D가 자동으로 호출하는 효과의 PrepareForRender 메서드에서 수행됩니다. 그러나 초기화 또는 효과의 속성 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가 아닌 픽셀로 정의됩니다.

또한 이 메서드는 셰이더의 논리와 각 입력의 불투명 영역을 기반으로 불투명한 출력의 영역을 계산합니다. 이미지의 불투명 영역은 사각형 전체에 대해 알파 채널이 '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

Direct2DMapInvalidRect 메서드도 호출합니다. 그러나 MapInputRectsToOutputRectMapOutputRectToInputRects 메서드와 달리 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;
}

다대다 픽셀 매핑을 사용하여 변환

변환의 출력 픽셀이 주변 영역에 종속된 경우 잘못된 입력 사각형을 그에 따라 확장해야 합니다. 이는 잘못된 입력 사각형을 둘러싼 픽셀도 영향을 받고 유효하지 않음을 반영하기 위한 것입니다. 예를 들어 5픽셀 흐림은 다음 계산을 사용합니다.

// 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 구현

픽셀 셰이더를 사용하려면 변환에서 섹션 5에 설명된 ID2D1Transform 인터페이스에서 상속되는 ID2D1DrawTransform 인터페이스를 구현해야 합니다. 이 인터페이스에는 구현할 새 메서드가 하나 있습니다.

SetDrawInfo(ID2D1DrawInfo *pDrawInfo)

Direct2D 는 변환이 효과의 변환 그래프에 처음 추가되면 SetDrawInfo 메서드를 호출합니다. 이 메서드는 변환 렌더링 방법을 제어하는 ID2D1DrawInfo 매개 변수를 제공합니다. 여기에서 사용할 수 있는 방법은 ID2D1DrawInfo 항목을 참조하세요.

변환이 이 매개 변수를 클래스 멤버 변수로 저장하도록 선택하는 경우 속성 setter 또는 MapInputRectsToOutputRect와 같은 다른 메서드에서 drawInfo 개체에 액세스하고 변경할 수 있습니다. 특히 ID2D1TransformMapOutputRectToInputRects 또는 MapInvalidRect 메서드에서 호출할 수 없습니다.

픽셀 셰이더에 대한 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 메서드를 호출합니다. Direct2D 는 GUID가 동일한 셰이더가 이미 로드된 경우 LoadPixelShader 호출을 무시합니다.

픽셀 셰이더를 메모리에 로드한 후에는 SetDrawInfo 메서드 중에 제공된 ID2D1DrawInfo 매개 변수의 SetPixelShader 메서드에 GUID를 전달하여 실행을 위해 변환을 선택해야 합니다. 실행하기 위해 선택되기 전에 픽셀 셰이더를 이미 메모리에 로드해야 합니다.

상수 버퍼를 사용하여 셰이더 작업 변경

셰이더 실행 방법을 변경하기 위해 변환은 픽셀 셰이더에 상수 버퍼를 전달할 수 있습니다. 이렇게 하려면 변환은 클래스 헤더에 원하는 변수를 포함하는 구조체를 정의합니다.

// 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 는 레지스터 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를 만들고(선택적으로) 상수 버퍼를 셰이더에 전달합니다. 그러나 꼭짓점 셰이더에 고유한 몇 가지 주요 추가 단계가 있습니다.

꼭짓점 버퍼 만들기

정의별 꼭짓점 셰이더는 개별 픽셀이 아닌 해당 꼭짓점에서 실행됩니다. 셰이더가 실행할 꼭짓점을 지정하기 위해 변환은 셰이더에 전달할 꼭짓점 버퍼를 만듭니다. 꼭짓점 버퍼의 레이아웃은 이 문서의 scope. 자세한 내용은 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)한 다음 총 36개의 스레드 인스턴스에 대해 각각 9개의 스레드가 있는 4개의 스레드 그룹이 실행됩니다.

일반적인 시나리오는 컴퓨팅 셰이더의 각 instance 대해 하나의 출력 픽셀을 처리하는 것입니다. 이 시나리오의 스레드 그룹 수를 계산하기 위해 변환은 이미지의 너비와 높이를 컴퓨팅 셰이더 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
    )
{
...

이미지 데이터 읽기

컴퓨팅 셰이더는 변환의 입력 이미지에 단일 2차원 텍스처로 액세스합니다.

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) 셰이더는 2차원 출력 텍스처를 사용할 수 있습니다.

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 버전을 보여줍니다. 이제 셰이더가 1차원 출력 버퍼로 렌더링됩니다.

#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 샘플