Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Controlling a Pixel Shader’s Parameters With Reflection

0.00/5 (No votes)
27 Nov 2010 1  
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:

Attributes.png

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.

XAML:
<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> 
C# code:
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:

screenshot.jpg

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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here