Edited at

WPF で ShaderEffect


はじめに

この記事は 城東.NET #29 でお話した内容をもう少し詳しく書いたものです。

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

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


ShaderEffect とは

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


WPF の Effect

WPF では映像フィルターとして "BitmapEffect" という機能を最初期から実装していました。

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

その後、 .NET 3.5 SP1 で "Effect" が新たに実装されます。

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

プリセットはこの 2 つしかありませんが、これに加えて 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 を利用する。

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

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

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 で例外を起きないようにもできます (戻り値のチェックが必要) 。

SharpDX.Configuration.ThrowOnShaderCompileError = false;


DependencyProperty を定義する

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

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

public static readonly DependencyProperty InputProperty =

ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(CustomShaderEffect), 0);

RegisterPixelShaderSamplerProperty は Effect として入力されるパスを定義するもので Brush 型のインスタンスが割り当てられますが、それをコードから使うことはないと思います。ここでは SamplingMode の指定もできますが明示的に NearestNeighbor を使いたい時以外は特に指定する必要もないと思います。

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

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 で、ここに指定する引数が定数レジスターのインデックスになります。上記の場合ですと HLSL 上では次のように定義します。

float time : register(c2);

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

同様に Brush の DependencyProperty の PropertyMetadata に PixelShaderSamplerCallback を適用すると他の映像を Shader の入力ソースに使えます。


Effect の適用

Effect は UIElement の Effect プロパティ に設定します。

<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 で更新

sampler2D input : register(s0);

float width : register(c0);
float height : register(c1);
float time : register(c2);


基本

lsl

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

190228_1.jpg

無加工でそのまま出力。


波打ち

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

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


レトロ調

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


  • セピア調

  • スキャンラインっぽい縞

  • 周辺を若干暗くする

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


おはじき

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

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


おわりに

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

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