Introduction
If you have used Effects in WPF before, you have inadvertently also (hopefully) used GPU accelerated functions in WPF. These affects are called ShaderEffects and were introduced with the relese of .NET3.5SP1 (The old class BitmapEffects was obsolite from .NET 4.0, so don't use them). Prior to that, the Effects were done in the CPU, causing serious performance issues on computer intensive effects, like blur. Now WPF offers Effects that have all the calculation done in the GPU, so visual effects does not affect your CPU performance. The downside is that WPF only offer two effects in the class Media.Effects (the old BitmapEffects are obsolete, and should NEVER be used):
- Blur Effect
- DropShadowEffect
WPF also offers something quite interesting, namely a ShaderEffect class, and this is the class you can use ito communicate with custom created ShaderEffects. These effect files, *.ps, are generated by the DirectX compiler from *.fx (HLSL) files. This article will go these steps:
- Writing you own HLSL (.fx) file
- How to compile the file into .ps (Pixel shader byte code)
- Set up a class in either VB.NET or C# in WPF to communicate with the .ps file
- Apply the effect to any WPF UIElement
If you read through this article my hope is that you will gain the ability to create your own custom pixel shader file and use it in your own WPF application. I should mention that most of the Shader files on the internet uses the shaders in the Silverlight language, but this article will only focus on WPF usages, but will contain material from programs original written using Silverlight and shaders.
This article relies heavily on the knowledge learned from reading the source code for Walt Ritscher for his program Shazzam Tool, and with the basis in his code I made some changes and improvements as his tool didn't run properly for me. Since the tool first was published there has also been some changes, especially on the Windows operation system, as the DirectX compiler and DLL's are now included by default.
If you have a bought version of Visual Studio 2013 or above you could use the built in graphics debugger instead. This article is mainly written for Visual Studio Express users, allowing user to write fx files with Visual Studio highlight the HLSL, although you might pick up some hints and tips on the usage of pixel shaders.
Generating HLSL files
The fx files or HLSL (an abbreviation for High Level Shading Language) files as they also are called. The files are programmed using a C like language, with a very limited number of functions that you can use. This makes it rather fast to learn and easy to use, but it do have some quirky details.
The fx files that are used by the ShaderEffect class in WPF always begin with a series of registered values that are stored in the GPU which you can communicate with using the ShaderEffect class (However, if you use the lib file in C++ you can access all of them). You can now see that the number of inputs that are availiable are fairly limited, there is also a maximum on how many you can use.
https://msdn.microsoft.com/en-us/library/hh315751(v=vs.110).aspx
In the PixelShaders used in WPF this line of code below has to be included in the fx file. Its were the Image itself is stored in the GPU register, and the pointer to where it can be reached.
sampler2D Input : register(S0);
This line of code actually specifies that the sampler2D value Input should be stored in the GPU register. It also specify in what register type it is in (S) but there are many more available, but the WPF application written in C# and VB can only communicate with the S and C register values.
In the Pixel Shader you work on one Pixel at the time, and in the main the function call (entry point of the ps file), you get as an input the current pixel location in x and y coordinates. Well that not quite true, you do get the coordinates but they are between 0 to 1 (0,0 are upper left corner), that is why its a float2 variable.
float4 main(float2 uv : TEXCOORD) : COLOR
{
...
}
you get the 4 values for the color of the given pixel by using the tex2 function, and the color is stored in a 4 item vector with float (again with values form 0 to 1, instead of the normal byte from 0 to 255) precision using the float4 deliberative field:
float4 color = tex2D( Input, uv );
And to, let's say, invert the colors of the image, we simply add the line:
float4 inverted_color = 1 - color ;
And to avoid the alpha (transparency factor) to be changed (there are more ways of getting the value), and to return the inverted color.:
inverted_color.a = 1;
return inverted_color;
This knowledge allows you to create all the effects that is calculated by one single pixel at the time. But most of the interesting stuff on images involves getting the neighboring pixels as well. Effects like a Gaussian filer, Sobel Filter etc all require calculations being done of several Neighboring pixels, so we need this function (the edge detector use a version of the derivative, which is actually available on the GPU as a function):
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);
};
There is one thing that is special about functions in the fx files, that is; each function must be declared before it is used:
float TestFunc(float n)
{
}
float main()
{
}
or have a pointer declared before its used:
float TestFunc(float);
float main()
{
}
float TestFunc(float n)
{
}
Assuming all is well, and you have defined the function properly, you should realize that you need the actual size of the picture. Since all the coordinates are on double precision format from 0 to 1, a pixel step is in double format.
float2 InputSize : register(C0);
In reality, you should now be able to write any PixelShader you want, just glace over the available functions from the documentation. Some cool example, that are pretty straight forward to follow, by Tamir Khason can be seen in these two links below:
There are also some old legacy tools for creating cool effects that might be worth taking a look at:
For more advanced shaders you might even want to use functions and class like structures in order to effectively program it. While the functions used are very useful for complex effects, you might need to compile the shader with a higher version than the standard version of 2.0 or 3.0.
Compiling HLSL files
Before I start to explain how to compile the fx files, I'd just want to say that this is only really needed if you have the Express editions of Visual Studio (of either 2012, 2013 or 2015), in the version you buy, you get to compiler as a resource. In the VS 2010 there is also a tool for compiling the fx file as a resource using a codeplex plugin. However, I think it would be useful to read the section anyway. I should also mention that there is yet another option to compile the fx file in VS 2010 and 2008 by installing a add-in to Visual Studio
Using the fxc to compile
In previous Windows versions you needed to install the DirectX SDK in order to get the fxc.exe file that is needed to compile the fx files into machine codeed ps. From Windows 8 the SDK is deployed as standard, saving you several hundred MB download (although the fxc file, the only one you really need, is only about 250KB.).
To compile the fx file most seem to do a quick compile using the build event in VS 2013. The compilation (on Windows 8.1) would be the following in the post build command:
"C:\Program Files\Windows Kits\8.1\bin\x86\fxc.exe" /T ps_2_0 /E main /Fo"Watercolor.ps" "Watercolor.fx"
But as it turned out, the build event, isn't availiable in VB.NET! Well, that wouldn't have to be a problem, I would just have to build it in the Application.XAML file. However you must remember to implement the namespace in your application to accssess classes you have there:
Imports WpfAdventuresInPixelShader
Class Application
Public Sub New()
End Sub
End Class
Having this hurdle made me go into the details of how an fx file is compiled a lot earlier than I thought, and I quickly found out one critical issue. If you open the fx file in VS (any year really), it stores the text in UTM-8 by default. The funny, or not so funny depending one once perspective is that it wont compile it if it is not in the notepad ANSI code or the ASCII code. This was actually so annoying that I decided to open the file and store it using the ASCII encoding:
For Each file As String In directoryEntries
Dim OpenFxFileInTextFormat() As String = IO.File.ReadAllLines(file)
IO.File.WriteAllLines(file, OpenFxFileInTextFormat, Text.Encoding.ASCII)
Next
This little code snippet enabled me to write and edit files directly in the VS 2013 and still use the small fxc.exe compiler (There are other ways of achieving this by importing a DLL file to compile it in code, more on that later.). There is one more thing that you could do. If you run this code the VS 2013 compiler will detect changes in the file and will ask you if you want to reload it. So to save yourself some trouble, in the top menu go to:
TOOLS->OPTIONS...
A dialog will appear where you navigate to
Environment->Documents
Tick off the Auto-Load changes, if saved here:
I assume that you will have all the uncompiled fx files added as a resource in a folder, and that you want the compiled files to be contained in a folder where your exe file is compiled at. So I did the following:
Dim ExeFileDirectory As String = AppDomain.CurrentDomain.BaseDirectory
Dim FolderNameForTheCompiledPsFiles As String = "ShaderFiles"
Dim FolderWithCompiledShaderFiles As String = ExeFileDirectory & FolderNameForTheCompiledPsFiles
If (Not IO.Directory.Exists(FolderWithCompiledShaderFiles)) Then
IO.Directory.CreateDirectory(FolderWithCompiledShaderFiles)
End If
Dim ShaderSourceFiles As String = IO.Path.Combine(GetParentDirectory(ExeFileDirectory, 2), "ShaderSourceFiles")
Dim directoryEntries As String() = System.IO.Directory.GetFileSystemEntries(ShaderSourceFiles, "*.fx")
In the normal debug mode, the exe file is stored in a folder c:\ ... YouProject\bin\debug\YourExeFile.Exe so you need to back up two directories to get to the folder where the resource folder is located at. So I mad a short little function:
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
Now that we got the desired folders, one folder where we get the fx files and another where we will place the compiled files in. I decided to make two classes, one that hold the specific fx file properties and compilation settings for the file, and one that would do all the compiler stuff. Below is an exert from the 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
The Compiler helper would then hold a list of HLSLFileHelperClass
that was going to be compiled, and the actual methods for the compiling. The compiler is done in a hidden cmd.exe window:
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
The error, warnings and compile completed messages are collected by the two handlers, and the information is stored in a String property that implements INotifiedChange
interface.The CompileFile class just connects the necessary information and runs the command in 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
The different attributes in the command line is explained in the table below:
Attribute |
Description |
/T |
Shader profile that will be used in compiling the file. ps_2_0 is simply Shader 2.0. |
/E |
the entery function in the shader, like the console programs in the old days started with void main |
/Fo |
the name and location of the file that is produced by the compiler (File Out) |
These three are simply the must have to compile the file, but there are many more settings available.
I now have the equivalent to the build event, that is actually a bit better than it. It will run the compiler before the MainWindow starts, and you can bind the compiler results (either just have the error and warnings of all the compiled files, or the complete build store that will tell you if the file was compiled or not.) Lets face it, if you are a normal person, you will need the error massage sooner or later. The error (or warning!) will be given with a error code example: X3123, and the text that will follow is the corresponding to the list here. And lastly, it will give you the line number where the error or warning was thrown.
Using DLL's to compile the fx file
The fxc file compiler is not that bad, but it requires quite a lot of command line code and you would have to convert the file to ANSI format before you can compile the files. Both of these nusiances can be avoided if you instead compile the fx with an unmanaged (c++ compiled) dll call.
Before I go one here I'm going to have to explain something first. The actual dll (32 bit system that is) file that has this function is located (on Windows 8.0, 8.1 and 10, just exchange the 10 with 8.1 or 8.1 to get the appropriate path for your version) here:
C:\Program Files\Windows Kits\10\bin\x86
If you have a keen eye, you will see that this is the same place that the fxc.exe file is located. However, if you wish to include a DLL in your program make sure to use the files that are meant for redistribution and are found here (for 32 bit again):
C:\Program Files\Windows Kits\10\Redist\D3D\x86
There could be differences on the two files, as one might be tailor made to fit your hardware. If you have any other versions of Windows you might want to have a look at this Codeproject article. It explains various ways of adding the DLL as a resource in a Visual studio project:
I decided to go an even easier way of to specify what DLL to use, as suggested by Jonathan Swift by employing LoadLibrary from the kernel32.dll:
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
You can now load the DLL into memory, we will now take a closer look at how to implement the unmanaged code in C# or VB.NET. In the documentation the parameters for loading the DLL is given:
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
);
When you define the entry point of the DLL, be sure to read the input types carefully, the strings especially. They have several different types of marshal. The Definition that finally worked looked like this:
<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
The two last elements , one that returns the compiled code and another that returns the possible error messages. It is given the name ID3DBlob, and its defined in the documentation. You also need the PreserveSig in this section, otherwise it wont work (The documentation).
<Guid("8BA5FB08-5195-40e2-AC58-0D989C3A0102"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> _
Public Interface ID3DBlob
<PreserveSig> _
Function GetBufferPointer() As IntPtr
<PreserveSig> _
Function GetBufferSize() As Integer
End Interface
We now have all the code necessary for compiling the fx files with the DLL file instead of the tedious and complicated use of the fxc.exe file. The code blocks above that defined the dll function could be used as it is if you have Windows 8.0 or higher, as the location is known to the system.
To make sure that it will work on older versions of Windows, we need to specify the location and file we want to use. But to use the LoadLibrary function you need to define a delegate (that functions as a C++ style a pointer in this case) to define the function call you want to call within the 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
In the origianl post from Jonathan swift he recommended to use this calling convention line above the delegate:
<UnmanagedFunctionPointer(CallingConvention.Cdecl)>
It is only nessecery to use that if the function call contains a varargs, in fact; if you include this in the compiler for fx files it would give you an error, saying that the number of parameters in the delegate mismatch the numbers in the DLL function call. In fact Martin Costello suggest using the function call:
<UnmanagedFunctionPointer(CallingConvention.StdCall)>
It is not nessesary to include this line, as it is the default calling conavention.
Since the DLL only needs to be loaded once, I decided to use shared variables to hold pointers to the position in memory.
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
All the entries are stored as shared members, as I only need to load the functions into memory once, and use this as an entry point for further calls. I can also call FreeLibary after I have finished calling the method, but I don't, so the DLL's get released from memory once the application is terminated. You should be careful not to call FreeLibrary if you arn't finished using the function, as it will throw an error if you try to relese it after each call. Mike Stall has created a wrapper for unmanaged calls to the kernel32.dll
here that you could use instead.
The Entry point of the DLL's are kept in a module, so it has only one point to start it from. The compile code for a file is thereby given as:
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
There is also one more advantage with compiling from this DLL, the files does not have to be in ANSI format, so you can edit the files directly in Visual Studio and you don't have to worry about the file format any more.
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
);
The code is nearly exactly the same as CompileFromFile code, the only difference is how the data is read in:
Dim s As String = IO.File.ReadAllText(file.GetSourceFileFullName)
Dim pSrcData As String = s
Dim SrcDataSize As Integer = s.Length
Interact with *.ps files in VB.NET/C#
The simplest way that you have for a class to interacts with the values in the ps file, is to inherit the ShaderEffect class. When you use the ShaderEffect class in WPF you only get to communicate with two types of registers (S and C). However they can hold a number of different types, and they don't exactly correspond, in name type, that is. The list of type in HLSL and the correspondent values in .NET are listed below (from this site):
.NET type |
HLSL type |
System.Boolean (C# keyword bool) |
Not Available |
System.Int32 (C# keyword int) |
Not Available |
System.Double (C# keyword double) |
float |
System.Single (C# keyword 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 |
|
You need to set up a dependency property for all the values that you want to interact with. There is however one value that you would always have to set up if you want to use the ShaderEffect class, and that is the Input property:
Inherits ShaderEffect
Public Shared ReadOnly InputProperty As DependencyProperty = ShaderEffect.RegisterPixelShaderSamplerProperty("Input", GetType(ShaderEffectBase), 0, SamplingMode.NearestNeighbor)
Protected Sub New()
Me.UpdateShaderValue(InputProperty)
End Sub
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
It has a custom build dependency property called RegisterPixelShaderSamplerProperty
, which connects to values in the ps file named S0
, S1
... Sn
where n
is the index that is given in the Register of the DependencyProperty, 0
in the above code. If you have more than one Sampler property (ImageBrushes
) a C1
will look like the one blow:
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
In the Effect is corresponds to the ImageBrush
of the object that is going to be treated by this custom effect class.
The second object type that cane be treated by the ShaderEffect
class is the Cn
values, given below as it communicates with the value C0
in the ps file (of float format):
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
Be sure to give the default value (first value) in the PropertyMetaData
a proper format, otherwise it wont recognize it as a float number and give you an error. I typically got this when I had 1 as the default value. I had to type 1.0f
or 1.0d
to make sure it got recognized.
The only thing that is missing to have a complete usable custom shader is the constructor:
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
It basically loading the ps file into the PixelShader, and sends a notification to update the shader effects with the new values. If you ever have the need to update a shader, that is actually possible if you add the code blow to the constructor:
Public Sub New()
...
AddHandler CompositionTarget.Rendering, AddressOf CompositionTarget_Rendering
End Sub
This will start the function CompositionTarger_Rendering
each time the render gets updated on the parent control. You can even adjust the time steps with a short addition of code in the function. This implementation will run once each second (give that the render updates more than once each second):
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
The program made by Rene Schulte uses this approch together with a WritableBitmap to store some random numbers. I used a
BitmapSource directly instead fro convinience.
And that is really all you need to know to implement shaders into a WPF application. In fact you might even think that it should be possible to generate the base code for any class given that you had the source code for the ps file (usually in fx format). It is a fairly limited number of values that needs to be converted into WPF code, and luckily for me, Walt Ritscher has already done so in his
Shazzam Shader Editor. Unfortunatly it did not compile for me, as it is dependent on a dll that is not included in the download. So I decided to strip out the needed bits and implement the code generator in VB.NET using the CodeDome from Shazzam.
His implementation is really a tree stage program, first he uses RegEx to find the type of values, the Default value and the range (if it is given) in the fx files. Secondly he stores the found properties in a new class, and from this he generates the code using CodeDom. The RegEx and the storing class is really straigth forward, at least if you use a handy tool like
Expresso or similar. My problem was that I really coun't find a good explanation for the CodeDome, so I thought I go through the implementation in some detail.
A simple walktrough of CodeDom
CodeDom is a great tool for generation code, and if you avoid using expressions that could only be found in C# or only in VB.NET you have a generic code generating tool without having to resort to a code converter, and that's pretty neat. So, I'll start off from the point that is similar to both code languages in CodeDom. We then start off with the instance
CodeCompileUnit:
Dim codeGraph As New CodeCompileUnit()
Having created the blueprint for the class, we usually need to import some namespaces to our class prior to the actual namespace our class will live in, and wrap it in a function:
Private Function AssignNamespacesToGraph(codeGraph As CodeCompileUnit, namespaceName As String) As CodeNamespace
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)
Dim ns As New CodeNamespace(namespaceName)
codeGraph.Namespaces.Add(ns)
Return ns
End Function
The next step is to actually declare the class with the name that we will use:
Dim shader As New CodeTypeDeclaration() With { _
.Name = shaderModel.GeneratedClassName
}
The class now needs to inherit the ShaderEffect class, and that is done by adding it to the the BaseTypes:
shader.BaseTypes.Add(New CodeTypeReference("ShaderEffect"))
If you wanted to add an interface instead you would do it in exactly the same method:
Dim iequatable As New CodeTypeReference("IEquatable", New CodeTypeReference(shader.Name))
shader.BaseTypes.Add(iequatable)
Just for the completeness you can implement the INotifiedChange logic as done in the following example:
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)
Back to the construction of the ps file ShaderEffect wrapper. We also need to implement the ps file in the constructor of the class, so we add logic for the creation of public constructor:
Dim constructor As New CodeConstructor() With { _
.Attributes = MemberAttributes.[Public]
}
We also need to create a relative Uri path to the location of the ps file, and set this Uri path to the PixelShader that is inherited from the ShaderEffect class:
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}
Then we need to create a new instance of a PixelShader and add the code above to the constructor, and we also add an empty line at the end for some space:
constructor.Statements.AddRange({New CodeAssignStatement() With {.Left = New CodePropertyReferenceExpression(New CodeThisReferenceExpression(), "PixelShader"),
.Right = New CodeObjectCreateExpression(New CodeTypeReference("PixelShader"))},
ConnectUriSource,
New CodeSnippetStatement("")})
In the constructor we also need to update the shader values by the command:
Dim result As New CodeMethodInvokeExpression() With { _
.Method = New CodeMethodReferenceExpression(New CodeThisReferenceExpression(), "UpdateShaderValue")
}
result.Parameters.Add(New CodeVariableReferenceExpression(propertyName & "Property"))
This is done in a function as we need to do this for all the properties found in the ps file.
The creation of DependencyProperties
is done in a function, please not that it only supports values of Cn
as it communicates through the 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
To communicate with the sample register (Sn
) you need a slightly different (and shorter) code:
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
Now its just a matter of looping trough all the properties that one found in the fx file, and generate and add all the code:
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
All the code that is needed to communicate with the ps file through ShaderEffect class in WPF is now included. Creating the VB or C# code is now really simple. This is indeed one of the true magic things with using CodeDom.
Private Function GenerateCode(provider As CodeDomProvider, compileUnit As CodeCompileUnit) As String
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()
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
The CodeDomProvider can either be in VB.NET:
Dim provider As New Microsoft.VisualBasic.VBCodeProvider()
or C#:
Dim provider As New Microsoft.CSharp.CSharpCodeProvider()
Now you can quite simply provide the user with the code that he prefers.
Some advanced tips and tricks
Store a picture of the Effect
To actually store the control with the applied effect, you will have to result to RenderTargetBitmap function. This is a bit upsetting perhaps, as RenderTargetBitmap uses the CPU to proccess the UIElement. You can also get into trouble if you don't do it correctly as shown by Jamie Rodriguez in his blog. The code shown below is originally written by Adam Smith though.
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
Apply multiple effects to one control
When the old (and now outdated function) BitmapEffects were released they came with something called BitmapEffectGroup, which allowed you to apply several effects on one control. However, when the new Effect class that used the GPU instead of the CPU was created, the EffectGroup control was considered too difficult to implement directly.
So if you still want to implement it there are two ways of doing it. The first one, suggested by Greg Schuster here, is to simply wrap a Border around the element you which to add an effect to, and add the second effect to the border as shown below.
<Border>
<Border.Effect>
<local:GaussianEffect/>
</Border.Effect>
<Image>
<Image.Effect>
<local:ScratchedShader></local:ScratchedShader>
</Image.Effect>
</Image>
</Border>
Its fairly straight forward to implement in WPF without doing anything to the fx files.
There is however a better option, at least if you consider the computation time. All the fx files have an entry point function, which means that all of the functions of the different fx files could be united in one new fx file and compiled. You would of course also need to have the variables that WPF gives the input to the ps file with also.
I seems that the XNA framework have some means to load the ps files directly, but that is beond the scope of this article.
Apply effects only to a part of the control
A question that arises quite often is to apply the Effect on a part of an Image or Control. One way to do this is to call the Image.Clip
and add an effect to that.
<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>
However I actually experienced a memory leak while using this, so It might be a bit unstable.
There are two other ways of chopping an element up. One is by adding an effect to a visual:
Public Class CoolDrawing
Inherits FrameworkElement
Implements System.ComponentModel.INotifyPropertyChanged
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
You can apply different effects on different DraingVisuals as you see fit.
And as always there is a possibility to have the fx file itself only work on a small section, as is the case with the magnifier glass that is included. You don not have the opportunity (at least not without considerable work) to manipulate particular sections of the image with this approach.
Points of interest
This wraps up part 1 of the series of adventures into shader effects, the beginning of the journey into shader effects, as this light Shazzam Editor tool will function as a toolbox for testing and debugging shader tools that you have created yourself.
I would also like to point out some of the references used in this article:
Shazzam Shader Editor - a fantastic tool written by Walt Ritscher (program wont run as it is though)
HLSL and Pixel Shaders for XAML Developers by Walt Ritscher
The blog Kodierer [Coder] by Rene Schulte, an MVP. He has some brilliant shadereffects articles for silverlight.
Examples of shaders in action
Beond fanzy pictures