WPF で ShaderEffect

## はじめに

この記事は [城東.NET #29](https://jtnug.connpass.com/event/117059/) でお話した内容をもう少し詳しく書いたものです。

WPF で私がすごいと思う機能の一つである "ShaderEffect" を紹介したいと思います。

サンプルとして簡単な Shader Editor (ShaderPad と呼んだ方がそれっぽい) を作ってみました。

* [aosoft/WPFShaderEditor](https://github.com/aosoft/WPFShaderEditor)

## ShaderEffect とは

読んで字のごとく、 "GPU の Shader で実装する Effect" です。

### WPF の Effect

WPF では映像フィルターとして ["BitmapEffect"](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.media.effects.bitmapeffect) という機能を最初期から実装していました。

しかし BitmapEffect は処理が CPU 上で実行するコードで書かれており、パフォーマンスに大きな問題を抱えていました。従って使いどころも非常に限定的になり、アニメーションがかかっているところに使うなどもってのほかでした。

その後、 .NET 3.5 SP1 で ["Effect"](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.media.effects.effect) が新たに実装されます。

Effect は BitmapEffect と異なり GPU 実装となり処理が大幅に高速化され、普通に使えるようになりました。 Effect ではプリセットとして下記のものが用意されています。

* [DropShadoWEffect](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.media.effects.dropshadoweffect)
* [BlurEffect](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.media.effects.blureffect)

プリセットはこの 2 つしかありませんが、これに加えて [ShaderEffect](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.media.effects.shadereffect) が提供されました (これら 3 つは全て Effect を継承している) 。 ShaderEffect を使うことで自分の好きなような Effect を書くことができるようになりました。もちろん Shader なので GPU 実行となり非常に高速です。

### ShaderEffect の特徴

* Pixel Shader (SM 3.0 まで) で任意のフィルター処理が書ける
* (基本的には) Direct3D をさわらなくても使える (知識は多少必要)
* Effect を適用する Visual のレンダリング結果が Shader 上で texture として参照できる
* 任意のパラメーターを DependencyProperty として定義すると Shader 上の定数として参照できる

WPF に実装する、と考えればある意味当たり前ではあるのですが、 DependencyProperty で Shader のパラメーターと接続できるというのは非常に強力で Animation と Binding することでごく自然に Shader アニメーションが実現できます。

## ShaderEffect を使う

### ShaderEffect クラスの継承クラスを定義する

ShaderEffect は ShaderEffect を継承したカスタムクラスの実装が必要です (Shader の設定が protected なため) 。

### PixelShader の準備をする

ShaderEffect で使う Shader は "PixelShader" クラスで扱います (ややこしい) 。

PixelShader にセットする Shader コードはコンパイル済である必要があります (Direct3D の Shader はコンパイルされている必要があるため) 。このコンパイルだけは WPF だけではどうにもならないので、別途コンパイラーが必要になります。

* Windows SDK に含まれるコマンドラインコンパイラー (fxc.exe) を使う。
* [SharpDX](https://github.com/sharpdx/SharpDX) を利用する。

通常は頒布するアニメーションに決まった効果を適用すると思うので、 fxc による事前コンパイルになると思います。バイナリをリソースに保持し、実行時にロードするのがよいと思います。

今回のサンプルは Editor なので動的にコンパイルする必要があります。よって SharpDX を利用しました。 SharpDX は DirectX の .NET ラッパーでこの中に Shader コンパイラーのラッパーも含まれているので簡単にコンパイラーを利用できます。

```csharp
string hlsl = @"
sampler2D input : register(s0);
float4 main(float2 uv : TEXCOORD) : COLOR
{
    return tex2D(input, uv));
}";

var pixelShader = new System.Windows.Media.Effects.PixelShader();
var compileResult = SharpDX.D3DCompiler.ShaderBytecode.Compile(hlsl, "main", "ps_3_0");
using (var ms = new System.IO.MemoryStream(compileResult.Bytecode))
{
    pixelShader.SetStreamSource(ms);
}

//  ShaderEffect.PixelShader プロパティにセット
this.PixelShader = pixelShader;
```

SharpDX を使って Shader をコンパイルし、設定するところまで実装例です。
コンパイルしたらバイナリーを PixelShader クラスのインスタンスにセットし、それを ShaderEffect に設定します。

ちなみに下記のプロパティを設定すると ShaderBytecode.Compile で例外を起きないようにもできます (戻り値のチェックが必要) 。

```csharp
SharpDX.Configuration.ThrowOnShaderCompileError = false;
```

### DependencyProperty を定義する

Shader は定数レジスタを通して実行時パラメーターを定義できます。所定のルールに従って定義することにより DependencyProperty を Shader の定数と接続することができます。 

まず入力ソースの定義をします。これはお約束のようです。

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

[RegisterPixelShaderSamplerProperty](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.media.effects.shadereffect.registerpixelshadersamplerpropert) は Effect として入力されるパスを定義するもので Brush 型のインスタンスが割り当てられますが、それをコードから使うことはないと思います。ここでは [SamplingMode](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.media.effects.samplingmode) の指定もできますが明示的に NearestNeighbor を使いたい時以外は特に指定する必要もないと思います。

Shader から参照できる定数を定義したい場合は対応する DependencyProperty を定義します。

```csharp
public double Time
{
    get { return (double)GetValue(TimeProperty); }
    set { SetValue(TimeProperty, value); }
}

public static readonly DependencyProperty TimeProperty =
DependencyProperty.Register(
    "Time",
    typeof(double),
    typeof(CustomShaderEffect),
    new PropertyMetadata(0.0, PixelShaderConstantCallback(2)));
```

要点は PropertyMetadata に指定する [PixelShaderConstantCallback](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.media.effects.shadereffect.pixelshaderconstantcallback) で、ここに指定する引数が定数レジスターのインデックスになります。上記の場合ですと HLSL 上では次のように定義します。

```glsl
float time : register(c2);
```

Shader 上では float 型にしなくてはなりませんが、 WPF は double にしないと扱いにくいので DependencyProperty は double にしています。ここはうまくキャストしてくれているようです。

同様に Brush の DependencyProperty の PropertyMetadata に [PixelShaderSamplerCallback]( https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.media.effects.shadereffect.pixelshadersamplercallback) を適用すると他の映像を Shader の入力ソースに使えます。


### Effect の適用

Effect は UIElement の [Effect プロパティ](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.uielement.effect) に設定します。

```xml
<Grid>
  <Grid.Effect>
    <local:CustomShaderEffect x:Name="shader" />      
  </Grid.Effect>
  <Grid.Triggers>
    <EventTrigger RoutedEvent="Grid.Loaded">
      <BeginStoryboard>
        <Storyboard>
          <DoubleAnimation
            Storyboard.TargetName="shader" Storyboard.TargetProperty="Time"
            From="0" To="3600" RepeatBehavior="Forever" AutoReverse="false"
            Duration="1:0:0"
            />
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  </Grid.Triggers>
</Grid>
```

合わせて Storyboard によるアニメーションの記述もしてみました。実装した Time プロパティに秒単位の時間が更新されていきます。これにより **Shader 側で時間に同期したアニメーションを記述できます (!!) 。**

## 簡単な例

WPF ShaderEditor では Width, Height, Time というプロパティを定義しています。

* Width (0) = ActualWidth と Binding (ソース実サイズの幅)
* Height (1) = ActualHeight と Binding (ソース実サイズの高さ)
* Time(2) = 秒単位の時間 (1 時間でループ) を Animation で更新

```glsl
sampler2D input : register(s0);
float width : register(c0);
float height : register(c1);
float time : register(c2);
```

### 基本

```glsl
lsl
float4 main(float2 uv : TEXCOORD) : COLOR
{
    return tex2D(input, uv);
}
```

![190228_1.jpg](https://qiita-image-store.s3.amazonaws.com/0/211064/49373e52-966f-4077-44ef-578f0a5de7c0.jpeg)

無加工でそのまま出力。

### 波打ち

```glsl
float4 main(float2 uv : TEXCOORD) : COLOR
{
    return tex2D(input, float2(uv.x + sin((uv.y + time * 0.5) * 3.14159 * 10) * 0.1, uv.y));
}
```

![190228_2.jpg](https://qiita-image-store.s3.amazonaws.com/0/211064/0ef3a313-543c-5bc2-440a-cecded9c7c80.jpeg)


いわゆるラスタースクロール (っぽいもの) 。

### レトロ調

```glsl
float4 main(float2 uv : TEXCOORD) : COLOR
{
    float4 c = tex2D(input, float2(uv.x, uv.y));
    float y = (0.299 * c.x + 0.587 * c.y + 0.114 * c.z) * max(0.2, sin(uv.y * 3.14159 * height));

    float d = distance(uv, float2(0.5, 0.5));
    d = min(1, 1 - d * d * 2.5);

    return float4(float3(1.0, 0.7, 0.5) * y * d, 1.0);
}
```

![190228_4.jpg](https://qiita-image-store.s3.amazonaws.com/0/211064/7221ba0d-f9ea-c98f-41e6-e4ba16c0f8b4.jpeg)


* セピア調
* スキャンラインっぽい縞
* 周辺を若干暗くする

ランダムで横にゆらすとさらによい。

### おはじき

```glsl
float4 main(float2 uv : TEXCOORD) : COLOR
{
    float x = 30.0;
    float y = 30.0;
    float l = 0.0;
    if (width > height)
    {
        x = x * width / height;
        l = 0.5 / y;
    }
    else
    {
        y = y * height / width;
        l = 0.5 / x;
    }
    float2 uv2 = float2(floor(uv.x * x) / x, floor(uv.y * y) / y);
    float2 uv3 = float2(uv2.x + 1 / (x * 2), uv2.y + 1 / (y * 2));
    if (distance(float2(uv.x * width / height, uv.y), float2(uv3.x * width / height, uv3.y)) < l)
    {
        return tex2D(input, uv2);
    }
    return float4(0.0, 0.0, 0.0, 1.0);
}
```

![190228_5.jpg](https://qiita-image-store.s3.amazonaws.com/0/211064/57d594b3-925f-0f8d-2c62-01e3fdf1e899.jpeg)

単純なモザイクではなく一つ一つを円にしています。

## おわりに

WPF は (私の主観では) .NET 4.0 を最後に機能向上もせず 10 年経ってしまった、という印象です (今現在でも使えているのはユーザー側の尽力によるものが大きいかと) 。しかし、あの当時で考えると相当先進的な機能を実装しており、ある意味「早すぎた」ものだったような気もします。

現在、グラフィック面の理由から WPF を積極的に使おうという事はそれほどないように思いますが、 ShaderEffect はかなり応用性が高いと思いますので、機会があれば是非活用してみてください。