ピクセルシェーダー④ 定数レジスター

次に Ripple.fx を使ってピクセルシェーダーの定数レジスターを WPF から使う方法を解説します。まず、PsPad で Ripple.fx をロードし、Save Shader ボタンで、Ripple.ps を保存します。Silverlight でも使えるようにシェーダーモデル 2.0 (PS2_0)でコンパイルするときは、Ripple.fx の終わりの数行で、以下のように lighting 変数の定義と仕様をコメントアウトします。ライティングの効果がなくなりますが、あまり目立ちません。

   float2 uv2 = center + distance * direction;
   //float lighting = saturate(wave.y * falloff) * 0.2 + 0.8;
   float4 color = tex2D( implicitInputSampler, uv2 );
   //color.rgb *= lighting;
   return color;
}

前回と同じように、VS2010で新規プロジェクトを作成し、画像(Stove.jpg)を表示して、その画像にRipple.psを適用します。加えて、Ripple.fx では3つの float (振幅、周波数、位相)と 1 つの float2(中心点) を使っているので、これらを使って、画面をクリックしたらその点を中心とした波紋が発生し数秒で消えるというアニメーション エフェクトにしましょう。

 

  1. PsPad で Ripple.fx をコンパイルしてバイトコード Ripple.ps を保存(Silverlightで使う時は上記の修正)
  2. Visual Studio 2010 でプロジェクトにRipple.psを追加([プロジェクト]→[既存項目の追加])
  3. Ripple.ps のプロパティのビルドアクションを resource に設定。
  4. 新しいクラス MyRippleEffect.cs を追加([プロジェクト]→[クラスの追加])
    1. 名前空間に System.Windows.Media.Effects を追加
    2. MyRippleEffect クラスを ShaderEffect の派生クラスにする
    3. コンストラクターを記述(Silverlight と WPF では Uri のパスの記述が違うので注意)
    4. シェーダーへのサンプラー入力レジスタ用の依存プロパティを記述(ここまでは前回とほぼ同じ)
    5. シェーダーへの定数レジスター用の依存プロパティを記述

using System.Windows.Media.Effects;
// Silverlight には UIPropertyMetadata がないので次の行のコメントを外す
//using UIPropertyMetaData = System.Windows.PropertyMetadata; 

 

namespace MyApplication
{
    class MyRippleEffect : ShaderEffect
    {
        // コンストラクター
        public MyRippleEffect()
        {
            PixelShader ps = new PixelShader();
// WPF 用
            Uri u = new Uri(
@"pack://application:,,,/MyApplication;component/Ripple.ps",
UriKind.RelativeOrAbsolute);
// Silverlight 用
            //Uri u = new Uri(@"/MyApplication;component/Ripple.ps",
// UriKind.RelativeOrAbsolute);
            ps.UriSource = u;
            this.PixelShader = ps;

 

 

            UpdateShaderValue(InputProperty);
            UpdateShaderValue(AmplitudeProperty); // 振幅
            UpdateShaderValue(FrequencyProperty); // 周波数
            UpdateShaderValue(PhaseProperty); // 位相
            UpdateShaderValue(CenterProperty); // 中心点
        }

 

 

        // サンプラー用依存プロパティ
        public Brush Input
        {
            get { return (Brush)GetValue(InputProperty); }
            set { SetValue(InputProperty, value); }
        }
        public static readonly DependencyProperty InputProperty =
            ShaderEffect.RegisterPixelShaderSamplerProperty("Input",
typeof(MyRippleEffect), 0);  

 

 

             // 定数レジスター用依存プロパティ
        public double Amplitude
        {
            get { return (double)GetValue(AmplitudeProperty); }
            set { SetValue(AmplitudeProperty, value); }
        }
        public static readonly DependencyProperty AmplitudeProperty =
            DependencyProperty.Register("Amplitude", typeof(double),
typeof(MyRippleEffect),
            new UIPropertyMetadata(1.0, PixelShaderConstantCallback(1))); 

 

        public double Frequency
        {
            get { return (double)GetValue(FrequencyProperty); }
            set { SetValue(FrequencyProperty, value); }
        }
        public static readonly DependencyProperty FrequencyProperty =
            DependencyProperty.Register("Frequency", typeof(double),
typeof(MyRippleEffect),
            new UIPropertyMetadata(1.0, PixelShaderConstantCallback(2))); 

 

 

        public double Phase
        {
            get { return (double)GetValue(PhaseProperty); }
            set { SetValue(PhaseProperty, value); }
        }
        public static readonly DependencyProperty PhaseProperty =
            DependencyProperty.Register("Phase", typeof(double),
typeof(MyRippleEffect),
            new UIPropertyMetadata(10.0, PixelShaderConstantCallback(3)));

        public Point Center
        {
            get { return (Point)GetValue(CenterProperty); }
            set { SetValue(CenterProperty, value); }
    }
        public static readonly DependencyProperty CenterProperty =
            DependencyProperty.Register("Center", typeof(Point),
typeof(MyRippleEffect),
            new UIPropertyMetadata(new Point(0.5, 0.5),
PixelShaderConstantCallback(5)));

 

 

 

    }

PixelShaderConstantCallbackメソッドの引数の数字(赤)がシェーダーの定数レジスター番号に対応します。

これでビルドすれば以下のようにXAMLでこのMyRippleShaderが適用でき、プロパティも設定できます。

<Image Name="MyImage" Source="Stove.jpg"
MouseLeftButtonDown="MyImage_MouseLeftButtonDown">
  <Image.Effect>
    <local:MyRippleEffect x:Name="myEffect" 
Amplitude="0.0" Frequency="0.8" Phase="1"  />
  </Image.Effect>
</Image>

しかし、Rippleの振幅(Amplitude)が 0 なので何もエフェクトがかかっていないように見えます。Amplitude を変えて様子を見てください。Visual Studio のデザイナーペインでも違いが確認できるはずです。

次に、アニメーションを作ります。以下のように2.5秒で Amplitude と Phase を変化させます。Storyboard は Window.Resources (WPFのとき)か UserControl.Resources (Silverlight のとき)に入れてください。

<Storyboard x:Key="Storyboard1">
  <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=
"(UIElement.Effect).(local:MyRippleEffect.Phase)"
Storyboard.TargetName="MyImage">
     <EasingDoubleKeyFrame KeyTime="0" Value="50"/>
     <EasingDoubleKeyFrame KeyTime="0:0:2.5" Value="1"/>
  </DoubleAnimationUsingKeyFrames>
  <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=
"(UIElement.Effect).(local:MyRippleEffect.Amplitude)"
Storyboard.TargetName="MyImage">
     <EasingDoubleKeyFrame KeyTime="0" Value="0.2"/>
     <EasingDoubleKeyFrame KeyTime="0:0:2.5" Value="0"/>
  </DoubleAnimationUsingKeyFrames>
</Storyboard>

 

 

 

最後に、マウスクリックのコールバックを実装します。クリックされた点から、最小を0最大を1に正規化した座標を計算し、エフェクトの Center プロパティに代入し、アニメーションを開始します。SilverlightとWPFでは、リソース内の key で定義した名前の取得方法が違うので注意してください。

private void MyImage_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
  Point p = e.GetPosition((FrameworkElement)sender);
  myEffect.Center = new Point(p.X/MyImage.ActualWidth,p.Y/MyImage.ActualHeight);
// WPF 用
  Storyboard s = FindResource("Storyboard1") as Storyboard;
// Silverlight 用
  //Storyboard s = this.Resources["Storyboard1"] as Storyboard;
  s.Begin();
}

image

ここで紹介した、Ripple.ps を適用した Silverlight 4 と WPF 4 のサンプルコードは PsPad.CodePlex.com からダウンロードできます。