WPF 中的着色器效果 - 基础知识





5.00/5 (5投票s)
创建 HLSL 文件,编译它们并在 WPF 应用程序中使用它们。Visual Studio 的轻量级 Shazzam 着色器编辑器工具
引言
如果您以前在 WPF 中使用过效果,那么您也(希望)无意中使用了 WPF 中的 GPU 加速功能。这些效果被称为 ShaderEffects,是在 .NET3.5SP1 发布时引入的(旧的 BitmapEffects 类从 .NET 4.0 开始已过时,所以不要使用它们)。在此之前,效果是在 CPU 中完成的,这在计算机密集型效果(如模糊)上会导致严重的性能问题。现在 WPF 提供了在 GPU 中完成所有计算的效果,因此视觉效果不会影响您的 CPU 性能。缺点是 WPF 在 Media.Effects 类中只提供了两种效果(旧的 BitmapEffects 已过时,并且绝不应该使用)
- 模糊效果
- 阴影效果
WPF 还提供了一些非常有趣的东西,即 ShaderEffect 类,这是您可以用来与自定义创建的 ShaderEffects 通信的类。这些效果文件 *.ps 是由 DirectX 编译器从 *.fx (HLSL) 文件生成的。本文将介绍这些步骤
- 编写您自己的 HLSL (.fx) 文件
- 如何将文件编译成 .ps(像素着色器字节码)
- 在 WPF 中设置一个 VB.NET 或 C# 类来与 .ps 文件通信
- 将效果应用于任何 WPF UIElement
如果您通读本文,我希望您将获得创建自己的自定义像素着色器文件并在自己的 WPF 应用程序中使用的能力。我应该提到,互联网上的大多数着色器文件都使用 Silverlight 语言中的着色器,但本文将只关注 WPF 的用法,但会包含最初使用 Silverlight 和着色器编写的程序中的材料。
本文严重依赖于从阅读 Walt Ritscher 的 Shazzam 工具源代码中学到的知识,并以他的代码为基础,我做了一些更改和改进,因为他的工具对我来说无法正常运行。自该工具首次发布以来,也发生了一些变化,尤其是在 Windows 操作系统上,因为 DirectX 编译器和 DLL 现在已默认包含在内。
如果您购买了 Visual Studio 2013 或更高版本,则可以使用内置的 图形调试器。本文主要为 Visual Studio Express 用户编写,允许用户使用 Visual Studio 编写 fx 文件时突出显示 HLSL,尽管您可能会从中获得一些关于像素着色器用法的提示和技巧。
生成 HLSL 文件
fx 文件或 HLSL(高级着色语言的缩写)文件,正如它们也常被称为的那样。这些文件使用类似 C 的语言进行编程,可以使用的函数数量非常有限。这使得它学习起来相当快,使用起来也容易,但它确实有一些古怪的细节。
WPF 中 ShaderEffect 类使用的 fx 文件总是以一系列存储在 GPU 中的注册值开头,您可以使用 ShaderEffect 类与这些值 通信(但是,如果您在 C++ 中使用 lib 文件,则可以访问所有这些值)。您现在可以看到可用输入的数量相当有限,并且可以使用多少个输入也有限制。
https://msdn.microsoft.com/en-us/library/hh315751(v=vs.110).aspx
在 WPF 中使用的像素着色器中,下面的这行代码必须包含在 fx 文件中。它将图像本身存储在 GPU 寄存器中,以及可以访问它的指针。
sampler2D Input : register(S0);
这行代码实际上指定了 sampler2D 值 Input 应该存储在 GPU 寄存器中。它还指定了它所属的寄存器类型 (S),但还有更多 可用 的类型,但用 C# 和 VB 编写的 WPF 应用程序只能与 S 和 C 寄存器值通信。
在像素着色器中,您一次处理一个像素,在主函数调用(ps 文件的入口点)中,您将获得当前像素的 x 和 y 坐标作为输入。好吧,这不太准确,您确实获得了坐标,但它们介于 0 到 1 之间(0,0 是左上角),这就是为什么它是一个 float2 变量。
float4 main(float2 uv : TEXCOORD) : COLOR
{
...
}
您通过使用 tex2 函数获得给定像素的 4 个颜色值,并且颜色存储在一个包含 float(再次使用 0 到 1 的值,而不是正常的 0 到 255 的字节)精度的 4 项向量中,使用 float4 显式字段
float4 color = tex2D( Input, uv );
比如说,为了反转图像的颜色,我们只需添加一行
float4 inverted_color = 1 - color ;
为了避免 alpha(透明度因子)被改变(获取该值有更多方法),并返回反转的颜色:
inverted_color.a = 1;
return inverted_color;
这些知识让您能够创建所有一次由一个像素计算的效果。但是大多数有趣的图像处理都涉及获取相邻像素。像高斯滤镜、Sobel 滤镜等效果都需要对几个相邻像素进行计算,因此我们需要这个函数(边缘检测器使用导数的版本,它实际上在 GPU 中作为 函数 可用)
float4 GetNeighborPixel(float2 uv, float2 pixelOffset)
{
float x = pixelOffset.x / InputSize.x;
float y = pixelOffset.y / InputSize.y;
float2 NeighborPoint = { uv.x + x, uv.y + y };
return tex2D(input, NeighborPoint);
};
fx 文件中的函数有一个特别之处,那就是;每个函数都必须在使用前声明
float TestFunc(float n)
{
// ...
}
float main()
{
// ...
}
或在使用前声明一个指针
float TestFunc(float);
float main()
{
// ...
}
float TestFunc(float n)
{
// ...
}
假设一切顺利,并且您已正确定义了函数,您应该意识到您需要图片的实际大小。由于所有坐标都采用 0 到 1 的双精度格式,因此像素步长采用双精度格式。
/// <summary>Explain the purpose of this variable.</summary>
/// <minValue>1, 1</minValue>
/// <maxValue>1024, 1024</maxValue>
/// <defaultValue>599, 124</defaultValue>
float2 InputSize : register(C0);
实际上,您现在应该能够编写任何您想要的像素着色器,只需浏览 文档 中可用的函数即可。以下两个链接中可以看到 Tamir Khason 提供的几个很酷的示例,它们非常直接易懂
- http://blogs.microsoft.co.il/tamir/2008/05/23/brightness-and-contrast-manipulation-in-wpf-35-sp1/
- http://blogs.microsoft.co.il/tamir/2008/06/17/hlsl-pixel-shader-effects-tutorial/
还有一些用于创建炫酷效果的旧版工具,可能值得一看
对于更高级的着色器,您甚至可能希望使用函数和类结构来有效地编程。虽然所使用的函数对于复杂效果非常有用,但您可能需要使用比标准版本 2.0 或 3.0 更高的版本来编译着色器。
编译 HLSL 文件
在我开始解释如何编译 fx 文件之前,我只想说这只在您拥有 Express 版本 的 Visual Studio(2012、2013 或 2015)时才真正需要,在您购买的版本中,您可以获得编译器作为资源。在 VS 2010 中,还有一个工具可以使用 codeplex 插件将 fx 文件编译为资源。但是,我认为阅读本节仍然有用。我还应该提到,在 VS 2010 和 2008 中,还有另一种编译 fx 文件的方法,即安装一个 Visual Studio 插件。
使用 fxc 进行编译
在以前的 Windows 版本中,您需要安装 DirectX SDK 才能获得编译 fx 文件为机器代码 ps 所需的 fxc.exe 文件。从 Windows 8 开始,SDK 作为标准部署,为您节省了数百 MB 的下载量(尽管 fxc 文件,您真正需要的唯一文件,只有大约 250KB)。
为了编译 fx 文件,大多数人似乎都使用 VS 2013 中的构建事件进行快速编译。在 post build 命令中(在 Windows 8.1 上),编译将如下所示
"C:\Program Files\Windows Kits\8.1\bin\x86\fxc.exe" /T ps_2_0 /E main /Fo"Watercolor.ps" "Watercolor.fx"
但事实证明,构建事件在 VB.NET 中不可用!好吧,那不必是个问题,我只需在 Application.XAML 文件中构建它。但是,您必须记住在您的应用程序中实现命名空间才能访问其中的类
Imports WpfAdventuresInPixelShader
Class Application
' Application-level events, such as Startup, Exit, and DispatcherUnhandledException
' can be handled in this file.
Public Sub New()
' My code to build the fx files goes here...
End Sub
End Class
这个障碍让我比我原以为的更早地深入研究了 fx 文件的编译细节,我很快发现了一个关键问题。如果您在 VS(任何年份)中打开 fx 文件,它默认将文本存储为 UTF-8。有趣的是(或者不那么有趣,取决于个人视角),如果它不是记事本 ANSI 代码或 ASCII 代码,它就不会编译。这实际上非常烦人,所以我决定打开文件并使用 ASCII 编码存储它
' Making sure the fx files are stored in an encoding
' that fxc understands. (It does not understand the default Visual studio encoding!)
For Each file As String In directoryEntries
'Open the .fx file
Dim OpenFxFileInTextFormat() As String = IO.File.ReadAllLines(file)
'Makeign sure its stored in a format that the fxc compiler understands
IO.File.WriteAllLines(file, OpenFxFileInTextFormat, Text.Encoding.ASCII)
Next
这个小代码片段使我能够直接在 VS 2013 中编写和编辑文件,并且仍然使用小的 fxc.exe 编译器(还有其他方法可以通过导入 DLL 文件在代码中编译,稍后会详细介绍)。还有一件事您可以做。如果您运行此代码,VS 2013 编译器将检测文件中的更改,并询问您是否要重新加载它。因此,为了省去一些麻烦,在顶部菜单中选择
工具->选项...
将出现一个对话框,您将导航到
环境->文档
勾选“自动加载更改,如果在此处保存”
我假设您已将所有未编译的 fx 文件作为资源添加到一个文件夹中,并且您希望将编译后的文件包含在您的 exe 文件编译的文件夹中。所以我做了以下操作
'Get the folder where the exe file is currently compiled
Dim ExeFileDirectory As String = AppDomain.CurrentDomain.BaseDirectory
'The name of the folder that will be opened or created at the exe folder location
Dim FolderNameForTheCompiledPsFiles As String = "ShaderFiles"
' Create the Directory were the shader files will be
Dim FolderWithCompiledShaderFiles As String = ExeFileDirectory & FolderNameForTheCompiledPsFiles
' If it dosn't exitst creat it
If (Not IO.Directory.Exists(FolderWithCompiledShaderFiles)) Then
IO.Directory.CreateDirectory(FolderWithCompiledShaderFiles)
End If
'Find the resource folder where the uncompiled fx filer are
Dim ShaderSourceFiles As String = IO.Path.Combine(GetParentDirectory(ExeFileDirectory, 2), "ShaderSourceFiles")
' Get all the uncopiled files in the folder
Dim directoryEntries As String() = System.IO.Directory.GetFileSystemEntries(ShaderSourceFiles, "*.fx")
在普通的调试模式下,exe 文件存储在 c:\ ... YourProject\bin\debug\YourExeFile.Exe 文件夹中,因此您需要返回两个目录才能到达资源文件夹所在的文件夹。所以我写了一个简短的小函数
Private Function GetParentDirectory(ByVal FolderName As String, Optional ByVal ParentNumber As Integer = 1) As String
If ParentNumber = 0 Then
Return FolderName
End If
Dim result As IO.DirectoryInfo
Dim CurrentFolderName As String = FolderName
For i As Integer = 1 To ParentNumber + 1
result = IO.Directory.GetParent(CurrentFolderName)
CurrentFolderName = result.FullName
Next
Return CurrentFolderName
End Function
现在我们得到了所需的文件夹,一个用于获取 fx 文件,另一个用于放置编译后的文件。我决定创建两个类,一个用于保存特定的 fx 文件属性和文件的编译设置,另一个用于完成所有编译器工作。下面是 HLSLFileHelperClass 的一部分
Public Class HLSLFileHelperClass
...
Private pFileNameWithoutExtension As String
Public Property FileNameWithoutExtension() As String
Get
Return pFileNameWithoutExtension
End Get
Set(ByVal value As String)
pFileNameWithoutExtension = value
End Set
End Property
...
Private pHLSLEntryPoint As String = "main"
Public Property HLSLEntryPoint() As String
Get
Return pHLSLEntryPoint
End Get
Set(ByVal value As String)
pHLSLEntryPoint = value
End Set
End Property
Private pShaderCompilerVersion As ShaderVersion = ShaderVersion.ps_3_0
Public Property ShaderCompilerVersion() As ShaderVersion
Get
Return pShaderCompilerVersion
End Get
Set(ByVal value As ShaderVersion)
pShaderCompilerVersion = value
End Set
End Property
Public Enum ShaderVersion
ps_2_0
ps_3_0
ps_4_0
ps_4_1
ps_5_0
End Enum
End Class
编译器辅助工具将包含一个需要编译的 `HLSLFileHelperClass` 列表,以及实际的编译方法。编译器是在一个隐藏的 cmd.exe 窗口中完成的。
Sub Compile()
Dim p As New Process()
Dim info As New ProcessStartInfo()
info.FileName = "cmd.exe"
info.CreateNoWindow = True
info.WindowStyle = ProcessWindowStyle.Hidden
info.RedirectStandardInput = True
info.UseShellExecute = False
info.RedirectStandardOutput = True
info.RedirectStandardError = True
p.StartInfo = info
p.Start()
p.BeginOutputReadLine()
p.BeginErrorReadLine()
AddHandler p.OutputDataReceived, AddressOf NormalOutputHandler
AddHandler p.ErrorDataReceived, AddressOf ErrorAndWarningOutputHandler
Dim sw As IO.StreamWriter = p.StandardInput
Dim result As String = ""
For Each File As HLSLFileHelperClass In FilesToCompile
CompileFile(sw, File)
Next
p.WaitForExit(1000)
End Sub
错误、警告和编译完成消息由两个处理程序收集,信息存储在实现 `INotifiedChange` 接口的 String 属性中。CompileFile 类只连接必要的信息并在 cmd 中运行命令。
Sub CompileFile(ByVal sw As IO.StreamWriter, ByVal sender As HLSLFileHelperClass)
If sw.BaseStream.CanWrite Then
Dim s As String = """" & FXCFileLocation & """ /T " & sender.ShaderCompilerVersion.ToString & " /E " & sender.HLSLEntryPoint & " /Fo""" & sender.GetCompiledFileFullName & """ """ & sender.GetSourceFileFullName & """"
sw.WriteLine(s)
End If
End Sub
命令行中的不同属性在下表中解释
Attribute | 描述 |
/T | 着色器配置文件,将用于编译文件。ps_2_0 只是 Shader 2.0。 |
/E | 着色器中的入口函数,就像旧时代控制台程序以 void main 开始一样 |
/Fo | 编译器生成的文件名称和位置(输出文件) |
这三个只是编译文件必须具备的,但还有更多 可用 的设置。
我现在拥有了与构建事件等效的功能,而且实际上比它更好一些。它将在 MainWindow 启动之前运行编译器,并且您可以绑定编译器结果(可以只显示所有已编译文件的错误和警告,或者显示完整的构建存储,它会告诉您文件是否已编译)。我们面对现实吧,如果您是普通人,您迟早会需要错误信息。错误(或警告!)将以错误代码示例:X3123 给出,后面跟着的文本与 此处 的列表对应。最后,它会给出抛出错误或警告的行号。
使用 DLL 编译 fx 文件
fxc 文件编译器还不错,但它需要大量的命令行代码,而且在编译文件之前您必须将文件转换为 ANSI 格式。如果您改为使用非托管 (C++ 编译的) DLL 调用来编译 fx,则可以避免这两个麻烦。
在我继续之前,我需要先解释一些事情。具有此功能的实际 dll(即 32 位系统)文件位于(在 Windows 8.0、8.1 和 10 上,只需将 10 替换为 8.1 或 8.1 即可获取您的版本对应的路径)这里
C:\Program Files\Windows Kits\10\bin\x86
如果您眼尖,您会发现这与 fxc.exe 文件位于同一个位置。但是,如果您希望在程序中包含 DLL,请确保使用用于重新分发的文件,这些文件位于此处(再次适用于 32 位)
C:\Program Files\Windows Kits\10\Redist\D3D\x86
这两个文件可能存在差异,因为其中一个可能是为您的硬件量身定制的。如果您有任何其他版本的 Windows,您可能希望查看这篇 Codeproject 文章。它解释了将 DLL 作为资源添加到 Visual Studio 项目的各种方法。
我决定采用一种更简单的方式来指定要使用的 DLL,正如 Jonathan Swift 建议的那样,通过使用 kernel32.dll 中的 LoadLibrary。
Imports System.Runtime.InteropServices
Module NativeDLLMethods
<DllImport("kernel32.dll")>
Public Function LoadLibrary(dllToLoad As String) As IntPtr
End Function
<DllImport("kernel32.dll")>
Public Function GetProcAddress(hModule As IntPtr, procedureName As String) As IntPtr
End Function
<DllImport("kernel32.dll")>
Public Function FreeLibrary(hModule As IntPtr) As Boolean
End Function
End Module
您现在可以将 DLL 加载到内存中,接下来我们将仔细研究如何在 C# 或 VB.NET 中实现非托管代码。在 文档 中给出了加载 DLL 的参数。
HRESULT WINAPI D3DCompileFromFile(
in LPCWSTR pFileName,
in_opt const D3D_SHADER_MACRO pDefines,
in_opt ID3DInclude pInclude,
in LPCSTR pEntrypoint,
in LPCSTR pTarget,
in UINT Flags1,
in UINT Flags2,
out ID3DBlob ppCode,
out_opt ID3DBlob ppErrorMsgs
);
当您定义 DLL 的入口点时,请务必仔细阅读输入类型,尤其是 字符串。它们有几种不同的封送处理类型。最终可用的定义如下所示
<DllImport("d3dcompiler_47.dll", CharSet:=CharSet.Auto)> _
Public Function D3DCompileFromFile(<MarshalAs(UnmanagedType.LPWStr)> pFilename As String,
pDefines As IntPtr,
pInclude As IntPtr,
<MarshalAs(UnmanagedType.LPStr)> pEntrypoint As String,
<MarshalAs(UnmanagedType.LPStr)> pTarget As String,
flags1 As Integer,
flags2 As Integer,
ByRef ppCode As ID3DBlob,
ByRef ppErrorMsgs As ID3DBlob) As Integer
最后两个元素,一个返回编译后的代码,另一个返回可能的错误消息。它被命名为 ID3DBlob,并在 文档 中定义。您还需要在此部分使用 PreserveSig,否则它将不起作用(文档)。
<Guid("8BA5FB08-5195-40e2-AC58-0D989C3A0102"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> _
Public Interface ID3DBlob
<PreserveSig> _
Function GetBufferPointer() As IntPtr
<PreserveSig> _
Function GetBufferSize() As Integer
End Interface
我们现在拥有了编译 fx 文件所需的所有代码,可以使用 DLL 文件,而不是繁琐复杂的 fxc.exe 文件。如果您的 Windows 版本为 8.0 或更高,上面定义 DLL 函数的代码块可以直接使用,因为系统已知其位置。
为了确保它能在旧版本的 Windows 上运行,我们需要指定要使用的位置和文件。但要使用 LoadLibrary 函数,您需要定义一个委托(在本例中充当 C++ 样式指针)来定义您想要在 DLL 中调用的函数。
Imports System.Runtime.InteropServices
Module DLLForLoadLibraryUse
Public Delegate Function D3DCompileFromFile(<MarshalAs(UnmanagedType.LPWStr)> pFilename As String,
pDefines As IntPtr,
pInclude As IntPtr,
<MarshalAs(UnmanagedType.LPStr)> pEntrypoint As String,
<MarshalAs(UnmanagedType.LPStr)> pTarget As String,
flags1 As Integer,
flags2 As Integer,
ByRef ppCode As ID3DBlob,
ByRef ppErrorMsgs As ID3DBlob) As Integer
Public Delegate Function D3DCompile(<MarshalAs(UnmanagedType.LPStr)> pSrcData As String,
SrcDataSize As Integer,
<MarshalAs(UnmanagedType.LPStr)> pSourceName As String,
pDefines As IntPtr,
pInclude As IntPtr,
<MarshalAs(UnmanagedType.LPStr)> pEntrypoint As String,
<MarshalAs(UnmanagedType.LPStr)> pTarget As String,
flags1 As Integer,
flags2 As Integer,
ByRef ppCode As ID3DBlob,
ByRef ppErrorMsgs As ID3DBlob) As Integer
End Module
在 Jonathan Swift 的原始帖子中,他建议在委托上方使用这条 调用约定 行
<UnmanagedFunctionPointer(CallingConvention.Cdecl)>
只有当函数调用包含可变参数时才需要使用它;实际上,如果您将其包含在 fx 文件的编译器中,它会给您一个错误,指出委托中的参数数量与 DLL 函数调用中的参数数量不匹配。事实上,Martin Costello 建议使用函数调用
<UnmanagedFunctionPointer(CallingConvention.StdCall)>
没有必要包含这一行,因为它是默认的调用约定。
由于 DLL 只需加载一次,我决定使用共享变量来保存内存中的位置指针。
Public Shared FxDllCompiler As IntPtr
Public Shared pAddressOfFxByteCompiler As IntPtr
Public Shared pAddressOfFxBFileompiler As IntPtr
Public Shared pFxByteStreamCompilation As DLLForLoadLibraryUse.D3DCompile
Public Shared pFxFileCompilation As DLLForLoadLibraryUse.D3DCompileFromFile
Public Shared DllFilesLocation As String
Public Shared Sub FreeDlls()
Dim result As Boolean = NativeDLLMethods.FreeLibrary(FxDllCompiler)
End Sub
Public Shared Sub SetUpDlls()
If IntPtr.Size = 4 Then
FxDllCompiler = NativeDLLMethods.LoadLibrary(IO.Path.Combine(DllFilesLocation, "d3dcompiler_47_32bit.dll"))
Else
FxDllCompiler = NativeDLLMethods.LoadLibrary(IO.Path.Combine(DllFilesLocation, "d3dcompiler_47_64bit.dll"))
End If
If FxDllCompiler = IntPtr.Zero Then
MessageBox.Show("Could not load the DLL file")
End If
pAddressOfFxByteCompiler = NativeDLLMethods.GetProcAddress(FxDllCompiler, "D3DCompile")
If pAddressOfFxByteCompiler = IntPtr.Zero Then
MessageBox.Show("Could not locate the function D3DCompile in the DLL")
End If
pAddressOfFxBFileompiler = NativeDLLMethods.GetProcAddress(FxDllCompiler, "D3DCompileFromFile")
If pAddressOfFxBFileompiler = IntPtr.Zero Then
MessageBox.Show("Could not locate the function D3DCompileFromFile in the DLL")
End If
pFxByteStreamCompilation = Marshal.GetDelegateForFunctionPointer(pAddressOfFxByteCompiler, GetType(DLLForLoadLibraryUse.D3DCompile))
pFxFileCompilation = Marshal.GetDelegateForFunctionPointer(pAddressOfFxBFileompiler, GetType(DLLForLoadLibraryUse.D3DCompileFromFile))
End Sub
所有条目都作为共享成员存储,因为我只需要将函数加载到内存中一次,并将其用作后续调用的入口点。我也可以在调用方法完成后调用 FreeLibrary,但我没有,因此 DLL 在应用程序终止时从内存中释放。如果您还没有完成使用函数,则应小心不要调用 FreeLibrary,因为如果您尝试在每次调用后释放它,它将引发错误。Mike Stall 在 此处 为 `kernel32.dll` 的非托管调用创建了一个包装器,您可以改用它。
DLL 的入口点保留在一个模块中,因此它只有一个起点。因此,文件的编译代码如下所示
Public Sub Compile(ByVal File As HLSLFileHelperClass)
Dim pFilename As String = File.GetSourceFileFullName
Dim pDefines As IntPtr = IntPtr.Zero
Dim pInclude As IntPtr = IntPtr.Zero
Dim pEntrypoint As String = File.HLSLEntryPoint
Dim pTarget As String = File.ShaderCompilerVersion.ToString
Dim flags1 As Integer = 0
Dim flags2 As Integer = 0
Dim ppCode As DLLEntryPointModule.ID3DBlob = Nothing
Dim ppErrorMsgs As DLLEntryPointModule.ID3DBlob = Nothing
Dim CompileResult As Integer = 0
CompileResult = DLLEntryPointModule.D3DCompileFromFile(pFilename,
pDefines,
pInclude,
pEntrypoint,
pTarget,
flags1,
flags2,
ppCode,
ppErrorMsgs)
If CompileResult <> 0 Then
Dim errors As IntPtr = ppErrorMsgs.GetBufferPointer()
Dim size As Integer = ppErrorMsgs.GetBufferSize()
ErrorText = Marshal.PtrToStringAnsi(errors)
IsCompiled = False
Else
IsCompiled = True
Dim psPath = File.GetCompiledFileFullName
Dim pCompiledPs As IntPtr = ppCode.GetBufferPointer()
Dim compiledPsSize As Integer = ppCode.GetBufferSize()
Dim compiledPs = New Byte(compiledPsSize - 1) {}
Marshal.Copy(pCompiledPs, compiledPs, 0, compiledPs.Length)
Using psFile = IO.File.Open(psPath, FileMode.Create, FileAccess.Write)
psFile.Write(compiledPs, 0, compiledPs.Length)
End Using
End If
If ppCode IsNot Nothing Then
Marshal.ReleaseComObject(ppCode)
End If
ppCode = Nothing
If ppErrorMsgs IsNot Nothing Then
Marshal.ReleaseComObject(ppErrorMsgs)
End If
ppErrorMsgs = Nothing
End Sub
从这个 DLL 编译还有一个优点,文件不必是 ANSI 格式,因此您可以直接在 Visual Studio 中编辑文件,并且不再需要担心文件格式。
HRESULT WINAPI D3DCompile(
in LPCVOID pSrcData,
in SIZE_T SrcDataSize,
in_opt LPCSTR pSourceName,
in_opt const D3D_SHADER_MACRO pDefines,
in_opt ID3DInclude pInclude,
in LPCSTR pEntrypoint,
in LPCSTR pTarget,
in UINT Flags1,
in UINT Flags2,
out ID3DBlob ppCode,
out_opt ID3DBlob ppErrorMsgs
);
代码几乎与 CompileFromFile 代码完全相同,唯一的区别是数据读取方式
Dim s As String = IO.File.ReadAllText(file.GetSourceFileFullName)
Dim pSrcData As String = s
Dim SrcDataSize As Integer = s.Length
在 VB.NET/C# 中与 *.ps 文件交互
类与 ps 文件中的值交互的最简单方法是继承 ShaderEffect 类。当您在 WPF 中使用 ShaderEffect 类时,您只能与两种类型的寄存器 (S 和 C) 通信。但是,它们可以保存多种不同的类型,并且它们的名称类型并不完全对应。下面列出了 HLSL 中的类型以及 .NET 中的对应值(来自 此 网站)
|
Inherits ShaderEffect
Public Shared ReadOnly InputProperty As DependencyProperty = ShaderEffect.RegisterPixelShaderSamplerProperty("Input", GetType(ShaderEffectBase), 0, SamplingMode.NearestNeighbor)
Protected Sub New()
Me.UpdateShaderValue(InputProperty)
End Sub
''' <summary>
''' Gets, Sets the effect input.
''' </summary>
Public Property Input() As Brush
Get
Return TryCast(Me.GetValue(InputProperty), Brush)
End Get
Set(value As Brush)
Me.SetValue(InputProperty, value)
End Set
End Property
它具有一个自定义构建的依赖属性,名为 `RegisterPixelShaderSamplerProperty`,它连接到 ps 文件中名为 `S0`、`S1` ... `Sn` 的值,其中 `n` 是 DependencyProperty 注册中给定的索引,在上面的代码中为 `0`。如果您有多个采样器属性 (`ImageBrushes`),`C1` 将如下所示
Public Shared ReadOnly Input2Property As DependencyProperty = ShaderEffect.RegisterPixelShaderSamplerProperty("Input2", GetType(BlendTwoPicturesEffect), 1, SamplingMode.NearestNeighbor)
Public Property Input2() As Brush
Get
Return TryCast(Me.GetValue(Input2Property), Brush)
End Get
Set(value As Brush)
Me.SetValue(Input2Property, value)
End Set
End Property
在 Effect 中,它对应于将由这个自定义效果类处理的对象的 `ImageBrush`。
可以通过 `ShaderEffect` 类处理的第二种对象类型是 `Cn` 值,如下所示,因为它与 ps 文件中(float 格式)的值 `C0` 进行通信。
Public Shared MixInAmountProperty As DependencyProperty = DependencyProperty.Register("MixInAmount", GetType(Double), GetType(BlendTwoPicturesEffect),
New PropertyMetadata(0.5f, PixelShaderConstantCallback(0)))
Public Property MixInAmount As Double
Get
Return DirectCast(Me.GetValue(MixInAmountProperty), Double)
End Get
Set(value As Double)
Me.SetValue(MixInAmountProperty, value)
End Set
End Property
Sub New()
Dim s As String = AppDomain.CurrentDomain.BaseDirectory
PixelShader = New PixelShader With {.UriSource = New Uri(s & "\ShaderFiles\BlendTwoPictures.ps")}
Me.UpdateShaderValue(Input2Property)
Me.UpdateShaderValue(MixInAmountProperty)
End Sub
Public Sub New()
...
AddHandler CompositionTarget.Rendering, AddressOf CompositionTarget_Rendering
End Sub
Dim LastTiumeRendered As Double = 0
Dim RenderPeriodInMS As Double = 1000
Private Sub CompositionTarget_Rendering(sender As Object, e As EventArgs)
Dim rargs As RenderingEventArgs = DirectCast(e, RenderingEventArgs)
If ((rargs.RenderingTime.TotalMilliseconds - LastTiumeRendered) > RenderPeriodInMS) Then
...
LastTiumeRendered = rargs.RenderingTime.TotalMilliseconds
End If
End Sub
CodeDom 简单讲解
Dim codeGraph As New CodeCompileUnit()
Private Function AssignNamespacesToGraph(codeGraph As CodeCompileUnit, namespaceName As String) As CodeNamespace
' Add imports to the global (unnamed) namespace.
Dim globalNamespace As New CodeNamespace()
globalNamespace.[Imports].AddRange({New CodeNamespaceImport("System"),
New CodeNamespaceImport("System.Windows"),
New CodeNamespaceImport("System.Windows.Media"),
New CodeNamespaceImport("System.Windows.Media.Effects"),
New CodeNamespaceImport("System.Windows.Media.Media3D")})
codeGraph.Namespaces.Add(globalNamespace)
' Create a named namespace.
Dim ns As New CodeNamespace(namespaceName)
codeGraph.Namespaces.Add(ns)
Return ns
End Function
下一步是实际声明我们将使用的类的名称
Dim shader As New CodeTypeDeclaration() With { _
.Name = shaderModel.GeneratedClassName
}
该类现在需要继承 ShaderEffect 类,这通过将其添加到 BaseTypes 中来完成。
shader.BaseTypes.Add(New CodeTypeReference("ShaderEffect"))
如果您想添加一个接口,您会以完全相同的方法进行操作
Dim iequatable As New CodeTypeReference("IEquatable", New CodeTypeReference(shader.Name))
shader.BaseTypes.Add(iequatable)
为了完整起见,您可以按照以下示例中的方式实现 INotifiedChange 逻辑
Dim myCodeTypeDecl As New CodeTypeDeclaration() With { _
.Name = "MyClass"
}
myCodeTypeDecl.BaseTypes.Add(GetType(System.ComponentModel.INotifyPropertyChanged))
Dim myEvent As New CodeMemberEvent()
With myEvent
.Name = "PropertyChanged"
.Type = New CodeTypeReference(GetType(System.ComponentModel.PropertyChangedEventHandler))
.Attributes = MemberAttributes.Public Or MemberAttributes.Final
.ImplementationTypes.Add(GetType(System.ComponentModel.INotifyPropertyChanged))
End With
myCodeTypeDecl.Members.Add(myEvent)
Dim myMethod As New CodeMemberMethod
With myMethod
.Name = "OnPropertyChanged"
.Parameters.Add(New CodeParameterDeclarationExpression(GetType(String), "pPropName"))
.ReturnType = New CodeTypeReference(GetType(Void))
.Statements.Add(New CodeExpressionStatement(
New CodeDelegateInvokeExpression(
New CodeEventReferenceExpression(
New CodeThisReferenceExpression(), "PropertyChanged"),
New CodeExpression() {
New CodeThisReferenceExpression(),
New CodeObjectCreateExpression(GetType(System.ComponentModel.PropertyChangedEventArgs),
New CodeArgumentReferenceExpression("pPropName"))})))
.Attributes = MemberAttributes.FamilyOrAssembly
End With
myCodeTypeDecl.Members.Add(myMethod)
Dim myProperty As New CodeMemberProperty
With myProperty
.Name = "fldItemNr"
.Attributes = MemberAttributes.Public Or MemberAttributes.Final
.Type = New CodeTypeReference(GetType(String))
.SetStatements.Add(New CodeAssignStatement(New CodeVariableReferenceExpression("m_fldItemNr"), New CodePropertySetValueReferenceExpression))
.SetStatements.Add(New CodeExpressionStatement(New CodeMethodInvokeExpression(New CodeMethodReferenceExpression(New CodeThisReferenceExpression(), "OnPropertyChanged"), New CodeExpression() {New CodePrimitiveExpression("fldItemNr")})))
.GetStatements.Add(New CodeMethodReturnStatement(New CodeVariableReferenceExpression("m_fldItemNr")))
End With
myCodeTypeDecl.Members.Add(myProperty)
回到 ps 文件 ShaderEffect 包装器的构建。我们还需要在类的构造函数中实现 ps 文件,因此我们添加了创建公共构造函数的逻辑
Dim constructor As New CodeConstructor() With { _
.Attributes = MemberAttributes.[Public]
}
我们还需要创建指向 ps 文件位置的相对 Uri 路径,并将此 Uri 路径设置为从 ShaderEffect 类继承的 PixelShader
Dim shaderRelativeUri As String = [String].Format("/{0};component/{1}.ps", shaderModel.GeneratedNamespace, shaderModel.GeneratedClassName)
Dim CreateUri As New CodeObjectCreateExpression
CreateUri.CreateType = New CodeTypeReference("Uri")
CreateUri.Parameters.AddRange({New CodePrimitiveExpression(shaderRelativeUri), New CodeFieldReferenceExpression(New CodeTypeReferenceExpression("UriKind"), "Relative")})
Dim ConnectUriSource As New CodeAssignStatement With {
.Left = New CodeFieldReferenceExpression(New CodeThisReferenceExpression, "PixelShader.UriSource"),
.Right = CreateUri}
然后我们需要创建一个新的 PixelShader 实例并将上面的代码添加到构造函数中,并在末尾添加一个空行以留出一些空间。
constructor.Statements.AddRange({New CodeAssignStatement() With {.Left = New CodePropertyReferenceExpression(New CodeThisReferenceExpression(), "PixelShader"),
.Right = New CodeObjectCreateExpression(New CodeTypeReference("PixelShader"))},
ConnectUriSource,
New CodeSnippetStatement("")})
在构造函数中,我们还需要通过命令更新着色器值
Dim result As New CodeMethodInvokeExpression() With { _
.Method = New CodeMethodReferenceExpression(New CodeThisReferenceExpression(), "UpdateShaderValue")
}
result.Parameters.Add(New CodeVariableReferenceExpression(propertyName & "Property"))
这在函数中完成,因为我们需要为 ps 文件中找到的所有属性执行此操作。
`DependencyProperties` 的创建是在一个函数中完成的,请注意它只支持 `Cn` 类型的值,因为它通过 `PixelShaderConstantCallback` 进行通信。
Private Function CreateShaderRegisterDependencyProperty(shaderModel As ShaderModel, register As ShaderModelConstantRegister) As CodeMemberField
Dim RegisterDependencyProperty As New CodeMethodInvokeExpression
Dim RegisterMethod As New CodeMethodReferenceExpression
RegisterMethod.TargetObject = New CodeTypeReferenceExpression("DependencyProperty")
RegisterMethod.MethodName = "Register"
RegisterDependencyProperty.Method = RegisterMethod
Dim PropertyMetadataFunction As New CodeObjectCreateExpression
PropertyMetadataFunction.CreateType = New CodeTypeReference("PropertyMetadata")
PropertyMetadataFunction.Parameters.Add(CreateDefaultValue(register.DefaultValue))
Dim PropertyMetadataCallback As New CodeMethodInvokeExpression
PropertyMetadataCallback.Method = New CodeMethodReferenceExpression(Nothing, "PixelShaderConstantCallback")
PropertyMetadataCallback.Parameters.Add(New CodePrimitiveExpression(register.RegisterNumber))
PropertyMetadataFunction.Parameters.Add(PropertyMetadataCallback)
RegisterDependencyProperty.Parameters.AddRange({New CodePrimitiveExpression(register.RegisterName), New CodeTypeOfExpression(register.RegisterType), New CodeTypeOfExpression(shaderModel.GeneratedClassName), PropertyMetadataFunction})
Dim InitiateDependencyProperty As New CodeMemberField
InitiateDependencyProperty.Type = New CodeTypeReference("DependencyProperty")
InitiateDependencyProperty.Name = String.Format("{0}Property", register.RegisterName)
InitiateDependencyProperty.Attributes = MemberAttributes.Public Or MemberAttributes.Static
InitiateDependencyProperty.InitExpression = RegisterDependencyProperty
Return InitiateDependencyProperty
End Function
为了与采样寄存器(`Sn`)通信,您需要稍微不同(更短)的代码
Private Function CreateSamplerDependencyProperty(className As String, propertyName As String, ByVal RegisterNumber As Integer) As CodeMemberField
Dim RegisterDependencyProperty As New CodeMethodInvokeExpression
Dim RegisterMethod As New CodeMethodReferenceExpression
RegisterMethod.TargetObject = New CodeTypeReferenceExpression("ShaderEffect")
RegisterMethod.MethodName = "RegisterPixelShaderSamplerProperty"
RegisterDependencyProperty.Method = RegisterMethod
RegisterDependencyProperty.Parameters.AddRange({New CodePrimitiveExpression(propertyName), New CodeTypeOfExpression(className), New CodePrimitiveExpression(RegisterNumber)})
Dim result As New CodeMemberField
result.Type = New CodeTypeReference("DependencyProperty")
result.Name = String.Format("{0}Property", propertyName)
result.Attributes = MemberAttributes.Public Or MemberAttributes.Static
result.InitExpression = RegisterDependencyProperty
Return result
End Function
现在只需循环遍历 fx 文件中找到的所有属性,然后生成并添加所有代码即可。
' Add a dependency property and a CLR property for each of the shader's register variables
For Each register As ShaderModelConstantRegister In shaderModel.Registers
If register.GPURegisterType.ToString.ToLower = "c" Then
shader.Members.Add(CreateShaderRegisterDependencyProperty(shaderModel, register))
shader.Members.Add(CreateCLRProperty(register.RegisterName, register.RegisterType, register.Description))
Else
shader.Members.Add(CreateSamplerDependencyProperty(shaderModel.GeneratedClassName, register.RegisterName, register.GPURegisterNumber))
shader.Members.Add(CreateCLRProperty(register.RegisterName, GetType(Brush), Nothing))
End If
Next
现在已包含通过 WPF 中的 ShaderEffect 类与 ps 文件通信所需的所有代码。创建 VB 或 C# 代码现在变得非常简单。这确实是使用 CodeDom 的真正神奇之处之一。
Private Function GenerateCode(provider As CodeDomProvider, compileUnit As CodeCompileUnit) As String
' Generate source code using the code generator.
Using writer As New StringWriter()
Dim indentString As String = ""
Dim options As New CodeGeneratorOptions() With { _
.IndentString = indentString, _
.BlankLinesBetweenMembers = True _
}
provider.GenerateCodeFromCompileUnit(compileUnit, writer, options)
Dim text As String = writer.ToString()
' Fix up code: make static DP fields readonly, and use triple-slash or triple-quote comments for XML doc comments.
If provider.FileExtension = "cs" Then
text = text.Replace("public static DependencyProperty", "public static readonly DependencyProperty")
text = Regex.Replace(text, "// <(?!/?auto-generated)", "/// <")
ElseIf provider.FileExtension = "vb" Then
text = text.Replace("Public Shared ", "Public Shared ReadOnly ")
text = text.Replace("'<", "'''<")
End If
Return text
End Using
End Function
CodeDomProvider 可以是 VB.NET
Dim provider As New Microsoft.VisualBasic.VBCodeProvider()
或 C#
Dim provider As New Microsoft.CSharp.CSharpCodeProvider()
现在您可以非常简单地为用户提供他喜欢的代码。
一些高级技巧
存储效果图片
要实际存储应用了效果的控件,您必须使用 RenderTargetBitmap 函数。这可能有点令人沮丧,因为 RenderTargetBitmap 使用 CPU 来处理 UIElement。如果您操作不当,也可能会遇到麻烦,如 Jamie Rodriguez 在他的博客中 所示。尽管下面显示的代码最初是由 Adam Smith 编写的。
Private Shared Function CaptureScreen(target As Visual, Optional dpiX As Double = 96, Optional dpiY As Double = 96) As BitmapSource
If target Is Nothing Then
Return Nothing
End If
Dim bounds As Rect = VisualTreeHelper.GetDescendantBounds(target)
Dim rtb As New RenderTargetBitmap(CInt(bounds.Width * dpiX / 96.0), CInt(bounds.Height * dpiY / 96.0), dpiX, dpiY, PixelFormats.Pbgra32)
Dim dv As New DrawingVisual()
Using ctx As DrawingContext = dv.RenderOpen()
Dim vb As New VisualBrush(target)
ctx.DrawRectangle(vb, Nothing, New Rect(New Point(), bounds.Size))
End Using
rtb.Render(dv)
Return rtb
End Function
将多种效果应用于一个控件
当旧的(现已过时)BitmapEffects 发布时,它们带有一个名为 BitmapEffectGroup 的东西,它允许您将多种效果应用于一个控件。然而,当创建新的 Effect 类(使用 GPU 而不是 CPU)时,EffectGroup 控件被认为直接实现过于困难。
因此,如果您仍然想实现它,有两种方法。第一种方法是 Greg Schuster 在此处 建议的,即简单地在您要添加效果的元素周围包裹一个 Border,然后将第二个效果添加到 Border,如下所示。
<Border>
<Border.Effect>
<local:GaussianEffect/>
</Border.Effect>
<Image>
<Image.Effect>
<local:ScratchedShader></local:ScratchedShader>
</Image.Effect>
</Image>
</Border>
它在 WPF 中实现起来相当简单,无需对 fx 文件进行任何操作。
然而,有一个更好的选择,至少如果您考虑计算时间的话。所有的 fx 文件都有一个入口点函数,这意味着不同 fx 文件的所有函数可以合并到一个新的 fx 文件中并进行编译。当然,您还需要拥有 WPF 提供给 ps 文件的输入变量。
似乎 XNA 框架 有一些直接加载 ps 文件的方法,但这超出了本文的范围。
仅将效果应用于控件的一部分
一个经常出现的问题是将效果应用于图像或控件的一部分。一种方法是调用 `Image.Clip` 并为其添加效果。
<Rectangle VerticalAlignment="Top" HorizontalAlignment="Left" x:Name="BoundImageRect" Height="50" Width="50" Panel.ZIndex="4" Stroke="Black" Fill="Transparent" StrokeThickness="2" MouseDown="BoundImageRect_MouseDown" MouseMove="BoundImageRect_MouseMove" MouseUp="BoundImageRect_MouseUp" >
<Rectangle.RenderTransform>
<TranslateTransform x:Name="Trans" X="0" Y="0"></TranslateTransform>
</Rectangle.RenderTransform>
</Rectangle>
<Image VerticalAlignment="Top" HorizontalAlignment="Left" Width="Auto" Height="356" Source="{Binding ElementName=Img, Path=Source }" Panel.ZIndex="3">
<Image.Clip>
<RectangleGeometry RadiusX="{Binding ElementName=BoundImageRect,Path=RadiusX}" RadiusY="{Binding ElementName=BoundImageRect,Path=RadiusY}" >
<RectangleGeometry.Rect>
<MultiBinding Converter="{StaticResource convertRect}">
<Binding ElementName="BoundImageRect" Path="Width"/>
<Binding ElementName="BoundImageRect" Path="Height"/>
<Binding ElementName="Trans" Path="X"/>
<Binding ElementName="Trans" Path="Y"/>
</MultiBinding>
</RectangleGeometry.Rect>
</RectangleGeometry>
</Image.Clip>
<Image.Effect>
<local:GaussianEffect></local:GaussianEffect>
</Image.Effect>
</Image>
然而,我实际上在使用它时遇到了内存泄漏,所以它可能有点不稳定。
还有两种其他方式来切割元素。一种是向视觉元素添加效果
Public Class CoolDrawing
Inherits FrameworkElement
Implements System.ComponentModel.INotifyPropertyChanged
' Create a collection of child visual objects.
Private _children As VisualCollection
Public Sub New()
_children = New VisualCollection(Me)
Dim VisualImage As DrawingVisual
VisualImage = CreateDrawingVisualCircle()
VisualImage.Effect = New GaussianEffect
_children.Add(VisualImage)
End Sub
您可以根据需要对不同的 DraingVisuals 应用不同的效果。
和往常一样,有一种可能性是 fx 文件本身只作用于一小部分,就像所包含的放大镜一样。您没有机会(至少在没有大量工作的情况下)通过这种方法操纵图像的特定部分。
关注点
这总结了着色器效果系列冒险的第一部分,这是着色器效果之旅的开端,因为这个轻量级的 Shazzam 编辑器工具将作为测试和调试您自己创建的着色器工具的工具箱。
我还想指出本文中用到的一些参考文献
Shazzam 着色器编辑器 - 由 Walt Ritscher 编写的绝佳工具(但程序无法按原样运行)
Walt Ritscher 撰写的《HLSL 和 XAML 开发人员的像素着色器》
Rene Schulte(MVP)的博客 Kodierer [Coder]。他有一些关于 Silverlight 的精彩着色器效果文章。
着色器效果示例
- https://www.shadertoy.com/
- https://codeproject.org.cn/Articles/36722/Create-Reflection-Shader-in-Silverlight
- https://codeproject.org.cn/Articles/226547/Mandelbrot-Set-with-PixelShader-in-WPF
- https://codeproject.org.cn/Articles/71617/Getting-Started-with-Shader-Effects-in-WPF
- https://codeproject.org.cn/Articles/89337/GLSL-Shader-for-Interpolating-Two-Textures
- https://codeproject.org.cn/Articles/94817/Pixel-Shader-for-Edge-Detection-and-Cartoon-Effect
- http://blogs.microsoft.co.il/tamir/2008/05/23/brightness-and-contrast-manipulation-in-wpf-35-sp1/
- http://khason.net/blog/hlsl-pixel-shader-effects-tutorial/
- http://www.rastertek.com/dx10tut32.html
- http://www.geeks3d.com/20101228/shader-library-frosted-glass-post-processing-shader-glsl/