UWP/WinUI3 Win2d PixelShaderEffect 实现ThresholdEffect 滤镜。
在上一遍文章中已经介绍了PixelShaderEffect 用hlsl(着色器) 可以实现各种自定义滤镜效果了,本文将用 "ThresholdEffect" 来讲解如何编写,编译hlsl,然后使用PixelShaderEffect制作自定义滤镜。
效果图:
一.hlsl帮助程序介绍
在写hlsl 代码前需要简单介绍下 “hlsl帮助程序”.通过学习了 hlsl帮助程序 后我们不需要将hlsl的所有知识都掌握了就可以写一写简单的hlsl代码了。hlsl帮助程序分为两部分,宏定义和函数。
1.宏定义
D2D_INPUT_COUNT N | 纹理输入个数。必须定义 |
D2D_INPUTn_SIMPLE | 指定第n个纹理的为简单采样,默认为此定义,可选的 |
D2D_INPUTn_COMPLEX | 指定第n个纹理为复杂采样.可选的 |
D2D_REQUIRES_SCENE_POSITION | 指示着色器函数调用使用场景位置的值的帮助方法(即使用D2DGetScenePosition函数,必须要有该定义) |
D2D_CUSTOM_ENTRY | 着色器程序入口 |
#include "d2d1effecthelpers.hlsli" | 引入 hlsl帮助程序 |
2.函数
D2DGetInput(n) | 获取第n张纹理的当前位置的像素,返回float4,即是rgba颜色。 |
D2DSampleInput(n,float2(x,y)) | (按百分比位置进行采样)获取第n张纹理指定位置(xy按照百分比0~1)的像素颜色,返回float4,即rgba颜色。(需要定义纹理为复杂采样才能使用该函数) |
D2DSampleInputAtOffset(n,float2(ox,oy)) | (按绝对位置进行偏移采样)从输入坐标偏移的偏移量对第n张纹理进行采样(需要定义纹理为复杂采样)。例子:比如需要获取当前像素的左边像素可以使用该函数 D2DSampleInputAtOffset(0,float(-1,0))来获取左边像素的颜色; |
D2DSampleInputAtPosition(n,float2(x,y)) | (按绝对位置进行采样) 例子:比如输入的纹理图像大小的宽和高都为100,现在需要获取该纹理位置 50,50位置的像素可以使用该函数D2DSampleInputAtPosition(0,float2(50,50));(需要定义纹理为复杂采样) |
D2DGetInputCoordinate(n) | 获取当前像素在屏幕上的坐标(相对位置0~1)(需要定义纹理为复杂采样) |
D2DGetScenePosition() | 获取当前像素在屏幕上的坐标(绝对位置)(需要定义 D2D_REQUIRES_SCENE_POSITION宏) |
D2D_PS_ENTRY 函数 |
一个宏,用于定义具有给定函数名称的像素着色器入口点。 |
hlsl帮助程序文档:HLSL 帮助程序 - Win32 apps | Microsoft Docs
hlsl文档:高级着色器语言 (HLSL) - Win32 apps | Microsoft Docs
二.编写hlsl代码
在项目程序中右键-》添加新建项-》常规-》文本文件 并将文件名改成ThresholdEffect.hlsl
在代码中用宏定义了 输入一张纹理,并且将纹理的采样模式定义为简单模式,通过#include 引入hlsl帮助程序。
然后在声明了 三个变量 hreshold,startColor,endColor;
声明一个 getGray(in float3 color) 函数将颜色的各个分量相加再除以3 并且返回;
最后定义了一个 D2D_PS_ENTRY 函数,该函数是着色器程序的入口函数,在函数里面调用了 D2DGetInput(0)函数获取第0张纹理的当前像素,然后通过调用getGray 函数将像素的颜色转换成灰度值后跟 threshold 比较再决定返回那个颜色。
注意:函数的声明需要写在调用前,否则编译的时候会找不到函数而报错;
//定义输入纹理数量为 1张 #define D2D_INPUT_COUNT 1 //定义第一张纹理的采样模式为简单模式 #define D2D_INPUT0_SIMPLE //引入 hlsl帮助程序 #include "d2d1effecthelpers.hlsli" //定义属性 //阈值 float threshold; //默认第一个颜色为白色 float4 startColor = float4(1,1,1,1); //默认第二个颜色为黑色 float4 endColor = float4(0, 0, 0, 1); //将输入颜色转换成灰度 (r+g+b)/3 float getGray(in float3 color) { float gray = (color.r + color.g + color.b) / 3; return gray; } //程序入口 D2D_PS_ENTRY(main){ //获取当前位置的输入纹理像素颜色 float3 color = D2DGetInput(0).rgb; float gray = getGray(color); if(gray<=threshold){ return endColor; } return startColor; }
三.编译hlsl
现在我们来将上面写好的hlsl 编译成二进制文件。
1.记事本方式打开我们报错好的 ThresholdEffect.hlsl 文件查看它的编码格式是否是 UTF-8 ,如果不是则需要将编码改成UTF-8后保存。如果编码不对的情况下会在编译时报错说”找不到任何代码“.
2.在vs工具栏中找到 工具-》命令行-》开发者提示 然后将位置切换到存放 ThresholdEffect.hlsl 文件的目录下,
然后输入以下命令进行编译,如果编译成功就会在存放目录下看到会多了一个ThresholdEffect.bin的文件,这个文件就是将hlsl编译好的二进制文件了:
set INCLUDEPATH="%WindowsSdkDir%\Include\%WindowsSDKVersion%\um" fxc ThresholdEffect.hlsl /nologo /T lib_4_0_level_9_3_ps_only /D D2D_FUNCTION /D D2D_ENTRY=main /Fl ThresholdEffect.fxlib /I %INCLUDEPATH% fxc ThresholdEffect.hlsl /nologo /T ps_4_0_level_9_3 /D D2D_FULL_SHADER /D D2D_ENTRY=main /E main /setprivate ThresholdEffect.fxlib /Fo:ThresholdEffect.bin /I %INCLUDEPATH% del ThresholdEffect.fxlib
编译命令就不在这里展开讲了感兴趣的可以 看文档:像素阴影效果构造函数 (microsoft.github.io) 里面有介绍;
编译成功
四.导入编译好的bin文件,然后右键文件->属性,将复制到输出目录改为 始终复制;
五.xaml界面
在界面中放置了一个 CanvasControl 控件用于显示绘制内容,在下面添加选择图片按钮,更改阈值的slider,和两个颜色选择器;
<Grid> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition Height="auto"></RowDefinition> </Grid.RowDefinitions> <canvas:CanvasControl x:Name="canvas"></canvas:CanvasControl> <StackPanel Grid.Row="1"> <Button Content="选择图片" x:Name="selectPicture"></Button> <Slider Header="阈值:" Value="0" Maximum="1" StepFrequency="0.01" x:Name="threshold"></Slider> <StackPanel Orientation="Horizontal"> <Button Content="颜色一" > <Button.Flyout> <Flyout> <ColorPicker Color="White" x:Name="cp1"></ColorPicker> </Flyout> </Button.Flyout> </Button> <Rectangle Width="50" Fill="{Binding ElementName=cp1,Path=Color,Mode=TwoWay,Converter={StaticResource colorToBrush}}"></Rectangle> </StackPanel> <StackPanel Orientation="Horizontal"> <Button Content="颜色二" > <Button.Flyout> <Flyout> <ColorPicker Color="Black" x:Name="cp2"></ColorPicker> </Flyout> </Button.Flyout> </Button> <Rectangle Width="50" Fill="{Binding ElementName=cp2,Path=Color,Mode=TwoWay,Converter={StaticResource colorToBrush}}"></Rectangle> </StackPanel> </StackPanel> </Grid>
六.后台代码
1.在 canvas的CreateResource 事件中读取bin文件中的字节数组并创建一个 PixelShaderEffect 对象;
2.添加选择图片事件用于选择一张输入的源图;
3.绘制图像,effect 对象通过 Properties 键值对的形式对着色器里面的属性进行赋值。这里需要注意 着色器里面的颜色范围是 0~1 并且是float类型,所以需要将c#里面的颜色的每个分量都除以255,转换成Vector4结构;
4.监听Slider 和颜色选择器 的更改并重新绘制显示图像;
public sealed partial class ThresholdEffectPage : Page { PixelShaderEffect effect; CanvasBitmap bitmap; public ThresholdEffectPage() { this.InitializeComponent(); Init(); } void Init() { canvas.CreateResources += async (s, e) => { //获取着色器二进制文件 StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Shaders/ThresholdEffect.bin")); IBuffer buffer = await FileIO.ReadBufferAsync(file); //转换成字节数组 var bytes = buffer.ToArray(); //用 字节数组 初始化一个 PixelShaderEffect 对象; effect = new PixelShaderEffect(bytes); }; //选择图片 selectPicture.Click += async (s, e) => { var file = await Ulit.SelectFileAsync(new List<string> { ".png", ".jpg" }); if (file == null) bitmap = null; else bitmap = await CanvasBitmap.LoadAsync(canvas.Device, await file.OpenAsync(FileAccessMode.Read)); canvas.Invalidate(); }; canvas.Draw += (s, e) => { //绘制黑白网格 Win2dUlit.DrawGridGraphics(e.DrawingSession, 100); //判断effect 和位图是否为空 if (effect == null || bitmap == null) return; var element = (FrameworkElement)s; float effectWidth = (float)element.ActualWidth; float effectHeight = (float)element.ActualHeight * 0.7f; float previewWidth = effectWidth; float previewHeight = (float)element.ActualHeight * 0.3f; //绘制原图 var previewTran = Win2dUlit.CalcutateImageCenteredTransform(previewWidth, previewHeight, bitmap.Size.Width, bitmap.Size.Height); previewTran.Source = bitmap; e.DrawingSession.DrawImage(previewTran, 0, effectHeight); //颜色将0~255 转换成0~1 因为hlsl里面的颜色是0~1范围的并且是float类型,所以这里需要将每个颜色的分量除以255 Vector4 startColor = new Vector4(cp1.Color.R / 255f, cp1.Color.G / 255f, cp1.Color.B / 255f, 1f); Vector4 endColor = new Vector4(cp2.Color.R / 255f, cp2.Color.G / 255f, cp2.Color.B / 255f, 1f); //通过键值对的形式设置属性传递到着色器里面 effect.Properties["threshold"] = (float)threshold.Value; effect.Properties["startColor"] = startColor; effect.Properties["endColor"] = endColor; effect.Source1 = bitmap; //绘制效果图 var effectTran = Win2dUlit.CalcutateImageCenteredTransform(effectWidth, effectHeight, bitmap.Size.Width, bitmap.Size.Height); effectTran.Source = effect; e.DrawingSession.DrawImage(effectTran); }; threshold.ValueChanged += (s, e) => canvas.Invalidate(); //这里遇到问题了 说找不到 WinRT.ObjectReferenceValue ABI.Windows.Foundation.TypedEventHandler`2.CreateMarshaler2(Windows.Foundation.TypedEventHandler`2<!0,!1>)'。” //cp1.ColorChanged += (s, e) => //{ // //if (canvas.IsLoaded) // // canvas.Invalidate(); //}; //cp2.ColorChanged += (s, e) => //{ // //if (canvas.IsLoaded) // // canvas.Invalidate(); //}; } }
结语:
现在已经将PixelShaderEffect 的整个使用过程都讲解完了。下一篇讲解 "ReplaceColorEffect";