다음을 통해 공유


사용자 지정 효과 구현

Win2D는 그릴 수 있는 개체를 표현하기 위한 여러 API를 제공하며, 이미지와 효과의 두 가지 범주로 나뉩니다. ICanvasImage 인터페이스로 표현되는 이미지는 입력이 없으며 지정된 표면에 직접 그릴 수 있습니다. 예를 들어 CanvasBitmap, VirtualizedCanvasBitmapCanvasRenderTarget는 이미지 형식의 예입니다. 반면에 효과는 ICanvasEffect 인터페이스로 표시됩니다. 입력과 추가 리소스를 포함할 수 있으며 임의 논리를 적용하여 출력을 생성할 수 있습니다(효과가 이미지이기도 하므로). Win2D에는 대부분의 D2D 효과(예: GaussianBlurEffect, TintEffectLuminanceToAlphaEffect)를 래핑하는 효과가 포함됩니다.

이미지와 효과를 함께 연결하여 애플리케이션에 표시할 수 있는 임의의 그래프를 만들 수도 있습니다(Direct2D 효과에 대한 D2D 문서 참조). 두 가지를 함께 사용하면 복잡한 그래픽을 효율적으로 작성할 수 있는 매우 유연한 시스템을 제공합니다. 그러나 기본 제공 효과가 충분하지 않은 경우가 있어 자신만의 Win2D 효과를 만들고 싶을 수도 있습니다. 이를 지원하기 위해 Win2D에는 Win2D와 원활하게 통합할 수 있는 사용자 지정 이미지 및 효과를 정의할 수 있는 강력한 interop API 집합이 포함되어 있습니다.

C#을 사용하고 사용자 지정 효과 또는 효과 그래프를 구현하려는 경우 처음부터 효과를 구현하는 대신 ComputeSharp를 사용하는 것이 좋습니다. 이 라이브러리를 사용하여 Win2D와 원활하게 통합되는 사용자 지정 효과를 구현하는 방법에 대한 자세한 설명은 아래 단락을 참조하세요.

플랫폼 API: ICanvasImage,, CanvasBitmap,CanvasRenderTargetVirtualizedCanvasBitmap, CanvasEffect, GaussianBlurEffectTintEffect, ICanvasLuminanceToAlphaEffectImage, , IGraphicsEffectSourceID2D1Factory1ID2D21ImageID2D1Effect

사용자 지정 ICanvasImage 구현

지원하는 가장 간단한 시나리오는 사용자 지정 ICanvasImage을 만드는 것입니다. 앞서 언급했듯이 이것은 Win2D가 정의한 WinRT 인터페이스로, Win2D가 상호 운용할 수 있는 모든 종류의 이미지를 나타냅니다. 이 인터페이스는 "일부 효과 소스"를 나타내는 표식 인터페이스인 두 GetBounds 메서드만 노출하고 IGraphicsEffectSource을 확장합니다.

여기서 볼 수 있듯이 실제로 그리기를 수행하기 위해 이 인터페이스에서 노출하는 "기능적인" API는 없습니다. 사용자 고유 ICanvasImage 개체를 구현하려면 Win2D에서 이미지를 그리는 데 필요한 모든 논리를 노출하는 ICanvasImageInterop 인터페이스도 구현해야 합니다. Win2D와 함께 제공되는 공용 Microsoft.Graphics.Canvas.native.h 헤더에 정의된 COM 인터페이스입니다.

인터페이스는 다음과 같이 정의됩니다.

[uuid("E042D1F7-F9AD-4479-A713-67627EA31863")]
class ICanvasImageInterop : IUnknown
{
    HRESULT GetDevice(
        ICanvasDevice** device,
        WIN2D_GET_DEVICE_ASSOCIATION_TYPE* type);

    HRESULT GetD2DImage(
        ICanvasDevice* device,
        ID2D1DeviceContext* deviceContext,
        WIN2D_GET_D2D_IMAGE_FLAGS flags,
        float targetDpi,
        float* realizeDpi,
        ID2D1Image** ppImage);
}

또한 동일한 헤더에서 다음 두 열거형 형식을 사용합니다.

enum WIN2D_GET_DEVICE_ASSOCIATION_TYPE
{
    WIN2D_GET_DEVICE_ASSOCIATION_TYPE_UNSPECIFIED,
    WIN2D_GET_DEVICE_ASSOCIATION_TYPE_REALIZATION_DEVICE,
    WIN2D_GET_DEVICE_ASSOCIATION_TYPE_CREATION_DEVICE
}

enum WIN2D_GET_D2D_IMAGE_FLAGS
{
    WIN2D_GET_D2D_IMAGE_FLAGS_NONE,
    WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT,
    WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION,
    WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION,
    WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION,
    WIN2D_GET_D2D_IMAGE_FLAGS_ALLOW_NULL_EFFECT_INPUTS,
    WIN2D_GET_D2D_IMAGE_FLAGS_UNREALIZE_ON_FAILURE
}

GetDeviceGetD2DImage 메서드는 사용자 지정 이미지(또는 효과)를 구현하는 데 필요한 모든 것이므로, Win2D에 주어진 장치에서 초기화하고 그릴 기본 D2D 이미지를 검색할 수 있는 확장성을 제공합니다. 지원되는 모든 시나리오에서 제대로 작동하려면 이러한 방법을 올바르게 구현하는 것이 중요합니다.

각 메서드의 작동 방식을 확인해보겠습니다.

GetDevice 구현

GetDevice 메서드는 두 가지 중 가장 간단합니다. 이 작업은 효과와 연결된 캔버스 디바이스를 검색하여 필요한 경우 Win2D에서 검사할 수 있도록 합니다(예: 사용 중인 디바이스와 일치하는지 확인). type 매개 변수는 반환된 디바이스에 대한 "연결 유형"을 나타냅니다.

크게 두 가지 경우가 있습니다.

  • 이미지가 효과가 있는 경우 여러 디바이스에서 "실현" 및 "실현되지 않은" 이미지를 지원해야 합니다. 즉, 지정된 효과는 초기화되지 않은 상태로 만들어지고, 그리는 동안 디바이스가 전달될 때 실현될 수 있으며, 그 후에는 해당 디바이스와 함께 계속 사용되거나 다른 디바이스로 이동할 수 있습니다. 이 경우 효과는 내부 상태를 다시 설정한 다음 새 디바이스에서 다시 실현됩니다. 즉, 연결된 캔버스 디바이스는 시간이 지남에 따라 변경될 수 있으며, 또한 null과 같을 수 있습니다. 따라서 typeWIN2D_GET_DEVICE_ASSOCIATION_TYPE_REALIZATION_DEVICE로 설정되어야 하며, 사용 가능한 경우 반환된 디바이스를 현재 실현 디바이스로 설정해야 합니다.
  • 일부 이미지에는 생성 시 할당되고 변경할 수 없는 단일 "소유 디바이스"가 있습니다. 예를 들어 텍스처를 나타내는 이미지의 경우 특정 디바이스에 할당되어 이동이 불가능하므로 이러한 경우에 해당합니다. GetDevice이 호출되면 생성 디바이스를 반환하고 typeWIN2D_GET_DEVICE_ASSOCIATION_TYPE_CREATION_DEVICE로 설정해야 합니다. 이 형식을 지정하면 반환된 디바이스는 null와 같지 않아야 합니다.

참고 항목

Win2D는 효과 그래프를 재귀적으로 트래버스하는 동안 GetDevice을 호출할 수 있습니다. 즉, 스택에 GetD2DImage를 위한 여러 활성 호출이 있을 수 있습니다. 따라서 GetDevice는 잠재적으로 교착 상태에 빠질 수 있으므로 현재 이미지에 차단 잠금을 설정해서는 안 됩니다. 오히려 차단하지 않는 방식으로 재진입 잠금을 사용하고, 획득할 수 없는 경우 오류를 반환해야 합니다. 이렇게 하면 동일한 스레드가 재귀적으로 호출하면 성공적으로 획득되지만 동일한 작업을 수행하는 동시 스레드는 정상적으로 실패합니다.

GetD2DImage 구현

GetD2DImage는 대부분의 작업이 수행되는 위치입니다. 이 메서드는 Win2D에서 그릴 수 있는 ID2D1Image 개체를 검색하고 필요에 따라 현재 효과를 실현합니다. 여기에는 이미지에 필요할 수 있는 상태(예: 상수 버퍼 및 기타 속성, 리소스 텍스처 등)를 초기화할 뿐만 아니라 모든 원본(있는 경우)에 대한 효과 그래프를 재귀적으로 트래버스하고 실현하는 것도 포함됩니다.

이 메서드의 정확한 구현은 이미지 형식에 따라 크게 달라지지만 일반적으로 임의의 효과의 경우 메서드가 다음 단계를 수행할 것으로 예상할 수 있습니다.

  • 동일한 인스턴스에서 호출이 재귀적이었는지 확인하고, 그렇지 않으면 실패합니다. 효과 그래프에서 주기를 검색하는 데 필요합니다(예: 효과 A가 소스로 효과 B가 있고 효과 B가 소스로 A 효과가 있음).
  • 동시 액세스로부터 보호하기 위해 이미지 인스턴스에 대한 잠금을 획득합니다.
  • 입력 플래그에 따라 대상 DPI 처리
  • 입력 디바이스가 사용 중인 디바이스와 일치하는지 확인합니다(있는 경우). 일치하지 않고 현재 효과가 실현을 지원하는 경우 효과를 실현하지 않습니다.
  • 입력 디바이스에 미치는 영향을 인식합니다. 여기에는 필요한 경우 입력 디바이스 또는 디바이스 컨텍스트에서 검색된 ID2D1Factory1 개체에 대한 D2D 효과 등록이 포함될 수 있습니다. 또한 필요한 모든 상태는 생성 중인 D2D 효과 인스턴스에 설정해야 합니다.
  • 모든 원본을 재귀적으로 트래버스하고 D2D 효과에 바인딩합니다.

입력 플래그와 관련하여 다른 모든 Win2D 효과와의 호환성을 보장하기 위해 사용자 지정 효과가 올바르게 처리해야 하는 몇 가지 경우가 있습니다. WIN2D_GET_D2D_IMAGE_FLAGS_NONE을 제외하면 처리할 플래그는 다음과 같습니다.

  • WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT: 이 경우 devicenull이 아닌 것으로 보장됩니다. 효과는 디바이스 컨텍스트 대상이 ID2D1CommandList인지 여부를 확인해야 하고 WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION 플래그를 추가해야 합니다. 그렇지 않으면 입력 컨텍스트에서 검색된 DPI에 targetDpi (또한 null이 아닐 것으로 보장됨)을 설정해야 합니다(그렇지 않을 수도 있음). 그런 다음 플래그에서 WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT을 제거해야 합니다.
  • WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATIONWIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION: 효과 소스를 설정할 때 사용됩니다(아래 참고 사항 참조).
  • WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION: 설정된 경우 효과 소스를 재귀적으로 실현하는 것을 건너뛰고 다른 변경 내용 없이 실현된 효과를 반환합니다.
  • WIN2D_GET_D2D_IMAGE_FLAGS_ALLOW_NULL_EFFECT_INPUTS: 설정되면 사용자가 아직 기존 소스로 설정하지 않은 경우 실현되는 효과 소스는 null가 되도록 허용됩니다.
  • WIN2D_GET_D2D_IMAGE_FLAGS_UNREALIZE_ON_FAILURE: 설정되고 설정 중인 효과 소스가 유효하지 않은 경우 실패하기 전에 효과의 크기를 조정하지 않아야 합니다. 즉, 효과를 인식한 후 효과 소스를 확인하는 동안 오류가 발생한 경우 호출자에게 오류를 반환하기 전에 효과 자체의 크기를 조정하지 않아야 합니다.

DPI 관련 플래그와 관련하여 이러한 플래그는 효과 소스가 설정되는 방식을 제어합니다. Win2D와의 호환성을 보장하려면 필요한 경우 효과가 입력에 DPI 보정 효과를 자동으로 추가해야 합니다. 다음과 같은 경우인지 여부를 제어할 수 있습니다.

  • WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION이 설정된 경우 inputDpi 매개 변수가 0가 아닐 때마다 DPI 보정 효과가 필요합니다.
  • 그렇지 않으면 inputDpi0아니거나, WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION이 설정되지 않았거나, WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION이 설정되거나, 입력 DPI 및 대상 DPI 값이 일치하지 않으면 DPI 보정이 필요합니다.

이 논리는 소스가 실현되고 현재 효과의 입력에 바인딩될 때마다 적용되어야 합니다. DPI 보정 효과가 추가되면 기본 D2D 이미지로 설정된 입력이어야 합니다. 그러나 사용자가 해당 소스에 대한 WinRT 래퍼를 검색하려고 하면 이 효과는 DPI 효과가 사용되었는지 여부를 감지하고 대신 원본 소스 개체에 대한 래퍼를 반환해야 합니다. 즉, DPI 보정 효과는 효과 사용자에게 투명해야 합니다.

모든 초기화 논리가 완료되면 결과 ID2D1Image (Win2D 개체와 마찬가지로 D2D 효과도 이미지임)를 현재 호출자가 아직 알지 못하는 대상 컨텍스트에서 Win2D가 그릴 준비가 되어 있어야 합니다.

참고 항목

이 메서드를 올바르게 구현하는 것(또한 일반적으로 ICanvasImageInterop)은 매우 복잡하며, 추가 유연성이 절대적으로 필요한 고급 사용자에 의해서만 수행됩니다. ICanvasImageInterop 구현을 작성하기 전에 D2D, Win2D, COM, WinRT 및 C++를 확실하게 이해하는 것이 좋습니다. 사용자 지정 Win2D 효과도 사용자 지정 D2D 효과를 래핑해야 하는 경우 사용자 고유의 ID2D1Effect 개체도 구현해야 합니다(이에 대한 자세한 내용은 사용자 지정 효과에 대한 D2D 문서 참조). 이러한 문서는 필요한 모든 논리에 대한 완전한 설명이 아니므로(예: D2D/Win2D 경계에서 효과 원본을 마샬링하고 관리하는 방법을 다루지 않음) Win2D 코드베이스의 CanvasEffect 구현을 사용자 지정 효과의 참조 지점으로 사용하고 필요에 따라 수정하는 것이 좋습니다.

GetBounds 구현

사용자 지정 ICanvasImage 효과를 완전히 구현하는 마지막 누락된 구성 요소는 두 GetBounds 오버로드를 지원하는 것입니다. 이를 쉽게 하기 위해 Win2D는 모든 사용자 지정 이미지의 Win2D에서 기존 논리를 활용하는 데 사용할 수 있는 C 내보내기를 노출합니다. 내보내기는 다음과 같습니다.

HRESULT GetBoundsForICanvasImageInterop(
    ICanvasResourceCreator* resourceCreator,
    ICanvasImageInterop* image,
    Numerics::Matrix3x2 const* transform,
    Rect* rect);

사용자 지정 이미지는 이 API를 호출하고 자신을 image 매개 변수로 전달한 다음 결과를 호출자에게 반환할 수 있습니다. 사용할 수 있는 변환이 없는 경우 transform 매개 변수가 null이 될 수 있습니다.

디바이스 컨텍스트 액세스 최적화

호출 전에 컨텍스트를 즉시 사용할 수 없는 경우 ICanvasImageInterop::GetD2DImagedeviceContext 매개 변수가 null가 될 수 있습니다. 이 작업은 의도적으로 수행되므로 컨텍스트가 실제로 필요할 때만 지연적으로 만들어집니다. 즉, 컨텍스트를 사용할 수 있는 경우 Win2D는 해당 컨텍스트를 GetD2DImage에 전달합니다. 그렇지 않으면 호출자는 필요한 경우 직접 검색할 수 있습니다.

디바이스 컨텍스트를 만드는 것은 비교적 비용이 많이 들기 때문에 Win2D를 더 빠르게 검색하면 API가 내부 디바이스 컨텍스트 풀에 액세스하도록 노출됩니다. 이를 통해 지정된 캔버스 디바이스와 관련된 디바이스 컨텍스트를 효율적으로 대여하고 반환하는 사용자 지정 효과를 사용할 수 있습니다.

디바이스 컨텍스트 임대 API는 다음과 같이 정의됩니다.

[uuid("A0928F38-F7D5-44DD-A5C9-E23D94734BBB")]
interface ID2D1DeviceContextLease : IUnknown
{
    HRESULT GetD2DDeviceContext(ID2D1DeviceContext** deviceContext);
}

[uuid("454A82A1-F024-40DB-BD5B-8F527FD58AD0")]
interface ID2D1DeviceContextPool : IUnknown
{
    HRESULT GetDeviceContextLease(ID2D1DeviceContextLease** lease);
}

ID2D1DeviceContextPool 인터페이스는 ICanvasDevice 인터페이스를 구현하는 Win2D 형식인 CanvasDevice에 의해 구현됩니다. 풀을 사용하려면 QueryInterface을 통해 디바이스 인터페이스에서 ID2D1DeviceContextPool 참조를 가져온 다음 ID2D1DeviceContextPool::GetDeviceContextLease을 호출하여 디바이스 컨텍스트에 액세스하는 ID2D1DeviceContextLease 개체를 가져옵니다. 더 이상 필요하지 않은 경우 임대를 해제합니다. 임대가 릴리스된 후 다른 스레드에서 동시에 사용될 수 있으므로 디바이스 컨텍스트를 건드리지 않도록 합니다.

WinRT 래퍼 조회 사용

Win2D interop 문서에서 볼 수 있듯이 Win2D 공용 헤더는 GetOrCreate 메서드도 노출합니다(ICanvasFactoryNative 활성화 팩터리에서 또는 동일한 헤더에 정의된 GetOrCreate C++/CX 도우미를 통해 액세스할 수 있음). 이렇게 하면 지정된 네이티브 리소스에서 WinRT 래퍼를 검색할 수 있습니다. 예를 들어 ID2D1Device1 개체, ID2D1Bitmap에서의 CanvasBitmap 개체 등에서 CanvasDevice 인스턴스를 검색하거나 만들 수 있습니다.

또한 이 메서드는 모든 기본 제공 Win2D 효과에 대해 작동합니다. 지정된 효과에 대한 네이티브 리소스를 검색한 다음 이를 사용하여 해당 Win2D 래퍼를 검색하면 해당 Win2D에 대한 소유 Win2D 효과가 올바르게 반환됩니다. 사용자 지정 효과도 동일한 매핑 시스템의 이점을 활용할 수 있도록 Win2D는 CanvasDevice에 대한 활성화 팩토리의 인터롭 인터페이스에 여러 API를 노출합니다. 즉, 이는 ICanvasFactoryNative 형식이며 추가 효과 팩토리 인터페이스인 ICanvasEffectFactoryNative입니다.

[uuid("29BA1A1F-1CFE-44C3-984D-426D61B51427")]
class ICanvasEffectFactoryNative : IUnknown
{
    HRESULT CreateWrapper(
        ICanvasDevice* device,
        ID2D1Effect* resource,
        float dpi,
        IInspectable** wrapper);
};

[uuid("695C440D-04B3-4EDD-BFD9-63E51E9F7202")]
class ICanvasFactoryNative : IInspectable
{
    HRESULT GetOrCreate(
        ICanvasDevice* device,
        IUnknown* resource,
        float dpi,
        IInspectable** wrapper);

    HRESULT RegisterWrapper(IUnknown* resource, IInspectable* wrapper);

    HRESULT UnregisterWrapper(IUnknown* resource);

    HRESULT RegisterEffectFactory(
        REFIID effectId,
        ICanvasEffectFactoryNative* factory);

    HRESULT UnregisterEffectFactory(REFIID effectId);
};

여기서 고려해야 할 몇 가지 API는 Win2D 효과를 사용할 수 있는 다양한 시나리오를 지원하는 데 필요하기 때문에 개발자가 D2D 계층과 interop을 수행하고 이를 위해 래퍼를 확인하는 방법을 고려해야 합니다. 이러한 각 API를 살펴보겠습니다.

RegisterWrapperUnregisterWrapper 메서드는 사용자 지정 효과에 의해 호출되어 내부 Win2D 캐시에 자신을 추가합니다.

  • RegisterWrapper: 네이티브 리소스 및 소유 WinRT 래퍼를 등록합니다. wrapper 매개 변수는 IWeakReferenceSource를 구현하려면 필수 요소입니다. 메모리 누수를 유발하는 참조 주기를 발생하지 않고 올바르게 캐시될 수 있도록 합니다. 이 메서드는 네이티브 리소스를 캐시에 추가할 수 있다면, S_OK를 반환하고 S_FALSE resource에 대한 래퍼가 이미 등록되어 있는 경우, 오류가 발생하면 오류 코드가 반환합니다.
  • UnregisterWrapper: 네이티브 리소스 및 해당 래퍼의 등록을 취소합니다. 리소스를 제거할 수 있다면 S_OK을 반환하고, resource가 아직 등록되지 않은 경우 S_FALSE을 반환하여, 다른 오류가 발생하는 경우 에러 코드를 반환합니다.

사용자 지정 효과는 RegisterWrapper and UnregisterWrapper을 호출해야 하며, 실현 및 미실현될 때마다, 즉 새로운 네이티브 리소스가 생성되어 연결될 때마다 호출합니다. 실현을 지원하지 않는 사용자 지정 효과(예: 연결된 고정 디바이스가 있는 효과)는 호출할 수 있으며 생성 및 제거될 때 RegisterWrapperUnregisterWrapper를 호출할 수 있습니다. 사용자 정의 효과는 래퍼를 유효하지 않게 만들 수 있는 모든 가능한 코드 경로에서 올바르게 등록을 취소해야 합니다(예: 객체가 관리되는 언어로 구현된 경우 객체가 최종 처리될 때 포함).

RegisterEffectFactoryUnregisterEffectFactory 네서드 또한 개발자가 "분리된" D2D 리소스에 대한 래퍼를 확인하려고 할 경우 콜백을 등록하여 새 래퍼를 만들 수 있도록 사용자 지정 효과에서 사용합니다.

  • RegisterEffectFactory: 개발자가 전달한 것과 동일한 매개 변수를 입력으로 받는 콜백을 GetOrCreate에 등록하고 입력 효과를 위한 검사 가능한 새 래퍼를 생성합니다. 효과 ID는 키로 사용되므로 각 사용자 지정 효과가 처음 로드될 때 팩터리를 등록할 수 있습니다. 물론 이 작업은 효과 유형별로 한 번만 수행되어야 하며 효과가 실현될 때마다 수행되지는 않습니다. device, resourcewrapper 매개 변수는 등록된 콜백을 호출하기 전에 Win2D에서 확인하므로 CreateWrapper이 호출될 때 null이 되지 않도록 보장되어 있습니다. dpi은 선택 사항으로 간주되며 효과 형식에 특정 용도가 없는 경우 무시할 수 있습니다. 등록된 팩터리에서 새 래퍼를 만들 때 해당 팩터리는 새 래퍼가 캐시에 등록되어 있는지도 확인해야 합니다(Win2D는 외부 팩터리에서 생성된 래퍼를 캐시에 자동으로 추가하지 않음).
  • UnregisterEffectFactory: 이전에 등록된 콜백을 제거합니다. 예를 들어, 언로드 중인 관리되는 어셈블리에서 효과 래퍼가 구현되는 경우 이를 사용할 수 있습니다.

참고 항목

ICanvasFactoryNative는 수동으로 RoGetActivationFactory을 호출하는 CanvasDevice에 대한 활성화 공장을 시행하거나 사용 중인 언어 확장의 도우미 API(예: winrt::get_activation_factoryC++/WinRT)를 사용하여 검색할 수 있는 활성화 팩터리에 의해 구현됩니다. 자세한 내용은 WinRT 유형 시스템을 참조하여 작동 방식에 대한 자세한 내용을 확인하세요.

이 매핑이 작동하는 실제 예를 보려면 기본 제공 Win2D 효과의 작동 방식을 고려합니다. 이러한 상태가 실현되지 않으면 모든 상태(예: 속성, 원본 등)가 각 효과 인스턴스의 내부 캐시에 저장됩니다. 이러한 상태가 실현되면 모든 상태가 네이티브 리소스로 전송되고(예: 속성이 D2D 효과에 설정되고, 모든 원본이 확인되고 효과 입력 등에 매핑됨), 효과가 실현되는 한 래퍼의 상태에 대한 권한으로 작동합니다. 즉, 래퍼에서 속성 값을 가져오는 경우 연결된 네이티브 D2D 리소스에서 업데이트된 값을 검색합니다.

이렇게 하면 D2D 리소스에 대한 변경 내용이 직접 수행되면 외부 래퍼에도 이러한 변경 내용이 표시되고 두 항목은 "동기화되지 않습니다". 효과가 실현되지 않으면 리소스가 해제되기 전에 모든 상태가 네이티브 리소스에서 래퍼 상태로 다시 전송됩니다. 다음 번에 효과가 실현될 때까지 유지되고 업데이트됩니다. 이제 다음 이벤트 시퀀스를 고려합니다.

  • 일부 Win2D 효과(기본 제공 또는 사용자 지정)가 있습니다.
  • ID2D1Image을 얻을 것입니다. (즉 ID2D1Effect).
  • 사용자 지정 효과의 인스턴스를 만듭니다.
  • 당신은 또한 그것에서 ID2D1Image을 얻을 것입니다.
  • 이 이미지를 이전 효과에 대한 입력으로 수동으로 설정합니다(ID2D1Effect::SetInput을 통해).
  • 그런 다음 해당 입력에 대한 WinRT 래퍼에 대한 첫 번째 효과를 요청합니다.

효과가 실현되었으므로(네이티브 리소스가 요청되었을 때 실현됨) 네이티브 리소스를 진리의 근원으로 사용합니다. 따라서 요청된 원본에 ID2D1Image 해당하는 래퍼를 가져와서 WinRT 래퍼를 검색합니다. 이 입력이 검색된 효과가 Win2D의 캐시에 자체 네이티브 리소스 및 WinRT 래퍼 쌍을 올바르게 추가한 경우 래퍼가 확인되고 호출자에게 반환됩니다. 그렇지 않은 경우, Win2D는 인스턴스화 방법을 모르기 때문에 소유하지 않은 효과에 대한 WinRT 래퍼를 확인할 수 없으므로 해당 속성 액세스가 실패합니다.

RegisterWrapperUnregisterWrapper가 있는 위치로, 사용자 지정 효과가 Win2D의 래퍼 확인 논리에 원활하게 참여할 수 있으므로 WinRT API에서 설정되었는지 또는 기본 D2D 계층에서 직접 설정되었는지에 관계없이 모든 효과 소스에 대해 올바른 래퍼를 항상 검색할 수 있도록 도움을 줍니다.

효과 팩터리도 어떻게 적용되는지 설명하려면 다음 시나리오를 고려하세요.

  • 사용자가 사용자 지정 래퍼의 인스턴스를 만들고 이를 실현합니다.
  • 그런 다음 기본 D2D 효과에 대한 참조를 가져오고 유지합니다.
  • 그런 다음 다른 디바이스에서 효과가 실현됩니다. 효과는 실현되지 않고 다시 실현되며, 이렇게 하면 새 D2D 효과가 만들어집니다. 이전 D2D 효과는 이 시점에서 더 이상 관련 검사 가능 래퍼로 표시되지 않습니다.
  • 그런 다음 사용자는 첫 번째 D2D 효과에서 GetOrCreate를 호출합니다.

콜백이 없으면 등록된 래퍼가 없으므로 Win2D는 래퍼를 해결하지 못합니다. 팩터리를 대신 등록하는 경우 해당 D2D 효과에 대한 새 래퍼를 만들고 반환할 수 있으므로 시나리오는 사용자에 대해 원활하게 작동합니다.

사용자 지정 ICanvasEffect 구현

Win2D ICanvasEffect 인터페이스가 ICanvasImage을 확장하므로 이전의 모든 점이 사용자 지정 효과에도 적용됩니다. 유일한 차이점은 ICanvasEffect은 원본 사각형 무효화, 필요한 사각형 가져오기 등 효과와 관련된 추가 메서드도 구현한다는 점입니다.

이를 지원하기 위해 Win2D는 사용자 지정 효과 작성자가 사용할 수 있는 C 내보내기를 노출하므로 이 모든 추가 논리를 처음부터 다시 구현할 필요가 없습니다. 이는 GetBounds에 대한 C 내보내기와 동일한 방식으로 작동합니다. 다음은 효과에 사용할 수 있는 내보내기입니다.

HRESULT InvalidateSourceRectangleForICanvasImageInterop(
    ICanvasResourceCreatorWithDpi* resourceCreator,
    ICanvasImageInterop* image,
    uint32_t sourceIndex,
    Rect const* invalidRectangle);

HRESULT GetInvalidRectanglesForICanvasImageInterop(
    ICanvasResourceCreatorWithDpi* resourceCreator,
    ICanvasImageInterop* image,
    uint32_t* valueCount,
    Rect** valueElements);

HRESULT GetRequiredSourceRectanglesForICanvasImageInterop(
    ICanvasResourceCreatorWithDpi* resourceCreator,
    ICanvasImageInterop* image,
    Rect const* outputRectangle,
    uint32_t sourceEffectCount,
    ICanvasEffect* const* sourceEffects,
    uint32_t sourceIndexCount,
    uint32_t const* sourceIndices,
    uint32_t sourceBoundsCount,
    Rect const* sourceBounds,
    uint32_t valueCount,
    Rect* valueElements);

사용할 수 있는 방법을 살펴보겠습니다.

  • InvalidateSourceRectangleForICanvasImageInteropInvalidateSourceRectangle을 지원하기 위한 것입니다. 입력 매개 변수를 마샬링하고 직접 호출하기만 하면 필요한 모든 작업을 처리합니다. image 매개 변수는 구현 중인 현재 효과 인스턴스입니다.
  • GetInvalidRectanglesForICanvasImageInteropGetInvalidRectangles을 지원합니다. 또한 더 이상 필요하지 않은 경우 반환된 COM 배열을 삭제해야 하는 것 외에는 특별한 고려 사항이 필요하지 않습니다.
  • GetRequiredSourceRectanglesForICanvasImageInteropGetRequiredSourceRectangleGetRequiredSourceRectangles 둘 다 지원할 수 있는 공유 메서드입니다. 즉, 채울 값의 기존 배열에 대한 포인터를 사용하므로 호출자는 단일 값(스택에 있을 수도 있고 할당을 방지하기 위해 하나도 있을 수 있음) 또는 값 배열에 포인터를 전달할 수 있습니다. 구현은 두 경우 모두 동일하므로 단일 C 내보내기는 둘 다 전원을 공급하기에 충분합니다.

ComputeSharp를 사용하는 C#의 사용자 지정 효과

멘션 때 C#을 사용하고 사용자 지정 효과를 구현하려는 경우 ComputeSharp 라이브러리를 사용하는 것이 좋습니다. 이를 통해 사용자 지정 D2D1 픽셀 셰이더를 완전히 C#으로 구현하고 Win2D와 호환되는 사용자 지정 효과 그래프를 쉽게 정의할 수 있습니다. Microsoft Store에서도 동일한 라이브러리를 사용하여 애플리케이션의 여러 그래픽 구성 요소에 전원을 공급합니다.

NuGet을 통해 프로젝트에서 ComputeSharp에 대한 참조를 추가할 수 있습니다.

  • UWP에서 ComputeSharp.D2D1.Uwp 패키지를 선택합니다.
  • WinAppSDK에서 ComputeSharp.D2D1.WinUI 패키지를 선택합니다.

참고 항목

ComputeSharp.D2D1.*의 많은 API는 UWP 및 WinAppSDK 대상에서 동일하며, 유일한 차이점은 네임스페이스(둘 중 하나 .Uwp 또는 .WinUI끝)입니다. 그러나 UWP 대상은 지속적인 유지 관리 중이며 새로운 기능을 받지 못하고 있습니다. 따라서 WinUI에 대해 여기에 표시된 샘플과 비교하여 일부 코드 변경이 필요할 수 있습니다. 이 문서의 코드 조각은 ComputeSharp.D2D1.WinUI 3.0.0(UWP 대상의 마지막 릴리스는 대신 2.1.0)의 API 표면을 반영합니다.

ComputeSharp에는 Win2D와 상호 운용하기 위한 두 가지 기본 구성 요소가 있습니다.

  • PixelShaderEffect<T>: D2D1 픽셀 셰이더에서 구동되는 Win2D 효과입니다. 셰이더 자체는 ComputeSharp에서 제공하는 API를 사용하여 C#으로 작성됩니다. 이 클래스는 효과 원본, 상수 값 등을 설정하는 속성도 제공합니다.
  • CanvasEffect: 임의의 효과 그래프를 래핑하는 사용자 지정 Win2D 효과의 기본 클래스입니다. 복잡한 효과를 애플리케이션의 여러 부분에서 재사용할 수 있는 사용하기 쉬운 개체로 "패키지"하는 데 사용할 수 있습니다.

다음은 사용자 지정 픽셀 셰이더(이 셰이더토이 셰이더에서 이식됨)를 PixelShaderEffect<T>와 함께 사용한 다음 Win2DCanvasControl에 그리는 예제입니다(PixelShaderEffect<T>ICanvasImage 구현 참고).

무한색 육각형을 표시하고 Win2D 컨트롤에 그려지고 앱 창에서 실행되는 샘플 픽셀 셰이더

두 줄의 코드에서 효과를 만들어 Win2D를 통해 그리는 방법을 확인할 수 있습니다. ComputeSharp는 셰이더를 컴파일하고 등록하며 Win2D 호환 효과의 복잡한 수명을 관리하는 데 필요한 모든 작업을 처리합니다.

다음으로 사용자 지정 D2D1 픽셀 셰이더를 사용하는 사용자 지정 Win2D 효과를 만드는 방법에 대한 단계별 가이드를 살펴보겠습니다. ComputeSharp를 사용하여 셰이더를 작성하고 해당 속성을 설정하는 방법과 애플리케이션에서 쉽게 재사용할 수 있는 형식으로 CanvasEffect에 패키지된 사용자 지정 효과 그래프를 만드는 방법을 살펴보겠습니다.

효과 디자인

이 데모에서는 간단한 서리가 내린 유리 효과를 만들려고 합니다.

다음 구성 요소가 포함됩니다.

  • 가우시안 흐림 효과
  • Tint 효과
  • 노이즈(셰이더를 사용하여 절차적으로 생성할 수 있는)

또한 흐림 효과 및 노이즈 양을 제어하는 속성을 노출하려고 합니다. 최종 효과에는 이 효과 그래프의 "패키지된" 버전이 포함되며 인스턴스를 만들고, 해당 속성을 설정하고, 원본 이미지를 연결한 다음 그리기만 하면 쉽게 사용할 수 있습니다. 그럼 시작하겠습니다.

사용자 지정 D2D1 픽셀 셰이더 만들기

효과 위에 있는 노이즈를 위해 간단한 D2D1 픽셀 셰이더를 사용할 수 있습니다. 셰이더는 해당 좌표(난수의 "시드"로 작동)를 기반으로 임의 값을 계산한 다음, 해당 노이즈 값을 사용하여 해당 픽셀의 RGB 양을 계산합니다. 그런 다음 결과 이미지 위에 이 노이즈를 혼합할 수 있습니다.

ComputeSharp를 사용하여 셰이더를 작성하려면 ID2D1PixelShader 인터페이스를 구현하는 partial struct 형식을 정의한 다음 Execute 메서드에 논리를 작성하기만 하면 됩니다. 이 노이즈 셰이더의 경우 다음과 같이 작성할 수 있습니다.

using ComputeSharp;
using ComputeSharp.D2D1;

[D2DInputCount(0)]
[D2DRequiresScenePosition]
[D2DShaderProfile(D2D1ShaderProfile.PixelShader40)]
[D2DGeneratedPixelShaderDescriptor]
public readonly partial struct NoiseShader(float amount) : ID2D1PixelShader
{
    /// <inheritdoc/>
    public float4 Execute()
    {
        // Get the current pixel coordinate (in pixels)
        int2 position = (int2)D2D.GetScenePosition().XY;

        // Compute a random value in the [0, 1] range for each target pixel. This line just
        // calculates a hash from the current position and maps it into the [0, 1] range.
        // This effectively provides a "random looking" value for each pixel.
        float hash = Hlsl.Frac(Hlsl.Sin(Hlsl.Dot(position, new float2(41, 289))) * 45758.5453f);

        // Map the random value in the [0, amount] range, to control the strength of the noise
        float alpha = Hlsl.Lerp(0, amount, hash);

        // Return a white pixel with the random value modulating the opacity
        return new(1, 1, 1, alpha);
    }
}

참고 항목

셰이더는 전적으로 C#으로 작성되지만 HLSL(ComputeSharp가 C#을 변환하는 DirectX 셰이더의 프로그래밍 언어)에 대한 기본 지식이 권장됩니다.

이 셰이더를 자세히 살펴보겠습니다.

  • 셰이더는 입력이 없으며 임의의 회색조 노이즈가 있는 무한 이미지를 생성합니다.
  • 셰이더는 현재 픽셀 좌표에 액세스해야 합니다.
  • 셰이더는 빌드 시 미리 컴파일됩니다(애플리케이션이 실행될 수 있는 모든 GPU에서 사용할 수 있도록 보장되는 PixelShader40 프로필 사용).
  • 이 특성은 [D2DGeneratedPixelShaderDescriptor] C# 코드를 분석하고, HLSL로 변환하고, 셰이더를 바이트 코드로 컴파일하는 ComputeSharp와 함께 번들로 제공되는 원본 생성기를 트리거하는 데 필요합니다.
  • 셰이더는 기본 생성자를 통해 매개 변수를 캡처 float amount 합니다. ComputeSharp의 원본 생성기는 셰이더에서 캡처된 모든 값을 추출하고 D2D가 셰이더 상태를 초기화하는 데 필요한 상수 버퍼를 준비하는 작업을 자동으로 처리합니다.

그리고이 부분은 완료! 이 셰이더는 필요할 때마다 사용자 지정 노이즈 텍스처를 생성합니다. 다음으로, 모든 효과를 함께 연결하는 효과 그래프를 사용하여 패키지된 효과를 만들어야 합니다.

사용자 지정 효과 생성

사용하기 쉬운 패키지 효과를 위해 ComputeSharp의 CanvasEffect 형식을 사용할 수 있습니다. 이 형식은 효과 그래프를 만들고 효과 사용자가 상호 작용할 수 있는 공용 속성을 통해 업데이트하는 데 필요한 모든 논리를 설정하는 간단한 방법을 제공합니다. 구현해야 하는 두 가지 기본 메서드가 있습니다.

  • BuildEffectGraph: 이 메서드는 그리려는 효과 그래프를 작성합니다. 즉, 필요한 모든 효과를 만들고 그래프의 출력 노드를 등록해야 합니다. 나중에 업데이트할 수 있는 효과의 경우 필요한 경우 그래프에서 효과를 검색하는 조회 키 역할을 하는 연결된 CanvasEffectNode<T> 값으로 등록이 수행됩니다.
  • ConfigureEffectGraph: 이 메서드는 사용자가 구성한 설정을 적용하여 효과 그래프를 새로 고칩니다. 이 메서드는 효과를 그리기 바로 전에 필요할 때 자동으로 호출되며, 효과가 마지막으로 사용된 이후 하나 이상의 효과 속성이 수정된 경우에만 호출됩니다.

사용자 지정 효과는 다음과 같이 정의할 수 있습니다.

using ComputeSharp.D2D1.WinUI;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;

public sealed class FrostedGlassEffect : CanvasEffect
{
    private static readonly CanvasEffectNode<GaussianBlurEffect> BlurNode = new();
    private static readonly CanvasEffectNode<PixelShaderEffect<NoiseShader>> NoiseNode = new();

    private ICanvasImage? _source;
    private double _blurAmount;
    private double _noiseAmount;

    public ICanvasImage? Source
    {
        get => _source;
        set => SetAndInvalidateEffectGraph(ref _source, value);
    }

    public double BlurAmount
    {
        get => _blurAmount;
        set => SetAndInvalidateEffectGraph(ref _blurAmount, value);
    }

    public double NoiseAmount
    {
        get => _noiseAmount;
        set => SetAndInvalidateEffectGraph(ref _noiseAmount, value);
    }

    /// <inheritdoc/>
    protected override void BuildEffectGraph(CanvasEffectGraph effectGraph)
    {
        // Create the effect graph as follows:
        //
        // ┌────────┐   ┌──────┐
        // │ source ├──►│ blur ├─────┐
        // └────────┘   └──────┘     ▼
        //                       ┌───────┐   ┌────────┐
        //                       │ blend ├──►│ output │
        //                       └───────┘   └────────┘
        //    ┌───────┐              ▲   
        //    │ noise ├──────────────┘
        //    └───────┘
        //
        GaussianBlurEffect gaussianBlurEffect = new();
        BlendEffect blendEffect = new() { Mode = BlendEffectMode.Overlay };
        PixelShaderEffect<NoiseShader> noiseEffect = new();
        PremultiplyEffect premultiplyEffect = new();

        // Connect the effect graph
        premultiplyEffect.Source = noiseEffect;
        blendEffect.Background = gaussianBlurEffect;
        blendEffect.Foreground = premultiplyEffect;

        // Register all effects. For those that need to be referenced later (ie. the ones with
        // properties that can change), we use a node as a key, so we can perform lookup on
        // them later. For others, we register them anonymously. This allows the effect
        // to autommatically and correctly handle disposal for all effects in the graph.
        effectGraph.RegisterNode(BlurNode, gaussianBlurEffect);
        effectGraph.RegisterNode(NoiseNode, noiseEffect);
        effectGraph.RegisterNode(premultiplyEffect);
        effectGraph.RegisterOutputNode(blendEffect);
    }

    /// <inheritdoc/>
    protected override void ConfigureEffectGraph(CanvasEffectGraph effectGraph)
    {
        // Set the effect source
        effectGraph.GetNode(BlurNode).Source = Source;

        // Configure the blur amount
        effectGraph.GetNode(BlurNode).BlurAmount = (float)BlurAmount;

        // Set the constant buffer of the shader
        effectGraph.GetNode(NoiseNode).ConstantBuffer = new NoiseShader((float)NoiseAmount);
    }
}

이 클래스에는 다음 네 개의 섹션이 있습니다.

  • 먼저 업데이트할 수 있는 효과와 같은 변경 가능한 모든 상태를 추적하는 필드와 효과의 사용자에게 노출하려는 모든 효과 속성에 대한 지원 필드가 있습니다.
  • 다음으로, 효과를 구성하는 속성이 있습니다. 각 속성의 설정자는 CanvasEffect가 노출하는 SetAndInvalidateEffectGraph 메서드를 사용하며, 설정되는 값이 현재 값과 다른 경우 효과가 자동으로 무효화됩니다. 이렇게 하면 실제로 필요한 경우에만 효과가 다시 구성됩니다.
  • 마지막으로 위에서 언급한 BuildEffectGraphConfigureEffectGraph 메서드가 있습니다.

참고 항목

PremultiplyEffect 노이즈 효과 이후의 노드는 매우 중요합니다. 출력이 미리 곱셈된 것으로 가정하지만 픽셀 셰이더는 일반적으로 곱셈되지 않은 픽셀로 작동하기 때문입니다. 따라서 사용자 정의 셰이더 전후에 곱하기/곱하기 해제 노드를 수동으로 삽입하여 색상이 올바르게 유지되도록 해야 합니다.

참고 항목

이 샘플 효과는 WinUI 3 네임스페이스를 사용하지만 UWP에서도 동일한 코드를 사용할 수 있습니다. 이 경우 ComputeSharp의 네임스페이스는 ComputeSharp.Uwp으로 패키지 이름과 일치합니다.

그릴 준비가 완료되었습니다!

그리고 이것으로, 우리의 사용자 정의 서리가 내린 유리 효과가 준비되었습니다! 다음과 같이 쉽게 그릴 수 있습니다.

private void CanvasControl_Draw(CanvasControl sender, CanvasDrawEventArgs args)
{
    FrostedGlassEffect effect = new()
    {
        Source = _canvasBitmap,
        BlurAmount = 12,
        NoiseAmount = 0.1
    };

    args.DrawingSession.DrawImage(effect);
}

이 예제에서는 이전에 소스로 로드한 CanvasBitmap를 사용하여 CanvasControlDraw 처리기에서 효과를 그립니다. 효과를 테스트하는 데 사용할 입력 이미지입니다.

흐린 하늘 아래 일부 산의 사진

결과는 다음과 같습니다.

위 그림의 흐리게 표시된 버전

참고 항목

그림에 대한 도미닉 랭에 대한 크레딧.

추가 리소스