开始使用 WPF 中的着色器效果
您需要了解的所有关于 Windows Presentation Foundation 中的 GPU 加速效果
引言
WPF 的硬件加速效果最早在 .NET 3.5 SP1 中引入。得益于现代图形卡的巨大计算能力,可以创建非常复杂的效果和图形丰富的应用程序,而对性能影响甚微。但是,如果您想利用此功能,首先需要了解一些知识。本文旨在提供您开始使用效果所需的所有信息。
什么是效果?
效果是一个易于使用的 API,用于创建(令人惊讶的)图形效果。例如,如果您希望按钮投射阴影,有几种方法可以完成此任务,但最简单有效的方法是将按钮的“Effect
”属性设置为代码或 XAML 中的值。
MyButton.Effect = new DropShadowEffect() { ... };
<Button Name="MyButton" ... >
<Button.Effect>
<DropShadowEffect ... />
</Button.Effect>
</Button>
如您所见,效果非常易于使用,您不需要任何进一步的解释。当您决定编写自己的效果时,乐趣就开始了...
BitmapEffect、Effect、ShaderEffect... 什么?
首先,有几个 .NET 类共享“Effect
”后缀,为了让事情更具混淆性,它们都位于 System.Windows.Media.Effects 命名空间中。然而,并非所有这些类在硬件加速方面都很有用,实际上其中一些完全没用。
BitmapEffect
该 BitmapEffect 类及其子类最初旨在提供效果的功能。然而,此 API 不使用任何硬件加速,并且在 .NET 4.0 中已被标记为过时。强烈建议避免使用 BitmapEffect 类或其任何子类!
Effect 及其派生类
如上所述,您可以通过设置控件的 Effect 属性来将效果应用于控件(该属性实际上是从 UIElement 继承的,如果您需要知道的话)。现在的问题是……Effect
属性需要设置什么?答案很简单——它是一个 Effect
类型的对象。
该 Effect 类是所有硬件加速效果的基类。它有三个子类:BlurEffect
、DropShadowEffect
和 ShaderEffect
。前两个是 .NET 库中直接包含的即用型效果。ShaderEffect 类
是所有自定义效果的基类。
为什么是 BlurEffect 和 DropShadowEffect?
为什么库中只有 2 种完全实现的效果,为什么这 2 种效果不派生自 ShaderEffect
?我无法回答第一个问题,但我可以告诉您是什么让 BlurEffect
和 DropShadowEffect
如此特别。
DropShadowEffect
和 BlurEffect
都使用需要多次传递的复杂算法,但通常不可能实现多次传递效果。然而,微软的人员可能在 WPF 渲染引擎的非托管核心深处做了一些肮脏的技巧,创造了这两种效果。
注意:有可能创建一个单次模糊算法,但这种算法与多次模糊相比非常慢。总之,这两种效果以特殊方式实现可能还有更多原因。
它是如何工作的?
如果您想利用硬件加速,首先需要了解整个工作原理。
关于 GPU 架构的几句话
图形处理单元 (GPU) 的架构与 CPU 的架构不同。GPU 不是通用目的的,它们被设计用于对大型数据集执行简单操作。操作以高度并行的方式执行,从而产生出色的性能。
现代 GPU 变得越来越可编程,并且可以在 GPU 上执行的任务范围正在扩大(尽管存在下面描述的几项限制)。在 GPU 上执行的小程序称为着色器。有几种类型的着色器——顶点着色器和几何着色器用于渲染 3D 对象(WPF 效果不使用),而像素着色器用于对像素执行简单操作。
甚至还有尝试利用 GPU 的计算能力进行通用编程……不幸的是,存在几项限制,例如单个程序中的指令数量有限、无法处理高级数据结构、内存管理能力有限等。惊人的速度伴随着一些权衡……
像素着色器
像素着色器是一个简短的程序,它定义了在输出图像的每个像素上执行的简单操作。这就是创建各种有趣的基于像素的效果所需的大部分内容。
在编写您的第一个效果之前...
WPF 对象,包括Effects
,都是使用 DirectX 引擎渲染的。DirectX 着色器是用 高级着色器语言 (HLSL) 编写的,然后编译成字节码。因此,HLSL 是您需要学习以编写自己的效果之一(有关 HLSL 的更多信息,请参阅本文)。
有些人会告诉您需要下载并安装整个 DirectX SDK 才能编译 HLSL 代码。幸运的是,事实并非如此。您需要下载 Greg Schechter 和 Gerhard Schneider 编写的 Visual Studio 插件。它被称为 Shader Effects BuildTask,您可以从 CodePlex WPF 网站获取。据报道,此插件适用于 Visual Studio 2008 和 2010。
安装插件后,Visual Studio 中将出现一个名为“WPF Shader Effect Library”的新项目模板。此插件最好的地方在于您可以直接在 Visual Studio 中编写 HLSL 代码(尽管没有智能感知支持和语法高亮显示),并且在构建项目时,所有着色器都会自动编译。
第一个简单效果
我们开始吧!如果您已经下载并安装了上面提到的 Shader Effects BuildTask,您可以打开本文附带的项目。
每个效果有 2 部分:一个用 HLSL 语言编写的像素着色器(扩展名为 .fx 的文件)和一个派生自 ShaderEffect
的类(.cs 文件),它充当像素着色器的托管包装器。构建项目时,所有 .fx 文件都会被编译,生成的像素着色器(扩展名为 .ps)将被包含在程序集中。
如果选择一个 .fx 文件并打开“属性”窗口,您会看到该文件的“生成任务”设置为“Effect”(请参阅右侧的图像)。这确保了效果将被正确编译。重要提示:向项目中添加新效果时,其生成任务属性不会自动设置,您需要手动更改它!
我将描述的效果很简单——它被称为“Transparency
”,并有一个参数“Opacity
”。它根据此参数使控件半透明。请忽略这样一个效果完全无用的事实……
创建像素着色器
让我们从难的部分开始:首先是像素着色器。“Transparency.fx”包含以下代码
sampler2D implicitInputSampler : register(S0);
float opacity : register(C0);
float4 main(float2 uv : TEXCOORD) : COLOR {
float4 color = tex2D(implicitInputSampler, uv);
return color * opacity;
}
前两行包含像素着色器常量。第一个常量是 sampler2D
类型,它引用应用了此效果的图像。是的,我知道效果是应用于控件(不一定是 Image
),但“图像”一词指的是目标控件的视觉表示……
另一个常量是我们的自定义输入参数(称为“opacity
”),它是 float
类型。尽管此参数的值可以随时间变化,但在像素着色器的范围内,它被视为常量。如上所述,像素着色器对每个像素执行一次,并且一帧中的所有像素需要相同的输入参数——这就是为什么“opacity
”被视为常量。
register
关键字用于将每个常量与存储输入值的寄存器关联起来。有几个“图像寄存器”包含输入图像数据,这些寄存器命名为 S0
、S1
、S2
等(大多数像素着色器只使用一个这样的寄存器)。还有“浮点寄存器”命名为 C0
、C1
、C2
等,这些寄存器存储其他输入参数的值。
着色器的其余部分是算法本身。有一个名为 main
的方法,这是我们着色器程序的入口点。此方法接受一个 float2
类型的参数并返回 float4
(具有此名称和签名的函数必须在每个像素着色器中)。此方法的返回类型是一个由 4 个浮点值组成的向量,表示 RGBA 颜色。方法参数是一个二维向量,您可以将其视为“当前像素的 x 和 y 坐标”。实际上,这些值不是基于像素的:左上角的坐标是 (0, 0),右下角由 (1, 1) 表示。
该方法的主体非常简单——通过调用 tex2D
方法找到源颜色,将其乘以透明度并返回。当“*”运算符用于将标量值与向量相乘时,该向量的所有分量都乘以标量。您可能认为只有 alpha 通道应该乘以得到正确结果。然而,DirectX 着色器使用预乘 alpha 通道,这意味着 RGB 通道的值始终乘以 alpha 通道。
Effect 类
让我们看一下“Transparency
”效果的另一部分。“Transparency.cs”包含以下类
public class Transparency : ShaderEffect {
static Transparency() {
// Associate _pixelShader with our compiled pixel shader
_pixelShader.UriSource = Global.MakePackUri("Transparency.ps");
}
private static PixelShader _pixelShader = new PixelShader();
public Transparency() {
this.PixelShader = _pixelShader;
UpdateShaderValue(InputProperty);
UpdateShaderValue(OpacityProperty);
}
public Brush Input {
get { return (Brush)GetValue(InputProperty); }
set { SetValue(InputProperty, value); }
}
public static readonly DependencyProperty InputProperty =
ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(Transparency), 0);
public double Opacity {
get { return (double)GetValue(OpacityProperty); }
set { SetValue(OpacityProperty, value); }
}
public static readonly DependencyProperty OpacityProperty =
DependencyProperty.Register("Opacity", typeof(double), typeof(Transparency),
new UIPropertyMetadata(1.0d, PixelShaderConstantCallback(0)));
}
正如您所见,这基本上是一个普通的类,带有几个依赖属性。但是,有几件重要的事情我必须指出。
像素着色器存储在 private static
字段 _pixelShader
中。此字段是 static
的,因为该类的已编译着色器代码的一个实例就足够了。有一个 static
构造函数用于初始化 _pixelShader
的 UriSource
属性——这基本上告诉 _pixelShader
在哪里查找已编译的着色器字节码。Global.MakePackUri()
方法是一个辅助方法,它将文件名转换为完整的 uri 路径,该路径大致如下:"pack://application:,,,/[assemblyname];component/Transparency.ps"
。(我相信您明白为什么需要一个辅助方法)。
必须有一个名为“Input
”的 Brush
类型属性。此属性包含输入图像,通常不直接设置——它在我们效果应用于控件时自动设置。相应的依赖属性不是通过调用 DependencyProperty.Register()
来初始化的,而是必须使用 ShaderEffect.RegisterPixelShaderSamplerProperty()
方法。注意此方法的最后一个参数:它是一个整数,对应于 S0
像素着色器寄存器。
另一个属性是我们自定义参数“Opacity
”。它像任何其他依赖属性一样声明,唯一区别在于 UIPropertyMetadata
构造函数中的 PropertyChangedCallback
的值。该值必须是 PixelShaderConstantCallback()
,并且整数参数必须是对应浮点寄存器的编号(请注意,值“0
”对应于寄存器名称 C0
)。
最后一件需要解释的重要事情是我们类的构造函数。它设置 PixelShader
属性(这是必需的),并强制着色器更新所有输入值。
关于 HLSL 的几点说明
如您所见,HLSL 语言是一种简单的 C 语言。可以使用常见的 C 运算符(+、-、*、/ 等)以及 许多数学函数。还可以使用代码流控制语句(如 if
、while
或 for
),完整的列表可以在 此处找到。
在 WPF 像素着色器中最常用的类型是 float
和基于 float
的向量(float2
、float3
和 float4
)。有关 HLSL 向量的详细说明(以及如何使用它们)可以在 此处找到。在当前版本的 WPF 像素着色器中没有 int
或 bool
类型(请参见下表)。
接受的参数类型
下表显示了所有允许的输入类型(在 ShaderEffect
类中定义)和相应的 HLSL 类型(在像素着色器中定义)。目前只允许浮点值。
.NET 类型 | HLSL 类型 |
System.Boolean (C# 关键字 bool ) |
不可用 |
System.Int32 (C# 关键字 int ) |
不可用 |
System.Double (C# 关键字 double ) |
float |
System.Single (C# 关键字 float ) |
float |
System.Windows.Size |
float2 |
System.Windows.Point |
float2 |
System.Windows.Vector |
float2 |
System.Windows.Media.Media3D.Point3D |
float3 |
System.Windows.Media.Media3D.Vector3D |
float3 |
System.Windows.Media.Media3D.Point4D |
float4 |
System.Windows.Media.Color |
float4 |
更进一步
如果您已阅读以上所有内容,您就了解了效果的基础知识。在您开始创建自己的效果之前,这里有一些您可能想知道的重要事项:
动画
效果是基于 DependencyProperty
的,并且可以像任何其他 WPF 元素一样进行动画处理。
位移
效果的功能远不止改变像素的颜色。请参见以下示例
float4 main(float2 uv : TEXCOORD) : COLOR {
uv = uv / 2;
float4 color = tex2D(implicitInputSampler, uv);
return color;
}
在获取源颜色之前,将 uv
值除以 2,这将把目标控件的左上角四分之一拉伸到控件的整个区域。可以使用更复杂的变换来创建有趣的效果。
您甚至可以创建一个能够正确响应用户输入(如鼠标悬停等)的位移效果——您只需要设置效果类的 Transform
属性即可。
多输入效果
效果可以有几个输入图像来执行高级混合操作。这超出了本文的范围,但您可以在 Greg Schechter 的博客上找到对此技术的详细说明。
常见错误
从 Visual Studio 编译像素着色器的方式会引入几个潜在的错误。这些错误不会导致任何编译时错误,并且有时很难找到。
- 首先,在库中添加新的 Shader Effect 时,请勿忘记将新的 .fx 文件的“生成任务”设置为“
Effect
”。如果您忘记这样做,您的着色器将不会被编译,并且您的应用程序在尝试使用该效果时将崩溃。 - 另一个可能的问题是编译效果如何包含在托管程序集中。如果您更改项目的项目结构,或者重命名效果源文件,请记住在关联的
PixelShader
构造函数中更改文件路径,否则当您的应用程序尝试创建效果实例时将崩溃。 - 如果向效果添加了新参数,请不要忘记在效果类的构造函数中添加一个新的
UpdateShaderValue
方法调用。否则,您的效果可能会使用错误的默认值。 - 定义效果参数的默认值时要小心,因为默认值类型必须与参数类型完全匹配。如果一个属性是
double
类型,您不能简单地使用整数文字(例如“1
”)作为其默认值,您必须使用双精度值,如“1.0
”或“1d
”。
推荐资源
Greg Schechter 撰写了一系列出色的文章,内容涉及 GPU 驱动的效果,到目前为止,这是我找到的最佳资源。该系列包括几个示例,解释了如何创建多输入效果等等。
Walt Ritscher 创建了一个很棒的工具,名为 Shazzam,这是一个用于着色器效果的交互式开发工具。Shazzam 允许您编写一个效果,将其应用于任何图像并交互式地更改所有输入参数。它甚至会为您生成相关的 C#/VB 代码。请访问 Shazzam 官方页面了解更多信息。(非常感谢Sacha Barber将 Shazzam 推荐给我。)
Nick Darnell 编写了 WPF ShaderEffect Generator,这是一个 Visual Studio 插件,允许您直接在 VS 中编写和编译着色器,是 Greg Schechter 和 Gerhard Schneider 的 BuildTask 的绝佳替代品。主要区别在于 Nick Darnell 的插件从完成的 HLSL 代码自动生成所有 C# 类(功能与 Shazzam 非常相似)。感谢U-P-G-R-A-Y-E-D-D发布此项目的链接!
Tamir Khason 创建了一个名为 HLSL Tester 的小型程序。如果您是 HLSL 语言的初学者,这个应用程序将非常有帮助。它允许您加载一个位图图像,然后编写简单的像素着色器,进行调试并交互式地将它们应用于图像。唯一的缺点是此应用程序需要安装 DirectX SDK。
WPF Pixel Shader Effects Library 是一个高质量的即用型效果的开源库。该库(包括源代码)可以从 CodePlex 下载:http://wpffx.codeplex.com/
示例项目
Transparency 效果以及一个简单的 WPF 测试应用程序可以 此处下载。
结束
就是这样。我希望本文为您提供了创建自己的出色硬件加速效果所需的一切。我建议您关注上面“推荐资源”部分中提到的链接。
我很乐意回答您所有的问题,并非常感谢您的反馈。
版本历史
- 编辑于 2010-07-24:向推荐资源部分添加了链接