共用方式為


實作自訂效果

Win2D 提供數個 API 來代表可繪製的對象,這些對象分為兩個類別:影像和效果。 影像由 ICanvasImage 介面所代表,本身沒有輸入,而且可以直接繪製在指定的介面上。 例如,CanvasBitmapVirtualizedCanvasBitmapCanvasRenderTarget 是影像類型的範例。 另一方面,效果是由 ICanvasEffect 介面來代表。 它們可以有輸入和其他資源,而且可以套用任意邏輯來產生其輸出 (因為一個效果也是一個影像)。 Win2D 包含的效果裡,也包裝了大部分的 D2D 效果,例如 GaussianBlurEffectTintEffectLuminanceToAlphaEffect

影像和效果也可以鏈結在一起,以建立任意圖形,然後顯示在您的應用程式中 (也請參閱 Direct2D 效果上的 D2D 文件)。 它們一起提供極其靈活的系統,以有效率的方式撰寫複雜的圖形。 不過,在某些情況下,內建效果還不足夠,而您可能想要建立您自己獨屬的 Win2D 效果。 為了支援這一點,Win2D 包含一組功能強大的交互操作 API,可定義自訂影像和效果,這些影像和效果可順暢地與 Win2D 整合的。

提示

如果您使用 C# 並想要實作自訂效果或效果圖形,建議您使用 ComputeSharp,而不是嘗試從頭開始實作效果。 如需如何使用此程式庫實作與 Win2D 無縫整合的自訂效果,請參閱以下段落

平臺 API:、、CanvasBitmapTintEffectCanvasEffectICanvasLuminanceToAlphaEffectImageGaussianBlurEffectVirtualizedCanvasBitmapCanvasRenderTarget、、、IGraphicsEffectSource、、 ID2D21ImageID2D1Factory1ICanvasImageID2D1Effect

實作一個自訂的 ICanvasImage

最簡單的支援案例是建立自訂 ICanvasImage。 如前所述,這是 Win2D 所定義的 WinRT 介面,代表 Win2D 可以與之交互操作的各種影像。 這個介面只會公開兩種 GetBounds 方法,並延伸了 IGraphicsEffectSource,這是代表「某些效果來源」的標記介面。

如您所見,此介面不會公開任何「功能性」API,來實際執行任何繪圖。 為了實作您自己的 ICanvasImage 物件,您也必須實作 ICanvasImageInterop 介面,這會公開 Win2D 繪製影像所需的所有邏輯。 這是公用 Microsoft.Graphics.Canvas.native.h 標頭中定義的 COM 介面,隨附於 Win2D。

介面定義如下:

[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。 因此,type 應該設定為 WIN2D_GET_DEVICE_ASSOCIATION_TYPE_REALIZATION_DEVICE,如果有可用的裝置,則傳回的裝置應該設定為目前的實現裝置。
  • 某些影像具有單一的「擁有裝置」,會在建立時指派,且永遠無法變更。 舉例來說,代表紋理的影像就是這種情況,因為該影像是在特定裝置上分配,而且無法移動。 呼叫 GetDevice 時,它應該傳回建立裝置,並將 type 設定為 WIN2D_GET_DEVICE_ASSOCIATION_TYPE_CREATION_DEVICE。 請注意,指定此類型時,傳回的裝置不應該是 null

注意

Win2D 可以在以遞迴方式周遊效果圖形時呼叫 GetDevice,這表示堆疊中可能會有多個對 GetD2DImage 的作用中呼叫。 因此,GetDevice 不應該對目前的影像採取封鎖鎖定,因為這可能會造成鎖死。 相反地,它應該以非封鎖方式使用重新進入鎖定,並在無法取得時傳回錯誤。 這可確保同一個以遞迴方式呼叫它的執行緒將會成功取得它,而執行相同作業的並行執行緒會正常失敗。

實作 GetD2DImage

GetD2DImage 是大部分工作發生的地方。 此方法負責擷取 Win2D 可以繪製的 ID2D1Image 物件,並視需要選擇性地實現目前的效果。 這也包括以遞迴方式周遊和實現所有來源的效果圖 (如果有的話),以及初始化影像可能需要的任何狀態 (例如常數緩衝區和其他屬性、資源紋理等)。

這個方法如何確切實作,會受到影像類型很大的影響,而且可能會有很大的差異,但一般而言,對於任意效果,您可以預期方法會執行下列步驟:

  • 檢查呼叫是否在相同執行個體上遞迴,如果是,則失敗。 這個步驟是為了在效果圖中偵測循環 (例如效果 A 的來源是效果 B,而效果 B 的來源是效果 A)。
  • 取得影像執行個體的鎖定,以防止並行存取。
  • 根據輸入旗標處理目標 DPI
  • 驗證輸入裝置是否符合使用中的輸入裝置 (如果有的話)。 如果不符,且目前的效果支持實現,則取消實現效果。
  • 在輸入裝置上實現效果。 如有需要,這可以包括將 D2D 效果註冊到 ID2D1Factory1 物件,該物件是從輸入裝置或裝置內容擷取而來。 此外,在建立 D2D 效果執行個體時,應該設定所有必要的狀態。
  • 以遞迴方式周遊任何來源,並將其繫結至 D2D 效果。

對於輸入旗標,有數個可能的情況可以適當處理自訂效果,以確保與所有其他 Win2D 效果的相容性。 排除 WIN2D_GET_D2D_IMAGE_FLAGS_NONE,要處理的旗標如下:

  • WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT:在此情況下,device 保證不會是 null。 效果應該檢查裝置內容目標是否為 ID2D1CommandList,如果是,請新增 WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION 旗標。 否則,它應該將 targetDpi (也保證不是 null) 設定為從輸入內容擷取的 DPI。 然後,它應該從旗標中移除 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 補償效果。
  • 否則,如果 inputDpi 不是 0、未設定 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) 是非常複雜的,而且只適合非常需要額外彈性的進階使用者來執行。 建議您先深入了解 D2D、Win2D、COM、WinRT 和 C++,再嘗試撰寫 ICanvasImageInterop 實作。 如果您的自訂 Win2D 效果也必須包裝自訂 D2D 效果,也將會需要實作自己的 ID2D1Effect 物件 (請參閱有關自訂效果的 D2D 文件,進一步了解這方面的詳細資訊)。 這些文件並非所有必要邏輯的詳盡描述 (舉例來說,它們未涵蓋應如何跨 D2D/Win2D 界限封送處理及管理效果來源),因此建議也使用 Win2D 程式碼基底中的 CanvasEffect 實作作為自訂效果的參考點,並視需要加以修改。

實作 GetBounds

要完全實作自訂 ICanvasImage 效果,還差最後一個部分,那就是要支援兩個 GetBounds 多載。 為了簡化這項操作,Win2D 會公開 C 匯出,這可以用來利用 Win2D 現有的相關邏輯,將其應用到任何自訂影像上。 該匯出如下:

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

自訂影像可以叫用此 API 並將自己傳遞為 image 參數,然後直接將結果傳回給其呼叫端。 如果沒有可用的轉換,參數 transform 可以是 null

最佳化裝置內容存取

如果叫用之前沒有立即可用的內容,ICanvasImageInterop::GetD2DImage 中的 deviceContext 參數有時可以是 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 包裝函式。 舉例來說,它可讓您從 ID2D1Device1 物件擷取或建立 CanvasDevice 執行個體、從 ID2D1Bitmap 取得 CanvasBitmap,諸如此類。

這個方法也適用於所有的內建 Win2D 效果:擷取指定效果的原生資源,然後使用該效果擷取對應的 Win2D 包裝函式,將會正確傳回其擁有的 Win2D 效果。 為了讓自訂效果也受益於相同的對應系統,Win2D 在 CanvasDevice (屬於 ICanvasFactoryNative 類型) 的啟用處理站交互操作介面中,公開了數個 API,還有一個額外的效果處理站介面 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 層進行交互操作,然後嘗試為它們解析包裝函式。 讓我們逐一看看這些 API。

RegisterWrapperUnregisterWrapper 方法要由自訂效果叫用,來將自己新增至內部 Win2D 快取:

  • RegisterWrapper:註冊原生資源及其擁有的 WinRT 包裝函式。 wrapper 參數也必須實作 IWeakReferenceSource,如此才可以正確地快取它,而不會造成導致記憶體流失的參考循環。 如果原生資源可以新增至快取,方法會傳回 S_OK;如果已經有 resource 的已註冊包裝函式,方法會傳回 S_FALSE,如果發生錯誤,則傳回錯誤碼。
  • UnregisterWrapper:取消註冊原生資源及其包裝函式。 如果資源可能已移除,會傳回 S_OK、如果尚未註冊 resource,則傳回 S_FALSE;如果發生其他錯誤,則傳回錯誤碼。

每當自訂效果被實現及取消實現時,都應該呼叫 RegisterWrapperUnregisterWrapper,也就是在建立新的原生資源並與它們建立關聯的時候。 不支援實現的自訂效果 (例如那些擁有固定相關聯裝置的效果) 可以在建立和終結時,呼叫 RegisterWrapperUnregisterWrapper。 對於會導致包裝函式變成無效的程式碼路徑 (舉例來說,如果物件是以 Managed 語言實作,則包括完成物件的時候),自訂效果務必要正確取消註冊所有這些可能的程式碼路徑。

RegisterEffectFactoryUnregisterEffectFactory 方法也是要供自訂效果使用,因此它們也可以註冊回呼來建立新的包裝函式,以備開發人員嘗試解決一個包裝函式來處理「孤立」D2D 資源:

  • RegisterEffectFactory:註冊回呼,以接受開發人員傳遞至 GetOrCreate 的相同參數,並針對輸入效果建立新的可檢查包裝函式。 效果識別碼會當做索引鍵使用,讓每個自訂效果可以在首次載入時為其註冊處理站。 當然,這應該只對每個效果類型執行一次,而不是每次實現效果時都執行。 在叫用任何已註冊的回呼之前,Win2D 會先檢查 deviceresourcewrapper 參數,因此保證不會在叫用 CreateWrapper 時為 nulldpi 會視為選擇性,而且若是效果類型對其沒有特定的用途,則可以忽略它。 請注意,從已註冊的處理站建立新的包裝函式時,該處理站也應該確定新的包裝函式已在快取中註冊 (Win2D 不會自動將外部處理站產生的包裝函式新增至快取)。
  • UnregisterEffectFactory:移除先前的註冊回呼。 例如,如果在要卸載的 Managed 組件中實作效果包裝函式,就可以使用此方式。

注意

ICanvasFactoryNative 是由 CanvasDevice 的啟用處理站實作,您可以手動呼叫 RoGetActivationFactory 來擷取,或是使用從您所用語言延伸模組提供的協助程式 API (例如 C++/WinRT 的 winrt::get_activation_factory)。 如需詳細資訊,請參閱 WinRT 類型系統,以取得其運作方式的詳細資訊。

為了提供一個實際範例來展示此對應的應用場合,請考慮內建 Win2D 效果的運作方式。 如果未實現它們,所有狀態 (例如屬性、來源等) 都會儲存在每個效果執行個體的內部快取中。 當它們實現時,所有狀態都會傳輸至原生資源 (例如,屬性是在 D2D 效果上設定,所有來源都會解析並對應至效果輸入等),而只要效果實現,它就會成為包裝函式狀態的授權單位。 也就是說,如果從包裝函式擷取任何屬性的值,它會為此從與其相關聯的原生 D2D 資源擷取更新的值。

這可確保如果直接對 D2D 資源進行任何變更,這些變更也會顯示在外部包裝函式上,而且兩者永遠不會「喪失同步」。 當效果未實現時,所有狀態都會從原生資源傳輸回包裝函式狀態,再釋放資源。 它會保留在該處並更新,直到下次實現效果為止。 現在,請考慮此事件的順序:

  • 您有一些 Win2D 效果 (可能是內建或自訂)。
  • 你從這個效果 (是一個 ID2D1Effect) 得到 ID2D1Image
  • 您建立了一個自訂效果的執行個體。
  • 你也會從中得到 ID2D1Image
  • 您手動將此影像設定為上一個效果的輸入 (透過 ID2D1Effect::SetInput)。
  • 然後,您要求第一個效果提供與該輸入相關的 WinRT 包裝函式。

由於效果已實現 (在要求原生資源時已實現),因此會使用原生資源作為事實來源。 因此,它會取得對應所要求來源的 ID2D1Image,並嘗試為其擷取 WinRT 包裝函式。 如果從中擷取此輸入的效果已正確將自己的原生資源配對和 WinRT 包裝函式新增至 Win2D 的快取,包裝函式將會解析並傳回給呼叫端。 如果沒有,該屬性存取將會失敗,因為 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);

讓我們來看看可以如何使用他們:

  • InvalidateSourceRectangleForICanvasImageInterop 是要支援 InvalidateSourceRectangle。 只要封送處理輸入參數並直接叫用它,就會處理所有必要的工作。 請注意,image 參數是正在實作的目前效果執行個體。
  • GetInvalidRectanglesForICanvasImageInterop 支援 GetInvalidRectangles。 除了當不再需要 COM 陣列之後必須處置傳回的 COM 陣列之外,這個狀況也不需要特別考慮。
  • GetRequiredSourceRectanglesForICanvasImageInterop 是可同時支援 GetRequiredSourceRectangleGetRequiredSourceRectangles的共用方法。 也就是說,它需要一個指向現有值陣列的指標來填入,因此呼叫端可以將指標傳遞至單一值 (也可以位於堆疊上,來避免一次配置),或傳遞至值的陣列。 在這兩種情況下,實作都相同,因此一個單獨的 C 匯出就足以驅動這兩種情況。

使用 ComputeSharp 在 C# 中的自訂效果

如前所述,如果您使用 C# 並想要實作自訂效果,建議的方法是使用 ComputeSharp 程式庫。 它可讓您完全在 C# 中實作自訂 D2D1 像素著色器,也能輕鬆地定義與 Win2D 相容的自訂效果圖形。 Microsoft Store 也使用相同的程式庫,在應用程式中提供數個圖形元件。

您可以透過 NuGet 在專案中加入對 ComputeSharp 的參考:

注意

ComputeSharp.D2D1.* 中的許多 API 在 UWP 和 WinAppSDK 目標上都相同,唯一的差異是命名空間(結尾 .Uwp 為 或 .WinUI)。 不過,UWP 目標處於持續維護中,而不會接收新功能。 因此,相較於 WinUI 此處所示的範例,可能需要某些程式代碼變更。 本檔中的代碼段會反映自 ComputeSharp.D2D1.WinUI 3.0.0 起的 API 介面(UWP 目標的最後一個版本是 2.1.0)。

ComputeSharp 中有兩個主要元件可與 Win2D 交互操作:

  • PixelShaderEffect<T>:由 D2D1 像素著色器驅動的 Win2D 效果。 著色器本身會使用 ComputeSharp 所提供的 API 以 C# 撰寫。 這個類別也提供屬性來設定效果來源、常數值等等。
  • CanvasEffect:自訂 Win2D 效果的基底類別,會包裝任意效果圖形。 它可以用來將複雜的效果「封裝」成容易使用的物件,可在應用程式的數個部分重複使用。

以下是自訂像素著色器的範例 (移植自這個 Shadertoy 著色器),搭配 PixelShaderEffect<T> 使用,然後繪製到 Win2D CanvasControl (請注意,PixelShaderEffect<T> 實作了 ICanvasImage):

顯示無限彩色六邊形的圖元著色器範例、繪製至 Win2D 控制件,並在應用程式視窗中顯示執行

您可以看見如何只用兩行程式碼,就能建立效果並透過 Win2D 加以繪製。 對於編譯著色器、註冊著色器,以及管理 Win2D 相容效果的複雜存留期,ComputeSharp 會負責所有必要的工作。

接下來,讓我們看看逐步指南,了解如何建立自訂 Win2D 效果,該效果也會使用自訂 D2D1 像素著色器。 我們將探討如何使用 ComputeSharp 撰寫著色器並設定其屬性,以及如何建立自訂效果圖形並封裝成可在應用程式中輕鬆重複使用的 CanvasEffect 類型。

設計效果

在此示範中,我們想要建立簡單的毛玻璃效果。

這將包含下列元件:

  • 高斯模糊
  • 色調效果
  • 雜點 (我們能以程序方式透過著色器產生)

我們也想要公開屬性來控制模糊和雜點量。 最終效果將包含這個效果圖形的「已封裝」版本,而且只要建立執行個體、設定這些屬性、連接來源影像,然後再繪製它,即可輕鬆使用。 現在就開始吧!

建立自訂 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 的基本知識 (DirectX 著色器的程式設計語言,ComputeSharp 會將 C# 轉譯成這個語言)。

讓我們詳細探討此著色器:

  • 著色器沒有輸入,它只會產生一個具有隨機灰階雜點的無限影像。
  • 著色器需要存取目前的像素座標。
  • 著色器會在建置階段預先編譯 (使用 PixelShader40 配置檔,以保證在可能執行應用程式的任何 GPU 上都可供使用)。
  • [D2DGeneratedPixelShaderDescriptor]需要 屬性才能觸發與 ComputeSharp 配套的來源產生器,其會分析 C# 程式代碼、將它轉譯至 HLSL、將著色器編譯為位元組程式代碼等等。
  • 著色器會透過其主要建構函式擷取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);
    }
}

您可以看到此類別中有四個區段:

  • 首先,我們有欄位可追蹤所有可變動的狀態,例如可以更新的效果,以及想要公開給效果使用者的所有效果屬性所適用的支援欄位。
  • 接下來,我們有屬性可設定效果。 每個屬性的 setter 都會使用 CanvasEffect 所公開的 SetAndInvalidateEffectGraph 方法,如果所設定的值與目前的值不同,這個方法會自動使效果失效。 這可確保只有在真正需要時,才會再次設定效果。
  • 最後,我們有上述的 BuildEffectGraphConfigureEffectGraph 方法。

注意

雜點效果之後的 PremultiplyEffect 節點非常重要:這是因為 Win2D 效果假設輸出是預乘的,而像素著色器通常會使用未預乘的像素。 因此,請記得在自訂著色器前後,手動插入預乘/非預乘節點,以確定正確保留色彩。

注意

此範例效果是使用 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,從 CanvasControl 的處理常式 Draw 來繪製效果。 這是我們將用來測試效果的輸入影像:

一張雲天下一些山的圖片

而以下為結果:

上圖的模糊版本

注意

感謝 Dominic Lange 提供圖片。

其他資源