WPF の ShaderEffect (Pixel Shader) に配列を渡したい

# はじめに
　画像の色情報を加工して表示したいときに、ピクセルシェーダは素晴らしい威力を発揮します。そして WPF はこの技術と相性が良いです。

　1ピクセルずつループ処理で色情報を編集するのもありですが、沢山のピクセルを相手にある程度同じような処理をする場合には、ピクセルシェーダの方がパフォーマンスが上がります。

　欠点は、使用するために手間が掛かることでしょうか…… C++ で DirectX のプログラムを書いた人は、逆に簡単に感じるかもしれません？

　大抵は画像の色情報の入力のみで事足りるのですが、外部から与えた配列に従って色情報を変更してもらいたい際の方法について書きます。

# 例
　WPF でピクセルシェーダを使用する [サンプルコード](https://github.com/Daiius/ColorEffectTest) を作っています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231644/f21f0388-a033-44be-d9d1-bc375a915d55.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231644/3fcdb9d9-4e66-c47c-beb3-2e5706e7b995.png)

　元は白黒の画像なのですが、外部から与えた配列に従って色情報を書き換えて表示しています。


# HLSLコード例
　入力画像（2次元テクスチャ）に加えて、渡したい配列を 1次元テクスチャ として入力可能な様にしておくのが楽そうです。この時、ピクセルシェーダ内では RGB の色データや画像内の UV 位置が 最小 0.0、最大 1.0 の float 値に置き換えられて扱われることに注意します。
　先ず、HLSLコード例を示します。

```HLSL:ArrayEffect.fx
sampler2D input : register(s0);
sampler1D array : register(s1);

float4 main(float2 uv : TEXCOORD) : COLOR
{
    // 入力画像の位置毎に色を取得する。
    float4 color = tex2D(input, uv);
    // 追加で渡した配列データは1次元の画像として扱われる。
    // 入力画像の U座標位置 (0.0-1.0) に対応した数値を
    // RGBA 値として取り出す。
    float4 tmp = tex1D(array, uv[0]);
    // 出力する色はほぼ元画像の色であるが...
    float4 result = color;
    // 赤色成分は配列で与えた値を設定する。
    result.r = tmp.r;
    return result;
}
```

# ShaderEffect クラスで WPF と ピクセルシェーダ の橋渡し
　WPFから上記 HLSL コードによるピクセルシェーダを呼び出す手順については、基本的なものから大きく変更する点はありません。こちらの [コード例 や 上記HLSLコードのコンパイル手順](http://www.kanazawa-net.ne.jp/~pmansato/wpf/wpf_graph_effect.htm) が参考になります。

<details><summary>クリックすると ShaderEffect を継承したクラスの全コードを表示</summary>
<div>

```C#:ArrayEffect.cs
public class ArrayEffect : ShaderEffect
{
    public static readonly DependencyProperty InputProperty 
        = RegisterPixelShaderSamplerProperty(
            "Input", typeof(ArrayEffect), 0
        );

    public static readonly DependencyProperty ArrayProperty
        = RegisterPixelShaderSamplerProperty(
            "Array", typeof(ArrayEffect), 1
        );
    public ArrayEffect()
    {
        var ps = new PixelShader();
        // 上で紹介した HLSL コードは、fxc コマンド
        // (Visual Studio Tools フォルダに含まれている、
        //  CUIからコンパイルを行う際等に使用する
        //  コマンドプロンプト等から使えます)
        // でコンパイルして ArrayEffect.ps ファイルとした上で、
        // プロジェクトに追加してビルドアクションを Resource にすると、
        // このコードで参照できるようになります。
        var asm = typeof(ColormapEffect).Assembly;
        var asmName = asm.GetName().Name;
        var uri = new Uri(
            "pack://application:,,,/" + asmName + ";component/Effects/ArrayEffect.ps",
            UriKind.RelativeOrAbsolute
            );
        ps.UriSource = uri;

        this.PixelShader = ps;
        UpdateShaderValue(InputProperty);
        UpdateShaderValue(ArrayProperty);
    }

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

    public Brush Array
    {
        get { return (Brush)GetValue(ArrayProperty); }
        set { SetValue(ArrayProperty, value); }
    }
}
```
</div>
</details>

# WPF 側で配列データを用意して、ShaderEffect 継承クラスに渡す
　最後に WPF 側で、渡したい配列を 1次元の画像データとして準備する必要が有ります。この部分は以下の様なコード例が最も直感的かと思います。

```C#:（WriteableBitmapを使った例）
using System.Windows; // For Int32Rect
using System.Windows.Media; // For PixelFormats, ImageBrush
using System.Windows.Media.Imaging; // For WriteableBitmap

    （...略...）

    // 幅 255, 高さ 1 の書き込み可能なビットマップを用意
    // 今回は 8 bitの白黒画像を与えてみる
    // ここでは 0-255 の byte値であるが、
    // ピクセルシェーダに渡されると
    // 0.0-1.0 の float値になることに注意する
    // 恐らく PixelFormats.Gray16 等とするとまた事情が変わる
    var wb = new WriteableBitmap(
        255, 1, 96.0, 96.0,
        PixelFormats.Gray8, null
        );
    var array = new byte[255];
    （→  array に好きな値を設定する）
    wb.WritePixels(
        new Int32Rect(0, 0, wb.PixelWidth, wb.PixelHeight),
        array, 
        wb.PixelWidth * wb.Format.BitsPerPixel / 8, 0
        );
    var arrayEffect = new ArrayEffect();
    // ピクセルシェーダに上記の設定で画像を渡す際には Brush 型にする
    arrayEffect.Array = new ImageBrush(wb);
    (→  arrayEffect を WPFコントロールの Image 等の
     Effect プロパティに設定すると、色に変化が付く)
```




　これで、細かいカスタマイズをしたエフェクトを WPF による GUI 中の画像に対して適応することが出来ます。

それでは皆様、良い ピクセルシェーダ + WPF ライフをお送り下さい。