Thanks for this question. It gave me a chance to learn something new. :)
Your goal is, once you know what you're doing, very easy to achieve. WPF supports the use of GPU shaders to modify images. They are fast at run-time (since they execute in your video card) and easy to apply. And in the case of the stated goal to invert the colors, very easy to implement as well.
To start with, you'll need the shader code. Shaders are written in a language called High Level Shader Language, or HLSL. Here is an HLSL "program" that will invert the input color:
sampler2D input : register(s0);
float4 main(float2 uv : TEXCOORD) : COLOR
{
float4 color = tex2D(input, uv);
float alpha = color.a;
color = 1 - color;
color.a = alpha;
color.rgb *= alpha;
return color;
}
But, Visual Studio doesn't handle this kind of code directly. You'll need to make sure you have the DirectX SDK installed, which will give you the fxc.exe compiler, used to compile shader code.
I compiled the above with this command line:
fxc /T ps_3_0 /E main /Fo<my shader file>.ps <my shader file>.hlsl
Where, of course, you replace <my shader file>
with your actual file name.
(Note: I did this manually, but you can of course create a custom build action in your project to do the same.)
You can then include the .ps
file in your project, setting the "Build Action" to "Resource".
That done, you now need to create the ShaderEffect
class that will use it. That looks like this:
class InvertEffect : ShaderEffect
{
private static readonly PixelShader _shader =
new PixelShader { UriSource = new Uri("pack://application:,,,/<my shader file>.ps") };
public InvertEffect()
{
PixelShader = _shader;
UpdateShaderValue(InputProperty);
}
public Brush Input
{
get { return (Brush)GetValue(InputProperty); }
set { SetValue(InputProperty, value); }
}
public static readonly DependencyProperty InputProperty =
ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(InvertEffect), 0);
}
Key points to the above code:
- You only need one copy of the shader itself. So I initialize this into a
static readonly
field. Since the .ps
file is included as a resource, I can refer to it using the pack:
scheme, as "pack://application:,,,/<my shader file>.ps"
. Again, you will need to replace <my shader file>
with the actual file name, of course.
- In the constructor, you must set the
PixelShader
property to the shader object. You must also call UpdateShaderValue()
to initialize the shader, for each property used as input to the shader (in this case, there's only the one).
- The
Input
property is special: it requires the use of RegisterPixelShaderSamplerProperty()
to register the dependency property.
- If your shader had other parameters, they would be registered normally with
DependencyProperty.Register()
. But they would require a special PropertyChangedCallback
value, obtained by calling ShaderEffect.PixelShaderConstantCallback()
with the register index declared in the shader code for that parameter.
That's all there is to it!
You can use the above in XAML simply by setting a UIElement.Effect
property to an instance of the InvertEffect
class. For example:
<Window x:Class="TestSO45093399PixelShader.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:l="clr-namespace:TestSO45093399PixelShader"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Rectangle Width="100" Height="100">
<Rectangle.Fill>
<LinearGradientBrush>
<GradientStop Color="Black" Offset="0"/>
<GradientStop Color="White" Offset="1"/>
</LinearGradientBrush>
</Rectangle.Fill>
<Rectangle.Effect>
<l:InvertEffect/>
</Rectangle.Effect>
</Rectangle>
</Grid>
</Window>
When you run that, you'll notice that even though the gradient is defined as black in the upper-left transitioning to white in the lower-right, it's displayed the opposite way, with white in the upper-left and black in the lower-right.
Finally, on the off-chance you want to just get this working immediately and don't have access to the fxc.exe compiler, here's a version of the above that has the compiled shader code embedded as Base64. It's tiny, so this is a practical alternative to compiling and including the shader as a resource.
class InvertEffect : ShaderEffect
{
private const string _kshaderAsBase64 =
@"AAP///7/HwBDVEFCHAAAAE8AAAAAA///AQAAABwAAAAAAQAASAAAADAAAAADAAAAAQACADgAAAAA
AAAAaW5wdXQAq6sEAAwAAQABAAEAAAAAAAAAcHNfM18wAE1pY3Jvc29mdCAoUikgSExTTCBTaGFk
ZXIgQ29tcGlsZXIgMTAuMQCrUQAABQAAD6AAAIA/AAAAAAAAAAAAAAAAHwAAAgUAAIAAAAOQHwAA
AgAAAJAACA+gQgAAAwAAD4AAAOSQAAjkoAIAAAMAAAeAAADkgQAAAKAFAAADAAgHgAAA/4AAAOSA
AQAAAgAICIAAAP+A//8AAA==";
private static readonly PixelShader _shader;
static InvertEffect()
{
_shader = new PixelShader();
_shader.SetStreamSource(new MemoryStream(Convert.FromBase64String(_kshaderAsBase64)));
}
public InvertEffect()
{
PixelShader = _shader;
UpdateShaderValue(InputProperty);
}
public Brush Input
{
get { return (Brush)GetValue(InputProperty); }
set { SetValue(InputProperty, value); }
}
public static readonly DependencyProperty InputProperty =
ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(InvertEffect), 0);
}
Finally, I'll note that the link offered in Bradley's comment does have a whole bunch of these kinds of shader-implemented effects. The author of those implemented the HLSL and the ShaderEffect
objects only slightly differently from the way I show here, so if you want to see other examples of effects and different ways to implement them, browsing that code would be a great place to look.
Enjoy!