はじめに
この記事は 城東.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);
}
無加工でそのまま出力。
波打ち
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));
}
いわゆるラスタースクロール (っぽいもの) 。
レトロ調
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);
}
- セピア調
- スキャンラインっぽい縞
- 周辺を若干暗くする
ランダムで横にゆらすとさらによい。
おはじき
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);
}
単純なモザイクではなく一つ一つを円にしています。
おわりに
WPF は (私の主観では) .NET 4.0 を最後に機能向上もせず 10 年経ってしまった、という印象です (今現在でも使えているのはユーザー側の尽力によるものが大きいかと) 。しかし、あの当時で考えると相当先進的な機能を実装しており、ある意味「早すぎた」ものだったような気もします。
現在、グラフィック面の理由から WPF を積極的に使おうという事はそれほどないように思いますが、 ShaderEffect はかなり応用性が高いと思いますので、機会があれば是非活用してみてください。