什么是羽化?

图像羽化 是指将图像 边缘 进行 渐变式透明化 并融合背景当中,实现一种 平滑的融合效果 ,一般应用在 画中画 ,或者一 张图插入到另外一种图 当中。
在软件开发里也经常将 图像羽化用于UI开发中 ,这么做能够起到 美观作用
以WinUI 3的demo应用程序 Gallery 为例:

如何在WinUI3中使用羽化效果?

想要在WinUI3中实现羽化效果不能通过单纯的静态XML方式实现,需要静态与动态结合,使用代码方式实现,需要使用WinUI3较为底层的类: Visual ,Visual里有一个 Compositor 模块,Compositor是合成器模块,它是用来创建管理每个控件的视觉、动画、合成效果例如: 各种 Brush(画刷) 动画(KeyFrameAnimation) Effect(效果) Surface(绘图表面) 功能模块的工厂类,它可以从任何一个控件里获取到,然后生命周期由某个控件来管理,它属于较为底层的工厂模块,它必须使用某个控件的Compositor,它不能单独被创建,因为它的一些渲染之类的使用的是 Composition Engine 的组件,而该组件会使用 DirectX 来使用 GPU 完成绘制,每个控件都有一个 DirectX Composition Engine 组件来绘制自身Visual,所以Compositor一般是通过获取某个组件上的来实现具体功能,但它所提供的API不针对该组件,它只使用该组件的GPU相关的模块并且生命周期由该组件管理。
那么如何通过 Compositor 实现羽化效果呢?Compositor里有一个合成器模块: CompositionMaskBrush ,该模块用于实现不规则图形,它接受两个参数:背景、Mask,它会根据Mask的透明度信息,调整背景上面的每个像素的透明度,也就是蒙版融合。

实现一个羽化效果

首先原始效果是这个样子的:

<Gridx:Name="Root1"Width="400"Height="300"Margin="0, 100, 0, 40"><Imagex:Name="BgImg"Source="C:\\Users\\stephen\\Downloads\\test.jpg"Stretch="UniformToFill"/><Rectanglex:Name="MaskRect"><Rectangle.Fill><LinearGradientBrushStartPoint="0,0"EndPoint="0,1"><GradientStopColor="#FF000000"Offset="0.0"/><GradientStopColor="#00FFB4FF"Offset="1.0"/></LinearGradientBrush></Rectangle.Fill></Rectangle></Grid>

Grid控件可以让每个控件大小保持一致并且XY都为0,这样后续作为 Mark 的时候可以更好的对齐。
增加之后效果如下:

auto compositor =ElementCompositionPreview::GetElementVisual(Root1()).Compositor();

然后获取 Grid Visual ,这些目的是为了将融合后的图像直接替换原本的 Grid Vsual ,因为融合后的图像必须有个载体显示:

 Visual rootVisual =ElementCompositionPreview::GetElementVisual(Root1());

现在我们已经有了Grid的Compositor和Visual,接下来要再获取Image的Visual:

 Visual bgVisual =ElementCompositionPreview::GetElementVisual(BgImg());

Visual是每个控件较为底层的一个类 ,负责整个控件生命周期的 渲染 动画 一切可视化效果 ,获取到Image的Visual之后我们使用compositor来创建一个 CompositionVisualSurface ,CompositionVisualSurface是一个表面工具功能,它是用于获取某个控件的表面图像,表面图像就是控件可视区域显示的图像,与截图一样。

 CompositionVisualSurface bgSurface = compositor.CreateVisualSurface();
 bgSurface.SourceVisual(bgVisual);{auto expr = compositor.CreateExpressionAnimation(L"v.Size");
     expr.SetReferenceParameter(L"v", rootVisual);
     bgSurface.StartAnimation(L"SourceSize", expr);}

然后将 SourceVisual 设置为 Image Visual ,这样它就会实时获取Image的表面图像。

 bgSurface.SourceVisual(bgVisual);

注意调用它之后虽然会实时获取但是它获取时的size并不会改变,默认还是0x0,所以需要使用表达式动画,来让它能够实时根据控件大小变化而改变大小:

{auto expr = compositor.CreateExpressionAnimation(L"v.Size");
     expr.SetReferenceParameter(L"v", rootVisual);
     bgSurface.StartAnimation(L"SourceSize", expr);}

注意这里获取的是Grid的,这里解释一下为什么,在WinUI3里面有两种大小,一种是 Visual大小 ,一种是 实际大小 Visual大小是你图像显示出来时能够客观看到的大小 而实际大小是真正的宽高,控件的宽高大小 ,例如Image Visual大小是200x300,但实际大小是600x400,这个原因是因为你在布局控件里使用了它,并且使用 Height/Width 属性来控制控件大小(如果在布局控件里修改宽高属性仅修改 Visual 的,布局控件会强制改变它的实际宽高),但 布局控件会强制改变它的大小 ,例如 Grid ,如果在 Image 使用了 UniformToFill 拉伸了它,那么它的 Visual 大小也会改变,就会导致我们实际根据宽高设置时会发现跟我们预想的不一样,所以这里使用 Grid 的宽高来作为实际应用的宽高。
接下来如果想要在别的地方使用它,例如合成、绘制到别处就需要使用 Brush ,所以为它创建一个 Brush

CompositionSurfaceBrush bgBrush = compositor.CreateSurfaceBrush(bgSurface);

随后我们按照上面一样的步骤获取 Mask Surface

 Visual maskVisual =ElementCompositionPreview::GetElementVisual(MaskRect());
 CompositionVisualSurface maskSurface = compositor.CreateVisualSurface();
 maskSurface.SourceVisual(maskVisual);{auto expr = compositor.CreateExpressionAnimation(L"v.Size");
     expr.SetReferenceParameter(L"v", maskVisual);
     maskSurface.StartAnimation(L"SourceSize", expr);}
 CompositionSurfaceBrush maskBrush = compositor.CreateSurfaceBrush(maskSurface);

最后我们就可以使用CompositionMaskBrush来合成了,它接受两个参数一个是Source和Mask,Source就是背景,Mask就是蒙版,会根据Mask上面的透明度信息来调整Source的像素透明度:

 CompositionMaskBrush maskEffect = compositor.CreateMaskBrush();
 maskEffect.Source(bgBrush);
 maskEffect.Mask(maskBrush);

Tips
需要值得注意的是MaskBrush在融合的时候Mask的坐标从左上(0, 0)开始,并且它使用的是实际大小作为

最后图像已经融合好了,那么就要将它设置到Grid表面去了,接下来就要使用SpriteVisual了,SpriteVisual是每个控件元素里的画布,也就是实际显示内容的模块,在WinUI3里面每个控件都以树形结构表示,控件里有许多子节点,其中SpriteVisual就是用来控制要显示的内容的。

SpriteVisual sprite = compositor.CreateSpriteVisual();
sprite.RelativeSizeAdjustment(float2{1.f,1.f});
sprite.Brush(maskEffect);

RelativeSizeAdjustment 是用来设置显示时的缩放程度,0~1是Float类型,通过Brush给它设置一个新的画刷。
最后通过SetElementChildVisual来给控件增加一个渲染子节点SpriteVisual,由于Grid的Visual根节点本身就是透明的什么都没有,所以新增的子节点不会被Grid本身的渲染内容所遮挡:

ElementCompositionPreview::SetElementChildVisual(Root1(), sprite);

改变之后Grid可视区域就变成了刚刚融合的图像,由于Grid内部有控件会遮挡所以这里需要将这两个控件隐藏掉:

BgImg().Opacity(0);MaskRect().Opacity(0);

最终运行效果: