Last Updated 2011/12/03
ビットマップ効果のページで説明したとおり、エフェクトファイル(.fx ファイル)は、 HLSL "High Level Shading Language" を使って作成します。このページでは HLSL の基本とできるだけ実用的なエフェクトファイルのサンプルコードを紹介します。
HLSL のリファレンスマニュアルは、Visual Studio 2010 のヘルプの中にありますが、残念ながら英文です。Microsoft のサイトにいくと日本語版が公開されています。ただし、このリファレンスはかなり不親切です。少なくとも、理解しやすくしようとする意欲を感じません。また、.NET Framework を対象としたものではないので、.NET Framework がサポートする範囲を超えています。どこまで使えるかの判断はかなり難しいと思います。要するに、やってみなければ分からないというレベルです。
さて、私はHLSL コードをテストするツールを公開しています。HlslPad と呼びますが、tools のページにあります。まず、これをダウンロードしておいてください。これから紹介する HLSL のサンプルコードはこの HlslPad を使うことを前提としているからです。
CodePlex の次のサイトにおいて、エフェクトファイルのサンプルコードが公開されていますので、紹介しておきます。
Windows Presentation Foundation Pixel Shader Effects Library
http://wpffx.codeplex.com/releases/view/25285
ただし、私の環境では正常にコンパイルできませんでした。何か手順を間違えたのかもしれませんが、ソースコードを見るだけなら困らないので、そのままにしています。
ShaderEffectLibrary は Microsoft 公認(?)のサンプルコードですが、内容としてはどうかなという感じです。ともあれ、HLSL に関係する手がかりが少ないので、役には立ちます。
これから紹介するサンプルコードは、この ShaderEffectLibrary から多くのヒントを得ています。ただし、私なりに手を入れています。
HLSL のリファレンスはいずれ WPFClass に盛り込む予定です。したがって、詳しくはそれを参照してもらうとして、ここでは HLSL および Shader に関係する基本的な事項について説明します。
HLSL を使って作成するエフェクトファイル(.fx ファイル)の文字コードは、ASCII でなければなりません。ただし、英数字と記号に限っては shift-jis とまったく同じですから shift-jis のテキストファイルとして作成することができます。
Visual Studio の [プロジェクトに追加] でテキストファイルを追加すると、このファイルの文字モードは UTF-8 になりますから注意してください。なお、漢字などの日本語は使えません。
.NET Framework 3.5 SP1 に System.Windows.Media.Effects.Effect クラスが追加されました。それにともなって、UIElement クラスに Effect プロパティが追加されました。さらに、.NET Framework 4.0 の UIElement クラスから BitmapEffect プロパティが削除されました。Microsoft はどうでもこうでも Shader を使わせたいということなのでしょうか。すでに存在する機能を削除することはないだろうと思わないでもありません。
ともあれ、Effect クラスから派生する次の 2 つのクラスが追加されました。
下図の左 2 つは BlurEffect クラスの例で、左端の図はオリジナル、中央が BlurEffect を適用したところです。右端のボタンコントロールは DropShadowEffect を適用したところです。
![]()  | ![]()  | 
どうしてこの 2 つだけが標準のクラスとして提供されたのかは分かりません。さらに追加があると期待していいのでしょうか。
ShaderEffect クラスの次の 4 つのプロパティについて説明します。
Shader は操作対象の UIElement オブジェクトに対して適用することになりますが、適用範囲は UIElement オブジェクトの境界内に限られます。しかし、これらのプロパティを利用することで対象のオブジェクトの境界外に Shader を適用することが可能になります。つまり、前の項で紹介した DropShadowEffect のような操作が可能になるということです。
これらのプロパティの使い方がどこにも書いていないので、ウロウロしてしまいますが、オボロゲながら分かってきました。下図は、ボタンコントロールに Shader を適用したところですが、ボタンコントロールの境界の外に効果が波及していることが分かります。というか、System.Windows.Media.Effects.OuterGlowBitmapEffect クラスのような効果をねらってみました。
これを実現するには、ShaderEffect クラスの DdxUvDdyUvRegisterIndex プロパティを使わなければなりません。具体的なコードは、「単一入力サンプラの例」-「OuterGlow」を参照してください。
DirectX には以下にリストアップする 3 種類の Shader があります。しかし、.NET Framework は PixelShader しかサポートしません。
Shader Model は、Shader のバージョンと考えていいと思います。.NET Framework 4.0 では Shader Model の 2.0 と 3.0 の一部をサポートします。HLSL のコードによっては PS3.0 でないとコンパイルできない場合がありますが、最初は PS2.0 でコンパイルし、ダメなら PS3.0 でコンパイルするほうをすすめます。
多くの場合、入力サンプラーは一つです。サンプラー "sampler" は、サンプリングといえば理解しやすいと思いますが、日本語では「抽出」ぐらいでしょうか。Shader は UIElement クラスの Effect プロパティに設定することになりますが、これは UIElement オブジェクトを .NET Framework(実態は DirectX)が描画するときに、描画するピクセルごとにデータ(ピクセル色とその位置)として抽出し、Shader を適用します。
次のコードは、受け取ったデータ(カラー値)のアルファ値に factor を乗算し、操作対象の UIElement オブジェクトの透明度を変更して(ビットマップ効果をほどこして)描画します。.NET Framework 対応のエフェクトファイル(,fx ファイル)の基本的なパターンです。以降の解説のために用意しました。
sampler2D input : register(S0); ← 2D サンプラ変数の宣言で、サンプラレジスタ S0 として登録する
float factor : register(C0);    ← 定数レジスタ C0 にグローバル変数 factor を登録する
float4 main(float2 uv : TEXCOORD) : COLOR ← 2 次元座標値を受け取り、COLOR 型として返すメイン関数
{
  float4 color = tex2D(input, uv); ← 2D テクスチャから座標値に対応する COLOR 型を取得する
  color.a *= factor;
  return color;
}
上記のコード中の uv は、UIElement オブジェクトの左上隅を原点とする座標値を返します。この正体を明らかにするために HLSL コードを書いてみました。その実行結果が下図です。
sampler2D input : register(S0);
float4 main(float2 uv : TEXCOORD) : COLOR
{
  float4 color = tex2D(input, uv);
  if ((uv.x < 0.10) &&(uv.y < 0.10))
    color.rgb = float4(1,0,0,1); // red
  else if ((uv.x > 0.90) &&(uv.y < 0.10))
    color.rgb = float4(0,1,0,1); // green
  else if ((uv.x > 0.90) &&(uv.y > 0.9))
    color.rgb = float4(0,0,1,1); // blue
  else if ((uv.x < 0.10) &&(uv.y > 0.9))
    color.rgb = float4(1,1,0,1); // yellow
  else
    color.rgb = float4(0.5,0.5,0.5,1); // silver
  return color;
}
UIElement オブジェクトの左上隅の座標値は (0.0, 0.0)、右下隅の座標値は (1.0, 1.0) です。このような座標系を Shader の世界では、テクスチャ座標系と呼びます。なお、論理ピクセル単位の数値をテクスチャ座標系の値に変換する手順は、OuterGlow のサンプルをご覧ください。
上記のコードの先頭の行の S0 は、インデックス 0 のサンプラーレジスタであることを示します。これは、ピクセルシェーダーファイル(.ps ファイル)と対応する分離コードの中で、ShaderEffect クラスの RegisterPixelShaderSamplerProperty メソッドを使って宣言します。
上記のコードの factor は、インデックス 0 の定数レジスタに登録しています。これは、ピクセルシェーダーファイル(.ps ファイル)と対応する分離コードの中で、DependencyProperty を宣言するときに ShaderEffect クラスの PixelShaderConstantCallback メソッドを使って宣言します。
定数レジスタに登録した変数は、エフェクトファイル内では const 宣言と同じ扱いになりますが、ShaderEffect クラスから派生するクラス内のプロパティを通じて変更することができます
上記のコードのメイン関数名は main になっています。関数名は任意の名前が可能ですが、HlslPad を使う場合は main のみが有効です。
上記のコード中の、TEXCOORD および COLOR をセマンティクス "semantics" と呼びます。HLSL には TEXCOORD 型とか COLOR 型というものはありません。float2 型とか float4 型では何を意味するかが分かりませんので、セマンティクスを付加することで、データ型の意味を明らかにする役割を持っています。
TEXCOORD セマンティクスにはプロパティのようなものがあります。x、y、xy です。つまり、uv.x、uv.y、uv.xy のような使い方が可能です。同様に、COLOR セマンティクスには r、g、b、a プロパティがあります。color.a は対応する float4 型の 4 番目の float 型がアルファ値であることを示します。また、color.rgb はアルファ値以外の値を示します。
.NET Framework がサポートする PixelShader ではセマンティクスとして TEXCOORD と COLOR しかないと思いますが、確実ではありません。
なお、セマンティクスは大文字と小文字との区別はありませんので、すべて小文字の texcoord や Texcoord も可能です。ただし、セマンティクスであることを明示する意味ですべて大文字にするのはいい習慣だと思います。
下図は、HlslPad を起動したところです。使い方は ZIP ファイルに同梱の readme.txt に書いておきましたので、それを参照してください。
フォーム右下のスライダコントロールは定数レジスタの C0 と C1 とに対応しています。グローバル変数がこれより多い場合の手順についてはサンプルコードの中で説明します。
PixelShader はほとんどの場合、入力サンプラは一つです。なお、サンプルコードは、HlslPad を使うことを前提として作成していますので、コードを HlslPad のほうにコピーしてテストしてください。
しきい値 threshold より明るい色だけを維持し、そのほかの色を暗くします。グローバル変数の threshold を 0.0 〜 1.0 の範囲で変化させると明暗がよりクッキリします。C0 のスライダーコントロールを操作してみてください。ちなみに、下図は 0.25 の場合です。なお、このサンプルでは saturate 関数を使っています。

sampler2D input : register(S0);
float threshold : register(C0);
float4 main(float2 uv : TEXCOORD) : COLOR
{
  float4 color = tex2D(input, uv);
  return saturate((color - threshold) / (1 - threshold));
}
カラー値が指定の値より小さいピクセルだけを透明にします。グローバル変数の threshold は 0.0 〜 1.0 の範囲の数値を指定します、下図は 0.3 の場合です。C0 のスライダーコントロールを操作してください。

sampler2D  input : register(S0);
float threshold : register(C0);
float4 main(float2 uv : TEXCOORD) : COLOR
{
  float4 color = tex2D(input, uv);
  if (color.r + color.g + color.b < threshold)
  {
    color.rgba = 0;
  }
  return color;
}
赤っぽくとか、青みがちなどに色調を変えます。下図は、C1 のスライダーコントロールを 0.5 にした場合です。なお、この例では C2 に対応するスライダーコントロールが用意されていませんので、ローカル変数として blue = 0.5 を設定しています。手入力は必要ですが、定数レジスタが多い場合はこの手順にならってください。
sampler2D input : register(S0);
float red   : register(C0);
float green : register(C1);
//float blue  : register(C2);
float4 main(float2 uv : TEXCOORD) : COLOR
{
  float blue = 0.5;
  float4 color = tex2D(input, uv);
  color.r += red * color.r;
  color.g += green * color.g;
  color.b += blue * color.b;
  return color;
}
カラー画像をグレースケール化する手順はいろいろあるようですが、ここではもっとも伝統的で簡単な計算式を採用しました。ただし、完全なグレースケールではありません。グレーに見えますが、実態はカラー画像です。
sampler2D input : register(S0);
float4 main(float2 uv : TEXCOORD) : COLOR
{
  float4 color = tex2D(input, uv);
  float4 gray = color.r * 0.299 + color.g * 0.587 + color.b * 0.114;
  gray.a = 1.0;
  return gray;
}
反転色に変換します。
sampler2D input : register(S0);
float4 main(float2 uv : TEXCOORD) : COLOR
{
  float4 color = tex2D(input, uv);
  float4 inverted_color = 1 - color;
  inverted_color.a = color.a;
  inverted_color.rgb *= inverted_color.a;
  return inverted_color;
}
モザイクとは下図を見てのとおりです。モザイクのサイズを 0.0 〜 1.0 の範囲の数値で指定すると、内部的に数値を 100 倍しています。HlslPad のスライダーコントロールは常に 0.0 〜 1.0 の範囲内の数値を設定しますので、その範囲の数値では不都合な場合にこの例の手順にならってください。なお、このサンプルでは、floor 関数を使っています。
sampler2D input: register(s0);
float size; // 40 is preferable
float4 main(float2 uv : TEXCOORD) : COLOR
{
  float size1 = size * 100;
  float2 uv1 = floor(uv * size1 + 0.5) / size1;
  return tex2D(input, uv1);
}
モザイクの基本的な手順は、ピクセルの一定の範囲を同じピクセル色で塗りつぶすことにあります。上記のコードでは size で指定した範囲の座標値を同じ一つの座標値を参照するように、つまり、同じピクセル色を参照するようにしています。
コード中の 0.5 はいくらでもいいのですが、これがないと画像がずれるので、その調整のためにいれています。
OuterGlowBitmapEffect クラスのようなエフェクト効果を考えてみました。下図は、Button コントロールに対して Shader を適用していますが、Button コントロールの境界外に効果が及んでいることが分かると思います。もちろん、Button コントロールは通常のボタンとして使えます。なお、このサンプルでは、ピクセル単位で指定したサイズをテクスチャ座標値に変換する手順を含みます。
(PaddingEffect.fx)
sampler2D Input  : register(S0);
float4 GlowColor : register(C0);
float  GlowSize  : register(C1);
float  Opacity   : register(C2);
float4 DdxDdy    : register(C6);
float4 main(float2 uv : TEXCOORD) : COLOR
{
  float4 color = tex2D(Input, uv);
  float glowSizeX = GlowSize * length(DdxDdy.xy);
  float glowSizeY = GlowSize * length(DdxDdy.zw);
  if ((uv.y < glowSizeY) || (uv.y > (1 - glowSizeY)) || (uv.x < glowSizeX) || (uv.x > (1 - glowSizeX)))
  {
    color.rgb = GlowColor.rgb;
    color.a = Opacity;
  }
  return color;
}
(PaddingEffect.cs)
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Effects;
namespace PaddingEffectTest
{
  public class PaddingEffect : ShaderEffect
  {
    static PropertyChangedCallback GlowSizeRegisterCallback = ShaderEffect.PixelShaderConstantCallback(1);
    public static readonly DependencyProperty InputProperty = ShaderEffect.RegisterPixelShaderSamplerProperty(
        "Input", typeof(PaddingEffect), 0);
    public static readonly DependencyProperty GlowColorProperty = DependencyProperty.Register(
        "GlowColor", typeof(Color), typeof(PaddingEffect), new UIPropertyMetadata(Colors.White, PixelShaderConstantCallback(0)));
    public static readonly DependencyProperty GlowSizeProperty = DependencyProperty.Register(
        "GlowSize", typeof(double), typeof(PaddingEffect), new UIPropertyMetadata(20.0, OnGlowSizeChanged));
    public static readonly DependencyProperty OpacityProperty = DependencyProperty.Register(
        "Opacity", typeof(double), typeof(PaddingEffect), new UIPropertyMetadata(1.0, PixelShaderConstantCallback(2)));
    public Brush Input
    {
      get { return (Brush)GetValue(InputProperty); }
      set { SetValue(InputProperty, value); }
    }
    public Color GlowColor
    {
      get { return (Color)GetValue(GlowColorProperty); }
      set { SetValue(GlowColorProperty, value); }
    }
    public double GlowSize
    {
      get { return (double)GetValue(GlowSizeProperty); }
      set { SetValue(GlowSizeProperty, value); }
    }
    public double Opacity
    {
      get { return (double)GetValue(OpacityProperty); }
      set { SetValue(OpacityProperty, value); }
    }
    //---------------------------------------------------------------------------------------------
    // コンストラクタ
    public PaddingEffect()
    {
      System.Reflection.Assembly a = typeof(PaddingEffect).Assembly;
      string assemblyName = a.GetName().Name;
      string uri = "pack://application:,,,/" + assemblyName + ";component/effects/PaddingEffect.ps";
      var pixelShader = new PixelShader();
      try
      {
        pixelShader.UriSource = new Uri(uri, UriKind.RelativeOrAbsolute);
      }
      catch (Exception ex)
      {
        MessageBox.Show(ex.Message);
      }
      this.PixelShader = pixelShader;
      // 定数レジスタ 6 に座標変換係数を設定する(6 は適当に決めたもので、定数レジスタの空き番号でよい)
      this.DdxUvDdyUvRegisterIndex = 6;
      UpdateShaderValue(InputProperty);
      UpdateShaderValue(GlowColorProperty);
      UpdateShaderValue(GlowSizeProperty);
      UpdateShaderValue(OpacityProperty);
    }
    static void OnGlowSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      GlowSizeRegisterCallback(d, e);
      var obj = d as PaddingEffect;
      if (obj != null)
        obj.OnGlowSizeChanged((double)e.OldValue, (double)e.NewValue);
    }
    protected virtual void OnGlowSizeChanged(double oldValue, double newValue)
    {
      this.PaddingLeft = newValue;
      this.PaddingTop = newValue;
      this.PaddingRight = newValue;
      this.PaddingBottom = newValue;
    }
  } // end of PaddingEffect class
} // end of namespace
(MainWindow.xaml)
<Window x:Class="PaddingEffectTest.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:PaddingEffectTest"
    Title="Padding のテスト" Height="350" Width="525">
  <StackPanel Orientation="Horizontal">
    <Border Width="300" Height="200" Margin="20" Background="AliceBlue">
      <Button Width="140" Height="30" Margin="20" Content="Padding Test">
        <Button.Effect>
          <local:PaddingEffect GlowColor="Red" GlowSize="10" Opacity="0.2" />
        </Button.Effect>
      </Button>
    </Border>
  </StackPanel>
</Window>
グレースケールと同じ手順ですが、セピア色風に色を調整します。rgb 値に指定した係数を変更すれば色合いを変化させることができます。
sampler2D input : register(s0);
float4 main(float2 uv : TEXCOORD) : COLOR
{
  float4 color = tex2D(input, uv.xy);
  float gray = color.r * 0.299 + color.g * 0.587+ color.b * 0.114;
  color.r = 0.8 * gray;
  color.g = 0.6 * gray;
  color.b = 0.4 * gray;
  return color;
}
step 関数をテストする過程で、チョットおもしろい Shader を考えてみました。gray はピクセル色の明るさと考えてください。ピクセルの赤成分の値と gray とを比較し、赤成分のほうが大きい場合はそのままの色に、その逆の場合は赤の成分を 0 にします。なお、このサンプルでは、step 関数を使っています。
次のコードのうち、画像によってはコメントアウトしたコードを生かすと異なる効果が見られます。コードを編集して試してください。
sampler2D input : register(S0);
float4 main(float2 uv : TEXCOORD) : COLOR
{
  float4 color = tex2D(input, uv.xy);
  float gray = color.r * 0.299 + color.g * 0.587 + color.b * 0.114;
  color.r *= step(color.r, gray);
//  color.g *= step(color.g, gray);
//  color.b *= step(color.b, gray);
  return color;
}
グローバル変数の factor は透明度で、0.0 〜 1.0 の範囲の数値です。1.0 のとき、完全な不透明、0.0 のとき、完全な透明となります。スライダーコントロールを使って factor を変更することで透明度を調整することが可能です。なお、動作内容は想像できるでしょうから図は示しません。
sampler2D input : register(S0);
float factor : register(C0);
float4 main(float2 uv : TEXCOORD) : COLOR
{
  float4 color = tex2D(input, uv);
  color.a *= factor;
  return color;
}
Wave としましたが、画像のゆがみというほうが適切でしょう。下図は、frequency に 0.3、amount に 0.5 を設定したところです。
sampler2D input : register(S0);
float frequency : register(C0); // 0.3
float amount : register(C1); // 0.5
float4 main(float2 uv : TEXCOORD) : COLOR
{
  float frequency1 = frequency * 100;
  float amount1 = amount / 10;
  uv.x += sin(uv.y * frequency1) * amount1;
  float4 c = tex2D(input,uv);
  return c;
}
画像を拡大します。下図は magnifier に 0.7、offset に 0.1 を指定したところです。
sampler2D input : register(S0);
float magnifier : register(C0); // 0.7
float offset : register(C1); // 0.1
float4 main(float2 uv : TEXCOORD) : COLOR
{
  uv.xy *= magnifier;
  uv.xy += offset;
  float4 c = tex2D(input, uv);
  return c;
}
入力サンプラーを複数にすることができます。というのは、Effect クラスの ImplicitInput プロパティを見ると分かりますが、このプロパティは Brush オブジェクトだからです。つまり、Brush クラスから派生するクラスを設定できます。画像であれば ImageBrush オブジェクトを使用します。
2 つの画像を水平にスライドすることで入れ替えます。下図は、花の画像と海の画像とをスライダコントロールを使って 30 % 入れ替えたところです。なお、画像ファイルのサイズは同一である必要はありません。

(MultiSlide.fx)
sampler2D Input0 : register(S0);
sampler2D Input1 : register(S1);
float LocationX  : register(C0);
float4 main(float2 uv : TEXCOORD) : COLOR
{
  float4 color;
  float4 color0 = tex2D(Input0, uv);
  float4 color1 = tex2D(Input1, uv);
  if (uv.x < LocationX)
    color = color0;
  else
    color = color1;
  return color;
}
(MainWindow.xaml)
<Window x:Class="MultiSlideTest.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MultiSlideTest"
    Title="Multi Input Effect" Height="350" Width="525">
  <Window.Resources>
    <ImageBrush x:Key="imageBrush" ImageSource="images/FrangipaniFlowers.jpg" />
  </Window.Resources>
  <DockPanel>
    <StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom" Margin="20,10,20,10">
      <TextBlock Text="LocationX : " Margin="0,0,10,0" />
      <Slider Name="slider" Minimum="0.0" Maximum="1.0" SmallChange="0.1" Width="200" Value="0.3" />
      <TextBlock Text="{Binding ElementName=slider, Path=Value}" Margin="10,0,0,0" />
    </StackPanel>
    <Image Name="image" Source="images/azul.jpg" Margin="10">
      <Image.Effect>
        <local:MultiSlideEffect Input1="{StaticResource imageBrush}"
                                LocationX="{Binding ElementName=slider, Path=Value}" />
      </Image.Effect>
    </Image>
  </DockPanel>
</Window>
(MultiSlide.cs)
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Effects;
namespace MultiSlideTest
{
  public class MultiSlideEffect : ShaderEffect
  {
    public static readonly DependencyProperty Input0Property = ShaderEffect.RegisterPixelShaderSamplerProperty(
        "Input0", typeof(MultiSlideEffect), 0);
    public static readonly DependencyProperty Input1Property = ShaderEffect.RegisterPixelShaderSamplerProperty(
      "Input1", typeof(MultiSlideEffect), 1);
    public static readonly DependencyProperty LocationXProperty = DependencyProperty.Register(
      "LocationX", typeof(double), typeof(MultiSlideEffect), new UIPropertyMetadata(0.0, PixelShaderConstantCallback(0)));
    public Brush Input0
    {
      get { return (Brush)GetValue(Input0Property); }
      set { SetValue(Input0Property, value); }
    }
    public Brush Input1
    {
      get { return (Brush)GetValue(Input1Property); }
      set { SetValue(Input1Property, value); }
    }
    public double LocationX
    {
      get { return (double)GetValue(LocationXProperty); }
      set { SetValue(LocationXProperty, value); }
    }
    //---------------------------------------------------------------------------------------------
    // コンストラクタ
    public MultiSlideEffect()
    {
      System.Reflection.Assembly a = typeof(MultiSlideEffect).Assembly;
      string assemblyName = a.GetName().Name;
      string uri = "pack://application:,,,/" + assemblyName + ";component/effects/MultiSlide.ps";
      var pixelShader = new PixelShader();
      try
      {
        pixelShader.UriSource = new Uri(uri, UriKind.RelativeOrAbsolute);
      }
      catch (Exception ex)
      {
        MessageBox.Show(ex.Message);
      }
      this.PixelShader = pixelShader;
      UpdateShaderValue(Input0Property);
      UpdateShaderValue(Input1Property);
      UpdateShaderValue(LocationXProperty);
    }
  } // end of MultiSlideEffect class
} // end of namespace
新旧の画像を透明度を調整することで徐々に入れ替えます。つまり、現在の画像はフェードアウト、新しい画像をフェードインします。下図の左はアプリケーション起動時の状態、中央はスライダコントロ−ルを少し移動したところで、新旧の画像の両方が見えます。右は新しい画像に入れ替わった状態です。なお、画像ファイルのサイズは同一である必要はありません。
(MultiOpacity.fx)
sampler2D Input0 : register(S0);
sampler2D Input1 : register(S1);
float Opacity    : register(C0);
float4 main(float2 uv : TEXCOORD) : COLOR
{
  float4 color0 = tex2D(Input0, uv);
  float4 color1 = tex2D(Input1, uv);
  return lerp(color0, color1, Opacity);
}
(MainWindow.xaml)
<Window x:Class="MultiInputTest.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MultiInputTest"
    Title="Multi Input Effect" Height="350" Width="525">
  <Window.Resources>
    <ImageBrush x:Key="imageBrush" ImageSource="images/FrangipaniFlowers.jpg" />
  </Window.Resources>
  <DockPanel>
    <StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom" Margin="20,10,20,10">
      <TextBlock Text="Opacity : " Margin="0,0,10,0" />
      <Slider Name="slider" Minimum="0.0" Maximum="1.0" SmallChange="0.1" Width="200" Value="0" />
      <TextBlock Text="{Binding ElementName=slider, Path=Value}" Margin="10,0,0,0" />
    </StackPanel>
    <Image Name="image" Source="images/azul.jpg" Margin="10">
      <Image.Effect>
        <local:MultiOpacityEffect Input1="{StaticResource imageBrush}"
                                  Opacity="{Binding ElementName=slider, Path=Value}" />
      </Image.Effect>
    </Image>
  </DockPanel>
</Window>
(MultiOpacity.cs)
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Effects;
namespace MultiInputTest
{
  public class MultiOpacityEffect : ShaderEffect
  {
    public static readonly DependencyProperty Input0Property = ShaderEffect.RegisterPixelShaderSamplerProperty(
        "Input0", typeof(MultiOpacityEffect), 0);
    public static readonly DependencyProperty Input1Property = ShaderEffect.RegisterPixelShaderSamplerProperty(
      "Input1", typeof(MultiOpacityEffect), 1);
    public static readonly DependencyProperty OpacityProperty = DependencyProperty.Register(
      "Opacity", typeof(double), typeof(MultiOpacityEffect), new UIPropertyMetadata(0.0, PixelShaderConstantCallback(0)));
    public Brush Input0
    {
      get { return (Brush)GetValue(Input0Property); }
      set { SetValue(Input0Property, value); }
    }
    public Brush Input1
    {
      get { return (Brush)GetValue(Input1Property); }
      set { SetValue(Input1Property, value); }
    }
    public double Opacity
    {
      get { return (double)GetValue(OpacityProperty); }
      set { SetValue(OpacityProperty, value); }
    }
    //---------------------------------------------------------------------------------------------
    // コンストラクタ
    public MultiOpacityEffect()
    {
      System.Reflection.Assembly a = typeof(MultiOpacityEffect).Assembly;
      string assemblyName = a.GetName().Name;
      string uri = "pack://application:,,,/" + assemblyName + ";component/effects/MultiOpacity.ps";
      var pixelShader = new PixelShader();
      try
      {
        pixelShader.UriSource = new Uri(uri, UriKind.RelativeOrAbsolute);
      }
      catch (Exception ex)
      {
        MessageBox.Show(ex.Message);
      }
      this.PixelShader = pixelShader;
      UpdateShaderValue(Input0Property);
      UpdateShaderValue(Input1Property);
      UpdateShaderValue(OpacityProperty);
    }
  } // end of MultiOpacityEffect class
} // end of namespace
ビットマップ効果のページで説明したとおり、エフェクトファイル(.fx ファイル)はコンパイルしなければなりません。エフェクトファイルコンパイラ(fxc.exe)は DirectX SDK に付属しています。まだ持っていない人は Microsoft のサイトからダウンロードしてください。無料です。
私が公開している HlslPad も PS ファイルを作成する機能があります。ただし、[コンパイル] ではなく、メニューの [PS ファイルとして出力する] を使ってください。
−以上−