Controlling a Pixel Shader’s Parameters With Reflection






4.89/5 (9 votes)
Controlling a pixel shader’s parameters with Reflection.
Introduction
Currently, I'm working on an image editor, which will use Pixel Shader effects. To save a lot of time manually hard coding each effect's control interface, I built a simple Reflection based controller system for the effects. This article shows the technique behind it. This is my first article that I publish here on CodeProject.
The Code
The Reflection control is achieved using a few custom attributes. The Reflection code looks for these attributes, and depending on the contained information of the attributes, it builds the user interface.
The UML diagram of the attributes looks like this:
The ShaderVersion
attribute specifies the shader's version. This is necessary because in WPF 4.0, Pixel Shader level 2.0 and 3.0 are both supported, so you need to check if the rendering hardware supports that level or not.
The IValueController
interface specifies a general interface for parameter controlling. Both DoubleCovnverter
and Point2DValueConverter
implement this interface. They are used to specify the effect's parameter minimum, maximum, and default value. Also, they contain a description that will be shown on the user interface. The main Reflection code is placed in a separate user control, which is named EffectOptionDialog
. The XAML is pretty simple for it:
<UserControl x:Class="RefactorShaderControl.EffectOptoonDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" d:DesignHeight="300"
d:DesignWidth="300">
<ScrollViewer>
<StackPanel x:Name="ControlRoot"/>
</ScrollViewer>
</UserControl>
It defines a StackPanel
, encapsulated in a ScrollViewer
. The controls for the shader will be added to this StackPanel
. The code for the control looks like this:
public partial class EffectOptoonDialog : UserControl
{
public EffectOptoonDialog()
{
InitializeComponent();
}
/// <summary>
/// Controlled effect defined as a deoendency property to support data binding
/// </summary>
public static readonly DependencyProperty ControledEffectProperty =
DependencyProperty.Register("ControledEffect",
typeof(ShaderEffect), typeof(EffectOptoonDialog));
/// <summary>
/// Gets or sets the Controlled Effect of this control
/// </summary>
public ShaderEffect ControledEffect
{
get { return (ShaderEffect)GetValue(ControledEffectProperty); }
set
{
SetValue(ControledEffectProperty, value);
Clear();
if (value != null) Build();
}
}
/// <summary>
/// Removes all controlls & collects garbage
/// </summary>
private void Clear()
{
ControlRoot.Children.Clear();
GC.Collect();
}
/// <summary>
/// Builds the user interface
/// </summary>
private void Build()
{
Type effType = ControledEffect.GetType();
bool hwsupport = true;
Attribute[] memberatribs;
Attribute[] effatribs = Attribute.GetCustomAttributes(effType);
// Seerch the class attributes for a ShaderVersion attribute.
// If the Level is not supported by the hardware a messagebox is shown
foreach (Attribute atrib in effatribs)
{
if (atrib is ShaderVersion)
{
ShaderVersion v = (ShaderVersion)atrib;
if (!RenderCapability.IsPixelShaderVersionSupported(v.Major, v.Minor))
{
MessageBox.Show("Pixel shader level " + v.ToString() +
" required by this effect is not supported by your hardware",
"Warning", MessageBoxButton.OK, MessageBoxImage.Error);
hwsupport = false;
}
}
}
// if no hw support found UI construction stopped
if (!hwsupport) return;
MemberInfo[] members = effType.GetMembers();
//label for description
Label descrpt;
//cycle trough all members
foreach (MemberInfo member in members)
{
//only check properties
if (member.MemberType == MemberTypes.Property)
{
memberatribs = Attribute.GetCustomAttributes(member);
//get attributes of the property
foreach (Attribute atr in memberatribs)
{
PropertyInfo pi = effType.GetProperty(member.Name);
//property is a double value
if (atr is DoubleValueContoller)
{
DoubleValueContoller ctrl = (DoubleValueContoller)atr;
//create label with description
descrpt = new Label();
descrpt.Content = ctrl.Description;
ControlRoot.Children.Add(descrpt);
//create a slider for it
//and set it's properties
Slider slider = new Slider();
slider.Minimum = ctrl.Minimum;
slider.Maximum = ctrl.Maximum;
slider.Value = ctrl.Curent;
slider.Margin = new Thickness(10, 0, 0, 0);
//bind the slider to the value
Binding doublebind = new Binding();
doublebind.Source = ControledEffect;
doublebind.Mode = BindingMode.TwoWay;
doublebind.Path = new PropertyPath(pi);
slider.SetBinding(Slider.ValueProperty, doublebind);
ControlRoot.Children.Add(slider);
}
//property is a 2D Point value (float2 in HLSL)
else if (atr is Point2DValueController)
{
Point2DValueController pctrl = (Point2DValueController)atr;
//create label with description
descrpt = new Label();
descrpt.Content = pctrl.Description;
ControlRoot.Children.Add(descrpt);
//create a for it
//and set it's properties
Point2DContol pointc = new Point2DContol();
pointc.Maximum = pctrl.Maximum;
pointc.Minimum = pctrl.Minimum;
pointc.Value = pctrl.Curent;
pointc.Margin = new Thickness(10, 0, 0, 0);
//bind the slider to the value
Binding pointbind = new Binding();
pointbind.Source = ControledEffect;
pointbind.Mode = BindingMode.TwoWay;
pointbind.Path = new PropertyPath(pi);
pointc.SetBinding(Point2DContol.ValueProperty, pointbind);
ControlRoot.Children.Add(pointc);
}
}
}
}
}
}
What It Does
The first thing that this Reflection code does is that it lists the specified Shader
class' attributes. If it finds a ShaderVersion
class among them, then it checks that the level is supported by the Graphics hardware. If it isn't, then it stops the user interface creation, because there's no point going further.
For Pixel Shader level 2.0, there's a software render fallback; however, this can be awfully slow when you run your code on a slow CPU and use large images. For level 3.0, there's no software fallback provided.
After this, the code looks for properties in the Shader
class that have a DoubleValueController
or a Point2DValueController
attribute attached to them. If it finds one, then it creates a control for the property and binds the Shader
's property to the Control
's value. I use two way binding, because this way, if you change one of the controlled values from the code, it will be reflected on the GUI.
An important thing to keep in mind is that you can only use bindings on Dependency Properties. So if you create custom controls, always implement their properties as Dependency Properties.
Using the Code
The control can be used like this:
BloomEffect eff = new BloomEffect();
EffectTarget.Effect = eff;
EffectOptions.ControledEffect = eff;
Building Your Own Value Controllers
For other pixel shader property types (float3
and float4
, which are mapped as Point3D
and Color
classes by .NET), you need to build your own controller, and then you can control every kind of shader based on this code.
A good starting example may be Point2DControl
.
<UserControl x:Name="Point2D"
x:Class="RefactorShaderControl.Controls.Point2DContol"
xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation
xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml
xmlns:mc=http://schemas.openxmlformats.org/markup-compatibility/2006
xmlns:d=http://schemas.microsoft.com/expression/blend/2008
mc:Ignorable="d" Height="50" d:DesignWidth="300">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Label Content="X:" Grid.Column="0" Grid.Row="0"/>
<Label Content="Y:" Grid.Column="0" Grid.Row="1"/>
<Slider x:Name="XValue" ValueChanged="ValueChanged"
Grid.Row="0" Grid.Column="1"/>
<Slider x:Name="YValue" ValueChanged="ValueChanged"
Grid.Row="1" Grid.Column="1"/>
</Grid>
</UserControl>
public partial class Point2DContol : UserControl
{
bool exec;
public Point2DContol()
{
InitializeComponent();
Maximum = new Size(10, 10);
Minimum = new Size(0, 0);
Value = new Size(0, 0);
exec = true;
}
public static readonly DependencyProperty MinimumProperty =
DependencyProperty.Register("Minimum", typeof(Size), typeof(Point2DContol));
public static readonly DependencyProperty MaximumProperty =
DependencyProperty.Register("Maximum", typeof(Size), typeof(Point2DContol));
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(Size), typeof(Point2DContol));
/// <summary>
/// Control Minimum value
/// </summary>
public Size Minimum
{
get { return (Size)GetValue(MinimumProperty); }
set
{
SetValue(MinimumProperty, value);
XValue.Minimum = value.Width;
YValue.Minimum = value.Height;
}
}
/// <summary>
/// Control maximum value
/// </summary>
public Size Maximum
{
get { return (Size)GetValue(MaximumProperty); }
set
{
SetValue(MaximumProperty, value);
XValue.Maximum = value.Width;
YValue.Maximum = value.Height;
}
}
/// <summary>
/// Control value
/// </summary>
public Size Value
{
get { return (Size)GetValue(ValueProperty); }
set
{
SetValue(ValueProperty, value);
if (exec)
{
XValue.Value = value.Width;
YValue.Value = value.Height;
}
else exec = true;
}
}
private void ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
exec = false;
Value = new Size(XValue.Value, YValue.Value);
}
}
This is a really simple control; it encapsulates two slider controls, which control the value of the X and Y axis (width and height). From the sample, you can see that each slider's value is set from the code instead of binding. This is done this way because I couldn't get it to work the other way. The exec
variable used in the ValueChanged
event and Value
setting code is used to prevent stack overflow from circular reference. I know it's not the prettiest way to do this, but I'm not an expert on WPF yet.
The Applciation
The finished application has three effects, and it looks like this:
It has three effects: two color effects and a distort. The channel mixer was created by me, while the others are simply taken from the Shazzam tools library. The source code also contains the source code for these effects. I used only Pixel Shader level 2.0 effects to maximize compatibility. The image used in the program is taken near the place where I live.
Points of Interest
- For learning about pixel shaders, I recommend Ren© Schulte's introduction to Silverlight and WPF pixel shaders. This excellent article can be found here.
- The Shazzam tool, which is a professional tool for creating pixel shaders for WPF and Silverlight: http://shazzam-tool.com/.
- If you are not quite familiar with Reflection in C#, then the following CodeProject article is a good starting point: http://www.codeproject.com/KB/cs/introreflection.aspx.
- MSDN page for using Reflection in C#: http://msdn.microsoft.com/en-us/library/ms173183%28VS.80%29.aspx.
- MSDN complete reference of HLSL instructions: http://msdn.microsoft.com/en-us/library/ff471376%28v=VS.85%29.aspx.
- http://blogs.msdn.com/b/coding4fun/archive/2010/05/25/10014965.aspx.