Automatically grayed out Images in WPF – Version 2

Created with Sketch.

This post describes a new, improved versions of the AutoGrayableImage introduced in this blog:

https://www.engineeringsolutions.de/wp-admin/post.php?post=334

Why?

There were some issues with the original version which made it hard to use in some scenarios. The new version will also include performance improvements and will enable us to use the same approach not only for images, but for any WPF control.

How?

In general, we will create a WPF ShaderEffect, which can be used as effect for any WPF control, similar to the DropShadowEffect or BlurEffect, which are built in into WPF.

The advantage of this approach is that the effect is directly applied by the graphics engine, which means it is very fast. Loading or conversion of the image is not required anymore.

GrayscaleEffect Class

To start, we will first create the GrayscaleEffect class derived from ShaderEffect:

  public class GrayscaleEffect : ShaderEffect
  {
    public GrayscaleEffect()
    {
      PixelShader = CreatePixelShader();
      UpdateShaderValue(InputProperty);
      UpdateShaderValue(SaturationFactorProperty);
    }

    /// <summary>
    /// Dependency property for Input
    /// </summary>
    public static readonly DependencyProperty InputProperty = RegisterPixelShaderSamplerProperty(nameof(Input), typeof(GrayscaleEffect), 0);

    /// <summary>
    /// Implicit input
    /// </summary>
    public Brush Input
    {
      get => (Brush)GetValue(InputProperty);
      set => SetValue(InputProperty, value);
    }

    /// <summary>
    /// Dependency property for saturation factor
    /// </summary>
    public static readonly DependencyProperty SaturationFactorProperty = DependencyProperty.Register(nameof(SaturationFactor), typeof(double), typeof(GrayscaleEffect), new UIPropertyMetadata(0.0, PixelShaderConstantCallback(0), CoerceSaturationFactor));

    public double SaturationFactor
    {
      get => (double)GetValue(SaturationFactorProperty);
      set => SetValue(SaturationFactorProperty, value);
    }

    private static object CoerceSaturationFactor(DependencyObject d, object value)
    {
      var effect = (GrayscaleEffect)d;
      double newFactor = (double)value;

      return newFactor < 0.0 || newFactor > 1.0 ? effect.SaturationFactor : (object)newFactor;
    }

    private PixelShader CreatePixelShader()
    {
      var pixelShader = new PixelShader();

      if (DesignerProperties.GetIsInDesignMode(this) == false)
      {
        pixelShader.UriSource = new Uri("pack://application:,,,/ES.WPF;component/Effects/Grayscale.ps", UriKind.Absolute);
      }

      return pixelShader;
    }
  }

This class contains two dependency properties: Input and SaturationFactor.

SaturationFactor is a factor that controls the saturation of the final image. Keeping the default value is fine for using the effect.

The Input DependencyProperty represents the input brush that is used to draw the element the effect is attached to. It is registered using the specialized RegisterPixelShaderSamplerProperty method. The third attibute (0) means that the content will be pushed to the sampler register 0. We’ll come to that later.

One more thing I need to mention is the CreatePixelShader method which will create the actual PixelShader object. First, it will be skipped if the control is displayed in the Visual Studio WPF designer. This could affect the stability and performance of the designer.

Finally, the PixelShader is created from a URI. This URI needs to point to the location of the pixel shader file (Grayscale.ps). You can download the complete pixel shader file here: Grayscale.cs, but I will also explain how you can create your own shader effect. Any kind of fancy effects can be accomplished by using pixel shaders.

Using GrayscaleEffect

First, I will explain how to use the GrayscaleEffect in your WPF project.

For that I have created the following simple button:

<Button Width="100" Height="50">
  <Image Source="/CMS.Config;component/Config.ico"/>
</Button>

The Result looks like this:

By applying the GrayscaleEffect, the whole button turns into a grayscale representation:

<Button Width="100" Height="50">
  <Button.Effect>
    <local:GrayscaleEffect/>
  </Button.Effect>
  <Image Source="/CMS.Config;component/Config.ico"/>
</Button>

Not only the image has changed into gray scale, but the whole button.

Pixel Shader Source Code

This is the content of Grayscale.fx. The Grayscale.cs is the compiled version of that file, which is uploaded to the graphics engine and applied. You just need to use this and modify it, if you want to change the behavior. Once it is compiled, you don’t need the source anymore.

/////////////////////////////////////////////////////////////////////////////////
//                                                                             //
//   An effect that turns the input into shades of gray.                       //
//                                                                             //
//   This file is needed to create the Grayscale.ps pixel shader file used     //
//   in GrayscaleEffect.cs. It's just stored here for completeness. The        //
//   actual effect uses the compiled .ps file.                                 //
//                                                                             //
//   Use the following command to generate the pixel shader file:              //
//                                                                             //
//   fxc /T ps_2_0 /E main / Fo"C:\temp\Grayscale.ps" "c:\temp\Grayscale.fx"   //
//   fxc.exe is located under                                                  //
//                                                                             //
//   C:\Program Files (x86)\Windows Kits\10\bin\10.0.15063.0\x86               //
//                                                                             //
/////////////////////////////////////////////////////////////////////////////////

//-----------------------------------------------------------------------------------------
// Shader constant register mappings (scalars - float, double, Point, Color, Point3D, etc.)
//-----------------------------------------------------------------------------------------

//--------------------------------------------------------------------------------------
// Saturation Factor (default: 0)
//--------------------------------------------------------------------------------------
float factor : register(c0);

//--------------------------------------------------------------------------------------
// Sampler Inputs (Brushes, including ImplicitInput)
//--------------------------------------------------------------------------------------
sampler2D implicitInput : register(s0);

//--------------------------------------------------------------------------------------
// Pixel Shader
//--------------------------------------------------------------------------------------
float4 main(float2 uv : TEXCOORD) : COLOR
{
    float4 color = tex2D(implicitInput, uv);
    float gray = color.r * 0.3 + color.g * 0.59 + color.b *0.11;

    float4 result;
    result.r = (color.r - gray) * factor + gray;
    result.g = (color.g - gray) * factor + gray;
    result.b = (color.b - gray) * factor + gray;
    result.a = color.a;

    return result;
}

The comment block above explains how you can compile the .fx file to a .ps file. To do so, you will need to dowload the Windows SDK.

This is not really C++ code, but similar enough to understand it. The register() method is used to take the information from the given registers and store them in local variables. The main() method does the conversion of a single pixel (but for all pixels of the Input). In this example, it will just convert the RGB components from the pixel and convert it into a grayscale representation.

AutoGrayableImage

Finally, we will use the new Effect in the new and improved AutoGrayableImage class:

  public class AutoGrayableImage : Image
  {
    private double _storedOpacity = 1.0;
    private static readonly Effect _grayscaleEffect = new GrayscaleEffect();

    /// <summary>
    /// Overwritten to handle changes of IsEnabled, Source and OpacityMask properties
    /// </summary>
    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
      base.OnPropertyChanged(e);

      if (e.Property.Name.Equals(nameof(IsEnabled)))
      {
        if (IsEnabled)
        {
          Opacity = _storedOpacity;
          Effect = null;
        }
        else
        {
          _storedOpacity = Opacity;
          Opacity = 0.5;
          Effect = _grayscaleEffect;
        }
      }
    }
  }

This is very simple, we will just set the effect when the IsEnabled property of the image is set to false and remove it again when it is set to true. Also changing the opacity will make the image look lighter when displayed in front of a white background.

 

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.