次の方法で共有


カスタム効果の実装

Win2D には、描画できるオブジェクトを表す API がいくつか用意されています。これは、画像と効果の 2 つのカテゴリに分かれています。 ICanvasImage インターフェイスで表されるイメージには入力がなく、特定のサーフェスに直接描画できます。 たとえば、 CanvasBitmapVirtualizedCanvasBitmapCanvasRenderTarget はイメージの種類の例です。 一方、効果は、 ICanvasEffect インターフェイスによって表されます。 入力や追加リソースを持ち、任意のロジックを用いることで出力を生成でき、効果は画像としても表現されます。 Win2D には、D2D 効果のほとんどをラップする効果が含まれており、たとえばGaussianBlurEffectTintEffectLuminanceToAlphaEffectなどがあります。

画像と効果を連結して任意のグラフを作成し、アプリケーションに表示することもできます ( Direct2D 効果の D2D ドキュメントも参照してください)。 一緒に、彼らは効率的な方法で複雑なグラフィックスを作成する非常に柔軟なシステムを提供します。 ただし、組み込みの効果では不十分な場合があり、独自の Win2D 効果を作成することもできます。 これをサポートするために、Win2D には、Win2D とシームレスに統合できるカスタム イメージと効果を定義できる強力な相互運用 API のセットが含まれています。

ヒント

C# を使用していて、カスタム効果グラフまたはエフェクト グラフを実装する場合は、ゼロから効果を実装するのではなく、 ComputeSharp を使用することをお勧めします。 このライブラリを使用して Win2D とシームレスに統合するカスタム効果を実装する方法の詳細については 以下の段落を参照してください。

プラットフォーム API:ICanvasImageCanvasBitmapVirtualizedCanvasBitmapCanvasRenderTargetCanvasEffectGaussianBlurEffectTintEffectICanvasLuminanceToAlphaEffectImageIGraphicsEffectSourceID2D21ImageID2D1Factory1ID2D1Effect

カスタムを実装するICanvasImage

サポートする最も簡単なシナリオは、カスタム ICanvasImageの作成です。 前述のように、これは Win2D によって定義された WinRT インターフェイスであり、Win2D が相互運用できるあらゆる種類のイメージを表します。 このインターフェイスは、2 つの GetBounds メソッドのみを公開し、 IGraphicsEffectSource拡張します。これは、"何らかのエフェクト ソース" を表すマーカー インターフェイスです。

ご覧のように、実際に描画を実行するためにこのインターフェイスによって公開される "機能" API はありません。 独自の ICanvasImage オブジェクトを実装するには、 ICanvasImageInterop インターフェイスも実装する必要があります。このインターフェイスは、Win2D がイメージを描画するために必要なすべてのロジックを公開します。 これは、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);
}

また、同じヘッダーの次の 2 つの列挙型にも依存しています。

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
}

カスタム イメージ (または効果) を実装するために必要な 2 つの GetDevice メソッドと GetD2DImage メソッドは、指定されたデバイス上でそれらを初期化し、描画する基になる D2D イメージを取得するための拡張ポイントを Win2D に提供するため、すべてです。 これらのメソッドを正しく実装することは、サポートされているすべてのシナリオで正常に動作することを保証するために重要です。

それらを見て、各メソッドがどのように機能するかを見てみましょう。

実装中 GetDevice

GetDeviceメソッドは、2 つの中で最も簡単です。 これは、効果に関連付けられているキャンバス デバイスを取得して、Win2D が必要に応じて検査できるようにします (たとえば、使用中のデバイスと一致することを確認するため)。 type パラメーターは、返されたデバイスの "関連付けの種類" を示します。

考えられる主なケースは 2 つあります。

  • 画像が効果である場合は、複数のデバイスでの "実現" と "非実現" をサポートする必要があります。 つまり、特定の効果は初期化されていない状態で作成されます。その後、描画中にデバイスが渡されたときに、そのデバイスをそのデバイスで使用し続けるか、別のデバイスに移動できる場合に実現できます。 その場合、効果は内部状態をリセットし、新しいデバイスで再び認識されます。 つまり、関連付けられているキャンバス デバイスは時間の経過と同時に変化する可能性があり、 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_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: 設定されている場合、ユーザーがまだ既存のソースに設定していない場合は、実現されるエフェクト ソースを 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 効果を完全に実装する最後の不足しているコンポーネントは、2 つの GetBounds オーバーロードをサポートするためです。 これを簡単にするために、Win2D は C エクスポートを公開します。これを使用して、任意のカスタム イメージで Win2D からの既存のロジックを活用できます。 エクスポートは次のとおりです。

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

カスタム イメージは、この API を呼び出し、 image パラメーターとして自分自身を渡し、その結果を呼び出し元に返すだけです。 変換が使用できない場合は、 transform パラメーターを nullできます。

デバイス コンテキスト アクセスの最適化

コンテキストが呼び出しの直前に使用できない場合、deviceContextICanvasImageInterop::GetD2DImage パラメーターが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 インターフェイスは、CanvasDeviceによって実装されます。これは、ICanvasDevice インターフェイスを実装する Win2D 型です。 プールを使用するには、デバイス インターフェイスの QueryInterface を使用して ID2D1DeviceContextPool 参照を取得し、 ID2D1DeviceContextPool::GetDeviceContextLease を呼び出して、デバイス コンテキストにアクセスする ID2D1DeviceContextLease オブジェクトを取得します。 不要になったら、リースを解放します。 リースが解放された後は、他のスレッドによって同時に使用される可能性があるため、デバイス コンテキストに触れないようにしてください。

WinRT ラッパー参照の有効化

Win2D 相互運用に関するドキュメントに示すように Win2D パブリック ヘッダーでは、GetOrCreate メソッドも公開されます (ICanvasFactoryNative アクティブ化ファクトリから、または同じヘッダーで定義されている GetOrCreate C++/CX ヘルパーを介してアクセスできます)。 これにより、特定のネイティブ リソースから WinRT ラッパーを取得できます。 たとえば、CanvasDevice オブジェクトからID2D1Device1 インスタンスを取得または作成したり、CanvasBitmapからID2D1Bitmapを作成したりできます。

このメソッドは、組み込みの Win2D 効果すべてに対しても機能します。特定の効果のネイティブ リソースを取得し、それを使用して対応する Win2D ラッパーを取得すると、所有している Win2D 効果が正しく返されます。 カスタム効果を同じマッピング システムからメリットを得るために、Win2D では、CanvasDevice型である ICanvasFactoryNative のアクティブ化ファクトリの相互運用インターフェイスと、追加のエフェクト ファクトリ インターフェイスICanvasEffectFactoryNativeで、いくつかの API が公開されます。

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

ここでは、Win2D 効果を使用できるさまざまなシナリオ、および開発者が D2D レイヤーとの相互運用を行い、それらのラッパーを解決する方法をサポートするために必要であるため、考慮すべき API がいくつかあります。 これらの各 API について説明します。

RegisterWrapperメソッドとUnregisterWrapperメソッドは、カスタム効果によって呼び出され、内部 Win2D キャッシュに追加されます。

  • RegisterWrapper: ネイティブ リソースとその所有 WinRT ラッパーを登録します。 wrapper パラメーターは、メモリ リークにつながる参照サイクルを発生させずに正しくキャッシュできるように、IWeakReferenceSourceを補完するためにも必要です。 このメソッドは、ネイティブ リソースをキャッシュに追加できる場合はS_OKS_FALSEの登録済みラッパーが既に存在する場合はresourceし、エラーが発生した場合はエラー コードを返します。
  • UnregisterWrapper: ネイティブ リソースとそのラッパーの登録を解除します。 リソースを削除できる場合はS_OKS_FALSEがまだ登録されていない場合はresourceし、別のエラーが発生した場合は erro コードを返します。

カスタム効果は、実装されたときと未実装になったとき、つまり、新しいネイティブリソースが作成されて関連付けられるときに、RegisterWrapperUnregisterWrapper を呼び出す必要があります。 実現をサポートしていないカスタム効果 (たとえば、固定の関連付けられたデバイスを持つもの) は、作成および破棄されたときに RegisterWrapperUnregisterWrapper を呼び出すことができます。 カスタム効果は、ラッパーが無効になる可能性のあるすべてのコード パスから正しく登録解除する必要があります (たとえば、オブジェクトがマネージド言語で実装されている場合に、オブジェクトが終了した場合を含む)。

RegisterEffectFactoryメソッドとUnregisterEffectFactoryメソッドは、カスタム効果によって使用されることも想定されているため、開発者が "孤立した" D2D リソースのラッパーを解決しようとした場合に備えて、コールバックを登録して新しいラッパーを作成することもできます。

  • RegisterEffectFactory: 開発者が GetOrCreateに渡したのと同じパラメーターを入力で受け取り、入力効果の新しい検査可能ラッパーを作成するコールバックを登録します。 エフェクト ID はキーとして使用されるため、各カスタムエフェクトは最初に読み込まれたときにファクトリを登録できます。 もちろん、これは効果の種類ごとに 1 回だけ行う必要があり、効果が実現されるたびには行われません。 deviceresource、およびwrapperパラメーターは、登録されたコールバックを呼び出す前に Win2D によってチェックされるため、nullが呼び出されたときにCreateWrapperされないことが保証されます。 dpiは省略可能と見なされ、効果の種類に特定の用途がない場合は無視できます。 登録済みファクトリから新しいラッパーを作成する場合は、そのファクトリも新しいラッパーがキャッシュに登録されていることを確認する必要があることに注意してください (Win2D は外部ファクトリによって生成されたラッパーをキャッシュに自動的に追加しません)。
  • UnregisterEffectFactory: 以前に登録されたコールバックを削除します。 たとえば、これは、アンロードされるマネージド アセンブリにエフェクト ラッパーが実装されている場合に使用できます。

ICanvasFactoryNative は、 CanvasDeviceのアクティブ化ファクトリによって実装されます。これは、 RoGetActivationFactoryを手動で呼び出すか、使用している言語拡張機能 (C++/WinRT で winrt::get_activation_factory など) からヘルパー API を使用して取得できます。 詳細については、「 WinRT 型システム の動作の詳細を参照してください。

このマッピングの実際の例については、組み込みの Win2D 効果のしくみを検討してください。 それらが実現されない場合、すべての状態 (プロパティ、ソースなど) は、各エフェクト インスタンスの内部キャッシュに格納されます。 それらが実現されると、すべての状態がネイティブ リソースに転送されます (たとえば、プロパティは D2D 効果に設定され、すべてのソースは解決され、効果の入力などにマップされます)。効果が実現される限り、それはラッパーの状態に対する権限として機能します。 つまり、いずれかのプロパティの値がラッパーからフェッチされた場合、それに関連付けられているネイティブ D2D リソースから更新された値が取得されます。

これにより、D2D リソースに直接変更が加えられた場合、それらは外部ラッパーにも表示され、2 つは "同期されていません" になることが保証されます。 効果が未実現の場合、リソースが解放される前に、すべての状態がネイティブ リソースからラッパー状態に戻されます。 次に効果が実現されるまで、保持され、そこで更新されます。 次に、次の一連のイベントについて考えます。

  • Win2D 効果 (組み込みまたはカスタム) があります。
  • そこから ID2D1Image を取得します (これは ID2D1Effectです)。
  • カスタム効果のインスタンスを作成します。
  • また、そこから ID2D1Image を取得します。
  • このイメージは、( ID2D1Effect::SetInputを使用して) 前の効果の入力として手動で設定します。
  • 次に、その入力の WinRT ラッパーの最初の効果を求めます。

効果が実現されるため (ネイティブ リソースが要求されたときに実現されました)、ネイティブ リソースを真実のソースとして使用します。 そのため、要求されたソースに対応する ID2D1Image を取得し、その WinRT ラッパーを取得しようとします。 この入力が取得された効果によって、ネイティブ リソースと WinRT ラッパーの独自のペアが Win2D のキャッシュに正しく追加された場合、ラッパーは解決され、呼び出し元に返されます。 存在しない場合、そのプロパティ アクセスは失敗します。なぜなら、Win2D は所有していない効果の WinRT ラッパーを解決できず、それをインスタンス化する方法を知らないためです。

これは、カスタム効果が Win2D のラッパー解決ロジックにシームレスに参加できるように、 RegisterWrapperUnregisterWrapper のヘルプです。これにより、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);

どのように使用できるかを見てみましょう。

  • InvalidateSourceRectangleForICanvasImageInterop は、 InvalidateSourceRectangleをサポートするためのものです。 入力パラメーターをマーシャリングして直接呼び出すだけで、必要なすべての作業が処理されます。 image パラメーターは、実装されている現在のエフェクト インスタンスであることに注意してください。
  • GetInvalidRectanglesForICanvasImageInterop では、 GetInvalidRectanglesがサポートされます。 これは、返された COM 配列が不要になったら破棄する必要がある以外に、特別な考慮事項も必要ありません。
  • GetRequiredSourceRectanglesForICanvasImageInterop は、 GetRequiredSourceRectangleGetRequiredSourceRectanglesの両方をサポートできる共有メソッドです。 つまり、設定する値の既存の配列へのポインターを受け取るので、呼び出し元は単一の値 (スタック上に配置することもでき、1 回の割り当てを回避することもできます) または値の配列へのポインターを渡すことができます。 どちらの場合も実装は同じであるため、1 つの C エクスポートで両方に電力を供給するのに十分です。

ComputeSharp を使用した C# でのカスタム効果

前述のように、C# を使用していて、カスタム効果を実装する場合は、 ComputeSharp ライブラリを使用することをお勧めします。 C# でカスタム D2D1 ピクセル シェーダーを完全に実装できるだけでなく、Win2D と互換性のあるカスタム 効果グラフを簡単に定義することもできます。 同じライブラリは、アプリケーション内の複数のグラフィックス コンポーネントに電力を供給するために、Microsoft Store でも使用されます。

NuGet を使用して、プロジェクトに ComputeSharp への参照を追加できます。

ComputeSharp.D2D1.* の多くの API は、UWP ターゲットと WinUI ターゲットで同じです。唯一の違いは名前空間 ( .Uwp または .WinUI で終わる) です。 ただし、UWP ターゲットは継続的なメンテナンスを行っており、新機能を受け取りません。 そのため、WinUI に関してここに示すサンプルと比較して、いくつかのコード変更が必要になる場合があります。 このドキュメントのスニペットは、ComputeSharp.D2D1.WinUI.0.0 (UWP ターゲットの最終リリースは 2.1.0) の時点の API サーフェスを反映しています。

ComputeSharp には、Win2D と相互運用するための主なコンポーネントが 2 つあります。

  • PixelShaderEffect<T>: D2D1 ピクセル シェーダーを利用する Win2D 効果。 シェーダー自体は、ComputeSharp によって提供される API を使用して C# で記述されます。 このクラスには、エフェクト ソース、定数値などを設定するためのプロパティも用意されています。
  • CanvasEffect: 任意の効果グラフをラップするカスタム Win2D 効果の基本クラス。 複雑な効果を、アプリケーションのいくつかの部分で再利用できる使いやすいオブジェクトに "パッケージ化" するために使用できます。

カスタムピクセルシェーダー(このshadertoyシェーダーから移植された)の例を次に示します。それはPixelShaderEffect<T>と一緒に使用され、Win2DCanvasControlに描画されます(PixelShaderEffect<T>ICanvasImageを実装します)。

無限色の六角形を表示し、Win2D コントロールに描画され、アプリ ウィンドウで実行中に表示されるサンプル ピクセル シェーダー

2 行のコードで効果を作成し、Win2D を使用して描画する方法を確認できます。 ComputeSharp は、シェーダーのコンパイル、登録、Win2D 互換効果の複雑な有効期間の管理に必要なすべての作業を処理します。

次に、カスタム D2D1 ピクセル シェーダーも使用するカスタム Win2D 効果を作成する方法のステップ バイ ステップ ガイドを見てみましょう。 ComputeSharp を使用してシェーダーを作成し、そのプロパティを設定する方法と、アプリケーションで簡単に再利用できる CanvasEffect 型にパッケージ化されたカスタム効果グラフを作成する方法について説明します。

効果の設計

このデモでは、単純なフロスト ガラス効果を作成します。

これには、次のコンポーネントが含まれます。

  • ガウスぼかし
  • 色合い効果
  • ノイズ (シェーダーを使用して手続き的に生成できます)

また、ぼかしとノイズの量を制御するプロパティを公開する必要があります。 最終的な効果には、このエフェクト グラフの "パッケージ化された" バージョンが含まれます。インスタンスを作成し、それらのプロパティを設定し、ソース イメージを接続して描画するだけで簡単に使用できます。 それでは始めましょう。

カスタム D2D1 ピクセル シェーダーの作成

効果の上のノイズには、単純な D2D1 ピクセル シェーダーを使用できます。 シェーダーは、その座標 (乱数の "シード" として機能する) に基づいてランダムな値を計算し、そのノイズ値を使用してそのピクセルの RGB 量を計算します。 その後、結果の画像の上にこのノイズをブレンドできます。

ComputeSharp を使用してシェーダーを記述するには、partial struct インターフェイスを実装するID2D1PixelShader型を定義し、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 シェーダーのプログラミング言語) に関する基本的な知識をお勧めします。

このシェーダーについて詳しく見てみましょう。

  • シェーダーには入力がなく、ランダムなグレースケール ノイズを含む無限の画像が生成されます。
  • シェーダーは、現在のピクセル座標にアクセスする必要があります。
  • シェーダーはビルド時にプリコンパイルされます ( PixelShader40 プロファイルを使用します。これは、アプリケーションが実行されている可能性がある任意の GPU で使用可能であることが保証されます)。
  • [D2DGeneratedPixelShaderDescriptor]属性は、ComputeSharp にバンドルされているソース ジェネレーターをトリガーするために必要です。これにより、C# コードが分析され、HLSL にトランスパイルされ、シェーダーがバイトコードにコンパイルされます。
  • シェーダーは、float amountをその主コンストラクタを通じてパラメーターをキャプチャします。 ComputeSharp のソース ジェネレーターは、シェーダーでキャプチャされたすべての値を抽出し、D2D がシェーダーの状態を初期化するために必要な定数バッファーを準備する処理を自動的に処理します。

そして、この部分は行われています! このシェーダーは、必要に応じてカスタム ノイズ テクスチャを生成します。 次に、すべての効果を結び付ける効果グラフを使用して、パッケージ化された効果を作成する必要があります。

カスタム効果の作成

使いやすくパッケージ化された効果を得るために、ComputeSharp の CanvasEffect 型を使用できます。 この型は、効果グラフを作成し、効果のユーザーが操作できるパブリック プロパティを使用して更新するために必要なすべてのロジックを簡単に設定する方法を提供します。 実装する必要がある主な方法は 2 つあります。

  • BuildEffectGraph: このメソッドは、描画する効果グラフを作成します。 つまり、必要なすべての効果を作成し、グラフの出力ノードを登録する必要があります。 後で更新できる効果の場合、登録は関連付けられた CanvasEffectNode<T> 値で行われます。これは、必要に応じてグラフから効果を取得するための参照キーとして機能します。
  • ConfigureEffectGraph: このメソッドは、ユーザーが構成した設定を適用して効果グラフを更新します。 このメソッドは、効果を描画する直前に、必要に応じて自動的に呼び出されます。このメソッドは、効果が最後に使用されてから少なくとも 1 つの効果プロパティが変更された場合にのみ呼び出されます。

カスタム効果は次のように定義できます。

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

このクラスには、次の 4 つのセクションがあります。

  • 最初に、変更可能なすべての状態 (更新可能な効果など) を追跡するフィールドと、効果のユーザーに公開するすべての効果プロパティのバッキング フィールドがあります。
  • 次に、効果を構成するためのプロパティがあります。 各プロパティのセッターは、SetAndInvalidateEffectGraphによって公開されるCanvasEffect メソッドを使用します。設定されている値が現在の値と異なる場合は、自動的に効果が無効になります。 これにより、効果は本当に必要なときにのみ再び構成されます。
  • 最後に、上記の BuildEffectGraph メソッドと ConfigureEffectGraph メソッドがあります。

ノイズ効果の後の PremultiplyEffect ノードは非常に重要です。これは、Win2D 効果では出力が事前に乗算されると想定されるのに対し、ピクセル シェーダーは一般的に未乗算ピクセルで動作するためです。 したがって、色が正しく保持されるように、カスタムシェーダーの前後にプリマルチプライ/アンプリマルチプライノードを手動で挿入することを心がけてください。

このサンプル効果は WinUI 名前空間を使用していますが、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);
}

この例では、以前にソースとして読み込んだDrawを使用して、CanvasControlCanvasBitmap ハンドラーから効果を描画します。 これは、効果のテストに使用する入力イメージです。

曇った空の下の山の写真

結果を次に示します。

上の図のぼやけたバージョン

画像の Dominic Lange へのクレジット。

その他のリソース