WPF – Experimenting with Multi-Input Pixel Shaders

Whilst my day job continues on its ever-accelerating decline 🙂 I thought I’d entertain myself by experimenting with multi-input pixel shaders in WPF following on from the post I wrote over here.

Now, luckily for me Greg over here has already talked about it and it turns out to be not much of a stretch over what I did previously – you define a 2nd input for your shader of type brush and then you can sample from either input in order to produce the resultant effect.

I took my previously created “Swirl” effect and added a second brush to make a sort of “BlendAndSwirl” effect;

float factor : register(C0);
float blend : register(C1);

sampler2D input : register(S0);
sampler2D input2 : register(S1);

float4 main(float2 uv : TEXCOORD) : COLOR
{
  float angle = -3.14;
  float yDist = uv.y – 0.5;
  float xDist = uv.x – 0.5;
  float dist = sqrt(xDist * xDist + yDist * yDist);
  angle *= dist * factor;

  float xN = cos(angle) * xDist – sin(angle) * yDist;
  float yN = sin(angle) * xDist + cos(angle) * yDist;
  uv.x = xN + 0.5;
  uv.y = yN + 0.5;
  float4 img1Colour = tex2D(input, uv); 
  float4 img2Colour = tex2D(input2, uv);
  img1Colour *= blend;
  img2Colour *= (1.0 – blend);
  img2Colour += img1Colour;
  return(img2Colour);
}

and then wrote the C# to go hand in hand with it;

  public class BlendSwirlEffect : ShaderEffect
  {
    static BlendSwirlEffect()
    {
      _pixelShader.UriSource = Global.MakePackUri("effect1.ps");
    }
    public BlendSwirlEffect()
    {
      this.PixelShader = _pixelShader;

      UpdateShaderValue(InputProperty);
      UpdateShaderValue(Input2Property);
      UpdateShaderValue(SwirlProperty);
    }
    public Brush Input
    {
      get { return (Brush)GetValue(InputProperty); }
      set { SetValue(InputProperty, value); }
    }

    public static readonly DependencyProperty InputProperty =
        ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(BlendSwirlEffect), 0);

    public Brush Input2
    {
      get { return ((Brush)GetValue(Input2Property)); }
      set { SetValue(Input2Property, value); }
    }

    public static readonly DependencyProperty Input2Property =
      ShaderEffect.RegisterPixelShaderSamplerProperty("Input2", typeof(BlendSwirlEffect), 1);

    public double Swirl
    {
      get { return (double)GetValue(SwirlProperty); }
      set { SetValue(SwirlProperty, value); }
    }

    public static readonly DependencyProperty SwirlProperty =
        DependencyProperty.Register("Swirl", typeof(double), typeof(BlendSwirlEffect),
                new UIPropertyMetadata(0.0, PixelShaderConstantCallback(0)));

    public double Blend
    {
      get { return (double)GetValue(BlendProperty); }
      set { SetValue(BlendProperty, value); }
    }

    public static readonly DependencyProperty BlendProperty =
        DependencyProperty.Register("Blend", typeof(double), typeof(BlendSwirlEffect),
                new UIPropertyMetadata(0.0, PixelShaderConstantCallback(1)));

    private static PixelShader _pixelShader = new PixelShader();
  }

 

there’s nothing too magical going on in here – we just have a 2nd brush to play with and sample and then I’ve got 2 factors to control the “level” of Blending of one picture with another and the “factor” applied to the Swirl.

I then built a little UI to use the effect;

  <Grid x:Name="mainGrid">
    <Grid.Resources>
      <ImageBrush
        x:Key="brush1"
        ImageSource="c:\windows\web\wallpaper\img1.jpg"
        Stretch="Fill" />
      <Storyboard
        x:Key="sbAnimate" Storyboard.TargetName="blendEffect" FillBehavior="HoldEnd">
        <DoubleAnimation
          Storyboard.TargetProperty="Blend"
          From="0"
          To="1" 
          BeginTime="00:00:00.5"
          Duration="00:00:00.5"/>
        <DoubleAnimation
          Storyboard.TargetProperty="Swirl"
          From="0"
          To="4" 
          Duration="00:00:01"/>
        <DoubleAnimation
          Storyboard.TargetProperty="Swirl"
          From="4"
          To="0"
          Duration="00:00:01"
          BeginTime="00:00:02"/>
      </Storyboard>
    </Grid.Resources>
    <Grid.RowDefinitions>
      <RowDefinition
        Height="4*" />
      <RowDefinition />
    </Grid.RowDefinitions>
    <Image
      Source="c:\windows\web\wallpaper\img2.jpg"
      Stretch="Fill"
      Margin="10">
      <Image.Effect>
        <local:BlendSwirlEffect
          x:Name="blendEffect"
          Input2="{DynamicResource brush1}"
          Swirl="{Binding ElementName=swirlSlider,Path=Value}"
          Blend="{Binding ElementName=blendSlider,Path=Value}" />
      </Image.Effect>
    </Image>
    <StackPanel Grid.Row="1">
    <Grid 
      HorizontalAlignment="Stretch">
      <Grid.ColumnDefinitions>
        <ColumnDefinition />
        <ColumnDefinition />
      </Grid.ColumnDefinitions>
      <StackPanel
        Margin="10">
        <Slider
          x:Name="blendSlider"
          Minimum="0"
          Maximum="1"
          MinWidth="192"
          Margin="5"/>
        <TextBlock
          FontSize="14"
          Text="Blend"
          TextAlignment="Center" />
      </StackPanel>
      <StackPanel
        Margin="10"
        Grid.Column="1">
        <Slider
          x:Name="swirlSlider"
          Minimum="0"
          Maximum="4"
          MinWidth="192"
          Margin="5"/>
        <TextBlock
        FontSize="14"
        Text="Swirl"
        TextAlignment="Center" />
      </StackPanel>      
    </Grid>
      <Button
        FontSize="14"
        Content="Animate"
        Click="OnAnimate" 
        MinHeight="48"
        MinWidth="192"
        HorizontalAlignment="Center"/>
    </StackPanel>
  </Grid>

 

Which just defines an Image that’s using the BlendSwirlEffect and a second image brush that it’s using as the 2nd image to sample in the pixel shader ( Input2 in the code and the XAML ).

This gives a UI like thi;s

image

where I can then go and blend/swirl a little;

image

and I stuck a little animation on there that animates both properties at the same time. Here’s the project code if you want to play with it – note that to compile it you’ll need the directX SDK so I’ve included the binary files in the project in case you just want to run it.