实现自定义效果

Win2D 提供了多个 API 来表示可绘制的对象,这些对象分为两个类别:图像和效果。 由接口表示 ICanvasImage 的图像没有输入,可以直接在给定图面上绘制。 例如,CanvasBitmapVirtualizedCanvasBitmap图像CanvasRenderTarget类型的示例。 另一方面,效果由 ICanvasEffect 接口表示。 它们可以具有输入和其他资源,并且可以应用任意逻辑来生成输出(作为效果也是图像)。 Win2D 包括包装大多数 D2D 效果的效果,例如 GaussianBlurEffectTintEffectLuminanceToAlphaEffect

图像和效果也可以链接在一起,以创建任意图形,然后可在应用程序中显示(另请参阅 Direct2D 效果上的 D2D 文档)。 它们共同提供了一个极其灵活的系统,以高效方式创作复杂的图形。 但是,在某些情况下,内置效果不够,你可能想要生成自己的 Win2D 效果。 为了支持这一点,Win2D 包括一组功能强大的互操作 API,用于定义可与 Win2D 无缝集成的自定义图像和效果。

提示

如果使用 C# 并想要实现自定义效果或效果图,建议使用 ComputeSharp ,而不是尝试从头开始实现效果。 请参阅下面的 段落,详细了解如何使用此库实现与 Win2D 无缝集成的自定义效果。

平台 API:、、、VirtualizedCanvasBitmapID2D21ImageCanvasBitmapCanvasRenderTargetIGraphicsEffectSourceCanvasEffectICanvasLuminanceToAlphaEffectImageTintEffectGaussianBlurEffectID2D1Factory1ICanvasImageID2D1Effect

实现自定义 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 是大部分工作发生的地方。 此方法负责检索 ID2D1Image Win2D 可以绘制的对象,根据需要选择性地实现当前效果。 这还包括递归遍历和实现所有源的效果图(如果有),以及初始化图像可能需要的任何状态(例如常量缓冲区和其他属性、资源纹理等)。

此方法的确切实现高度依赖于图像类型,它可能会有所不同,但通常说,任意效果,你期望该方法执行以下步骤:

  • 检查调用是否在同一实例上递归,如果出现,则失败。 这需要检测效果图中的周期(例如,效果A作为源效果B,效果B作为源效果)。A
  • 获取映像实例上的锁,以防止并发访问。
  • 根据输入标志处理目标 DPIs
  • 验证输入设备是否与正在使用的设备匹配(如果有)。 如果不匹配并且当前效果支持实现,则不现实效果。
  • 实现对输入设备的影响。 这可以包括根据需要注册从输入设备或设备上下文检索到的对象上的 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 添加标志。 否则,它应将从输入上下文中检索到的 DPIs 设置为 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设置 DPI 补偿,WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATIONWIN2D_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 边界内封送和管理效果源),因此建议还 CanvasEffect 使用 Win2D 代码库中的实现作为自定义效果的参考点,并根据需要对其进行修改。

实施 GetBounds

完全实现自定义 ICanvasImage 效果的最后一个缺失组件是支持两 GetBounds 个重载。 为了简化此操作,Win2D 公开了一个 C 导出,该导出可用于利用任何自定义映像上的 Win2D 中的现有逻辑。 导出如下所示:

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

自定义映像可以调用此 API 并将自己作为 image 参数传递,然后只是将结果返回到其调用方。 如果没有可用的转换,则 transform 参数可以是 null

优化设备上下文访问

deviceContext ICanvasImageInterop::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);
}

接口 ID2D1DeviceContextPoolCanvasDevice实现,这是实现接口的 ICanvasDevice Win2D 类型。 若要使用池,请在 QueryInterface 设备接口上使用以获取 ID2D1DeviceContextPool 引用,然后调用 ID2D1DeviceContextPool::GetDeviceContextLease 以获取对象 ID2D1DeviceContextLease 以访问设备上下文。 不再需要该租约后,请释放租约。 请确保在释放租约后不要触摸设备上下文,因为其他线程可能会同时使用它。

启用 WinRT 包装器查找

如 Win2D 互操作文档中所示,Win2D 公共标头还公开了一种方法(可从ICanvasFactoryNative激活工厂访问,或通过GetOrCreate在同一GetOrCreate标头中定义的C++/CX 帮助程序访问)。 这允许从给定的本机资源检索 WinRT 包装器。 例如,它允许从ID2D1Device1对象、CanvasBitmap对象、对象ID2D1Bitmap等检索或创建CanvasDevice实例。

此方法也适用于所有内置 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 层进行互操作,然后尝试解析它们的包装器。 让我们来了解其中每个 API。

这些 RegisterWrapperUnregisterWrapper 方法将由自定义效果调用,以将自身添加到内部 Win2D 缓存中:

  • RegisterWrapper:注册本机资源及其拥有的 WinRT 包装器。 参数 wrapper 也需要进行限制 IWeakReferenceSource,以便可以正确缓存该参数,而不会导致导致内存泄漏的引用周期。 此方法返回 S_OK 是否可以将本机资源添加到缓存、 S_FALSE 如果已有已注册包装器 resource,如果发生错误,则返回错误代码。
  • UnregisterWrapper:注销本机资源及其包装器。 返回 S_OK 是否可以删除资源( S_FALSE 如果 resource 尚未注册),如果发生了另一个错误,则返回 erro 代码。

自定义效果应调用 RegisterWrapperUnregisterWrapper 每当实现和未实现它们时,即创建新的本机资源并将其关联时。 不支持实现的自定义效果(例如具有固定关联设备的自定义效果)可以调用 RegisterWrapperUnregisterWrapper 以及创建和销毁它们时。 自定义效果应确保从所有可能的代码路径中正确注销自己,这些路径将导致包装器变得无效(例如,包括在最终完成对象时,以防它以托管语言实现)。

自定义 RegisterEffectFactory 效果也使用此方法 UnregisterEffectFactory ,以便它们还可以注册回调以创建新的包装器,以防开发人员尝试解析“孤立”D2D 资源的包装器:

  • RegisterEffectFactory:注册一个回调,该回调采用开发人员传递给 GetOrCreate的相同参数,并为输入效果创建新的可检查包装器。 效果 ID 用作键,以便每个自定义效果可以在首次加载时为其注册工厂。 当然,这只能为每个效果类型执行一次,而不是每次实现效果时。 device在调用任何已注册的回调之前, resource Win2D 会检查参数和wrapper参数,因此保证在调用时CreateWrapper不会null调用它们。 该 dpi 属性被视为可选,在效果类型没有特定用途的情况下,可以忽略它。 请注意,从已注册的工厂创建新的包装器时,该工厂还应确保新包装器在缓存中注册(Win2D 不会自动将外部工厂生成的包装器添加到缓存中)。
  • UnregisterEffectFactory:删除以前注册的回调。 例如,如果在正在卸载的托管程序集中实现效果包装器,则可以使用此方法。

注意

ICanvasFactoryNative 由激活工厂 CanvasDevice实现,可以通过手动调用 RoGetActivationFactory或使用所使用的语言扩展(例如 winrt::get_activation_factory 在 C++/WinRT 中)使用帮助程序 API 进行检索。 有关详细信息,请参阅 WinRT 类型系统 ,详细了解其工作原理。

有关此映射发挥作用的实际示例,请考虑内置 Win2D 效果的工作原理。 如果未实现它们,则所有状态(例如属性、源等)都存储在每个效果实例的内部缓存中。 实现它们后,所有状态都会传输到本机资源(例如,在 D2D 效果上设置属性,所有源都会解析并映射到效果输入等),只要实现效果,它就会充当包装器状态的权威。 也就是说,如果从包装中提取任何属性的值,它将从与其关联的本机 D2D 资源中检索其更新的值。

这可确保如果直接对 D2D 资源进行任何更改,这些更改也会显示在外部包装器上,并且两者永远不会“同步”。 当效果未实现时,所有状态都会在释放资源之前从本机资源传输回包装器状态。 它将一直保留并更新到下次实现效果为止。 现在,请考虑以下事件序列:

  • 你有一些 Win2D 效果(内置或自定义)。
  • 你从中得到它 ID2D1Image (这是一个 ID2D1Effect) 。
  • 创建自定义效果的实例。
  • 你也从那里得到。ID2D1Image
  • 手动将此图像设置为上一效果的输入(通过 ID2D1Effect::SetInput)。
  • 然后,要求该输入的 WinRT 包装器的第一个效果。

由于效果已实现(在请求本机资源时实现),因此它将使用本机资源作为事实来源。 因此,它将获取 ID2D1Image 与请求的源对应的源,并尝试检索其 WinRT 包装器。 如果从中检索此输入的效果已正确将自己的本机资源对和 WinRT 包装器添加到 Win2D 的缓存,包装器将解析并返回到调用方。 否则,该属性访问将失败,因为 Win2D 无法解析 WinRT 包装器,因为它不拥有效果,因为它不知道如何实例化它们。

这是一个位置和RegisterWrapperUnregisterWrapper帮助,因为它们允许自定义效果无缝参与 Win2D 的包装器解析逻辑,以便始终可以检索任何效果源的正确包装器,无论它是从 WinRT API 设置还是直接从基础 D2D 层进行设置。

若要说明工厂的作用如何发挥作用,请考虑以下方案:

  • 用户创建自定义包装器的实例并实现该实例
  • 然后,它们获取对基础 D2D 效果的引用,并保留它。
  • 然后,在不同的设备上实现效果。 该效果将实现并重新实现,为此,它将创建新的 D2D 效果。 此时,以前的 D2D 效果不再作为关联的可检查包装器。
  • 然后,用户调用 GetOrCreate 第一个 D2D 效果。

如果没有回调,Win2D 将无法解析包装器,因为没有为其注册的包装器。 如果改为注册了工厂,则可以创建并返回该 D2D 效果的新包装器,因此方案只是为用户无缝工作。

实现自定义 ICanvasEffect

Win2D ICanvasEffect 接口扩展 ICanvasImage,因此前面的所有点也适用于自定义效果。 唯一的区别是 ICanvasEffect ,还实现特定于效果的其他方法,例如使源矩形失效、获取所需矩形等。

为了支持这一点,Win2D 公开了自定义效果的作者可以使用的 C 导出,因此它们不必从头开始重新实现所有这些额外的逻辑。 这与 C 导出 GetBounds的工作方式相同。 下面是效果的可用导出:

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 是一种可同时支持和 GetRequiredSourceRectangle GetRequiredSourceRectangles. 也就是说,它采用指向现有值数组的指针进行填充,因此调用方可以将指针传递给单个值(也可以位于堆栈上,以避免一个分配),或者传递给值数组。 在这两种情况下,实现都是相同的,因此单个 C 导出足以为这两个导出提供支持。

使用 ComputeSharp 的 C# 中的自定义效果

如前所述,如果使用 C# 并想要实现自定义效果,建议的方法是使用 ComputeSharp 库。 它使你能够完全在 C# 中实现自定义 D2D1 像素着色器,以及轻松定义与 Win2D 兼容的自定义效果图。 Microsoft应用商店中也使用相同的库来为应用程序中的多个图形组件提供支持。

可以通过 NuGet 在项目中添加对 ComputeSharp 的引用:

注意

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 效果的基类。 它可用于将复杂效果“打包”到易于使用的对象中,该对象可在应用程序的多个部分重复使用。

下面是自定义像素着色器(移植自此着色器)的示例,该着色器与 Win2D 一起使用PixelShaderEffect<T>,然后绘制到 Win2D CanvasControl (请注意PixelShaderEffect<T>实现):ICanvasImage

显示无限彩色六边形的示例像素着色器,被绘制到 Win2D 控件上,并在应用窗口中显示

可以在两行代码中了解如何创建效果并通过 Win2D 绘制效果。 ComputeSharp 负责编译着色器、注册着色器和管理 Win2D 兼容效果的复杂生存期所需的所有工作。

接下来,让我们逐步了解如何创建自定义 Win2D 效果,该效果也使用自定义 D2D1 像素着色器。 我们将介绍如何使用 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(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 使用 SetAndInvalidateEffectGraph 公开 CanvasEffect的方法,如果设置的值不同于当前值,则该方法将自动使效果失效。 这可确保仅在真正必要的情况下再次配置效果。
  • 最后,我们有 BuildEffectGraph 上述方法和 ConfigureEffectGraph 方法。

注意

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。 这是我们将用于测试效果的输入图像:

云天下一些山的图片

下面是结果:

上图的模糊版本

注意

归功于 多米尼克·兰格 的图片。

其他资源