实现自定义效果

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

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

提示

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

实现自定义 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作为来源)。
  • 获取映像实例上的锁,以防止并发访问。
  • 根据输入标志处理目标 DPI
  • 验证输入设备是否与正在使用的设备匹配(如果有)。 如果不匹配且当前效果支持实现,则未实现该效果。
  • 在输入设备上实现该效果。 如果需要,这可以包括在从输入设备或设备上下文检索的 ID2D1Factory1 对象上注册 D2D 效果。 此外,应在正在创建的 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 标志。 否则,应将从输入上下文中检索到的 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 补偿效果。
  • 否则,如果inputDpi不等于0WIN2D_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 边界内封送和管理效果源),因此建议还 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);
}

接口 ID2D1DeviceContextPool 由实现接口 CanvasDevice 的 Win2D 类型 ICanvasDevice 实现。 要使用池,请在设备接口上使用QueryInterface以获得ID2D1DeviceContextPool引用,然后调用ID2D1DeviceContextPool::GetDeviceContextLease以获取ID2D1DeviceContextLease对象,从而访问设备上下文。 不再需要该租约后,请释放租约。 确保在租约释放后不要触碰设备上下文,因为它可能被其他线程并发使用。

启用 WinRT 包装器查找

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

此方法也适用于所有内置 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,以便能够正确缓存,从而避免导致内存泄漏的循环引用。 该方法返回 表示本机资源已成功添加到缓存,返回 表示已经存在< c2 />的注册包装器,发生错误时返回错误代码。
  • UnregisterWrapper:注销本地资源及其包装器。 返回 S_OK 是否可以删除资源( S_FALSE 如果 resource 尚未注册),如果发生了另一个错误,则返回 erro 代码。

自定义效果应在实现和未实现自身时(即当创建新的本机资源并与它们关联时)调用 RegisterWrapperUnregisterWrapper。 不支持实现的自定义效果(例如具有固定关联设备的自定义效果)在创建和销毁时可以调用 RegisterWrapperUnregisterWrapper。 自定义效果应确保从所有可能使包装器失效的代码路径中正确注销自身(例如,如果以托管语言实现,包括在对象终结时)。

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

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

注意

ICanvasFactoryNativeCanvasDevice 的激活工厂实现,你可以通过手动调用 RoGetActivationFactory,或使用所用语言扩展中的帮助程序 API(例如 C++/WinRT 中的 winrt::get_activation_factory)来检索它。 有关详细信息,请参阅 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 效果不再有关联的可检查包装器。
  • 然后,用户在第一个 D2D 效果上调用 GetOrCreate

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

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

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

可以通过 NuGet 在项目中添加对 ComputeSharp 的引用:选择 ComputeSharp.D2D1.WinUI 包。

注意

ComputeSharp.D2D1.* 中的许多 API 在 UWP 和 WinUI 目标之间是没有区别的,唯一的区别是命名空间(结尾是 .Uwp.WinUI)。 但是,UWP 目标处于持续维护状态,不会接收新功能。 因此,与 WinUI 此处显示的示例相比,可能需要进行一些代码更改。 本文档中的代码片段反映了 ComputeSharp.D2D1.WinUI.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 绘制效果。 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 上可用)。
  • 为了激活捆绑在 ComputeSharp 中的源生成器,[D2DGeneratedPixelShaderDescriptor] 属性是必需的。这个生成器会分析 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 方法,如果设置的值与当前值不同,该方法将自动使效果失效。 这可确保仅在真正必要的情况下再次配置效果。
  • 最后,我们有上述提到的 BuildEffectGraph 方法和 ConfigureEffectGraph 方法。

注意

PremultiplyEffect 干扰效果后的节点非常重要:这是因为 Win2D 效果假定输出是预乘的,而像素着色器通常使用非预乘像素。 因此,请记住在自定义着色器前后手动插入预乘/非多乘节点,以确保正确保留颜色。

准备绘制!

有了这个,我们的自定义霜化玻璃效果已经准备就绪! 我们可以轻松绘制它,如下所示:

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

    args.DrawingSession.DrawImage(effect);
}

在此示例中,我们从 CanvasControlDraw 处理程序中绘制效果,使用我们之前作为源加载的 CanvasBitmap。 这是我们将用于测试效果的输入图像:

云天下一些山的图片

下面是结果:

上图的模糊版本

注意

图片由 多米尼克·兰格 提供。

其他资源